diff --git a/LICENSE b/LICENSE new file mode 100644 index 0000000..29f81d8 --- /dev/null +++ b/LICENSE @@ -0,0 +1,201 @@ + 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. diff --git a/README.md b/README.md new file mode 100644 index 0000000..a3bd137 --- /dev/null +++ b/README.md @@ -0,0 +1,205 @@ +# 路漫漫其修远兮,吾将上下而求索 + +> +> +> 舒适的环境总让人感觉很满足,殊不知它却是毒药。 +> +> + +## c语言 + +1. [c语言入门](./c语言/c语言入门.md) + +## java + +### 文档 + +1. [CountDownLatch](./java/countdownlatch.md) +2. [future获取异步计算结果](./java/future获取异步计算结果.md) +3. [html元素解析工具jsoup](./java/html元素解析工具jsoup.md) +4. [httpclient](./java/httpclient.md) +5. [java persistence api](./java/java_persistence_api.md) +6. [java spi机制](./java/java_spi机制.md) +7. [javaagent](./java/javaagent.md) +8. [javassist使用指南](./java/javassist使用指南.md) +9. [java逃逸分析](./java/java逃逸分析.md) +10. [ReadWriteLock引发的假死](./java/ReadWriteLock引发的假死.md) +11. [一张图看懂java集合框架](./java/一张图看懂java集合框架.md) +12. [使用命令编译打包](./java/使用命令编译打包.md) +13. [动态代理](./java/动态代理.md) +14. [多次删除不掉文件](./java/多次删除不掉文件.md) +15. [多线程之间的管道流](./java/多线程之间的管道流.md) +16. [正则表达式分组](./java/正则表达式分组.md) +17. [正则表达式替换匹配的文本](./java/正则表达式替换匹配的文本.md) +18. [正则表达式规则大全](./java/正则表达式规则大全.md) +19. [正则表达式贪婪和懒惰匹配](./java/正则表达式贪婪和懒惰匹配.md) +20. [类加载器](./java/类加载器.md) +21. [维护多个数据库连接](./java/维护多个数据库连接.md) +22. [获取数据库元数据](./java/获取数据库元数据.md) +23. [获取程序工作路径](./java/获取程序工作路径.md) +24. [裁剪图片](./java/裁剪图片.md) +25. [计算身份证校验码](./java/计算身份证校验码.md) +26. [运行时编译](./java/运行时编译.md) +27. [远程debug](./java/远程debug.md) + +### 代码片 + +1. [Jackson转换JSON工具](./java/代码片/jackson转换json工具.md) +2. [java8日期之间的转换](./java/代码片/java8日期之间的转换.md) +3. [SVN基础操作](./java/代码片/SVN基础操作.md) +4. [zip文件操作](./java/代码片/zip文件操作.md) +6. [复制文件或文件夹](./java/代码片/复制文件或文件夹.md) +7. [手动分析调试json](./java/代码片/手动分析调试json.md) +8. [操作maven](./java/代码片/操作maven.md) +9. [数字转中文](./java/代码片/数字转中文.md) +10. [数字转英文](./java/代码片/数字转英文.md) +10. [文件操作工具类](./java/代码片/文件操作工具类.md) +11. [脚本调用](./java/代码片/脚本调用.md) +12. [获取文件创建时间](./java/代码片/获取文件创建时间.md) + +## java web + +1. [下载文件](./javaweb/下载文件.md) +2. [跨域访问](./javaweb/跨域访问.md) +3. [转发与重定向](./javaweb/转发与重定向.md) + +## spring boot + +入门 + +1. [1_入门](./springboot/入门/1_入门.md) +2. [2_配置](./springboot/入门/2_配置.md) +3. [3_日志](./springboot/入门/3_日志.md) +4. [4_web](./springboot/入门/4_web.md) +5. [5_docker](./springboot/入门/5_docker.md) +6. [6_数据访问](./springboot/入门/6_数据访问.md) +7. [7_启动配置原理](./springboot/入门/7_启动配置原理.md) +8. [8_自定义starter](./springboot/入门/8_自定义starter.md) + +**解决方案** + +1. [上传大文件报错](./springboot/上传大文件报错.md) +2. [修改JSON转换器](./springboot/修改JSON转换器.md) +3. [排除预定义的Configuration](./springboot/排除预定义的Configuration.md) +4. [自定义conditional](./springboot/自定义conditional.md) +5. [自定义filter](./springboot/自定义filter.md) +6. [运行时指定外部配置文件](./springboot/运行时指定外部配置文件.md) +7. [静态资源映射](./springboot/静态资源映射.md) + +## spring framework + +1. [aop](./springframework/aop.md) +2. [beanpostprocessor](./springframework/beanpostprocessor.md) +3. [jms](./springframework/jms.md) +4. [单元测试及事务](./springframework/单元测试及事务.md) +5. [运行时注册bean](./springframework/运行时注册bean.md) + +## 日志 + +### logback + +1. [logback](./日志/logback/logback.md) +2. [logback设置控制台颜色](./日志/logback/logback设置控制台颜色.md) + +## lua + +1. [搭建开发环境](./lua/搭建开发环境.md) + +## js + +1. [jquery属性操作](./js/jquery属性操作.md) +2. [jquery获取非form标签的value](./js/jquery获取非form标签的value.md) +3. [使用FormData上传文件](./js/使用FormData上传文件.md) +4. [定时器](./js/定时器.md) +5. [触发事件](./js/触发事件.md) + +## vuejs + +1. [vue入门](./vuejs/vue入门.md) + +## 数据库 + +1. [mysql存储过程](./数据库/mysql存储过程.md) +2. [mysql异常](./数据库/mysql异常.md) +3. [mysql清档](./数据库/mysql清档.md) +4. [sql基础](./数据库/sql基础.md) +5. [url添加时区](./数据库/url添加时区.md) + +## 计算机基础 + +1. [原码、反码、补码](./计算机基础/原码、反码、补码.md) +2. [有符号右移>>无符号右移>>>](./计算机基础/有符号右移与无符号右移.md) +3. [进制转换](./计算机基础/进制转换.md) + +## linux + +1. [centos7安装mysql5.6](./linux/centos7安装mysql5.6.md) +2. [centos扩大swap分区](./linux/centos扩大swap分区.md) +3. [centos搭建consul集群](./linux/centos搭建consul集群.md) +4. [centos搭建redis集群](./linux/centos搭建redis集群.md) +5. [centos搭建shadowsocks](./linux/centos搭建shadowsocks.md) +6. [centos设置网络代理](./linux/centos设置网络代理.md) +7. [goflyway访问外网](./linux/goflyway访问外网.md) +8. [nginx转发规则](./linux/nginx转发规则.md) +9. [screen命令](./linux/screen命令.md) +10. [source命令](./linux/source命令.md) +11. [查看占用端口的进程](./linux/查看占用端口的进程.md) +12. [虚拟机设置静态ip](./linux/虚拟机设置静态ip.md) + +## windows + +2. [安装ubuntu子系统](./windows/安装ubuntu子系统.md) +3. [查看占用端口的进程](./windows/查看占用端口的进程.md) + +## 开发工具 + +### git + +1. [使用文档](./开发工具/git/使用文档.md) +2. [修改文件名大小写](./开发工具/git/修改文件名大小写.md) +3. [只拉取最新版本的代码](./开发工具/git/只拉取最新版本的代码.md) +4. [清空提交记录](./开发工具/git/清空提交记录.md) + +### idea + +1. [单个文件超限制](./开发工具/idea/单个文件超限制.md) +2. [安装eclipse格式化插件](./开发工具/idea/安装eclipse格式化插件.md) +3. [快捷键](./开发工具/idea/快捷键.md) +4. [提升运行及编译速度](./开发工具/idea/提升运行及编译速度.md) +5. [正则替换文本](./开发工具/idea/正则替换文本.md) +6. [社区版安装tomcat](./开发工具/idea/社区版安装tomcat.md) +7. [编译项目时内存溢出](./开发工具/idea/编译项目时内存溢出.md) +8. [自动导入内部类](./开发工具/idea/自动导入内部类.md) + +### maven + +1. [idea中包offline_mode错误](./开发工具/maven/idea中包offline_mode错误.md) +2. [idea控制台中文乱码](./开发工具/maven/idea控制台中文乱码.md) +3. [jar打包插件](./开发工具/maven/jar打包插件.md) +4. [maven](./开发工具/maven/maven.md) +5. [maven配置文件](./开发工具/maven/maven配置文件.md) +6. [下载jar包](./开发工具/maven/下载jar包.md) +7. [依赖war包项目](./开发工具/maven/依赖war包项目.md) +8. [原生插件一览表](./开发工具/maven/原生插件一览表.md) +9. [安装本地jar包到仓库](./开发工具/maven/安装本地jar包到仓库.md) +10. [打包文件名添加时间戳](./开发工具/maven/打包文件名添加时间戳.md) +11. [打包时跳过测试](./开发工具/maven/打包时跳过测试.md) +12. [无法识别java下的配置文件](./开发工具/maven/无法识别java下的配置文件.md) +13. [统一管理版本号](./开发工具/maven/统一管理版本号.md) +14. [编码GBK的不可映射字符](./开发工具/maven/编码GBK的不可映射字符.md) +15. [编译时无法识别com.sun包](./开发工具/maven/编译时无法识别com.sun包.md) +16. [配置阿里私服镜像](./开发工具/maven/配置阿里私服镜像.md) + +### svn + +1. [clean提示先前的操作未结束](./开发工具/svn/clean提示先前的操作未结束.md) + +## 服务器 + +1. [idea控制台中文乱码](./服务器/tomcat/idea控制台中文乱码.md) + +## chrome + +1. [switchyomega](./chrome/switchyomega.md) +2. [打包扩展程序](./chrome/打包扩展程序.md) + diff --git a/_config.yml b/_config.yml new file mode 100644 index 0000000..c419263 --- /dev/null +++ b/_config.yml @@ -0,0 +1 @@ +theme: jekyll-theme-cayman \ No newline at end of file diff --git a/chrome/images/001/001.png b/chrome/images/001/001.png new file mode 100644 index 0000000..3bed2ad Binary files /dev/null and b/chrome/images/001/001.png differ diff --git a/chrome/images/001/002.png b/chrome/images/001/002.png new file mode 100644 index 0000000..30d1ab2 Binary files /dev/null and b/chrome/images/001/002.png differ diff --git a/chrome/images/002/001.png b/chrome/images/002/001.png new file mode 100644 index 0000000..9c0c9d4 Binary files /dev/null and b/chrome/images/002/001.png differ diff --git a/chrome/images/002/002.png b/chrome/images/002/002.png new file mode 100644 index 0000000..09e04b0 Binary files /dev/null and b/chrome/images/002/002.png differ diff --git a/chrome/images/002/003.png b/chrome/images/002/003.png new file mode 100644 index 0000000..9d5b8c5 Binary files /dev/null and b/chrome/images/002/003.png differ diff --git a/chrome/switchyomega.md b/chrome/switchyomega.md new file mode 100644 index 0000000..1cce63d --- /dev/null +++ b/chrome/switchyomega.md @@ -0,0 +1,24 @@ +# SwitchyOmega + +**SwitchyOmega**是一个代理设置工具,用于管理不同域名使用不同代理服务器 + +## 新增代理配置 + +![添加配置](./images/002/002.png) + +点击左侧的**新建情景模式**,输入名称后点击**创建**按钮。 + +点击左侧刚刚创建的情景名称。配置如下信息,并点击**应用选项**: + +![情景配置](./images/002/003.png) + +## 配置GFW列表 + +![自动代理页面](./images/002/001.png) + +在**SwitchyOmega**管理页面选择**auto switch**,下拉到**规则列表设置**,选择**AutoProxy**,并将`https://raw.githubusercontent.com/gfwlist/gfwlist/master/gfwlist.txt`填入**规则列表网址**,然后选择**立即更新情景场景**。 + +将上方匹配规则列表设置为刚刚新建的**情景模式** + +点击左侧**应用选项**。 + diff --git "a/chrome/\346\211\223\345\214\205\346\211\251\345\261\225\347\250\213\345\272\217.md" "b/chrome/\346\211\223\345\214\205\346\211\251\345\261\225\347\250\213\345\272\217.md" new file mode 100644 index 0000000..5732bb3 --- /dev/null +++ "b/chrome/\346\211\223\345\214\205\346\211\251\345\261\225\347\250\213\345\272\217.md" @@ -0,0 +1,34 @@ +## 1. 打开扩展文件安装目录 + +资源管理器中打开下面的目录: + +``` +C:\Users\用户名\AppData\Local\Google\Chrome\User Data\Default\Extensions +``` + +该目录下存放着所有的插件,文件夹的名称就是插件的ID。 + +## 2. 打开chrome扩展程序管理页面 + +选择chrome工具栏最右侧的 三个点(.),选择更多工具 > 扩展程序。 + +启用扩展程序左侧的 **开发者模式** + +![扩展程序管理页面](./images/001/001.png) + +该页面中显示了扩展程序的ID以及版本号。 + +## 3.打包 + +点击扩展程序管理页面的 **打包扩展程序** + +![打包扩展程序](./images/001/002.png) + +**扩展程序根目录**选择刚才资源管理器中打开路径的 + ID + 版本号,如: + +``` +C:\Users\用户\AppData\Local\Google\Chrome\User Data\Default\Extensions\cfhdojbkjhnklbpkdaibdccddilifddb\3.5.2_0 +``` + +最后点击打包扩展程序,chrome会提示插件以及秘钥打包的位置。 + diff --git "a/c\350\257\255\350\250\200/c\350\257\255\350\250\200\345\205\245\351\227\250.md" "b/c\350\257\255\350\250\200/c\350\257\255\350\250\200\345\205\245\351\227\250.md" new file mode 100644 index 0000000..62f9555 --- /dev/null +++ "b/c\350\257\255\350\250\200/c\350\257\255\350\250\200\345\205\245\351\227\250.md" @@ -0,0 +1,113 @@ +### 第一个C语言程序 + +```c +# include // 引入头文件 +int main(void) { // 函数 + int number = 10; // 变量声明及赋值 + printf("当前的number是%d\n", number); // 输出语句 + return 0; // 函数返回 +} +``` + +### 基本数据类型 + +C语言的基本数据类型为:整型、字符型、实数型。这些类型按其在计算机中的存储方式可被分为两个系列,即整数(integer)类型和浮点数(floating-point)类型。 +这三种类型之下分别是:short、int、long、char、float、double 这六个关键字再加上两个符号说明符signed和unsigned就基本表示了C语言的最常用的数据类型。 +下面列出了在32位操作系统下 常见编译器下的数据类型大小及表示的数据范围: + +| 类型名称 | 占字节数 | 其他叫法 | 表示的数据范围 | +| -------------- | -------- | ------------------ | ------------------------------ | +| char | 1 | signed char | -128 ~ 127 | +| unsigned char | 1 | none | 0 ~ 255 | +| int | 4 | signed int | -2,147,483,648 ~ 2,147,483,647 | +| unsigned int | 4 | unsigned | 0 ~ 4,294,967,295 | +| short | 2 | short int | -32,768 ~ 32,767 | +| unsigned short | 2 | unsigned short int | 0 ~ 65,535 | +| long | 4 | long int | -2,147,483,648 ~ 2,147,483,647 | +| unsigned long | 4 | unsigned long | 0 ~ 4,294,967,295 | +| float | 4 | none | 3.4E +/- 38 (7 digits) | +| double | 8 | none | 1.7E +/- 308 (15 digits) | +| long double | 10 | none | 1.2E +/- 4932 (19 digits) | + + + +### 关键字 + +| C语言中的32个关键字 | | | | +| ------------------- | ------ | -------- | -------- | +| auto | double | int | struct | +| break | else | long | switch | +| case | enum | register | typedef | +| char | extern | return | union | +| const | float | short | unsigned | +| continue | for | signed | void | +| default | goto | sizeof | volatile | +| do | if | static | while | + + + +### 基本输入输出函数 + +#### 字符输出函数putchar + +putchar函数是字符输出函数,其功能是在终端(显示器)输出单个字符。其一般调用形式为: +putchar(字符变量); +例: + +```c +`putchar``(‘A’); ``/*输出大写字母A */``putchar``(x); ``/*输出字符变量x的值*/``putchar``(‘\n’); ``/*换行*/` +``` + +#### 字符输入函数getchar + +getchar函数的功能是接收用户从键盘上输入的一个字符。其一般调用形式为: +getchar(); +getchar会以返回值的形式返回接收到的字符.通常的用法如下: + +```c +`char` `c; ``/*定义字符变量c*/``c=``getchar``(); ``/*将读取的字符赋值给字符变量c*/` +``` + +#### 格式化输出函数printf + +printf函数叫做格式输出函数,其功能是按照用户指定的格式,把指定的数据输出到屏幕上 + +printf函数的格式为: + +printf(“格式控制字符串”,输出表项); + +常用的输出格式及含义如下: + +| 格式字符 | | +| -------- | -------------------------------------------------- | +| d , i | 以十进制形式输出有符号整数(正数不输出符号) | +| O | 以八进制形式输出无符号整数(不输出前缀0) | +| x | 以十六进制形式输出无符号整数(不输出前缀0x) | +| U | 以十进制形式输出无符号整数 | +| f | 以小数形式输出单、双精度类型实数 | +| e | 以指数形式输出单、双精度实数 | +| g | 以%f或%e中较短输出宽度的一种格式输出单、双精度实数 | +| C | 输出单个字符 | +| S | 输出字符串 | + +#### 格式化输入函数scanf + +scanf函数称为格式输入函数,即按照格式字符串的格式,从键盘上把数据输入到指定的变量之中。Scanf函数的调用的一般形式为: +scanf(“格式控制字符串”,输入项地址列表); +其中,格式控制字符串的作用与printf函数相同,但不能显示非格式字符串,也就是不能显示提示字符串。地址表项中的地址给出各变量的地址,地址是由地址运算符”&”后跟变量名组成的。 +Scanf 函数中格式字符串的构成与printf函数基本相同,但使用时有几点不同. +(1) 格式说明符中,可以指定数据的宽度,但不能指定数据的精度。例: + +```c +`float` `a;``scanf``(“%10f”,&a); ``//正确``scanf``(“%10.2f”,&a); ``//错误` +``` + +(2) 输入long类型数据时必须使用%ld,输入double数据必须使用%lf或%le。 +(3) 附加格式说明符”*”使对应的输入数据不赋给相应的变量。 + +*修饰符在scanf中()的用法: +*在scanf()中提供截然不同的服务,当把它放在%和说明符字母之间时,它使函数跳过相应的输入项目。 + +关于scanf()的返回值 +scanf() 函数返回成功读入的项目的个数。如果它没有读取任何项目(比如它期望接收一个数字而您却输入的一个非数字字符时就会发生这种情况),scanf()返回0。 +当它检测到“文件末尾”(end of file)时,它返回EOF(EOF在是文件stdio.h中的定义好的一个特殊值,一般,#define指令将EOF的值定义为-1)。 \ No newline at end of file diff --git "a/java/ReadWriteLock\345\274\225\345\217\221\347\232\204\345\201\207\346\255\273.md" "b/java/ReadWriteLock\345\274\225\345\217\221\347\232\204\345\201\207\346\255\273.md" new file mode 100644 index 0000000..3356091 --- /dev/null +++ "b/java/ReadWriteLock\345\274\225\345\217\221\347\232\204\345\201\207\346\255\273.md" @@ -0,0 +1,42 @@ +## 一个有趣的Java多线程"bug": ReadWriteLock引发的"假死" + +### 摘要 + +在一个Java多线程服务里, 发现有reader和writer线程竞争同一个ReentrantReadWriteLock却双双被卡住. 乍一看, 大概率是因为该服务的多线程实现有bug而导致了死锁. 然而经过排查源码后却没有发现该服务的实现上的问题. 最后发现事故的真相是一场"假死". 而其罪魁祸首是JDK的ReentrantReadWriteLock默认实现里的一个"坑". + +### 问题背景及描述 + +简化一下我们遇到问题: 假设有一个分布式文件系统, 其master节点提供文件系统的元数据(metadata)服务来管理该文件系统中所有的目录和文件. 该文件系统的元数据服务有一个线程池通过RPC来响应不同用户的请求, 并通过为每个目录/文件维护一个ReentrantReadWriteLock(使用默认方式创建)来协同线程保证consistency. + +此时我们有三个不同的用户各自通过RPC对master节点发起请求: + +1. 客户端A请求对目录"/foo/"下的所有文件以及子目录递归检查和校验. 这个请求会请求"/foo/"对应的读锁, 且当"/foo/"目录下文件过多时耗时会很长; +2. 客户端B请求删除文件"/foo/bar". 这个请求会竞争目录"/foo/"的写锁(因为"bar"是"/foo/" inode里的一个child); +3. 客户端C请求列出目录"/foo/"底下的文件. 该请求会竞争"/foo/"的读锁. + +问题表现为客户端A运行的时候, B和C均发生了"卡死"而无法继续. 因为A仅仅是要求拿到"/foo/"的读锁, B由于竞争写锁需要等待A完成还可以理解, 但是C在这里也是要求读锁并不能继续, 令人非常疑惑. + +### ReentrantReadWriteLock: 皮一下很开心 + +最后发现B和C对应的服务端读写线程均发生"卡死"其实是ReentrantReadWriteLock在默认的unfair policy下的一个合法行为. 这里我们先解释一下, 什么是ReentrantReadWriteLock的fair以及unfair policy: + +- fair policy: 在这种模式下, ReentrantReadWriteLock会尽量按照锁请求的时间顺序来决定锁竞争结果. 这样的用意是在锁竞争激烈的时候, 保证写锁的竞争线程总有机会拿到锁而不是永远被读锁的竞争线程排斥. +- unfair policy: 在这种模式下, 由一系列的启发函数来决定锁竞争的结果, 而并不是依赖锁请求的顺序. 请注意, 这是ReentrantReadWriteLock默认的模式. + +在我们的情况下, ReentrantReadWriteLock已经是使用unfair policy创建了, 但却依然发生请求读锁的客户端C卡死, 这又是为什么呢? + +原来在unfair policy的启发函数中有一条规定, 当lock已经被读锁占用, 且锁竞争队列里排第一的是写锁请求的时候, 其他的读锁请求并不能以为是读锁就"插队"抢锁. 这个用意和fair policy一样, 为了防止写锁请求一直不能被轮巡到. 换言之, 所谓的unfair也并非完全效率第一, 而是一定程度上还是在兼顾fairness (参见 [ReadWriteLock: writeLock request blocks future readLock despite policy unfair](https://link.zhihu.com/?target=https%3A//bugs.openjdk.java.net/browse/JDK-6893626)) + +回到我们的场景: 客户端A成功的使用了读锁霸占住了"/foo/"对应的ReentrantReadWriteLock后并"超长待机"一直霸占, 请求写锁的客户端B排到了队列的头一名, 在unfair policy的作用下, 客户端C无法插队也一直被B的写锁请求抑制. 最后造成的假象就是, 没有线程可以继续工作, 形同"服务器卡死". 但实际上当A在工作了10分钟完成之后, B和C也分别顺利结束. + +### 总结 + +通过对这个问题的排查, 确定了服务的多线程实现并没有问题, 出现的问题是对fairness了解不足. 如果要解决这个问题, 可以考虑以下几种方法: + +1. 绕过"lock()"里的启发函数, 转而使用使用"tryLock()"和"while"循环来手动实现一个正真unfair的ReentrantReadWriteLock. +2. 通过业务逻辑层面的修改防止master的文件系统服务端为了响应一个请求而占用锁时间过长, 哪怕这是读锁. +3. 转用非JDK实现或者自己实现一个ReentrantReadWriteLock, 可以自定义各种启发函数来平衡线程间的公平和效率. + +### 引用 + +[一个有趣的Java多线程"bug": ReadWriteLock引发的"假死"](https://zhuanlan.zhihu.com/p/34672421) \ No newline at end of file diff --git a/java/countdownlatch.md b/java/countdownlatch.md new file mode 100644 index 0000000..4d61c3d --- /dev/null +++ b/java/countdownlatch.md @@ -0,0 +1,97 @@ +## CountDownLatch + +### 解释 + +`CountDownLatch` 是 `java.util.concurrent`包中的一个工具类,用于线程计数。 + +它提供了一种同步的功能,允许一个或多个线程等待其他一系列在其他线程的操作完成后,继续执行当前线程。 + +并且,该对象是不可重用的,对象内存储的状态值一旦被减少为“0”,就无法再次重新设置。 + + + +### 入门demo + +```java + public static void main(String[] args) throws InterruptedException { + int num = 3; // 创建的线程数量 + CountDownLatch countDownLatch = new CountDownLatch(num); + for (int i = 0; i < num; i++) { + new Thread(new Worker(countDownLatch)).start(); + } + countDownLatch.await(); // 等待所有线程执行完毕,再向下执行 + System.out.println("主线程执行完毕"); + } + + static class Worker implements Runnable { + private CountDownLatch countDownLatch; + public Worker(CountDownLatch countDownLatch) { + this.countDownLatch = countDownLatch; + } + @Override + public void run() { + System.out.println(Thread.currentThread().getName() + "正在执行..."); + countDownLatch.countDown(); // 通知count执行完毕 + } + } +``` + +上面的程序定义了一个`num`变量用于指定`CountDownLatch`的`await()`方法等待的线程数量。 + +初始化一个`CountDownLatch`对象,并将该对象传入子线程中。子线程中通过调用`CountDownLatch`的`countDown()`方法来通知`CountDownLatch`当前线程执行完毕,由`CountDownLatch`对象进行计数。 + +主线程通过调用`CountDownLatch`的`await()`方法等待线程计数等于指定的线程数量。当两者相等时,唤醒主线程继续执行。 + +程序输出如下: + +``` +Thread-1正在执行... +Thread-2正在执行... +Thread-0正在执行... +主线程执行完毕 +``` + + + +### 源码解析 + +#### 原理 + +`CountDownLatch`通过使用线程锁的方式,对线程进行计数。 + +#### 内部锁 + +```java +private static final class Sync extends AbstractQueuedSynchronizer {} +``` + +上述对象是`CountDownLatch`内部的锁实现类。 + +该类中通过设置父类的`state`状态值变量,并重写`tryAcquireShared`和`tryReleaseShared`对此状态进行修改。`tryAcquireShared`方法通过检查状态值是否为`0`尝试获取共享锁,`tryReleaseShared`释放锁的同时修改状态值,释放成功后对状态值进行`-1`操作。 + +#### 构造器 + +```java +public CountDownLatch(int count) {} +``` + +`CountDownLatch`仅提供了上面一个构造器,用于指定监听计数的线程数量。 + +#### await() + +```java +public void await() throws InterruptedException {} +public boolean await(long timeout, TimeUnit unit) + throws InterruptedException {} +``` + +该方法用于获取共享锁,内部通过调用`Sync`对象的`tryAcquireShared`获取锁,但是该方法会检测锁的状态值,只有状态值为`0`,也就是所有的监听线程都执行完毕后,锁才能正常获取到,否则,调用该方法的线程进行阻塞。 + +#### countDown() + +```java +public void countDown() {} +``` + +`countDown()`方法通过释放锁的操作来修改状态值,每调用一次,状态值就减少1,知道状态值减少为`0`时,通知调用`await()`方法的线程再次进行锁获取操作。 + diff --git "a/java/future\350\216\267\345\217\226\345\274\202\346\255\245\350\256\241\347\256\227\347\273\223\346\236\234.md" "b/java/future\350\216\267\345\217\226\345\274\202\346\255\245\350\256\241\347\256\227\347\273\223\346\236\234.md" new file mode 100644 index 0000000..4097198 --- /dev/null +++ "b/java/future\350\216\267\345\217\226\345\274\202\346\255\245\350\256\241\347\256\227\347\273\223\346\236\234.md" @@ -0,0 +1,239 @@ +## Future + +所谓异步调用其实就是实现一个可无需等待被调用函数的返回值而让操作继续运行的方法。在 Java 语言中,简单的讲就是另启一个线程来完成调用中的部分计算,使调用继续运行或返回,而不需要等待计算结果。但调用者仍需要取线程的计算结果。 + +JDK5新增了Future接口,用于描述一个异步计算的结果。 + +### Callable、Runnable + +`Callable`和`Runnable`都是线程执行接口,两者的区别是:是否可以获取返回值。 + +`Runnable`接口的定义为: + +```java +public interface Runnable { + public abstract void run(); +} +``` + +`Callable`接口的定义为: + +```java +public interface Callable { + V call() throws Exception; +} +``` + +可以看出Callable存在一个泛型V,并且call()方法返回的就是该泛型类型。 + +### Future + +`Future`对当前执行的线程进行了封装,可以检测当前线程的状态,以及打断线程,获取线程结果等。 + +`Future`接口的定位为: + +```java +public interface Future { + boolean cancel(boolean mayInterruptIfRunning); + boolean isCancelled(); + boolean isDone(); + V get() throws InterruptedException, ExecutionException; + V get(long timeout, TimeUnit unit) + throws InterruptedException, ExecutionException, TimeoutException; +} +``` + +调用`get()`方法时,当前线程会被阻塞,知道该`Future`对象的线程执行完毕或被打断时才被唤醒继续执行。 + +### FutureTask + +`FutureTask`是`Future`接口的实现类,同时`FutureTask`实现了`Future`和`Runnable`接口,也就说明`FutureTask`同时拥有了这两个接口的特性,即可以作为线程方法执行, 也可以获取返回结果。 + +`FutureTask`定义: + +```java +public interface RunnableFuture extends Runnable, Future { + void run(); +} +public class FutureTask implements RunnableFuture{} +``` + +`FutureTask`需要接受`Runnable`或`Callable`接口的实现类来操作任务。它提供了一下两种构造器: + +```java +public FutureTask(Callable callable) { +} +public FutureTask(Runnable runnable, V result) { +} +``` + +### 使用方式 + +`Runnable`、`Callable`必须和`ExecuteService`配合使用才能获取到返回结果,而`FutureTask`既可以和`ExecuteService`配合使用,也可以和`Thread`类配合使用,但有一点需要注意,**`FutureTask`对象是无法被重用的**。 + +### 案例 + +**MyCallable.java** + +```java +public class MyCallable implements Callable { + + @Override + public String call() throws Exception { + System.out.println(Thread.currentThread().getName() + "--执行"); + Thread.sleep(3000); + return Thread.currentThread().getName() + "--get string text"; + } +} +``` + +**CallableMain.java** + +```java +public class CallableMain { + + public static void main(String[] args) throws ExecutionException, InterruptedException { + // 创建线程池 + ExecutorService executorService = Executors.newCachedThreadPool(); + + // 创建callable对象 + MyCallable task = new MyCallable(); + + System.out.println("使用callable + future"); + Future future = executorService.submit(task); + System.out.println(future.get()); + + System.out.println(); + System.out.println("使用callable + futureTask + ThreadPool 执行开始"); + FutureTask futureTask = new FutureTask<>(task); + executorService.submit(futureTask); + System.out.println(futureTask.get()); + System.out.println("使用callable + futureTask + ThreadPool 执行完成"); + + System.out.println(); + System.out.println("使用callable + futureTask + Thread"); + // 构建一个新的 FutureTask + FutureTask futureTask1 = new FutureTask<>(task); + Thread thread = new Thread(futureTask1); + thread.setName("new Thread"); + thread.start(); + System.out.println(futureTask1.get()); + + // 销毁线程池 + executorService.shutdown(); + } + +} +``` + +**结果输出** + +``` +使用callable + future +pool-1-thread-1--执行 +pool-1-thread-1--get string text + +使用callable + futureTask + ThreadPool 执行开始 +pool-1-thread-1--执行 +pool-1-thread-1--get string text +使用callable + futureTask + ThreadPool 执行完成 + +使用callable + futureTask + Thread +new Thread--执行 +new Thread--get string text +``` + + + +## CompletableFuture + +虽然 Future 以及相关使用方法提供了异步执行任务的能力,但是对于结果的获取却是很不方便,只能通过阻塞或者轮询的方式得到任务的结果。阻塞的方式显然和我们的异步编程的初衷相违背,轮询的方式又会耗费无谓的 CPU 资源,而且也不能及时地得到计算结果。 + +在Java8中,CompletableFuture提供了非常强大的Future的扩展功能,可以帮助我们简化异步编程的复杂性,并且提供了函数式编程的能力,可以通过回调的方式处理计算结果,也提供了转换和组合 CompletableFuture 的方法。 + +它可能代表一个明确完成的Future,也有可能代表一个完成阶段( CompletionStage ),它支持在计算完成以后触发一些函数或执行某些动作。 + +### Future 接口的局限性 + +Future接口可以构建异步应用,但依然有其局限性。它很难直接表述多个Future 结果之间的依赖性。实际开发中,我们经常需要达成以下目的: + +1. 将多个异步计算的结果合并成一个 +2. 等待Future集合中的所有任务都完成 +3. Future完成事件(即,任务完成以后触发执行动作) +4. 。。。 + +### CompletionStage + +- CompletionStage代表异步计算过程中的某一个阶段,一个阶段完成以后可能会触发另外一个阶段 +- 一个阶段的计算执行可以是一个Function,Consumer或者Runnable。比如:stage.thenApply(x -> square(x)).thenAccept(x -> System.out.print(x)).thenRun(() -> System.out.println()) +- 一个阶段的执行可能是被单个阶段的完成触发,也可能是由多个阶段一起触发 + +### 函数 + +`Async`后缀存在的区别在于,如果调用`Async`结尾的方法,则会将当前函数放入线程池内执行,否则使用当前任务的线程继续执行。 + +函数共分为如下几类: + +1. `CompletableFuture`对象创建函数 +2. 数据处理函数,无返回,仅对传入的数据进行处理(值拷贝,非指针传递,修改该值并不会影响下面的操作) +3. 数据处理函数,有返回,且返回类型不限制 +4. 多个`CompletionStage`交叉函数 +5. `Future`固定的方法 + +| 函数名 | 解释 | +| --------------------------------------------- | ------------------------------------------------------------ | +| static CompletableFuture\ runAsync | 无返回创建CompletableFuture对象,可接收线程池 | +| static \ CompletableFuture\ supplyAsync | 有返回创建CompletableFuture对象,可接收线程池 | +| whenComplete | 当CompletableFuture的计算结果完成,执行特定的Action。 | +| exceptionally | 当CompletableFuture抛出异常的时,执行特定的Action。 | +| thenApply | 当一个线程依赖另一个线程时,可以使用 thenApply 方法来把这两个线程串行化。 | +| handle | handle 是执行任务完成时对结果的处理。handle 方法和 thenApply 方法处理方式基本一样。不同的是 handle 是在任务完成后再执行,还可以处理异常的任务。thenApply 只可以执行正常的任务,任务出现异常则不执行 thenApply 方法。 | +| thenAccept | 接收任务的处理结果,并消费处理,无返回结果。 | +| thenRun | 跟 thenAccept 方法不一样的是,不关心任务的处理结果。只要上面的任务执行完成,就开始执行 thenRun。 | +| thenCombine | thenCombine 会把 两个 CompletionStage 的任务都执行完成后,把两个任务的结果一块交给 thenCombine 来处理。 | +| thenAcceptBoth | 当两个CompletionStage都执行完成后,把结果一块交给thenAcceptBoth来进行消耗 | +| applyToEither | 两个CompletionStage,使用最先执行完成的CompletionStage的结果进行下一步的转化操作。 | +| acceptEither | 两个CompletionStage,使用最先执行完成的CompletionStage的结果进行下一步的消费操作。 | +| runAfterEither | 两个CompletionStage,任何一个完成了都会执行下一步的操作(Runnable) | +| runAfterBoth | 两个CompletionStage,都完成了计算才会执行下一步的操作(Runnable) | +| thenCompose | thenCompose 方法允许你对两个 CompletionStage 进行流水线操作,第一个操作完成时,将其结果作为参数传递给第二个操作。 | + +### 案例 + +```java +public class CompletableFutureMain { + + public static void main(String[] args) throws ExecutionException, InterruptedException { + CompletableFuture future1 = CompletableFuture + // 创建对象 + .supplyAsync(() -> new Random().nextInt(10)) + // 接受上一步的参数并处理 + .thenApply(integer -> integer + ":2") + // 接受上一步的参数并处理,同时可以处理异常 + .handle((str, ex) -> Integer.parseInt(str.split(":")[1])); + + CompletableFuture future2 = CompletableFuture + .supplyAsync(() -> new Random().nextInt(20)) + // 上一步处理完成对参数进行处理 + .whenComplete((integer, throwable) -> { + // 这里的处理并不会影响下面的处理 + if (throwable == null) { + integer = 0; + } + }) + // 上一步执行完成执行下一步 + .thenApply(integer -> integer += 50) + // 合并两个future + .thenCombine(future1, (i1, i2) -> { + System.out.println(i1); + System.out.println(i2); + return i1 + i2; + }); + + System.out.println("程序最终结果:" + future2.get()); + + } + +} +``` + diff --git "a/java/html\345\205\203\347\264\240\350\247\243\346\236\220\345\267\245\345\205\267jsoup.md" "b/java/html\345\205\203\347\264\240\350\247\243\346\236\220\345\267\245\345\205\267jsoup.md" new file mode 100644 index 0000000..5a80ffb --- /dev/null +++ "b/java/html\345\205\203\347\264\240\350\247\243\346\236\220\345\267\245\345\205\267jsoup.md" @@ -0,0 +1,501 @@ +# Jsoup 简介 + + jsoup 是一款Java 的HTML解析器,可直接解析某个URL地址、HTML文本内容。它提供了一套非常省力的API,可通过DOM,CSS以及类似于jQuery的操作方法来取出和操作数据。 + +## 输入 + +### 解析一个html字符串 + +#### 存在问题 + +来自用户输入,一个文件或一个网站的HTML字符串,你可能需要对它进行解析并取其内容,或校验其格式是否完整,或想修改它。怎么办?jsonu能够帮你轻松解决这些问题 + +#### 解决方法 + +使用静态`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加载一个Document + +#### 存在问题 + +你需要从一个网站获取和解析一个HTML文档,并查找其中的相关数据。你可以使用下面解决方法: + +#### 解决方法 + +使用 `Jsoup.connect(String url)`方法: + +```java +Document doc = Jsoup.connect("http://example.com/").get(); +String title = doc.title(); +``` + +#### 说明 + +`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(); +``` + +这个方法只支持Web URLs (`http`和`https` 协议); 假如你需要从一个文件加载,可以使用 `parse(File in, String charsetName)` 代替。 + + + +### 从一个文件加载一个文档 + +#### 问题 + +在本机硬盘上有一个HTML文件,需要对它进行解析从中抽取数据或进行修改。 + +#### 办法 + +可以使用静态 `Jsoup.parse(File in, String charsetName, String baseUri)` 方法: + +``` +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方法来更改数据. + +### 处理URLs + +#### 问题 + +你有一个包含相对URLs路径的HTML文档,需要将这些相对路径转换成绝对路径的URLs。 + +#### 方法 + +1. 在你解析文档时确保有指定`base URI`,然后 +2. 使用 `abs:` 属性前缀来取得包含`base URI`的绝对路径。代码如下: + +``` +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"` 可以使用如下方法: + +```java +doc.select("div.comments a").attr("rel", "nofollow"); +``` + +#### 说明 + +与`Element`中的其它方法一样,`attr` 方法也是返回当 `Element` (或在使用选择器是返回 `Elements` 集合)。这样能够很方便使用方法连用的书写方式。比如: + +```java +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内容。 + +### 设置元素的文本内容 + +#### 问题 + +你需要修改一个HTML文档中的文本内容 + +#### 方法 + +可以使用`Element`的设置方法:: + +```java +Element div = doc.select("div").first(); //
    +div.text("five > four"); //
    five > four
    +div.prepend("First "); +div.append(" Last"); +// now:
    First five > four Last
    +``` + +#### 说明 + +文本设置方法与设置元素的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攻击,还可以限制用户可以输入的标签范围。 + + + +## 程序实例:获取所有链接 + +```java +package org.jsoup.examples; + +import org.jsoup.Jsoup; +import org.jsoup.helper.Validate; +import org.jsoup.nodes.Document; +import org.jsoup.nodes.Element; +import org.jsoup.select.Elements; + +import java.io.IOException; + +/** + * Example program to list links from a URL. + */ +public class ListLinks { + public static void main(String[] args) throws IOException { + Validate.isTrue(args.length == 1, "usage: supply url to fetch"); + String url = args[0]; + print("Fetching %s...", url); + + Document doc = Jsoup.connect(url).get(); + Elements links = doc.select("a[href]"); + Elements media = doc.select("[src]"); + Elements imports = doc.select("link[href]"); + + print("\nMedia: (%d)", media.size()); + for (Element src : media) { + if (src.tagName().equals("img")) + print(" * %s: <%s> %sx%s (%s)", + src.tagName(), src.attr("abs:src"), src.attr("width"), src.attr("height"), + trim(src.attr("alt"), 20)); + else + print(" * %s: <%s>", src.tagName(), src.attr("abs:src")); + } + + print("\nImports: (%d)", imports.size()); + for (Element link : imports) { + print(" * %s <%s> (%s)", link.tagName(),link.attr("abs:href"), link.attr("rel")); + } + + print("\nLinks: (%d)", links.size()); + for (Element link : links) { + print(" * a: <%s> (%s)", link.attr("abs:href"), trim(link.text(), 35)); + } + } + + private static void print(String msg, Object... args) { + System.out.println(String.format(msg, args)); + } + + private static String trim(String s, int width) { + if (s.length() > width) + return s.substring(0, width-1) + "."; + else + return s; + } +} +``` + + + +## 引用 + +[jsonp入门教程](https://www.open-open.com/jsoup/parsing-a-document.htm) \ No newline at end of file diff --git a/java/httpclient.md b/java/httpclient.md new file mode 100644 index 0000000..b9cef6b --- /dev/null +++ b/java/httpclient.md @@ -0,0 +1,130 @@ +## 引入pom + +```xml + + org.apache.httpcomponents + httpclient + 4.2 + +``` + +## 编写HttpClient发送请求的步骤 + +1. 创建HttpClient对象 +2. 创建Http[HttpMethod]具体发送http方式对象 +3. 设置http方法对象 +4. 使用HttpClient执行http方法对象 +5. 接受HttpClient方法返回值 + +## 案例 + +### 处理url + +```java + /** + * 构造请求 url + * @param url 源url + * @param param 键值对参数 + * @return 带参数的url + */ + private static String buildUrl(String url, Map param) { + if (url == null || "".equals(url = url.trim())) { + throw new IllegalArgumentException("Http request url must not null"); + } + StringBuilder requestUrl = new StringBuilder(url); + if (param != null && param.size() > 0) { + boolean first = true; + for (Map.Entry entry : param.entrySet()) { + if (first) { + // 不存在 ? 添加一个 + if (!url.contains("?")) { + requestUrl.append("?"); + } + if (url.contains("&") && url.indexOf("&") < url.length() - 1) { + requestUrl.append("&"); + } + first = !first; + } else { + requestUrl.append("&"); + } + requestUrl.append(entry.getKey()).append("=").append(entry.getValue()); + } + } + return requestUrl.toString(); + } +``` + +### get 请求 + +```java + /** + * 发送get请求 + * @param url 请求地址 + * @param headers 头部信息 + * @param params 请求参数 + * @return 返回值 + */ + public static String get(String url, Map headers, Map params) { + String result = null; + // http 客户端 + HttpClient httpClient = new DefaultHttpClient(); + try { + // 执行get请求 + HttpGet httpGet = new HttpGet(buildUrl(url, params)); + if (headers != null && headers.size() > 0) { + headers.forEach(httpGet::setHeader); + } + HttpResponse response = httpClient.execute(httpGet); + HttpEntity httpEntity = response.getEntity(); + if (httpEntity != null) { + result = EntityUtils.toString(httpEntity); + } + } catch (IOException e) { + throw new RuntimeException(e); + } finally { + httpClient.getConnectionManager().shutdown(); + } + return result; + } +``` + +### post 请求 + +```java + /** + * 发送get请求 + * @param url 请求地址 + * @param headers 头部信息 + * @param params 请求参数 (普通的键值对参数) + * @param body 内容体,可以是任何字符串形式 + * @return 返回值 + */ + public static String post(String url, Map headers, Map params, String body) { + String result = null; + // http 客户端 + HttpClient httpClient = new DefaultHttpClient(); + try { + HttpPost httpPost = new HttpPost(buildUrl(url, params)); + if (headers != null && headers.size() > 0) { + headers.forEach(httpPost::setHeader); + } + if (body != null && !"".equals(body.trim())) { + HttpEntity httpEntity = new StringEntity(body, Charset.forName("UTF-8")); + httpPost.setEntity(httpEntity); + } + + HttpResponse response = httpClient.execute(httpPost); + HttpEntity httpEntity = response.getEntity(); + if (httpEntity != null) { + result = EntityUtils.toString(httpEntity); + } + } catch (IOException e) { + throw new RuntimeException(e); + } finally { + httpClient.getConnectionManager().shutdown(); + } + + return result; + } +``` + diff --git a/java/images/001/001.png b/java/images/001/001.png new file mode 100644 index 0000000..ee0105a Binary files /dev/null and b/java/images/001/001.png differ diff --git "a/java/images/002/java\351\233\206\345\220\210\346\241\206\346\236\266.jpg" "b/java/images/002/java\351\233\206\345\220\210\346\241\206\346\236\266.jpg" new file mode 100644 index 0000000..5ca9e6f Binary files /dev/null and "b/java/images/002/java\351\233\206\345\220\210\346\241\206\346\236\266.jpg" differ diff --git a/java/images/003/1570773224689.png b/java/images/003/1570773224689.png new file mode 100644 index 0000000..36b0820 Binary files /dev/null and b/java/images/003/1570773224689.png differ diff --git a/java/java_persistence_api.md b/java/java_persistence_api.md new file mode 100644 index 0000000..118ef96 --- /dev/null +++ b/java/java_persistence_api.md @@ -0,0 +1,599 @@ +## Entities + +**标准的实体类的要求** + +* 必须被 `javax.persistence.Entity`注解修饰 +* 实体类必须是 `public`或者`protect` 的, 必须得有一个无参的构造器,当然,它可以有其他的构造器。 +* 必须不能为`final`修饰,没有方法或实体常量必须被声明为`final`. +* 如果实体需要被远程调用或传输,必须实现`Serializable`接口。 +* 实体类可以扩展非实体类和实体类。非实体类可以扩展实体类。 +* 实体的属性必须被声明为`private`、`protect`或`package-privare`,并且只能通过实体类的方法访问。客户端必须通过访问者或业务方法访问实体的状态。 + +**实体持久化字段和属性的类型** + +- Java 基本数据类型 +- `java.lang.String` +- 其他可被序列化的类: + - Java 基本数据类型的包装类 + - `java.math.BigInteger` + - `java.math.BigDecimal` + - `java.util.Date` + - `java.util.Calendar` + - `java.sql.Date` + - `java.sql.Time` + - `java.sql.TimeStamp` + - 用户定义的可序列化的类型 + - `byte[]` + - `Byte[]` + - `char[]` + - `Character[]` +- 枚举类型 +- 其他实体和/或实体的集合 +- 内部类 + +**持久化字段** + +如果一个字段没有注解`javax.persitence.Transient`或者没有被标注为`transient`,那么这个字段将会被持久化到数据库。 + +**实体中的主键** + +每个实体都有一个唯一的对象标识。 + +简单的主键可以使用`javax.persitence.Id`注解来表明这个字段是一个主键。 + +复合主键必须对应于单个持久属性或字段,或者对应于一组单个持久性属性或字段。必须在主键类中定义组合主键 。复合主键可以使用`javax.persistence.EmbeddedId`和`javax.persistence.IdClass`注解来表明。 + +复合主键的类型必须是下面的几种类型 + +* java 基本数据类型 +* java 基本数据类型的包装类 +* `java.lang.String` +* `java.util.Date` +* `java.sql.Date` + +主键必须是一个整形,而不能是浮点型。 + +**主键类的要求** + +* 访问修饰符必须是`public` +* 如果使用基于属性的访问,则主键类的属性必须是`public`或`protect`的。 +* 必须可以被序列化(实现`Serializable`接口) +* 必须表示复合主键并将其映射到实体类的多个字段或属性,或者必须将其表示并映射为可嵌入类。 +* 如果类映射到实体类的多个字段或属性,则主键类中主键字段或属性的名称和类型必须与实体类的名称和类型匹配。 + +```java +public final class LineItemKey implements Serializable { + public Integer orderId; + public int itemId; + + public LineItemKey() {} + + public LineItemKey(Integer orderId, int itemId) { + this.orderId = orderId; + this.itemId = itemId; + } + + public boolean equals(Object otherOb) { + if (this == otherOb) { + return true; + } + if (!(otherOb instanceof LineItemKey)) { + return false; + } + LineItemKey other = (LineItemKey) otherOb; + return ( + (orderId==null?other.orderId==null:orderId.equals + (other.orderId) + ) + && + (itemId == other.itemId) + ); + } + + public int hashCode() { + return ( + (orderId==null?0:orderId.hashCode()) + ^ + ((int) itemId) + ); + } + + public String toString() { + return "" + orderId + "-" + itemId; + } +} +``` + +**实体之间的关系** + +* One-to-one:一对一的关系使用`javax.persistence.OneToOne`注解来声明持久化字段或属性 +* One-to-many:一对多的关系使用`javax.persistence.OneToMany`来声明持久化字段或属性 +* Many-to-one:多对一的关系使用`javax.persistence.ManyToOne`来声明持久化字段或属性 +* Many-to-many:多对多的关系使用`javax.persistence.ManyToMany`来声明持久化字段或属性 + +**实体关系之间的方向** + +实体之间的关系可以是双向的,也可以是单向的。 + +**双向关系** + +在一个双向关系中,每一个实体都有一个字段来映射其他的实体。 + +双向关联的关系必须有如下规则: + +* 双向关系的反面必须通过使用`@OneToOne`,`@ OneToMany`或`@ManyToMany`注释的`mappedBy`元素来引用其拥有方。 `mappedBy`元素指定作为关系所有者的实体中的属性或字段。 +* 多对一的关系中,多的一次不能使用`mappedBy`。因为多的一方始终是关系的拥有方。 +* 对于一对一的双向关系,拥有方对应于包含相应外键的一侧。 +* 对于多对多双向关系,任何一方都可能是拥有方。 + +**级联删除关系** + +使用关系的实体通常依赖于关系中另一个实体的存在, 如果一方删除,也另一方也应该删除,这种方式叫做"级联删除"。 + +使用`@OneToOne`和`@OneToMany`关系的`cascade = REMOVE`元素规范指定级联删除关系 + +```java +@OneToMany(cascade=REMOVE, mappedBy="customer") +public Set getOrders() { return orders; } +``` + +**抽象实体** + +```java +@Entity +public abstract class Employee { + @Id + protected Integer employeeId; + ... +} +@Entity +public class FullTimeEmployee extends Employee { + protected Integer salary; + ... +} +@Entity +public class PartTimeEmployee extends Employee { + protected Float hourlyWage; +} +``` + +**映射超类** + +通过使用`javax.persistence.MappedSuperclass`注解对类进行装饰来指定映射的超类。 + +```java +@MappedSuperclass +public class Employee { + @Id + protected Integer employeeId; + ... +} +@Entity +public class FullTimeEmployee extends Employee { + protected Integer salary; + ... +} +@Entity +public class PartTimeEmployee extends Employee { + protected Float hourlyWage; + ... +} +``` + + + +## EntityManger + +**获取EntityManger** + +```java +@PersistenceContext +EntityManager em; +``` + +**获取EntityMangerFactory** + +```java +@PersistenceUnit +EntityManagerFactory emf; +``` + +我们可以从`EntityMangerFactory`中通过调用`createEntityManger`方法获取`EntityManger` + +```java +EntityManager em = emf.createEntityManager(); +``` + +**使用EntityManger从`data store`中通过主键查询实体 + +```java +@PersistenceContext +EntityManager em; +public void enterOrder(int custID, Order newOrder) { + Customer cust = em.find(Customer.class, custID); + cust.getOrders().add(newOrder); + newOrder.setCustomer(cust); +} +``` + +**Entity的声明周期** + +Entity 的声明周期有 `new`(新实体), `managed`(被管理), `detached`(分离), `removed`(被移除)四种状态 + +* `new`没有持久化标识(主键),和持久化上下文(persistence context)没有关联 +* `managed`有持久化标识,且和持久化上下文有关联。 +* `detached` 有持久化标识,并且当前不与持久性上下文相关联。 +* `removed`有持久化标识,与持久性上下文关联,并计划从数据存储中删除。 + +**持久化实体(保存)** + +通过调用`persist`方法或通过在关系注释中设置了`cascade = PERSIST`或`cascade = ALL`元素的相关实体调用的级联持久操作,新实体实例变为托管和持久化 。如果实体已经是被管理的实例,则会忽略`persist`操作。如果`persist`的方法被 `removed`状态的实体调用,则该实体将会从`removed`状态转变为`managed`状态。如果实体为`detached`状态,则`persist`将会抛出`IllegalArgumentException `, 并且事务的提交会失败。 + +```java +@PersistenceContext +EntityManager em; +... +public LineItem createLineItem(Order order, Product product, + int quantity) { + LineItem li = new LineItem(order, product, quantity); + order.getLineItems().add(li); + em.persist(li); + return li; +} +``` + +`persist`(持久化)操作将传播到实体中所有的关系注解(OneToMany等),如果关系注解中的`cascade`  属性被设置为`ALL`或者`PERSIST`。则其注解的对象也会被持久化到数据库。 + +```java +@OneToMany(cascade=ALL, mappedBy="order") +public Collection getLineItems() { + return lineItems; +} +``` + +**删除 Entity 实例(删除)** + +`managed`(被管理得)实例通过调用`remove`方法删除,并且如果被删除的实例中的关系注解的`cascade`属性被设置为`ALL`或`REMOVE`,则对应的对象也会被删除。如果删除的对象是一个新的实体,则删除操作将会被忽略。如果删除的是一个`detached`对象,则会抛出`IllegalArgumentException `,且事务提交也会失败。如果删除的是一个已被删除的实体,则操作将被忽略。当事务提交或者执行`flush`操作时,数据将会从数据库中删除。 + +```java +public void removeOrder(Integer orderId) { + try { + Order order = em.find(Order.class, orderId); + em.remove(order); + }... +``` + +**创建查询语句** + +`EntityManager.createQuery `和`EntityManager.createNamedQuery `被用来使用`JPA`查询语言从数据库查询数据。 + +* `createQuery `被用来创建动态查询 + +```java +public List findWithName(String name) { +return em.createQuery( + "SELECT c FROM Customer c WHERE c.name LIKE :custName") + .setParameter("custName", name) + .setMaxResults(10) + .getResultList(); +} +``` + +* `createNamedQuery `被用来创建静态查询,通过使用`javax.persistence.NamedQuery `注解在实体类上声明,该注解中的`name`元素指定了该静态查询语句的名称。`query` 元素指定了查询的语句。 + +```java +@NamedQuery( + name="findAllCustomersWithName", + query="SELECT c FROM Customer c WHERE c.name LIKE :custName" +) +``` + +```java +@PersistenceContext +public EntityManager em; +... +customers = em.createNamedQuery("findAllCustomersWithName") + .setParameter("custName", "Smith") + .getResultList(); +``` + +**查询语句中的参数** + +命名参数在查询中使用(:)作为前缀,它会被`javax.persistence.Query.setParameter(String name, Object value) `方法绑定 + +```java +public List findWithName(String name) { + Query query = em.createQuery("SELECT c FROM Customer c WHERE c.name LIKE :custName"); + query.setParameter("custName", name); + return query.getResultList(); +} +``` + +也可以使用参数下标的形式对参数进行赋值 + +```java +public List findWithName(String name) { + return em.createQuery( + “SELECT c FROM Customer c WHERE c.name LIKE ?1”) + .setParameter(1, name) + .getResultList(); +} +``` + +**持久化单元(配置文件)** + +持久性单元定义由应用程序中的EntityManager实例管理的所有实体类的集合。这组实体类表示单个数据存储中包含的数据。 + +持久性单元由persistence.xml配置文件定义,META-INF目录包含persistence.xml的JAR文件或目录称为持久性单元的根,持久性单元的范围由持久性单元的根确定。 + +```xml + + + + This unit manages orders and customers. + It does not rely on any vendor-specific features and can + therefore be deployed to any persistence provider. + + jdbc/MyOrderDB + MyOrderApp.jar + com.widgets.Order + com.widgets.Customer + + +``` + + + +## JPA 在WEB 中的应用 + +**定义持久化单元** + +持久化单元被定义在`persistence.xml`文件中,其中包含了以下内容: + +* `persistence` 元素,用于标识描述符验证的模式,并包含`persistence-unit`元素。 +* `persistence-unit` 元素定义了持久化单元的名称和事务类型 +* `description` 可选 +* `jta-data-source` 它指定JTA数据源的全局JNDI名称。 + +`jta-data-source` 元素指示实体管理器参与的事务是JTA事务,这意味着事务由容器管理。 也可以使用`resource-local`来管理事务,这是由应用程序本身提供的事务。 + +资源本地实体管理器不能参与全局事务。此外,Web容器不会回滚由编写不良的应用程序留下的待处理事务。 + +**创建持久化实体** + +```java +@Entity +@Table(name="WEB_BOOKSTORE_BOOKS") +public class Book implements Serializable { + + @Id + private String bookId; + private String title; + ... //getter and setter +} +``` + +**通过EntityManager访问数据** + +```java +public final class ContextListener implements SerlvetContextListener { +... +@PersistenceUnit +private EntityManagerFactory emf; + +public void contextInitialized(ServletContexEvent event) { + context = event.getServletContext(); + ... + try { + BookDBAO bookDB = new BookDBAO(emf); + context.setAttribute("bookDB", bookDB); + } catch (Exception ex) { + System.out.println( + "Couldn’t create bookstore database bean: " + + ex.getMessage()); + } +} +} +``` + +`BookDBAO`源码如下 + +```java +private EntityManager em; + +public BookDBAO (EntityManagerFactory emf) throws Exception { + em = emf.getEntityManager(); + ... +} +``` + +也可以使用下面的方式在 `DAO` 中直接获取 `EntityManager` + +```java +public class BookDBAO { + + @PersistenceContext + private EntityManager em; +... +``` + +我们可以通过`EntityManager`内提供的方法执行`CRUD`操作 + +**事务** + +```java +@Resource +UserTransaction utx; +... +try { + utx.begin(); + bookDBAO.buyBooks(cart); + utx.commit(); +} catch (Exception ex) { + try { + utx.rollback(); + } catch (Exception exe) { + System.out.println("Rollback failed: "+exe.getMessage()); +} +... +``` + + + +## JAVA 持久化查询语言 + +**基本查询语句** + +```sql +SELECT p FROM Player p +``` + +`FROM` 元素后面跟的不再试 SQL 语言中的表名,而是JAVA 中的实体名称。 + +**去重** + +```sql +SELECT DISTINCT + p +FROM Player p +WHERE p.position = ?1 +``` + +**命名参数** + +```sql +SELECT DISTINCT p +FROM Player p +WHERE p.position = :position AND p.name = :name +``` + +**关联查询** + +***一对多 或 多对多*** + +```sql +SELECT DISTINCT p +FROM Player p, IN(p.teams) t +``` + +也可以使用 `JOIN` 操作符 + +```sql +SELECT DISTINCT p +FROM Player p JOIN p.teams t + +-- 也可以重写为: +SELECT DISTINCT p +FROM Player p +WHERE p.team IS NOT EMPTY +``` + +***一对一*** + +```sql +SELECT t + FROM Team t JOIN t.league l + WHERE l.sport = ’soccer’ OR l.sport =’football’ +``` + +***整合使用*** + +```sql +SELECT DISTINCT p +FROM Player p, IN (p.teams) AS t +WHERE t.city = :city +``` + +***遍历多条关系*** + +```sql +SELECT DISTINCT p +FROM Player p, IN (p.teams) t +WHERE t.league = :league +``` + +***通过关联对象的属性查询*** + +```sql +SELECT DISTINCT p +FROM Player p, IN (p.teams) t +WHERE t.league.sport = :sport +``` + +***LIKE 查询*** + +```sql +SELECT p + FROM Player p + WHERE p.name LIKE ’Mich%’ +``` + +***IS NULL*** + +```sql +SELECT t + FROM Team t + WHERE t.league IS NULL +``` + +***IS EMPTY*** + +```SQL +SELECT p +FROM Player p +WHERE p.teams IS EMPTY +``` + +***BETWEEN*** + +```SQL +SELECT DISTINCT p +FROM Player p +WHERE p.salary BETWEEN :lowerSalary AND :higherSalary +``` + +***比较*** + +```sql +SELECT DISTINCT p1 +FROM Player p1, Player p2 +WHERE p1.salary > p2.salary AND p2.name = :name +``` + +***更新*** + +```sql +UPDATE Player p +SET p.status = ’inactive’ +WHERE p.lastPlayed < :inactiveThresholdDate +``` + +***删除*** + +```sql +DELETE +FROM Player p +WHERE p.status = ’inactive’ +AND p.teams IS EMPTY +``` + + + + + + + + + + + + + + + + + + + + + diff --git "a/java/java_spi\346\234\272\345\210\266.md" "b/java/java_spi\346\234\272\345\210\266.md" new file mode 100644 index 0000000..3f8c74c --- /dev/null +++ "b/java/java_spi\346\234\272\345\210\266.md" @@ -0,0 +1,100 @@ +### 什么是SPI + +SPI的全名为:Service Provider Interface,这个东西是针对厂商或者插件的,在java.util.ServiceLoad的文档中有详细的介绍。 + +简单来说,我们的系统中抽象的各个模块,往往有很多不同的实现方案,比如日志模块的方案,jdbc模块的方案等。我们一般推荐模块之间基于接口编程,模块之间不对实现类进行硬编码。 + + + +### Java SPI机制 + +Java SPI提供了为某个接口寻找服务实现的的机制,有点类似于IOC的思想,就是将装配的控制权移交到程序之外,在模块化设计中这个机制尤为重要。 + + + +### 约定 + +当服务的提供者提供了服务接口的一种实现后,在**jar**包的**META-INF/services/**目录里同时创建一个服务接口命名的文件。该文件里就是实现服务接口的具体实现类。 + +而当外部程序装配这个模块时,就能通过该配置文件找到具体的实现类名,并装载实例化,完成模块的注入。 + +基于这样一个约定就能很好的找到服务接口的实现类,而不需要在代码里指定。jdk提供服务实现查找的一个工具类为**java.util.ServiceLoad**。 + + + +### 代码实现SPI + +#### 定义抽象接口 + +首先需要定义一个抽象接口,用于将功能分离。具体实现由各个厂商负责。 + +```java +package com.demo.inter; + +public interface Service { + + void say(); +} +``` + +#### 定义实现接口的类 + +实现类用于实现抽象接口中的具体方法,为服务提供功能需要。 + +```java +package com.demo.a.impl; + +import com.demo.inter.Service; + +public class ServiceImpl implements Service { + @Override + public void say() { + System.out.println("I am A"); + } +} +``` + +#### 定义配置文件 + +在基于MAVEN搭建的Java工程中,需要将 **META-INF/services/** 建立在 **resources** 目录下。 + +在 **resources** 目录下创建 **META-INF/services/** 目录,并在 **META-INF/services/** 目录下建立 **com.demo.inter.Service** 文件。 + +**com.demo.inter.Service** 文件中填写具体的实现类,如下: + +```java +com.demo.a.impl.ServiceImpl +``` + +#### 定义测试类 + +```java +package com.demo; + +import com.demo.inter.Service; +import java.util.Iterator; +import java.util.ServiceLoader; + +public class Main { + + public static void main(String[] args) { + // 使用ServiceLoad加载 META-INF/services/com.demo.inter.Service 文件下的内容 + ServiceLoader services = ServiceLoader.load(Service.class); + // 获取所有的实现类 + Iterator iterator = services.iterator(); + if (iterator.hasNext()) { + // META-INF/services/com.demo.inter.Service 文件中配置的实现类 + Service service = iterator.next(); + service.say(); + } + } +} + +``` + +运行代码后输出: + +```java +I am A +``` + diff --git a/java/javaagent.md b/java/javaagent.md new file mode 100644 index 0000000..50c70aa --- /dev/null +++ b/java/javaagent.md @@ -0,0 +1,529 @@ +## Instrumentation 简介 + +利用 Java 代码,即 java.lang.instrument 做动态 Instrumentation 是 Java SE 5 的新特性,它把 Java 的 instrument 功能从本地代码中解放出来,使之可以用 Java 代码的方式解决问题。使用 Instrumentation,开发者可以构建一个独立于应用程序的代理程序(Agent),用来监测和协助运行在 JVM 上的程序,甚至能够替换和修改某些类的定义。有了这样的功能,开发者就可以实现更为灵活的运行时虚拟机监控和 Java 类操作了,这样的特性实际上提供了一种虚拟机级别支持的 AOP 实现方式,使得开发者无需对 JDK 做任何升级和改动,就可以实现某些 AOP 的功能了。 + +在 Java SE 6 里面,instrumentation 包被赋予了更强大的功能:启动后的 instrument、本地代码(native code)instrument,以及动态改变 classpath 等等。这些改变,意味着 Java 具有了更强的动态控制、解释能力,它使得 Java 语言变得更加灵活多变。 + +在 Java SE6 里面,最大的改变使运行时的 Instrumentation 成为可能。在 Java SE 5 中,Instrument 要求在运行前利用命令行参数或者系统参数来设置代理类,在实际的运行之中,虚拟机在初始化之时(在绝大多数的 Java 类库被载入之前),instrumentation 的设置已经启动,并在虚拟机中设置了回调函数,检测特定类的加载情况,并完成实际工作。但是在实际的很多的情况下,我们没有办法在虚拟机启动之时就为其设定代理,这样实际上限制了 instrument 的应用。而 Java SE 6 的新特性改变了这种情况,通过 Java Tool API 中的 attach 方式,我们可以很方便地在运行过程中动态地设置加载代理类,以达到 instrumentation 的目的。 + +另外,对 native 的 Instrumentation 也是 Java SE 6 的一个崭新的功能,这使以前无法完成的功能 —— 对 native 接口的 instrumentation 可以在 Java SE 6 中,通过一个或者一系列的 prefix 添加而得以完成。 + +最后,Java SE 6 里的 Instrumentation 也增加了动态添加 class path 的功能。所有这些新的功能,都使得 instrument 包的功能更加丰富,从而使 Java 语言本身更加强大。 + + + + + +## Instrumentation 的基本功能和用法 + +“java.lang.instrument”包的具体实现,依赖于 JVMTI。JVMTI(Java Virtual Machine Tool Interface)是一套由 Java 虚拟机提供的,为 JVM 相关的工具提供的本地编程接口集合。JVMTI 是从 Java SE 5 开始引入,整合和取代了以前使用的 Java Virtual Machine Profiler Interface (JVMPI) 和 the Java Virtual Machine Debug Interface (JVMDI),而在 Java SE 6 中,JVMPI 和 JVMDI 已经消失了。JVMTI 提供了一套”代理”程序机制,可以支持第三方工具程序以代理的方式连接和访问 JVM,并利用 JVMTI 提供的丰富的编程接口,完成很多跟 JVM 相关的功能。事实上,java.lang.instrument 包的实现,也就是基于这种机制的:在 Instrumentation 的实现当中,存在一个 JVMTI 的代理程序,通过调用 JVMTI 当中 Java 类相关的函数来完成 Java 类的动态操作。除开 Instrumentation 功能外,JVMTI 还在虚拟机内存管理,线程控制,方法和变量操作等等方面提供了大量有价值的函数。关于 JVMTI 的详细信息,请参考 Java SE 6 文档(请参见 [参考资源](http://www.ibm.com/developerworks/cn/java/j-lo-jse61/#resources))当中的介绍。 + +Instrumentation 的最大作用,就是类定义动态改变和操作。在 Java SE 5 及其后续版本当中,开发者可以在一个普通 Java 程序(带有 main 函数的 Java 类)运行时,通过 `– javaagent`参数指定一个特定的 jar 文件(包含 Instrumentation 代理)来启动 Instrumentation 的代理程序。 + +在 Java SE 5 当中,开发者可以让 Instrumentation 代理在 main 函数运行前执行。简要说来就是如下几个步骤: + +1. **编写** **premain** **函数** + +编写一个 Java 类,包含如下两个方法当中的任何一个 + +``` +public static void premain(String agentArgs, Instrumentation inst); [1] +public static void premain(String agentArgs); [2] +``` + +其中,[1] 的优先级比 [2] 高,将会被优先执行([1] 和 [2] 同时存在时,[2] 被忽略)。 + +在这个 premain 函数中,开发者可以进行对类的各种操作。 + +agentArgs 是 premain 函数得到的程序参数,随同 “`– javaagent`”一起传入。与 main 函数不同的是,这个参数是一个字符串而不是一个字符串数组,如果程序参数有多个,程序将自行解析这个字符串。 + +Inst 是一个 java.lang.instrument.Instrumentation 的实例,由 JVM 自动传入。java.lang.instrument.Instrumentation 是 instrument 包中定义的一个接口,也是这个包的核心部分,集中了其中几乎所有的功能方法,例如类定义的转换和操作等等。 + +1. **jar** **文件打包** + +将这个 Java 类打包成一个 jar 文件,并在其中的 manifest 属性当中加入” Premain-Class”来指定步骤 1 当中编写的那个带有 premain 的 Java 类。(可能还需要指定其他属性以开启更多功能) + +1. **运行** + +用如下方式运行带有 Instrumentation 的 Java 程序: + +``` + java -javaagent:jar 文件的位置 [= 传入 premain 的参数 ] +``` + +对 Java 类文件的操作,可以理解为对一个 byte 数组的操作(将类文件的二进制字节流读入一个 byte 数组)。开发者可以在“ClassFileTransformer”的 transform 方法当中得到,操作并最终返回一个类的定义(一个 byte 数组)。这方面,Apache 的 BCEL 开源项目提供了强有力的支持,读者可以在参考文章“[Java SE 5 特性 Instrumentation 实践](http://www.ibm.com/developerworks/cn/java/j-lo-instrumentation/)”中看到一个 BCEL 和 Instrumentation 结合的例子。具体的字节码操作并非本文的重点,所以,本文中所举的例子,只是采用简单的类文件替换的方式来演示 Instrumentation 的使用。 + +下面,我们通过简单的举例,来说明 Instrumentation 的基本使用方法。 + +首先,我们有一个简单的类,TransClass, 可以通过一个静态方法返回一个整数 1。 + +``` + public class TransClass { + public int getNumber() { + return 1; + } + } +``` + +我们运行如下类,可以得到输出 ”1“。 + +``` + public class TestMainInJar { + public static void main(String[] args) { + System.out.println(new TransClass().getNumber()); + } + } +``` + +然后,我们将 TransClass 的 getNumber 方法改成如下 : + +``` + public int getNumber() { + return 2; + } +``` + +再将这个返回 2 的 Java 文件编译成类文件,为了区别开原有的返回 1 的类,我们将返回 2 的这个类文件命名为 TransClass2.class.2。 + +接下来,我们建立一个 Transformer 类: + +``` + import java.io.File; + import java.io.FileInputStream; + import java.io.IOException; + import java.io.InputStream; + import java.lang.instrument.ClassFileTransformer; + import java.lang.instrument.IllegalClassFormatException; + import java.security.ProtectionDomain; + + class Transformer implements ClassFileTransformer { + + public static final String classNumberReturns2 = "TransClass.class.2"; + + public static byte[] getBytesFromFile(String fileName) { + try { + // precondition + File file = new File(fileName); + InputStream is = new FileInputStream(file); + long length = file.length(); + byte[] bytes = new byte[(int) length]; + + // Read in the bytes + int offset = 0; + int numRead = 0; + while (offset = 0) { + offset += numRead; + } + + if (offset < bytes.length) { + throw new IOException("Could not completely read file " + + file.getName()); + } + is.close(); + return bytes; + } catch (Exception e) { + System.out.println("error occurs in _ClassTransformer!" + + e.getClass().getName()); + return null; + } + } + + /** + * 参数: + * loader - 定义要转换的类加载器;如果是引导加载器,则为 null + * className - 完全限定类内部形式的类名称和 The Java Virtual Machine Specification 中定义的接口名称。例如,"java/util/List"。 + * classBeingRedefined - 如果是被重定义或重转换触发,则为重定义或重转换的类;如果是类加载,则为 null + * protectionDomain - 要定义或重定义的类的保护域 + * classfileBuffer - 类文件格式的输入字节缓冲区(不得修改) + * 返回: + * 一个格式良好的类文件缓冲区(转换的结果),如果未执行转换,则返回 null。 + * 抛出: + * IllegalClassFormatException - 如果输入不表示一个格式良好的类文件 + */ + public byte[] transform(ClassLoader l, String className, Class c, + ProtectionDomain pd, byte[] b) throws IllegalClassFormatException { + if (!className.equals("TransClass")) { + return null; + } + return getBytesFromFile(classNumberReturns2); + + } + } +``` + +这个类实现了 ClassFileTransformer 接口。其中,getBytesFromFile 方法根据文件名读入二进制字符流,而 ClassFileTransformer 当中规定的 transform 方法则完成了类定义的替换转换。 + +最后,我们建立一个 Premain 类,写入 Instrumentation 的代理方法 premain: + +``` + public class Premain { + public static void premain(String agentArgs, Instrumentation inst) + throws ClassNotFoundException, UnmodifiableClassException { + inst.addTransformer(new Transformer()); + } + } +``` + +可以看出,addTransformer 方法并没有指明要转换哪个类。转换发生在 premain 函数执行之后,main 函数执行之前,这时每装载一个类,transform 方法就会执行一次,看看是否需要转换,所以,在 transform(Transformer 类中)方法中,程序用 className.equals("TransClass") 来判断当前的类是否需要转换。 + +代码完成后,我们将他们打包为 TestInstrument1.jar。返回 1 的那个 TransClass 的类文件保留在 jar 包中,而返回 2 的那个 TransClass.class.2 则放到 jar 的外面。在 manifest 里面加入如下属性来指定 premain 所在的类: + +``` + Manifest-Version: 1.0 + Premain-Class: Premain +``` + +在运行这个程序的时候,如果我们用普通方式运行这个 jar 中的 main 函数,可以得到输出“1”。如果用下列方式运行 : + +``` + java – javaagent:TestInstrument1.jar – cp TestInstrument1.jar TestMainInJar +``` + +则会得到输出“2”。 + +当然,程序运行的 main 函数不一定要放在 premain 所在的这个 jar 文件里面,这里只是为了例子程序打包的方便而放在一起的。 + +除开用 addTransformer 的方式,Instrumentation 当中还有另外一个方法“redefineClasses”来实现 premain 当中指定的转换。用法类似,如下: + +``` + public class Premain { + public static void premain(String agentArgs, Instrumentation inst) + throws ClassNotFoundException, UnmodifiableClassException { + ClassDefinition def = new ClassDefinition(TransClass.class, Transformer + .getBytesFromFile(Transformer.classNumberReturns2)); + inst.redefineClasses(new ClassDefinition[] { def }); + System.out.println("success"); + } + } +``` + +redefineClasses 的功能比较强大,可以批量转换很多类。 + + + + + +## Java SE 6 的新特性:虚拟机启动后的动态 instrument + +在 Java SE 5 当中,开发者只能在 premain 当中施展想象力,所作的 Instrumentation 也仅限与 main 函数执行前,这样的方式存在一定的局限性。 + +在 Java SE 5 的基础上,Java SE 6 针对这种状况做出了改进,开发者可以在 main 函数开始执行以后,再启动自己的 Instrumentation 程序。 + +在 Java SE 6 的 Instrumentation 当中,有一个跟 premain“并驾齐驱”的“agentmain”方法,可以在 main 函数开始运行之后再运行。 + +跟 premain 函数一样, 开发者可以编写一个含有“agentmain”函数的 Java 类: + +``` + public static void agentmain (String agentArgs, Instrumentation inst); [1] + public static void agentmain (String agentArgs); [2] +``` + +同样,[1] 的优先级比 [2] 高,将会被优先执行。 + +跟 premain 函数一样,开发者可以在 agentmain 中进行对类的各种操作。其中的 agentArgs 和 Inst 的用法跟 premain 相同。 + +与“Premain-Class”类似,开发者必须在 manifest 文件里面设置“Agent-Class”来指定包含 agentmain 函数的类。 + +可是,跟 premain 不同的是,agentmain 需要在 main 函数开始运行后才启动,这样的时机应该如何确定呢,这样的功能又如何实现呢? + +在 Java SE 6 文档当中,开发者也许无法在 java.lang.instrument 包相关的文档部分看到明确的介绍,更加无法看到具体的应用 agnetmain 的例子。不过,在 Java SE 6 的新特性里面,有一个不太起眼的地方,揭示了 agentmain 的用法。这就是 Java SE 6 当中提供的 Attach API。 + +Attach API 不是 Java 的标准 API,而是 Sun 公司提供的一套扩展 API,用来向目标 JVM ”附着”(Attach)代理工具程序的。有了它,开发者可以方便的监控一个 JVM,运行一个外加的代理程序。 + +Attach API 很简单,只有 2 个主要的类,都在 com.sun.tools.attach 包里面: VirtualMachine 代表一个 Java 虚拟机,也就是程序需要监控的目标虚拟机,提供了 JVM 枚举,Attach 动作和 Detach 动作(Attach 动作的相反行为,从 JVM 上面解除一个代理)等等 ; VirtualMachineDescriptor 则是一个描述虚拟机的容器类,配合 VirtualMachine 类完成各种功能。 + +为了简单起见,我们举例简化如下:依然用类文件替换的方式,将一个返回 1 的函数替换成返回 2 的函数,Attach API 写在一个线程里面,用睡眠等待的方式,每隔半秒时间检查一次所有的 Java 虚拟机,当发现有新的虚拟机出现的时候,就调用 attach 函数,随后再按照 Attach API 文档里面所说的方式装载 Jar 文件。等到 5 秒钟的时候,attach 程序自动结束。而在 main 函数里面,程序每隔半秒钟输出一次返回值(显示出返回值从 1 变成 2)。 + +TransClass 类和 Transformer 类的代码不变,参看上一节介绍。 含有 main 函数的 TestMainInJar 代码为: + +``` + public class TestMainInJar { + public static void main(String[] args) throws InterruptedException { + System.out.println(new TransClass().getNumber()); + int count = 0; + while (true) { + Thread.sleep(500); + count++; + int number = new TransClass().getNumber(); + System.out.println(number); + if (3 == number || count >= 10) { + break; + } + } + } + } +``` + +含有 agentmain 的 AgentMain 类的代码为: + +``` + import java.lang.instrument.ClassDefinition; + import java.lang.instrument.Instrumentation; + import java.lang.instrument.UnmodifiableClassException; + + public class AgentMain { + public static void agentmain(String agentArgs, Instrumentation inst) + throws ClassNotFoundException, UnmodifiableClassException, + InterruptedException { + inst.addTransformer(new Transformer (), true); + inst.retransformClasses(TransClass.class); + System.out.println("Agent Main Done"); + } + } +``` + +其中,retransformClasses 是 Java SE 6 里面的新方法,它跟 redefineClasses 一样,可以批量转换类定义,多用于 agentmain 场合。 + +Jar 文件跟 Premain 那个例子里面的 Jar 文件差不多,也是把 main 和 agentmain 的类,TransClass,Transformer 等类放在一起,打包为“TestInstrument1.jar”,而 Jar 文件当中的 Manifest 文件为 : + +``` + Manifest-Version: 1.0 + Agent-Class: AgentMain +``` + +另外,为了运行 Attach API,我们可以再写一个控制程序来模拟监控过程:(代码片段) + +``` + import com.sun.tools.attach.VirtualMachine; + import com.sun.tools.attach.VirtualMachineDescriptor; +…… + // 一个运行 Attach API 的线程子类 + static class AttachThread extends Thread { + + private final List listBefore; + + private final String jar; + + AttachThread(String attachJar, List vms) { + listBefore = vms; // 记录程序启动时的 VM 集合 + jar = attachJar; + } + + public void run() { + VirtualMachine vm = null; + List listAfter = null; + try { + int count = 0; + while (true) { + listAfter = VirtualMachine.list(); + for (VirtualMachineDescriptor vmd : listAfter) { + if (!listBefore.contains(vmd)) { + // 如果 VM 有增加,我们就认为是被监控的 VM 启动了 + // 这时,我们开始监控这个 VM + vm = VirtualMachine.attach(vmd); + break; + } + } + Thread.sleep(500); + count++; + if (null != vm || count >= 10) { + break; + } + } + vm.loadAgent(jar); + vm.detach(); + } catch (Exception e) { + ignore + } + } + } +…… + public static void main(String[] args) throws InterruptedException { + new AttachThread("TestInstrument1.jar", VirtualMachine.list()).start(); + + } +``` + +运行时,可以首先运行上面这个启动新线程的 main 函数,然后,在 5 秒钟内(仅仅简单模拟 JVM 的监控过程)运行如下命令启动测试 Jar 文件 : + +``` + java – javaagent:TestInstrument2.jar – cp TestInstrument2.jar TestMainInJar +``` + +如果时间掌握得不太差的话,程序首先会在屏幕上打出 1,这是改动前的类的输出,然后会打出一些 2,这个表示 agentmain 已经被 Attach API 成功附着到 JVM 上,代理程序生效了,当然,还可以看到“Agent Main Done”字样的输出。 + +以上例子仅仅只是简单示例,简单说明这个特性而已。真实的例子往往比较复杂,而且可能运行在分布式环境的多个 JVM 之中。 + + + +## Java SE 6 新特性:本地方法的 Instrumentation + +在 1.5 版本的 instumentation 里,并没有对 Java 本地方法(Native Method)的处理方式,而且在 Java 标准的 JVMTI 之下,并没有办法改变 method signature, 这就使替换本地方法非常地困难。一个比较直接而简单的想法是,在启动时替换本地代码所在的动态链接库 —— 但是这样,本质上是一种静态的替换,而不是动态的 Instrumentation。而且,这样可能需要编译较大数量的动态链接库 —— 比如,我们有三个本地函数,假设每一个都需要一个替换,而在不同的应用之下,可能需要不同的组合,那么如果我们把三个函数都编译在同一个动态链接库之中,最多我们需要 8 个不同的动态链接库来满足需要。当然,我们也可以独立地编译之,那样也需要 6 个动态链接库——无论如何,这种繁琐的方式是不可接受的。 + +在 Java SE 6 中,新的 Native Instrumentation 提出了一个新的 native code 的解析方式,作为原有的 native method 的解析方式的一个补充,来很好地解决了一些问题。这就是在新版本的 java.lang.instrument 包里,我们拥有了对 native 代码的 instrument 方式 —— 设置 prefix。 + +假设我们有了一个 native 函数,名字叫 nativeMethod,在运行中过程中,我们需要将它指向另外一个函数(需要注意的是,在当前标准的 JVMTI 之下,除了 native 函数名,其他的 signature 需要一致)。比如我们的 Java 代码是: + +``` + package nativeTester; + class nativePrefixTester{ + … + native int nativeMethod(int input); + … + } +``` + +那么我们已经实现的本地代码是 : + +``` + jint Java_nativeTester_nativeMethod(jclass thiz, jobject thisObj, jint input); +``` + +现在我们需要在调用这个函数时,使之指向另外一个函数。那么按照 J2SE 的做法,我们可以按他的命名方式,加上一个 prefix 作为新的函数名。比如,我们以 "another_" 作为 prefix,那么我们新的函数是 : + +``` + jint Java_nativeTester_another_nativePrefixTester(jclass thiz, jobject thisObj, + jint input); +``` + +然后将之编入动态链接库之中。 + +现在我们已经有了新的本地函数,接下来就是做 instrument 的设置。正如以上所说的,我们可以使用 premain 方式,在虚拟机启动之时就载入 premain 完成 instrument 代理设置。也可以使用 agentmain 方式,去 attach 虚拟机来启动代理。而设置 native 函数的也是相当简单的 : + +``` + premain(){ // 或者也可以在 agentmain 里 +… + if (!isNativeMethodPrefixSupported()){ + return; // 如果无法设置,则返回 + } + setNativeMethodPrefix(transformer,"another_"); // 设置 native 函数的 prefix,注意这个下划线必须由用户自己规定 +… + } +``` + +在这里要注意两个问题。一是不是在任何的情况下都是可以设置 native 函数的 prefix 的。首先,我们要注意到 agent 包之中的 Manifest 所设定的特性 : + +``` + Can-Set-Native-Method-Prefix +``` + +要注意,这一个参数都可以影响是否可以设置 native prefix,而且,在默认的设置之中,这个参数是 false 的,我们需要将之设置成 true(顺便说一句,对 Manifest 之中的属性来说都是大小写无关的,当然,如果给一个不是“true”的值,就会被当作 false 值处理)。 + +当然,我们还需要确认虚拟机本身是否支持 setNativePrefix。在 Java API 里,Instrumentation 类提供了一个函数 isNativePrefix,通过这个函数我们可以知道该功能是否可以实行。 + +二是我们可以为每一个 ClassTransformer 加上它自己的 nativeprefix;同时,每一个 ClassTransformer 都可以为同一个 class 做 transform,因此对于一个 Class 来说,一个 native 函数可能有不同的 prefix,因此对这个函数来说,它可能也有好几种解析方式。 + +在 Java SE 6 当中,Native prefix 的解释方式如下:对于某一个 package 内的一个 class 当中的一个 native method 来说,首先,假设我们对这个函数的 transformer 设置了 native 的 prefix“another”,它将这个函数接口解释成 : + +由 Java 的函数接口 + +``` + native void method() +``` + +和上述 prefix"another",去寻找本地代码中的函数 + +``` + void Java_package_class_another_method(jclass theClass, jobject thiz); + // 请注意 prefix 在函数名中出现的位置! +``` + +一旦可以找到,那么调用这个函数,整个解析过程就结束了;如果没有找到,那么虚拟机将会做进一步的解析工作。我们将利用 Java native 接口最基本的解析方式 , 去找本地代码中的函数 : + +``` + void Java_package_class_method(jclass theClass, jobject thiz); +``` + +如果找到,则执行之。否则,因为没有任何一个合适的解析方式,于是宣告这个过程失败。 + +那么如果有多个 transformer,同时每一个都有自己的 prefix,又该如何解析呢?事实上,虚拟机是按 transformer 被加入到的 Instrumentation 之中的次序去解析的(还记得我们最基本的 addTransformer 方法吗?)。 + +假设我们有三个 transformer 要被加入进来,他们的次序和相对应的 prefix 分别为:transformer1 和“prefix1_”,transformer2 和 “prefix2_”,transformer3 和 “prefix3_”。那么,虚拟机会首先做的就是将接口解析为 : + +``` + native void prefix1_prefix2_prefix3_native_method() +``` + +然后去找它相对应的 native 代码。 + +但是如果第二个 transformer(transformer2)没有设定 prefix,那么很简单,我们得到的解析是: + +``` + native void prefix1_prefix3_native_method() +``` + +这个方式简单而自然。 + +当然,对于多个 prefix 的情况,我们还要注意一些复杂的情况。比如,假设我们有一个 native 函数接口是: + +``` + native void native_method() +``` + +然后我们为它设置了两个 prefix,比如 "wrapped_" 和 "wrapped2_",那么,我们得到的是什么呢?是 + +``` + void Java_package_class_wrapped_wrapped2_method(jclass theClass, jobject thiz); + // 这个函数名正确吗? +``` + +吗?答案是否定的,因为事实上,对 Java 中 native 函数的接口到 native 中的映射,有一系列的规定,因此可能有一些特殊的字符要被代入。而实际中,这个函数的正确的函数名是: + +``` + void Java_package_class_wrapped_1wrapped2_1method(jclass theClass, jobject thiz); + // 只有这个函数名会被找到 +``` + +很有趣不是吗?因此如果我们要做类似的工作,一个很好的建议是首先在 Java 中写一个带 prefix 的 native 接口,用 javah 工具生成一个 c 的 header-file,看看它实际解析得到的函数名是什么,这样我们就可以避免一些不必要的麻烦。 + +另外一个事实是,与我们的想像不同,对于两个或者两个以上的 prefix,虚拟机并不做更多的解析;它不会试图去掉某一个 prefix,再来组装函数接口。它做且仅作两次解析。 + +总之,新的 native 的 prefix-instrumentation 的方式,改变了以前 Java 中 native 代码无法动态改变的缺点。在当前,利用 JNI 来写 native 代码也是 Java 应用中非常重要的一个环节,因此它的动态化意味着整个 Java 都可以动态改变了 —— 现在我们的代码可以利用加上 prefix 来动态改变 native 函数的指向,正如上面所说的,如果找不到,虚拟机还会去尝试做标准的解析,这让我们拥有了动态地替换 native 代码的方式,我们可以将许多带不同 prefix 的函数编译在一个动态链接库之中,而通过 instrument 包的功能,让 native 函数和 Java 函数一样动态改变、动态替换。 + +当然,现在的 native 的 instrumentation 还有一些限制条件,比如,不同的 transformer 会有自己的 native prefix,就是说,每一个 transformer 会负责他所替换的所有类而不是特定类的 prefix —— 因此这个粒度可能不够精确。 + + + +## Java SE 6 新特性:BootClassPath / SystemClassPath 的动态增补 + +我们知道,通过设置系统参数或者通过虚拟机启动参数,我们可以设置一个虚拟机运行时的 boot class 加载路径(-Xbootclasspath)和 system class(-cp)加载路径。当然,我们在运行之后无法替换它。然而,我们也许有时候要需要把某些 jar 加载到 bootclasspath 之中,而我们无法应用上述两个方法;或者我们需要在虚拟机启动之后来加载某些 jar 进入 bootclasspath。在 Java SE 6 之中,我们可以做到这一点了。 + +实现这几点很简单,首先,我们依然需要确认虚拟机已经支持这个功能,然后在 premain/agantmain 之中加上需要的 classpath。我们可以在我们的 Transformer 里使用 appendToBootstrapClassLoaderSearch/appendToSystemClassLoaderSearch 来完成这个任务。 + +同时我们可以注意到,在 agent 的 manifest 里加入 Boot-Class-Path 其实一样可以在动态地载入 agent 的同时加入自己的 boot class 路径,当然,在 Java code 中它可以更加动态方便和智能地完成 —— 我们可以很方便地加入判断和选择成分。 + +在这里我们也需要注意几点。首先,我们加入到 classpath 的 jar 文件中不应当带有任何和系统的 instrumentation 有关的系统同名类,不然,一切都陷入不可预料之中 —— 这不是一个工程师想要得到的结果,不是吗? + +其次,我们要注意到虚拟机的 ClassLoader 的工作方式,它会记载解析结果。比如,我们曾经要求读入某个类 someclass,但是失败了,ClassLoader 会记得这一点。即使我们在后面动态地加入了某一个 jar,含有这个类,ClassLoader 依然会认为我们无法解析这个类,与上次出错的相同的错误会被报告。 + +再次我们知道在 Java 语言中有一个系统参数“java.class.path”,这个 property 里面记录了我们当前的 classpath,但是,我们使用这两个函数,虽然真正地改变了实际的 classpath,却不会对这个 property 本身产生任何影响。 + +在公开的 JavaDoc 中我们可以发现一个很有意思的事情,Sun 的设计师们告诉我们,这个功能事实上依赖于 ClassLoader 的 appendtoClassPathForInstrumentation 方法 —— 这是一个非公开的函数,因此我们不建议直接(使用反射等方式)使用它,事实上,instrument 包里的这两个函数已经可以很好的解决我们的问题了。 + + + +从以上的介绍我们可以得出结论,在 Java SE 6 里面,instrumentation 包新增的功能 —— 虚拟机启动后的动态 instrument、本地代码(native code)instrumentation,以及动态添加 classpath 等等,使得 Java 具有了更强的动态控制、解释能力,从而让 Java 语言变得更加灵活多变。 + +这些能力,从某种意义上开始改变 Java 语言本身。在过去很长的一段时间内,动态 脚本语言的大量涌现和快速发展,对整个软件业和网络业提高生产率起到了非常重要的作用。在这种背景之下,Java 也正在慢慢地作出改变。而 Instrument 的新功能和 Script 平台(本系列的后面一篇中将介绍到这一点)的出现,则大大强化了语言的动态化和与动态语言融合,它是 Java 的发展的值得考量的新趋势。 + +## MANIFEST.MF属性 + +为代理JAR文件定义了以下清单属性: + +> - `Premain-Class` +> +> 在JVM启动时指定代理程序时,此属性指定代理程序类。也就是说,包含该`premain`方法的类。在JVM启动时指定代理程序时,此属性是必需的。如果该属性不存在,则JVM将中止。注意:这是类名,而不是文件名或路径。 +> +> - `Agent-Class` +> +> 如果实现支持在VM启动后的某个时间启动代理的机制,则此属性指定代理类。也就是说,包含该`agentmain`方法的类。此属性是必需的,如果不存在,则不会启动代理。注意:这是类名,而不是文件名或路径。 +> +> - `Boot-Class-Path` +> +> 引导类加载器要搜索的路径列表。路径表示目录或库(在许多平台上通常称为JAR或zip库)。在定位类的平台特定机制失败之后,引导类加载器搜索这些路径。按列出的顺序搜索路径。列表中的路径由一个或多个空格分隔。路径采用分层URI的路径组件的语法。如果它以斜杠字符('/')开头,则该路径是绝对的,否则它是相对的。根据代理JAR文件的绝对路径解析相对路径。格式错误且不存在的路径将被忽略。在VM启动后的某个时间启动代理程序时,将忽略不代表JAR文件的路径。此属性是可选的。在VM启动后的某个时间启动代理程序时,将忽略不代表JAR文件的路径。此属性是可选的。 +> +> - `Can-Redefine-Classes` +> +> 布尔(`true`或`false`)。是否能够重新定义此代理所需的类。此属性是可选的,默认为`false`。 +> +> - `Can-Retransform-Classes` +> +> 布尔(`true`或`false`)。是否能够重新转换此代理所需的类。此属性是可选的,默认为`false`。 +> +> - `Can-Set-Native-Method-Prefix` +> +> 布尔(`true`或`false`)。是否能够设置此代理所需的本机方法前缀。此属性是可选的,默认为`false`。 + +代理JAR文件可以 在清单中同时包含`Premain-Class`和`Agent-Class`属性。使用该`-javaagent`选项在命令行上启动代理程序时,该`Premain-Class`属性指定代理程序类的名称,并`Agent-Class`忽略该属性。同样,如果代理在VM启动后的某个时间启动,则该`Agent-Class`属性指定代理类的名称(`Premain-Class`忽略属性的值)。 + diff --git "a/java/javassist\344\275\277\347\224\250\346\214\207\345\215\227.md" "b/java/javassist\344\275\277\347\224\250\346\214\207\345\215\227.md" new file mode 100644 index 0000000..ec09307 --- /dev/null +++ "b/java/javassist\344\275\277\347\224\250\346\214\207\345\215\227.md" @@ -0,0 +1,1516 @@ +## 1. 读写字节码 + +我们知道 Java 字节码以二进制的形式存储在 class 文件中,每一个 class 文件包含一个 Java 类或接口。Javaassist 就是一个用来处理 Java 字节码的类库。 + +在 Javassist 中,类 `Javaassit.CtClass` 表示 class 文件。一个 GtClass (编译时类)对象可以处理一个 class 文件,下面是一个简单的例子: + +```java +ClassPool pool = ClassPool.getDefault(); +CtClass cc = pool.get("test.Rectangle"); +cc.setSuperclass(pool.get("test.Point")); +cc.writeFile(); +``` + +这段代码首先获取一个 ClassPool 对象。ClassPool 是 CtClass 对象的容器。它按需读取类文件来构造 CtClass 对象,并且保存 CtClass 对象以便以后使用。 + +为了修改类的定义,首先需要使用 ClassPool.get() 方法来从 ClassPool 中获得一个 CtClass 对象。上面的代码中,我们从 ClassPool 中获得了代表 test.Rectangle 类的 CtClass 对象的引用,并将其赋值给变量 cc。使用 getDefault() 方法获取的 ClassPool 对象使用的是默认系统的类搜索路径。 + +从实现的角度来看,ClassPool 是一个存储 CtClass 的 Hash 表,类的名称作为 Hash 表的 key。ClassPool 的 get() 函数用于从 Hash 表中查找 key 对应的 CtClass 对象。如果没有找到,get() 函数会创建并返回一个新的 CtClass 对象,这个新对象会保存在 Hash 表中。 + +从 ClassPool 中获取的 CtClass 是可以被修改的(稍后会讨论细节)。 + +在上面的例子中,test.Rectangle 的父类被设置为 test.Point。调用 writeFile() 后,这项修改会被写入原始类文件。writeFile() 会将 CtClass 对象转换成类文件并写到本地磁盘。也可以使用 toBytecode() 函数来获取修改过的字节码: + +```java +byte[] b = cc.toBytecode(); +``` + +你也可以通过 toClass() 函数直接将 CtClass 转换成 Class 对象: + +```java +Class clazz = cc.toClass(); +``` + +toClass() 请求当前线程的 ClassLoader 加载 CtClass 所代表的类文件。它返回此类文件的 java.lang.Class 对象,更多细节,请参考[下面的章节](https://github.com/jboss-javassist/javassist/wiki/Tutorial-1#toclass)。 + +定义新类 + +使用 ClassPool 的 makeClass() 方法可以定义一个新类。 + +```java +ClassPool pool = ClassPool.getDefault(); +CtClass cc = pool.makeClass("Point"); +``` + +这段代码定义了一个空的 Point 类。Point 类的成员方法可以通过 CtNewMethod 类的工厂方法来创建,然后使用 CtClass 的 addMethod() 方法将其添加到 Point 中。 + +使用 ClassPool 中的 makeInterface() 方法可以创建新接口。接口中的方法可以使用 CtNewMethod 的 abstractMethod() 方法创建。 + +将类冻结 + +如果一个 CtClass 对象通过 writeFile(), toClass(), toBytecode() 被转换成一个类文件,此 CtClass 对象会被冻结起来,不允许再修改。因为一个类只能被 JVM 加载一次。 + +但是,一个冷冻的 CtClass 也可以被解冻,例如: + +```java +CtClasss cc = ...; + : +cc.writeFile(); +cc.defrost(); +cc.setSuperclass(...); // 因为类已经被解冻,所以这里可以调用成功 +``` + +调用 defrost() 之后,此 CtClass 对象又可以被修改了。 + +如果 ClassPool.doPruning 被设置为 true,Javassist 在冻结 CtClass 时,会修剪 CtClass 的数据结构。为了减少内存的消耗,修剪操作会丢弃 CtClass 对象中不必要的属性。例如,Code_attribute 结构会被丢弃。一个 CtClass 对象被修改之后,方法的字节码是不可访问的,但是方法名称、方法签名、注解信息可以被访问。修剪过的 CtClass 对象不能再次被解冻。ClassPool.doPruning 的默认值为 false。 + +stopPruning() 可以用来驳回修剪操作。 + +```java +CtClasss cc = ...; +cc.stopPruning(true); + : +cc.writeFile(); // 转换成一个 class 文件 +// cc is not pruned. +``` + +这个 CtClass 没有被修剪,所以在 writeFile() 之后,可以被解冻。 + +**注意**:调试的时候,你可能临时需要停止修剪和冻结,然后保存一个修改过的类文件到磁盘,debugWriteFile() 方法正是为此准备的。它停止修剪,然后写类文件,然后解冻并再次打开修剪(如果开始时修养是打开的)。 + +类搜索路径 + +通过 ClassPool.getDefault() 获取的 ClassPool 使用 JVM 的类搜索路径。如果程序运行在 JBoss 或者 Tomcat 等 Web 服务器上,ClassPool 可能无法找到用户的类,因为 Web 服务器使用多个类加载器作为系统类加载器。在这种情况下,ClassPool 必须添加额外的类搜索路径。 + +下面的例子中,pool 代表一个 ClassPool 对象: + +```java +pool.insertClassPath(new ClassClassPath(this.getClass())); +``` + +上面的语句将 this 指向的类添加到 pool 的类加载路径中。你可以使用任意 Class 对象来代替 this.getClass(),从而将 Class 对象添加到类加载路径中。 + +也可以注册一个目录作为类搜索路径。下面的例子将 /usr/local/javalib 添加到类搜索路径中: + +```java +ClassPool pool = ClassPool.getDefault(); +pool.insertClassPath("/usr/local/javalib"); +``` + +类搜索路径不但可以是目录,还可以是 URL : + +```java +ClassPool pool = ClassPool.getDefault(); +ClassPath cp = new URLClassPath("www.javassist.org", 80, "/java/", "org.javassist."); +pool.insertClassPath(cp); +``` + +上述代码将 [http://www.javassist.org:80/java/](http://www.javassist.org/java/) 添加到类搜索路径。并且这个URL只能搜索 `org.javassist` 包里面的类。例如,为了加载 `org.javassist.test.Main`,它的类文件会从获取 [http://www.javassist.org:80/java/org/javassist/test/Main.class](http://www.javassist.org/java/org/javassist/test/Main.class) 获取。 + +此外,也可以直接传递一个 byte 数组给 ClassPool 来构造一个 CtClass 对象,完成这项操作,需要使用 ByteArrayPath 类。示例: + +```java +ClassPool cp = ClassPool.getDefault(); +byte[] b = a byte array; +String name = class name; +cp.insertClassPath(new ByteArrayClassPath(name, b)); +CtClass cc = cp.get(name); +``` + +示例中的 CtClass 对象表示 b 代表的 class 文件。将对应的类名传递给 ClassPool 的 get() 方法,就可以从 ByteArrayClassPath 中读取到对应的类文件。 + +如果你不知道类的全名,可以使用 makeClass() 方法: + +```java +ClassPool cp = ClassPool.getDefault(); +InputStream ins = an input stream for reading a class file; +CtClass cc = cp.makeClass(ins); +``` + +makeClass() 返回从给定输入流构造的 CtClass 对象。 你可以使用 makeClass() 将类文件提供给 ClassPool 对象。如果搜索路径包含大的 jar 文件,这可能会提高性能。由于 ClassPool 对象按需读取类文件,它可能会重复搜索整个 jar 文件中的每个类文件。 makeClass() 可以用于优化此搜索。由 makeClass() 构造的 CtClass 保存在 ClassPool 对象中,从而使得类文件不会再被读取。 + +用户可以通过实现 ClassPath 接口来扩展类加载路径,然后调用 ClassPool 的 insertClassPath() 方法将路径添加进来。这种技术主要用于将非标准资源添加到类搜索路径中。 + +## 2. ClassPool + +ClassPool 是 CtClass 对象的容器。因为编译器在编译引用 CtClass 代表的 Java 类的源代码时,可能会引用 CtClass 对象,所以一旦一个 CtClass 被创建,它就被保存在 ClassPool 中. + +例如,一个 CtClass 类代表 Point 类,并给 CtClass 添加 getter() 方法。然后,程序尝试编译一段代码,代码中包含了 Point 的 getter() 调用,然后将这段代码添加了另一个类 Line 中,如果代表 Point 的 CtClass 丢失,编译器就无法编译 Line 中的 Point.getter() 方法。注:原来的 Point 类中无 getter() 方法。因此,为了能够正确编译这个方法调用,ClassPool 必须在程序执行期间包含所有的 CtClass 实例。 + +避免内存溢出 + +如果 CtClass 对象的数量变得非常大(这种情况很少发生,因为 Javassist 试图以各种方式减少内存消耗),ClassPool 可能会导致巨大的内存消耗。 为了避免此问题,可以从 ClassPool 中显式删除不必要的 CtClass 对象。 如果对 CtClass 对象调用 detach(),那么该 CtClass 对象将被从 ClassPool 中删除。 例如: + +```java +CtClass cc = ... ; +cc.writeFile(); +cc.detach(); +``` + +在调用 detach() 之后,就不能调用这个 CtClass 对象的任何方法了。但是如果你调用 ClassPool 的 get() 方法,ClassPool 会再次读取这个类文件,创建一个新的 CtClass 对象。 + +另一个办法是用新的 ClassPool 替换旧的 ClassPool,并将旧的 ClassPool 丢弃。 如果旧的 ClassPool 被垃圾回收掉,那么包含在 ClassPool 中的 CtClass 对象也会被回收。要创建一个新的 ClassPool,参见以下代码: + +```java +ClassPool cp = new ClassPool(true); +// if needed, append an extra search path by appendClassPath() +``` + +这段代码创建了一个 ClassPool 对象,它的行为与 ClassPool.getDefault() 类似。 请注意,ClassPool.getDefault() 是为了方便而提供的单例工厂方法,它保留了一个`ClassPool`的单例并重用它。getDefault() 返回的 ClassPool 对象并没有特殊之处。 + +**注意**:new ClassPool(true) 构造一个 ClassPool 对象,并附加了系统搜索路径。 +调用此构造函数等效于以下代码: + +```java +ClassPool cp = new ClassPool(); +cp.appendSystemPath(); // or append another path by appendClassPath() +``` + +级联的 ClassPools + +如果程序正在 Web 应用程序服务器上运行,则可能需要创建多个 ClassPool 实例; 应为每一个 ClassLoader 创建一个 ClassPool 的实例。 程序应该通过 ClassPool 的构造函数,而不是调用 getDefault() 来创建一个 ClassPool 对象。 +多个 ClassPool 对象可以像 java.lang.ClassLoader 一样级联。 例如, + +```java +ClassPool parent = ClassPool.getDefault(); +ClassPool child = new ClassPool(parent); +child.insertClassPath("./classes"); +``` + +如果调用 child.get(),子 ClassPool 首先委托给父 ClassPool。如果父 ClassPool 找不到类文件,那么子 ClassPool 会尝试在 ./classes 目录下查找类文件。 + +如果 child.childFirstLookup 返回 true,那么子类 ClassPool 会在委托给父 ClassPool 之前尝试查找类文件。 例如: + +```java +ClassPool parent = ClassPool.getDefault(); +ClassPool child = new ClassPool(parent); +child.appendSystemPath(); // the same class path as the default one. +child.childFirstLookup = true; // changes the behavior of the child. +``` + +拷贝一个已经存在的类来定义一个新的类 + +```java +ClassPool pool = ClassPool.getDefault(); +CtClass cc = pool.get("Point"); +cc.setName("Pair"); +``` + +这个程序首先获得类 Point 的 CtClass 对象。然后它调用 setName() 将这个 CtClass 对象的名称设置为 Pair。在这个调用之后,这个 CtClass 对象所代表的类的名称 Point 被修改为 Pair。类定义的其他部分不会改变。 + +**注意**:CtClass 中的 setName() 改变了 ClassPool 中的记录。从实现的角度来看,一个 ClassPool 对象是一个 CtClass 对象的哈希表。setName() 更改了与哈希表中的 CtClass 对象相关联的 Key。Key 从原始类名更改为新类名。 + +因此,如果后续在 ClassPool 对象上再次调用 get("Point"),则它不会返回变量 cc 所指的 CtClass 对象。 而是再次读取类文件 Point.class,并为类 Point 构造一个新的 CtClass 对象。 因为与 Point 相关联的 CtClass 对象不再存在。示例: + +```java +ClassPool pool = ClassPool.getDefault(); +CtClass cc = pool.get("Point"); +CtClass cc1 = pool.get("Point"); // cc1 is identical to cc. +cc.setName("Pair"); +CtClass cc2 = pool.get("Pair"); // cc2 is identical to cc. +CtClass cc3 = pool.get("Point"); // cc3 is not identical to cc. +``` + +cc1 和 cc2 指向 CtClass 的同一个实例,而 cc3 不是。 注意,在执行 cc.setName("Pair") 之后,cc 和 cc1 引用的 CtClass 对象都表示 Pair 类。 + +ClassPool 对象用于维护类和 CtClass 对象之间的一对一映射关系。 为了保证程序的一致性,Javassist 不允许用两个不同的 CtClass 对象来表示同一个类,除非创建了两个独立的 ClassPool。 + +如果你有两个 ClassPool 对象,那么你可以从每个 ClassPool 中,获取一个表示相同类文件的不同的 CtClass 对象。 你可以修改这些 CtClass 对象来生成不同版本的类。 + +通过重命名冻结的类来生成新的类 + +一旦一个 CtClass 对象被 writeFile() 或 toBytecode() 转换为一个类文件,Javassist 会拒绝对该 CtClass 对象的进一步修改。因此,在表示 Point 类的 CtClass 对象被转换为类文件之后,你不能将 Pair 类定义为 Point 的副本,因为在 Point 上执行 setName() 会被拒绝。 以下代码段是错误的: + +```java +ClassPool pool = ClassPool.getDefault(); +CtClass cc = pool.get("Point"); +cc.writeFile(); +cc.setName("Pair"); // wrong since writeFile() has been called. +``` + +为了避免这种限制,你应该在 ClassPool 中调用 getAndRename() 方法。 例如: + +```java +ClassPool pool = ClassPool.getDefault(); +CtClass cc = pool.get("Point"); +cc.writeFile(); +CtClass cc2 = pool.getAndRename("Point", "Pair"); +``` + +如果调用 getAndRename(),ClassPool 首先读取 Point.class 来创建一个新的表示 Point 类的 CtClass 对象。 而且,它会在这个 CtClass 被记录到哈希表之前,将 CtClass 对象重命名为 Pair。因此,getAndRename() 可以在表示 Point 类的 CtClass 对象上调用 writeFile() 或 toBytecode() 后执行。 + +## 3. 类加载器 (Class Loader) + +如果事先知道要修改哪些类,修改类的最简单方法如下: + +1. 调用 ClassPool.get() 获取 CtClass 对象, +2. 修改 CtClass +3. 调用 CtClass 对象的 writeFile() 或者 toBytecode() 获得修改过的类文件。 + +如果在加载时,可以确定是否要修改某个类,用户必须使 Javassist 与类加载器协作,以便在加载时修改字节码。用户可以定义自己的类加载器,也可以使用 Javassist 提供的类加载器。 + +### 3.1 CtClass.toClass() + +CtClass 的 toClass() 方法请求当前线程的上下文类加载器,加载 CtClass 对象所表示的类。要调用此方法,调用者必须具有相关的权限; 否则,可能会抛出 SecurityException。示例: + +```java +public class Hello { + public void say() { + System.out.println("Hello"); + } +} + +public class Test { + public static void main(String[] args) throws Exception { + ClassPool cp = ClassPool.getDefault(); + CtClass cc = cp.get("Hello"); + CtMethod m = cc.getDeclaredMethod("say"); + m.insertBefore("{ System.out.println(\"Hello.say():\"); }"); + Class c = cc.toClass(); + Hello h = (Hello)c.newInstance(); + h.say(); + } +} +``` + +Test.main() 在 Hello 中的 say() 方法体中插入一个 println()。然后它构造一个修改过的 Hello 类的实例,并在该实例上调用 say() 。 +**注意**:上面的程序要正常运行,Hello 类在调用 toClass() 之前不能被加载。 如果 JVM 在 toClass() 调用之前加载了原始的 Hello 类,后续加载修改的 Hello 类将会失败(LinkageError 抛出)。 +例如,如果 Test 中的 main() 是这样的: + +```java +public static void main(String[] args) throws Exception { + Hello orig = new Hello(); + ClassPool cp = ClassPool.getDefault(); + CtClass cc = cp.get("Hello"); + : +} +``` + +那么,原始的 Hello 类在 main 的第一行被加载,toClass() 调用会抛出一个异常,因为类加载器不能同时加载两个不同版本的 Hello 类。 + +如果程序在某些应用程序服务器(如JBoss和Tomcat)上运行,toClass() 使用的上下文类加载器可能是不合适的。在这种情况下,你会看到一个意想不到的 ClassCastException。为了避免这个异常,必须给 toClass() 指定一个合适的类加载器。 例如,如果 'bean' 是你的会话 bean 对象,那么下面的代码: + +```java +CtClass cc = ...; +Class c = cc.toClass(bean.getClass().getClassLoader()); +``` + +可以工作。你应该给 toClass() 传递加载了你的程序的类加载器(上例中,`bean`对象的类)。 + +toClass() 是为了简便而提供的方法。如果你需要更复杂的功能,你应该编写自己的类加载器。 + +### 3.2 Java的类加载机制 + +在Java中,多个类加载器可以共存,每个类加载器创建自己的名称空间。不同的类加载器可以加载具有相同类名的不同类文件。加载的两个类被视为不同的类。此功能使我们能够在单个 JVM 上运行多个应用程序,即使这些程序包含具有相同名称的不同的类。 + +**注意**:JVM 不允许动态重新加载类。一旦类加载器加载了一个类,它不能在运行时重新加载该类的修改版本。因此,在JVM 加载类之后,你不能更改类的定义。但是,JPDA(Java平台调试器架构)提供有限的重新加载类的能力。参见[3.6节](https://blog.csdn.net/21aspnet/article/details/81671777#hotswap)。 + +如果相同的类文件由两个不同的类加载器加载,则 JVM 会创建两个具有相同名称和定义的不同的类。由于两个类不相同,一个类的实例不能被分配给另一个类的变量。两个类之间的转换操作将失败并抛出一个 ClassCastException。 +例如,下面的代码会抛出异常: + +```java +MyClassLoader myLoader = new MyClassLoader(); +Class clazz = myLoader.loadClass("Box"); +Object obj = clazz.newInstance(); +Box b = (Box)obj; // this always throws ClassCastException. +``` + +Box 类由两个类加载器加载。假设类加载器 CL 加载包含此代码片段的类。因为这段代码引用了 MyClassLoader,Class,Object 和 Box,CL 也加载这些类(除非它委托给另一个类加载器)。 因此,变量 b 的类型是 CL 加载的 Box 类。 另一方面, myLoader 也加载了 Box class。 对象 obj 是由 myLoader 加载的 Box 类的一个实例。 因此,最后一个语句总是抛出 ClassCastException ,因为 obj 的类是一个不同的 Box 类的类型,而不是用作变量 b 的类型。 + +多个类加载器形成一个树型结构。 除引导类加载器之外的每个类加载器,都有一个父类加载器,它通常加载该子类加载器的类。 因为加载类的请求可以沿类加载器的这个层次委派,所以即使你没有请求加载一个类,它也可能被加载。因此,已经请求加载类 C 的类加载器可以不同于实际加载类 C 的加载器。为了区分,我们将前加载器称为 C 的发起者,将后加载器称为 C 的实际加载器 。 + +此外,如果请求加载类 C(C的发起者)的类加载器 CL 委托给父类加载器 PL,则类加载器 CL 不会加载类 C 引用的任何类。因为 CL 不是那些类的发起者。 相反,父类加载器 PL 成为它们的启动器,并且加载它们。 + +请参考下面的例子来理解: + +```java +public class Point { // loaded by PL + private int x, y; + public int getX() { return x; } + : +} + +public class Box { // the initiator is L but the real loader is PL + private Point upperLeft, size; + public int getBaseX() { return upperLeft.x; } + : +} + +public class Window { // loaded by a class loader L + private Box box; + public int getBaseX() { return box.getBaseX(); } +} +``` + +假设一个类 Window 由类加载器 L 加载。Window 的启动器和实际加载器都是 L。由于 Window 的定义引用了 Box,JVM 将请求 L 加载 Box。 这里,假设 L 将该任务委托给父类加载器 PL。Box 的启动器是 L,但真正的加载器是 PL。 在这种情况下,Point 的启动器不是 L 而是 PL,因为它与 Box 的实际加载器相同。 因此,Point 不会被 L 加载。 + +接下来,看一个稍微修改过的例子: + +```java +public class Point { + private int x, y; + public int getX() { return x; } + : +} + +public class Box { // the initiator is L but the real loader is PL + private Point upperLeft, size; + public Point getSize() { return size; } + : +} + +public class Window { // loaded by a class loader L + private Box box; + public boolean widthIs(int w) { + Point p = box.getSize(); + return w == p.getX(); + } +} +``` + +现在,Window 的定义也引用了 Point。 在这种情况下,如果请求加载 Point,类加载器 L 也必须委托给 PL。 **你必须避免有两个类加载器两次加载同一个类**。两个加载器之一必须委托给另一个。 + +当 Point 加载时,如果 L 不委托给 PL,widthIs() 就会抛出一个 ClassCastException 异常。因为 Box 的实际加载器是 PL,在 Box 中引用的 Point 也由 PL 加载。 getSize() 的结果值是由 PL 加载的 Point,widthIs() 中的变量 p 是由 L 加载的 Point。JVM 认为它们是不同的类型,因此它会抛出类型不匹配的异常。 + +这种设计有点不方便,但也是必须的。 + +```java +Point p = box.getSize(); +``` + +如果上面的语句没有抛出异常,那么 Window 的程序员可以破坏 Point 对象的封装。 例如,字段 x 在 PL 中加载的 Point 中是私有的。 然而,如果 L 加载具有以下定义的 Point,则 Window 类可以直接访问 x 的值: + +```java +public class Point { + public int x, y; // not private + public int getX() { return x; } + : +} +``` + +有关 Java 类加载器的更多详细信息,可以参看以下文章: + +> Sheng Liang 和 Gilad Bracha,“Dynamic Class Loading in the Java Virtual Machine”,* ACM OOPSLA'98 *,pp.36-44,1998。 + +### 3.3 使用 javassist.Loader + +Javassit 提供一个类加载器 javassist.Loader。它使用 javassist.ClassPool 对象来读取类文件。 +例如,javassist.Loader 可以用于加载用 Javassist 修改过的类。 + +```java +import javassist.*; +import test.Rectangle; + +public class Main { + public static void main(String[] args) throws Throwable { + ClassPool pool = ClassPool.getDefault(); + Loader cl = new Loader(pool); + CtClass ct = pool.get("test.Rectangle"); + ct.setSuperclass(pool.get("test.Point")); + Class c = cl.loadClass("test.Rectangle"); + Object rect = c.newInstance(); + : + } +} +``` + +这个程序将 test.Rectangle 的超类设置为 test.Point。然后再加载修改的类,并创建新的 test.Rectangle 类的实例。 + +如果用户希望在加载时按需修改类,则可以向 javassist.Loader 添加事件监听器。当类加载器加载类时会通知监听器。事件监听器类必须实现以下接口: + +```java +public interface Translator { + public void start(ClassPool pool) + throws NotFoundException, CannotCompileException; + public void onLoad(ClassPool pool, String classname) + throws NotFoundException, CannotCompileException; +} +``` + +当事件监听器通过 addTranslator() 添加到 javassist.Loader 对象时,start() 方法会被调用。在 javassist.Loader 加载类之前,会调用 onLoad() 方法。可以在 onLoad() 方法中修改被加载的类的定义。 + +例如,下面的事件监听器在类加载之前,将所有类更改为 public 类。 + +```java +public class MyTranslator implements Translator { + void start(ClassPool pool) throws NotFoundException, CannotCompileException {} + void onLoad(ClassPool pool, String classname) throws NotFoundException, CannotCompileException { + CtClass cc = pool.get(classname); + cc.setModifiers(Modifier.PUBLIC); + } +} +``` + +注意,onLoad() 不必调用 toBytecode() 或 writeFile(),因为 javassist.Loader 会调用这些方法来获取类文件。 + +要使用 MyTranslator 对象运行一个应用程序类 MyApp,主类代码如下: + +```java +import javassist.*; + +public class Main2 { + public static void main(String[] args) throws Throwable { + Translator t = new MyTranslator(); + ClassPool pool = ClassPool.getDefault(); + Loader cl = new Loader(); + cl.addTranslator(pool, t); + cl.run("MyApp", args); + } +} +``` + +执行下面的命令来运行程序: + +```shell +% java Main2 arg1 arg2... +``` + +类 MyApp 和其他应用程序类会被 MyTranslator 监听。 + +注意,MyApp 不能访问 loader 类,如 Main2,MyTranslator 和 ClassPool,因为它们是由不同的加载器加载的。 应用程序类由 javassist.Loader 加载,而加载器类(例如 Main2)由默认的 Java 类加载器加载。 + +javassist.Loader 以不同的顺序从 java.lang.ClassLoader 中搜索类。ClassLoader 首先将加载操作委托给父类加载器,只有当父类加载器无法找到它们时才尝试自己加载类。另一方面,javassist.Loader 尝试在委托给父类加载器之前加载类。它仅在以下情况下进行委派: + +1. 在 ClassPool 对象上调用 get() 找不到这个类; +2. 这些类已经通过 delegateLoadingOf() 来指定由父类加载器加载。 + +此搜索顺序允许 Javassist 加载修改过的类。但是,如果找不到修改的类,它将委托父类加载器来加载。一旦一个类被父类加载器加载,那个类中引用的其他类也将被父类加载器加载,因此它们是没有被修改的。 回想一下,C 类引用的所有类都由 C 的实际加载器加载的。如果你的程序无法加载修改的类,你应该确保所有使用该类的类都是由 javassist 加载的。 + +### 3.4 自定义类加载器 + +下面看一个简单的带 Javassist 的类加载器: + +```java +import javassist.*; + +public class SampleLoader extends ClassLoader { + /* Call MyApp.main(). */ + public static void main(String[] args) throws Throwable { + SampleLoader s = new SampleLoader(); + Class c = s.loadClass("MyApp"); + c.getDeclaredMethod("main", new Class[] { String[].class }) + .invoke(null, new Object[] { args }); + } + + private ClassPool pool; + + public SampleLoader() throws NotFoundException { + pool = new ClassPool(); + pool.insertClassPath("./class"); // MyApp.class must be there. + } + /* + * Finds a specified class. + * The bytecode for that class can be modified. + */ + protected Class findClass(String name) throws ClassNotFoundException { + try { + CtClass cc = pool.get(name); + // *modify the CtClass object here* + byte[] b = cc.toBytecode(); + return defineClass(name, b, 0, b.length); + } catch (NotFoundException e) { + throw new ClassNotFoundException(); + } catch (IOException e) { + throw new ClassNotFoundException(); + } catch (CannotCompileException e) { + throw new ClassNotFoundException(); + } + } +} +``` + +MyApp 类是一个应用程序。 要执行此程序,首先将类文件放在 ./class 目录下,它不能包含在类搜索路径中。 否则,MyApp.class 将由默认系统类加载器加载,它是 SampleLoader 的父加载器。目录名 ./class 由构造函数中的 insertClassPath() 指定。然后运行: + +```shell +% java SampleLoader +``` + +类加载器会加载类 MyApp (./class/MyApp.class),并使用命令行参数调用 MyApp.main()。 + +这是使用 Javassist 的最简单的方法。 但是,如果你编写一个更复杂的类加载器,你可能需要更详细地了解 Java 的类加载机制。 例如,上面的程序将 MyApp 类放在与 SampleLoader 类不同的命名空间中,因为这两个类由不同的类装载器加载。 因此,MyApp 类不能直接访问类 SampleLoader。 + +### 3.5 修改系统的类 + +像 java.lang.String 这样的系统类只能被系统类加载器加载。因此,上面的 SampleLoader 或 javassist.Loader 在加载时不能修改系统类。系统类必须被静态地修改。下面的程序向 java.lang.String 添加一个新字段 hiddenValue: + +```java +ClassPool pool = ClassPool.getDefault(); +CtClass cc = pool.get("java.lang.String"); +CtField f = new CtField(CtClass.intType, "hiddenValue", cc); +f.setModifiers(Modifier.PUBLIC); +cc.addField(f); +cc.writeFile("."); +``` + +这段程序生成一个新文件 ./java/lang/String.class + +可以使用 MyApp 这样测试修改过的 String 类: + +```shell +% java -Xbootclasspath/p:. MyApp arg1 arg2... +``` + +MyApp 的定义如下: + +```java +public class MyApp { + public static void main(String[] args) throws Exception { + System.out.println(String.class.getField("hiddenValue").getName()); + } +} +``` + +如果修改过的 String 类被加载,MyApp 会打印出 hiddenValue。 + +**注意**:如果应用使用此技术来覆盖 rt.jar 中的系统类,那么部署这个应用会违反 Java 2 运行时二进制代码许可协议。 + +### 3.6 在运行时重新加载类 + +如果 JVM 在启用 JPDA(Java平台调试器体系结构)的情况下启动,那么类可以被动态地重新加载。在 JVM 加载类之后,旧版本的类可以被卸载,新版本可以再次重新加载。也就是说,该类的定义可以在运行时动态被修改。然而,新的类定义必须与旧的类定义有些兼容。JVM 不允许两个版本之间的模式更改。它们必须具有相同的方法和字段。 + +Javassist 提供了一个方便的类,用于在运行时重新加载类。更多相关信息,请参阅javassist.tools.HotSwapper 的 API 文档。 + +## 4. 自省和自定制 (Introspection and customization) + +CtClass 提供了自省的方法。Javassist 的自省能力与 Java 反射 API 兼容。 CtClass 提供了 getName(),getSuperclass(),getMethods() 等方法来获取类的信息,也提供了修改类定义的方法(添加字段,添加构造函数、添加方法),同时也可以对方法体的语句进行检测。 + +方法由 CtMethod 对象表示。CtMethod 提供了几个函数来修改方法的定义。 +**注意**,如果一个方法继承自一个超类,那么表示继承方法的 CtMethod 对象,同样也表示该超类中声明的方法。 + +例如,如果类 Point 声明方法 move() , Point 的子类 ColorPoint 不覆盖 move() ,那么在 Point 中声明的 move 和 ColorPoint 的 move 具有相同的 CtMethod。 如果由这个 CtMethod 对象表示的方法定义被修改,那么修改将反映在这两种方法上。 如果你只想修改 ColorPoint 中的 move() 方法,你首先必须在 Point 中加入 CtMethod 对象的副本 move() 。CtMethod 对象的副本可以通过 CtNewMethod.copy() 获得。 + +Javassist 不允许删除方法或字段,但它允许更改名称。所以,如果一个方法是没有必要的,可以通过调用 CtMethod 的 setName() 和 setModifiers() 中将其改为一个私有方法。 + +Javassist 不允许向现有方法添加额外的参数。你可以通过新建一个方法达到同样的效果。 例如,如果你想为一个方法添加一个额外的 int 参数 newZ 到 Point 类, + +```java +void move(int newX, int newY) { x = newX; y = newY; } +``` + +你可以添加一个这样的方法到 Point 类: + +```java +void move(int newX, int newY, int newZ) { + // do what you want with newZ. + move(newX, newY); +} +``` + +Javassist 还提供了用于直接编辑原始类文件的低级API。 +例如,CtClass 中的 getClassFile() 返回一个表示类文件的 ClassFile 对象。CtMethod 的 getMethodInfo() 方法返回一个 MethodInfo 对象,表示类文件中的 method_info 结构。 低级API使用 Java 虚拟机规范中的词汇表。 用户必须具有类文件和字节码的知识。有关更多详细信息,请参考 [javassist.bytecode 包](https://link.jianshu.com/?t=tutorial3.html#intro)。 + +由 Javassist 修改的类文件只有在使用以 $ 开头的特殊标识符时才需要 javassist.runtime 包来提供运行时支持。接下来的内容会讨论这些特殊标识符。在没有这些特殊标识符的情况下,在运行时修改类文件不需要 javassist.runtime 包或任何其他 Javassist 包。有关更多详细信息,请参阅 javassist.runtime 包的API文档。 + +### 4.1 在方法体的开始/结尾处添加代码 + +CtMethod 和 CtConstructor 提供了 insertBefore(),insertAfter() 和 addCatch() 方法。 它们可以将用 Java 编写的代码片段插入到现有方法中。Javassist 包括一个用于处理源代码的简单编译器,它接收用 Java 编写的源代码,并将其编译成 Java 字节码,并内联方法体中。 + +也可以按行号来插入代码段(如果行号表包含在类文件中)。向 CtMethod 和 CtConstructor 中的 insertAt() 方法提供源代码和原始类定义中的源文件的行号,就可以将编译后的代码插入到指定行号位置。 + +方法 insertBefore() ,insertAfter(),addCatch() 和 insertAt() 接收一个表示语句或语句块的 String 对象。一个语句是一个单一的控制结构,比如 if 和 while 或者以分号结尾的表达式。语句块是一组用大括号 {} 包围的语句。因此,以下每行都是有效语句或块的示例: + +```java +System.out.println("Hello"); +{ System.out.println("Hello"); } +if (i < 0) { i = -i; } +``` + +语句和语句块可以引用字段和方法。如果使用 -g 选项(在类文件中包含局部变量属性)编译该方法,则它们还可以引用方法的参数。 否则,它们必须通过特殊变量 $0,$1,$2,... 来访问方法参数,下面会讨论。不允许访问在方法中声明的局部变量,尽管在块中声明一个新的局部变量是允许的。但是,insertAt() 允许语句和块访问局部变量,前提是这些变量在指定的行号处可用,并且目标方法是使用 -g 选项编译的。 + +传递给方法 insertBefore() ,insertAfter() ,addCatch() 和 insertAt() 的 String 对象是由Javassist 的编译器编译的。 由于编译器支持语言扩展,以 $ 开头的几个标识符有特殊的含义: + +| 符号 | 含义 | +| :-------------------- | :---------------------------------------------- | +| `$0`, `$1`, `$2`, ... | `this` and 方法的参数 | +| `$args` | 方法参数数组.它的类型为 `Object[]` | +| `$$` | 所有实参。例如, `m($$)` 等价于 `m($1,$2,`...`)` | +| `$cflow(`...`)` | `cflow` 变量 | +| `$r` | 返回结果的类型,用于强制类型转换 | +| `$w` | 包装器类型,用于强制类型转换 | +| `$_` | 返回值 | +| `$sig` | 类型为 java.lang.Class 的参数类型数组 | +| `$type` | 一个 java.lang.Class 对象,表示返回值类型 | +| `$class` | 一个 java.lang.Class 对象,表示当前正在修改的类 | + +$0, $1, $2, ... + +传递给目标方法的参数使用 $1,$2,... 访问,而不是原始的参数名称。 $1 表示第一个参数,$2 表示第二个参数,以此类推。 这些变量的类型与参数类型相同。 $0 等价于 `this` 指针。 如果方法是静态的,则 $0 不可用。 + +下面有一些使用这些特殊变量的例子。假设一个类 Point: + +```java +class Point { + int x, y; + void move(int dx, int dy) { x += dx; y += dy; } +} +``` + +要在调用方法 move() 时打印 dx 和 dy 的值,请执行以下程序: + +```java +ClassPool pool = ClassPool.getDefault(); +CtClass cc = pool.get("Point"); +CtMethod m = cc.getDeclaredMethod("move"); +m.insertBefore("{ System.out.println($1); System.out.println($2); }"); +cc.writeFile(); +``` + +请注意,传递给 insertBefore() 的源文本是用大括号 {} 括起来的。insertBefore() 只接受单个语句或用大括号括起来的语句块。 + +修改后的类 Point 的定义是这样的: + +```java +class Point { + int x, y; + void move(int dx, int dy) { + { System.out.println(dx); System.out.println(dy); } + x += dx; y += dy; + } +} +``` + +`$1` and `$2` are replaced with `dx` and `dy`, respectively. +`$1`, `$2`, `$3` ... are updatable. If a new value is assigend to one of those variables, then the value of the parameter represented by that variable is also updated. + +$1 和 $2 分别替换为 dx 和 dy。 +$1,$2,$3 ...是可更新的。如果这些变量被赋予新值,则由该变量表示的参数的值也将被更新。 + +$args + +变量 $args 表示所有参数的数组。该变量的类型是 Object 类的数组。如果参数类型是原始类型(如 int),则该参数值将转换为包装器对象(如java.lang.Integer)以存储在 $args 中。 因此,如果第一个参数的类型是不是原始类型,那么 $args[0] 等于 $1。注意 $args[0] 不等于 $0,因为 $0 表示 `this`。 + +如果 Object 的数组被分配给 $args,那么该数组的每个元素都被分配给每个参数。如果参数类型是基本类型,则相应元素的类型必须是包装类型。 在将值分配给参数之前,必须将该值从包装器类型转换为基本类型。 + +$$ + +变量 $$ 是所有参数列表的缩写,用逗号分隔。 例如,如果方法 move() 的有 3 个参数,则 + +``` +move($1, $2, $3) +``` + +如果 move() 不带任何参数,则 move( + +)等同于move()。)等同于move()。 + +可以与其他方法一起使用。 如果你写一个表达式: + +``` +exMove($$, context) +``` + +这个表达式等价于 + +``` +exMove($1, $2, $3, context) +``` + +注意,$$ 开启了通用符号方法调用,它通常与稍后要介绍的 $proceed 一起使用。 + +$cflow + +$ cflow表示 控制流。此只读变量返回特定方法的递归调用的深度。 + +假设下面所示的方法由CtMethod对象cm表示: + +```java +int fact(int n) { + if (n <= 1) + return n; + else + return n * fact(n - 1); +} +``` + +要使用 $cflow,首先声明使用 $cflow 监视方法 fact() 的调用: + +```java +CtMethod cm = ...; +cm.useCflow("fact"); +``` + +useCflow() 的参数是 $cflow 变量的标识符。任何有效的 Java 名称都可以用作标识符。标识符还可以包括 `.` ,例如,“my.Test.fact”是有效的标识符。 + +然后,$cflow(fact) 表示由 cm 指定的方法的递归调用深度。$cflow(fact) 的值在方法第一次调用时为 0,而当方法在方法中递归调用时为 1。 例如, + +```java +cm.insertBefore("if ($cflow(fact) == 0)" + + " System.out.println(\"fact \" + $1);"); +``` + +翻译方法fact(),以便它显示参数。因为检查了 $cflow(fact) 的值,所以如果在 fact() 中递归调用,则方法 fact() 不会显示参数。 + +$cflow 的值是当前线程的最顶层堆栈帧下与 cm 相关联的堆栈帧数。 $cflow 也可以不在 cm 方法中访问。 + +$r + +$r 表示方法的结果类型(返回类型)。它用在 cast 表达式中作 cast 转换类型。 下面是一个典型的用法: + +```java +Object result = ... ; +$_ = ($r)result; +``` + +如果结果类型是原始类型,则 ($r) 遵循特殊语义。 首先,如果 cast 表达式的操作数是原始类型,($r) 作为普通转换运算符。 另一方面,如果操作数是包装类型,($r) 将从包装类型转换为结果类型。 例如,如果结果类型是 int,那么 ($r) 将从 java.lang.Integer 转换为 int。 + +如果结果类型为void,那么 ($r) 不转换类型; 它什么也不做。 但是,如果操作数是对 void 方法的调用,则 ($r) 将导致 null。 例如,如果结果类型是 void,而 foo() 是一个 void 方法,那么 + +``` +$_ = ($r)foo(); +``` + +是一个正确的表达式。 + +cast 运算符 ($r) 在 return 语句中也很有用。 即使结果类型是 void,下面的 return 语句也是有效的: + +``` +return ($r)result; +``` + +这里,result是局部变量。 因为指定了 ($r),所以结果值被丢弃。此返回语句被等价于: + +``` +return; +``` + +$w + +$w 表示包装类型。它用在 cast 表达式中作 cast 转换类型。($w) 把基本类型转换为包装类型。 以下代码是一个示例: + +``` +Integer i = ($w)5; +``` + +包装后的类型取决于 ($w) 后面表达式的类型。如果表达式的类型为 double,则包装器类型为 java.lang.Double。 + +If the type of the expression following `($w)` is not a primitive type, then `($w)` does nothing. +如果下面的表达式 ($w) 的类型不是原始类型,那么($w) 什么也不做。 + +$_ + +CtMethod 中的 insertAfter() 和 CtConstructor 在方法的末尾插入编译的代码。传递给insertAfter() 的语句中,不但可以使用特殊符号如 $0,$1。也可以使用 $_ 来表示方法的结果值。 +该变量的类型是方法的结果类型(返回类型)。如果结果类型为 void,那么 $_ 的类型为Object,$_ 的值为 null。 +虽然由 insertAfter() 插入的编译代码通常在方法返回之前执行,但是当方法抛出异常时,它也可以执行。要在抛出异常时执行它,insertAfter() 的第二个参数 asFinally 必须为true。 +如果抛出异常,由 insertAfter() 插入的编译代码将作为 finally 子句执行。$_ 的值 0 或 null。在编译代码的执行终止后,最初抛出的异常被重新抛出给调用者。注意,$_ 的值不会被抛给调用者,它将被丢弃。 + +$sig + +$sig 的值是一个 java.lang.Class 对象的数组,表示声明的形式参数类型。 + +$type + +$type 的值是一个 java.lang.Class 对象,表示结果值的类型。 如果这是一个构造函数,此变量返回 Void.class。 + +$class + +$class 的值是一个 java.lang.Class 对象,表示编辑的方法所在的类。 即表示 $0 的类型。 + +addCatch() + +addCatch() 插入方法体抛出异常时执行的代码,控制权会返回给调用者。 在插入的源代码中,异常用 $e 表示。 + +例如: + +```java +CtMethod m = ...; +CtClass etype = ClassPool.getDefault().get("java.io.IOException"); +m.addCatch("{ System.out.println($e); throw $e; }", etype); +``` + +转换成对应的 java 代码如下: + +```java +try { + // the original method body +} catch (java.io.IOException e) { + System.out.println(e); + throw e; +} +``` + +请注意,插入的代码片段必须以 throw 或 return 语句结束。 + +### 4.2 修改方法体 + +CtMethod 和 CtConstructor 提供 setBody() 来替换整个方法体。他将新的源代码编译成 Java 字节码,并用它替换原方法体。 如果给定的源文本为 null,则替换后的方法体仅包含返回语句,返回零或空值,除非结果类型为 void。 + +在传递给 setBody() 的源代码中,以 $ 开头的标识符具有特殊含义: + +| 符号 | 含义 | +| :-------------------- | :---------------------------------------------- | +| `$0`, `$1`, `$2`, ... | `this` and 方法的参数 | +| `$args` | 方法参数数组.它的类型为 `Object[]` | +| `$$` | 所有实参。例如, `m($$)` 等价于 `m($1,$2,`...`)` | +| `$cflow(`...`)` | `cflow` 变量 | +| `$r` | 返回结果的类型,用于强制类型转换 | +| `$w` | 包装器类型,用于强制类型转换 | +| `$sig` | 类型为 java.lang.Class 的参数类型数组 | +| `$type` | 一个 java.lang.Class 对象,表示返回值类型 | +| `$class` | 一个 java.lang.Class 对象,表示当前正在修改的类 | + +注意 $_ 不可用。 + +替换表达式 + +Javassist 只允许修改方法体中包含的表达式。javassist.expr.ExprEditor 是一个用于替换方法体中的表达式的类。用户可以定义 ExprEditor 的子类来指定修改表达式的方式。 + +要运行 ExprEditor 对象,用户必须在 CtMethod 或 CtClass 中调用 instrument()。 +例如, + +```java +CtMethod cm = ... ; +cm.instrument( + new ExprEditor() { + public void edit(MethodCall m) throws CannotCompileException { + if (m.getClassName().equals("Point") + && m.getMethodName().equals("move")) + m.replace("{ $1 = 0; $_ = $proceed($$); }"); + } + }); +``` + +上述代码,搜索由 cm 表示的方法体,并用使用下面的代码替换 Point 中的 move()调用: + +``` +{ $1 = 0; $_ = $proceed($$); } +``` + +因此 move() 的第一个参数总是0。注意,替换的代码不是一个表达式,而是一个语句或块。 它不能是或包含 try-catch 语句。 + +方法 instrument() 搜索方法体。 如果它找到一个表达式,如方法调用、字段访问和对象创建,那么它调用给定的 ExprEditor 对象上的 edit() 方法。 edit() 的参数表示找到的表达式。 edit() 可以检查和替换该表达式。 + +调用 edit() 参数的 replace() 方法可以将表达式替换为我们给定的语句。如果给定的语句是空块,即执行replace("{}"),则将表达式删除。如果要在表达式之前或之后插入语句(或块),则应该将类似以下的代码传递给 replace(): + +``` +{ *before-statements;* + $_ = $proceed($$); + *after-statements;* } +``` + +无论表达式是方法调用、字段访问还是对象创建或其他。 + +如果表达式是读操作,第二个语句应该是: + +``` +$_ = $proceed(); +``` + +如果表达式是写操作,则第二个语句应该是: + +``` +$proceed($$); +``` + +如果由 instrument() 搜索的方法是使用 -g 选项(类文件包含一个局部变量属性)编译的,目标表达式中可用的局部变量,也可以传递给 replace() 的源代码中使用。 + +javassist.expr.MethodCall + +MethodCall 表示方法调用。MethodCall 的 replace() 方法用于替换方法调用,它接收表示替换语句或块的源代码。和 insertBefore() 方法一样,传递给 replace 的源代码中,以 $ 开头的标识符具有特殊的含义。 + +| 符号 | 含义 | +| :------------ | :----------------------------------------------------------- | +| `$0` | 方法调用的目标对象。它不等于 this,它代表了调用者。 如果方法是静态的,则 $0 为 null | +| `$1`, `$2` .. | 方法的参数 | +| `$_` | 方法调用的结果 | +| `$r` | 返回结果的类型,用于强制类型转换 | +| `$class` | 一个 java.lang.Class 对象,表示当前正在修改的类 | +| `$sig` | 类型为 java.lang.Class 的参数类型数组 | +| `$type` | 一个 java.lang.Class 对象,表示返回值类型 | +| `$class` | 一个 java.lang.Class 对象,表示当前正在修改的类 | +| `$proceed` | 调用表达式中方法的名称 | + +这里的方法调用意味着由 MethodCall 对象表示的方法。 + +其他标识符如 $w,$args 和 $$ 也可用。 + +除非方法调用的返回类型为 void,否则返回值必须在源代码中赋给 $_,$_ 的类型是表达式的结果类型。如果结果类型为 void,那么 $_ 的类型为Object,并且分配给 $_ 的值将被忽略。 + +$proceed 不是字符串值,而是特殊的语法。 它后面必须跟一个由括号括起来的参数列表。 + +javassist.expr.ConstructorCall + +ConstructorCall 表示构造函数调用,例如包含在构造函数中的 this() 和 super()。ConstructorCall 中的方法 replace() 可以使用语句或代码块来代替构造函数。它接收表示替换语句或块的源代码。和 insertBefore() 方法一样,传递给 replace 的源代码中,以 $ 开头的标识符具有特殊的含义。 + +| 符号 | 含义 | +| :-------------- | :---------------------------------------------- | +| `$0` | 构造调用的目标对象。它等于 this | +| `$1`, `$2`, ... | 构造函数的参数 | +| `$class` | 一个 java.lang.Class 对象,表示当前正在修改的类 | +| `$sig` | 类型为 java.lang.Class 的参数类型数组 | +| `$proceed` | 调用表达式中构造函数的名称 | + +这里的构造函数调用是由 ConstructorCall 对象表示的。 + +其他标识符如 $w,$args 和 $$ 也可用。 + +由于任何构造函数必须调用超类的构造函数或同一类的另一个构造函数,所以替换语句必须包含构造函数调用,通常是对 $proceed() 的调用。 + +$proceed 不是字符串值,而是特殊的语法。 它后面必须跟一个由括号括起来的参数列表。 + +javassist.expr.FieldAccess + +FieldAccess 对象表示字段访问。 如果找到对应的字段访问操作,ExprEditor 中的 edit() 方法将接收到一个 FieldAccess 对象。FieldAccess 中的 replace() 方法接收替源代码来替换字段访问。 + +在源代码中,以 $ 开头的标识符具有特殊含义: + +| 符号 | 含义 | +| :--------- | :----------------------------------------------------------- | +| `$0` | 表达式访问的字段。它不等于 this。this 表示调用表达式所在方法的对象。如果字段是静态的,则 $0 为 null | +| `$1` | 如果表达式是写操作,则写的值将保存在 $1 中。否则 $1 不可用 | +| `$_` | 如果表达式是读操作,则结果值保存在 $1 中,否则将舍弃存储在 $_ 中的值 | +| `$r` | 如果表达式是读操作,则 $r 读取结果的类型。 否则 $r 为 void | +| `$class` | 一个 java.lang.Class 对象,表示字段所在的类 | +| `$type` | 一个 java.lang.Class 对象,表示字段的类型 | +| `$proceed` | 执行原始字段访问的虚拟方法的名称 | + +其他标识符如 $w,$args 和 $$ 也可用。 +如果表达式是读操作,则必须在源文本中将值分配给 $。 $的类型是字段的类型。 + +javassist.expr.NewExpr + +NewExpr 表示使用 new 运算符(不包括数组创建)创建对象的表达式。 如果发现创建对象的操作,NewEditor 中的 edit() 方法将接收到一个 NewExpr 对象。NewExpr 中的 replace() 方法接收替源代码来替换字段访问。 + +在源文本中,以 $ 开头的标识符具有特殊含义: + +| 符号 | 含义 | +| :--------- | :---------------------------------------------- | +| `$0` | null | +| `$1` | 构造函数的参数 | +| `$_` | 创建对象的返回值。一个新的对象存储在 $_ 中 | +| `$r` | 所创建的对象的类型 | +| `$sig` | 类型为 java.lang.Class 的参数类型数组 | +| `$type` | 一个 java.lang.Class 对象,表示创建的对象的类型 | +| `$proceed` | 执行对象创建虚拟方法的名称 | + +其他标识符如 $w,$args 和 $$ 也可用。 + +javassist.expr.NewArray + +NewArray 表示使用 new 运算符创建数组。如果发现数组创建的操作,ExprEditor 中的 edit() 方法一个 NewArray 对象。NewArray 中的 replace() 方法可以使用源代码来替换数组创建操作。 + +在源文本中,以$开头的标识符具有特殊含义: + +| 符号 | 含义 | +| :--------- | :---------------------------------------------- | +| `$0` | null | +| `$1`, `$1` | 每一维的大小 | +| `$_` | 创建数组的返回值。一个新的数组对象存储在 $_ 中 | +| `$r` | 所创建的数组的类型 | +| `$type` | 一个 java.lang.Class 对象,表示创建的数组的类型 | +| `$proceed` | 执行数组创建虚拟方法的名称 | + +其他标识符如 $w,$args 和 $$ 也可用。 +例如,如果按下面的方式创建数组: + +``` +String[][] s = new String[3][4]; +``` + +那么 $1 和 $2 的值分别是 3 和 4。 $3 不可用。 + +例如,如果按下面的方式创建数组: + +``` +String[][] s = new String[3][]; +``` + +那么 $1 的值为 3,但 $2 不可用。 + +javassist.expr.Instanceof + +一个 InstanceOf 对象表示一个 instanceof 表达式。 如果找到 instanceof 表达式,则ExprEditor 中的 edit() 方法接收此对象。Instanceof 中的 replace() 方法可以使用源代码来替换 instanceof 表达式。 + +在源文本中,以$开头的标识符具有特殊含义: + +| 符号 | 含义 | +| :--------- | :----------------------------------------------------------- | +| `$0` | null | +| `$1` | instanceof 运算符左侧的值 | +| `$_` | 表达式的返回值。类型为 boolean | +| `$r` | instanceof 运算符右侧的值 | +| `$type` | 一个 java.lang.Class 对象,表示 instanceof 运算符右侧的类型 | +| `$proceed` | 执行 instanceof 表达式的虚拟方法的名称。它需要一个参数(类型是 java.lang.Object)。如果参数类型和 instanceof 表达式右侧的类型一致,则返回 true。否则返回 false。 | + +其他标识符如 $w,$args 和 $$ 也可用。 + +javassist.expr.Cast + +Cast 表示 cast 表达式。如果找到 cast 表达式,ExprEditor 中的 edit() 方法会接收到一个 Cast 对象。 Cast 的 replace() 方法可以接收源代码来替换替换 �cast 表达式。 + +在源文本中,以$开头的标识符具有特殊含义: + +| 符号 | 含义 | +| :--------- | :----------------------------------------------------------- | +| `$0` | null | +| `$1` | 显示类型转换的目标类型(?) | +| `$_` | 表达式的结果值。$_ 的类型和被括号括起来的类型相同(?) | +| `$r` | 转换之后的类型,即被括号括起来的类型(?) | +| `$type` | 一个 java.lang.Class 对象,和 $r 的类型相同 | +| `$proceed` | 执行类型转换的虚拟方法的名称。它需要一个参数(类型是 java.lang.Object)。并在类型转换完成后返回它 | + +其他标识符如 $w,$args 和 $$ 也可用。 + +javassist.expr.Handler + +Handler 对象表示 try-catch 语句的 catch 子句。 如果找到 catch,ExprEditor 中的 edit() 方法会接收此对象。 Handler 中的 insertBefore() 方法会将收到的源代码插入到 catch 子句的开头。 + +在源文本中,以$开头的标识符具有意义: + +| 符号 | 含义 | +| :------ | :----------------------------------------------------- | +| `$1` | catch 分支获得的异常对象 | +| `$r` | catch 分支获得的异常对象的类型,用于强制类型转换 | +| `$w` | 包装类型,用于强制类型转换 | +| `$type` | 一个 java.lang.Class 对象,表示 catch 捕获的异常的类型 | + +如果一个新的异常分配给 $1,它将作为捕获的异常传递给原始的 catch 子句。 + +### 4.3 添加新方法和字段 + +添加新方法 + +Javassist 可以创建新的方法和构造函数。CtNewMethod 和 CtNewConstructor 提供了几个工厂方法来创建 CtMethod 或 CtConstructor 对象。make() 方法可以通过源代码来CtMethod 或 CtConstructor 对象。 + +例如: + +```java +CtClass point = ClassPool.getDefault().get("Point"); +CtMethod m = CtNewMethod.make( + "public int xmove(int dx) { x += dx; }", + point); +point.addMethod(m); +``` + +上面的代码向类 Point 添加了一个公共方法 xmove()。在这个例子中,x 是类 Point 的一个int 字段。 + +传递给 make() 和 setBody() 的源文本可以包括以 $ 开头的标识符 ($_ 除外)。 如果目标对象和目标方法名也被传递给 make() 方法,源文本中也可以包括 $proceed。 + +例如: + +```java +CtClass point = ClassPool.getDefault().get("Point"); +CtMethod m = CtNewMethod.make( + "public int ymove(int dy) { $proceed(0, dy); }", + point, "this", "move"); +``` + +这个程序创建一个 ymove() 方法,定义如下: + +``` +public int ymove(int dy) { this.move(0, dy); } +``` + +注意,$proceed 已经被替换为 this.move。 + +Javassist 还提供了另一种添加新方法的方式。 你可以先创建一个抽象方法,然后给它一个方法体: + +```java +CtClass cc = ... ; +CtMethod m = new CtMethod(CtClass.intType, "move", + new CtClass[] { CtClass.intType }, cc); +cc.addMethod(m); +m.setBody("{ x += $1; }"); +cc.setModifiers(cc.getModifiers() & ~Modifier.ABSTRACT); +``` + +因为 Javassist 在类中添加了的方法是抽象的,所以在调用 setBody() 之后,必须将类显式地改回非抽象类。 + +相互递归的方法 (Mutual recursive methods) + +Javassist 不能这种方法:如果它调用另一个方法,而另一个方法没有被添加到一个类(Javassist可以编译一个以递归方式调用的方法)。如果要向类添加相互递归方法,需要使用如下的技巧。假设你想要将方法 m() 和 n() 添加到由 cc 表示的类中: + +```java +CtClass cc = ... ; +CtMethod m = CtNewMethod.make("public abstract int m(int i);", cc); +CtMethod n = CtNewMethod.make("public abstract int n(int i);", cc); +cc.addMethod(m); +cc.addMethod(n); +m.setBody("{ return ($1 <= 0) ? 1 : (n($1 - 1) * $1); }"); +n.setBody("{ return m($1); }"); +cc.setModifiers(cc.getModifiers() & ~Modifier.ABSTRACT); +``` + +你必须先创建两个抽象方法,并将它们添加到类中。然后设置它们的方法体,即使方法体包括互相递归的调用。 最后,必须将类更改为非抽象类。 + +添加一个字段 + +Javassist 还允许用户创建一个新字段。 + +```java +CtClass point = ClassPool.getDefault().get("Point"); +CtField f = new CtField(CtClass.intType, "z", point); +point.addField(f); +``` + +该程序向类 Point 添加一个名为 z 的字段。 +如果必须指定添加字段的初始值,那么上面的程序必须修改为: + +```java +CtClass point = ClassPool.getDefault().get("Point"); +CtField f = new CtField(CtClass.intType, "z", point); +point.addField(f, "0"); // initial value is 0 +``` + +现在,方法 addField() 接收两个参数,第二个参数表示计算初始值的表达式。这个表达式可以是任意 Java 表达式,只要其结果与字段的类型匹配。 请注意,表达式不以分号结尾。 + +此外,上述代码可以重写为更简单代码: + +```java +CtClass point = ClassPool.getDefault().get("Point"); +CtField f = CtField.make("public int z = 0;", point); +point.addField(f); +``` + +删除成员 + +要删除字段或方法,请在 CtClass 的 removeField() 或 removeMethod() 方法。 一个CtConstructor 可以通过 CtClass 的 removeConstructor() 删除。 + +### 4.4 注解 (Annotations) + +CtClass,CtMethod,CtField 和 CtConstructor 提供 getAnnotations() 方法,用于读取注解。 它返回一个注解类型的对象。 + +例如,假设有以下注解: + +```java +public @interface Author { + String name(); + int year(); +} +``` + +下面是使用注解的代码: + +```java +@Author(name="Chiba", year=2005) +public class Point { + int x, y; +} +``` + +然后,可以使用 getAnnotations() 获取注解的值。 它返回一个包含注解类型对象的数组。 + +```java +CtClass cc = ClassPool.getDefault().get("Point"); +Object[] all = cc.getAnnotations(); +Author a = (Author)all[0]; +String name = a.name(); +int year = a.year(); +System.out.println("name: " + name + ", year: " + year); +``` + +这段代码输出: + +``` +name: Chiba, year: 2005 +``` + +由于 Point 的注解只有 @Author,所以数组的长度是 1,all[0] 是一个 Author 对象。 注解成员值可以通过调用Author对象的 name() 和 year() 来获取。 + +要使用 getAnnotations(),注释类型(如 Author)必须包含在当前类路径中。它们也必须也可以从 ClassPool 对象访问。如果未找到注释类型的类文件,Javassist 将无法获取该注释类型的成员的默认值。 + +### 4.5 运行时支持类 + +在大多数情况下,使用 Javassist 修改类不需要运行 Javassist。 但是,Javassist 编译器生成的某些字节码需要运行时支持类,这些类位于 javassist.runtime 包中(有关详细信息,请阅读该包的API文档)。请注意,javassist.runtime 是修改的类时唯一可能需要使用的包。 修改类的运行时不会再使用其他的 Javassist 类。 + +### 4.6 导入(Import) + +源代码中的所有类名都必须是完整的(必须包含包名,java.lang 除外)。例如,Javassist 编译器可以解析 Object 以及 java.lang.Object。 + +要告诉编译器在解析类名时搜索其他包,请在 ClassPool中 调用 importPackage()。 例如, + +```java +ClassPool pool = ClassPool.getDefault(); +pool.importPackage("java.awt"); +CtClass cc = pool.makeClass("Test"); +CtField f = CtField.make("public Point p;", cc); +cc.addField(f); +``` + +第二行导入了 java.awt 包。 因此,第三行不会抛出异常。 编译器可以将 Point 识别为java.awt.Point。 + +注意 importPackage() 不会影响 ClassPool 中的 get() 方法。只有编译器才考虑导入包。 get() 的参数必须是完整类名。 + +### 4.7 限制 (Limitations) + +在目前实现中,Javassist 中包含的 Java 编译器有一些限制: + +- J2SE 5.0 引入的新语法(包括枚举和泛型)不受支持。注释由 Javassist 的低级 API 支持。 参见 javassist.bytecode.annotation 包(以及 CtClass 和 CtBehavior 中的� getAnnotations())。对泛型只提供部分支持。更多信息,请参阅后面的部分; +- 初始化数组时,只有一维数组可以用大括号加逗号分隔元素的形式初始化,多维数组还不支持; +- 编译器不能编译包含内部类和匿名类的源代码。 但是,Javassist 可以读取和修改内部/匿名类的类文件; +- 不支持带标记的 continue 和 break 语句; +- 编译器没有正确实现 Java 方法调度算法。编译器可能会混淆在类中定义的重载方法(方法名称相同,查参数列表不同)。例如: + +```java +class A {} + +class B extends A {} + +class C extends B {} + +class X { + void foo(A a) { .. } + void foo(B b) { .. } +} +``` + +如果编译的表达式是 `x.foo(new C())`,其中 `x` 是 `X` 的实例,编译器将产生对 `foo(A)` 的调用,尽管编译器可以正确地编译 `foo((B) new C())` 。 + +- 建议使用 # 作为类名和静态方法或字段名之间的分隔符。 例如,在常规 Java 中, + +``` +javassist.CtClass.intType.getName() +``` + +在 javassist.CtClass 中的静态字段 intType 指示的对象上调用一个方法 getName()。 在Javassist 中,用户也可以写上面的表达式,但是建议写成这样: + +``` +javassist.CtClass#intType.getName() +``` + +使编译器可以快速解析表达式。 + +## 5. 字节码操作 + +Javassist 还提供了用于直接编辑类文件的低级级 API。 使用此 API之前,你需要详细了解Java 字节码和类文件格式,因为它允许你对类文件进行任意修改。 + +如果你只想生成一个简单的类文件,使用`javassist.bytecode.ClassFileWriter`就足够了。 它比`javassist.bytecode.ClassFile`更快而且更小。 + +### 获取 ClassFile 对象 + +javassist.bytecode.ClassFile 对象表示类文件。要获得这个对象,应该调用 CtClass 中的 getClassFile() 方法。 +你也可以直接从类文件构造 javassist.bytecode.ClassFile 对象。 例如: + +```java +BufferedInputStream fin + = new BufferedInputStream(new FileInputStream("Point.class")); +ClassFile cf = new ClassFile(new DataInputStream(fin)); +``` + +这代码段从 Point.class 创建一个 ClassFile 对象。 +ClassFile 对象可以写回类文件。ClassFile 的 write() 将类文件的内容写入给定的 DataOutputStream。 + +### 5.2 添加和删除成员 + +ClassFile 提供了 addField(),addMethod() 和 addAttribute(),来向类添加字段、方法和类文件属性。 + +注意,FieldInfo,MethodInfo 和 AttributeInfo 对象包括到 ConstPool(常量池表)对象的链接。 ConstPool 对象必须对 ClassFile 对象和添加到该 ClassFile 对象的 FieldInfo(或MethodInfo 等)对象是通用的。 换句话说,FieldInfo(或MethodInfo等)对象不能在不同的ClassFile 对象之间共享。 + +要从 ClassFile 对象中删除字段或方法,必须首先获取包含该类的所有字段的 java.util.List 对象。 getFields() 和 getMethods() 返回列表。可以通过在List对象上调用 remove() 来删除字段或方法。可以以类似的方式去除属性。在 FieldInfo 或 MethodInfo 中调用 getAttributes() 以获取属性列表,并从列表中删除一个。 + +### 5.3 遍历方法体 + +使用 CodeIterator 可以检查方法体中的每个字节码指令,要获得 CodeIterator 对象,参考以下代码: + +```java +ClassFile cf = ... ; +MethodInfo minfo = cf.getMethod("move"); // we assume move is not overloaded. +CodeAttribute ca = minfo.getCodeAttribute(); +CodeIterator ci = ca.iterator(); +``` + +CodeIterator 对象允许你逐个访问每个字节码指令。下面展示了一部分 CodeIterator 中声明的方法: + +- void begin() + 移动到第一条指令。 +- void move(int index) + 移动到指定位置的指令。 +- boolean hasNext() + 是否有下一条指定 +- int next() + 返回下一条指令的索引。注意,它不返回下一条指令的操作码。 +- int byteAt(int index) + 返回索引处的无符号8位整数。 +- int u16bitAt(int index) + 返回索引处的无符号16位整数。 +- int write(byte [] code,int index) + 在索引处写入字节数组。 +- void insert(int index,byte [] code) + 在索引处插入字节数组。自动调整分支偏移量。 + +以下代码段打印了方法体中所有的指令: + +```java +CodeIterator ci = ... ; +while (ci.hasNext()) { + int index = ci.next(); + int op = ci.byteAt(index); + System.out.println(Mnemonic.OPCODE[op]); +} +``` + +### 5.4 生成字节码序列 + +`Bytecode` 对象表示字节码指令序列。它是一个可扩展的字节码数组。 +以下是示例代码段: + +```java +ConstPool cp = ...; // constant pool table +Bytecode b = new Bytecode(cp, 1, 0); +b.addIconst(3); +b.addReturn(CtClass.intType); +CodeAttribute ca = b.toCodeAttribute(); +``` + +这段代码产生以下序列的代码属性: + +``` +iconst_3 +ireturn +``` + +您还可以通过调用 Bytecode 中的 get() 方法来获取包含此序列的字节数组。获得的数组可以插入另一个代码属性。 +Bytecode 提供了许多方法来添加特定的指令,例如使用 addOpcode() 添加一个 8 位操作码,使用 addIndex() 用于添加一个索引。每个操作码的值定义在 Opcode 接口中。 +addOpcode() 和添加特定指令的方法,将自动维持最大堆栈深度,除非控制流没有分支。可以通过调用 Bytecode 的 getMaxStack() 方法来获得这个深度。它也反映在从 Bytecode对象构造的 CodeAttribute 对象上。要重新计算方法体的最大堆栈深度,可以调用 CodeAttribute 的 computeMaxStack() 方法。 + +### 5.5 注释(元标签) + +注释作为运行时不可见(或可见)的注记属性,存储在类文件中。调用 getAttribute(AnnotationsAttribute.invisibleTag)方法,可以从 ClassFile,MethodInfo 或 FieldInfo 中获取注记属性。更多信息,请参阅 `javassist.bytecode.AnnotationsAttribute`和`javassist.bytecode.annotation` 包的 javadoc 手册。 + +Javassist还允许您通过更高级别的API访问注释。 如果要通过CtClass访问注释,请在CtClass或CtBehavior中调用getAnnotations()。 + +## 6. 泛型 + +Javassist 的低级别 API 完全支持 Java 5 引入的泛型。但是,高级别的API(如CtClass)不直接支持泛型。 + +Java 的泛型是通过擦除技术实现。 编译后,所有类型参数都将被删除。 例如,假设您的源代码声明一个参数化类型 Vector: + +```java +Vector v = new Vector(); + : +String s = v.get(0); +``` + +编译后的字节码等价于以下代码: + +```java +Vector v = new Vector(); + : +String s = (String)v.get(0); +``` + +因此,在编写字节码变换器时,您可以删除所有类型参数,因为 Javassist 的编译器不支持泛型。如果源代码使用 Javassist 编译,例如通过 CtMethod.make(),源代码必须显式类型转换。如果源代码由常规 Java 编译器(如javac)编译,则不需要做类型转换。 + +例如,如果你有一个类: + +```java +public class Wrapper { + T value; + public Wrapper(T t) { value = t; } +} +``` + +并想添加一个接口 Getter 到类 Wrapper: + +```java +public interface Getter { + T get(); +} +``` + +那么你真正要添加的接口其实是Getter(将类型参数掉落),最后你添加到 Wrapper 类的方法是这样的: + +```java +public Object get() { return value; } +``` + +注意,不需要类型参数。 由于 get 返回一个 Object,如果源代码是由 Javassist 编译的,那么在调用方需要进行显式类型转换。 例如,如果类型参数 T 是 String,则必须插入(String),如下所示: + +```java +Wrapper w = ... +String s = (String)w.get(); +``` + +## 7.可变参数 + +目前,Javassist 不直接支持可变参数。 因此,要使用 varargs 创建方法,必须显式设置方法修饰符。假设要定义下面这个方法: + +```java +public int length(int... args) { return args.length; } +``` + +使用 Javassist 应该是这样的: + +```java +CtClass cc = /* target class */; +CtMethod m = CtMethod.make("public int length(int[] args) { return args.length; }", cc); +m.setModifiers(m.getModifiers() | Modifier.VARARGS); +cc.addMethod(m); +``` + +参数类型`int ...`被更改为`int []`,`Modifier.VARARGS`被添加到方法修饰符中。 + +要在由 Javassist 的编译器编译的源代码中调用此方法,需要这样写: + +``` +length(new int[] { 1, 2, 3 }); +``` + +而不是这样: + +``` +length(1, 2, 3); +``` + +## 8. J2ME + +如果要修改 J2ME 执行环境的类文件,则必须先执行预验证。预验证基本上是生成堆栈映射,这类似于在 JDK 1.6 中引入 J2SE 的堆栈映射表。当`javassist.bytecode.MethodInfo.doPreverify` 为 true 时,Javassist 才会维护 J2ME 的堆栈映射。 + +对于指定的 CtMethod 对象,你可以调用以下方法,手动生成堆栈映射: + +```java +m.getMethodInfo().rebuildStackMapForME(cpool); +``` + +这里,cpool 是一个 ClassPool 对象,通过在 CtClass 对象上调用 getClassPool() 可以获得。 ClassPool 对象负责从给定类路径中查找类文件。要获得所有的 CtMethod 对象,需要在 CtClass 对象上调用 getDeclaredMethods() 方法。 + +## 9.装箱/拆箱 + +Java 中的装箱和拆箱是语法糖。没有用于装箱或拆箱的字节码。所以 Javassist 的编译器不支持它们。 例如,以下语句在 Java 中有效: + +```java +Integer i = 3; +``` + +因为隐式地执行了装箱。 但是,对于 Javassist,必须将值类型从 int 显式地转换为 Integer: + +```java +Integer i = new Integer(3); +``` + +## 10. 调试 + +将 CtClass.debugDump 设为本地目录。 然后 Javassist 修改和生成的所有类文件都保存在该目录中。要停止此操作,将 CtClass.debugDump 设置为 null 即可。其默认值为 null。 + +例如, + +``` +CtClass.debugDump =“./dump”; +``` + +所有修改的类文件都保存在 ./dump 中。 + +## 11.引用 + +**原文出处**: + + + +[https://github.com/jboss-javassist/javassist/wiki/Tutorial-2](https://link.jianshu.com/?t=https://github.com/jboss-javassist/javassist/wiki/Tutorial-2) + + + +**翻译:** + +作者:二胡 + +连接:https://www.jianshu.com/p/43424242846b + +来源:简书 + +**第一来源** + +作者:[21aspnet](https://me.csdn.net/21aspnet) + +连接:https://blog.csdn.net/21aspnet/article/details/81671777 + +来源:CSDN \ No newline at end of file diff --git "a/java/java\351\200\203\351\200\270\345\210\206\346\236\220.md" "b/java/java\351\200\203\351\200\270\345\210\206\346\236\220.md" new file mode 100644 index 0000000..9f42c2b --- /dev/null +++ "b/java/java\351\200\203\351\200\270\345\210\206\346\236\220.md" @@ -0,0 +1,106 @@ +## 问题 + +**java中的对象都在堆中分配内存吗?** + +## 什么是逃逸分析? + +定义:逃逸分析(Escape Analysis)简单来讲就是,Java Hotspot 虚拟机可以分析新创建对象的使用范围,并决定是否在 Java 堆上分配内存的一项技术。 + +**逃逸分析的 JVM 参数如下:** + +* 开启逃逸分析: -XX:+DoEscapeAnalysis +* 关闭逃逸分析: -XX:-DoEscapeAnalysis +* 显示分析结果: -XX:+PrintEscapeAnalysis + +逃逸分析技术在 Java SE 6u23+ 开始支持,并默认设置为启用状态,可以不用额外加这个参数。 + +## 逃逸分析算法 + +Java Hotspot 编译器实现下面论文中描述的逃逸算法: + +> [Choi99] Jong-Deok Choi. Manish Gupta. Mauricio Seffano. +> +> ​ Vugranam C. Sreedhar . Sam Midkiff . +> +> ​ "Escape Analysis for Java". Procedings of ACM SIGPLAN +> +> ​ OOPSLA Conference. November 1, 1999 + +根据 Jong-Deok Choi. Manish Gupta. Mauricio Seffano. Vugranam C. Sreedhar . Sam Midkiff .等大牛在论文《Escape Analysis for Java》中描述的算法进行逃逸分析的。 + +该算法引入了连通图,用连通图来构建对象和对象引用之间的可达性关系,并在此基础上,提出一种组合数据流分析流。 + +由于算法是上下文相关和流敏感的,并且模拟了对象任意层次的嵌套关系,所以分析精度较高,只是运行时间和内存消耗相对较大。 + +## 对象逃逸状态 + +### 1、全局逃逸(GlobalEscape) + +即一个对象的作用范围逃出了当前方法或者当前线程,有一下几种场景: + +* 对象是一个静态变量 +* 对象是一个已经发生逃逸的对象 +* 对象作为当前方法的返回值 + +### 2、参数逃逸(ArgEscape) + +即一个对象被作为方法参数传递或者被参数引用,但在调用过程中不会发生全局逃逸,这个状态时通过被调方法的字节码确定的。 + +### 3、没有逃逸 + +即方法中的对象没有发生逃逸。 + +## 逃逸分析优化 + +针对以上三点,当一个对象没有逃逸时,可以得到以下几个虚拟机的优化。 + +### 1、锁消除 + +我们知道线程同步锁是非常牺牲性能的,当编译器确定当前对象只有在当前线程使用,那么就会移除该对象的同步锁。 + +例如,StringBuffer 和 Vactor 都是用 synchronized 修饰线程安全的,但大部分清苦下,他们都只能在当前线程中用到,这样编译器就会优化移除掉这些锁操作。 + +**锁消除的 JVM 参数如下:** + +* 开启锁消除:-XX:+EliminateLocks +* 关闭锁消除:-XX:-EliminateLocks + +锁消除在 JDK8 中默认开启,并且锁消除都要建立在逃逸分析的基础上。 + +### 2、标量替换 + +首先要明白标量和聚合量,基础类型和对象类型的引用可以理解为标量,他们不能被进一步分解。而能被进一步分解的量就是聚合量,比如:对象。 + +对象是聚合量,它又可以被进一步分解为标量,将其成员分解为分散的变量,这就叫做标量替换。 + +这样,如果一个对象没有发生逃逸,那根本就不会创建它,只会在栈或者寄存器上创建它用到的成员标量,节省了内存空间,也提升了应用程序性能。 + +**标量替换的 JVM 参数如下:** + +* 开启标量替换:-XX:+EliminateAllocations +* 关闭标量替换:-XX:-EliminateAllocations +* 显示标量替换详情:-XX:+PrintEliminateAllocations + +标量替换在 JKD8 中都是默认开启的,并且都要建立在逃逸分析的基础上。 + +### 3、栈上分配 + +当对象没有发生逃逸时,该对象就可以通过标量替换分解为成员标量分配在栈内存中,和方法的声明周期一致,随着栈针出栈时销毁,减少了 GC 压力,提高了应用程序性能。 + +## 总结 + +逃逸分析是为了优化 JVM 内存和提升程序性能的。 + +在写代码时,尽量缩小变量的作用范围,如: + +```java +return stringBuilder; +``` + +可以优化为: + +```java +return stringBuilder.toString(); +``` + +将 StringBuilder 对象的范围尽量控制在方法内部,防止对象逃逸。 \ No newline at end of file diff --git "a/java/\344\270\200\345\274\240\345\233\276\347\234\213\346\207\202java\351\233\206\345\220\210\346\241\206\346\236\266.md" "b/java/\344\270\200\345\274\240\345\233\276\347\234\213\346\207\202java\351\233\206\345\220\210\346\241\206\346\236\266.md" new file mode 100644 index 0000000..eb432f2 --- /dev/null +++ "b/java/\344\270\200\345\274\240\345\233\276\347\234\213\346\207\202java\351\233\206\345\220\210\346\241\206\346\236\266.md" @@ -0,0 +1,6 @@ +## JAVA 集合框架结构 + + + +![java集合框架](./images/002/java集合框架.jpg) + diff --git "a/java/\344\273\243\347\240\201\347\211\207/SVN\345\237\272\347\241\200\346\223\215\344\275\234.md" "b/java/\344\273\243\347\240\201\347\211\207/SVN\345\237\272\347\241\200\346\223\215\344\275\234.md" new file mode 100644 index 0000000..2961b1c --- /dev/null +++ "b/java/\344\273\243\347\240\201\347\211\207/SVN\345\237\272\347\241\200\346\223\215\344\275\234.md" @@ -0,0 +1,230 @@ +## 使用SVN进行操作 + +### 依赖 + +```xml + + org.tmatesoft.svnkit + svnkit + 1.10.1 + +``` + +### 创建与释放客户端管理器 + +```java + /** + * 根据用户名和密码创建SVN客户端管理器 + * + * @param username 用户名 + * @param password 密码 + * @return 客户端管理器 + */ + public static SVNClientManager getSVNClientManager(String username, String password) { + if (StringUtil.isBlank(username)) { + throw new IllegalArgumentException("SVN username must not null"); + } + + DefaultSVNOptions options = new DefaultSVNOptions(); + SVNClientManager svnClientManager = SVNClientManager + .newInstance(options, username, password); + svnClientManager.setIgnoreExternals(false); + return svnClientManager; + } +``` + +```java + /** + * 释放SVN客户端连接 + * + * @param svnClientManager svn 客户端管理器 + */ + public static void releaseSVNClientManager(SVNClientManager svnClientManager) { + if (ObjectUtil.isNull(svnClientManager)) { + return; + } + svnClientManager.dispose(); + } + +``` + +### checkout + +```java + /** + * checkout 文件夹 + * + * @param svnClientManager svn客户端管理器 {{@link #getSVNClientManager(String, String)}} + * @param svnUrl svn地址 + * @param directorPath 本地保存路径 + * @return 版本号 + */ + public static long checkout(SVNClientManager svnClientManager, String svnUrl, + String directorPath) { + File dir = new File(directorPath); + IoUtil.createDirector(dir); + SVNUpdateClient updateClient = svnClientManager.getUpdateClient(); + try { + return updateClient + .doCheckout(SVNURL.parseURIEncoded(svnUrl), dir, SVNRevision.HEAD, SVNRevision.HEAD, + SVNDepth.INFINITY, true); + } catch (SVNException e) { + throw new RuntimeException(e); + } + } + +``` + +### update + +```java + /** + * 更新 + * + * @param svnClientManager svn客户端管理器 + * @param filepath 文件路径 + * @return 文件版本号 + */ + public static long update(SVNClientManager svnClientManager, String filepath) { + if (StringUtil.isBlank(filepath)) { + throw new IllegalArgumentException("update file must not null"); + } + return update(svnClientManager, new File(filepath)); + } + + /** + * 更新 + * + * @param svnClientManager svn客户端管理器 + * @param file 需更新的文件 + * @return 文件版本号 + */ + public static long update(SVNClientManager svnClientManager, File file) { + if (ObjectUtil.isNull(file)) { + throw new NullPointerException("update file must not null"); + } + File[] files = new File[1]; + files[0] = file; + return update(svnClientManager, files)[0]; + } + + /** + * 更新 + * + * @param svnClientManager svn客户端管理器 + * @param filesets 更新的文件集合 + * @return 文件版本号列表 + */ + public static long[] update(SVNClientManager svnClientManager, String[] filesets) { + if (CollectionUtil.isEmpty(filesets)) { + throw new IllegalArgumentException("update files must has one or more"); + } + File[] files = new File[filesets.length]; + for (int i = 0; i < filesets.length; i++) { + files[i] = new File(filesets[i]); + } + return update(svnClientManager, files); + } + + /** + * 更新文件 + * + * @param svnClientManager svn客户端管理器 + * @param files 需要更新的文件列表 + * @return 文件版本号列表 + */ + public static long[] update(SVNClientManager svnClientManager, File[] files) { + if (CollectionUtil.isEmpty(files)) { + throw new IllegalArgumentException("update files must has one or more"); + } + if (ObjectUtil.isNull(svnClientManager)) { + throw new NullPointerException("SVNClientManager must not null"); + } + SVNUpdateClient updateClient = svnClientManager.getUpdateClient(); + try { + return updateClient.doUpdate(files, SVNRevision.HEAD, SVNDepth.INFINITY, true, false); + } catch (SVNException e) { + throw new RuntimeException(e); + } + } +``` + +### commit + +```java + /** + * 提交 + * + * @param svnClientManager svn客户端管理器 + * @param filepath 提交的文件 + * @param commitMessage 提交的文本描述 + * @return 新的版本号, -1:没有数据提交 + */ + public static long commit(SVNClientManager svnClientManager, String filepath, + String commitMessage) { + if (StringUtil.isBlank(filepath)) { + throw new IllegalArgumentException("commit file must not null"); + } + return commit(svnClientManager, new File(filepath), commitMessage); + } + + /** + * 提交 + * + * @param svnClientManager svn客户端管理器 + * @param file 提交的文件 + * @param commitMessage 提交的文本描述 + * @return 新的版本号, -1:没有数据提交 + */ + public static long commit(SVNClientManager svnClientManager, File file, String commitMessage) { + if (ObjectUtil.isNull(file)) { + throw new IllegalArgumentException("commit file must not null"); + } + File[] files = new File[1]; + files[0] = file; + return commit(svnClientManager, files, commitMessage); + } + + /** + * 提交 + * + * @param svnClientManager svn客户端管理器 + * @param paths 提交的路径 + * @param commitMessage 提交的文本描述 + * @return 新的版本号, -1:没有数据提交 + */ + public static long commit(SVNClientManager svnClientManager, File[] paths, String commitMessage) { + if (ObjectUtil.isNull(svnClientManager)) { + throw new NullPointerException("SVNClientManager must not null"); + } + if (CollectionUtil.isEmpty(paths)) { + throw new IllegalArgumentException("commit file set must not null"); + } + SVNCommitClient commitClient = svnClientManager.getCommitClient(); + try { + SVNCommitInfo svnCommitInfo = commitClient + .doCommit(paths, false, commitMessage, null, null, false, true, SVNDepth.INFINITY); + return svnCommitInfo.getNewRevision(); + } catch (SVNException e) { + throw new RuntimeException(e); + } + } +``` + +### 使用方式 + +```java +SVNClientManager svnClientManager = null; +try { + svnClientManager = getSVNClientManager("myUsername", "myPassword"); + // checkout + long checkoutVersion = checkout(svnClientManager, "http://ip:port/url", "C://dir"); + // update + long updateVersion = update(svnClientManager, "path to update file"); + // commit + long commitVersion = commit(svnClientManager, "path to commit file or director", "commit message"); +} finally { + releaseSVNClientManager(svnClientManager); +} +``` + diff --git "a/java/\344\273\243\347\240\201\347\211\207/jackson\350\275\254\346\215\242json\345\267\245\345\205\267.md" "b/java/\344\273\243\347\240\201\347\211\207/jackson\350\275\254\346\215\242json\345\267\245\345\205\267.md" new file mode 100644 index 0000000..40c32c2 --- /dev/null +++ "b/java/\344\273\243\347\240\201\347\211\207/jackson\350\275\254\346\215\242json\345\267\245\345\205\267.md" @@ -0,0 +1,211 @@ +## 使用Jackson操作JSON数据 + +```java +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.core.JsonGenerator; +import com.fasterxml.jackson.core.JsonParser; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.type.TypeReference; +import com.fasterxml.jackson.databind.DeserializationContext; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JsonDeserializer; +import com.fasterxml.jackson.databind.JsonSerializer; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.SerializationFeature; +import com.fasterxml.jackson.databind.SerializerProvider; +import com.fasterxml.jackson.databind.module.SimpleModule; +import com.fasterxml.jackson.databind.ser.std.ToStringSerializer; +import java.io.IOException; +import java.math.BigDecimal; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.util.Date; +import java.util.List; + +/** + * json字符串和对象之间互转 + * + * @author zhoutaotao + * @date 2019/5/15 + */ +public class JsonUtil { + + private static final ObjectMapper mapper; + + static { + mapper = new ObjectMapper(); + initMapper(mapper); + } + + /** + * 获取 mapper + */ + public static ObjectMapper getMapper() { + return mapper; + } + + /** + * 初始化 mapper + */ + public static void initMapper(ObjectMapper mapper) { + // 注册不序列化 null 对象 + mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + // 序列化配置 + mapper.configure(SerializationFeature.FAIL_ON_EMPTY_BEANS, false); + mapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, true); + // 反序列化配置 + mapper.configure(DeserializationFeature.ACCEPT_EMPTY_STRING_AS_NULL_OBJECT, true); + mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + mapper.configure(DeserializationFeature.FAIL_ON_INVALID_SUBTYPE, false); + // 注册自定义类型 + mapper.registerModule(numericalAccuracy()); + mapper.registerModule(dateModule()); + mapper.registerModule(localDateModule()); + mapper.registerModule(localDateModuleTime()); + } + + + /** + * 处理数值精度的模块 + */ + public static SimpleModule numericalAccuracy() { + SimpleModule module = new SimpleModule(); + // 序列化 + module.addSerializer(new ToStringSerializer(Long.TYPE)); + module.addSerializer(new ToStringSerializer(Long.class)); + module.addSerializer(new ToStringSerializer(BigDecimal.class)); + + // 反序列化 + module.addDeserializer(Long.TYPE, new JsonDeserializer() { + @Override + public Long deserialize(JsonParser p, DeserializationContext ctx) + throws IOException, JsonProcessingException { + String value = p.getValueAsString(); + return StringUtil.isBlank(value) ? null : Long.parseLong(value.trim()); + } + }); + module.addDeserializer(Long.class, new JsonDeserializer() { + @Override + public Long deserialize(JsonParser p, DeserializationContext ctx) + throws IOException, JsonProcessingException { + String value = p.getValueAsString(); + return StringUtil.isBlank(value) ? null : Long.parseLong(value.trim()); + } + }); + module.addDeserializer(BigDecimal.class, new JsonDeserializer() { + @Override + public BigDecimal deserialize(JsonParser p, DeserializationContext ctx) + throws IOException, JsonProcessingException { + String value = p.getValueAsString(); + return StringUtil.isBlank(value) ? null : BigDecimal.valueOf(Long.parseLong(value.trim())); + } + }); + return module; + } + + /** + * java.util.Date 数据处理模块 + */ + public static SimpleModule dateModule() { + SimpleModule module = new SimpleModule(); + module.addSerializer(Date.class, new JsonSerializer() { + @Override + public void serialize(Date value, JsonGenerator gen, SerializerProvider serializers) + throws IOException { + gen.writeObject(ObjectUtil.isNull(value) ? value : DateUtil.getTimestamp(value)); + } + }); + module.addDeserializer(Date.class, new JsonDeserializer() { + @Override + public Date deserialize(JsonParser p, DeserializationContext ctx) + throws IOException, JsonProcessingException { + String value = p.getValueAsString(); + return StringUtil.isBlank(value) ? null : DateUtil.parseDate(Long.parseLong(value.trim())); + } + }); + return module; + } + + /** + * java.time.LocalDate 处理模块 + */ + public static SimpleModule localDateModule() { + SimpleModule module = new SimpleModule(); + module.addSerializer(LocalDate.class, new JsonSerializer() { + @Override + public void serialize(LocalDate value, JsonGenerator gen, SerializerProvider serializers) + throws IOException { + gen.writeObject(ObjectUtil.isNull(value) ? value : DateUtil.getTimestamp(value)); + } + }); + module.addDeserializer(LocalDate.class, new JsonDeserializer() { + @Override + public LocalDate deserialize(JsonParser p, DeserializationContext ctx) + throws IOException, JsonProcessingException { + String value = p.getValueAsString(); + return StringUtil.isBlank(value) ? null + : DateUtil.parseLocalDate(Long.parseLong(value.trim())); + } + }); + return module; + } + + /** + * java.time.LocalDateTime 处理模块 + */ + public static SimpleModule localDateModuleTime() { + SimpleModule module = new SimpleModule(); + module.addSerializer(LocalDateTime.class, new JsonSerializer() { + @Override + public void serialize(LocalDateTime value, JsonGenerator gen, SerializerProvider serializers) + throws IOException { + gen.writeObject(ObjectUtil.isNull(value) ? value : DateUtil.getTimestamp(value)); + } + }); + module.addDeserializer(LocalDateTime.class, new JsonDeserializer() { + @Override + public LocalDateTime deserialize(JsonParser p, DeserializationContext ctx) + throws IOException, JsonProcessingException { + String value = p.getValueAsString(); + return StringUtil.isBlank(value) ? null + : DateUtil.parseLocalDateTime(Long.parseLong(value.trim())); + } + }); + return module; + } + + /** + * 把 java 对象转换为 json 字符串 + */ + public static String toJson(T data) { + try { + return mapper.writeValueAsString(data); + } catch (JsonProcessingException e) { + throw new RuntimeException(e); + } + } + + /** + * 将 json 字符串转换为 java 对象 + */ + public static T fromJson(String json, Class clazz) { + try { + return mapper.readValue(json, clazz); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * 将 json 字符串数组转换为 java 数组 + */ + public static List fromJsonArray(String json, TypeReference> typeReference) { + try { + return mapper.readValue(json, typeReference); + } catch (IOException e) { + throw new RuntimeException(e); + } + } +} +``` + diff --git "a/java/\344\273\243\347\240\201\347\211\207/java8\346\227\245\346\234\237\344\271\213\351\227\264\347\232\204\350\275\254\346\215\242.md" "b/java/\344\273\243\347\240\201\347\211\207/java8\346\227\245\346\234\237\344\271\213\351\227\264\347\232\204\350\275\254\346\215\242.md" new file mode 100644 index 0000000..94fed0b --- /dev/null +++ "b/java/\344\273\243\347\240\201\347\211\207/java8\346\227\245\346\234\237\344\271\213\351\227\264\347\232\204\350\275\254\346\215\242.md" @@ -0,0 +1,119 @@ +## 使用JAVA8在Date、LocalDateTime等等之间的转换 + +```java + +import java.time.Instant; +import java.time.LocalDate; +import java.time.LocalDateTime; +import java.time.ZoneId; +import java.time.ZoneOffset; +import java.time.format.DateTimeFormatter; +import java.util.Date; + +/** + * 日期工具类 + * + * @author feb13th + * @since 2019/5/16 0:27 + */ +public class DateUtil { + + public static final String FORMAT_DATA_SIMPLE = "yyyymmdd"; + public static final String FORMAT_DATE = "yyyy-mm-dd"; + public static final String FORMAT_DATETIME = "yyyy-mm-dd HH:MM:ss"; + + /** + * 将日期格式化成 yyyy-mm-dd格式 + */ + public static String formatDate(LocalDate date) { + return formatDate(date, FORMAT_DATE); + } + + /** + * 将日期格式化成指定格式 + */ + public static String formatDate(LocalDate date, String format) { + DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(format); + return date.format(dateTimeFormatter); + } + + /** + * 将时间格式化成 yyyy-mm-dd HH:MM:ss 格式 + */ + public static String formatDateTime(LocalDateTime datetime) { + return formatDateTime(datetime, FORMAT_DATETIME); + } + + /** + * 将时间格式化成指定格式 + */ + public static String formatDateTime(LocalDateTime dateTime, String format) { + DateTimeFormatter dateTimeFormatter = DateTimeFormatter.ofPattern(format); + return dateTime.format(dateTimeFormatter); + } + + /** + * 获取 Date 对应的时间戳 + */ + public static long getTimestamp(Date date) { + return date.getTime(); + } + + /** + * 获取 LocalDate 对应的时间戳 + */ + public static long getTimestamp(LocalDate localDate) { + return localDate.atStartOfDay(ZoneId.systemDefault()).toInstant().toEpochMilli(); + } + + /** + * 获取 LocalDateTime 对应的时间戳 + */ + public static long getTimestamp(LocalDateTime localDateTime) { + return localDateTime.toInstant(ZoneOffset.ofHours(8)).toEpochMilli(); + } + + /** + * 根据时间戳获取 Date + */ + public static Date parseDate(long timestamp) { + return new Date(timestamp); + } + + /** + * 根据时间戳获取 LocalDate + */ + public static LocalDate parseLocalDate(long timestamp) { + return Instant.ofEpochMilli(timestamp).atZone(ZoneId.systemDefault()).toLocalDate(); + } + + /** + * 根据时间戳获取 LocalDateTime + */ + public static LocalDateTime parseLocalDateTime(long timestamp) { + return Instant.ofEpochMilli(timestamp).atZone(ZoneId.systemDefault()).toLocalDateTime(); + } + + /** + * 转换为 Date + */ + public static Date convertToDate(LocalDate localDate) { + return Date.from(localDate.atStartOfDay(ZoneId.systemDefault()).toInstant()); + } + + /** + * 转换为 LocalDate + */ + public static LocalDate convertToLocalDate(Date date) { + return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDate(); + } + + /** + * 转换为 LocalDateTime + */ + public static LocalDateTime convertToLocalDateTime(Date date) { + return date.toInstant().atZone(ZoneId.systemDefault()).toLocalDateTime(); + } +} +``` + diff --git "a/java/\344\273\243\347\240\201\347\211\207/zip\346\226\207\344\273\266\346\223\215\344\275\234.md" "b/java/\344\273\243\347\240\201\347\211\207/zip\346\226\207\344\273\266\346\223\215\344\275\234.md" new file mode 100644 index 0000000..b181ba5 --- /dev/null +++ "b/java/\344\273\243\347\240\201\347\211\207/zip\346\226\207\344\273\266\346\223\215\344\275\234.md" @@ -0,0 +1,168 @@ +## 通过java代码对zip文件进行压缩或解压操作 + +### 解压操作 + +#### 步骤 + +1. 创建`ZipFile`文件对象,标识`.zip`文件 +2. 创建`ZipInputStream`数据流对象用于标识`.zip`文件的输入流 +3. 循环读取`ZipEntry`, 拷贝此条目到解压后的文件中 + +#### 代码 + +```java + /** + * 解压zip文件 + * + * @param zipFilepath zip文件路径 + * @param unzipFilepath 解压路径 + */ + public void unzip(String zipFilepath, String unzipFilepath) { + + // 待解压的文件目录 + File file = new File(zipFilepath); + ZipInputStream zipInputStream = null; + try { + ZipFile zipFile = new ZipFile(file); + zipInputStream = new ZipInputStream(new FileInputStream(file)); + ZipEntry entry = null; + while ((entry = zipInputStream.getNextEntry()) != null) { + InputStream inputStream = null; + OutputStream outputStream = null; + try { + // 压缩文件内, 单条记录完整的文件夹名+文件名 + String entryName = entry.getName(); + System.out.println(entryName); + + File outFile = new File(unzipFilepath + File.separator + entryName); + if (!outFile.getParentFile().exists()) { + outFile.getParentFile().mkdirs(); + } + + // 如果是目录的话, 直接创建目录 + if (entry.isDirectory()) { + outFile.mkdirs(); + continue; + } + + if (!outFile.exists()) { + outFile.createNewFile(); + } + + // 拷贝文件内容 + copy(zipFile.getInputStream(entry), new FileOutputStream(outFile)); + } finally { + if (outputStream != null) { + outputStream.close(); + } + if (inputStream != null) { + inputStream.close(); + } + } + } + } catch (IOException e) { + throw new RuntimeException(e); + } finally { + // 关闭zip文件流 + if (zipInputStream != null) { + try { + zipInputStream.close(); + } catch (IOException ignore) { + } + } + } + } + + /** + * 从输入流拷贝数据到输出流 + * + * @param src 输入流 + * @param dest 输出流 + */ + private void copy(InputStream src, OutputStream dest) throws IOException { + int len = 0; + byte[] buffer = new byte[4096]; + while ((len = src.read(buffer)) != -1) { + dest.write(buffer, 0, len); + } + } + +``` + +### 压缩操作 + +#### 步骤 + +1. 创建`ZipOutputStream`输出流对象 +2. 判断当前读取的文件是文件还是目录 +3. 如果是文件,那么使用当前文件的名称创建`ZipEntry`对象, 并将该文件拷贝到`ZipOutputStream`流中 +4. 如果是目录, 则递归目录分别创建文件 + +#### 代码 + +```java + /** + * 压缩文件为zip + * + * @param srcFilepath 源文件或文件夹路径 + * @param destFilepath zip文件路径 + */ + public void toZip(String srcFilepath, String destFilepath) { + File srcFile = new File(srcFilepath); + if (!srcFile.exists()) { + throw new IllegalArgumentException("被压缩的文件或文件夹不存在"); + } + // 如果目标文件不存在,创建文件 + File zipFile = new File(destFilepath); + if (zipFile.exists()) { + zipFile.delete(); + } + if (zipFile.getParentFile().exists()) { + zipFile.getParentFile().mkdirs(); + } + ZipOutputStream outputStream = null; + try { + zipFile.createNewFile(); + outputStream = new ZipOutputStream(new FileOutputStream(zipFile)); + compress(outputStream, srcFile, ""); + } catch (IOException e) { + throw new RuntimeException(e); + } finally { + try { + outputStream.close(); + } catch (IOException ignore) { + } + } + } + + /** + * 递归压缩文件 + * + * @param outputStream zip文件输出流 + * @param srcFile 被压缩的文件 + * @param filenamePrefix zip文件内部的条目前缀 + */ + private void compress(ZipOutputStream outputStream, File srcFile, String filenamePrefix) + throws IOException { + if (srcFile.isDirectory()) { + File[] files = srcFile.listFiles(); + // 先写一个目录 + filenamePrefix = filenamePrefix.length() == 0 ? srcFile.getName() + : filenamePrefix + srcFile.getName(); + if (files == null) { + return; + } + // 递归写入所有存在的文件 + for (File file : files) { + compress(outputStream, file, filenamePrefix + "/"); + } + } else { + // 将单个文件写入到zip中 + outputStream.putNextEntry(new ZipEntry(filenamePrefix + srcFile.getName())); + try (InputStream inputStream = new FileInputStream(srcFile)) { + copy(inputStream, outputStream); + } + } + } +``` + diff --git "a/java/\344\273\243\347\240\201\347\211\207/\345\244\215\345\210\266\346\226\207\344\273\266\346\210\226\346\226\207\344\273\266\345\244\271.md" "b/java/\344\273\243\347\240\201\347\211\207/\345\244\215\345\210\266\346\226\207\344\273\266\346\210\226\346\226\207\344\273\266\345\244\271.md" new file mode 100644 index 0000000..e63fbfa --- /dev/null +++ "b/java/\344\273\243\347\240\201\347\211\207/\345\244\215\345\210\266\346\226\207\344\273\266\346\210\226\346\226\207\344\273\266\345\244\271.md" @@ -0,0 +1,82 @@ +## 将文件或文件夹从一个位置复制到另一个位置 + +### 代码 + +#### 复制文件 + +```java + /** + * 拷贝两个文件的内容 + * + * @param from 源文件 + * @param to 目标文件 + */ + public static void copyFile(File from, File to) { + FileInputStream fromIs = null; + FileOutputStream toOs = null; + FileChannel fromChannel = null; + FileChannel toChannel = null; + try { + if (!to.exists()) { + to.createNewFile(); + } + fromIs = new FileInputStream(from); + toOs = new FileOutputStream(to); + fromChannel = fromIs.getChannel(); + toChannel = toOs.getChannel(); + fromChannel.transferTo(0, fromChannel.size(), toChannel); + } catch (IOException e) { + throw new RuntimeException(e); + } finally { + close(fromChannel); + close(fromIs); + close(toChannel); + close(toOs); + } + } + + public static void close(Closeable closeable) { + if (closeable != null) { + try { + closeable.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } +``` + +#### 复制文件夹 + +```java + /** + * 目录内容拷贝,只拷贝文件内部的文件及文件夹 + * + * @param from 源目录 + * @param to 目标目录 + */ + public static void copyDir(File from, File to) { + String[] list; + if (!from.isDirectory() || (list = from.list()) == null) { + return; + } + String fromPath = from.getAbsolutePath(); + String toPath = to.getAbsolutePath(); + + for (String filename : list) { + String fromFilepath = fromPath + File.separator + filename; + String toFilepath = toPath + File.separator + filename; + File fromFile = new File(fromFilepath); + File toFile = new File(toFilepath); + if (fromFile.isDirectory()) { + // 如果赋值的是文件夹,则创建该文件夹, 并进入文件夹内部拷贝文件 + toFile.mkdirs(); + copyDir(fromFile, toFile); + } else { + // 拷贝文件内容 + copyFile(fromFile, toFile); + } + } + } +``` + diff --git "a/java/\344\273\243\347\240\201\347\211\207/\346\211\213\345\212\250\345\210\206\346\236\220\350\260\203\350\257\225json.md" "b/java/\344\273\243\347\240\201\347\211\207/\346\211\213\345\212\250\345\210\206\346\236\220\350\260\203\350\257\225json.md" new file mode 100644 index 0000000..8d5736d --- /dev/null +++ "b/java/\344\273\243\347\240\201\347\211\207/\346\211\213\345\212\250\345\210\206\346\236\220\350\260\203\350\257\225json.md" @@ -0,0 +1,344 @@ +## 抛开工具进行手动解析JSON + +### 测试代码 + +```java +public class JsonTest { + + public static void main(String[] args) { + String jsonString = "{\"str\":\"string\",\"num\":100,\"boolean\":true,\"obj\":{\"key1\":\"value1\",\"key2\":\"value2\"},\"list\":[{\"list1\":\"list1\"},{\"list2\":\"list2\"}]}"; + JSONReader jr = new JSONReader(); + Map map = (Map) jr.read(jsonString); + System.out.println("Json解析完成"); + System.out.println("Map----" + map.toString()); + System.out.println("list----" + map.get("list").getClass().getName() + ":" + map.get("list")); + System.out.println("str----" + map.get("str").getClass().getName() + ":" + map.get("str")); + System.out.println("num----" + map.get("num").getClass().getName() + ":" + map.get("num")); + System.out.println("boolean----" + map.get("boolean").getClass().getName() + ":" + map.get("boolean")); + System.out.println("obj----" + map.get("obj").getClass().getName() + ":" + map.get("obj")); + + } + +} +``` + +**输出** + +> Json解析完成 +> Map----{str=string, boolean=true, obj={key1=value1, key2=value2}, num=100, list=[{list1=list1}, {list2=list2}]} +> list----java.util.ArrayList:[{list1=list1}, {list2=list2}] +> str----java.lang.String:string +> num----java.lang.Long:100 +> boolean----java.lang.Boolean:true +> obj----java.util.HashMap:{key1=value1, key2=value2} + +### 解析代码 + +```java +package top.feb13th.json; + +import java.math.BigDecimal; +import java.math.BigInteger; +import java.text.CharacterIterator; +import java.text.StringCharacterIterator; +import java.util.ArrayList; +import java.util.HashMap; +import java.util.List; +import java.util.Map; + +public class JSONReader { + private static final Object OBJECT_END = new Object(); + private static final Object ARRAY_END = new Object(); + private static final Object COLON = new Object(); + private static final Object COMMA = new Object(); + public static final int FIRST = 0; + public static final int CURRENT = 1; + public static final int NEXT = 2; + + private static Map escapes = new HashMap(); + + static { + escapes.put(Character.valueOf('"'), Character.valueOf('"')); + escapes.put(Character.valueOf('\\'), Character.valueOf('\\')); + escapes.put(Character.valueOf('/'), Character.valueOf('/')); + escapes.put(Character.valueOf('b'), Character.valueOf('\b')); + escapes.put(Character.valueOf('f'), Character.valueOf('\f')); + escapes.put(Character.valueOf('n'), Character.valueOf('\n')); + escapes.put(Character.valueOf('r'), Character.valueOf('\r')); + escapes.put(Character.valueOf('t'), Character.valueOf('\t')); + } + + private CharacterIterator it; + private char c; + private Object token; + private StringBuffer buf = new StringBuffer(); + + private char next() { + c = it.next(); + return c; + } + + private void skipWhiteSpace() { + while (Character.isWhitespace(c)) { + next(); + } + } + + public Object read(CharacterIterator ci, int start) { + it = ci; + switch (start) { + case FIRST: + c = it.first(); + break; + case CURRENT: + c = it.current(); + break; + case NEXT: + c = it.next(); + break; + } + return read(); + } + + public Object read(CharacterIterator it) { + return read(it, NEXT); + } + + public Object read(String string) { + return read(new StringCharacterIterator(string), FIRST); + } + + private Object read() { + skipWhiteSpace(); + char ch = c; + next(); + switch (ch) { + case '"': + token = string(); + break; + case '[': + token = array(); + break; + case ']': + token = ARRAY_END; + break; + case ',': + token = COMMA; + break; + case '{': + token = object(); + break; + case '}': + token = OBJECT_END; + break; + case ':': + token = COLON; + break; + case 't': + next(); + next(); + next(); // assumed r-u-e + token = Boolean.TRUE; + break; + case 'f': + next(); + next(); + next(); + next(); // assumed a-l-s-e + token = Boolean.FALSE; + break; + case 'n': + next(); + next(); + next(); // assumed u-l-l + token = null; + break; + default: + c = it.previous(); + if (Character.isDigit(c) || c == '-') { + token = number(); + } + } + //logger.debug("token: " + token); + System.out.println("token: " + token); // enable this line to see the token stream + return token; + } + + private Object object() { + Map ret = new HashMap(); + Object key = read(); + while (token != OBJECT_END) { + read(); // should be a colon + if (token != OBJECT_END) { + ret.put(key, read()); + if (read() == COMMA) { + key = read(); + } + } + } + + return ret; + } + + private Object array() { + List ret = new ArrayList(); + Object value = read(); + while (token != ARRAY_END) { + ret.add(value); + if (read() == COMMA) { + value = read(); + } + } + return ret; + } + + private Object number() { + int length = 0; + boolean isFloatingPoint = false; + buf.setLength(0); + + if (c == '-') { + add(); + } + length += addDigits(); + if (c == '.') { + add(); + length += addDigits(); + isFloatingPoint = true; + } + if (c == 'e' || c == 'E') { + add(); + if (c == '+' || c == '-') { + add(); + } + addDigits(); + isFloatingPoint = true; + } + + String s = buf.toString(); + return isFloatingPoint + ? (length < 17) ? (Object) Double.valueOf(s) : new BigDecimal(s) + : (length < 19) ? (Object) Long.valueOf(s) : new BigInteger(s); + } + + private int addDigits() { + int ret; + for (ret = 0; Character.isDigit(c); ++ret) { + add(); + } + return ret; + } + + private Object string() { + buf.setLength(0); + while (c != '"') { + if (c == '\\') { + next(); + if (c == 'u') { + add(unicode()); + } else { + Object value = escapes.get(Character.valueOf(c)); + if (value != null) { + add(((Character) value).charValue()); + } + } + } else { + add(); + } + } + next(); + + return buf.toString(); + } + + private void add(char cc) { + buf.append(cc); + next(); + } + + private void add() { + add(c); + } + + private char unicode() { + int value = 0; + for (int i = 0; i < 4; ++i) { + switch (next()) { + case '0': + case '1': + case '2': + case '3': + case '4': + case '5': + case '6': + case '7': + case '8': + case '9': + value = (value << 4) + c - '0'; + break; + case 'a': + case 'b': + case 'c': + case 'd': + case 'e': + case 'f': + value = (value << 4) + c - 'k'; + break; + case 'A': + case 'B': + case 'C': + case 'D': + case 'E': + case 'F': + value = (value << 4) + c - 'K'; + break; + } + } + return (char) value; + } +} +``` + +**输出** + +> token: str +> token: java.lang.Object@2a098129 +> token: string +> token: java.lang.Object@198e2867 +> token: num +> token: java.lang.Object@2a098129 +> token: 100 +> token: java.lang.Object@198e2867 +> token: boolean +> token: java.lang.Object@2a098129 +> token: true +> token: java.lang.Object@198e2867 +> token: obj +> token: java.lang.Object@2a098129 +> token: key1 +> token: java.lang.Object@2a098129 +> token: value1 +> token: java.lang.Object@198e2867 +> token: key2 +> token: java.lang.Object@2a098129 +> token: value2 +> token: java.lang.Object@3ada9e37 +> token: {key1=value1, key2=value2} +> token: java.lang.Object@198e2867 +> token: list +> token: java.lang.Object@2a098129 +> token: list1 +> token: java.lang.Object@2a098129 +> token: list1 +> token: java.lang.Object@3ada9e37 +> token: {list1=list1} +> token: java.lang.Object@198e2867 +> token: list2 +> token: java.lang.Object@2a098129 +> token: list2 +> token: java.lang.Object@3ada9e37 +> token: {list2=list2} +> token: java.lang.Object@5cbc508c +> token: [{list1=list1}, {list2=list2}] +> token: java.lang.Object@3ada9e37 +> token: {str=string, boolean=true, obj={key1=value1, key2=value2}, num=100, list=[{list1=list1}, {list2=list2}]} \ No newline at end of file diff --git "a/java/\344\273\243\347\240\201\347\211\207/\346\223\215\344\275\234maven.md" "b/java/\344\273\243\347\240\201\347\211\207/\346\223\215\344\275\234maven.md" new file mode 100644 index 0000000..83784ac --- /dev/null +++ "b/java/\344\273\243\347\240\201\347\211\207/\346\223\215\344\275\234maven.md" @@ -0,0 +1,48 @@ +## 使用java调用maven + +### 依赖 + +```xml + + org.apache.maven.shared + maven-invoker + 3.0.1 + +``` + +### 代码 + +```java + // 调用请求 + InvocationRequest request = new DefaultInvocationRequest(); + request.setPomFile(new File("path to pom.xml"); + List command = new ArrayList<>(); + command.add("clean"); + command.add("package"); + request.setGoals(command); + + // 开始调用 + Invoker invoker = new DefaultInvoker(); + invoker.setMavenHome(new File("path to maven home")); + InvocationResult result = invoker.execute(request); + + // 更新状态为执行中 + updateState(MavenState.EXECUTING); + + int resultCode = result.getExitCode(); + + // 更新成功或失败状态 + if (resultCode == 0) { + // 执行成功 + } +``` + +**获取maven执行输出** + +```java +// 通过为 invoker 添加输出处理器监听maven产生的日志 +private void addMavenOutputHandler(Invoker invoker) { + invoker.setOutputHandler(line -> System.out::println); +} +``` + diff --git "a/java/\344\273\243\347\240\201\347\211\207/\346\225\260\345\255\227\350\275\254\344\270\255\346\226\207.md" "b/java/\344\273\243\347\240\201\347\211\207/\346\225\260\345\255\227\350\275\254\344\270\255\346\226\207.md" new file mode 100644 index 0000000..ecbb411 --- /dev/null +++ "b/java/\344\273\243\347\240\201\347\211\207/\346\225\260\345\255\227\350\275\254\344\270\255\346\226\207.md" @@ -0,0 +1,239 @@ +## 使用中文描述数字 + +### 代码 + +```java +/** + * 数字转中文类
    + * 包括: + *
    + * 1. 数字转中文大写形式,比如一百二十一
    + * 2. 数字转金额用的大写形式,比如:壹佰贰拾壹
    + * 3. 转金额形式,比如:壹佰贰拾壹整
    + * 
    + * + * @author fanqun,looly + **/ +public class NumberChineseFormater { + + /** 简体中文形式 **/ + private static final String[] simpleDigits = { "零", "一", "二", "三", "四", "五", "六", "七", "八", "九" }; + /** 繁体中文形式 **/ + private static final String[] traditionalDigits = { "零", "壹", "贰", "叁", "肆", "伍", "陆", "柒", "捌", "玖" }; + + /** 简体中文单位 **/ + private static final String[] simpleUnits = { "", "十", "百", "千" }; + /** 繁体中文单位 **/ + private static final String[] traditionalUnits = { "", "拾", "佰", "仟" }; + + /** + * 阿拉伯数字转换成中文,小数点后四舍五入保留两位. 使用于整数、小数的转换. + * + * @param amount 数字 + * @param isUseTraditional 是否使用繁体 + * @return 中文 + */ + public static String format(double amount, boolean isUseTraditional) { + return format(amount, isUseTraditional, false); + } + + /** + * 阿拉伯数字转换成中文,小数点后四舍五入保留两位. 使用于整数、小数的转换. + * + * @param amount 数字 + * @param isUseTraditional 是否使用繁体 + * @param isMoneyMode 是否为金额模式 + * @return 中文 + */ + public static String format(double amount, boolean isUseTraditional, boolean isMoneyMode) { + final String[] numArray = isUseTraditional ? traditionalDigits : simpleDigits; + + if (amount > 99999999999999.99 || amount < -99999999999999.99) { + throw new IllegalArgumentException("Number support only: (-99999999999999.99 ~ 99999999999999.99)!"); + } + + boolean negative = false; + if (amount < 0) { + negative = true; + amount = - amount; + } + + long temp = Math.round(amount * 100); + int numFen = (int) (temp % 10); + temp = temp / 10; + int numJiao = (int) (temp % 10); + temp = temp / 10; + + //将数字以万为单位分为多份 + int[] parts = new int[20]; + int numParts = 0; + for (int i = 0; temp != 0; i++) { + int part = (int) (temp % 10000); + parts[i] = part; + numParts++; + temp = temp / 10000; + } + + boolean beforeWanIsZero = true; // 标志“万”下面一级是不是 0 + + String chineseStr = ""; + for (int i = 0; i < numParts; i++) { + String partChinese = toChinese(parts[i], isUseTraditional); + if (i % 2 == 0) { + beforeWanIsZero = (null == partChinese || "".equals(partChinese.trim())); + } + + if (i != 0) { + if (i % 2 == 0) { + chineseStr = "亿" + chineseStr; + } else { + if ("".equals(partChinese) && false == beforeWanIsZero) { + // 如果“万”对应的 part 为 0,而“万”下面一级不为 0,则不加“万”,而加“零” + chineseStr = "零" + chineseStr; + } else { + if (parts[i - 1] < 1000 && parts[i - 1] > 0) { + // 如果"万"的部分不为 0, 而"万"前面的部分小于 1000 大于 0, 则万后面应该跟“零” + chineseStr = "零" + chineseStr; + } + chineseStr = "万" + chineseStr; + } + } + } + chineseStr = partChinese + chineseStr; + } + + // 整数部分为 0, 则表达为"零" + if ("".equals(chineseStr)) { + chineseStr = numArray[0]; + } + //负数 + if (negative) { // 整数部分不为 0 + chineseStr = "负" + chineseStr; + } + + // 小数部分 + if (numFen != 0 || numJiao != 0) { + if (numFen == 0) { + chineseStr += (isMoneyMode ? "元" : "点") + numArray[numJiao] + (isMoneyMode ? "角" : ""); + } else { // “分”数不为 0 + if (numJiao == 0) { + chineseStr += (isMoneyMode ? "元零" : "点零") + numArray[numFen] + (isMoneyMode ? "分" : ""); + } else { + chineseStr += (isMoneyMode ? "元" : "点") + numArray[numJiao] + (isMoneyMode ? "角" : "") + numArray[numFen] + (isMoneyMode ? "分" : ""); + } + } + }else if(isMoneyMode) { + //无小数部分的金额结尾 + chineseStr += "元整"; + } + + return chineseStr; + + } + + /** + * 把一个 0~9999 之间的整数转换为汉字的字符串,如果是 0 则返回 "" + * + * @param amountPart 数字部分 + * @param isUseTraditional 是否使用繁体单位 + * @return 转换后的汉字 + */ + private static String toChinese(int amountPart, boolean isUseTraditional) { + + String[] numArray = isUseTraditional ? traditionalDigits : simpleDigits; + String[] units = isUseTraditional ? traditionalUnits : simpleUnits; + + int temp = amountPart; + + String chineseStr = ""; + boolean lastIsZero = true; // 在从低位往高位循环时,记录上一位数字是不是 0 + for (int i = 0; temp > 0; i++) { + if (temp == 0) { + // 高位已无数据 + break; + } + int digit = temp % 10; + if (digit == 0) { // 取到的数字为 0 + if (false == lastIsZero) { + // 前一个数字不是 0,则在当前汉字串前加“零”字; + chineseStr = "零" + chineseStr; + } + lastIsZero = true; + } else { // 取到的数字不是 0 + chineseStr = numArray[digit] + units[i] + chineseStr; + lastIsZero = false; + } + temp = temp / 10; + } + return chineseStr; + } +} + +``` + +### 测试 + +```java + @Test + public void formatTest() { + String f1 = NumberChineseFormater.format(10889.72356, false); + Assert.assertEquals("一万零八百八十九点七二", f1); + f1 = NumberChineseFormater.format(12653, false); + Assert.assertEquals("一万二千六百五十三", f1); + f1 = NumberChineseFormater.format(215.6387, false); + Assert.assertEquals("二百一十五点六四", f1); + f1 = NumberChineseFormater.format(1024, false); + Assert.assertEquals("一千零二十四", f1); + f1 = NumberChineseFormater.format(100350089, false); + Assert.assertEquals("一亿三十五万零八十九", f1); + f1 = NumberChineseFormater.format(1200, false); + Assert.assertEquals("一千二百", f1); + f1 = NumberChineseFormater.format(12, false); + Assert.assertEquals("一十二", f1); + f1 = NumberChineseFormater.format(0.05, false); + Assert.assertEquals("零点零五", f1); + } + + @Test + public void formatTest2() { + String f1 = NumberChineseFormater.format(-0.3, false, false); + Assert.assertEquals("负零点三", f1); + } + + @Test + public void formatTranditionalTest() { + String f1 = NumberChineseFormater.format(10889.72356, true); + Assert.assertEquals("壹万零捌佰捌拾玖点柒贰", f1); + f1 = NumberChineseFormater.format(12653, true); + Assert.assertEquals("壹万贰仟陆佰伍拾叁", f1); + f1 = NumberChineseFormater.format(215.6387, true); + Assert.assertEquals("贰佰壹拾伍点陆肆", f1); + f1 = NumberChineseFormater.format(1024, true); + Assert.assertEquals("壹仟零贰拾肆", f1); + f1 = NumberChineseFormater.format(100350089, true); + Assert.assertEquals("壹亿叁拾伍万零捌拾玖", f1); + f1 = NumberChineseFormater.format(1200, true); + Assert.assertEquals("壹仟贰佰", f1); + f1 = NumberChineseFormater.format(12, true); + Assert.assertEquals("壹拾贰", f1); + f1 = NumberChineseFormater.format(0.05, true); + Assert.assertEquals("零点零伍", f1); + } + + @Test + public void digitToChineseTest() { + String digitToChinese = Convert.digitToChinese(12412412412421.12); + Assert.assertEquals("壹拾贰万肆仟壹佰贰拾肆亿壹仟贰佰肆拾壹万贰仟肆佰贰拾壹元壹角贰分", digitToChinese); + + String digitToChinese2 = Convert.digitToChinese(12412412412421D); + Assert.assertEquals("壹拾贰万肆仟壹佰贰拾肆亿壹仟贰佰肆拾壹万贰仟肆佰贰拾壹元整", digitToChinese2); + + String digitToChinese3 = Convert.digitToChinese(2421.02); + Assert.assertEquals("贰仟肆佰贰拾壹元零贰分", digitToChinese3); + } +``` + +## 引用 + +[Hutool NumberChineseFormater](https://github.com/looly/hutool/blob/v4-master/hutool-core/src/main/java/cn/hutool/core/convert/NumberChineseFormater.java) + diff --git "a/java/\344\273\243\347\240\201\347\211\207/\346\225\260\345\255\227\350\275\254\350\213\261\346\226\207.md" "b/java/\344\273\243\347\240\201\347\211\207/\346\225\260\345\255\227\350\275\254\350\213\261\346\226\207.md" new file mode 100644 index 0000000..38e83dd --- /dev/null +++ "b/java/\344\273\243\347\240\201\347\211\207/\346\225\260\345\255\227\350\275\254\350\213\261\346\226\207.md" @@ -0,0 +1,176 @@ +## 使用英文描述数字 + +### 代码 + +```java +/** + * 将浮点数类型的number转换成英语的表达方式
    + * 参考博客:http://blog.csdn.net/eric_sunah/article/details/8713226 + * + * @author Looly + * @since 3.0.9 + * @see http://blog.csdn.net/eric_sunah/article/details/8713226 + */ +public class NumberWordFormater { + + private static final String[] NUMBER = new String[] { "", "ONE", "TWO", "THREE", "FOUR", "FIVE", "SIX", "SEVEN", + "EIGHT", "NINE" }; + private static final String[] NUMBER_TEEN = new String[] { "TEN", "ELEVEN", "TWELEVE", "THIRTEEN", "FOURTEEN", + "FIFTEEN", "SIXTEEN", "SEVENTEEN", "EIGHTEEN", "NINETEEN" }; + private static final String[] NUMBER_TEN = new String[] { "TEN", "TWENTY", "THIRTY", "FORTY", "FIFTY", "SIXTY", + "SEVENTY", "EIGHTY", "NINETY" }; + private static final String[] NUMBER_MORE = new String[] { "", "THOUSAND", "MILLION", "BILLION" }; + + /** + * 将阿拉伯数字转为英文表达式 + * + * @param x + * 阿拉伯数字,可以为{@link Number}对象,也可以是普通对象,最后会使用字符串方式处理 + * @return 英文表达式 + */ + public static String format(Object x) { + if (x != null) { + return format(x.toString()); + } else { + return ""; + } + } + + /** + * 将阿拉伯数字转为英文表达式 + * + * @param x + * 阿拉伯数字字符串 + * @return 英文表达式 + */ + private static String format(String x) { + int z = x.indexOf("."); // 取小数点位置 + String lstr = "", rstr = ""; + if (z > -1) { // 看是否有小数,如果有,则分别取左边和右边 + lstr = x.substring(0, z); + rstr = x.substring(z + 1); + } else { + // 否则就是全部 + lstr = x; + } + + String lstrrev = reverse(lstr); // 对左边的字串取反 + String[] a = new String[5]; // 定义5个字串变量来存放解析出来的叁位一组的字串 + + switch (lstrrev.length() % 3) { + case 1: + lstrrev += "00"; + break; + case 2: + lstrrev += "0"; + break; + } + String lm = ""; // 用来存放转换後的整数部分 + for (int i = 0; i < lstrrev.length() / 3; i++) { + a[i] = reverse(lstrrev.substring(3 * i, 3 * i + 3)); // 截取第一个叁位 + if (!a[i].equals("000")) { // 用来避免这种情况:1000000 = one million + // thousand only + if (i != 0) { + lm = transThree(a[i]) + " " + parseMore(String.valueOf(i)) + " " + lm; // 加: + // thousand、million、billion + } else { + lm = transThree(a[i]); // 防止i=0时, 在多加两个空格. + } + } else { + lm += transThree(a[i]); + } + } + + String xs = ""; // 用来存放转换後小数部分 + if (z > -1) { + xs = "AND CENTS " + transTwo(rstr) + " "; // 小数部分存在时转换小数 + } + + return lm.trim() + " " + xs + "ONLY"; + } + + private static String reverse(String substring) { + char[] chars = substring.toCharArray(); + int len = chars.length - 1; + for (int i = 0; i < chars.length / 2; i++) { + char tmp = chars[i]; + chars[i] = chars[len]; + chars[len] = tmp; + len--; + } + + return new String(chars); + } + + private static String parseFirst(String s) { + return NUMBER[Integer.parseInt(s.substring(s.length() - 1))]; + } + + private static String parseTeen(String s) { + return NUMBER_TEEN[Integer.parseInt(s) - 10]; + } + + private static String parseTen(String s) { + return NUMBER_TEN[Integer.parseInt(s.substring(0, 1)) - 1]; + } + + private static String parseMore(String s) { + return NUMBER_MORE[Integer.parseInt(s)]; + } + + // 两位 + private static String transTwo(String s) { + String value = ""; + // 判断位数 + if (s.length() > 2) { + s = s.substring(0, 2); + } else if (s.length() < 2) { + s = "0" + s; + } + + if (s.startsWith("0")) {// 07 - seven 是否小於10 + value = parseFirst(s); + } else if (s.startsWith("1")) {// 17 seventeen 是否在10和20之间 + value = parseTeen(s); + } else if (s.endsWith("0")) {// 是否在10与100之间的能被10整除的数 + value = parseTen(s); + } else { + value = parseTen(s) + " " + parseFirst(s); + } + return value; + } + + // 制作叁位的数 + // s.length = 3 + private static String transThree(String s) { + String value = ""; + if (s.startsWith("0")) {// 是否小於100 + value = transTwo(s.substring(1)); + } else if (s.substring(1).equals("00")) {// 是否被100整除 + value = parseFirst(s.substring(0, 1)) + " HUNDRED"; + } else { + value = parseFirst(s.substring(0, 1)) + " HUNDRED AND " + transTwo(s.substring(1)); + } + return value; + } +} +``` + +### 测试 + +```java + @Test + public void formatTest() { + String format = NumberWordFormater.format(100.23); + Assert.assertEquals("ONE HUNDRED AND CENTS TWENTY THREE ONLY", format); + + String format2 = NumberWordFormater.format("2100.00"); + Assert.assertEquals("TWO THOUSAND ONE HUNDRED AND CENTS ONLY", format2); + } +``` + +### 引用 + +[Hutool NumberWordFormater](https://github.com/looly/hutool/blob/v4-master/hutool-core/src/main/java/cn/hutool/core/convert/NumberWordFormater.java) + diff --git "a/java/\344\273\243\347\240\201\347\211\207/\346\226\207\344\273\266\346\223\215\344\275\234\345\267\245\345\205\267\347\261\273.md" "b/java/\344\273\243\347\240\201\347\211\207/\346\226\207\344\273\266\346\223\215\344\275\234\345\267\245\345\205\267\347\261\273.md" new file mode 100644 index 0000000..9f7cc7e --- /dev/null +++ "b/java/\344\273\243\347\240\201\347\211\207/\346\226\207\344\273\266\346\223\215\344\275\234\345\267\245\345\205\267\347\261\273.md" @@ -0,0 +1,294 @@ +## 文件操作工具类 + +### 代码 + +```java +package top.feb13th.deploy.util; + +import java.io.Closeable; +import java.io.File; +import java.io.FileInputStream; +import java.io.FileOutputStream; +import java.io.IOException; +import java.nio.channels.FileChannel; +import java.nio.charset.StandardCharsets; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.Paths; +import java.util.Arrays; +import java.util.StringJoiner; + +/** + * 输入输出工具类 + * + * @author zhoutaotao + * @date 2019/9/24 16:26 + */ +public class IoUtil { + + /** + * 获取系统临时目录 + * + * @param dirs 在临时目录后额外增加的文件名称 + * @return 系统临时目录 + */ + public static String getTmpdir(String... dirs) { + String tmpdir = System.getProperty("java.io.tmpdir"); + StringJoiner joiner = new StringJoiner(File.separator); + joiner.add(tmpdir); + for (String dir : dirs) { + joiner.add(dir); + } + return joiner.toString(); + } + + /** + * 获取应用程序的运行路径 + * + * @param dirs 子目录 + * @return 应用程序运行目录 + 子目录 + */ + public static String getAppDir(String... dirs) { + // 获取当前程序的运行路径 + String location = IoUtil.class.getProtectionDomain().getCodeSource().getLocation() + .getFile(); + if (location.startsWith("file:")) { + location = location.substring(5); + } + if (location.contains(".jar")) { + location = location.substring(0, location.indexOf(".jar")); + File file = new File(location); + location = file.getParent(); + } + File file = new File(location); + if (file.isFile()) { + // 如果当前是执行的jar文件, 获取jar文件的所在目录 + location = file.getParent(); + } + return appendPath(location, dirs); + } + + /** + * 拼接路径 + * + * @param prefix 前缀 + * @param paths 路径列表 + * @return 路径 + */ + public static String appendPath(String prefix, String... paths) { + if (StringUtil.isBlank(prefix)) { + return null; + } + StringJoiner joiner = new StringJoiner(File.separator); + joiner.add(prefix); + for (String path : paths) { + joiner.add(path); + } + return joiner.toString(); + } + + /** + * 删除文件 + * + * @param dirPath 文件路径 + */ + public static void delete(String dirPath) { + // 文件路径为空时, 直接返回 + if (StringUtil.isBlank(dirPath)) { + return; + } + File dirFile = new File(dirPath); + String[] list = dirFile.list(); + if (list != null && list.length > 0) { + for (String filename : list) { + String newFilePath = dirPath + File.separator + filename; + File file = new File(newFilePath); + if (file.isDirectory()) { + delete(newFilePath); + } + file.delete(); + } + } + if (dirFile.exists()) { + dirFile.delete(); + } + } + + /** + * 目录内容拷贝,只拷贝文件内部的文件及文件夹 + * + * @param from 源目录 + * @param to 目标目录 + */ + public static void copyDir(File from, File to) { + String[] list; + if (!from.isDirectory() || (list = from.list()) == null) { + return; + } + String fromPath = from.getAbsolutePath(); + String toPath = to.getAbsolutePath(); + + for (String filename : list) { + String fromFilepath = fromPath + File.separator + filename; + String toFilepath = toPath + File.separator + filename; + File fromFile = new File(fromFilepath); + File toFile = new File(toFilepath); + if (fromFile.isDirectory()) { + // 如果赋值的是文件夹,则创建该文件夹, 并进入文件夹内部拷贝文件 + toFile.mkdirs(); + copyDir(fromFile, toFile); + } else { + // 拷贝文件内容 + copyFile(fromFile, toFile); + } + } + } + + /** + * 拷贝两个文件的内容 + * + * @param from 源文件 + * @param to 目标文件 + */ + public static void copyFile(File from, File to) { + FileInputStream fromIs = null; + FileOutputStream toOs = null; + FileChannel fromChannel = null; + FileChannel toChannel = null; + try { + if (!to.exists()) { + createFile(to); + } + fromIs = new FileInputStream(from); + toOs = new FileOutputStream(to); + fromChannel = fromIs.getChannel(); + toChannel = toOs.getChannel(); + fromChannel.transferTo(0, fromChannel.size(), toChannel); + } catch (IOException e) { + throw new RuntimeException(e); + } finally { + close(fromChannel); + close(fromIs); + close(toChannel); + close(toOs); + } + } + + public static void close(Closeable closeable) { + if (closeable != null) { + try { + closeable.close(); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + } + + /** + * 从指定文件中读取字符串 + * + * @param filepath 文件路径 + * @return 如果文件不存在则返回null, 否则返回文件字符串内容 + */ + public static String readString(String filepath) { + Path path = Paths.get(new File(filepath).toURI()); + if (!Files.exists(path)) { + return null; + } + try { + return Files.readAllLines(path, StandardCharsets.UTF_8).stream().reduce("", String::concat); + } catch (IOException e) { + return null; + } + } + + /** + * 向文件中写入content + * + * @param filepath 文件路径 + * @param content 文件内容字符串 + */ + public static void writeString(String filepath, String... content) { + try { + Path path = Paths.get(new File(filepath).toURI()); + Files.deleteIfExists(path); + Path parent = path.getParent(); + if (!Files.exists(parent)) { + Files.createDirectories(parent); + } + Files.createFile(path); + Files.write(path, Arrays.asList(content), StandardCharsets.UTF_8); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * 创建新文件, 如果存在则删除重新创建 + * + * @param filepath 文件路径 + */ + public static void createFile(String filepath) { + if (StringUtil.isBlank(filepath)) { + throw new NullPointerException("filepath must not empty"); + } + createFile(new File(filepath)); + } + + /** + * 创建新文件, 如果存在则删除重新创建 + * + * @param file 文件 + */ + public static void createFile(File file) { + if (ObjectUtil.isNull(file)) { + throw new NullPointerException("core must not null"); + } + try { + Path path = Paths.get(file.toURI()); + Files.deleteIfExists(path); + Path parent = path.getParent(); + if (!Files.exists(parent)) { + Files.createDirectories(parent); + } + Files.createFile(path); + } catch (IOException e) { + throw new RuntimeException(e); + } + } + + /** + * 创建目录 + * + * @param directorPath 目录路径 + */ + public static void createDirector(String directorPath) { + if (StringUtil.isBlank(directorPath)) { + throw new NullPointerException("director path must not empty"); + } + createDirector(new File(directorPath)); + } + + /** + * 创建目录 + * + * @param file 目录 + */ + public static void createDirector(File file) { + if (ObjectUtil.isNull(file)) { + throw new NullPointerException("director core must not null"); + } + try { + Path path = Paths.get(file.toURI()); + if (!Files.exists(path)) { + Files.createDirectories(path); + } + } catch (IOException e) { + throw new RuntimeException(e); + } + } + +} + +``` + diff --git "a/java/\344\273\243\347\240\201\347\211\207/\350\204\232\346\234\254\350\260\203\347\224\250.md" "b/java/\344\273\243\347\240\201\347\211\207/\350\204\232\346\234\254\350\260\203\347\224\250.md" new file mode 100644 index 0000000..b9b270e --- /dev/null +++ "b/java/\344\273\243\347\240\201\347\211\207/\350\204\232\346\234\254\350\260\203\347\224\250.md" @@ -0,0 +1,22 @@ +## 调用服务器本地脚本 + +### 代码 + +```java + public static String exec(String command) { + StringBuilder sb = new StringBuilder(); + try { + Process exec = Runtime.getRuntime().exec(command); + exec.waitFor(); + BufferedReader br = new BufferedReader(new InputStreamReader(exec.getInputStream())); + String tmp = null; + while ((tmp = br.readLine()) != null) { + sb.append(tmp); + } + } catch (IOException | InterruptedException e) { + throw new RuntimeException(e); + } + return sb.toString(); + } +``` + diff --git "a/java/\344\273\243\347\240\201\347\211\207/\350\216\267\345\217\226\346\226\207\344\273\266\345\210\233\345\273\272\346\227\266\351\227\264.md" "b/java/\344\273\243\347\240\201\347\211\207/\350\216\267\345\217\226\346\226\207\344\273\266\345\210\233\345\273\272\346\227\266\351\227\264.md" new file mode 100644 index 0000000..ab40f13 --- /dev/null +++ "b/java/\344\273\243\347\240\201\347\211\207/\350\216\267\345\217\226\346\226\207\344\273\266\345\210\233\345\273\272\346\227\266\351\227\264.md" @@ -0,0 +1,25 @@ +## 获取文件的创建时间 + +### 代码 + +由于linux下不能获取文件的创建时间,并且java中没有对应获取文件创建时间的api,只有获取修改时间的api,所以如果想在windows下获取创建时间可以这样(适用于windows和linux,linux下获取的是访问时间即修改时间,windows下获取的是创建时间): + +```java +private Long getFileCreateTime(String filePath){ + File file = new File(filePath); + try { + Path path= Paths.get(filePath); + BasicFileAttributeView basicview= Files.getFileAttributeView(path, BasicFileAttributeView.class, LinkOption.NOFOLLOW_LINKS ); + BasicFileAttributes attr = basicview.readAttributes(); + return attr.creationTime().toMillis(); + } catch (Exception e) { + e.printStackTrace(); + return file.lastModified(); + } +} +``` + +### 引用 + +[java获取文件的创建时间](https://blog.csdn.net/qingfengmuzhu1993/article/details/84238731) + diff --git "a/java/\344\275\277\347\224\250\345\221\275\344\273\244\347\274\226\350\257\221\346\211\223\345\214\205.md" "b/java/\344\275\277\347\224\250\345\221\275\344\273\244\347\274\226\350\257\221\346\211\223\345\214\205.md" new file mode 100644 index 0000000..12e25ea --- /dev/null +++ "b/java/\344\275\277\347\224\250\345\221\275\344\273\244\347\274\226\350\257\221\346\211\223\345\214\205.md" @@ -0,0 +1,177 @@ +## 目录准备 + +1. 创建项目根目录 `project` +2. 创建源码目录`project/src` +3. 创建编译文件后的文件目录`project/classes` +4. 创建`lib`库目录`project/lib` + +## 编译 + +### 不依赖任何类库 + +1、在`project/src`目录下创建`com/feb13th/demo1`文件夹及其子文件夹。 + +2、在`demo1`文件夹下创建`HelloWorld.java`文件。 + +`package`用于指定当前文件的包名,如果当前文件在根目录(本项目中是`src`目录),那么可以省略`package`一行。 + +`import`用于引入其他在项目中可能用到的文件。格式为`包名.文件名`(文件名需去除**.java**后缀)。 + +`HelloWorld.java` + +```java +package com.feb13th.demo1; + +import com.feb13th.demo1.util.MyTools; + +public class HelloWorld { + public static void main(String[] args) { + System.out.println("Hello World!!"); + System.out.println(new MyTools().getTime()); + } +} +``` + +3、在`demo1`文件夹下创建`util`文件夹用于存在`MyTools.java`文件。 + +`MyTools.java` + +```java +package com.feb13th.demo1.util; + +import java.util.Date; + +public class MyTools { + public String getTime() { + return new Date().toString(); + } +} +``` + +4、在`project`文件夹下执行如下命令编译: + +```shell +javac -d .\classes .\src\com\feb13th\demo1\HelloWorld.java .\src\com\feb13th\demo1\util\MyTools.java +``` + +目录分隔符根据操作系统划分,windows为**\\**, linux为**/** + +**-d** 用于指定文件编译后的输出目录。 + +后面跟的两项为我们要编译的**java**源文件。如果在同一个目录下存在多个需要编译的源文件,可以使用匹配符***.java**,如`.\src\com\feb13th\demo1\*.java`。 + +**注: ** ***.java**只能匹配与单个文件夹,对其子文件无效。 + +### 依赖类库 + +将依赖的类库文件放入`lib`目录下。如`lib\fastjson-1.2.58.jar` + +1、修改`HelloWorld.java` + +```java +package com.feb13th.demo1; + +import com.feb13th.demo1.util.MyTools; +import com.alibaba.fastjson.JSONObject; + +public class HelloWorld { + public static void main(String[] args) { + System.out.println("Hello World!!"); + System.out.println(new MyTools().getTime()); + JSONObject obj = new JSONObject(); + obj.put("1", "one"); + obj.put("2", "two"); + System.out.println(obj.toJSONString()); + } +} +``` + +导入`fastjson`对象,并使用该对象生成`json`字符串。 + +2、使用`-classpath`指定类库文件 + +```shell +javac -d .\classes -classpath .\lib\fastjson-1.2.58.jar .\src\com\feb13th\demo1\HelloWorld.java .\src\com\feb13th\demo1\util\MyTools.java +``` + +**-classpath** 用于指定代码编译时使用类库的位置,多个类库可以使用分隔符进行分割,windows使用 **;** ,linux使用 **:** + + + +## 运行 + +`java`指令用于运行**java**程序 + +### 运行未依赖类库的程序 + +执行`java`指令需要在编译文件的根目录 + +```shell +java com.feb13th.demo1.HelloWorld +``` + +语法为:`java main函数所在的文件` + +### 运行依赖类库的程序 + +切换到`project`目录。执行如下脚本 + +```shell +java -classpath ./classes/;./lib/fastjson-1.2.58.jar;./dependency/* com.feb13th.demo1.HelloWorld +``` + +**-classpath** 用于指定可执行文件的位置。 + +`./classes`:刚刚编译的class文件目录 + +`./lib/fastjson-1.2.58.jar`:程序中依赖的类库。 + +`./dependency/*`:存放jar的文件夹,当我们有很多jar的时候,一个个指定classpath无疑费时费力,那么我们可以将所有的jar存放到一个目录中,使用通配符进行指定classpath。 + +多个classpath之间使用分隔符进行分割,windows使用 **;** ,linux使用 **:** + +**注:** 指定jar包时,不能通过匹配符进行匹配。例如`spring-*.jar` + + + +## 打包 + +1、拷贝`lib`目录到`classes`目录下 + +2、编写`MANIFEST.MF`文件。 + +``` +Manifest-Version: 1.0 +Created-By: 1.8.0_71 (Oracle Corporation) +Main-Class: com.feb13th.demo1.HelloWorld +Class-Path: lib/fastjson-1.2.58.jar + + +``` + +该文件最后必须**保留两个空行**。**:**(冒号)后面必须跟一个空格。 + +**Manifest-Version**:指定版本号(可选) + +**Created-By**:构建的JDK版本(可选) + +**Main-Class**:main方法所有在类,如果指定必须唯一。 + +**Class-Path**:jar包中访问第三方类库的路径,和本地jar包的存储路径不是一个东西。多个类库之间使用 **空格** 分割。如果没有依赖第三方库可以不指定。 + +3、切换到`project`目录,执行打包脚本 + +```shell +jar -cvfm my.jar ./MANIFEST.MF -C ./classes . +``` + +**-C** 用于指定目录并包含该目录下的文件, **.** (点)用于匹配当前文件夹下的所有文件。 + +4、运行jar + +```shell +java -jar my.jar +``` + +也可以指定**-classpath** + diff --git "a/java/\345\212\250\346\200\201\344\273\243\347\220\206.md" "b/java/\345\212\250\346\200\201\344\273\243\347\220\206.md" new file mode 100644 index 0000000..38a2a0e --- /dev/null +++ "b/java/\345\212\250\346\200\201\344\273\243\347\220\206.md" @@ -0,0 +1,312 @@ +# Java核心技术点之动态代理 + +​ 本篇博文会从代理的概念出发,介绍Java中动态代理技术的使用,并进一步探索它的实现原理。由于个人水平有限,叙述中难免出现不清晰或是不准确的地方,希望大家可以指正,谢谢大家:) + +# 一、概述 + +## 1. 什么是代理 + +​ 我们大家都知道微商代理,简单地说就是代替厂家卖商品,厂家“委托”代理为其销售商品。关于微商代理,首先我们从他们那里买东西时通常不知道背后的厂家究竟是谁,也就是说,“委托者”对我们来说是不可见的;其次,微商代理主要以朋友圈的人为目标客户,这就相当于为厂家做了一次对客户群体的“过滤”。我们把微商代理和厂家进一步抽象,前者可抽象为代理类,后者可抽象为委托类(被代理类)。通过使用代理,通常有两个优点,并且能够分别与我们提到的微商代理的两个特点对应起来: + +- 优点一:可以隐藏委托类的实现; +- 优点二:可以实现客户与委托类间的解耦,在不修改委托类代码的情况下能够做一些额外的处理。 + +## + +## 2. 静态代理 + +​ 若代理类在程序运行前就已经存在,那么这种代理方式被成为**静态代理**,这种情况下的代理类通常都是我们在Java代码中定义的。 通常情况下,**静态代理中的代理类和委托类会实现同一接口或是派生自相同的父类。**下面我们用Vendor类代表生产厂家,BusinessAgent类代表微商代理,来介绍下静态代理的简单实现,委托类和代理类都实现了Sell接口,Sell接口的定义如下: + +```java +public interface Sell { + void sell(); + void ad(); +} +``` + + + +​ Vendor类的定义如下: + +``` +public class Vendor implements Sell { + public void sell() { + System.out.println("In sell method"); + } + public void ad() { + System,out.println("ad method") + } +} +``` + + + +​ 代理类BusinessAgent的定义如下: + +```java + 1 public class BusinessAgent implements Sell { + 2 private Vendor mVendor; + 3 + 4 public BusinessAgent(Vendor vendor) { + 5 mVendor = vendor; + 6 } + 7 + 8 public void sell() { mVendor.sell(); } + 9 public void ad() { mVendor.ad(); } +10 } +``` + +​ 从BusinessAgent类的定义我们可以了解到,静态代理可以通过聚合来实现,让代理类持有一个委托类的引用即可。 + +​ 下面我们考虑一下这个需求:给Vendor类增加一个过滤功能,只卖货给大学生。通过静态代理,我们无需修改Vendor类的代码就可以实现,只需在BusinessAgent类中的sell方法中添加一个判断即可如下所示: + +```java +public class BusinessAgent implements Sell { + ... + public void sell() { + if (isCollegeStudent()) { + vendor.sell(); + } + } + ... +} +``` + +​ 这对应着我们上面提到的使用代理的第二个优点:可以实现客户与委托类间的解耦,在不修改委托类代码的情况下能够做一些额外的处理。静态代理的局限在于运行前必须编写好代理类,下面我们重点来介绍下运行时生成代理类的动态代理方式。 + + + +# 二、动态代理 + +## 1. 什么是动态代理 + +​ 代理类在程序运行时创建的代理方式被成为**动态代理。**也就是说,这种情况下,代理类并不是在Java代码中定义的,而是在运行时根据我们在Java代码中的“指示”动态生成的。相比于静态代理,**动态代理的优势在于可以很方便的对代理类的函数进行统一的处理,而不用修改每个代理类的函数。**这么说比较抽象,下面我们结合一个实例来介绍一下动态代理的这个优势是怎么体现的。 + +​ 现在,假设我们要实现这样一个需求:在执行委托类中的方法之前输出“before”,在执行完毕后输出“after”。我们还是以上面例子中的Vendor类作为委托类,BusinessAgent类作为代理类来进行介绍。首先我们来使用静态代理来实现这一需求,相关代码如下: + +```java +public class BusinessAgent implements Sell { + private Vendor mVendor; + + public BusinessAgent(Vendor vendor) { + this.mVendor = vendor; + } + + public void sell() { + System.out.println("before"); + mVendor.sell(); + System.out.println("after"); + } + + public void ad() { + System.out.println("before"); + mVendor.ad(); + System.out.println("after"); + } +} +``` + +​ 从以上代码中我们可以了解到,通过静态代理实现我们的需求需要我们在每个方法中都添加相应的逻辑,这里只存在两个方法所以工作量还不算大,假如Sell接口中包含上百个方法呢?这时候使用静态代理就会编写许多冗余代码。通过使用动态代理,我们可以做一个“统一指示”,从而对所有代理类的方法进行统一处理,而不用逐一修改每个方法。下面我们来具体介绍下如何使用动态代理方式实现我们的需求。 + +## + +## 2. 使用动态代理 + +### (1)InvocationHandler接口 + +​ 在使用动态代理时,我们需要定义一个位于代理类与委托类之间的中介类,这个中介类被要求实现InvocationHandler接口,这个接口的定义如下: + +```java +public interface InvocationHandler { + Object invoke(Object proxy, Method method, Object[] args); +} +``` + +​ 从InvocationHandler这个名称我们就可以知道,实现了这个接口的中介类用做“调用处理器”。当我们调用代理类对象的方法时,这个“调用”会转送到invoke方法中,代理类对象作为proxy参数传入,参数method标识了我们具体调用的是代理类的哪个方法,args为这个方法的参数。这样一来,我们对代理类中的所有方法的调用都会变为对invoke的调用,这样我们可以在invoke方法中添加统一的处理逻辑(也可以根据method参数对不同的代理类方法做不同的处理)。因此我们只需在中介类的invoke方法实现中输出“before”,然后调用委托类的invoke方法,再输出“after”。下面我们来一步一步具体实现它。 + +### + +### (2)委托类的定义 + +​ 动态代理方式下,要求委托类必须实现某个接口,这里我们实现的是Sell接口。委托类Vendor类的定义如下: + +```java +public class Vendor implements Sell { + public void sell() { + System.out.println("In sell method"); + } + public void ad() { + System,out.println("ad method") + } +} +``` + + + +### (3)中介类 + +​ 上面我们提到过,中介类必须实现InvocationHandler接口,作为调用处理器”拦截“对代理类方法的调用。中介类的定义如下: + +```java +public class DynamicProxy implements InvocationHandler { + private Object obj; //obj为委托类对象; + + public DynamicProxy(Object obj) { + this.obj = obj; + } + + @Override + public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { + System.out.println("before"); + Object result = method.invoke(obj, args); + System.out.println("after"); + return result; + } +} +``` + +​ 从以上代码中我们可以看到,中介类持有一个委托类对象引用,在invoke方法中调用了委托类对象的相应方法(第11行),看到这里是不是觉得似曾相识?通过聚合方式持有委托类对象引用,把外部对invoke的调用最终都转为对委托类对象的调用。这不就是我们上面介绍的静态代理的一种实现方式吗?实际上,中介类与委托类构成了静态代理关系,在这个关系中,中介类是代理类,委托类就是委托类; + +代理类与中介类也构成一个静态代理关系,在这个关系中,中介类是委托类,代理类是代理类。也就是说,动态代理关系由两组静态代理关系组成,这就是动态代理的原理。下面我们来介绍一下如何”指示“以动态生成代理类。 + + + +### (4)动态生成代理类 + +​ 动态生成代理类的相关代码如下: + +```java +public class Main { + public static void main(String[] args) { + //创建中介类实例 + DynamicProxy inter = new DynamicProxy(new Vendor()); + //加上这句将会产生一个$Proxy0.class文件,这个文件即为动态生成的代理类文件 + System.getProperties().put("sun.misc.ProxyGenerator.saveGeneratedFiles","true"); + //获取代理类实例sell + Sell sell = (Sell)(Proxy.newProxyInstance(Sell.class.getClassLoader(), + new Class[] {Sell.class}, inter)); + //通过代理类对象调用代理类方法,实际上会转到invoke方法调用 + sell.sell(); + sell.ad(); + } +} +``` + +​ 在以上代码中,我们调用Proxy类的newProxyInstance方法来获取一个代理类实例。这个代理类实现了我们指定的接口并且会把方法调用分发到指定的调用处理器。这个方法的声明如下: + +```java +public static Object newProxyInstance(ClassLoader loader, Class[] interfaces, InvocationHandler h) throws IllegalArgumentException +``` + + 方法的三个参数含义分别如下: + +- loader:定义了代理类的ClassLoder; + +- interfaces:代理类实现的接口列表 + +- h:调用处理器,也就是我们上面定义的实现了InvocationHandler接口的类实例 + + + +​ 上面我们已经简单提到过动态代理的原理,这里再简单的总结下:首先通过newProxyInstance方法获取代理类实例,而后我们便可以通过这个代理类实例调用代理类的方法,对代理类的方法的调用实际上都会调用中介类(调用处理器)的invoke方法,在invoke方法中我们调用委托类的相应方法,并且可以添加自己的处理逻辑。下面我们来看一下生成的代理类的代码究竟是怎样的。 + +## 3. 动态代理类的源码分析 + +​ 通过运行Main,我们会得到一个名为“$Proxy”的class文件,这个文件即为动态生成的代理类,我们通过反编译来查看下这个代理类的源代码: + +```java +package com.sun.proxy; + +import java.lang.reflect.InvocationHandler; +import java.lang.reflect.Method; +import java.lang.reflect.Proxy; +import java.lang.reflect.UndeclaredThrowableException; + +public final class $Proxy0 extends Proxy implements Sell { + //这5个Method对象分别代表equals()、toString()、ad()、sell()、hashCode()方法 + private static Method m1; + private static Method m2; + private static Method m4; + private static Method m3; + private static Method m0; + + //构造方法接收一个InvocationHandler对象为参数,这个对象就是代理类的“直接委托类”(真正的委托类可以看做代理类的“间接委托类”) + public $Proxy0(InvocationHandler var1) throws { + super(var1); + } + + //对equals方法的调用实际上转为对super.h.invoke方法的调用,父类中的h即为我们在构造方法中传入的InvocationHandler对象,以下的toString()、sell()、ad()、hashCode()等方法同理 + public final boolean equals(Object var1) throws { + try { + return ((Boolean)super.h.invoke(this, m1, new Object[]{var1})).booleanValue(); + } catch (RuntimeException | Error var3) { + throw var3; + } catch (Throwable var4) { + throw new UndeclaredThrowableException(var4); + } + } + + public final String toString() throws { + try { + return (String)super.h.invoke(this, m2, (Object[])null); + } catch (RuntimeException | Error var2) { + throw var2; + } catch (Throwable var3) { + throw new UndeclaredThrowableException(var3); + } + } + + public final void ad() throws { + try { + super.h.invoke(this, m4, (Object[])null); + } catch (RuntimeException | Error var2) { + throw var2; + } catch (Throwable var3) { + throw new UndeclaredThrowableException(var3); + } + } + + public final void sell() throws { + try { + super.h.invoke(this, m3, (Object[])null); + } catch (RuntimeException | Error var2) { + throw var2; + } catch (Throwable var3) { + throw new UndeclaredThrowableException(var3); + } + } + + public final int hashCode() throws { + try { + return ((Integer)super.h.invoke(this, m0, (Object[])null)).intValue(); + } catch (RuntimeException | Error var2) { + throw var2; + } catch (Throwable var3) { + throw new UndeclaredThrowableException(var3); + } + } + + //这里完成Method对象的初始化(通过反射在运行时获得Method对象) + static { + try { + m1 = Class.forName("java.lang.Object").getMethod("equals", new Class[]{Class.forName("java.lang.Object")}); + m2 = Class.forName("java.lang.Object").getMethod("toString", new Class[0]); + m4 = Class.forName("Sell").getMethod("ad", new Class[0]); + m3 = Class.forName("Sell").getMethod("sell", new Class[0]); + m0 = Class.forName("java.lang.Object").getMethod("hashCode", new Class[0]); + } catch (NoSuchMethodException var2) { + throw new NoSuchMethodError(var2.getMessage()); + } catch (ClassNotFoundException var3) { + throw new NoClassDefFoundError(var3.getMessage()); + } + } +} +``` + +​ 我们可以看到,以上代码的逻辑十分简单,我们在注释中也做出了相关的说明。 + +​ 现在,我们已经了解了动态代理的使用,也搞清楚了它的实现原理,更进一步的话我们可以去了解动态代理类的生成过程,只需要去阅读newProxyInstance方法的源码即可,这个方法的逻辑也没有复杂的地方,这里就不展开了。 + +# 三、引用 + +[Java核心技术点之动态代理](https://www.cnblogs.com/absfree/p/5392639.html) \ No newline at end of file diff --git "a/java/\345\244\232\346\254\241\345\210\240\351\231\244\344\270\215\346\216\211\346\226\207\344\273\266.md" "b/java/\345\244\232\346\254\241\345\210\240\351\231\244\344\270\215\346\216\211\346\226\207\344\273\266.md" new file mode 100644 index 0000000..76c4b37 --- /dev/null +++ "b/java/\345\244\232\346\254\241\345\210\240\351\231\244\344\270\215\346\216\211\346\226\207\344\273\266.md" @@ -0,0 +1,11 @@ +### 原因 + +被删除的文件任然被虚拟机占用,导致文件删除不掉。 + +最大的原因是因为将文件打开为流的位置没有被关闭导致的。 + +### 解决方案 + +1、找出文件被使用为流的位置,将文件流关闭 + +2、多次进行删除文件,通过使用`System.gc()`进行内存回收,从而释放内存。(不推荐,会导致频繁gc) \ No newline at end of file diff --git "a/java/\345\244\232\347\272\277\347\250\213\344\271\213\351\227\264\347\232\204\347\256\241\351\201\223\346\265\201.md" "b/java/\345\244\232\347\272\277\347\250\213\344\271\213\351\227\264\347\232\204\347\256\241\351\201\223\346\265\201.md" new file mode 100644 index 0000000..59c790d --- /dev/null +++ "b/java/\345\244\232\347\272\277\347\250\213\344\271\213\351\227\264\347\232\204\347\256\241\351\201\223\346\265\201.md" @@ -0,0 +1,59 @@ +### 管道输入/输出流 + +管道输入/输出流和普通的文件输入/输出流或网络输入/输出流不同之处在于, 它主要用于线程之间的数据传输,而传输的媒介为内存。 + +管道输入/输出流主要包括了有如下两类实现: + +**面向字节的管道流:** + +* PipedOutputStream +* PipedInputStream + +**面向字符的管道流:** + +* PipedReader +* PipedWriter + +对于**Piped**类型的流,必须先要进行绑定,也就是调用**connect()**方法,如果没有将输入/输出流绑定起来,对于该流的访问将会抛出异常。 + + + +#### DEMO + +```java +public class Piped { + public static void main(String[] args) { + PipedWriter out = new PipedWriter(); + PipedReader in = new PipedReader(); + // 将输出流和输入流进行连接,否则在使用时会抛出异常 + out.connect(in); + Thread t = new Thread(new Print(in), "PrintThread"); + t.start(); + int receive = 0; + try { + while((receive = System.in.read()) != -1) { + out.write(receive); + } + } finally { + out.close(); + } + } + + static class Print implements Runnable { + private PipedReader in; + public Print(PipedReader in) { + this.in = in; + } + public void run() { + int receive = 0; + try { + while((receive = in.read()) != -1) { + System.out.println((char) receive); + } + } catch (IOException ex){ + } + } + } +} +``` + diff --git "a/java/\346\255\243\345\210\231\350\241\250\350\276\276\345\274\217\345\210\206\347\273\204.md" "b/java/\346\255\243\345\210\231\350\241\250\350\276\276\345\274\217\345\210\206\347\273\204.md" new file mode 100644 index 0000000..2a9eb62 --- /dev/null +++ "b/java/\346\255\243\345\210\231\350\241\250\350\276\276\345\274\217\345\210\206\347\273\204.md" @@ -0,0 +1,113 @@ +### 分组 + +在正则表达式中使用`()`进行分组。`()`中匹配到的规则为一组。 + +在`JAVA`中,默认分组`0`为整个表达式。其余子分组下标从`1`开始。 + + + +### 分组嵌套下标规则 + +在多个分组嵌套的正则表达式中。下标的计算规则为:从左到右,从外到内,遇内则进。 + +比如我们有这样一种正则表达式`((()())())()`。为了更直观的表达式,换下符号:`{[()()][]}{}`。 + +这是三层嵌套结果。那么下标和分组的对对应关系如下: + +| 下标 | 分组符号 | +| ---- | ------------ | +| 0 | {\[\(\)\()][]}{} | +| 1 | 左边的{} | +| 2 | 左边的[] | +| 3 | 左边的() | +| 4 | 右边的() | +| 5 | 右边的[] | +| 6 | 右边的{} | + + + +### JAVA 代码实现。 + +#### 一层分组 + +```java + /** + * 分组获取 + * @param regex 正则表达式 + * @param input 输入 + */ + public static void group(String regex, String input) { + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(input); + while (matcher.find()) { + String group = matcher.group(); + String chines = matcher.group(1); + String math = matcher.group(2); + String english = matcher.group(3); + System.out.println(group); + System.out.println(chines); + System.out.println(math); + System.out.println(english); + } + + } + + public static void main(String[] args) { + String regex = "语文:(\\d+), 数学:(\\d+), 英语:(\\d+)"; + String input = "小明在这次考试的成绩为:语文:100, 数学:99, 英语:88"; + group(regex, input); + } +``` + +代码执行结果为: + +``` +语文:100, 数学:99, 英语:88 +100 +99 +88 +``` + +#### 多层分组 + +```java + /** + * 分组获取 + * @param regex 正则表达式 + * @param input 输入 + */ + public static void group(String regex, String input) { + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(input); + while (matcher.find()) { + String group = matcher.group(); + String group1 = matcher.group(1); + String group2 = matcher.group(2); + String group3 = matcher.group(3); + String group4 = matcher.group(4); + System.out.println(group); + System.out.println(group1); + System.out.println(group2); + System.out.println(group3); + System.out.println(group4); + } + + } + + public static void main(String[] args) { + String regex = "a(a(e)?(e)?+b)(cc)+"; + String input = "aaebcc"; + group(regex, input); + } +``` + +代码执行结果: + +``` +aaebcc +aeb +e +null +cc +``` + diff --git "a/java/\346\255\243\345\210\231\350\241\250\350\276\276\345\274\217\346\233\277\346\215\242\345\214\271\351\205\215\347\232\204\346\226\207\346\234\254.md" "b/java/\346\255\243\345\210\231\350\241\250\350\276\276\345\274\217\346\233\277\346\215\242\345\214\271\351\205\215\347\232\204\346\226\207\346\234\254.md" new file mode 100644 index 0000000..6021d0a --- /dev/null +++ "b/java/\346\255\243\345\210\231\350\241\250\350\276\276\345\274\217\346\233\277\346\215\242\345\214\271\351\205\215\347\232\204\346\226\207\346\234\254.md" @@ -0,0 +1,64 @@ +### 方法 + +`Matcher`提供了`replaceAll`和`appendReplacement`方法用于替换字符串。 + +`replaceAll`用于将所有匹配到的字符替换为单一的字符串。 + +`appendReplacement`可以为每一个匹配的字符指定唯一的字符串,字符串内容不受限。 + +**注: ** `appendReplacement`替换后必须使用`appendTail`将剩余的字符收集完整。否则会从最后一次匹配到的字符后截断。 + + + +### JAVA 实现 + +```java + /** + * 替换字符 + * @param regex 正则表达式 + * @param input 输入 + * @param replaceStr 用于替换的字符 + */ + public static void replace(String regex, String input, String replaceStr) { + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(input); + + System.out.println("replaceAll : 用于将所有匹配的字符替换为指定的字符串"); + String replaceAll = matcher.replaceAll(replaceStr); + System.out.println("replaceAll : " + replaceAll); + + // 重置匹配器,用于测试下面的方法 + matcher.reset(); + + System.out.println("appendReplacement : 用于替换当前匹配到的字符"); + StringBuffer sb = new StringBuffer(); + int i = 0; + while (matcher.find()) { + // 将匹配之前的字符串复制到sb,再将匹配结果替换为: replaceChar,并追加到sb + matcher.appendReplacement(sb, replaceStr + ++i); + } + System.out.println("appendReplacement : " + sb); + // 收集剩余的字符 + matcher.appendTail(sb); + System.out.println("appendReplacement : " + sb); + + } + + public static void main(String[] args) { + String regex = "\\{.*?}"; + String input = "this is {} text {} text"; + String replaceStr = "a"; + replace(regex, input, replaceStr); + } +``` + +代码输出结果: + +``` +replaceAll : 用于将所有匹配的字符替换为指定的字符串 +replaceAll : this is a text a text +appendReplacement : 用于替换当前匹配到的字符 +appendReplacement : this is a1 text a2 +appendReplacement : this is a1 text a2 text +``` + diff --git "a/java/\346\255\243\345\210\231\350\241\250\350\276\276\345\274\217\350\247\204\345\210\231\345\244\247\345\205\250.md" "b/java/\346\255\243\345\210\231\350\241\250\350\276\276\345\274\217\350\247\204\345\210\231\345\244\247\345\205\250.md" new file mode 100644 index 0000000..16225f5 --- /dev/null +++ "b/java/\346\255\243\345\210\231\350\241\250\350\276\276\345\274\217\350\247\204\345\210\231\345\244\247\345\205\250.md" @@ -0,0 +1,93 @@ +## 常用正则表达式 + +``` +匹配中文字符的正则表达式: [u4e00-u9fa5] +评注:匹配中文还真是个头疼的事,有了这个表达式就好办了 +匹配双字节字符(包括汉字在内):[^x00-xff] +评注:可以用来计算字符串的长度(一个双字节字符长度计2,ASCII字符计1) +匹配空白行的正则表达式:ns*r +评注:可以用来删除空白行 +匹配HTML标记的正则表达式:<(S*?)[^>]*>.*?|<.*? /> +评注:网上流传的版本太糟糕,上面这个也仅仅能匹配部分,对于复杂的嵌套标记依旧无能为力 +匹配首尾空白字符的正则表达式:^s*|s*$ +评注:可以用来删除行首行尾的空白字符(包括空格、制表符、换页符等等),非常有用的表达式 +匹配Email地址的正则表达式:w+([-+.]w+)*@w+([-.]w+)*.w+([-.]w+)* +评注:表单验证时很实用 +匹配网址URL的正则表达式:[a-zA-z]+://[^s]* +评注:网上流传的版本功能很有限,上面这个基本可以满足需求 +匹配帐号是否合法(字母开头,允许5-16字节,允许字母数字下划线):^[a-zA-Z][a-zA-Z0-9_]{4,15}$ +评注:表单验证时很实用 +匹配国内电话号码:d{3}-d{8}|d{4}-d{7} +评注:匹配形式如 0511-4405222 或 021-87888822 +匹配腾讯QQ号:[1-9][0-9]{4,} +评注:腾讯QQ号从10000开始 +匹配中国邮政编码:[1-9]d{5}(?!d) +评注:中国邮政编码为6位数字 +匹配身份证:d{15}|d{18} +评注:中国的身份证为15位或18位 +匹配ip地址:d+.d+.d+.d+ +评注:提取ip地址时有用 +货币格式: +(?=(?!^)(?:\d{3})+(?:\.|$))(\d{3}(\.\d+$)?) +匹配特定数字: +^[1-9]d*$    //匹配正整数 +^-[1-9]d*$   //匹配负整数 +^-?[1-9]d*$   //匹配整数 +^[1-9]d*|0$  //匹配非负整数(正整数 + 0) +^-[1-9]d*|0$   //匹配非正整数(负整数 + 0) +^[1-9]d*.d*|0.d*[1-9]d*$   //匹配正浮点数 +^-([1-9]d*.d*|0.d*[1-9]d*)$  //匹配负浮点数 +^-?([1-9]d*.d*|0.d*[1-9]d*|0?.0+|0)$  //匹配浮点数 +^[1-9]d*.d*|0.d*[1-9]d*|0?.0+|0$   //匹配非负浮点数(正浮点数 + 0) +^(-([1-9]d*.d*|0.d*[1-9]d*))|0?.0+|0$  //匹配非正浮点数(负浮点数 + 0) +评注:处理大量数据时有用,具体应用时注意修正 +匹配特定字符串: +^[A-Za-z]+$  //匹配由26个英文字母组成的字符串 +^[A-Z]+$  //匹配由26个英文字母的大写组成的字符串 +^[a-z]+$  //匹配由26个英文字母的小写组成的字符串 +^[A-Za-z0-9]+$  //匹配由数字和26个英文字母组成的字符串 +^w+$  //匹配由数字、26个英文字母或者下划线组成的字符串 +在使用RegularExpressionValidator验证控件时的验证功能及其验证表达式介绍如下: +只能输入数字:“^[0-9]*$” +只能输入n位的数字:“^d{n}$” +只能输入至少n位数字:“^d{n,}$” +只能输入m-n位的数字:“^d{m,n}$” +只能输入零和非零开头的数字:“^(0|[1-9][0-9]*)$” +只能输入有两位小数的正实数:“^[0-9]+(.[0-9]{2})?$” +只能输入有1-3位小数的正实数:“^[0-9]+(.[0-9]{1,3})?$” +只能输入非零的正整数:“^+?[1-9][0-9]*$” +只能输入非零的负整数:“^-[1-9][0-9]*$” +只能输入长度为3的字符:“^.{3}$” +只能输入由26个英文字母组成的字符串:“^[A-Za-z]+$” +只能输入由26个大写英文字母组成的字符串:“^[A-Z]+$” +只能输入由26个小写英文字母组成的字符串:“^[a-z]+$” +只能输入由数字和26个英文字母组成的字符串:“^[A-Za-z0-9]+$” +只能输入由数字、26个英文字母或者下划线组成的字符串:“^w+$” +验证用户密码:“^[a-zA-Z]w{5,17}$”正确格式为:以字母开头,长度在6-18之间, +只能包含字符、数字和下划线。 +验证是否含有^%&'',;=?$"等字符:“[^%&'',;=?$x22]+” +只能输入汉字:“^[u4e00-u9fa5],{0,}$” +验证Email地址:“^w+[-+.]w+)*@w+([-.]w+)*.w+([-.]w+)*$” +验证InternetURL:“^http://([w-]+.)+[w-]+(/[w-./?%&=]*)?$” +验证电话号码:“^((d{3,4})|d{3,4}-)?d{7,8}$” +正确格式为:“XXXX-XXXXXXX”,“XXXX-XXXXXXXX”,“XXX-XXXXXXX”, +“XXX-XXXXXXXX”,“XXXXXXX”,“XXXXXXXX”。 +验证身份证号(15位或18位数字):“^d{15}|d{}18$” +验证一年的12个月:“^(0?[1-9]|1[0-2])$”正确格式为:“01”-“09”和“1”“12” +验证一个月的31天:“^((0?[1-9])|((1|2)[0-9])|30|31)$” +正确格式为:“01”“09”和“1”“31”。 +匹配中文字符的正则表达式: [u4e00-u9fa5] +匹配双字节字符(包括汉字在内):[^x00-xff] +匹配空行的正则表达式:n[s| ]*r +匹配HTML标记的正则表达式:/<(.*)>.*|<(.*) />/ +匹配首尾空格的正则表达式:(^s*)|(s*$) +匹配Email地址的正则表达式:w+([-+.]w+)*@w+([-.]w+)*.w+([-.]w+)* +匹配网址URL的正则表达式:http://([w-]+.)+[w-]+(/[w- ./?%&=]*)? +``` + + + +## 参考 + +[常用正则表达式大全!(例如:匹配中文、匹配html)](https://www.cnblogs.com/wenmaoyu/archive/2011/07/21/2113124.html) + diff --git "a/java/\346\255\243\345\210\231\350\241\250\350\276\276\345\274\217\350\264\252\345\251\252\345\222\214\346\207\222\346\203\260\345\214\271\351\205\215.md" "b/java/\346\255\243\345\210\231\350\241\250\350\276\276\345\274\217\350\264\252\345\251\252\345\222\214\346\207\222\346\203\260\345\214\271\351\205\215.md" new file mode 100644 index 0000000..2adb7f5 --- /dev/null +++ "b/java/\346\255\243\345\210\231\350\241\250\350\276\276\345\274\217\350\264\252\345\251\252\345\222\214\346\207\222\346\203\260\345\214\271\351\205\215.md" @@ -0,0 +1,70 @@ +### 贪婪匹配、懒惰匹配 + +`贪恋匹配`对应的是`懒惰匹配`。 + +`贪恋匹配`指的是在满足`正则表达式`的规则的前提下,尽可能的匹配尽可能多的字符,并且只匹配一次。 + +`懒惰匹配`则和`贪婪匹配`相反。它会尽可能少的匹配字符。可能会匹配到多个。 + +例如我们想要匹配这个规则:`a.*b`, 匹配的字符串为`aababa`。 + +贪恋匹配会匹配到:`aabaab`,匹配到的最长的字符串。 + +懒惰匹配则会匹配到:`aab`、`ab`,尽可能的匹配了最短的字符,切匹配到了多个。 + + + +### 贪婪和懒惰匹配转换 + +贪婪匹配和懒惰匹配影响的是正则表达式`限定符`的匹配结果。 + +在限定符后面加`?`,则为懒惰匹配,否则为贪婪匹配。 + +**懒惰限定符** + +| 代码/语法 | 说明 | +| --------- | ------------- | +| *? | 重复任意次 | +| +? | 重复1次或更多 | +| ?? | 重复0次或1次 | +| {m, n}? | 重复n到m次 | +| {m,}? | 重复m次以上 | + + + +### JAVA 实现 + +```java + public static void match(String regex, String input) { + Pattern pattern = Pattern.compile(regex); + Matcher matcher = pattern.matcher(input); + while (matcher.find()) { + String m = matcher.group(); + System.out.println(m); + } + } + + public static void main(String[] args) { + String desc = "{}this is a test{}"; + System.out.println("贪婪匹配:"); + String greedy = "\\{.*}"; + match(greedy, desc); + // 分割线 + System.out.println("================分割线================="); + System.out.println("懒惰匹配:"); + String lazy = "\\{.*?}"; + match(lazy, desc); + } +``` + +以上代码执行结果: + +``` +贪婪匹配: +{}this is a test{} +================分割线================= +懒惰匹配: +{} +{} +``` + diff --git "a/java/\347\261\273\345\212\240\350\275\275\345\231\250.md" "b/java/\347\261\273\345\212\240\350\275\275\345\231\250.md" new file mode 100644 index 0000000..8aad6ce --- /dev/null +++ "b/java/\347\261\273\345\212\240\350\275\275\345\231\250.md" @@ -0,0 +1,115 @@ +## JAVA类装载方式 + +1. 隐式装载, 程序在运行过程中当碰到通过new 等方式生成对象时,隐式调用类装载器加载对应的类到jvm中。 +2. 显式装载, 通过class.forname()等方法,显式加载需要的类。 + +## 系统类加载器 + +**Bootstrp loader** Bootstrp加载器是用C++语言写的,它是在Java虚拟机启动后初始化的,它主要负责加载%JAVA_HOME%/jre/lib,-Xbootclasspath参数指定的路径以及%JAVA_HOME%/jre/classes中的类。 + +**ExtClassLoader** Bootstrp loader加载ExtClassLoader,并且将ExtClassLoader的父加载器设置为Bootstrp loader.ExtClassLoader是用Java写的,具体来说就是 sun.misc.Launcher$ExtClassLoader,ExtClassLoader主要加载%JAVA_HOME%/jre/lib/ext,此路径下的所有classes目录以及java.ext.dirs系统变量指定的路径中类库。 + +**AppClassLoader** Bootstrp loader加载完ExtClassLoader后,就会加载AppClassLoader,并且将AppClassLoader的父加载器指定为 ExtClassLoader。AppClassLoader也是用Java写成的,它的实现类是 sun.misc.Launcher$AppClassLoader,另外我们知道ClassLoader中有个getSystemClassLoader方法,此方法返回的正是AppclassLoader.AppClassLoader主要负责加载classpath所指定的位置的类或者是jar文档,它也是Java程序默认的类加载器。 + +**类加载器继承关系** + +![类加载器继承关系](./images/001/001.png) + +## 工作原理 + +java的类加载器采用了**委托模型机制**,这个机制简单来讲,就是类装载器有载入类的需求时,会先请示其Parent使用其搜索路径帮忙载入,如果Parent 找不到,那么才由自己依照自己的搜索路径搜索类。 + +加载class文件的步骤如下: + +1、装载:查找和导入Class文件 + +2、链接:其中解析步骤是可以选择的 (a)检查:检查载入的class文件数据的正确性 (b)准备:给类的静态变量分配存储空间 (c)解析:将符号引用转成直接引用 + +3、初始化:对静态变量,静态代码块执行初始化工作 + +## 自定义类加载器 + +定义自已的类加载器分为两步: + +1. 继承java.lang.ClassLoader +2. 重写父类的findClass方法 + +## 线程上下文加载器 + +线程上下文类加载器(context class loader)是从 JDK 1.2 开始引入的。类 java.lang.Thread中的方法 getContextClassLoader()和 setContextClassLoader(ClassLoader cl)用来获取和设置线程的上下文类加载器。如果没有通过 setContextClassLoader(ClassLoader cl)方法进行设置的话,线程将继承其父线程的上下文类加载器。Java 应用运行的初始线程的上下文类加载器是系统类加载器。在线程中运行的代码可以通过此类加载器来加载类和资源。 + + + +## 实现自定义的类加载器 + +**CustomClassLoader.java** + +```java +public class CustomClassLoader extends ClassLoader { + + public CustomClassLoader(ClassLoader parent) { + super(parent); + } + + @Override + protected Class findClass(String name) throws ClassNotFoundException { + byte[] date = getClassDate(name); + if (date != null) { + return defineClass(name, date, 0, date.length); + } + throw new ClassNotFoundException(); + } + + private byte[] getClassDate(String className) { + String filename = className.replaceAll("\\.", File.separator).concat(".class"); + try (InputStream in = new FileInputStream(filename); + ByteArrayOutputStream bos = new ByteArrayOutputStream()) { + byte[] buffer = new byte[1024]; + int len = 0; + while ((len = in.read(buffer)) != -1) { + bos.write(buffer, 0, len); + } + return bos.toByteArray(); + } catch (IOException e) { + e.printStackTrace(); + } + return null; + } +} +``` + +**Main.java** + +```java +public class Main { + + public static void main(String[] args) { + + // 获取当前的类加载器 + ClassLoader currentClassLoader = Main.class.getClassLoader(); + // 新建类加载器 + ClassLoader customClassLoader = new CustomClassLoader(currentClassLoader); + // 设置当前线程使用自定义的类加载器 + Thread.currentThread().setContextClassLoader(customClassLoader); + + // 查看子线程的类加载器 + new Thread(new Runner()).start(); + } +} + +class Runner implements Runnable { + + @Override + public void run() { + ClassLoader classLoader = Thread.currentThread().getContextClassLoader(); + System.out.println(classLoader); + } +} +``` + +**输出** + +``` +com.classload.CustomClassLoader@60cb300b +``` + diff --git "a/java/\347\273\264\346\212\244\345\244\232\344\270\252\346\225\260\346\215\256\345\272\223\350\277\236\346\216\245.md" "b/java/\347\273\264\346\212\244\345\244\232\344\270\252\346\225\260\346\215\256\345\272\223\350\277\236\346\216\245.md" new file mode 100644 index 0000000..3195c44 --- /dev/null +++ "b/java/\347\273\264\346\212\244\345\244\232\344\270\252\346\225\260\346\215\256\345\272\223\350\277\236\346\216\245.md" @@ -0,0 +1,239 @@ +## java 多数据库连接维护 + +### 1.DriverManager维护了一个驱动列表 + +以我们熟悉的MysqlDriver来举例: + +```java +package com.mysql.jdbc; + +import java.sql.SQLException; +public class Driver extends NonRegisteringDriver implements java.sql.Driver { + // + // Register ourselves with the DriverManager + // + static { + try { + java.sql.DriverManager.registerDriver(new Driver()); + } catch (SQLException E) { + throw new RuntimeException("Can't register driver!"); + } + } + + public Driver() throws SQLException { + // Required for Class.forName().newInstance() + } +} +``` + +在我们执行如下语句的时候,static块的内容会被执行,于是com.mysql.jdbc.Driver就成功的把自己给注册到DriverManager的驱动列表里面去了。 + +```java +Class.forName("com.mysql.jdbc.Driver"); +``` + +来看看DriverManager的注册实现: + +```java +private final static CopyOnWriteArrayList registeredDrivers = new CopyOnWriteArrayList<>(); +public static synchronized void registerDriver(java.sql.Driver driver, + DriverAction da) + throws SQLException { + + /* Register the driver if it has not already been added to our list */ + if(driver != null) { + registeredDrivers.addIfAbsent(new DriverInfo(driver, da)); + } else { + // This is for compatibility with the original DriverManager + throw new NullPointerException(); + } + + println("registerDriver: " + driver); + + } +``` + +代码的意思就是如果当前的Driver不存在就添加,否则就啥也不执行。 + +于是在DriverManager这个类里面就有了我们Mysql的驱动类了。 + +对于Oracle也是一样的,被加载的驱动都需要在被加载的时候,在static块中,自动把自己给注册到DriverManager中。 + +于是我们明白DriverManager就是维护了一个数据库的驱动列表,而且这个列表中同类型的数据库连接只有一份,比如我们系统里面即用到了mysql也用到了oracle那么我们的DriverManager里面只维护了2种类型的数据库驱动,不论我们实际上用了多个mysql数据库,驱动都是一样的。 + +### 2.获取逻辑由具体驱动自己实现 +看看DriverManager是如何获取数据库连接的: + +第一步:构造用户信息 + +```java + @CallerSensitive + public static Connection getConnection(String url, + String user, String password) throws SQLException { + java.util.Properties info = new java.util.Properties(); + + if (user != null) { + info.put("user", user); + } + if (password != null) { + info.put("password", password); + } + + return (getConnection(url, info, Reflection.getCallerClass())); + } +``` + +第二步:获取连接 + +```java +// Worker method called by the public getConnection() methods. + private static Connection getConnection( + String url, java.util.Properties info, Class caller) throws SQLException { + ClassLoader callerCL = caller != null ? caller.getClassLoader() : null; + // 线程同步,防止并发出问题 + synchronized(DriverManager.class) { + // synchronize loading of the correct classloader. + if (callerCL == null) { + callerCL = Thread.currentThread().getContextClassLoader(); + } + } + + if(url == null) { + throw new SQLException("The url cannot be null", "08001"); + } + + println("DriverManager.getConnection(\"" + url + "\")"); + + SQLException reason = null; + // 循环当前的数据库驱动来获取数据库连接 + for(DriverInfo aDriver : registeredDrivers) { + // If the caller does not have permission to load the driver then + // skip it. + if(isDriverAllowed(aDriver.driver, callerCL)) { + try { + println(" trying " + aDriver.driver.getClass().getName()); + // 这个地方由具体的数据库驱动自己来实现 + Connection con = aDriver.driver.connect(url, info); + if (con != null) { + // Success! + println("getConnection returning " + aDriver.driver.getClass().getName()); + return (con); + } + } catch (SQLException ex) { + if (reason == null) { + reason = ex; + } + } + + } else { + println(" skipping: " + aDriver.getClass().getName()); + } + + } + + // if we got here nobody could connect. + if (reason != null) { + println("getConnection failed: " + reason); + throw reason; + } + + println("getConnection: no suitable driver found for "+ url); + throw new SQLException("No suitable driver found for "+ url, "08001"); + } +``` + +对于上面的代码,我们不需要全部关注,只需要知道,连接的获取过程是**通过循环已有的驱动,然后由每个驱动自己来完成的**。我们来看看mysql的驱动实现: + +```java + public java.sql.Connection connect(String url, Properties info) throws SQLException { + if (url == null) { + throw SQLError.createSQLException(Messages.getString("NonRegisteringDriver.1"), SQLError.SQL_STATE_UNABLE_TO_CONNECT_TO_DATASOURCE, null); + } + // 首先判断当前的url是不是负载均衡的url,如果是走负载均衡的获取逻辑 + if (StringUtils.startsWithIgnoreCase(url, LOADBALANCE_URL_PREFIX)) { + return connectLoadBalanced(url, info); + } else if (StringUtils.startsWithIgnoreCase(url, REPLICATION_URL_PREFIX)) { + return connectReplicationConnection(url, info); + } + + Properties props = null; + // 这个地方会判断当前url是不是属于mysql连接的前缀,不是就return + if ((props = parseURL(url, info)) == null) { + return null; + } + + if (!"1".equals(props.getProperty(NUM_HOSTS_PROPERTY_KEY))) { + return connectFailover(url, info); + } + // 总之经过了一系列的判断我们的程序开始真正的去拿我们要的连接了 + try { + Connection newConn = com.mysql.jdbc.ConnectionImpl.getInstance(host(props), port(props), props, database(props), url); + + return newConn; + } catch (SQLException sqlEx) { + // Don't wrap SQLExceptions, throw + // them un-changed. + throw sqlEx; + } catch (Exception ex) { + SQLException sqlEx = SQLError.createSQLException( + Messages.getString("NonRegisteringDriver.17") + ex.toString() + Messages.getString("NonRegisteringDriver.18"), + SQLError.SQL_STATE_UNABLE_TO_CONNECT_TO_DATASOURCE, null); + + sqlEx.initCause(ex); + + throw sqlEx; + } + } +``` + +我们看看parseURL方法实现: + +```java +private static final String URL_PREFIX = "jdbc:mysql://"; +@SuppressWarnings("deprecation") + public Properties parseURL(String url, Properties defaults) throws java.sql.SQLException { + Properties urlProps = (defaults != null) ? new Properties(defaults) : new Properties(); + + if (url == null) { + return null; + } +// 判断当前的url是不是以"jdbc:mysql://";开始 + if (!StringUtils.startsWithIgnoreCase(url, URL_PREFIX) && !StringUtils.startsWithIgnoreCase(url, MXJ_URL_PREFIX) + && !StringUtils.startsWithIgnoreCase(url, LOADBALANCE_URL_PREFIX) && !StringUtils.startsWithIgnoreCase(url, REPLICATION_URL_PREFIX)) { + + return null; + } + + ...还有一大堆逻辑 + + return urlProps; + } +``` + +对于不同的数据库,因为使用的连接url不一样,比如mysql的连接格式如下 + +```java +jdbc:mysql://localhost:3306/test?characterEncoding=utf-8 +``` + +而oracle的连接字符串如下: + +```java +jdbc:oracle:thin:@127.0.0.1:1521:news +``` + +sql server的连接字符串如下: + +```java +jdbc:sqlserver://localhost:1434;DataBaseName=testdatabase +``` + +所以通过连接字符串的前缀不同可以区分出当前的驱动是不是目标驱动,如果不是,DriverManager接着循环下一个驱动来尝试获取连接。这样就可以通过DriverManager通过url来获取不同类型数据库的连接了。到此我们发现其实DriverManager维护的只是驱动而已,我们要获取那种类型数据库的连接,以及获取那个数据库连接还是取决于我们自己,因为获取数据库连接的时候,连接信息是我们自己指定的。 + +### 3.如何维护多个数据库连接 +从上面的分析我们知道了,我们获取数据库的连接就是提供连接的url,用户名,密码就可以获取一个相应数据库的连接了,而如果要维护多个数据库连接,不就是提供多套url,用户名和密码吗?而如果你想手动的来把这些连接管理起来也很简单,其实就是如何管理多套数据库连接信息而已。 + +### 4.引用 + +[java 多数据库连接维护示例](https://blog.csdn.net/tengdazhang770960436/article/details/98223427) + diff --git "a/java/\350\216\267\345\217\226\346\225\260\346\215\256\345\272\223\345\205\203\346\225\260\346\215\256.md" "b/java/\350\216\267\345\217\226\346\225\260\346\215\256\345\272\223\345\205\203\346\225\260\346\215\256.md" new file mode 100644 index 0000000..bc72285 --- /dev/null +++ "b/java/\350\216\267\345\217\226\346\225\260\346\215\256\345\272\223\345\205\203\346\225\260\346\215\256.md" @@ -0,0 +1,122 @@ +## 获取数据库元数据 + +### 1. 代码 + +```java + import java.sql.Connection; + import java.sql.DatabaseMetaData; + import java.sql.DriverManager; + import java.sql.ResultSet; + import java.sql.ResultSetMetaData; + import java.sql.Statement; + import java.sql.Types; + import java.util.ArrayList; + import java.util.HashMap; + import java.util.Iterator; + import java.util.List; + import java.util.Map; + import java.util.AbstractMap.SimpleEntry; + import java.util.Map.Entry; + public class TestDbMetaData { + @SuppressWarnings("unchecked") + public static void main(String[] args) throws Exception { + String dburl = "jdbc:mysql://127.0.0.1:3306/fhadmin"; + String dbuser = "root"; + String dbpwd = "root"; + Class.forName("com.mysql.jdbc.Driver"); + List tableName = new ArrayList(); + Map>> tables = new HashMap>>(); + Connection con = DriverManager.getConnection(dburl, dbuser, dbpwd); + // 获取整个数据库的元数据 + DatabaseMetaData metaDb = con.getMetaData(); + ResultSet rsTableName = metaDb.getTables(null, null, null, new String[] { "table" }); + while (rsTableName.next()) { + String tmpTableName = rsTableName.getString("table_name"); + tableName.add(tmpTableName); + String sql = "select * from " + tmpTableName; + Statement st = con.createStatement(); + ResultSet rsColumName = st.executeQuery(sql); + // 获取查询语句对应的元数据 + ResultSetMetaData metaRs = rsColumName.getMetaData(); + List> colums = new ArrayList>(); + for (int i = 1; i <= metaRs.getColumnCount(); i++) { + System.out.print( metaRs.getColumnName(i)+","+metaRs.getColumnClassName(i).substring(metaRs.getColumnClassName(i).lastIndexOf(".") + 1)); + System.out.println(",是否为空:"+metaRs.isNullable(i));//0 不能为空   1可以为空 +    //System.out.println(metaRs.getColumnLabel(i)); +    //System.out.println(metaRs.getColumnDisplaySize(i)); + String columName = metaRs.getColumnName(i); + // 某列类型的精确度(类型的长度) +                int precision = metaRs.getPrecision(i); +                // 小数点后的位数  +                int scale = metaRs.getScale(i); +                // 是否自动递增  +                boolean isAutoInctement = metaRs.isAutoIncrement(i); + String aa= metaRs.getColumnTypeName(i); + String columType = ""; + switch (metaRs.getColumnType(i)) { + case Types.CHAR: + columType = "String"; + break; + case Types.BIGINT: + columType = "int"; + break; + case Types.DATE: + columType = "Date"; + break; + case Types.DECIMAL: + columType = "int"; + break; + case Types.INTEGER: + columType = "int"; + break; + case Types.NCHAR: + columType = "String"; + break; + case Types.NUMERIC: + columType = "int"; + break; + case Types.NVARCHAR: + columType = "String"; + break; + case Types.SMALLINT: + columType = "int"; + break; + case Types.TIME: + columType = "Date"; + break; + case Types.TINYINT: + columType = "int"; + break; + case Types.TIMESTAMP: + columType = "Date"; + break; + case Types.VARCHAR: + columType = "String"; + break; + default: + throw new Exception("数据类型不支持,orm映射异常"); + } + Entry entry = new SimpleEntry(columName, columType); + colums.add(entry); + } + tables.put(tmpTableName, colums); + } + + + Iterator>>> iter = tables.entrySet().iterator(); + while (iter.hasNext()) { + Entry>> entry = iter.next(); + System.out.println("表名:" + entry.getKey()); + for (int i = 0; i < entry.getValue().size(); i++) { + System.out.print(entry.getValue().get(i).getKey() + " " + entry.getValue().get(i).getValue()); + } + System.out.println(); + } + System.out.println("complete..."); + } +} +``` + +### 2. 引用 + +[java获取mysql表结构](https://blog.csdn.net/jacke121/article/details/54835491) \ No newline at end of file diff --git "a/java/\350\216\267\345\217\226\347\250\213\345\272\217\345\267\245\344\275\234\350\267\257\345\276\204.md" "b/java/\350\216\267\345\217\226\347\250\213\345\272\217\345\267\245\344\275\234\350\267\257\345\276\204.md" new file mode 100644 index 0000000..9c1dcbc --- /dev/null +++ "b/java/\350\216\267\345\217\226\347\250\213\345\272\217\345\267\245\344\275\234\350\267\257\345\276\204.md" @@ -0,0 +1,19 @@ +## 获取应用程序当前的工作路径 + +以前用于获取当前类路径下的文件的方式: + +```java +URL resource = getClass().getClassLoader().getResource(""); +// or +URL resource = Thread.currentThread().getContextClassLoader().getResource(""); +``` + +**限制: **该方式只能在java文件未被打包的情况下才能正常使用,如果使用jar执行该文件,则会返回null。 + +假设我们有一个`app.jar`文件,我们使用`java -jar app.jar`,若我们想在`app.jar`获取此文件所在的路径,那么使用上述的两种方式都是无效的,返回的`URL`对象都是null,为了解决这种文件,可以使用下面的方式: + +```java +URL url = getClass().getProtectionDomain().getCodeSource().getLocation(); +``` + +这种方式会获取到`app.jar`文件在系统上的绝对路径。 \ No newline at end of file diff --git "a/java/\350\243\201\345\211\252\345\233\276\347\211\207.md" "b/java/\350\243\201\345\211\252\345\233\276\347\211\207.md" new file mode 100644 index 0000000..5de21df --- /dev/null +++ "b/java/\350\243\201\345\211\252\345\233\276\347\211\207.md" @@ -0,0 +1,42 @@ +### 主要思路 + +使用`ImageReader`类,通过设置矩阵的方式进行裁剪。 + +### 代码 + +```java +/** + * 将原始图片居中裁剪为正方形 + */ +public static void cutSquareImage(String src, String dest) { + InputStream secInputSteam = null; + try { + secInputSteam = new FileInputStream(src); + // 获取图片读取器 + ImageReader reader = ImageIO.getImageReadersByFormatName("jpeg").next(); + // 读取原始图片 + ImageInputStream imageInputStream = ImageIO.createImageInputStream(secInputSteam); + // 将原始图片信息设置到读取器内 + reader.setInput(imageInputStream, true); + // 获取长宽,取正方形的边长 + int imageIndex = 0; + int height = reader.getHeight(imageIndex); + int width = reader.getWidth(imageIndex); + // 边长 + int side = Math.min(width, height); + // 绘制一个正方形,使读取器读取正方形指定的范围 + Rectangle rectangle = new Rectangle((width - side) / 2, (height - side) / 2, side, side); + ImageReadParam param = reader.getDefaultReadParam(); + param.setSourceRegion(rectangle); + // 输出图片 + BufferedImage bufferedImage = reader.read(imageIndex, param); + File destFile = IoUtil.createFile(dest); + ImageIO.write(bufferedImage, "jpeg", destFile); + } catch (IOException e) { + throw ExceptionUtil.unchecked(e); + } finally { + IoUtil.close(secInputSteam); + } +} +``` + diff --git "a/java/\350\256\241\347\256\227\350\272\253\344\273\275\350\257\201\346\240\241\351\252\214\347\240\201.md" "b/java/\350\256\241\347\256\227\350\272\253\344\273\275\350\257\201\346\240\241\351\252\214\347\240\201.md" new file mode 100644 index 0000000..562b10f --- /dev/null +++ "b/java/\350\256\241\347\256\227\350\272\253\344\273\275\350\257\201\346\240\241\351\252\214\347\240\201.md" @@ -0,0 +1,55 @@ +## 计算校验码 + +**身份证号与权重的对应表** + +| 号码 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | +| ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ------ | +| 权重 | 7 | 9 | 10 | 5 | 8 | 4 | 2 | 1 | 6 | 3 | 7 | 9 | 10 | 5 | 8 | 4 | 2 | 校验码 | + +**余数与校验码对应表** + +| 余数 | 0 | 1 | 2 | 3 | 4 | 5 | 6 | 7 | 8 | 9 | 10 | +| ------ | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | ---- | +| 校验码 | 1 | 0 | X | 9 | 8 | 7 | 6 | 5 | 4 | 3 | 2 | + +**计算公式** + +1. 身份证每一位号码与其对应的权重,使用当前位上的**数字**和对应的**权重** **相乘**,并将十七位计算的数字**求和**。 +2. 将第一步**求和的结果**,除以 **11**,**取余**。 +3. 参照**余数与校验码对照表**获取真实的校验码。 +4. 将计算的**校验码**对用户输入的第**十八**位进行对比,一致则身份证号正确。 + +## 代码 + +```java +public class IDCardValidate { + + // 每一位号码的权重 + private static final int[] WEIGHT = { 7, 9, 10, 5, 8, 4, 2, 1, 6, 3, 7, 9, 10, 5, 8, 4, 2 }; + // 最后一位身份证号码 + private static final String[] VALICATION_NUMBER = { "1", "0", "X", "9", "8", "7", "6", "5", "4", "3", "2" }; + + /** + * 计算身份证最后一位校验码 + * @param idCard + * @return null:获取校验码错误 + */ + public static String computeValidationCode(String idCard) { + String[] idNums; + if (idCard == null || "".equalsIgnoreCase(idCard.trim()) || (idNums = idCard.split("")).length != 18) { + return null; + } + // 第一步,计算每一位身份证号和对应数字的总和 + int sum = 0; + for (int i = 0; i < WEIGHT.length; i++) { + sum += (Integer.parseInt(idNums[i]) * WEIGHT[i]); + } + // 计算最后一位校验数字的下标 + int index = sum % 11; + // 计算最后一位字符 + return VALICATION_NUMBER[index]; + } + +} +``` + diff --git "a/java/\350\277\220\350\241\214\346\227\266\347\274\226\350\257\221.md" "b/java/\350\277\220\350\241\214\346\227\266\347\274\226\350\257\221.md" new file mode 100644 index 0000000..3d996af --- /dev/null +++ "b/java/\350\277\220\350\241\214\346\227\266\347\274\226\350\257\221.md" @@ -0,0 +1,157 @@ +## 技术前提 + +请完整阅读`java使用命令编译打包`文档后再继续阅读。 + +关键组件介绍: + +- JavaCompiler - 表示java编译器, run方法执行编译操作. 或者可以先生成编译任务(CompilationTask), 然后与调用任务的call方法执行 +- JavaFileObject - 表示一个java源文件对象 +- JavaFileManager - Java源文件管理类, 管理一系列JavaFileObject +- Diagnostic - 表示一个诊断信息 +- DiagnosticListener - 诊断信息监听器, 编译过程触发. 生成编译task(`JavaCompiler#getTask()`)或获取FileManager(`JavaCompiler#getStandardFileManager()`)时需要传递DiagnosticListener以便收集诊断信息 + + + +## 编译本地源文件并输出到本地 + +读取本地的`.java`文件,动态编译为`.class`文件后保存在磁盘上。 + +**第一种方式** + +```java +public static void main(String[] args) { + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + // 输入流、输出流、错误流、javac参数 + int result = compiler.run(null, null, null, "/path/to/source"); + if (result != 0) { + System.out.println("编译失败"); + } +} +``` + +通过以上的方式,默认编译的文件和源文件存在同一目录下。可以通过`-d`参数指定编译输出位置。 + +**第二种方式** + +```java +public static void main(String[] args) { + // 获取java编译器 + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + // 获取文件管理器 + StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null); + // 定义要编译的源文件 + File file = new File("F:\\javalearning\\src\\com\\feb13th\\demo1\\HelloWorld.java"); + //通过源文件获取到要编译的Java类源码迭代器,包括所有内部类,其中每个类都是一个 JavaFileObject,也被称为一个汇编单元 + Iterable compilationUnits = fileManager.getJavaFileObjects(file); + // options -d 指定编译后的文件位置, -classpath 指定依赖 + Iterable options = Arrays.asList( + "-d", "F:\\javalearning\\classes", + "-classpath", "F:\\javalearning\\classes;F:\\javalearning\\lib\\fastjson-1.2.58.jar"); + // 生成编译任务 + JavaCompiler.CompilationTask task = compiler.getTask(null, + fileManager, + null, + options, + null, + compilationUnits); + // 执行编译任务 + Boolean success = task.call(); + if (success) { + System.out.println("编译成功"); + } else { + System.out.println("编译失败"); + } +} +``` + + + +## 编译内存中的源码并写入内存 + +编译内存中的源码需要自定义`JavaFileObject`以及`JavaFileManager`。下面的例子中实现了读取内存中的java源码,并将编译后的结果字节码保存在map中。 + +如果只需要读取内存中的源码,将**(1)**修改为上一节中对应的代码即可。 + +如果只需要将编译后的字节码保存在内存中,将**(2)**修改为上一节对应的代码即可。 + +**注:** (1)(2)都有两处需要修改。 + +```java +public static void main(String[] args) { + // 获取java编译器 + JavaCompiler compiler = ToolProvider.getSystemJavaCompiler(); + // 获取文件管理器 + StandardJavaFileManager fileManager = compiler.getStandardFileManager(null, null, null); + String className = "com.feb13th.HelloWorld"; + String code = "package com.feb13th;" + + "public class HelloWorld { " + + " public static void main(String[] args) { " + + " System.out.println(\"HelloWorld!!\");" + + " }" + + "}"; + // (1) 自定义JavaFileObject实现读取内存中的源文件 + JavaFileObject javaFileObject = new SimpleJavaFileObject( + // 创建 uri, com/feb13th/HelloWorld.java + URI.create(className.replaceAll("\\.", "/") + JavaFileObject.Kind.SOURCE.extension), + JavaFileObject.Kind.SOURCE) { + @Override + public CharSequence getCharContent(boolean ignoreEncodingErrors) throws IOException { + return code; + } + }; + + // 自定义用于存储字节码的map + Map bytes = new HashMap<>(); + // (2) 自定义JavaFileManager实现将编译后的字节码存储在内存中 + JavaFileManager javaFileManager = new ForwardingJavaFileManager(fileManager) { + @Override + public JavaFileObject getJavaFileForOutput(Location location, + String className, + JavaFileObject.Kind kind, + FileObject sibling) throws IOException { + if (kind == JavaFileObject.Kind.CLASS) { + return new SimpleJavaFileObject( + URI.create(className + JavaFileObject.Kind.CLASS.extension), + JavaFileObject.Kind.CLASS) { + @Override + public OutputStream openOutputStream() throws IOException { + return new FilterOutputStream(new ByteArrayOutputStream()) { + @Override + public void close() throws IOException { + out.close(); + ByteArrayOutputStream bos = (ByteArrayOutputStream) out; + bytes.put(className, bos.toByteArray()); + } + }; + } + }; + } + return super.getJavaFileForOutput(location, className, kind, sibling); + } + }; + + // 生成编译任务 + JavaCompiler.CompilationTask task = compiler.getTask(null, + javaFileManager, //(2) + null, + null, //(1) + null, + Arrays.asList(javaFileObject)); + // 执行编译任务 + Boolean success = task.call(); + if (success) { + System.out.println("编译成功"); + } else { + System.out.println("编译失败"); + } + + // 查看map中有没有存储字节码 + bytes.forEach((k, v) -> { + System.out.println(k); + System.out.println(v.length); + }); +} +``` + + + diff --git "a/java/\350\277\234\347\250\213debug.md" "b/java/\350\277\234\347\250\213debug.md" new file mode 100644 index 0000000..3ed80e5 --- /dev/null +++ "b/java/\350\277\234\347\250\213debug.md" @@ -0,0 +1,75 @@ +## 远程debug调试java代码 + +日常环境和预发环境遇到问题时,可以用远程调试的方法本地打断点,在本地调试。生产环境由于网络隔离和系统稳定性考虑,不能进行远程代码调试。 + +整体过程是通过修改远程服务JAVA_OPTS参数,然后本地通过Eclipse或IDEA等工具调试。 + +### 理论 + +JPDA(Java Platform Debugger Architecture)是Java平台调试体系结构的缩写。由3个规范组成,分别是JVMTI(JVM Tool Interface),JDWP(Java Debug Wire Protocol),JDI(Java Debug Interface) 。 + +* 1.JVMTI定义了虚拟机应该提供的调试服务,包括调试信息(Information譬如栈信息)、调试行为(Action譬如客户端设置一个断点)和通知(Notification譬如到达某个断点时通知客户端),该接口由虚拟机实现者提供实现,并结合在虚拟机中 +* 2.JDWP定义调试服务和调试器之间的通信,包括定义调试信息格式和调试请求机制 +* 3.JDI在语言的高层次上定义了调试者可以使用的调试接口以能方便地与远程的调试服务进行交互,Java语言实现,调试器实现者可直接使用该接口访问虚拟机调试服务。 java调试工具jdb,就是sun公司提供的JDI实现。eclipse IDE,它的两个插件org.eclipse.jdt.debug.ui和org.eclipse.jdt.debug与其强大的调试功能密切相关,其中前者是eclipse调试工具界面的实现,而后者则是JDI的一个完整实现。 + +### 远程调试 + +远程调试分为主动连接调试,和被动连接调试。 + +> 主动连接调试:服务端配置监控端口,本地IDE连接远程监听端口进行调试,一般调试问题用这种方式。 +> +> 被动连接调试:本地IDE监听某端口,等待远程连接本地端口。一般用于远程服务启动不了,启动时连接到本地调试分析。 + +### 主动连接调试 + +首先需要远程服务配置启动脚本: + +```shell +JAVA_OPTS="$JAVA_OPTS -Xdebug +-Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=8000" +``` + +如果是启动jar包,指令: + +```shell +java -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=8000 -jar test.jar +``` + +| 命令 | 解释 | +| --------- | ------------------------------------------------------------ | +| -Xdebug | 通知JVM工作在DEBUG模式下 | +| -Xrunjdwp | 通知JVM使用(java debug wire protocol)来运行调试环境 | +| transport | 监听Socket端口连接方式 | +| server | =y表示当前是调试服务端,=n表示当前是调试客户端 | +| suspend | =n表示启动时不中断(如果启动时中断,一般用于调试启动不了的问题) | +| address | =8000表示本地监听8000端口 | + +### 被动连接 + +首先需要远程服务配置启动脚本: + +```shell +JAVA_OPTS="$JAVA_OPTS -Xdebug +-Xrunjdwp:transport=dt_socket,address=127.0.0.1:8000,suspend=y" +``` + +如果是启动jar包,指令: + +```shell +java -Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=n,address=8000 -jar test.jar +``` + +参数含义和主动连接调试一样,只是这里suspend=y表示启动时就中断,需要连接本地IDE调试启动。address=ip:port,ip需要修改为本地的对外IP。 + +这样远程项目启动时就连接到本地,方便调试项目启动不了的问题。 + +### IDEA远程debug + +![1570773224689](./images/003/1570773224689.png) + +添加Remote配置,在右侧的`Configuration`窗口中配置具体内容。 + +如果是主动连接调试,`Debugger mode`选择`Attach to remote JVM`。 + +如果是被动连接调试,`Debugger mode`选择`Listen to remote JVM`。 + diff --git "a/javaweb/\344\270\213\350\275\275\346\226\207\344\273\266.md" "b/javaweb/\344\270\213\350\275\275\346\226\207\344\273\266.md" new file mode 100644 index 0000000..193b85c --- /dev/null +++ "b/javaweb/\344\270\213\350\275\275\346\226\207\344\273\266.md" @@ -0,0 +1,43 @@ +## 下载文件 + +**思路:** + +1. 设置返回的内容格式为二进制 +2. 设置下载后的文件名称 +3. 读取文件流并写出Response的输出流 +4. 关闭文件输入流 + +```java + /** + * 下载文件 + * + * @param filepath 下载文件的路径 + * @param filename 下载后文件的名称 + */ + public static void download(HttpServletResponse response, String filepath, String filename) { + InputStream inputStream = null; + try { + // 设置返回类型为二进制 + response.setHeader("content-type", "application/octet-stream"); + response.setContentType("application/octet-stream"); + // 设置下载后的文件名称 + response.setHeader("Content-Disposition", + "attachment;filename=" + URLEncoder.encode(filename, "UTF-8")); + // 写出二进制数据 + OutputStream outputStream = response.getOutputStream(); + inputStream = new FileInputStream(new File(filepath)); + int len; + byte[] data = new byte[1024]; + while ((len = inputStream.read(data)) != -1) { + outputStream.write(data, 0, len); + } + outputStream.flush(); + + } catch (Exception e) { + throw new RuntimeException(e); + } finally { + IoUtil.close(inputStream); + } + } +``` + diff --git "a/javaweb/\350\267\250\345\237\237\350\256\277\351\227\256.md" "b/javaweb/\350\267\250\345\237\237\350\256\277\351\227\256.md" new file mode 100644 index 0000000..e15e691 --- /dev/null +++ "b/javaweb/\350\267\250\345\237\237\350\256\277\351\227\256.md" @@ -0,0 +1,65 @@ +### 为什么要解决跨域访问? + +在`http`请求中,默认只能在同一个域中进行访问,如果访问不同的域,则会被认为是非法操作,浏览器会提示`No 'Access-Control-Allow-Origin'`错误。 + +**注: ** 所谓 `域` 就是网址名称。如`https://www.baidu.com`,它的域就是`baidu.com`。如果我在`baidu.com`下面想访问`google.com`下面的资源,这就叫跨域。 + + + +### 怎么解决跨域? + +#### 前端使用 JSONP 的方式请求 + +前端可以通过使用`jsonp`执行跨域请求。这里不多做解释。 + +#### 服务器通过对响应进行配置 + +`java`服务器可以对所有的请求进行拦截,通过设置`response`对象的`header`实现跨域。 + +**`response`需要设置的header和值的对应关系** + +| header 名 | header 值 | +| ---------------------------- | ------------------------------------------------------------ | +| Access-Control-Allow-Origin | * | +| Access-Control-Allow-Methods | POST, GET, DELETE, OPTIONS, DELETE, PUT | +| Access-Control-Allow-Headers | Content-Type, x-requested-with, X-Custom-Header, HaiYi-Access-Token | + +### 代码 + +```java +import javax.servlet.*; +import javax.servlet.http.HttpServletRequest; +import javax.servlet.http.HttpServletResponse; +import java.io.IOException; + +/** + * 允许跨域过滤器 + * + * @author zhoutaotao + * @date 2019/5/31 + */ +public class AllowOriginFilter implements Filter { + + @Override + public void init(FilterConfig filterConfig) throws ServletException { + + } + + @Override + public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain chain) throws IOException, ServletException { + HttpServletRequest request = (HttpServletRequest) servletRequest; + HttpServletResponse response = (HttpServletResponse) servletResponse; + + response.setHeader("Access-Control-Allow-Origin", "*"); + response.setHeader("Access-Control-Allow-Methods", "POST, GET, DELETE, OPTIONS, DELETE, PUT"); + response.setHeader("Access-Control-Allow-Headers", "Content-Type, x-requested-with, X-Custom-Header, HaiYi-Access-Token"); + chain.doFilter(request, response); + } + + @Override + public void destroy() { + + } +} +``` + diff --git "a/javaweb/\350\275\254\345\217\221\344\270\216\351\207\215\345\256\232\345\220\221.md" "b/javaweb/\350\275\254\345\217\221\344\270\216\351\207\215\345\256\232\345\220\221.md" new file mode 100644 index 0000000..a52b605 --- /dev/null +++ "b/javaweb/\350\275\254\345\217\221\344\270\216\351\207\215\345\256\232\345\220\221.md" @@ -0,0 +1,42 @@ +## 转发 + +```java +request.getRequestDispatcher("success.jsp").forward(request,response); +``` + +> 在服务器组件收到用户请求后。经过它的处理后有传递给了另一个组件。不修改用户的请求码。各组件处理完之后在返回给用户 + +**请求流程** + +用户请求 ---> 服务器 ---> 组件1 ---> 组件2 ---> 服务器 ---> 用户 + +> 转发时,用户的请求不会改变,只能服务器内部组件之间进行跳转,同时,多个组件之间共享一个**request**对象,所以说转发是可以传值的,但也仅限于`request`范围 + + + +## 重定向 + +```java +response.sendRedirect("http://domain/success.jsp"); +``` + +> 在服务器组件收到用户请求后。经过处理修改用户请求。在返回给用户。这样用户再次使用这个请求就会被动的使用新的请求了。 + +**请求流程** + +用户请求 ---> 服务器 ---> 组件 ---> 服务器 ---> 用户 ---> 新的请求 + +> 重定向使用两次客户端请求操作。效率相较于转发较低。由于是多次请求,所以请求之间不会共享`request`对象,所以说重定向是无法传值的,但是可以通过在`url`中增加参数进行传递。 +> +> 如:`response.sendRedirect("http://domain/success.jsp?name=user");` + + + +## 转发与重定向的区别 + +1. 转发使用的是`request.getRequestDispatcher()`方法;重定向使用的是`response.sendRedirect()`; +2. 转发:浏览器URL的地址栏不变。重定向:浏览器URL的地址栏改变; +3. 转发是服务器行为,重定向是客户端行为; +4. 转发是浏览器只做了一次访问请求。重定向是浏览器做了至少两次的访问请求; +5. 转发2次跳转之间传输的信息不会丢失,重定向2次跳转之间传输的信息会丢失(request范围)。 + diff --git "a/js/jquery\345\261\236\346\200\247\346\223\215\344\275\234.md" "b/js/jquery\345\261\236\346\200\247\346\223\215\344\275\234.md" new file mode 100644 index 0000000..1078785 --- /dev/null +++ "b/js/jquery\345\261\236\346\200\247\346\223\215\344\275\234.md" @@ -0,0 +1,12 @@ +## 获取属性 + +obj.attr('disabled'); + +## 添加属性 + +obj.attr('disabled', 'disabled'); + +## 移除属性 + +obj.removeAttr('disabled'); + diff --git "a/js/jquery\350\216\267\345\217\226\351\235\236form\346\240\207\347\255\276\347\232\204value.md" "b/js/jquery\350\216\267\345\217\226\351\235\236form\346\240\207\347\255\276\347\232\204value.md" new file mode 100644 index 0000000..d0c6226 --- /dev/null +++ "b/js/jquery\350\216\267\345\217\226\351\235\236form\346\240\207\347\255\276\347\232\204value.md" @@ -0,0 +1,29 @@ +## jquery 获取\
  • 标签的value值 + +对于普通标签来说,可以使用jquery的`val()`方法获取属性,如: + +```html + +``` + +```js +var myname = $('input').val(); +``` + +而对于`
  • `标签来说,如果value存放的不是数字,使用`val()`方法是无法获取到真正的值的, 如: + +```html +
      +
    • username
    • +
    +``` + +```js +var zero = $('li').val(); // 获取不到,结果为0 +var myname = $('li').attr('value'); // 可以正确获取到myname值 +``` + +**原因:** + +HTML的li标签的value属性存在一下规定:规定列表项目为数字,所以它的value只能是数字。 + diff --git "a/js/\344\275\277\347\224\250FormData\344\270\212\344\274\240\346\226\207\344\273\266.md" "b/js/\344\275\277\347\224\250FormData\344\270\212\344\274\240\346\226\207\344\273\266.md" new file mode 100644 index 0000000..90d44bd --- /dev/null +++ "b/js/\344\275\277\347\224\250FormData\344\270\212\344\274\240\346\226\207\344\273\266.md" @@ -0,0 +1,72 @@ +## 使用FormData上传文件 + +### form标签上传文件 + +```html +
    + + + + + +
    +``` + +使用常规的form表单方式可以实现文件上传,但是当用户点击提交按钮时,页面也会跟随着跳转,用户体验很差。 + +### FromData异步上传 + +`FormData`是js提供的用于手动组装from表单的数据,为form数据的提交提供了更加灵活的方式。 + +#### 初始化FromData + +```html +
    + + + + +
    +``` + +```js +// 获取form表单对象 +var formObj = document.getElementById('myform'); +// 通过form表单构造FromData对象 +var formData = new FormData(formObj); +``` + +#### 添加属性 + +```js +// 获取文件 +var file = $('#id_file')[0].files[0]; +// 文件名 +var filename = file.name; +// 直接构建FormData +var formData = new FormData(); +// 添加上传的文件 +formData.append('file', file); +// 添加其他属性 +formData.append('serverId', 'myServerId'); +``` + +### 使用jquery上传 + +```js +$.ajax({ + url: '后端提供的上传url', + data: formData, // 使用上面构建好的FromData对象 + type: 'post', + cache: false, // 上传文件不需要缓存 + processData: false, // 用于对data参数进行序列化,必须为false + contentType: false, // 必须为false + success: function(data) { + // 处理成功后的回调函数 + }, + error: function(data) { + // 处理失败后的回调函数 + } +}) +``` + diff --git "a/js/\345\256\232\346\227\266\345\231\250.md" "b/js/\345\256\232\346\227\266\345\231\250.md" new file mode 100644 index 0000000..11db017 --- /dev/null +++ "b/js/\345\256\232\346\227\266\345\231\250.md" @@ -0,0 +1,40 @@ +## JS 定时器 + +### setTimeout + +设置一个定时器,在定时器到期后执行一次函数或代码段 + +```js +var timeoutId = setTimeout(func[,delay, param1, param2...]); +var timeoutId = setTimeout(code[,delay]); +``` + +> * timeoutId: 定时器id +> * func: 延迟执行的函数 +> * code: 延迟后执行的代码字符串, 不推荐使用 +> * delay: 延迟时间(单位:毫秒),默认0 +> * param1: 向延迟函数传递的参数 + +```js +clearTimeout(timeoutId); // 清除定时器 +``` + +### setInterval + +以固定的时间间隔重复一个函数或代码段 + +```js +var intervalId = setInterval(func[,delay, param1, param2...]); +var intervalId = setInterval(code[,delay]); +``` + +> - intervalId: 定时器id +> - func: 延迟执行的函数 +> - code: 延迟后执行的代码字符串, 不推荐使用 +> - delay: 延迟时间(单位:毫秒),默认0 +> - param1: 向延迟函数传递的参数 + +```js +clearInterval(intervalId); // 清除定时器 +``` + diff --git "a/js/\350\247\246\345\217\221\344\272\213\344\273\266.md" "b/js/\350\247\246\345\217\221\344\272\213\344\273\266.md" new file mode 100644 index 0000000..bb593c2 --- /dev/null +++ "b/js/\350\247\246\345\217\221\344\272\213\344\273\266.md" @@ -0,0 +1,26 @@ +## 添加事件监听器 + +在**文件上传**的业务中,我们不免会遇到由于原生的文件上传按钮太丑导致需要自定位上传按钮的情形。 + +在该需求中,我们可以通过一个绘制好的按钮或其他标签的点击事件来实现,通过监听该标签的点击事件,动态的调用文件上传按钮的点击事件。 + +**HTML** + +```html +
    + + + +
    +``` + +**JS** + +```js +// 动态将按钮的点击事件替换为文件上传框的点击事件 +document.querySelector('#header_select_file').addEventListener('click', + function () { + document.querySelector('#header_file').click(); + }); +``` + diff --git "a/linux/centos7\345\256\211\350\243\205mysql5.6.md" "b/linux/centos7\345\256\211\350\243\205mysql5.6.md" new file mode 100644 index 0000000..f24d8b5 --- /dev/null +++ "b/linux/centos7\345\256\211\350\243\205mysql5.6.md" @@ -0,0 +1,262 @@ +### 环境准备 + +| 环境 | 版本 | +| ------ | ---- | +| centos | 7 | +| mysql | 5.7 | + +### 必要工具 + +[MySQL yum源5.7](https://cdn.mysql.com//Downloads/MySQL-5.7/mysql-community-server-5.7.26-1.el7.x86_64.rpm) + +### 开始安装 + +#### 下载mysql yum源 + +执行如下命令下载mysql rpm 官网源: + +```shell + wget https://cdn.mysql.com//Downloads/MySQL-5.7/mysql-community-server-5.7.26-1.el7.x86_64.rpm +#### 输出 +--2019-05-08 03:25:45-- https://cdn.mysql.com//Downloads/MySQL-5.7/mysql-community-server-5.7.26-1.el7.x86_64.rpm +Resolving cdn.mysql.com (cdn.mysql.com)... 23.209.176.104 +Connecting to cdn.mysql.com (cdn.mysql.com)|23.209.176.104|:443... connected. +HTTP request sent, awaiting response... 200 OK +Length: 173541272 (166M) [application/x-redhat-package-manager] +Saving to: ‘mysql-community-server-5.7.26-1.el7.x86_64.rpm’ +100%[=================================================================>] 173,541,272 99.1MB/s in 1.7s +2019-05-08 03:25:46 (99.1 MB/s) - ‘mysql-community-server-5.7.26-1.el7.x86_64.rpm’ saved [173541272/173541272] +``` + +下载完成后会在当前目录下出现名字为:`mysql-community-server-5.7.26-1.el7.x86_64.rpm`的文件。 + +#### 安装yum源 + +通过`yum`命令安装mysql源 + +```shell +yum localinstall mysql-community-server-5.7.26-1.el7.x86_64.rpm +### 输出 +Loaded plugins: fastestmirror +Examining mysql-community-server-5.7.26-1.el7.x86_64.rpm: mysql-community-server-5.7.26-1.el7.x86_64 +Marking mysql-community-server-5.7.26-1.el7.x86_64.rpm to be installed +Resolving Dependencies +--> Running transaction check +---> Package mysql-community-server.x86_64 0:5.7.26-1.el7 will be installed +--> Processing Dependency: mysql-community-common(x86-64) = 5.7.26-1.el7 for package: mysql-community-server-5.7.26-1.el7.x86_64 +... +``` + +#### 查看当前源启用的mysql版本 + +```shell +yum repolist all | grep mysql +### 输出 +mysql-connectors-community/x86_64 MySQL Connectors Community enabled: 108 +mysql-connectors-community-source MySQL Connectors Community - disabled +mysql-tools-community/x86_64 MySQL Tools Community enabled: 90 +mysql-tools-community-source MySQL Tools Community - Sour disabled +mysql55-community/x86_64 MySQL 5.5 Community Server disabled +mysql55-community-source MySQL 5.5 Community Server - disabled +mysql56-community/x86_64 MySQL 5.6 Community Server enabled: 463 +mysql56-community-source MySQL 5.6 Community Server - disabled +mysql57-community-dmr/x86_64 MySQL 5.7 Community Server D disabled +mysql57-community-dmr-source MySQL 5.7 Community Server D disabled +``` + +倒数第二列**disable**表示直接通过**yum**进行安装时,会直接忽略对应的项目。**enable**则恰恰相反。 + +**通过命令启用或禁止源** + +```shell +yum-config-manager --disable mysql56-community # 禁用 mysql56-community +yum-config-manager --enable mysql56-community # 启用 mysql56-community +``` + +**通过配置文件启用或禁止源** + +`vim /etc/yum.repos.d/mysql-community.repo` + +*enabled*为**1**时,表示启用该源。为**0**则表示禁用该源。 + +```properties +... +# Enable to use MySQL 5.5 +[mysql55-community] +name=MySQL 5.5 Community Server +baseurl=http://repo.mysql.com/yum/mysql-5.5-community/el/7/$basearch/ +enabled=0 +gpgcheck=1 +gpgkey=file:/etc/pki/rpm-gpg/RPM-GPG-KEY-mysql + +# Enable to use MySQL 5.6 +[mysql56-community] +name=MySQL 5.6 Community Server +baseurl=http://repo.mysql.com/yum/mysql-5.6-community/el/7/$basearch/ +enabled=1 +gpgcheck=1 +gpgkey=file:/etc/pki/rpm-gpg/RPM-GPG-KEY-mysql +... +``` + +#### 安装mysql服务 + +通过`yum`安装mysql server + +```shell +yum install mysql-community-server +### 输出 +Loaded plugins: fastestmirror +mysql-connectors-community | 2.5 kB 00:00:00 +mysql-tools-community | 2.5 kB 00:00:00 +mysql56-community | 2.5 kB 00:00:00 +... + +Dependencies Resolved +========================================================================================= + Package Arch Version Repository Size +========================================================================================= +Installing: + mysql-community-server x86_64 5.6.44-2.el7 mysql56-community 60 M + +Transaction Summary +======================================================================================= +Install 1 Package + +Total download size: 60 M +Installed size: 252 M +Is this ok [y/d/N]: y # 这里需要确认一下 +Downloading packages: +mysql-community-server-5.6.44-2.el7.x86_64.rpm | 60 MB 00:00:01 +... +Installed: + mysql-community-server.x86_64 0:5.6.44-2.el7 +Complete! +``` + +### 使用mysql + +#### 启动和查看mysql服务状态 + +```shell +# 启动mysql服务 +systemctl start mysqld +# 查看mysql状态 +systemctl status mysqld +### 输出 +● mysqld.service - MySQL Community Server + Loaded: loaded (/usr/lib/systemd/system/mysqld.service; enabled; vendor preset: disabled) + Active: active (running) since Wed 2019-05-08 03:34:08 EDT; 8s ago + Process: 2694 ExecStartPost=/usr/bin/mysql-systemd-start post (code=exited, status=0/SUCCESS) + Process: 2634 ExecStartPre=/usr/bin/mysql-systemd-start pre (code=exited, status=0/SUCCESS) + Main PID: 2693 (mysqld_safe) +... +``` + +**Active: active (running)**表示当前服务正在运行。 + +#### 修改密码 + +**mysqladmin**可以修改root用户的密码。语法为:`mysqladmin -u用户名 -p原密码 password 新密码`。 + +由于mysql5.6安装完成后默认密码为空,所有我们可以使用下面的命令来将root用户的密码修改为root + +```shell +mysqladmin -u root password root +``` + +#### 修改mysql的配置 + +配置文件的路径为:`/etc/my.conf` + +查看数据库的编码格式 + +```sql +mysql> show variables like '%char%'; ++--------------------------+----------------------------+ +| Variable_name | Value | ++--------------------------+----------------------------+ +| character_set_client | utf8 | +| character_set_connection | utf8 | +| character_set_database | utf8 | +| character_set_filesystem | binary | +| character_set_results | utf8 | +| character_set_server | utf8 | +| character_set_system | utf8 | +| character_sets_dir | /usr/share/mysql/charsets/ | ++--------------------------+----------------------------+ +8 rows in set (0.01 sec) +``` + +修改默认的编码格式,在配置文件中增加如下内容 + +```properties +[client] +default-character-set=utf8 + +[mysqld] +default-storage-engine=INNODB +character-set-server=utf8 +collation-server=utf8_general_ci +``` + +修改指定表的编码格式 + +```sql +alter table tableName character set utf8; +``` + +### mysql 核心参数 + +#### cpu优化 + +| 参数 | 参数功能 | 取值范围 | 经验值 | +| ------------------------- | ------------------------------------------------------------ | -------- | ---------------------- | +| innodb_thread_concurrency | 并发执行的线程的数量(同时干活的线程的数量),保护系统不被hang住 | 0-1000 | 一般要求是cpu核数的4倍 | + +#### 内存优化 + +| 参数 | 参数功能 | 取值范围 | 经验值 | +| ------------------------------ | ---------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | +| innodb_buffer_pool_size | 缓存innodb表和索引数据的内存池大小 | <=5.7.4:非动态全局;>=5.7.5:动态全局;64位:默认128M,最小5M,最大2^64-1 | 一般设置内存的50%~80%,根据实际内存大小设置,如果内存不是很大,可以考虑50~70%,如果内存很大,可以考虑到70%~80%,如果总是产生Innodb_buffer_pool_wait_free,说明buffer_pool设置过小 | +| tmpdir | 存放临时文件和临时表目录,可以设置多个路径用:分隔 | 全局参数,非动态,更改需要重启数据库 | 单独挂载,对读写要求很高,放在高性能盘,独立分区 | +| innodb_buffer_pool_instances | buffer_pool被分成多少实例 | 非动态,全局; | 8个或者16个,根据实际buffer pool大小设置,如果实例数量过小,会导致latch争用 | +| innodb_max_dirty_pages_pct | buffer pool中最大脏页占比 | 百分比 | 75%~90%,如果io能力足够强,例如使用了闪卡,可以将这个参数调小;该参数设置越小,写入压力越大。 | +| innodb_max_dirty_pages_pct_lwm | 预刷新脏页比例,可以有效控制脏页比例达到最大脏页占比 | 动态,全局;<5.7.4:默认0,范围0~99;>=5.7.5:默认0,范围0~99.99 | 70,控制脏页比率,防止达到脏页最大占比 | + +#### io优化 + +| 参数 | 参数功能 | 取值范围 | 经验值 | +| ------------------------------ | ---------------------------------------------------------- | ------------------------------------------------------------ | ------------------------------------------------------------ | +| innodb_io_capacity | 设定了后台任务执行的io量的上限 | 动态,全局;64位:默认200,最小100,最大2^64-1 | 每秒后台进程处理IO数据的上限,一般为IO QPS总能力的75% | +| innodb_io_capacity_max | 在紧急情况下,innodbio容量上限的最大值,2000是初始默认值。 | 动态,全局;unix-64位:默认是2000,最小100,最大2^32-1 | 根据innodb_io_capacity的2倍进行设置 | +| innodb_log_files_in_group | redo日志的组数,即logfile的数量 | 非动态,全局;默认2个,范围2~100 | 一般设置5组 | +| innodb_log_file_size | 每个logfile的大小 | \<=5.7.10:默认50M,最小1M,最大512G/innodb_log_files_in_group;>=5.7.11:默认50M,最小4M,最大512G/innodb_log_files_in_group | 一般设置2G,每20~30分钟切换一次redo比较合适, 案例:业务人员反映晚上9:10-9:15有大量交易放弃,交易失败,让dba看看这个时间段数据库的性能情况,dba发现这个时间段有redo切换,buffer写入明显增加(写入抖动),TPS明显降低(事务提交了降低),怀疑redo文件过小,组数过少,脏数据写入速度慢。 解决方案:增加redo数量和大小,加大脏数据的写入力度,需要重启数据库 | +| innodb_flush_method | 采用O_DIRECT方式写入的时候绕过文件系统缓存,直接写入磁盘 | 非动态,全局;默认NULL,有效值:fsync、O_DSYNC、littlesync、nosync、O_DIRECT、O_DIRECT_NO_FSYNC | =O_DIRECT | +| innodb_max_dirty_pages_pct | buffer pool中最大脏页占比 | | 75%~90%,如果io能力足够强,例如使用了闪卡,可以将这个参数调小;该参数设置越小,写入压力越大。 | +| innodb_max_dirty_pages_pct_lwm | 预刷新脏页比例,可以有效控制脏页比例达到最大脏页占比 | 动态,全局;<5.7.4:默认0,范围0~99;>=5.7.5:默认0,范围0~99.99 | 70,,当脏块达到70%的时候,触发网磁盘写,控制脏页比率,防止达到脏页最大占比 | +| innodb_flush_neighbors | 是否关闭邻接页刷新 | 0关闭,1不关闭 | 一般关闭邻接页刷新 | + +#### 连接优化 +| 参数 | 参数功能 | 取值范围 | 经验值 | +| -------------------- | ------------------------------------------------------------ | ----------------------------------------- | ------------------------------------------------------ | +| max_connections | 允许客户端并发连接数量 | 动态,全局;默认151,最小1,最大10000 | 1024,一般不要超过2000 | +| max_user_connections | 任何一个mysql用户允许最大并发连接数 | 动态,全局和会话;默认0,范围0~4294967295 | 256 | +| table_open_cache | 打开表缓存,跟表数量没关系1000个连接上来,都需要访问A表,那么会打开1000个表,打开1000个表是指mysql创建1000个这个表的对象,连接直接访问表对象 | 动态,全局;默认 | 4096 ,监控opened_tables值,如果很大,说明该值设置小了 | +| thread_cache_size | 线程缓存 | | 512 | +| wait_timeout | app应用连接mysql进行操作完毕后,空闲断开的时间,单位秒 | | 120 | + +#### 数据一致性优化 + +```innodb_flush_log_at_trx_commit=1`` + +0,不管有没有提交,每秒钟都写到binlog日志里 +1,每次提交事务,都会把log buffer的内容写到磁盘里去,对日志文件做到磁盘刷新,安全最好 +2,每次提交事务,都写到操作系统缓存,由OS刷新到磁盘,性能最好 + +```sync_binlog=1``` + +0,事务提交后,mysql不做fsync之类的刷盘,由文件系统来决定什么落盘 +n,多少次提交,每n次提交持久化磁盘 +生产设为1 + diff --git "a/linux/centos\346\211\251\345\244\247swap\345\210\206\345\214\272.md" "b/linux/centos\346\211\251\345\244\247swap\345\210\206\345\214\272.md" new file mode 100644 index 0000000..f84c774 --- /dev/null +++ "b/linux/centos\346\211\251\345\244\247swap\345\210\206\345\214\272.md" @@ -0,0 +1,64 @@ +### 前言 + +linux下扩大系统swap分区有两种方法,一种是重新建立swap分区,一种是增加swap分区。 + +**注:**必须拥有`root`权限,并且不小心会破坏硬盘数据(做好备份),建议增加swap分区。 + + + +#### 方法一:新建swap分区 + +1、以root身份进入系统,输入下面命令停止交换分区 + +```sh +# swapoff -a +``` + +2、用 `fdisk` 命令加swap分区的盘符,(例如:`# fdisk /dev/sdb`)剔除swap分区,输入 `d` 删除swap分区,然后再 `n` 添加分区(添加时硬盘必须要有可用空间),然后再用 `t` 将新添的分区`id` 改为 `82`(linux swap类型),最后用 `w` 将操作实际写入硬盘。 + +3、格式化swap分区,`sdb2`盘符要使用 `p` 命令显示的实际分区设备名 + +```sh +# mkswap /dev/sdb2 +``` + +4、启动新的swap分区 + +```sh +swapon /dev/sdb2 +``` + +5、配置开机启动swap分区,在 `/etc/fstab` 文件内最后添加一行 + +``` +/dev/sdb2 swap swap defaults 0 0 +``` + + + +#### 方法二:增加swap分区 + +1、创建交换分区的文件,增加1G大小的交换分区,命令如下,其中count等于要增加的块大小 + +```sh +# dd if=/dev/zero of /home/swapfile bs=1M count=1024 +``` + +2、设置交换分区文件,建立swap的文件系统 + +```sh +# mkswap /home/swapfile +``` + +3、立即启用交换分区文件 + +```sh +# swapon /home/swapfile +``` + +4、配置开机启动swap分区,在 `/etc/fstab` 文件内最后添加一行 + +``` +/home/swapfile swap swap defaults 0 0 +``` + diff --git "a/linux/centos\346\220\255\345\273\272consul\351\233\206\347\276\244.md" "b/linux/centos\346\220\255\345\273\272consul\351\233\206\347\276\244.md" new file mode 100644 index 0000000..e87b9f9 --- /dev/null +++ "b/linux/centos\346\220\255\345\273\272consul\351\233\206\347\276\244.md" @@ -0,0 +1,621 @@ +### 准备 + +#### 下载 + +下载地址`https://www.consul.io/downloads.html` + +#### 安装 + +- 解压`unzip consule-consul_1.2.2_linux_amd64.zip` +- 拷贝 `cp ./consule /usr/bin` + +### 命令(cli) + +#### 帮助 + +`consul command -h` + +#### API选项 + +- `-ca-file=`- 与Consul通信时用于TLS的CA文件的路径。这也可以通过`CONSUL_CACERT`环境变量指定。 +- `-ca-path=` - 与Consul通信时用于TLS的CA证书目录的路径。这也可以通过`CONSUL_CAPATH`环境变量指定。 +- `-client-cert=`- verify_incoming启用时用于TLS的客户端证书文件的路径 。这也可以通过CONSUL_CLIENT_CERT 环境变量指定。 +- `-client-key=`- verify_incoming启用时用于TLS的客户机密钥文件的路径 。这也可以通过CONSUL_CLIENT_KEY 环境变量指定。 +- `-http-addr=` - 具有端口的Consul代理的地址。这可以是IP地址或DNS地址,但必须包含端口。这也可以通过`CONSUL_HTTP_ADDR`环境变量指定。在Consul 0.8及更高版本中,默认值为[http://127.0.0.1:8500](http://127.0.0.1:8500/),可以选择使用https。也可以通过设置环境变量将方案设置为HTTPS `CONSUL_HTTP_SSL=true`。 +- `-tls-server-name=` - 通过TLS连接时用作SNI主机的服务器名称。这也可以通过`CONSUL_TLS_SERVER_NAME` 环境变量指定。 +- `-token=`- 在请求中使用的ACL令牌。这也可以通过`CONSUL_HTTP_TOKEN`环境变量指定。如果未指定,则查询将默认为HTTP地址处的Consul代理的令牌。 +- `-datacenter=` - 要查询的数据中心的名称。如果未指定,则查询将默认为HTTP地址处的Consul代理的数据中心。 + +- `-stale` - 允许任何Consul服务器(非领导者)响应此请求。这允许更低的延迟和更高的吞吐量,但可能导致过时的数据。此选项对非读取操作没有影响。默认值为false。 + +#### 代理人(agent) + +该`consul agent`命令是Consul的核心:它运行代理,执行维护成员资格信息,运行检查,公布服务,处理查询等重要任务。 + +`console agent -dev` + +#### 目录(category) + +| 命令 | 解释 | +| ---------------------------------------- | -------------------------- | +| consul category datacenters | 列出多有的数据中心 | +| consul category nodes | 列出所有的节点 | +| console category nodes -service=redis | 列出提供特定服务的所有节点 | +| consul category services | 列出所有服务 | +| consul category services -node=worker-01 | 列出节点上的所有服务 | + +##### 数据中心 + +用法: `consul catalog datacenters [options]` + +##### 节点 + +用法: `consul catalog nodes [options]` + +目录列表节点选项 + +- `-detailed`- 输出有关节点的详细信息,包括其地址和元数据。 +- `-near=` - 节点名称,用于根据从该节点估计的往返时间按升序对节点列表进行排序。传递`"_agent"`将使用此代理的节点进行排序。 +- `-node-meta=` - 使用给定键=值对过滤节点的元数据。可以多次指定该标志以过滤多个元数据源。 +- `-service=` - 过滤节点的服务ID或名称。仅返回提供给定服务的节点。 + +##### 服务 + +用法: `consul catalog services [options]` + +目录列表节点选项 + +- `-node=`- `id or name`列出服务的节点。 +- `-node-meta=`- 使用给定`key=value`对过滤节点的元数据 。如果指定,则仅返回在与给定元数据匹配的节点上运行的服务。可以多次指定该标志以过滤多个元数据源。 +- `-tags` - 将每个服务的标签显示为每个服务条目旁边的逗号分隔列表。 + +#### 链接(connect) + +##### 证书颁发机构(CA) + +**get-config** + +此命令显示当前的CA配置。 + +用法: `consul connect ca get-config [options]` + +**set-config** + +修改当前的CA配置。如果这导致使用新的根证书,则将触发[根循环](https://www.consul.io/docs/connect/ca.html#root-certificate-rotation)过程。 + +用法: `consul connect ca set-config [options]` + +命令选项 + +- `-config-file`- (必需)指定用于新配置的JSON格式的文件。 + +##### 代理(proxy) + +用法: `consul connect proxy [options]` + +代理选项 + +- `-service` - 此代理表示的服务的名称。此服务不需要实际存在于Consul目录中,但需要正确的ACL权限(`service:write`)。 +- `-upstream`- 支持连接的上游服务。格式应为'name:addr',例如'db:8181'。这将使端口8181上的“db”可用。当与端口8181建立常规TCP连接时,代理将服务发现“db”并建立一个标识为`-service`值的Connect mTLS连接。该标志可以重复多次。 +- `-listen` - 用于侦听代理服务的入站连接的地址。必须使用-service和-service-addr指定。如果未指定,则不启动入站侦听器。 +- `-service-addr` - 代理的本地服务的地址。必需的 `-listen`。 +- `-register` - 向当地Consul代理自行注册,使该代理在目录中作为Connect-capable服务提供。这只适用于`-listen`。 +- `-register-id`- `-register`设置为消除服务ID歧义时服务的可选ID后缀。默认情况下,服务ID为“-proxy”,其中``是`-service`的值。 +- `-log-level`- 指定日志级别。 +- `-pprof-addr`- 通过pprof启用调试。提供host:port(或只是':port')可以在该地址上分析HTTP端点。 +- `-proxy-id` - 本地代理上的代理ID。这仅适用于测试托管代理模式。 + +例子 + +下面的示例显示如何启动本地代理以建立表示前端服务的“db”的出站连接。一旦运行,任何创建到指定端口(8181)的TCP连接的进程将建立与标识为“前端”的“db”的相互TLS连接。 + +``` +$ consul connect proxy -service frontend -upstream db:8181 +``` + +下一个示例启动一个本地代理,该代理也接受端口8443上的入站连接,授权连接,然后将其代理到端口8080: + +``` +$ consul connect proxy \ + -service frontend \ + -service-addr 127.0.0.1:8080 \ + -listen ':8443' +``` + +#### 加入(join) + +该`join`命令告知Consul代理加入现有集群。新的Consul代理必须与群集中的至少一个现有成员连接才能加入现有群集。在加入该一个成员之后,八卦层接管,在集群中传播更新的成员资格状态。 + +如果您未加入现有群集,则该代理程序是其自己的隔离群集的一部分。其他节点可以加入它。 + +代理可以多次加入其他代理而不会出现问题。如果已经是集群一部分的节点加入另一个节点,则两个节点的集群将成为一个集群。 + +用法: `consul join [options] address ...` + +命令选项 + +- `-wan` - 对于以服务器模式运行的代理,代理将尝试加入WAN群集中的其他服务器集群。这用于在多个数据中心之间形成桥梁。 + +#### KV + +该`kv`命令用于通过命令行与Consul的KV存储进行交互。它公开了用于从商店插入,更新,读取和删除的顶级命令。 + +用法: `consul kv ` + +##### 删除(delete) + +用法: `consul kv delete [options] KEY_OR_PREFIX` + +KV删除选项 + +- `-cas` - 执行检查和设置操作。指定此值还需要设置-modify-index标志。默认值为false。 +- `-modify-index=` - 表示密钥的ModifyIndex的无符号整数。这与-cas标志结合使用。 +- `-recurse` - 递归删除路径中的所有键。默认值为false。 + +例子 + +要删除KV存储中名为“redis / config / connections”的键的值,请执行以下操作: + +`consul kv delete redis/config/connections` + +如果密钥不存在,则命令不会出错,并返回成功消息 + +要仅删除自给定索引后尚未修改的密钥,请指定`-cas`和`-modify-index`标记: + +``` +$ consul kv get -detailed redis/config/connections | grep ModifyIndex +ModifyIndex 456 + +$ consul kv delete -cas -modify-index=123 redis/config/connections +Error! Did not delete key redis/config/connections: CAS failed + +$ consul kv delete -cas -modify-index=456 redis/config/connections +Success! Deleted key: redis/config/connections +``` + +要以递归方式删除以给定前缀开头的所有键,请指定 `-recurse`标志: + +``` +$ consul kv delete -recurse redis/ +Success! Deleted keys with prefix: redis/ +``` + +**尾随斜杠**在递归删除操作中**很重要**,因为Consul会对提供的前缀执行贪婪匹配。如果你使用“foo”作为键,这将递归删除任何以这些字母开头的键,例如“foo”,“food”和“football”,而不仅仅是“foo”。要确保删除文件夹,请始终使用尾部斜杠。 + +将`-cas`选项组合在一起无效`-recurse`,因为您在单个操作中删除前缀下的多个键: + +``` +$ consul kv delete -cas -recurse redis/ +Cannot specify both -cas and -recurse! +``` + +##### 导出(export) + +该`kv export`命令用于从Consul的KV存储中检索给定前缀的KV对,并将JSON表示写入stdout。这可以与命令“consul kv import”一起使用,以在Consul集群之间移动整个树。 + +用法: `consul kv export [PREFIX]` + +例子 + +要在键值存储中的“vault /”处导出树: + +``` +$ consul kv export vault/ +# JSON output +``` + +##### 取值(get) + +用法: `consul kv get [options] [KEY_OR_PREFIX]` + +KV获取选项 + +- `-base64` - Base 64对值进行编码。默认值为false。 +- `-detailed` - 除了值之外,还提供有关密钥的其他元数据,例如ModifyIndex和可能已在密钥上设置的任何标志。默认值为false。 +- `-keys` - 列出以给定前缀开头但不是其值的键。如果您只需要键名本身,这将特别有用。此选项通常与-separator选项结合使用。默认值为false。 +- `-recurse` - 递归查看前缀为给定路径的所有键。默认值为false。 +- `-separator=` - 用作键之间分隔符的字符串。默认值为“/”,但只有在与-keys标志配对时才考虑此选项。 + +例子 + +要仅列出以指定前缀开头的键,请使用“-keys”选项。这样性能更高,并且有效载荷更小: + +``` +$ consul kv get -keys redis/config/ +redis/config/connections +redis/config/cpu +redis/config/memory +``` + +默认情况下,该`-keys`操作使用“/”分隔符,这意味着它不会递归到该分隔符之外。您可以通过设置选择不同的分隔符 `-separator=""`。 + +``` +$ consul kv get -keys -separator="c" redis +redis/c +``` + +或者,您可以通过将分隔符设置为空字符串来完全禁用分隔符: + +``` +$ consul kv get -keys -separator="" redis +redis/config/connections +redis/config/cpu +redis/config/memory +``` + +要列出根目录下的所有密钥,只需省略prefix参数: + +``` +$ consul kv get -keys +memcached/ +redis/ +``` + +##### 导入(import) + +该`kv import`命令用于从`kv export`命令生成的JSON表示中导入KV对。 + +用法: `consul kv import [DATA]` + +例子 + +要从文件导入,请在文件名前加上`@`: + +``` +$ consul kv import @values.json +# Output +``` + +要从stdin导入,请`-`用作数据参数: + +``` +$ cat values.json | consul kv import - +# Output +``` + +您也可以直接传递JSON,但必须小心shell转义: + +``` +$ consul kv import "$(cat values.json)" +# Output +``` + +##### 存值 (put) + +该`kv put`命令将数据写入KV存储中的给定路径。 + +用法: `consul kv put [options] KEY [DATA]` + +选项 + +- `-acquire` - 获得钥匙锁。如果密钥不存在,则此操作将创建密钥并获取锁定。会话必须已存在并通过-session标志指定。默认值为false。 +- `-base64` - 将数据视为基数为64的编码。默认值为false。 +- `-cas` - 执行检查和设置操作。指定此值还需要设置-modify-index标志。默认值为false。 +- `-flags=`- 要分配给此KV对的无符号整数值。Consul不会读取此值,因此客户端可以使用此值但对其用例有意义。默认值为0(无标志)。 +- `-modify-index=` - 表示密钥的ModifyIndex的无符号整数。这与-cas标志结合使用。 +- `-release`- 放弃给定路径上钥匙的锁定。这需要设置-session标志。密钥必须由会话保存才能解锁。默认值为false。 +- `-session=` - 用户定义的此会话标识符为字符串。这通常与-acquire和-release操作一起使用来构建强大的锁定,但它可以在任何键上设置。默认值为空(无会话)。 + +例子 + +对于更长或更敏感的值,可以通过在`@`符号前面添加前缀来读取文件: + +``` +$ consul kv put redis/config/password @password.txt +Success! Data written to: redis/config/connections +``` + +或者通过指定`-`符号从stdin读取值: + +``` +$ echo "5" | consul kv put redis/config/password - +Success! Data written to: redis/config/connections + +$ consul kv put redis/config/password - +5 + +Success! Data written to: redis/config/connections +``` + +要创建或调整锁定,请使用`-acquire`和`-session`标志。会话必须已存在(此命令不会创建或管理它): + +``` +$ consul kv put -acquire -session=abc123 redis/lock/update +Success! Lock acquired on: redis/lock/update +``` + +完成后,释放锁: + +``` +$ consul kv put -release -session=acb123 redis/lock/update +Success! Lock released on: redis/lock/update +``` + +### 代理(agent) + +使用该`consul agent`命令启动代理程序。此命令阻止,永久运行或直到被告知退出。agent命令有很多种`configuration options`,但大多数都有合理的默认值。 + +运行时`consul agent`,您应该看到与此类似的输出: + +``` +$ consul agent -data-dir=/tmp/consul +==> Starting Consul agent... +==> Consul agent running! + Node name: 'Armons-MacBook-Air' + Datacenter: 'dc1' + Server: false (bootstrap: false) + Client Addr: 127.0.0.1 (HTTP: 8500, DNS: 8600) + Cluster Addr: 192.168.1.43 (LAN: 8301, WAN: 8302) + +==> Log data will now stream in as it occurs: + + [INFO] serf: EventMemberJoin: Armons-MacBook-Air.local 192.168.1.43 +... +``` + +`consul agent`输出有几条重要信息: + +- **节点名称**:这是代理的唯一名称。默认情况下,这是计算机的主机名,但您可以使用该`-node`标志对其进行自定义 。 +- **数据中心**:这是代理配置为运行的数据中心。Consul为多个数据中心提供一流的支持; 但是,要高效工作,必须将每个节点配置为报告其数据中心。该`-datacenter`标志可用于设置数据中心。对于单DC配置,代理将默认为“dc1”。 +- **服务器**:这表示代理是在服务器还是客户端模式下运行。服务器节点具有参与共识仲裁,存储群集状态和处理查询的额外负担。另外,服务器可以处于“引导”模式。多个服务器无法处于引导模式,因为这会使群集处于不一致状态。 +- **客户端地址**:这是用于代理的客户端接口的地址。这包括HTTP和DNS接口的端口。默认情况下,这仅绑定到localhost。如果更改此地址或端口,则必须`-http-addr` 在运行命令时指定,`consul members`以指示如何联系代理。其他应用程序也可以使用HTTP地址和端口 来控制Consul。 +- **Cluster Addr**:这是用于群集中Consul代理之间通信的端口和端口集。并非群集中的所有Consul代理都必须使用相同的端口,但所有其他节点都**必须**可以访问此地址。 + +当在运行`systemd`在Linux上,领事通知发送systemd `READY=1`到`$NOTIFY_SOCKET`当LAN连接已经完成。为此,必须设置`join`或`retry_join`选项,并且必须设置服务定义文件`Type=notify`。 + +#### 命令行配置 + +- `-advertise` - 广告地址用于将我们通告的地址更改为群集中的其他节点。默认情况下,`-bind`通告地址。但是,在某些情况下,可能存在无法绑定的可路由地址。此标志允许闲聊不同的地址以支持此功能。如果此地址不可路由,则该节点将处于恒定的振荡状态,因为其他节点将不可路由性视为故障。 +- `-advertise-wan` - 广告WAN地址用于将我们通告的地址更改为通过WAN加入的服务器节点。当与`translate_wan_addrs`配置选项结合使用时,也可以在客户端代理上设置此选项。默认情况下,`-advertise`通告地址。但是,在某些情况下,所有数据中心的所有成员都不能位于同一物理或虚拟网络上,尤其是混合云和私有数据中心的混合设置。此标志使服务器节点通过公共网络为WAN进行闲聊,同时使用专用VLAN互相闲聊及其客户端代理,并且如果远程数据中心是远程数据中心,则允许从远程数据中心访问此地址时访问客户端代理。配置了`translate_wan_addrs`。 +- `-bootstrap` - 此标志用于控制服务器是否处于“引导”模式。重要的是,在此模式下,*每个*数据中心只能运行一台服务器。从技术上讲,允许自举模式的服务器作为Raft领导者自行选举。重要的是只有一个节点处于这种模式; 否则,无法保证一致性,因为多个节点能够自我选择。在引导群集后,建议不要使用此标志。 +- `-bootstrap-expect` - 此标志提供数据中心中预期的服务器数。不应提供此值,或者该值必须与群集中的其他服务器一致。提供后,Consul将等待指定数量的服务器可用,然后引导群集。这允许自动选择初始领导者。这不能与传统`-bootstrap`标志一起使用。此标志需要`-server`模式。 +- `-bind` - 应绑定到内部群集通信的地址。这是群集中所有其他节点都应该可以访问的IP地址。默认情况下,这是“0.0.0.0”,这意味着Consul将绑定到本地计算机上的所有地址,并将 第一个可用的私有IPv4地址通告给群集的其余部分。如果有**多个私有IPv4地址**可用,Consul将在启动时退出并显示错误。如果指定“[::]”,Consul将 通告第一个可用的公共IPv6地址。如果有**多个**可用的**公共IPv6地址**,Consul将在启动时退出并显示错误。Consul同时使用TCP和UDP以及相同的端口。如果您有防火墙,请务必同时允许这两种协议。 +- `-serf-wan-bind` - 应该绑定到Serf WAN八卦通信的地址。默认情况下,该值遵循与`-bind`相同的规则,如果未指定,`-bind`则使用该选项。 +- `-serf-lan-bind` - 应该绑定到Serf LAN八卦通信的地址。这是群集中所有其他LAN节点都应该可以访问的IP地址。默认情况下,该值遵循与`-bind`命令行标志相同的规则,如果未指定,`-bind`则使用该选项。 +- `-client`- Consul将绑定客户端接口的地址,包括HTTP和DNS服务器。默认情况下,这是“127.0.0.1”,仅允许环回连接。 +- `-config-file` - 要加载的配置文件。有关此文件格式的更多信息,请阅读“ 配置文件”部分。可以多次指定此选项以加载多个配置文件。如果多次指定,则稍后加载的配置文件将与先前加载的配置文件合并。在配置合并期间,单值键(string,int,bool)将简单地替换它们的值,而列表类型将被附加在一起。 +- `-config-dir` - 要加载的配置文件的目录。Consul将使用后缀“.json”或“.hcl”加载此目录中的所有文件。加载顺序是按字母顺序排列的,并且使用与上述`config-file`选项相同的合并例程 。可以多次指定此选项以加载多个目录。未加载config目录的子目录。有关配置文件格式的详细信息,请参阅“配置文件”部分。 +- `-config-format`- 要加载的配置文件的格式。通常,Consul会从“.json”或“.hcl”扩展名中检测配置文件的格式。将此选项设置为“json”或“hcl”会强制Consul解释具有或不具有扩展名的任何文件,以便以该格式进行解释。 +- `-data-dir` - 此标志为代理程序存储状态提供数据目录。这是所有代理商都需要的。该目录在重新启动后应该是持久的。这对于在服务器模式下运行的代理尤其重要,因为它们必须能够持久化群集状态。此外,该目录必须支持使用文件系统锁定,这意味着某些类型的已安装文件夹(例如VirtualBox共享文件夹)可能不适合。**注意:**服务器代理和非服务器代理都可以在此目录中的状态中存储ACL令牌,因此读访问可以授予对服务器上的任何令牌以及非服务器上的服务注册期间使用的任何令牌的访问权限。在基于Unix的平台上,文件使用0600权限编写,因此您应确保只有受信任的进程才能与Consul作为同一用户执行。在Windows上,您应确保该目录具有适当的权限,因为这些权限将被继承。 +- `-datacenter` - 此标志控制代理程序运行的数据中心。如果未提供,则默认为“dc1”。Consul拥有对多个数据中心的一流支持,但它依赖于正确的配置。同一数据中心中的节点应位于单个LAN上。 +- `-dev`- 启用开发服务器模式。这对于快速启动Consul代理并关闭所有持久性选项非常有用,可以启用内存服务器,该服务器可用于快速原型设计或针对API进行开发。在此模式下, Connect已启用,默认情况下将在启动时创建新的根CA证书。此模式**不适**用于生产用途,因为它不会将任何数据写入磁盘。 +- `-disable-host-node-id` - 将此设置为true将阻止Consul使用来自主机的信息生成确定性节点ID,而是生成将保留在数据目录中的随机节点ID。在同一主机上运行多个Consul代理进行测试时,这非常有用。在版本0.8.5之前的Consul中默认为false,在0.8.5及更高版本中默认为true,因此您必须选择加入基于主机的ID。使用生成基于主机的ID ,这是与HashiCorp的[Nomad](https://www.nomadproject.io/)共享的 ,因此如果您选择使用基于主机的ID,那么Consul和Nomad将使用信息在主机上自动在两个系统中分配相同的ID。 +- `-disable-keyring-file` - 如果设置,密钥环将不会持久保存到文件中。关机时任何已安装的密钥都将丢失,`-encrypt`启动时只有给定的 密钥可用。默认为false。 +- `-dns-port` - 要侦听的DNS端口。这将覆盖默认端口8600.这在Consul 0.7及更高版本中可用。 +- `-domain` - 默认情况下,Consul在“consul”中响应DNS查询。域。此标志可用于更改该域。假定此域中的所有查询都由Consul处理,不会以递归方式解析。 +- `-enable-script-checks`这可以控制是否在此代理上启用了执行脚本的运行状况检查,默认为`false`运营商必须选择允许这些检查。如果启用,建议还启用ACL以控制允许哪些用户注册新检查以执行脚本。这是在Consul 0.9.0中添加的。 +- `-encrypt` - 指定用于加密Consul网络流量的密钥。该密钥必须是16字节的Base64编码。创建加密密钥的最简单方法是使用 `consul keygen`。群集中的所有节点必须共享相同的加密密钥才能进行通信。提供的密钥将自动持久保存到数据目录,并在重新启动代理时自动加载。这意味着要加密Consul的八卦协议,只需在每个代理的初始启动序列上提供一次该选项。如果在使用加密密钥初始化Consul之后提供,则忽略提供的密钥并显示警告。 +- `-hcl` - HCL配置片段。此HCL配置片段将附加到配置中,并允许在命令行上指定配置文件的所有选项。可以多次指定此选项。这是在Consul 1.0中添加的。 +- `-http-port`- 要监听的HTTP API端口。这将覆盖默认端口8500.将Consul部署到通过环境传输HTTP端口的环境(例如CloudFoundry等PaaS)时,此选项非常有用,允许您通过Procfile直接设置端口。 +- `-join` - 启动时加入的另一个代理的地址。可以多次指定,以指定要加入的多个代理。如果Consul无法加入任何指定的地址,则代理启动将失败。默认情况下,代理在启动时不会加入任何节点。请注意,`retry_join`在自动执行Consul群集部署时,使用 可能更适合帮助缓解节点启动竞争条件。 + +- `-retry-join`- 类似`-join`但允许在第一次尝试失败时重试连接。这对于您知道地址最终可用的情况很有用。该列表可以包含IPv4,IPv6或DNS地址。如果Consul在非默认的Serf LAN端口上运行,则必须同时指定。IPv6必须使用“括号”语法。如果给出了多个值,则会按列出的顺序尝试和重试它们,直到第一个成功为止。这里有些例子: + + ``` + # Using a DNS entry + $ consul agent -retry-join "consul.domain.internal" + ``` + + ``` + # Using IPv4 + $ consul agent -retry-join "10.0.4.67" + ``` + + ``` + # Using IPv6 + $ consul agent -retry-join "[::1]:8301" + ``` + +#### 配置文件 + +示例配置文件 + +``` +{ + "datacenter": "east-aws", + "data_dir": "/opt/consul", + "log_level": "INFO", + "node_name": "foobar", + "server": true, + "watches": [ + { + "type": "checks", + "handler": "/usr/bin/health-check-handler.sh" + } + ], + "telemetry": { + "statsite_address": "127.0.0.1:2180" + } +} +``` + +示例配置文件,带有TLS + +``` +{ + "datacenter": "east-aws", + "data_dir": "/opt/consul", + "log_level": "INFO", + "node_name": "foobar", + "server": true, + "addresses": { + "https": "0.0.0.0" + }, + "ports": { + "https": 8080 + }, + "key_file": "/etc/pki/tls/private/my.key", + "cert_file": "/etc/pki/tls/certs/my.crt", + "ca_file": "/etc/pki/tls/certs/ca-bundle.crt" +} +``` + +特别参见`ports`设置的使用: + +``` +"ports": { + "https": 8080 +} +``` + +除非为`https`端口分配了端口号,否则Consul不会为HTTP API启用TLS `> 0`。 + +#### 服务定义 + +服务发现的主要目标之一是提供可用服务的目录。为此,代理提供了一种简单的服务定义格式来声明服务的可用性,并可能将其与运行状况检查相关联。如果运行状况检查与服务关联,则将其视为应用程序级别。服务在配置文件中定义,或在运行时通过HTTP接口添加。 + +要配置服务,请将服务定义作为`-config-file`代理提供给代理,或将其置于`-config-dir`代理中。该文件必须以Consul加载的`.json`或`.hcl`扩展名结尾。可以通过向`SIGHUP`代理发送更新检查定义。或者,可以使用HTTP API动态注册服务。 + +服务定义是一种如下所示的配置。此示例显示了所有可能的字段,但请注意,只需要几个字段。 + +``` +{ + "service": { + "name": "redis", + "tags": ["primary"], + "address": "", + "meta": { + "meta": "for my service" + }, + "port": 8000, + "enable_tag_override": false, + "checks": [ + { + "args": ["/usr/local/bin/check_redis.py"], + "interval": "10s" + } + ], + "kind": "connect-proxy", + "proxy_destination": "redis", + "connect": { + "native": false, + "proxy": { + "command": [], + "config": {} + } + } + } +} +``` + +服务定义必须包括`name`和可任选地提供 `id`,`tags`,`address`,`port`,`check`,`meta`和`enable_tag_override`。如果没有提供,`id`则设置为`name`。要求所有服务每个节点都有唯一的ID,因此如果名称可能存在冲突,则应提供唯一ID。 + + + +可以使用`services`配置文件中的复数键一次提供多个服务定义 。 + +``` +{ + "services": [ + { + "id": "red0", + "name": "redis", + "tags": [ + "primary" + ], + "address": "", + "port": 6000, + "checks": [ + { + "args": ["/bin/check_redis", "-p", "6000"], + "interval": "5s", + "ttl": "20s" + } + ] + }, + { + "id": "red1", + "name": "redis", + "tags": [ + "delayed", + "secondary" + ], + "address": "", + "port": 7000, + "checks": [ + { + "args": ["/bin/check_redis", "-p", "7000"], + "interval": "30s", + "ttl": "60s" + } + ] + }, + ... + ] +} +``` + + + +#### 检查定义 + +代理的主要角色之一是管理系统级和应用程序级运行状况检查。如果运行状况检查与服务关联,则将其视为应用程序级别。如果未与服务关联,则检查将监视整个节点的运行状况。 + +检查在配置文件中定义,或在运行时通过HTTP接口添加。通过HTTP接口创建的检查将与该节点保持一致。 + +脚本检查: + +``` +{ + "check": { + "id": "mem-util", + "name": "Memory utilization", + "args": ["/usr/local/bin/check_mem.py", "-limit", "256MB"], + "interval": "10s", + "timeout": "1s" + } +} +``` + +HTTP检查: + +``` +{ + "check": { + "id": "api", + "name": "HTTP API on port 5000", + "http": "https://localhost:5000/health", + "tls_skip_verify": false, + "method": "POST", + "header": {"x-foo":["bar", "baz"]}, + "interval": "10s", + "timeout": "1s" + } +} +``` + +本地服务的别名检查: + +``` +{ + "check": { + "id": "web-alias", + "alias_service": "web" + } +} +``` + + + +### 集群搭建 + +服务端 + +``` +consul agent -server -bootstrap-expect=1 \ + -data-dir=/tmp/consul -node=agent-one -bind=172.20.20.10 \ + -enable-script-checks=true -config-dir=/etc/consul.d +``` + +客户端 + +``` +consul agent -data-dir=/tmp/consul -node=agent-two \ + -bind=172.20.20.11 -enable-script-checks=true -config-dir=/etc/consul.d +``` + +服务端调用 `join`将节点加入集群 + +``` +consul join 172.20.20.11 +``` \ No newline at end of file diff --git "a/linux/centos\346\220\255\345\273\272redis\351\233\206\347\276\244.md" "b/linux/centos\346\220\255\345\273\272redis\351\233\206\347\276\244.md" new file mode 100644 index 0000000..19dda93 --- /dev/null +++ "b/linux/centos\346\220\255\345\273\272redis\351\233\206\347\276\244.md" @@ -0,0 +1,512 @@ +### 软件 + +``` +centos-release-7-5.1804.1.el7.centos.x86_64 +Redis server v=4.0.11 +ruby 2.5.0p0 (2017-12-25 revision 61468) [x86_64-linux] +gem 2.7.3 +``` + +### 系统准备 + +系统必要更新 + +```shell +# yum -y update +# yum -y upgrade +``` + +安装 gcc 、gcc-c++、openssl-devel + +```shell +# yum install -y gcc gcc-c++ openssl-devel +``` + +关闭防火墙 + +```shell +# systemctl stop firewalld.service +``` + +### 安装 Ruby 及 RubyGems + +查看当前安装的 Ruby 及 Gems + +```shell +# ruby -v +# gem -v +``` + +如果出现版本号,则说明当前环境已安装过,可以跳过安装 + +若版本太低,删除原有 Ruby + +```shell +# yum remove ruby ruby-devel +``` + +下载需要安装的 Ruby 版本:http://cache.ruby-lang.org/pub/ruby/ + +下载完成后利用 FTP 传入 Centos 机器,也可以使用如下命令下载 + +```shell +# wget http://cache.ruby-lang.org/pub/ruby/ruby-2.5.0.tar.gz +``` + +开始安装 + +```shell +# tar zxvf ruby-2.5.0.tar.gz +# cd ruby-2.5.0 +# ./configure +# make && make install +``` + +#### 配置 zlib + +```shell +# cd ruby-2.5.0/ext/zlib +# ruby ./extconf.rb +# vim Makefile +``` + +更改 Makefile 中的配置 + +```shell + zlib.o: $(top_srcdir)/include/ruby.h + 改成 + zlib.o: ../../include/ruby.h + // 退出 vim 并保存 + # make && make install +``` + + + +#### 配置 openssl + +```shell +# cd ruby-2.5.0/ext/openssl +# ruby ./extconf.rb +# vim Makefile +``` + +更改 Makefile 中的配置 + +```shell +将所有的 $(top_srcdir) +替换为: ../.. +vim 快捷命令 :1,$s/$(top_srcdir)/..\/.. + // 退出 vim 并保存 +# make && make install +``` + +安装 zlib-devel (当时忘记了这一步是否执行完全了) +`yum install zlib-devel && rvm reinstall 2.5.1` + +这里提示-bash: rvm: command not found, +然后执行 `curl -L get.rvm.io | bash -s stable`,报错为gpg: Can't check signature: No public key, +然后执行 `gpg2 --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3`,再执行`curl -L get.rvm.io | bash -s stable`,到这里rvm安装成功! + +```shell +# gpg2 --recv-keys 409B6B1796C275462A1703113804BB82D39DC0E3 +# yum install zlib-devel && rvm reinstall 2.5.1 +``` + +更新 RubyGems 和 Bundler (可选) + +```shell +# gem update --system +# gem install bundler +``` + +更新老版本安装的 RubyGems + +```shell +# gem update +``` + +安装 redis 插件 + +```shell +# gem install redis +``` + +### 安装 Redis + +#### 安装 Redis + +下载,解压,编译安装 + +```shell +cd /opt +# wget http://download.redis.io/releases/redis-4.0.1.tar.gz +# tar xzf redis-4.0.1.tar.gz +# cd redis-4.0.1 +# make +``` + +如果因为上次编译失败,有残留的文件 + +```shell +# make distclean +``` + +#### 创建节点 + +1.首先在 192.168.252.101机器上 /opt/redis-4.0.1目录下创建 `redis-cluster` 目录 + +```shell +# mkdir /opt/redis-4.0.1/redis-cluster +``` + +2.在 `redis-cluster` 目录下,创建名为`7000、7001、7002`的目录 + +```shell +# cd /opt/redis-4.0.1/redis-cluster +# mkdir 7000 7001 7002 +``` + +3.分别修改这三个配置文件,把如下`redis.conf 配置`内容粘贴进去 + +```shell +# vi 7000/redis.conf +# vi 7001/redis.conf +# vi 7002/redis.conf +``` + +redis.conf 配置 + +```properties +port 7000 +bind 192.168.252.101 +daemonize yes +pidfile /var/run/redis_7000.pid +cluster-enabled yes +cluster-config-file nodes_7000.conf +cluster-node-timeout 10100 +appendonly yes +``` + +redis.conf 配置说明 + +```properties +#端口7000,7001,7002 +port 7000 + +#默认ip为127.0.0.1,需要改为其他节点机器可访问的ip,否则创建集群时无法访问对应的端口,无法创建集群 +bind 192.168.252.101 + +#redis后台运行 +daemonize yes + +#pidfile文件对应7000,7001,7002 +pidfile /var/run/redis_7000.pid + +#开启集群,把注释#去掉 +cluster-enabled yes + +#集群的配置,配置文件首次启动自动生成 7000,7001,7002 +cluster-config-file nodes_7000.conf + +#请求超时,默认15秒,可自行设置 +cluster-node-timeout 10100 + +#aof日志开启,有需要就开启,它会每次写操作都记录一条日志 +appendonly yes +``` + +接着在另外两台机器上(192.168.252.102,192.168.252.103)重复以上三步,只是把目录改为7003、7004、7005、7006、7007、7008对应的配置文件也按照这个规则修改即可 + +启动集群 + +```shell +#第一台机器上执行 3个节点 +# for((i=0;i<=2;i++)); do /opt/redis-4.0.1/src/redis-server /opt/redis-4.0.1/redis-cluster/700$i/redis.conf; done + +#第二台机器上执行 3个节点 +# for((i=3;i<=5;i++)); do /opt/redis-4.0.1/src/redis-server /opt/redis-4.0.1/redis-cluster/700$i/redis.conf; done + +#第三台机器上执行 3个节点 +# for((i=6;i<=8;i++)); do /opt/redis-4.0.1/src/redis-server /opt/redis-4.0.1/redis-cluster/700$i/redis.conf; done +``` + +#### 检查服务 + +检查各 Redis 各个节点启动情况 + +```shell +# ps -ef | grep redis //redis是否启动成功 +# netstat -tnlp | grep redis //监听redis端口 +``` + +#### 创建集群 + +**注意:在任意一台上运行** 不要在每台机器上都运行,一台就够了 + +Redis 官方提供了 `redis-trib.rb` 这个工具,就在解压目录的 src 目录中 + +```shell +# /opt/redis-4.0.1/src/redis-trib.rb create --replicas 1 192.168.252.101:7000192.168.252.101:7001 192.168.252.101:7002 192.168.252.102:7003 192.168.252.102:7004192.168.252.102:7005 192.168.252.103:7006 192.168.252.103:7007 192.168.252.103:7008 +``` + +出现以下内容 + +``` +[root@localhost redis-cluster]# /opt/redis-4.0.1/src/redis-trib.rb create --replicas 1 192.168.252.101:7000 192.168.252.101:7001 192.168.252.101:7002 192.168.252.102:7003 192.168.252.102:7004 192.168.252.102:7005 192.168.252.103:7006 192.168.252.103:7007 192.168.252.103:7008 +>>> Creating cluster +>>> Performing hash slots allocation on 9 nodes... +Using 4 masters: +192.168.252.101:7000 +192.168.252.102:7003 +192.168.252.103:7006 +192.168.252.101:7001 +Adding replica 192.168.252.102:7004 to 192.168.252.101:7000 +Adding replica 192.168.252.103:7007 to 192.168.252.102:7003 +Adding replica 192.168.252.101:7002 to 192.168.252.103:7006 +Adding replica 192.168.252.102:7005 to 192.168.252.101:7001 +Adding replica 192.168.252.103:7008 to 192.168.252.101:7000 +M: 7c622ac191edd40dd61d9b79b27f6f69d02a5bbf 192.168.252.101:7000 + slots:0-4095 (4096 slots) master +M: 44c81c15b01d992cb9ede4ad35477ec853d70723 192.168.252.101:7001 + slots:12288-16383 (4096 slots) master +S: 38f03c27af39723e1828eb62d1775c4b6e2c3638 192.168.252.101:7002 + replicates f1abb62a8c9b448ea14db421bdfe3f1d8075189c +M: 987965baf505a9aa43e50e46c76189c51a8f17ec 192.168.252.102:7003 + slots:4096-8191 (4096 slots) master +S: 6555292fed9c5d52fcf5b983c441aff6f96923d5 192.168.252.102:7004 + replicates 7c622ac191edd40dd61d9b79b27f6f69d02a5bbf +S: 2b5ba254a0405d4efde4c459867b15176f79244a 192.168.252.102:7005 + replicates 44c81c15b01d992cb9ede4ad35477ec853d70723 +M: f1abb62a8c9b448ea14db421bdfe3f1d8075189c 192.168.252.103:7006 + slots:8192-12287 (4096 slots) master +S: eb4067373d36d8a8df07951f92794e67a6aac022 192.168.252.103:7007 + replicates 987965baf505a9aa43e50e46c76189c51a8f17ec +S: 2919e041dd3d1daf176d6800dcd262f4e727f366 192.168.252.103:7008 + replicates 7c622ac191edd40dd61d9b79b27f6f69d02a5bbf +Can I set the above configuration? (type 'yes' to accept): yes +``` + +**输入 yes** + +``` +>>> Nodes configuration updated +>>> Assign a different config epoch to each node +>>> Sending CLUSTER MEET messages to join the cluster +Waiting for the cluster to join......... +>>> Performing Cluster Check (using node 192.168.252.101:7000) +M: 7c622ac191edd40dd61d9b79b27f6f69d02a5bbf 192.168.252.101:7000 + slots:0-4095 (4096 slots) master + 2 additional replica(s) +S: 6555292fed9c5d52fcf5b983c441aff6f96923d5 192.168.252.102:7004 + slots: (0 slots) slave + replicates 7c622ac191edd40dd61d9b79b27f6f69d02a5bbf +M: 44c81c15b01d992cb9ede4ad35477ec853d70723 192.168.252.101:7001 + slots:12288-16383 (4096 slots) master + 1 additional replica(s) +S: 2919e041dd3d1daf176d6800dcd262f4e727f366 192.168.252.103:7008 + slots: (0 slots) slave + replicates 7c622ac191edd40dd61d9b79b27f6f69d02a5bbf +M: f1abb62a8c9b448ea14db421bdfe3f1d8075189c 192.168.252.103:7006 + slots:8192-12287 (4096 slots) master + 1 additional replica(s) +S: eb4067373d36d8a8df07951f92794e67a6aac022 192.168.252.103:7007 + slots: (0 slots) slave + replicates 987965baf505a9aa43e50e46c76189c51a8f17ec +S: 38f03c27af39723e1828eb62d1775c4b6e2c3638 192.168.252.101:7002 + slots: (0 slots) slave + replicates f1abb62a8c9b448ea14db421bdfe3f1d8075189c +S: 2b5ba254a0405d4efde4c459867b15176f79244a 192.168.252.102:7005 + slots: (0 slots) slave + replicates 44c81c15b01d992cb9ede4ad35477ec853d70723 +M: 987965baf505a9aa43e50e46c76189c51a8f17ec 192.168.252.102:7003 + slots:4096-8191 (4096 slots) master + 1 additional replica(s) +[OK] All nodes agree about slots configuration. +>>> Check for open slots... +>>> Check slots coverage... +[OK] All 16384 slots covered. +``` + +#### 关闭集群 + +这样也可以,推荐 + +```shell +# pkill redis +``` + +循环节点逐个关闭 + +```shell +# for((i=0;i<=2;i++)); do /opt/redis-4.0.1/src/redis-cli -c -h 192.168.252.101 -p 700$i shutdown; done + +# for((i=3;i<=5;i++)); do /opt/redis-4.0.1/src/redis-cli -c -h 192.168.252.102 -p 700$i shutdown; done + +# for((i=6;i<=8;i++)); do /opt/redis-4.0.1/src/redis-cli -c -h 192.168.252.103 -p 700$i shutdown; done +``` + +#### 集群验证 + +参数 -C 可连接到集群,因为 redis.conf 将 bind 改为了ip地址,所以 -h 参数不可以省略,-p 参数为端口号 + +- **我们在192.168.252.101机器redis 7000 的节点set 一个key** + +``` +$ /opt/redis-4.0.1/src/redis-cli -h 192.168.252.101 -c -p 7000 +192.168.252.101:7000> set name www.ymq.io +-> Redirected to slot [5798] located at 192.168.252.102:7003 +OK +192.168.252.102:7003> get name +"www.ymq.io" +192.168.252.102:7003> +``` + +发现redis set name 之后重定向到192.168.252.102机器 redis 7003 这个节点 + +- **我们在192.168.252.103机器redis 7008 的节点get一个key** + +``` +[root@localhost redis-cluster]# /opt/redis-4.0.1/src/redis-cli -h 192.168.252.103 -c -p 7008 +192.168.252.103:7008> get name +-> Redirected to slot [5798] located at 192.168.252.102:7003 +"www.ymq.io" +192.168.252.102:7003> +``` + +发现redis get name 重定向到192.168.252.102机器 redis 7003 这个节点 + +> 如果您看到这样的现象,说明集群已经是可用的了 + +#### 检查集群状态 + +``` +$ /opt/redis-4.0.1/src/redis-trib.rb check 192.168.252.101:7000 +``` + +``` +>>> Performing Cluster Check (using node 192.168.252.101:7000) +M: 7c622ac191edd40dd61d9b79b27f6f69d02a5bbf 192.168.252.101:7000 + slots:0-4095 (4096 slots) master + 2 additional replica(s) +S: 6555292fed9c5d52fcf5b983c441aff6f96923d5 192.168.252.102:7004 + slots: (0 slots) slave + replicates 7c622ac191edd40dd61d9b79b27f6f69d02a5bbf +M: 44c81c15b01d992cb9ede4ad35477ec853d70723 192.168.252.101:7001 + slots:12288-16383 (4096 slots) master + 1 additional replica(s) +S: 2919e041dd3d1daf176d6800dcd262f4e727f366 192.168.252.103:7008 + slots: (0 slots) slave + replicates 7c622ac191edd40dd61d9b79b27f6f69d02a5bbf +M: f1abb62a8c9b448ea14db421bdfe3f1d8075189c 192.168.252.103:7006 + slots:8192-12287 (4096 slots) master + 1 additional replica(s) +S: eb4067373d36d8a8df07951f92794e67a6aac022 192.168.252.103:7007 + slots: (0 slots) slave + replicates 987965baf505a9aa43e50e46c76189c51a8f17ec +S: 38f03c27af39723e1828eb62d1775c4b6e2c3638 192.168.252.101:7002 + slots: (0 slots) slave + replicates f1abb62a8c9b448ea14db421bdfe3f1d8075189c +S: 2b5ba254a0405d4efde4c459867b15176f79244a 192.168.252.102:7005 + slots: (0 slots) slave + replicates 44c81c15b01d992cb9ede4ad35477ec853d70723 +M: 987965baf505a9aa43e50e46c76189c51a8f17ec 192.168.252.102:7003 + slots:4096-8191 (4096 slots) master + 1 additional replica(s) +[OK] All nodes agree about slots configuration. +>>> Check for open slots... +>>> Check slots coverage... +[OK] All 16384 slots covered. +``` + +#### 列出集群节点 + +列出集群当前已知的所有节点(node),以及这些节点的相关信息 + +```shell +# /opt/redis-4.0.1/src/redis-cli -h 192.168.252.101 -c -p 7000 + +192.168.252.101:7000> cluster nodes +``` + +``` +6555292fed9c5d52fcf5b983c441aff6f96923d5 192.168.252.102:7004@17004 slave 7c622ac191edd40dd61d9b79b27f6f69d02a5bbf 0 1502815268317 5 connected +44c81c15b01d992cb9ede4ad35477ec853d70723 192.168.252.101:7001@17001 master - 0 1502815268000 2 connected 12288-16383 +2919e041dd3d1daf176d6800dcd262f4e727f366 192.168.252.103:7008@17008 slave 7c622ac191edd40dd61d9b79b27f6f69d02a5bbf 0 1502815269000 9 connected +7c622ac191edd40dd61d9b79b27f6f69d02a5bbf 192.168.252.101:7000@17000 myself,master - 0 1502815269000 1 connected 0-4095 +f1abb62a8c9b448ea14db421bdfe3f1d8075189c 192.168.252.103:7006@17006 master - 0 1502815269000 7 connected 8192-12287 +eb4067373d36d8a8df07951f92794e67a6aac022 192.168.252.103:7007@17007 slave 987965baf505a9aa43e50e46c76189c51a8f17ec 0 1502815267000 8 connected +38f03c27af39723e1828eb62d1775c4b6e2c3638 192.168.252.101:7002@17002 slave f1abb62a8c9b448ea14db421bdfe3f1d8075189c 0 1502815269327 7 connected +2b5ba254a0405d4efde4c459867b15176f79244a 192.168.252.102:7005@17005 slave 44c81c15b01d992cb9ede4ad35477ec853d70723 0 1502815270336 6 connected +987965baf505a9aa43e50e46c76189c51a8f17ec 192.168.252.102:7003@17003 master - 0 1502815271345 4 connected 4096-8191 +192.168.252.101:7000> +``` + +打印集群信息 + +``` +# 192.168.252.101:7000> cluster info +``` + +``` +cluster_state:ok +cluster_slots_assigned:16384 +cluster_slots_ok:16384 +cluster_slots_pfail:0 +cluster_slots_fail:0 +cluster_known_nodes:9 +cluster_size:4 +cluster_current_epoch:9 +cluster_my_epoch:1 +cluster_stats_messages_ping_sent:485 +cluster_stats_messages_pong_sent:485 +cluster_stats_messages_sent:970 +cluster_stats_messages_ping_received:477 +cluster_stats_messages_pong_received:485 +cluster_stats_messages_meet_received:8 +cluster_stats_messages_received:970 +192.168.252.101:7000> +``` + +### 集群命令 + +#### 语法格式 + +``` +redis-cli -c -p port +``` + +#### 集群 + +``` +cluster info :打印集群的信息 +cluster nodes :列出集群当前已知的所有节点( node),以及这些节点的相关信息。 +``` + +#### 节点 + +``` +cluster meet :将 ip 和 port 所指定的节点添加到集群当中,让它成为集群的一份子。 +cluster forget :从集群中移除 node_id 指定的节点。 +cluster replicate :将当前节点设置为 node_id 指定的节点的从节点。 +cluster saveconfig :将节点的配置文件保存到硬盘里面。 +``` + +#### 槽(slot) + +``` +cluster addslots [slot ...] :将一个或多个槽( slot)指派( assign)给当前节点。 +cluster delslots [slot ...] :移除一个或多个槽对当前节点的指派。 +cluster flushslots :移除指派给当前节点的所有槽,让当前节点变成一个没有指派任何槽的节点。 +cluster setslot node :将槽 slot 指派给 node_id 指定的节点,如果槽已经指派给另一个节点,那么先让另一个节点删除该槽>,然后再进行指派。 +cluster setslot migrating :将本节点的槽 slot 迁移到 node_id 指定的节点中。 +cluster setslot importing :从 node_id 指定的节点中导入槽 slot 到本节点。 +cluster setslot stable :取消对槽 slot 的导入( import)或者迁移( migrate)。 +``` + +#### 键 + +``` +cluster keyslot :计算键 key 应该被放置在哪个槽上。 +cluster countkeysinslot :返回槽 slot 目前包含的键值对数量。 +cluster getkeysinslot :返回 count 个 slot 槽中的键 。 +``` \ No newline at end of file diff --git "a/linux/centos\346\220\255\345\273\272shadowsocks.md" "b/linux/centos\346\220\255\345\273\272shadowsocks.md" new file mode 100644 index 0000000..33917fc --- /dev/null +++ "b/linux/centos\346\220\255\345\273\272shadowsocks.md" @@ -0,0 +1,152 @@ +#### 安装shadowsocks + +1、下载安装脚本 + +`wget --no-check-certificate -O shadowsocks-all.sh https://raw.githubusercontent.com/teddysun/shadowsocks_install/master/shadowsocks-all.sh` + +下载按成后会在当前下载目录中出现`shadowsocks-all.sh`文件,如果下载不到可以使用`./software/shadowsocks-all.sh` + +![下载脚本](./images/ss_1_01.jpg) + +2、为脚本赋予执行权限 + +`chmod +x shadowsocks-all.sh` + +![添加执行权限](./images/ss_1_02.jpg) + +3、执行脚本 + +`./shadowsocks-all.sh 2>&1 | tee shadowsocks-all.log` + +![执行脚本](./images/ss_1_03.jpg) + +4、出现以上画面后输入1,或者直接回车,下面会提示你输入你的SS SERVER的密码,和端口。不输入就是默认。跑完命令后会出来你的SS客户端的信息。 + +![设置ss密码](./images/ss_1_04.jpg) + +![设置端口](./images/ss_1_05.jpg) + +5、输入完成ss的密码和端口号之后,进行回车: + +![选择协议](./images/ss_1_06.jpg) + +6、特别注意,由于iPhone端的的wingy目前只支持到**cfb**,所以我们选择**aes-256-cfb**,即**7** ,回车: + +![选择协议](./images/ss_1_07.jpg) + +7、这一步按回车继续,然后需要几分钟的安装过程,请耐心等待出现下面的画面: + +![选择协议](./images/ss_1_08.jpg) + +8、当我们看到*Congratulations, Shadowsocks-Python server install completed!*时,则证明我们已经成功安装了ss。请立即将这些信息复制下来加以保存,我们就会用到这几个比较重要的信息:主机服务器IP地址、端口号、密码和加密方式。上面的命令全部回车执行后,如果没有报错,即为执行成功,出现确认提示的时候,输入 y 后,回车即可。 + +#### Shadowsocks设备终端下载地址 + +[Windows版下载地址](https://github.com/shadowsocks/shadowsocks-windows/releases) + +[Android版下载地址]() + +[Mac版下载地址]() + +**iphone**因存在限制,国内用户无法在App Store搜到,可以使用如下代替 + +[Kite Ass Proxy]() + +[FirstWingy]() + +[SuperWingy]() + +#### 优化Shadowsocks性能 + +1、增加系统文件描述符,编辑`/etc/security/limits.conf`,新增如下两行,*不要忘记。 + +``` +* soft nofile 51200 +* hard nofile 51200 +``` + +2、配置`/etc/sysctl.d/local.conf` + +```properties +# max open files +fs.file-max = 1024000 +# max read buffer +net.core.rmem_max = 67108864 +# max write buffer +net.core.wmem_max = 67108864 +# default read buffer +net.core.rmem_default = 65536 +# default write buffer +net.core.wmem_default = 65536 +# max processor input queue +net.core.netdev_max_backlog = 4096 +# max backlog +net.core.somaxconn = 4096 + +# resist SYN flood attacks +net.ipv4.tcp_syncookies = 1 +# reuse timewait sockets when safe +net.ipv4.tcp_tw_reuse = 1 +# turn off fast timewait sockets recycling +net.ipv4.tcp_tw_recycle = 0 +# short FIN timeout +net.ipv4.tcp_fin_timeout = 30 +# short keepalive time +net.ipv4.tcp_keepalive_time = 1200 +# outbound port range +net.ipv4.ip_local_port_range = 10000 65000 +# max SYN backlog +net.ipv4.tcp_max_syn_backlog = 4096 +# max timewait sockets held by system simultaneously +net.ipv4.tcp_max_tw_buckets = 5000 +# TCP receive buffer +net.ipv4.tcp_rmem = 4096 87380 67108864 +# TCP write buffer +net.ipv4.tcp_wmem = 4096 65536 67108864 +# turn on path MTU discovery +net.ipv4.tcp_mtu_probing = 1 + +# for high-latency network +net.ipv4.tcp_congestion_control = hybla +# forward ivp4 +net.ipv4.ip_forward = 1 +``` + +2、使配置生效 + +```sh +sysctl --system +``` + +#### 配置多用户 + +修改`/etc/shadowsocks.json` + +```json +{ + "server":"本机外网IP", + "local_address": "127.0.0.1", + "local_port":1080, + "port_password": { + "端口1": "密码1", + "端口2": "密码2" + }, + "timeout":300, + "method":"aes-256-cfb", + "fast_open": false +} + +``` + +#### 启动、停止 + +1. 建议使用后端启动 + - 前端启动:`ssserver -c /etc/shadowsocks.json`; + - 后端启动:`ssserver -c /etc/shadowsocks.json -d start`; + - 停止:`ssserver -c /etc/shadowsocks.json -d stop`; + - 重启(修改配置要重启才生效):`ssserver -c /etc/shadowsocks.json -d restart` +2. 设置开机启动 + 1. 在终端输入`vi /etc/rc.local`, + 2. 把里面最后的带有ssserver的一大段默认的代码删除掉, + 3. 再把`ssserver -c /etc/shadowsocks.json -d start`加进去, + 4. 按`wq`保存退出。 \ No newline at end of file diff --git "a/linux/centos\350\256\276\347\275\256\347\275\221\347\273\234\344\273\243\347\220\206.md" "b/linux/centos\350\256\276\347\275\256\347\275\221\347\273\234\344\273\243\347\220\206.md" new file mode 100644 index 0000000..3d43cb8 --- /dev/null +++ "b/linux/centos\350\256\276\347\275\256\347\275\221\347\273\234\344\273\243\347\220\206.md" @@ -0,0 +1,24 @@ +## CentOS配置网络代理 + +### 涉及的环境变量 + +| 环境变量 | 描述 | 值示例 | +| ----------- | ------------------------------------------------------------ | ------------------------------------------------------------ | +| http_proxy | 为http变量设置代理;默认不填开头以http协议传输 | 10.0.0.51:8080 user:pass@10.0.0.10:8080 socks4://10.0.0.51:1080 socks5://192.168.1.1:1080 | +| https_proxy | 为https变量设置代理; | 同上 | +| ftp_proxy | 为ftp变量设置代理; | 同上 | +| all_proxy | 全部变量设置代理,设置了这个时候上面的不用设置 | 同上 | +| no_proxy | 无需代理的主机或域名; 可以使用通配符; 多个时使用“,”号分隔; | \*.aiezu.com,10.\*.\*.\*,192.168.\*.\*, *.local,localhost,127.0.0.1 | + +### 修改文件 + +`/etc/profile` + +```shell +export proxy="http://ip:1080" # 这里修改为指定协议的格式 +export http_proxy=$proxy +export https_proxy=$proxy +export ftp_proxy=$proxy +export no_proxy="localhost, 127.0.0.1, ::1" +``` + diff --git "a/linux/goflyway\350\256\277\351\227\256\345\244\226\347\275\221.md" "b/linux/goflyway\350\256\277\351\227\256\345\244\226\347\275\221.md" new file mode 100644 index 0000000..83eacdb --- /dev/null +++ "b/linux/goflyway\350\256\277\351\227\256\345\244\226\347\275\221.md" @@ -0,0 +1,164 @@ +# GoFlyWay + CDN + +通过使用 GoFlyWay + CDN 的方式实现外网访问。 + +**优点** + +* 隐藏个人VPN真实IP +* 被墙的IP也可以通过这种方式救活 + +## 前提准备 + +* VPS 一台 +* centos7系统 +* 个人域名一个 + +## namesilo + +如果已有个人域名,可以跳过这一步。 + +### 注册账号 + +使用浏览器访问[namesilo官网](),点击**Create New Account**按钮。 + +![创建账户](./images/goflyway_1/001.png) + +完成**创建账户的上半部分**,部分英文解释如图: + +![创建账户](./images/goflyway_1/002.png) + +完成**创建账户的下半部分**,部分英文解释如图: + +![创建账户](./images/goflyway_1/003.png) + +页面拉倒最下方,输入验证码,同意协议后,点击**CREATE MY NEW ACCOUNT**进行注册。 + +### 购买域名 + +注册完成后通过首页第一步中**Create New Account**左边的**log in**登录。 + +登录成功后按下图的**顺序点击** + +![创建账户](./images/goflyway_1/004.png) + +点击**register**【1】,在输入框中输入自己打算购买的域名【2】,点击**SEARCH**按钮【3】会弹出输入框下面的结果【4】,选择域名后缀【5】,点击**REGISTER CHECKED DOMAINS**。选择域名的过程完成。 + +弹出购物车页面,确认个人购买信息后点击**CONTINUE**按钮。 + +![创建账户](./images/goflyway_1/005.png) + +确认完成后会跳转付款页面,国内就选择支付宝,同时填写支付宝注册时的邮箱,如果没有,则填一个正在使用的邮箱,点击**GO**按钮后会进入支付宝的付款页面。扫码支付就可以了。 + +![创建账户](./images/goflyway_1/006.png) + +**支付成功后,等待完成可能需要几个小时的时间,如果超过两个小时未收到订单成功的邮件,请联系在线客服** + + + +## cloudflare + +### 注册账号 + +访问[cloudflare登录页面](https://dash.cloudflare.com/login),点击**Sign up**按钮进行注册。根据网站流程进行注册账号。 + +注册成功后,cloudflare会要求输入网站地址,如下,输入刚才购买的域名。 + +![创建账户](./images/goflyway_1/007.png) + +输入完成后点击**add site**按钮,然后选择套餐,选择免费版就可以。确认方案后,Cloudflare会扫描该域名当前的DNS记录,你可以保留、添加或者删除: + +![创建账户](./images/goflyway_1/008.png) + +我们在上图的红色框中输入name和ip地址,点击**Add Record**。之后点击**Continue**就是修改**NS**了。 + +### 配置NameServer + +配置**NS**需要回到**namesilo**官网。选择**Manager My Domain**【1】,并在购买的域名前打勾【2】,点击**change name servers**【3】 + +![创建账户](./images/goflyway_1/009.png) + +namesilo会有三个默认的nameservers,将其删除,替换成cloudflare提供了地址,点击**COMMIT**按钮即可。首次解析域名可能需要一点时间,可以等待邮件通知。 + +![创建账户](./images/goflyway_1/010.png) + + + +## 安装GoFlyWay服务端 + +通过命令行工具连接主机后,执行如下命令,或者使用[离线脚本](./software/goflyway.sh) + +```shell +wget -N --no-check-certificate https://raw.githubusercontent.com/ToyoDAdoubi/doubi/master/goflyway.sh && chmod +x goflyway.sh && bash goflyway.sh +``` + +之后输入1即可进行安装,其中**端口选择80**,HTTP伪装填写 + +http://kernel.ubuntu.com/~kernel-ppa/mainline/ + +**密码必须设置为: ?c域名:端口号** + +例如:购买的域名为 testd.top,在cloudflare中解析的名字为 fly, 那么我们的密码应该为 :`?cfly.testd.top:80`。这样设置密码是为了安卓客户端使用。 + +![创建账户](./images/goflyway_1/011.png) + +## 安装客户端 + +### windows + +访问[下载GoFlyWay Tools](http://yun.iiwl.cc/s/ivmmqekw) + +**功能简介**: +支持 多服务器管理 +支持 直连模式、PAC模式、全局模式 +支持 生成/导入 分享链接功能(分享链接格式) +支持 HTTP、KCP、CDN、WebSocket 传输协议 +支持 账号自检 +支持 开机启动 +等等 +和酸酸乳shadowsocks差不多一样的。 +其他不介绍,放图和教程就行。 + +![创建账户](./images/goflyway_1/012.png) + +打开之后就差不多是这样,我这个是导入了配置界面。 +其中协议一般是http,因为此节点被墙,使用了伪装,所以选择cdn。 + +**配置** + +1.最简单的方法就是导入分享链接,上面仔细看就行 +2.点击浏览,选择相应位数的文件 + +![创建账户](./images/goflyway_1/013.png) + +### android + +访问[GoFlyWay release下载apk](https://github.com/coyove/goflyway/releases) + +由于该apk是使用shadowsocks进行修改的,所以手机上如果安装可shadowsocks,需要先将其卸载后再安装。 + +打开 Goflyway 安卓客户端后,可以看到一个默认的账号,因为 Goflyway 安卓客户端是基于 Shadowsocks 安卓客户端修改而来的,所以此处的默认账户实际上是 Shadowsocks 安卓客户端自带的 SS 账号,删除该账号。 + +点击右上角的+号,选择手动配置 + +![创建账户](./images/goflyway_1/014.png) + +然后依次填写 服务器、远程端口、密码 这三项即可。 + +![创建账户](./images/goflyway_1/015.png) + +在该页的配置中,可以启用**分应用VPN**指定使用VPN的应用程序。 + + + +## 参考 + +[域名解析教程:Cloudflare解析与DNSPod解析](https://blog.csdn.net/wf632856695/article/details/86689607) + +[IP被墙怎么办?利用Goflyway+CDN救活你的被墙IP!](https://www.xiaobaidaxue.com/operation/288.html) + +[GoFlyway全解+让被墙IP继续“上班”](https://www.lsland.cn/Technical/453.html) + +[GoFlyway 基础教程Android 客户端使用方法](https://doubibackup.com/wlqx27bd.html) + +[GoFlyWay github 托管地址](https://github.com/coyove/goflyway) + diff --git a/linux/images/goflyway_1/001.png b/linux/images/goflyway_1/001.png new file mode 100644 index 0000000..87367ed Binary files /dev/null and b/linux/images/goflyway_1/001.png differ diff --git a/linux/images/goflyway_1/002.png b/linux/images/goflyway_1/002.png new file mode 100644 index 0000000..6823d1a Binary files /dev/null and b/linux/images/goflyway_1/002.png differ diff --git a/linux/images/goflyway_1/003.png b/linux/images/goflyway_1/003.png new file mode 100644 index 0000000..ebc1ac8 Binary files /dev/null and b/linux/images/goflyway_1/003.png differ diff --git a/linux/images/goflyway_1/004.png b/linux/images/goflyway_1/004.png new file mode 100644 index 0000000..bc98529 Binary files /dev/null and b/linux/images/goflyway_1/004.png differ diff --git a/linux/images/goflyway_1/005.png b/linux/images/goflyway_1/005.png new file mode 100644 index 0000000..36ac0cf Binary files /dev/null and b/linux/images/goflyway_1/005.png differ diff --git a/linux/images/goflyway_1/006.png b/linux/images/goflyway_1/006.png new file mode 100644 index 0000000..db208d0 Binary files /dev/null and b/linux/images/goflyway_1/006.png differ diff --git a/linux/images/goflyway_1/007.png b/linux/images/goflyway_1/007.png new file mode 100644 index 0000000..3b6d219 Binary files /dev/null and b/linux/images/goflyway_1/007.png differ diff --git a/linux/images/goflyway_1/008.png b/linux/images/goflyway_1/008.png new file mode 100644 index 0000000..2e3b1e8 Binary files /dev/null and b/linux/images/goflyway_1/008.png differ diff --git a/linux/images/goflyway_1/009.png b/linux/images/goflyway_1/009.png new file mode 100644 index 0000000..01c511d Binary files /dev/null and b/linux/images/goflyway_1/009.png differ diff --git a/linux/images/goflyway_1/010.png b/linux/images/goflyway_1/010.png new file mode 100644 index 0000000..3e91a46 Binary files /dev/null and b/linux/images/goflyway_1/010.png differ diff --git a/linux/images/goflyway_1/011.png b/linux/images/goflyway_1/011.png new file mode 100644 index 0000000..45e5a96 Binary files /dev/null and b/linux/images/goflyway_1/011.png differ diff --git a/linux/images/goflyway_1/012.png b/linux/images/goflyway_1/012.png new file mode 100644 index 0000000..aac52d4 Binary files /dev/null and b/linux/images/goflyway_1/012.png differ diff --git a/linux/images/goflyway_1/013.png b/linux/images/goflyway_1/013.png new file mode 100644 index 0000000..77dcda6 Binary files /dev/null and b/linux/images/goflyway_1/013.png differ diff --git a/linux/images/goflyway_1/014.png b/linux/images/goflyway_1/014.png new file mode 100644 index 0000000..7eced08 Binary files /dev/null and b/linux/images/goflyway_1/014.png differ diff --git a/linux/images/goflyway_1/015.png b/linux/images/goflyway_1/015.png new file mode 100644 index 0000000..e51f86f Binary files /dev/null and b/linux/images/goflyway_1/015.png differ diff --git a/linux/images/ss_1_01.jpg b/linux/images/ss_1_01.jpg new file mode 100644 index 0000000..4ce9cfe Binary files /dev/null and b/linux/images/ss_1_01.jpg differ diff --git a/linux/images/ss_1_02.jpg b/linux/images/ss_1_02.jpg new file mode 100644 index 0000000..9720136 Binary files /dev/null and b/linux/images/ss_1_02.jpg differ diff --git a/linux/images/ss_1_03.jpg b/linux/images/ss_1_03.jpg new file mode 100644 index 0000000..c542096 Binary files /dev/null and b/linux/images/ss_1_03.jpg differ diff --git a/linux/images/ss_1_04.jpg b/linux/images/ss_1_04.jpg new file mode 100644 index 0000000..c1f0c56 Binary files /dev/null and b/linux/images/ss_1_04.jpg differ diff --git a/linux/images/ss_1_05.jpg b/linux/images/ss_1_05.jpg new file mode 100644 index 0000000..6f93188 Binary files /dev/null and b/linux/images/ss_1_05.jpg differ diff --git a/linux/images/ss_1_06.jpg b/linux/images/ss_1_06.jpg new file mode 100644 index 0000000..23cd3ab Binary files /dev/null and b/linux/images/ss_1_06.jpg differ diff --git a/linux/images/ss_1_07.jpg b/linux/images/ss_1_07.jpg new file mode 100644 index 0000000..3b99407 Binary files /dev/null and b/linux/images/ss_1_07.jpg differ diff --git a/linux/images/ss_1_08.jpg b/linux/images/ss_1_08.jpg new file mode 100644 index 0000000..815b8b6 Binary files /dev/null and b/linux/images/ss_1_08.jpg differ diff --git "a/linux/nginx\350\275\254\345\217\221\350\247\204\345\210\231.md" "b/linux/nginx\350\275\254\345\217\221\350\247\204\345\210\231.md" new file mode 100644 index 0000000..bc30724 --- /dev/null +++ "b/linux/nginx\350\275\254\345\217\221\350\247\204\345\210\231.md" @@ -0,0 +1,36 @@ +### nginx 正则规则 + +以=开头表示精确匹配 +如 A 中只匹配根目录结尾的请求,后面不能带任何字符串。 +^~ 开头表示uri以某个常规字符串开头,不是正则匹配 +~ 开头表示区分大小写的正则匹配; +~* 开头表示不区分大小写的正则匹配 +/ 通用匹配, 如果没有其它匹配,任何请求都会匹配到 + +### 匹配顺序 + (location =) > (location 完整路径) > (location ^~) > (location ~) > (location ~*) > (location 部分路径) > (location /) + +### 案例 + +``` + # 配置反向代理 + location / { + rewrite '^/download/([\s\S]*)' /assetes/$1; + proxy_pass http://127.0.0.1:8080; + root html; + index index.html index.htm; + } + + # 将所有以 /assetes开头的资源转发到 /opt/res/assetes/目录下 + location ^~ /assetes/ { + root /opt/res/; + autoindex on; + } + + # 静态资源(网页静态资源使用) + # 忽略大小写,并且匹配后面的正则 + location ~* \.(htm|html|gif|jpg|jpeg|png|css|js|icon)$ { + root /opt/res/web/; + autoindex off; + } +``` \ No newline at end of file diff --git "a/linux/screen\345\221\275\344\273\244.md" "b/linux/screen\345\221\275\344\273\244.md" new file mode 100644 index 0000000..7f2963a --- /dev/null +++ "b/linux/screen\345\221\275\344\273\244.md" @@ -0,0 +1,37 @@ +## screen命令 + +### 创建会话 + +```shell +screen -S 会话名称 +或者 +screen -R 会话名称 +``` + +### 查看会话列表 + +```shell +screen -ls +``` + +### 恢复会话 + +```shell +screen -r 会话名称 +或 +screen -R 会话名称 +``` + +**恢复失败时:** + +```shell +screen -d 会话名称 +screen -r 会话名称 +``` + +### 删除会话 + +```shell +screen -S 会话名称 -X quit +``` + diff --git a/linux/software/goflyway.sh b/linux/software/goflyway.sh new file mode 100644 index 0000000..d320f1f --- /dev/null +++ b/linux/software/goflyway.sh @@ -0,0 +1,688 @@ +#!/usr/bin/env bash +PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin +export PATH + +#================================================= +# System Required: CentOS/Debian/Ubuntu +# Description: GoFlyway +# Version: 1.0.11 +# Author: Toyo +# Blog: https://doub.io/goflyway-jc2/ +#================================================= + +sh_ver="1.0.11" +filepath=$(cd "$(dirname "$0")"; pwd) +file_1=$(echo -e "${filepath}"|awk -F "$0" '{print $1}') +Folder="/usr/local/goflyway" +File="/usr/local/goflyway/goflyway" +CONF="/usr/local/goflyway/goflyway.conf" +Now_ver_File="/usr/local/goflyway/ver.txt" +Log_File="/usr/local/goflyway/goflyway.log" +Crontab_file="/usr/bin/crontab" + +Green_font_prefix="\033[32m" && Red_font_prefix="\033[31m" && Green_background_prefix="\033[42;37m" && Red_background_prefix="\033[41;37m" && Font_color_suffix="\033[0m" +Info="${Green_font_prefix}[信息]${Font_color_suffix}" +Error="${Red_font_prefix}[错误]${Font_color_suffix}" +Tip="${Green_font_prefix}[注意]${Font_color_suffix}" + +check_root(){ + [[ $EUID != 0 ]] && echo -e "${Error} 当前非ROOT账号(或没有ROOT权限),无法继续操作,请更换ROOT账号或使用 ${Green_background_prefix}sudo su${Font_color_suffix} 命令获取临时ROOT权限(执行后可能会提示输入当前账号的密码)。" && exit 1 +} +#检查系统 +check_sys(){ + if [[ -f /etc/redhat-release ]]; then + release="centos" + elif cat /etc/issue | grep -q -E -i "debian"; then + release="debian" + elif cat /etc/issue | grep -q -E -i "ubuntu"; then + release="ubuntu" + elif cat /etc/issue | grep -q -E -i "centos|red hat|redhat"; then + release="centos" + elif cat /proc/version | grep -q -E -i "debian"; then + release="debian" + elif cat /proc/version | grep -q -E -i "ubuntu"; then + release="ubuntu" + elif cat /proc/version | grep -q -E -i "centos|red hat|redhat"; then + release="centos" + fi + bit=`uname -m` +} +check_installed_status(){ + [[ ! -e ${File} ]] && echo -e "${Error} GoFlyway 没有安装,请检查 !" && exit 1 +} +check_crontab_installed_status(){ + if [[ ! -e ${Crontab_file} ]]; then + echo -e "${Error} Crontab 没有安装,开始安装..." + if [[ ${release} == "centos" ]]; then + yum install crond -y + else + apt-get install cron -y + fi + if [[ ! -e ${Crontab_file} ]]; then + echo -e "${Error} Crontab 安装失败,请检查!" && exit 1 + else + echo -e "${Info} Crontab 安装成功!" + fi + fi +} +check_pid(){ + PID=$(ps -ef| grep "goflyway"| grep -v grep| grep -v "goflyway.sh"| grep -v "init.d"| grep -v "service"| awk '{print $2}') +} +check_new_ver(){ + new_ver=$(wget --no-check-certificate -qO- -t1 -T3 https://api.github.com/repos/coyove/goflyway/releases| grep "tag_name"|grep -v "caddy"| head -n 1| awk -F ":" '{print $2}'| sed 's/\"//g;s/,//g;s/ //g') + if [[ -z ${new_ver} ]]; then + echo -e "${Error} GoFlyway 最新版本获取失败,请手动获取最新版本号[ https://github.com/coyove/goflyway/releases ]" + read -e -p "请输入版本号 [ 格式如 1.3.0a ] :" new_ver + [[ -z "${new_ver}" ]] && echo "取消..." && exit 1 + else + echo -e "${Info} 检测到 GoFlyway 最新版本为 [ ${new_ver} ]" + fi +} +check_ver_comparison(){ + now_ver=$(cat ${Now_ver_File}) + [[ -z ${now_ver} ]] && echo "${new_ver}" > ${Now_ver_File} + if [[ ${now_ver} != ${new_ver} ]]; then + echo -e "${Info} 发现 GoFlyway 已有新版本 [ ${new_ver} ],当前版本 [ ${now_ver} ]" + read -e -p "是否更新 ? [Y/n] :" yn + [[ -z "${yn}" ]] && yn="y" + if [[ $yn == [Yy] ]]; then + check_pid + [[ ! -z $PID ]] && kill -9 ${PID} + cp "${CONF}" "/tmp/goflyway.conf" + rm -rf ${Folder} + mkdir ${Folder} + Download_goflyway + mv "/tmp/goflyway.conf" "${CONF}" + Start_goflyway + fi + else + echo -e "${Info} 当前 GoFlyway 已是最新版本 [ ${new_ver} ]" && exit 1 + fi +} +Download_goflyway(){ + cd ${Folder} + if [[ ${bit} == "x86_64" ]]; then + wget --no-check-certificate -N "https://github.com/coyove/goflyway/releases/download/${new_ver}/goflyway_linux_amd64.tar.gz" + mv goflyway_linux_amd64.tar.gz goflyway_linux.tar.gz + else + wget --no-check-certificate -N "https://github.com/coyove/goflyway/releases/download/${new_ver}/goflyway_linux_386.tar.gz" + mv goflyway_linux_386.tar.gz goflyway_linux.tar.gz + fi + [[ ! -e "goflyway_linux.tar.gz" ]] && echo -e "${Error} GoFlyway 下载失败 !" && exit 1 + tar -xzf goflyway_linux.tar.gz + [[ ! -e "goflyway" ]] && echo -e "${Error} GoFlyway 解压失败 !" && rm -f goflyway_linux.tar.gz && exit 1 + rm -f goflyway_linux.tar.gz + chmod +x goflyway + ./goflyway -gen-ca + echo "${new_ver}" > ${Now_ver_File} +} +Service_goflyway(){ + if [[ ${release} = "centos" ]]; then + if ! wget --no-check-certificate https://raw.githubusercontent.com/ToyoDAdoubi/doubi/master/service/goflyway_centos -O /etc/init.d/goflyway; then + echo -e "${Error} GoFlyway 服务管理脚本下载失败 !" && exit 1 + fi + chmod +x /etc/init.d/goflyway + chkconfig --add goflyway + chkconfig goflyway on + else + if ! wget --no-check-certificate https://raw.githubusercontent.com/ToyoDAdoubi/doubi/master/service/goflyway_debian -O /etc/init.d/goflyway; then + echo -e "${Error} GoFlyway 服务管理脚本下载失败 !" && exit 1 + fi + chmod +x /etc/init.d/goflyway + update-rc.d -f goflyway defaults + fi + echo -e "${Info} GoFlyway 服务管理脚本下载完成 !" +} +Installation_dependency(){ + mkdir ${Folder} +} +Write_config(){ + cat > ${CONF}<<-EOF +port=${new_port} +passwd=${new_passwd} +protocol=${new_protocol} +proxy_pass=${new_proxy_pass} +EOF +} +Read_config(){ + [[ ! -e ${CONF} ]] && echo -e "${Error} GoFlyway 配置文件不存在 !" && exit 1 + port=$(cat ${CONF}|grep "port"|awk -F "=" '{print $NF}') + passwd=$(cat ${CONF}|grep "passwd"|awk -F "=" '{print $NF}') + proxy_pass=$(cat ${CONF}|grep "proxy_pass"|awk -F "=" '{print $NF}') + protocol=$(cat ${CONF}|grep "protocol"|awk -F "=" '{print $NF}') + if [[ -z "${protocol}" ]]; then + protocol="http" + new_protocol="http" + new_port="${port}" + new_passwd="${passwd}" + new_proxy_pass="${proxy_pass}" + Write_config + fi +} +Set_port(){ + while true + do + echo -e "请输入 GoFlyway 监听端口 [1-65535](如果要伪装或者套CDN,那么只能使用端口:80 8080 8880 2052 2082 2086 2095)" + read -e -p "(默认: 8880):" new_port + [[ -z "${new_port}" ]] && new_port="8880" + echo $((${new_port}+0)) &>/dev/null + if [[ $? -eq 0 ]]; then + if [[ ${new_port} -ge 1 ]] && [[ ${new_port} -le 65535 ]]; then + echo && echo "========================" + echo -e " 端口 : ${Red_background_prefix} ${new_port} ${Font_color_suffix}" + echo "========================" && echo + break + else + echo "输入错误, 请输入正确的端口。" + fi + else + echo "输入错误, 请输入正确的端口。" + fi + done +} +Set_passwd(){ + echo "请输入 GoFlyway 密码" + read -e -p "(默认: doub.io):" new_passwd + [[ -z "${new_passwd}" ]] && new_passwd="doub.io" + echo && echo "========================" + echo -e " 密码 : ${Red_background_prefix} ${new_passwd} ${Font_color_suffix}" + echo "========================" && echo +} +Set_proxy_pass(){ + echo "请输入 GoFlyway 要伪装的网站(反向代理,只支持 HTTP:// 网站)" + read -e -p "(默认不伪装):" new_proxy_pass + if [[ ! -z ${new_proxy_pass} ]]; then + echo && echo "========================" + echo -e " 伪装 : ${Red_background_prefix} ${new_proxy_pass} ${Font_color_suffix}" + echo "========================" && echo + fi +} +Set_protocol(){ + echo -e "请选择 GoFlyway 传输协议 + + ${Green_font_prefix}1.${Font_color_suffix} HTTP (默认,要使用 CDN、WebSocket 则必须选择 HTTP 协议) + ${Green_font_prefix}2.${Font_color_suffix} KCP (将 TCP 数据转为 KCP,并通过UDP方式传输,可复活被TCP阻断的IP) + ${Tip} 如果使用 KCP 协议,那么将不能使用 CDN、WebSocket。另外,部分地区对海外的UDP链接会QOS限速,这可能导致 KCP 协议速度不理想。" && echo + read -e -p "(默认: 1. HTTP):" new_protocol + [[ -z "${new_protocol}" ]] && new_protocol="3" + if [[ ${new_protocol} == "1" ]]; then + new_protocol="http" + elif [[ ${new_protocol} == "2" ]]; then + new_protocol="kcp" + else + new_protocol="http" + fi + echo && echo "========================" + echo -e " 协议 : ${Red_background_prefix} ${new_protocol^^} ${Font_color_suffix}" + echo "========================" && echo +} +Set_conf(){ + Set_port + Set_passwd + Set_protocol + Set_proxy_pass +} +Modify_port(){ + Read_config + Set_port + new_passwd="${passwd}" + new_proxy_pass="${proxy_pass}" + new_protocol="${protocol}" + Del_iptables + Write_config + Add_iptables + Save_iptables + Restart_goflyway +} +Modify_passwd(){ + Read_config + Set_passwd + new_port="${port}" + new_proxy_pass="${proxy_pass}" + new_protocol="${protocol}" + Write_config + Restart_goflyway +} +Modify_proxy_pass(){ + Read_config + Set_proxy_pass + new_port="${port}" + new_passwd="${passwd}" + new_protocol="${protocol}" + Write_config + Restart_goflyway +} +Modify_protocol(){ + Read_config + Set_protocol + new_port="${port}" + new_passwd="${passwd}" + new_proxy_pass="${proxy_pass}" + Write_config + Restart_goflyway +} +Modify_all(){ + Read_config + Set_conf + Del_iptables + Write_config + Add_iptables + Save_iptables + Restart_goflyway +} +Set_goflyway(){ + check_installed_status + echo && echo -e "你要做什么? + ${Green_font_prefix}1.${Font_color_suffix} 修改 端口配置 + ${Green_font_prefix}2.${Font_color_suffix} 修改 密码配置 + ${Green_font_prefix}3.${Font_color_suffix} 修改 传输协议 + ${Green_font_prefix}4.${Font_color_suffix} 修改 伪装配置(反向代理) + ${Green_font_prefix}5.${Font_color_suffix} 修改 全部配置 +———————————————— + ${Green_font_prefix}6.${Font_color_suffix} 监控 运行状态 + + ${Tip} 用户的端口是不能重复的,密码可以重复 !" && echo + read -e -p "(默认: 取消):" gf_modify + [[ -z "${gf_modify}" ]] && echo "已取消..." && exit 1 + if [[ ${gf_modify} == "1" ]]; then + Modify_port + elif [[ ${gf_modify} == "2" ]]; then + Modify_passwd + elif [[ ${gf_modify} == "3" ]]; then + Modify_protocol + elif [[ ${gf_modify} == "4" ]]; then + Modify_proxy_pass + elif [[ ${gf_modify} == "5" ]]; then + Modify_all + elif [[ ${gf_modify} == "6" ]]; then + Set_crontab_monitor_goflyway + else + echo -e "${Error} 请输入正确的数字(1-6)" && exit 1 + fi +} +Install_goflyway(){ + check_root + [[ -e ${File} ]] && echo -e "${Error} 检测到 GoFlyway 已安装 !" && exit 1 + echo -e "${Info} 开始设置 用户配置..." + Set_conf + echo -e "${Info} 开始安装/配置 依赖..." + Installation_dependency + echo -e "${Info} 开始检测最新版本..." + check_new_ver + echo -e "${Info} 开始下载/安装..." + Download_goflyway + echo -e "${Info} 开始下载/安装 服务脚本(init)..." + Service_goflyway + echo -e "${Info} 开始写入 配置文件..." + Write_config + echo -e "${Info} 开始设置 iptables防火墙..." + Set_iptables + echo -e "${Info} 开始添加 iptables防火墙规则..." + Add_iptables + echo -e "${Info} 开始保存 iptables防火墙规则..." + Save_iptables + echo -e "${Info} 所有步骤 安装完毕,开始启动..." + Start_goflyway +} +Start_goflyway(){ + check_installed_status + check_pid + [[ ! -z ${PID} ]] && echo -e "${Error} GoFlyway 正在运行,请检查 !" && exit 1 + /etc/init.d/goflyway start + sleep 1s + check_pid + [[ ! -z ${PID} ]] && View_goflyway +} +Stop_goflyway(){ + check_installed_status + check_pid + [[ -z ${PID} ]] && echo -e "${Error} GoFlyway 没有运行,请检查 !" && exit 1 + /etc/init.d/goflyway stop +} +Restart_goflyway(){ + check_installed_status + check_pid + [[ ! -z ${PID} ]] && /etc/init.d/goflyway stop + /etc/init.d/goflyway start + sleep 1s + check_pid + [[ ! -z ${PID} ]] && View_goflyway +} +Update_goflyway(){ + check_installed_status + check_new_ver + check_ver_comparison +} +Uninstall_goflyway(){ + check_installed_status + echo "确定要卸载 GoFlyway ? (y/N)" + echo + read -e -p "(默认: n):" unyn + [[ -z ${unyn} ]] && unyn="n" + if [[ ${unyn} == [Yy] ]]; then + check_pid + [[ ! -z $PID ]] && kill -9 ${PID} + Read_config + Del_iptables + Save_iptables + rm -rf ${Folder} + if [[ ${release} = "centos" ]]; then + chkconfig --del goflyway + else + update-rc.d -f goflyway remove + fi + rm -rf /etc/init.d/goflyway + echo && echo "GoFlyway 卸载完成 !" && echo + else + echo && echo "卸载已取消..." && echo + fi +} +View_goflyway(){ + check_installed_status + Read_config + ip=$(wget -qO- -t1 -T2 ipinfo.io/ip) + if [[ -z "${ip}" ]]; then + ip=$(wget -qO- -t1 -T2 api.ip.sb/ip) + if [[ -z "${ip}" ]]; then + ip=$(wget -qO- -t1 -T2 members.3322.org/dyndns/getip) + if [[ -z "${ip}" ]]; then + ip="VPS_IP" + fi + fi + fi + [[ -z ${proxy_pass} ]] && proxy_pass="无" + link_qr + clear && echo "————————————————" && echo + echo -e " GoFlyway 信息 :" && echo + echo -e " 地址\t: ${Green_font_prefix}${ip}${Font_color_suffix}" + echo -e " 端口\t: ${Green_font_prefix}${port}${Font_color_suffix}" + echo -e " 密码\t: ${Green_font_prefix}${passwd}${Font_color_suffix}" + echo -e " 协议\t: ${Green_font_prefix}${protocol^^}${Font_color_suffix}" + echo -e " 伪装\t: ${Green_font_prefix}${proxy_pass}${Font_color_suffix}" + echo -e "${link}" + echo -e "${Tip} 链接仅适用于Windows系统的 Goflyway Tools 客户端(https://doub.io/dbrj-11/)。" + echo && echo "————————————————" +} +urlsafe_base64(){ + date=$(echo -n "$1"|base64|sed ':a;N;s/\n//g;ta'|sed 's/=//g;s/+/-/g;s/\//_/g') + echo -e "${date}" +} +link_qr(){ + PWDbase64=$(urlsafe_base64 "${passwd}") + base64=$(urlsafe_base64 "${ip}:${port}@${PWDbase64}:${protocol}") + url="goflyway://${base64}" + QRcode="http://doub.pw/qr/qr.php?text=${url}" + link=" 链接\t: ${Red_font_prefix}${url}${Font_color_suffix} \n 二维码 : ${Red_font_prefix}${QRcode}${Font_color_suffix} \n " +} +View_Log(){ + check_installed_status + [[ ! -e ${Log_File} ]] && echo -e "${Error} GoFlyway 日志文件不存在 !" && exit 1 + echo && echo -e "${Tip} 按 ${Red_font_prefix}Ctrl+C${Font_color_suffix} 终止查看日志" && echo -e "如果需要查看完整日志内容,请用 ${Red_font_prefix}cat ${Log_File}${Font_color_suffix} 命令。" && echo + tail -f ${Log_File} +} +# 显示 连接信息 +debian_View_user_connection_info(){ + format_1=$1 + Read_config + user_port=${port} + user_IP_1=$(netstat -anp |grep 'ESTABLISHED' |grep 'goflyway' |grep 'tcp6' |grep ":${user_port} " |awk '{print $5}' |awk -F ":" '{print $1}' |sort -u |grep -E -o "([0-9]{1,3}[\.]){3}[0-9]{1,3}") + if [[ -z ${user_IP_1} ]]; then + user_IP_total="0" + echo -e "端口: ${Green_font_prefix}"${user_port}"${Font_color_suffix}\t 链接IP总数: ${Green_font_prefix}"${user_IP_total}"${Font_color_suffix}\t 当前链接IP: " + else + user_IP_total=`echo -e "${user_IP_1}"|wc -l` + if [[ ${format_1} == "IP_address" ]]; then + echo -e "端口: ${Green_font_prefix}"${user_port}"${Font_color_suffix}\t 链接IP总数: ${Green_font_prefix}"${user_IP_total}"${Font_color_suffix}\t 当前链接IP: " + get_IP_address + echo + else + user_IP=$(echo -e "\n${user_IP_1}") + echo -e "端口: ${Green_font_prefix}"${user_port}"${Font_color_suffix}\t 链接IP总数: ${Green_font_prefix}"${user_IP_total}"${Font_color_suffix}\t 当前链接IP: ${Green_font_prefix}${user_IP}${Font_color_suffix}\n" + fi + fi + user_IP="" +} +centos_View_user_connection_info(){ + format_1=$1 + Read_config + user_port=${port} + user_IP_1=`netstat -anp |grep 'ESTABLISHED' |grep 'goflyway' |grep 'tcp' |grep ":${user_port} "|grep '::ffff:' |awk '{print $5}' |awk -F ":" '{print $4}' |sort -u |grep -E -o "([0-9]{1,3}[\.]){3}[0-9]{1,3}"` + if [[ -z ${user_IP_1} ]]; then + user_IP_total="0" + echo -e "端口: ${Green_font_prefix}"${user_port}"${Font_color_suffix}\t 链接IP总数: ${Green_font_prefix}"${user_IP_total}"${Font_color_suffix}\t 当前链接IP: " + else + user_IP_total=`echo -e "${user_IP_1}"|wc -l` + if [[ ${format_1} == "IP_address" ]]; then + echo -e "端口: ${Green_font_prefix}"${user_port}"${Font_color_suffix}\t 链接IP总数: ${Green_font_prefix}"${user_IP_total}"${Font_color_suffix}\t 当前链接IP: " + get_IP_address + echo + else + user_IP=$(echo -e "\n${user_IP_1}") + echo -e "端口: ${Green_font_prefix}"${user_port}"${Font_color_suffix}\t 链接IP总数: ${Green_font_prefix}"${user_IP_total}"${Font_color_suffix}\t 当前链接IP: ${Green_font_prefix}${user_IP}${Font_color_suffix}\n" + fi + fi + user_IP="" +} +View_user_connection_info(){ + check_installed_status + echo && echo -e "请选择要显示的格式: + ${Green_font_prefix}1.${Font_color_suffix} 显示 IP 格式 + ${Green_font_prefix}2.${Font_color_suffix} 显示 IP+IP归属地 格式" && echo + read -e -p "(默认: 1):" goflyway_connection_info + [[ -z "${goflyway_connection_info}" ]] && goflyway_connection_info="1" + if [[ "${goflyway_connection_info}" == "1" ]]; then + View_user_connection_info_1 "" + elif [[ "${goflyway_connection_info}" == "2" ]]; then + echo -e "${Tip} 检测IP归属地(ipip.net),如果IP较多,可能时间会比较长..." + View_user_connection_info_1 "IP_address" + else + echo -e "${Error} 请输入正确的数字(1-2)" && exit 1 + fi +} +View_user_connection_info_1(){ + format=$1 + if [[ ${release} = "centos" ]]; then + cat /etc/redhat-release |grep 7\..*|grep -i centos>/dev/null + if [[ $? = 0 ]]; then + debian_View_user_connection_info "$format" + else + centos_View_user_connection_info "$format" + fi + else + debian_View_user_connection_info "$format" + fi +} +get_IP_address(){ + #echo "user_IP_1=${user_IP_1}" + if [[ ! -z ${user_IP_1} ]]; then + #echo "user_IP_total=${user_IP_total}" + for((integer_1 = ${user_IP_total}; integer_1 >= 1; integer_1--)) + do + IP=$(echo "${user_IP_1}" |sed -n "$integer_1"p) + #echo "IP=${IP}" + IP_address=$(wget -qO- -t1 -T2 http://freeapi.ipip.net/${IP}|sed 's/\"//g;s/,//g;s/\[//g;s/\]//g') + #echo "IP_address=${IP_address}" + #user_IP="${user_IP}\n${IP}(${IP_address})" + echo -e "${Green_font_prefix}${IP}${Font_color_suffix} (${IP_address})" + #echo "user_IP=${user_IP}" + sleep 1s + done + fi +} +Set_crontab_monitor_goflyway(){ + check_crontab_installed_status + crontab_monitor_goflyway_status=$(crontab -l|grep "goflyway.sh monitor") + if [[ -z "${crontab_monitor_goflyway_status}" ]]; then + echo && echo -e "当前监控模式: ${Green_font_prefix}未开启${Font_color_suffix}" && echo + echo -e "确定要开启 ${Green_font_prefix}Goflyway 服务端运行状态监控${Font_color_suffix} 功能吗?(当进程关闭则自动启动 Goflyway 服务端)[Y/n]" + read -e -p "(默认: y):" crontab_monitor_goflyway_status_ny + [[ -z "${crontab_monitor_goflyway_status_ny}" ]] && crontab_monitor_goflyway_status_ny="y" + if [[ ${crontab_monitor_goflyway_status_ny} == [Yy] ]]; then + crontab_monitor_goflyway_cron_start + else + echo && echo " 已取消..." && echo + fi + else + echo && echo -e "当前监控模式: ${Green_font_prefix}已开启${Font_color_suffix}" && echo + echo -e "确定要关闭 ${Green_font_prefix}Goflyway 服务端运行状态监控${Font_color_suffix} 功能吗?(当进程关闭则自动启动 Goflyway 服务端)[y/N]" + read -e -p "(默认: n):" crontab_monitor_goflyway_status_ny + [[ -z "${crontab_monitor_goflyway_status_ny}" ]] && crontab_monitor_goflyway_status_ny="n" + if [[ ${crontab_monitor_goflyway_status_ny} == [Yy] ]]; then + crontab_monitor_goflyway_cron_stop + else + echo && echo " 已取消..." && echo + fi + fi +} +crontab_monitor_goflyway_cron_start(){ + crontab -l > "$file_1/crontab.bak" + sed -i "/goflyway.sh monitor/d" "$file_1/crontab.bak" + echo -e "\n* * * * * /bin/bash $file_1/goflyway.sh monitor" >> "$file_1/crontab.bak" + crontab "$file_1/crontab.bak" + rm -r "$file_1/crontab.bak" + cron_config=$(crontab -l | grep "goflyway.sh monitor") + if [[ -z ${cron_config} ]]; then + echo -e "${Error} Goflyway 服务端运行状态监控功能 启动失败 !" && exit 1 + else + echo -e "${Info} Goflyway 服务端运行状态监控功能 启动成功 !" + fi +} +crontab_monitor_goflyway_cron_stop(){ + crontab -l > "$file_1/crontab.bak" + sed -i "/goflyway.sh monitor/d" "$file_1/crontab.bak" + crontab "$file_1/crontab.bak" + rm -r "$file_1/crontab.bak" + cron_config=$(crontab -l | grep "goflyway.sh monitor") + if [[ ! -z ${cron_config} ]]; then + echo -e "${Error} Goflyway 服务端运行状态监控功能 停止失败 !" && exit 1 + else + echo -e "${Info} Goflyway 服务端运行状态监控功能 停止成功 !" + fi +} +crontab_monitor_goflyway(){ + check_installed_status + check_pid + echo "${PID}" + if [[ -z ${PID} ]]; then + echo -e "${Error} [$(date "+%Y-%m-%d %H:%M:%S %u %Z")] 检测到 Goflyway服务端 未运行 , 开始启动..." | tee -a ${Log_File} + /etc/init.d/goflyway start + sleep 1s + check_pid + if [[ -z ${PID} ]]; then + echo -e "${Error} [$(date "+%Y-%m-%d %H:%M:%S %u %Z")] Goflyway服务端 启动失败..." | tee -a ${Log_File} + else + echo -e "${Info} [$(date "+%Y-%m-%d %H:%M:%S %u %Z")] Goflyway服务端 启动成功..." | tee -a ${Log_File} + fi + else + echo -e "${Info} [$(date "+%Y-%m-%d %H:%M:%S %u %Z")] Goflyway服务端 进程运行正常..." | tee -a ${Log_File} + fi +} +Add_iptables(){ + iptables -I INPUT -m state --state NEW -m tcp -p tcp --dport ${new_port} -j ACCEPT + iptables -I INPUT -m state --state NEW -m udp -p udp --dport ${new_port} -j ACCEPT +} +Del_iptables(){ + iptables -D INPUT -m state --state NEW -m tcp -p tcp --dport ${port} -j ACCEPT + iptables -D INPUT -m state --state NEW -m udp -p udp --dport ${port} -j ACCEPT +} +Save_iptables(){ + if [[ ${release} == "centos" ]]; then + service iptables save + else + iptables-save > /etc/iptables.up.rules + fi +} +Set_iptables(){ + if [[ ${release} == "centos" ]]; then + service iptables save + chkconfig --level 2345 iptables on + else + iptables-save > /etc/iptables.up.rules + echo -e '#!/bin/bash\n/sbin/iptables-restore < /etc/iptables.up.rules' > /etc/network/if-pre-up.d/iptables + chmod +x /etc/network/if-pre-up.d/iptables + fi +} +Update_Shell(){ + sh_new_ver=$(wget --no-check-certificate -qO- -t1 -T3 "https://raw.githubusercontent.com/ToyoDAdoubi/doubi/master/goflyway.sh"|grep 'sh_ver="'|awk -F "=" '{print $NF}'|sed 's/\"//g'|head -1) && sh_new_type="github" + [[ -z ${sh_new_ver} ]] && echo -e "${Error} 无法链接到 Github !" && exit 0 + if [[ -e "/etc/init.d/goflyway" ]]; then + rm -rf /etc/init.d/goflyway + Service_goflyway + fi + wget -N --no-check-certificate "https://raw.githubusercontent.com/ToyoDAdoubi/doubi/master/goflyway.sh" && chmod +x goflyway.sh + echo -e "脚本已更新为最新版本[ ${sh_new_ver} ] !(注意:因为更新方式为直接覆盖当前运行的脚本,所以可能下面会提示一些报错,无视即可)" && exit 0 +} +check_sys +action=$1 +if [[ "${action}" == "monitor" ]]; then + crontab_monitor_goflyway +else +echo && echo -e " GoFlyway 一键管理脚本 ${Red_font_prefix}[v${sh_ver}]${Font_color_suffix} + -- Toyo | doub.io/goflyway-jc2 -- + + ${Green_font_prefix} 0.${Font_color_suffix} 升级脚本 +———————————— + ${Green_font_prefix} 1.${Font_color_suffix} 安装 GoFlyway + ${Green_font_prefix} 2.${Font_color_suffix} 升级 GoFlyway + ${Green_font_prefix} 3.${Font_color_suffix} 卸载 GoFlyway +———————————— + ${Green_font_prefix} 4.${Font_color_suffix} 启动 GoFlyway + ${Green_font_prefix} 5.${Font_color_suffix} 停止 GoFlyway + ${Green_font_prefix} 6.${Font_color_suffix} 重启 GoFlyway +———————————— + ${Green_font_prefix} 7.${Font_color_suffix} 设置 账号配置 + ${Green_font_prefix} 8.${Font_color_suffix} 查看 账号信息 + ${Green_font_prefix} 9.${Font_color_suffix} 查看 日志信息 + ${Green_font_prefix}10.${Font_color_suffix} 查看 链接信息 +————————————" && echo +if [[ -e ${File} ]]; then + check_pid + if [[ ! -z "${PID}" ]]; then + echo -e " 当前状态: ${Green_font_prefix}已安装${Font_color_suffix} 并 ${Green_font_prefix}已启动${Font_color_suffix}" + else + echo -e " 当前状态: ${Green_font_prefix}已安装${Font_color_suffix} 但 ${Red_font_prefix}未启动${Font_color_suffix}" + fi +else + echo -e " 当前状态: ${Red_font_prefix}未安装${Font_color_suffix}" +fi +echo +read -e -p " 请输入数字 [0-10]:" num +case "$num" in + 0) + Update_Shell + ;; + 1) + Install_goflyway + ;; + 2) + Update_goflyway + ;; + 3) + Uninstall_goflyway + ;; + 4) + Start_goflyway + ;; + 5) + Stop_goflyway + ;; + 6) + Restart_goflyway + ;; + 7) + Set_goflyway + ;; + 8) + View_goflyway + ;; + 9) + View_Log + ;; + 10) + View_user_connection_info + ;; + *) + echo "请输入正确数字 [0-10]" + ;; +esac +fi \ No newline at end of file diff --git a/linux/software/shadowsocks-all.sh b/linux/software/shadowsocks-all.sh new file mode 100644 index 0000000..3a7adc6 --- /dev/null +++ b/linux/software/shadowsocks-all.sh @@ -0,0 +1,1383 @@ +#!/usr/bin/env bash +PATH=/bin:/sbin:/usr/bin:/usr/sbin:/usr/local/bin:/usr/local/sbin:~/bin +export PATH +# +# Auto install Shadowsocks Server (all version) +# +# Copyright (C) 2016-2019 Teddysun +# +# System Required: CentOS 6+, Debian7+, Ubuntu12+ +# +# Reference URL: +# https://github.com/shadowsocks/shadowsocks +# https://github.com/shadowsocks/shadowsocks-go +# https://github.com/shadowsocks/shadowsocks-libev +# https://github.com/shadowsocks/shadowsocks-windows +# https://github.com/shadowsocksr-rm/shadowsocksr +# https://github.com/shadowsocksrr/shadowsocksr +# https://github.com/shadowsocksrr/shadowsocksr-csharp +# +# Thanks: +# @clowwindy +# @breakwa11 +# @cyfdecyf +# @madeye +# @linusyang +# @Akkariiin +# +# Intro: https://teddysun.com/486.html + +red='\033[0;31m' +green='\033[0;32m' +yellow='\033[0;33m' +plain='\033[0m' + +[[ $EUID -ne 0 ]] && echo -e "[${red}Error${plain}] This script must be run as root!" && exit 1 + +cur_dir=$( pwd ) +software=(Shadowsocks-Python ShadowsocksR Shadowsocks-Go Shadowsocks-libev) + +libsodium_file="libsodium-1.0.17" +libsodium_url="https://github.com/jedisct1/libsodium/releases/download/1.0.17/libsodium-1.0.17.tar.gz" + +mbedtls_file="mbedtls-2.16.0" +mbedtls_url="https://tls.mbed.org/download/mbedtls-2.16.0-gpl.tgz" + +shadowsocks_python_file="shadowsocks-master" +shadowsocks_python_url="https://github.com/shadowsocks/shadowsocks/archive/master.zip" +shadowsocks_python_init="/etc/init.d/shadowsocks-python" +shadowsocks_python_config="/etc/shadowsocks-python/config.json" +shadowsocks_python_centos="https://raw.githubusercontent.com/teddysun/shadowsocks_install/master/shadowsocks" +shadowsocks_python_debian="https://raw.githubusercontent.com/teddysun/shadowsocks_install/master/shadowsocks-debian" + +shadowsocks_r_file="shadowsocksr-3.2.2" +shadowsocks_r_url="https://github.com/shadowsocksrr/shadowsocksr/archive/3.2.2.tar.gz" +shadowsocks_r_init="/etc/init.d/shadowsocks-r" +shadowsocks_r_config="/etc/shadowsocks-r/config.json" +shadowsocks_r_centos="https://raw.githubusercontent.com/teddysun/shadowsocks_install/master/shadowsocksR" +shadowsocks_r_debian="https://raw.githubusercontent.com/teddysun/shadowsocks_install/master/shadowsocksR-debian" + +shadowsocks_go_file_64="shadowsocks-server-linux64-1.2.2" +shadowsocks_go_url_64="https://dl.lamp.sh/shadowsocks/shadowsocks-server-linux64-1.2.2.gz" +shadowsocks_go_file_32="shadowsocks-server-linux32-1.2.2" +shadowsocks_go_url_32="https://dl.lamp.sh/shadowsocks/shadowsocks-server-linux32-1.2.2.gz" +shadowsocks_go_init="/etc/init.d/shadowsocks-go" +shadowsocks_go_config="/etc/shadowsocks-go/config.json" +shadowsocks_go_centos="https://raw.githubusercontent.com/teddysun/shadowsocks_install/master/shadowsocks-go" +shadowsocks_go_debian="https://raw.githubusercontent.com/teddysun/shadowsocks_install/master/shadowsocks-go-debian" + +shadowsocks_libev_init="/etc/init.d/shadowsocks-libev" +shadowsocks_libev_config="/etc/shadowsocks-libev/config.json" +shadowsocks_libev_centos="https://raw.githubusercontent.com/teddysun/shadowsocks_install/master/shadowsocks-libev" +shadowsocks_libev_debian="https://raw.githubusercontent.com/teddysun/shadowsocks_install/master/shadowsocks-libev-debian" + +# Stream Ciphers +common_ciphers=( +aes-256-gcm +aes-192-gcm +aes-128-gcm +aes-256-ctr +aes-192-ctr +aes-128-ctr +aes-256-cfb +aes-192-cfb +aes-128-cfb +camellia-128-cfb +camellia-192-cfb +camellia-256-cfb +xchacha20-ietf-poly1305 +chacha20-ietf-poly1305 +chacha20-ietf +chacha20 +salsa20 +rc4-md5 +) +go_ciphers=( +aes-256-cfb +aes-192-cfb +aes-128-cfb +aes-256-ctr +aes-192-ctr +aes-128-ctr +chacha20-ietf +chacha20 +salsa20 +rc4-md5 +) +r_ciphers=( +none +aes-256-cfb +aes-192-cfb +aes-128-cfb +aes-256-cfb8 +aes-192-cfb8 +aes-128-cfb8 +aes-256-ctr +aes-192-ctr +aes-128-ctr +chacha20-ietf +chacha20 +salsa20 +xchacha20 +xsalsa20 +rc4-md5 +) +# Reference URL: +# https://github.com/shadowsocksr-rm/shadowsocks-rss/blob/master/ssr.md +# https://github.com/shadowsocksrr/shadowsocksr/commit/a3cf0254508992b7126ab1151df0c2f10bf82680 +# Protocol +protocols=( +origin +verify_deflate +auth_sha1_v4 +auth_sha1_v4_compatible +auth_aes128_md5 +auth_aes128_sha1 +auth_chain_a +auth_chain_b +auth_chain_c +auth_chain_d +auth_chain_e +auth_chain_f +) +# obfs +obfs=( +plain +http_simple +http_simple_compatible +http_post +http_post_compatible +tls1.2_ticket_auth +tls1.2_ticket_auth_compatible +tls1.2_ticket_fastauth +tls1.2_ticket_fastauth_compatible +) +# libev obfuscating +obfs_libev=(http tls) +# initialization parameter +libev_obfs="" + +disable_selinux(){ + if [ -s /etc/selinux/config ] && grep 'SELINUX=enforcing' /etc/selinux/config; then + sed -i 's/SELINUX=enforcing/SELINUX=disabled/g' /etc/selinux/config + setenforce 0 + fi +} + +check_sys(){ + local checkType=$1 + local value=$2 + + local release='' + local systemPackage='' + + if [[ -f /etc/redhat-release ]]; then + release="centos" + systemPackage="yum" + elif grep -Eqi "debian|raspbian" /etc/issue; then + release="debian" + systemPackage="apt" + elif grep -Eqi "ubuntu" /etc/issue; then + release="ubuntu" + systemPackage="apt" + elif grep -Eqi "centos|red hat|redhat" /etc/issue; then + release="centos" + systemPackage="yum" + elif grep -Eqi "debian|raspbian" /proc/version; then + release="debian" + systemPackage="apt" + elif grep -Eqi "ubuntu" /proc/version; then + release="ubuntu" + systemPackage="apt" + elif grep -Eqi "centos|red hat|redhat" /proc/version; then + release="centos" + systemPackage="yum" + fi + + if [[ "${checkType}" == "sysRelease" ]]; then + if [ "${value}" == "${release}" ]; then + return 0 + else + return 1 + fi + elif [[ "${checkType}" == "packageManager" ]]; then + if [ "${value}" == "${systemPackage}" ]; then + return 0 + else + return 1 + fi + fi +} + +version_ge(){ + test "$(echo "$@" | tr " " "\n" | sort -rV | head -n 1)" == "$1" +} + +version_gt(){ + test "$(echo "$@" | tr " " "\n" | sort -V | head -n 1)" != "$1" +} + +check_kernel_version(){ + local kernel_version=$(uname -r | cut -d- -f1) + if version_gt ${kernel_version} 3.7.0; then + return 0 + else + return 1 + fi +} + +check_kernel_headers(){ + if check_sys packageManager yum; then + if rpm -qa | grep -q headers-$(uname -r); then + return 0 + else + return 1 + fi + elif check_sys packageManager apt; then + if dpkg -s linux-headers-$(uname -r) > /dev/null 2>&1; then + return 0 + else + return 1 + fi + fi + return 1 +} + +getversion(){ + if [[ -s /etc/redhat-release ]]; then + grep -oE "[0-9.]+" /etc/redhat-release + else + grep -oE "[0-9.]+" /etc/issue + fi +} + +centosversion(){ + if check_sys sysRelease centos; then + local code=$1 + local version="$(getversion)" + local main_ver=${version%%.*} + if [ "$main_ver" == "$code" ]; then + return 0 + else + return 1 + fi + else + return 1 + fi +} + +autoconf_version(){ + if [ ! "$(command -v autoconf)" ]; then + echo -e "[${green}Info${plain}] Starting install package autoconf" + if check_sys packageManager yum; then + yum install -y autoconf > /dev/null 2>&1 || echo -e "[${red}Error:${plain}] Failed to install autoconf" + elif check_sys packageManager apt; then + apt-get -y update > /dev/null 2>&1 + apt-get -y install autoconf > /dev/null 2>&1 || echo -e "[${red}Error:${plain}] Failed to install autoconf" + fi + fi + local autoconf_ver=$(autoconf --version | grep autoconf | grep -oE "[0-9.]+") + if version_ge ${autoconf_ver} 2.67; then + return 0 + else + return 1 + fi +} + +get_ip(){ + local IP=$( ip addr | egrep -o '[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}\.[0-9]{1,3}' | egrep -v "^192\.168|^172\.1[6-9]\.|^172\.2[0-9]\.|^172\.3[0-2]\.|^10\.|^127\.|^255\.|^0\." | head -n 1 ) + [ -z ${IP} ] && IP=$( wget -qO- -t1 -T2 ipv4.icanhazip.com ) + [ -z ${IP} ] && IP=$( wget -qO- -t1 -T2 ipinfo.io/ip ) + echo ${IP} +} + +get_ipv6(){ + local ipv6=$(wget -qO- -t1 -T2 ipv6.icanhazip.com) + [ -z ${ipv6} ] && return 1 || return 0 +} + +get_libev_ver(){ + libev_ver=$(wget --no-check-certificate -qO- https://api.github.com/repos/shadowsocks/shadowsocks-libev/releases/latest | grep 'tag_name' | cut -d\" -f4) + [ -z ${libev_ver} ] && echo -e "[${red}Error${plain}] Get shadowsocks-libev latest version failed" && exit 1 +} + +get_opsy(){ + [ -f /etc/redhat-release ] && awk '{print ($1,$3~/^[0-9]/?$3:$4)}' /etc/redhat-release && return + [ -f /etc/os-release ] && awk -F'[= "]' '/PRETTY_NAME/{print $3,$4,$5}' /etc/os-release && return + [ -f /etc/lsb-release ] && awk -F'[="]+' '/DESCRIPTION/{print $2}' /etc/lsb-release && return +} + +is_64bit(){ + if [ `getconf WORD_BIT` = '32' ] && [ `getconf LONG_BIT` = '64' ] ; then + return 0 + else + return 1 + fi +} + +debianversion(){ + if check_sys sysRelease debian;then + local version=$( get_opsy ) + local code=${1} + local main_ver=$( echo ${version} | sed 's/[^0-9]//g') + if [ "${main_ver}" == "${code}" ];then + return 0 + else + return 1 + fi + else + return 1 + fi +} + +download(){ + local filename=$(basename $1) + if [ -f ${1} ]; then + echo "${filename} [found]" + else + echo "${filename} not found, download now..." + wget --no-check-certificate -c -t3 -T60 -O ${1} ${2} + if [ $? -ne 0 ]; then + echo -e "[${red}Error${plain}] Download ${filename} failed." + exit 1 + fi + fi +} + +download_files(){ + cd ${cur_dir} + + if [ "${selected}" == "1" ]; then + download "${shadowsocks_python_file}.zip" "${shadowsocks_python_url}" + if check_sys packageManager yum; then + download "${shadowsocks_python_init}" "${shadowsocks_python_centos}" + elif check_sys packageManager apt; then + download "${shadowsocks_python_init}" "${shadowsocks_python_debian}" + fi + elif [ "${selected}" == "2" ]; then + download "${shadowsocks_r_file}.tar.gz" "${shadowsocks_r_url}" + if check_sys packageManager yum; then + download "${shadowsocks_r_init}" "${shadowsocks_r_centos}" + elif check_sys packageManager apt; then + download "${shadowsocks_r_init}" "${shadowsocks_r_debian}" + fi + elif [ "${selected}" == "3" ]; then + if is_64bit; then + download "${shadowsocks_go_file_64}.gz" "${shadowsocks_go_url_64}" + else + download "${shadowsocks_go_file_32}.gz" "${shadowsocks_go_url_32}" + fi + if check_sys packageManager yum; then + download "${shadowsocks_go_init}" "${shadowsocks_go_centos}" + elif check_sys packageManager apt; then + download "${shadowsocks_go_init}" "${shadowsocks_go_debian}" + fi + elif [ "${selected}" == "4" ]; then + get_libev_ver + shadowsocks_libev_file="shadowsocks-libev-$(echo ${libev_ver} | sed -e 's/^[a-zA-Z]//g')" + shadowsocks_libev_url="https://github.com/shadowsocks/shadowsocks-libev/releases/download/${libev_ver}/${shadowsocks_libev_file}.tar.gz" + + download "${shadowsocks_libev_file}.tar.gz" "${shadowsocks_libev_url}" + if check_sys packageManager yum; then + download "${shadowsocks_libev_init}" "${shadowsocks_libev_centos}" + elif check_sys packageManager apt; then + download "${shadowsocks_libev_init}" "${shadowsocks_libev_debian}" + fi + fi + +} + +get_char(){ + SAVEDSTTY=$(stty -g) + stty -echo + stty cbreak + dd if=/dev/tty bs=1 count=1 2> /dev/null + stty -raw + stty echo + stty $SAVEDSTTY +} + +error_detect_depends(){ + local command=$1 + local depend=`echo "${command}" | awk '{print $4}'` + echo -e "[${green}Info${plain}] Starting to install package ${depend}" + ${command} > /dev/null 2>&1 + if [ $? -ne 0 ]; then + echo -e "[${red}Error${plain}] Failed to install ${red}${depend}${plain}" + echo "Please visit: https://teddysun.com/486.html and contact." + exit 1 + fi +} + +config_firewall(){ + if centosversion 6; then + /etc/init.d/iptables status > /dev/null 2>&1 + if [ $? -eq 0 ]; then + iptables -L -n | grep -i ${shadowsocksport} > /dev/null 2>&1 + if [ $? -ne 0 ]; then + iptables -I INPUT -m state --state NEW -m tcp -p tcp --dport ${shadowsocksport} -j ACCEPT + iptables -I INPUT -m state --state NEW -m udp -p udp --dport ${shadowsocksport} -j ACCEPT + /etc/init.d/iptables save + /etc/init.d/iptables restart + else + echo -e "[${green}Info${plain}] port ${green}${shadowsocksport}${plain} already be enabled." + fi + else + echo -e "[${yellow}Warning${plain}] iptables looks like not running or not installed, please enable port ${shadowsocksport} manually if necessary." + fi + elif centosversion 7; then + systemctl status firewalld > /dev/null 2>&1 + if [ $? -eq 0 ]; then + default_zone=$(firewall-cmd --get-default-zone) + firewall-cmd --permanent --zone=${default_zone} --add-port=${shadowsocksport}/tcp + firewall-cmd --permanent --zone=${default_zone} --add-port=${shadowsocksport}/udp + firewall-cmd --reload + else + echo -e "[${yellow}Warning${plain}] firewalld looks like not running or not installed, please enable port ${shadowsocksport} manually if necessary." + fi + fi +} + +config_shadowsocks(){ + +if check_kernel_version && check_kernel_headers; then + fast_open="true" +else + fast_open="false" +fi + +if [ "${selected}" == "1" ]; then + if [ ! -d "$(dirname ${shadowsocks_python_config})" ]; then + mkdir -p $(dirname ${shadowsocks_python_config}) + fi + cat > ${shadowsocks_python_config}<<-EOF +{ + "server":"0.0.0.0", + "server_port":${shadowsocksport}, + "local_address":"127.0.0.1", + "local_port":1080, + "password":"${shadowsockspwd}", + "timeout":300, + "method":"${shadowsockscipher}", + "fast_open":${fast_open} +} +EOF +elif [ "${selected}" == "2" ]; then + if [ ! -d "$(dirname ${shadowsocks_r_config})" ]; then + mkdir -p $(dirname ${shadowsocks_r_config}) + fi + cat > ${shadowsocks_r_config}<<-EOF +{ + "server":"0.0.0.0", + "server_ipv6":"::", + "server_port":${shadowsocksport}, + "local_address":"127.0.0.1", + "local_port":1080, + "password":"${shadowsockspwd}", + "timeout":120, + "method":"${shadowsockscipher}", + "protocol":"${shadowsockprotocol}", + "protocol_param":"", + "obfs":"${shadowsockobfs}", + "obfs_param":"", + "redirect":"", + "dns_ipv6":false, + "fast_open":${fast_open}, + "workers":1 +} +EOF +elif [ "${selected}" == "3" ]; then + if [ ! -d "$(dirname ${shadowsocks_go_config})" ]; then + mkdir -p $(dirname ${shadowsocks_go_config}) + fi + cat > ${shadowsocks_go_config}<<-EOF +{ + "server":"0.0.0.0", + "server_port":${shadowsocksport}, + "local_port":1080, + "password":"${shadowsockspwd}", + "method":"${shadowsockscipher}", + "timeout":300 +} +EOF +elif [ "${selected}" == "4" ]; then + local server_value="\"0.0.0.0\"" + if get_ipv6; then + server_value="[\"[::0]\",\"0.0.0.0\"]" + fi + + if [ ! -d "$(dirname ${shadowsocks_libev_config})" ]; then + mkdir -p $(dirname ${shadowsocks_libev_config}) + fi + + if [ "${libev_obfs}" == "y" ] || [ "${libev_obfs}" == "Y" ]; then + cat > ${shadowsocks_libev_config}<<-EOF +{ + "server":${server_value}, + "server_port":${shadowsocksport}, + "password":"${shadowsockspwd}", + "timeout":300, + "user":"nobody", + "method":"${shadowsockscipher}", + "fast_open":${fast_open}, + "nameserver":"8.8.8.8", + "mode":"tcp_and_udp", + "plugin":"obfs-server", + "plugin_opts":"obfs=${shadowsocklibev_obfs}" +} +EOF + else + cat > ${shadowsocks_libev_config}<<-EOF +{ + "server":${server_value}, + "server_port":${shadowsocksport}, + "password":"${shadowsockspwd}", + "timeout":300, + "user":"nobody", + "method":"${shadowsockscipher}", + "fast_open":${fast_open}, + "nameserver":"8.8.8.8", + "mode":"tcp_and_udp" +} +EOF + fi + +fi +} + +install_dependencies(){ + if check_sys packageManager yum; then + echo -e "[${green}Info${plain}] Checking the EPEL repository..." + if [ ! -f /etc/yum.repos.d/epel.repo ]; then + yum install -y epel-release > /dev/null 2>&1 + fi + [ ! -f /etc/yum.repos.d/epel.repo ] && echo -e "[${red}Error${plain}] Install EPEL repository failed, please check it." && exit 1 + [ ! "$(command -v yum-config-manager)" ] && yum install -y yum-utils > /dev/null 2>&1 + [ x"$(yum-config-manager epel | grep -w enabled | awk '{print $3}')" != x"True" ] && yum-config-manager --enable epel > /dev/null 2>&1 + echo -e "[${green}Info${plain}] Checking the EPEL repository complete..." + + yum_depends=( + unzip gzip openssl openssl-devel gcc python python-devel python-setuptools pcre pcre-devel libtool libevent + autoconf automake make curl curl-devel zlib-devel perl perl-devel cpio expat-devel gettext-devel + libev-devel c-ares-devel git qrencode + ) + for depend in ${yum_depends[@]}; do + error_detect_depends "yum -y install ${depend}" + done + elif check_sys packageManager apt; then + apt_depends=( + gettext build-essential unzip gzip python python-dev python-setuptools curl openssl libssl-dev + autoconf automake libtool gcc make perl cpio libpcre3 libpcre3-dev zlib1g-dev libev-dev libc-ares-dev git qrencode + ) + + apt-get -y update + for depend in ${apt_depends[@]}; do + error_detect_depends "apt-get -y install ${depend}" + done + fi +} + +install_check(){ + if check_sys packageManager yum || check_sys packageManager apt; then + if centosversion 5; then + return 1 + fi + return 0 + else + return 1 + fi +} + +install_select(){ + if ! install_check; then + echo -e "[${red}Error${plain}] Your OS is not supported to run it!" + echo "Please change to CentOS 6+/Debian 7+/Ubuntu 12+ and try again." + exit 1 + fi + + clear + while true + do + echo "Which Shadowsocks server you'd select:" + for ((i=1;i<=${#software[@]};i++ )); do + hint="${software[$i-1]}" + echo -e "${green}${i}${plain}) ${hint}" + done + read -p "Please enter a number (Default ${software[0]}):" selected + [ -z "${selected}" ] && selected="1" + case "${selected}" in + 1|2|3|4) + echo + echo "You choose = ${software[${selected}-1]}" + echo + break + ;; + *) + echo -e "[${red}Error${plain}] Please only enter a number [1-4]" + ;; + esac + done +} + +install_prepare_password(){ + echo "Please enter password for ${software[${selected}-1]}" + read -p "(Default password: teddysun.com):" shadowsockspwd + [ -z "${shadowsockspwd}" ] && shadowsockspwd="teddysun.com" + echo + echo "password = ${shadowsockspwd}" + echo +} + +install_prepare_port() { + while true + do + dport=$(shuf -i 9000-19999 -n 1) + echo -e "Please enter a port for ${software[${selected}-1]} [1-65535]" + read -p "(Default port: ${dport}):" shadowsocksport + [ -z "${shadowsocksport}" ] && shadowsocksport=${dport} + expr ${shadowsocksport} + 1 &>/dev/null + if [ $? -eq 0 ]; then + if [ ${shadowsocksport} -ge 1 ] && [ ${shadowsocksport} -le 65535 ] && [ ${shadowsocksport:0:1} != 0 ]; then + echo + echo "port = ${shadowsocksport}" + echo + break + fi + fi + echo -e "[${red}Error${plain}] Please enter a correct number [1-65535]" + done +} + +install_prepare_cipher(){ + while true + do + echo -e "Please select stream cipher for ${software[${selected}-1]}:" + + if [[ "${selected}" == "1" || "${selected}" == "4" ]]; then + for ((i=1;i<=${#common_ciphers[@]};i++ )); do + hint="${common_ciphers[$i-1]}" + echo -e "${green}${i}${plain}) ${hint}" + done + read -p "Which cipher you'd select(Default: ${common_ciphers[0]}):" pick + [ -z "$pick" ] && pick=1 + expr ${pick} + 1 &>/dev/null + if [ $? -ne 0 ]; then + echo -e "[${red}Error${plain}] Please enter a number" + continue + fi + if [[ "$pick" -lt 1 || "$pick" -gt ${#common_ciphers[@]} ]]; then + echo -e "[${red}Error${plain}] Please enter a number between 1 and ${#common_ciphers[@]}" + continue + fi + shadowsockscipher=${common_ciphers[$pick-1]} + elif [ "${selected}" == "2" ]; then + for ((i=1;i<=${#r_ciphers[@]};i++ )); do + hint="${r_ciphers[$i-1]}" + echo -e "${green}${i}${plain}) ${hint}" + done + read -p "Which cipher you'd select(Default: ${r_ciphers[1]}):" pick + [ -z "$pick" ] && pick=2 + expr ${pick} + 1 &>/dev/null + if [ $? -ne 0 ]; then + echo -e "[${red}Error${plain}] Please enter a number" + continue + fi + if [[ "$pick" -lt 1 || "$pick" -gt ${#r_ciphers[@]} ]]; then + echo -e "[${red}Error${plain}] Please enter a number between 1 and ${#r_ciphers[@]}" + continue + fi + shadowsockscipher=${r_ciphers[$pick-1]} + elif [ "${selected}" == "3" ]; then + for ((i=1;i<=${#go_ciphers[@]};i++ )); do + hint="${go_ciphers[$i-1]}" + echo -e "${green}${i}${plain}) ${hint}" + done + read -p "Which cipher you'd select(Default: ${go_ciphers[0]}):" pick + [ -z "$pick" ] && pick=1 + expr ${pick} + 1 &>/dev/null + if [ $? -ne 0 ]; then + echo -e "[${red}Error${plain}] Please enter a number" + continue + fi + if [[ "$pick" -lt 1 || "$pick" -gt ${#go_ciphers[@]} ]]; then + echo -e "[${red}Error${plain}] Please enter a number between 1 and ${#go_ciphers[@]}" + continue + fi + shadowsockscipher=${go_ciphers[$pick-1]} + fi + + echo + echo "cipher = ${shadowsockscipher}" + echo + break + done +} + +install_prepare_protocol(){ + while true + do + echo -e "Please select protocol for ${software[${selected}-1]}:" + for ((i=1;i<=${#protocols[@]};i++ )); do + hint="${protocols[$i-1]}" + echo -e "${green}${i}${plain}) ${hint}" + done + read -p "Which protocol you'd select(Default: ${protocols[0]}):" protocol + [ -z "$protocol" ] && protocol=1 + expr ${protocol} + 1 &>/dev/null + if [ $? -ne 0 ]; then + echo -e "[${red}Error${plain}] Please enter a number" + continue + fi + if [[ "$protocol" -lt 1 || "$protocol" -gt ${#protocols[@]} ]]; then + echo -e "[${red}Error${plain}] Please enter a number between 1 and ${#protocols[@]}" + continue + fi + shadowsockprotocol=${protocols[$protocol-1]} + echo + echo "protocol = ${shadowsockprotocol}" + echo + break + done +} + +install_prepare_obfs(){ + while true + do + echo -e "Please select obfs for ${software[${selected}-1]}:" + for ((i=1;i<=${#obfs[@]};i++ )); do + hint="${obfs[$i-1]}" + echo -e "${green}${i}${plain}) ${hint}" + done + read -p "Which obfs you'd select(Default: ${obfs[0]}):" r_obfs + [ -z "$r_obfs" ] && r_obfs=1 + expr ${r_obfs} + 1 &>/dev/null + if [ $? -ne 0 ]; then + echo -e "[${red}Error${plain}] Please enter a number" + continue + fi + if [[ "$r_obfs" -lt 1 || "$r_obfs" -gt ${#obfs[@]} ]]; then + echo -e "[${red}Error${plain}] Please enter a number between 1 and ${#obfs[@]}" + continue + fi + shadowsockobfs=${obfs[$r_obfs-1]} + echo + echo "obfs = ${shadowsockobfs}" + echo + break + done +} + +install_prepare_libev_obfs(){ + if autoconf_version || centosversion 6; then + while true + do + echo -e "Do you want install simple-obfs for ${software[${selected}-1]}? [y/n]" + read -p "(default: n):" libev_obfs + [ -z "$libev_obfs" ] && libev_obfs=n + case "${libev_obfs}" in + y|Y|n|N) + echo + echo "You choose = ${libev_obfs}" + echo + break + ;; + *) + echo -e "[${red}Error${plain}] Please only enter [y/n]" + ;; + esac + done + + if [ "${libev_obfs}" == "y" ] || [ "${libev_obfs}" == "Y" ]; then + while true + do + echo -e "Please select obfs for simple-obfs:" + for ((i=1;i<=${#obfs_libev[@]};i++ )); do + hint="${obfs_libev[$i-1]}" + echo -e "${green}${i}${plain}) ${hint}" + done + read -p "Which obfs you'd select(Default: ${obfs_libev[0]}):" r_libev_obfs + [ -z "$r_libev_obfs" ] && r_libev_obfs=1 + expr ${r_libev_obfs} + 1 &>/dev/null + if [ $? -ne 0 ]; then + echo -e "[${red}Error${plain}] Please enter a number" + continue + fi + if [[ "$r_libev_obfs" -lt 1 || "$r_libev_obfs" -gt ${#obfs_libev[@]} ]]; then + echo -e "[${red}Error${plain}] Please enter a number between 1 and ${#obfs_libev[@]}" + continue + fi + shadowsocklibev_obfs=${obfs_libev[$r_libev_obfs-1]} + echo + echo "obfs = ${shadowsocklibev_obfs}" + echo + break + done + fi + else + echo -e "[${green}Info${plain}] autoconf version is less than 2.67, simple-obfs for ${software[${selected}-1]} installation has been skipped" + fi +} + +install_prepare(){ + + if [[ "${selected}" == "1" || "${selected}" == "3" || "${selected}" == "4" ]]; then + install_prepare_password + install_prepare_port + install_prepare_cipher + if [ "${selected}" == "4" ]; then + install_prepare_libev_obfs + fi + elif [ "${selected}" == "2" ]; then + install_prepare_password + install_prepare_port + install_prepare_cipher + install_prepare_protocol + install_prepare_obfs + fi + + echo + echo "Press any key to start...or Press Ctrl+C to cancel" + char=`get_char` + +} + +install_libsodium(){ + if [ ! -f /usr/lib/libsodium.a ]; then + cd ${cur_dir} + download "${libsodium_file}.tar.gz" "${libsodium_url}" + tar zxf ${libsodium_file}.tar.gz + cd ${libsodium_file} + ./configure --prefix=/usr && make && make install + if [ $? -ne 0 ]; then + echo -e "[${red}Error${plain}] ${libsodium_file} install failed." + install_cleanup + exit 1 + fi + else + echo -e "[${green}Info${plain}] ${libsodium_file} already installed." + fi +} + +install_mbedtls(){ + if [ ! -f /usr/lib/libmbedtls.a ]; then + cd ${cur_dir} + download "${mbedtls_file}-gpl.tgz" "${mbedtls_url}" + tar xf ${mbedtls_file}-gpl.tgz + cd ${mbedtls_file} + make SHARED=1 CFLAGS=-fPIC + make DESTDIR=/usr install + if [ $? -ne 0 ]; then + echo -e "[${red}Error${plain}] ${mbedtls_file} install failed." + install_cleanup + exit 1 + fi + else + echo -e "[${green}Info${plain}] ${mbedtls_file} already installed." + fi +} + +install_shadowsocks_python(){ + cd ${cur_dir} + unzip -q ${shadowsocks_python_file}.zip + if [ $? -ne 0 ];then + echo -e "[${red}Error${plain}] unzip ${shadowsocks_python_file}.zip failed, please check unzip command." + install_cleanup + exit 1 + fi + + cd ${shadowsocks_python_file} + python setup.py install --record /usr/local/shadowsocks_python.log + + if [ -f /usr/bin/ssserver ] || [ -f /usr/local/bin/ssserver ]; then + chmod +x ${shadowsocks_python_init} + local service_name=$(basename ${shadowsocks_python_init}) + if check_sys packageManager yum; then + chkconfig --add ${service_name} + chkconfig ${service_name} on + elif check_sys packageManager apt; then + update-rc.d -f ${service_name} defaults + fi + else + echo + echo -e "[${red}Error${plain}] ${software[0]} install failed." + echo "Please visit: https://teddysun.com/486.html and contact." + install_cleanup + exit 1 + fi +} + +install_shadowsocks_r(){ + cd ${cur_dir} + tar zxf ${shadowsocks_r_file}.tar.gz + mv ${shadowsocks_r_file}/shadowsocks /usr/local/ + if [ -f /usr/local/shadowsocks/server.py ]; then + chmod +x ${shadowsocks_r_init} + local service_name=$(basename ${shadowsocks_r_init}) + if check_sys packageManager yum; then + chkconfig --add ${service_name} + chkconfig ${service_name} on + elif check_sys packageManager apt; then + update-rc.d -f ${service_name} defaults + fi + else + echo + echo -e "[${red}Error${plain}] ${software[1]} install failed." + echo "Please visit; https://teddysun.com/486.html and contact." + install_cleanup + exit 1 + fi +} + +install_shadowsocks_go(){ + cd ${cur_dir} + if is_64bit; then + gzip -d ${shadowsocks_go_file_64}.gz + if [ $? -ne 0 ];then + echo -e "[${red}Error${plain}] Decompress ${shadowsocks_go_file_64}.gz failed." + install_cleanup + exit 1 + fi + mv -f ${shadowsocks_go_file_64} /usr/bin/shadowsocks-server + else + gzip -d ${shadowsocks_go_file_32}.gz + if [ $? -ne 0 ];then + echo -e "[${red}Error${plain}] Decompress ${shadowsocks_go_file_32}.gz failed." + install_cleanup + exit 1 + fi + mv -f ${shadowsocks_go_file_32} /usr/bin/shadowsocks-server + fi + + if [ -f /usr/bin/shadowsocks-server ]; then + chmod +x /usr/bin/shadowsocks-server + chmod +x ${shadowsocks_go_init} + + local service_name=$(basename ${shadowsocks_go_init}) + if check_sys packageManager yum; then + chkconfig --add ${service_name} + chkconfig ${service_name} on + elif check_sys packageManager apt; then + update-rc.d -f ${service_name} defaults + fi + else + echo + echo -e "[${red}Error${plain}] ${software[2]} install failed." + echo "Please visit: https://teddysun.com/486.html and contact." + install_cleanup + exit 1 + fi +} + +install_shadowsocks_libev(){ + cd ${cur_dir} + tar zxf ${shadowsocks_libev_file}.tar.gz + cd ${shadowsocks_libev_file} + ./configure --disable-documentation && make && make install + if [ $? -eq 0 ]; then + chmod +x ${shadowsocks_libev_init} + local service_name=$(basename ${shadowsocks_libev_init}) + if check_sys packageManager yum; then + chkconfig --add ${service_name} + chkconfig ${service_name} on + elif check_sys packageManager apt; then + update-rc.d -f ${service_name} defaults + fi + else + echo + echo -e "[${red}Error${plain}] ${software[3]} install failed." + echo "Please visit: https://teddysun.com/486.html and contact." + install_cleanup + exit 1 + fi +} + +install_shadowsocks_libev_obfs(){ + if [ "${libev_obfs}" == "y" ] || [ "${libev_obfs}" == "Y" ]; then + cd ${cur_dir} + git clone https://github.com/shadowsocks/simple-obfs.git + [ -d simple-obfs ] && cd simple-obfs || echo -e "[${red}Error:${plain}] Failed to git clone simple-obfs." + git submodule update --init --recursive + if centosversion 6; then + if [ ! "$(command -v autoconf268)" ]; then + echo -e "[${green}Info${plain}] Starting install autoconf268..." + yum install -y autoconf268 > /dev/null 2>&1 || echo -e "[${red}Error:${plain}] Failed to install autoconf268." + fi + # replace command autoreconf to autoreconf268 + sed -i 's/autoreconf/autoreconf268/' autogen.sh + # replace #include to #include + sed -i 's@^#include @#include @' src/local.h + sed -i 's@^#include @#include @' src/server.h + fi + ./autogen.sh + ./configure --disable-documentation + make + make install + if [ ! "$(command -v obfs-server)" ]; then + echo -e "[${red}Error${plain}] simple-obfs for ${software[${selected}-1]} install failed." + echo "Please visit: https://teddysun.com/486.html and contact." + install_cleanup + exit 1 + fi + [ -f /usr/local/bin/obfs-server ] && ln -s /usr/local/bin/obfs-server /usr/bin + fi +} + +install_completed_python(){ + clear + ${shadowsocks_python_init} start + echo + echo -e "Congratulations, ${green}${software[0]}${plain} server install completed!" + echo -e "Your Server IP : ${red} $(get_ip) ${plain}" + echo -e "Your Server Port : ${red} ${shadowsocksport} ${plain}" + echo -e "Your Password : ${red} ${shadowsockspwd} ${plain}" + echo -e "Your Encryption Method: ${red} ${shadowsockscipher} ${plain}" +} + +install_completed_r(){ + clear + ${shadowsocks_r_init} start + echo + echo -e "Congratulations, ${green}${software[1]}${plain} server install completed!" + echo -e "Your Server IP : ${red} $(get_ip) ${plain}" + echo -e "Your Server Port : ${red} ${shadowsocksport} ${plain}" + echo -e "Your Password : ${red} ${shadowsockspwd} ${plain}" + echo -e "Your Protocol : ${red} ${shadowsockprotocol} ${plain}" + echo -e "Your obfs : ${red} ${shadowsockobfs} ${plain}" + echo -e "Your Encryption Method: ${red} ${shadowsockscipher} ${plain}" +} + +install_completed_go(){ + clear + ${shadowsocks_go_init} start + echo + echo -e "Congratulations, ${green}${software[2]}${plain} server install completed!" + echo -e "Your Server IP : ${red} $(get_ip) ${plain}" + echo -e "Your Server Port : ${red} ${shadowsocksport} ${plain}" + echo -e "Your Password : ${red} ${shadowsockspwd} ${plain}" + echo -e "Your Encryption Method: ${red} ${shadowsockscipher} ${plain}" +} + +install_completed_libev(){ + clear + ldconfig + ${shadowsocks_libev_init} start + echo + echo -e "Congratulations, ${green}${software[3]}${plain} server install completed!" + echo -e "Your Server IP : ${red} $(get_ip) ${plain}" + echo -e "Your Server Port : ${red} ${shadowsocksport} ${plain}" + echo -e "Your Password : ${red} ${shadowsockspwd} ${plain}" + if [ "$(command -v obfs-server)" ]; then + echo -e "Your obfs : ${red} ${shadowsocklibev_obfs} ${plain}" + fi + echo -e "Your Encryption Method: ${red} ${shadowsockscipher} ${plain}" +} + +qr_generate_python(){ + if [ "$(command -v qrencode)" ]; then + local tmp=$(echo -n "${shadowsockscipher}:${shadowsockspwd}@$(get_ip):${shadowsocksport}" | base64 -w0) + local qr_code="ss://${tmp}" + echo + echo "Your QR Code: (For Shadowsocks Windows, OSX, Android and iOS clients)" + echo -e "${green} ${qr_code} ${plain}" + echo -n "${qr_code}" | qrencode -s8 -o ${cur_dir}/shadowsocks_python_qr.png + echo "Your QR Code has been saved as a PNG file path:" + echo -e "${green} ${cur_dir}/shadowsocks_python_qr.png ${plain}" + fi +} + +qr_generate_r(){ + if [ "$(command -v qrencode)" ]; then + local tmp1=$(echo -n "${shadowsockspwd}" | base64 -w0 | sed 's/=//g;s/\//_/g;s/+/-/g') + local tmp2=$(echo -n "$(get_ip):${shadowsocksport}:${shadowsockprotocol}:${shadowsockscipher}:${shadowsockobfs}:${tmp1}/?obfsparam=" | base64 -w0) + local qr_code="ssr://${tmp2}" + echo + echo "Your QR Code: (For ShadowsocksR Windows, Android clients only)" + echo -e "${green} ${qr_code} ${plain}" + echo -n "${qr_code}" | qrencode -s8 -o ${cur_dir}/shadowsocks_r_qr.png + echo "Your QR Code has been saved as a PNG file path:" + echo -e "${green} ${cur_dir}/shadowsocks_r_qr.png ${plain}" + fi +} + +qr_generate_go(){ + if [ "$(command -v qrencode)" ]; then + local tmp=$(echo -n "${shadowsockscipher}:${shadowsockspwd}@$(get_ip):${shadowsocksport}" | base64 -w0) + local qr_code="ss://${tmp}" + echo + echo "Your QR Code: (For Shadowsocks Windows, OSX, Android and iOS clients)" + echo -e "${green} ${qr_code} ${plain}" + echo -n "${qr_code}" | qrencode -s8 -o ${cur_dir}/shadowsocks_go_qr.png + echo "Your QR Code has been saved as a PNG file path:" + echo -e "${green} ${cur_dir}/shadowsocks_go_qr.png ${plain}" + fi +} + +qr_generate_libev(){ + if [ "$(command -v qrencode)" ]; then + local tmp=$(echo -n "${shadowsockscipher}:${shadowsockspwd}@$(get_ip):${shadowsocksport}" | base64 -w0) + local qr_code="ss://${tmp}" + echo + echo "Your QR Code: (For Shadowsocks Windows, OSX, Android and iOS clients)" + echo -e "${green} ${qr_code} ${plain}" + echo -n "${qr_code}" | qrencode -s8 -o ${cur_dir}/shadowsocks_libev_qr.png + echo "Your QR Code has been saved as a PNG file path:" + echo -e "${green} ${cur_dir}/shadowsocks_libev_qr.png ${plain}" + fi +} + +install_main(){ + install_libsodium + if ! ldconfig -p | grep -wq "/usr/lib"; then + echo "/usr/lib" > /etc/ld.so.conf.d/lib.conf + fi + ldconfig + + if [ "${selected}" == "1" ]; then + install_shadowsocks_python + install_completed_python + qr_generate_python + elif [ "${selected}" == "2" ]; then + install_shadowsocks_r + install_completed_r + qr_generate_r + elif [ "${selected}" == "3" ]; then + install_shadowsocks_go + install_completed_go + qr_generate_go + elif [ "${selected}" == "4" ]; then + install_mbedtls + install_shadowsocks_libev + install_shadowsocks_libev_obfs + install_completed_libev + qr_generate_libev + fi + + echo + echo "Welcome to visit: https://teddysun.com/486.html" + echo "Enjoy it!" + echo +} + +install_cleanup(){ + cd ${cur_dir} + rm -rf simple-obfs + rm -rf ${libsodium_file} ${libsodium_file}.tar.gz + rm -rf ${mbedtls_file} ${mbedtls_file}-gpl.tgz + rm -rf ${shadowsocks_python_file} ${shadowsocks_python_file}.zip + rm -rf ${shadowsocks_r_file} ${shadowsocks_r_file}.tar.gz + rm -rf ${shadowsocks_go_file_64}.gz ${shadowsocks_go_file_32}.gz + rm -rf ${shadowsocks_libev_file} ${shadowsocks_libev_file}.tar.gz +} + +install_shadowsocks(){ + disable_selinux + install_select + install_prepare + install_dependencies + download_files + config_shadowsocks + if check_sys packageManager yum; then + config_firewall + fi + install_main + install_cleanup +} + +uninstall_shadowsocks_python(){ + printf "Are you sure uninstall ${red}${software[0]}${plain}? [y/n]\n" + read -p "(default: n):" answer + [ -z ${answer} ] && answer="n" + if [ "${answer}" == "y" ] || [ "${answer}" == "Y" ]; then + ${shadowsocks_python_init} status > /dev/null 2>&1 + if [ $? -eq 0 ]; then + ${shadowsocks_python_init} stop + fi + local service_name=$(basename ${shadowsocks_python_init}) + if check_sys packageManager yum; then + chkconfig --del ${service_name} + elif check_sys packageManager apt; then + update-rc.d -f ${service_name} remove + fi + + rm -fr $(dirname ${shadowsocks_python_config}) + rm -f ${shadowsocks_python_init} + rm -f /var/log/shadowsocks.log + if [ -f /usr/local/shadowsocks_python.log ]; then + cat /usr/local/shadowsocks_python.log | xargs rm -rf + rm -f /usr/local/shadowsocks_python.log + fi + echo -e "[${green}Info${plain}] ${software[0]} uninstall success" + else + echo + echo -e "[${green}Info${plain}] ${software[0]} uninstall cancelled, nothing to do..." + echo + fi +} + +uninstall_shadowsocks_r(){ + printf "Are you sure uninstall ${red}${software[1]}${plain}? [y/n]\n" + read -p "(default: n):" answer + [ -z ${answer} ] && answer="n" + if [ "${answer}" == "y" ] || [ "${answer}" == "Y" ]; then + ${shadowsocks_r_init} status > /dev/null 2>&1 + if [ $? -eq 0 ]; then + ${shadowsocks_r_init} stop + fi + local service_name=$(basename ${shadowsocks_r_init}) + if check_sys packageManager yum; then + chkconfig --del ${service_name} + elif check_sys packageManager apt; then + update-rc.d -f ${service_name} remove + fi + rm -fr $(dirname ${shadowsocks_r_config}) + rm -f ${shadowsocks_r_init} + rm -f /var/log/shadowsocks.log + rm -fr /usr/local/shadowsocks + echo -e "[${green}Info${plain}] ${software[1]} uninstall success" + else + echo + echo -e "[${green}Info${plain}] ${software[1]} uninstall cancelled, nothing to do..." + echo + fi +} + +uninstall_shadowsocks_go(){ + printf "Are you sure uninstall ${red}${software[2]}${plain}? [y/n]\n" + read -p "(default: n):" answer + [ -z ${answer} ] && answer="n" + if [ "${answer}" == "y" ] || [ "${answer}" == "Y" ]; then + ${shadowsocks_go_init} status > /dev/null 2>&1 + if [ $? -eq 0 ]; then + ${shadowsocks_go_init} stop + fi + local service_name=$(basename ${shadowsocks_go_init}) + if check_sys packageManager yum; then + chkconfig --del ${service_name} + elif check_sys packageManager apt; then + update-rc.d -f ${service_name} remove + fi + rm -fr $(dirname ${shadowsocks_go_config}) + rm -f ${shadowsocks_go_init} + rm -f /usr/bin/shadowsocks-server + echo -e "[${green}Info${plain}] ${software[2]} uninstall success" + else + echo + echo -e "[${green}Info${plain}] ${software[2]} uninstall cancelled, nothing to do..." + echo + fi +} + +uninstall_shadowsocks_libev(){ + printf "Are you sure uninstall ${red}${software[3]}${plain}? [y/n]\n" + read -p "(default: n):" answer + [ -z ${answer} ] && answer="n" + if [ "${answer}" == "y" ] || [ "${answer}" == "Y" ]; then + ${shadowsocks_libev_init} status > /dev/null 2>&1 + if [ $? -eq 0 ]; then + ${shadowsocks_libev_init} stop + fi + local service_name=$(basename ${shadowsocks_libev_init}) + if check_sys packageManager yum; then + chkconfig --del ${service_name} + elif check_sys packageManager apt; then + update-rc.d -f ${service_name} remove + fi + rm -fr $(dirname ${shadowsocks_libev_config}) + rm -f /usr/local/bin/ss-local + rm -f /usr/local/bin/ss-tunnel + rm -f /usr/local/bin/ss-server + rm -f /usr/local/bin/ss-manager + rm -f /usr/local/bin/ss-redir + rm -f /usr/local/bin/ss-nat + rm -f /usr/local/bin/obfs-local + rm -f /usr/local/bin/obfs-server + rm -f /usr/local/lib/libshadowsocks-libev.a + rm -f /usr/local/lib/libshadowsocks-libev.la + rm -f /usr/local/include/shadowsocks.h + rm -f /usr/local/lib/pkgconfig/shadowsocks-libev.pc + rm -f /usr/local/share/man/man1/ss-local.1 + rm -f /usr/local/share/man/man1/ss-tunnel.1 + rm -f /usr/local/share/man/man1/ss-server.1 + rm -f /usr/local/share/man/man1/ss-manager.1 + rm -f /usr/local/share/man/man1/ss-redir.1 + rm -f /usr/local/share/man/man1/ss-nat.1 + rm -f /usr/local/share/man/man8/shadowsocks-libev.8 + rm -fr /usr/local/share/doc/shadowsocks-libev + rm -f ${shadowsocks_libev_init} + echo -e "[${green}Info${plain}] ${software[3]} uninstall success" + else + echo + echo -e "[${green}Info${plain}] ${software[3]} uninstall cancelled, nothing to do..." + echo + fi +} + +uninstall_shadowsocks(){ + while true + do + echo "Which Shadowsocks server you want to uninstall?" + for ((i=1;i<=${#software[@]};i++ )); do + hint="${software[$i-1]}" + echo -e "${green}${i}${plain}) ${hint}" + done + read -p "Please enter a number [1-4]:" un_select + case "${un_select}" in + 1|2|3|4) + echo + echo "You choose = ${software[${un_select}-1]}" + echo + break + ;; + *) + echo -e "[${red}Error${plain}] Please only enter a number [1-4]" + ;; + esac + done + + if [ "${un_select}" == "1" ]; then + if [ -f ${shadowsocks_python_init} ]; then + uninstall_shadowsocks_python + else + echo -e "[${red}Error${plain}] ${software[${un_select}-1]} not installed, please check it and try again." + echo + exit 1 + fi + elif [ "${un_select}" == "2" ]; then + if [ -f ${shadowsocks_r_init} ]; then + uninstall_shadowsocks_r + else + echo -e "[${red}Error${plain}] ${software[${un_select}-1]} not installed, please check it and try again." + echo + exit 1 + fi + elif [ "${un_select}" == "3" ]; then + if [ -f ${shadowsocks_go_init} ]; then + uninstall_shadowsocks_go + else + echo -e "[${red}Error${plain}] ${software[${un_select}-1]} not installed, please check it and try again." + echo + exit 1 + fi + elif [ "${un_select}" == "4" ]; then + if [ -f ${shadowsocks_libev_init} ]; then + uninstall_shadowsocks_libev + else + echo -e "[${red}Error${plain}] ${software[${un_select}-1]} not installed, please check it and try again." + echo + exit 1 + fi + fi +} + +# Initialization step +action=$1 +[ -z $1 ] && action=install +case "${action}" in + install|uninstall) + ${action}_shadowsocks + ;; + *) + echo "Arguments error! [${action}]" + echo "Usage: $(basename $0) [install|uninstall]" + ;; +esac diff --git "a/linux/source\345\221\275\344\273\244.md" "b/linux/source\345\221\275\344\273\244.md" new file mode 100644 index 0000000..d452dcb --- /dev/null +++ "b/linux/source\345\221\275\344\273\244.md" @@ -0,0 +1,29 @@ +## 定义 + +source命令也称为“点命令”,也就是一个点符号(.),是bash的内部命令。 + +## 功能 + +使Shell读入指定的Shell程序文件并依次执行文件中的所有语句 source命令通常用于重新执行刚修改的初始化文件,使之立即生效,而不必注销并重新登录。 + +## 用法 + +`source filename` 或` . filename` + + source命令(从 C Shell 而来)是bash shell的内置命令;点命令(.),就是个点符号(从Bourne Shell而来)是source的另一名称。 + + + +## source filename 与 sh filename 及./filename执行脚本的区别在那里呢? + +1.当shell脚本具有可执行权限时,用sh filename与./filename执行脚本是没有区别得。./filename是因为当前目录没有在PATH中,所有"."是用来表示当前目录的。 + +2.sh filename 重新建立一个子shell,在子shell中执行脚本里面的语句,该子shell继承父shell的环境变量,但子shell新建的、改变的变量不会被带回父shell,除非使用export。 + +3.source filename:这个命令其实只是简单地读取脚本里面的语句依次在当前shell里面执行,没有建立新的子shell。那么脚本里面所有新建、改变变量的语句都会保存在当前shell里面。 + + + +## 引用 + +[source命令](https://www.cnblogs.com/wqbin/p/10244063.html) \ No newline at end of file diff --git "a/linux/\346\237\245\347\234\213\345\215\240\347\224\250\347\253\257\345\217\243\347\232\204\350\277\233\347\250\213.md" "b/linux/\346\237\245\347\234\213\345\215\240\347\224\250\347\253\257\345\217\243\347\232\204\350\277\233\347\250\213.md" new file mode 100644 index 0000000..069a74f --- /dev/null +++ "b/linux/\346\237\245\347\234\213\345\215\240\347\224\250\347\253\257\345\217\243\347\232\204\350\277\233\347\250\213.md" @@ -0,0 +1,63 @@ +## 查看端口占用情况 + +`lsof -i`:用于查看所有IPV4/6端口占用情况 + +`lsof -i:port`:查看指定端口的占用情况 + +```shell +[root@yw ~]# lsof -i:80 +COMMAND PID USER FD TYPE DEVICE SIZE/OFF NODE NAME +nginx 1124 root 9u IPv4 8842 0t0 TCP *:http (LISTEN) +nginx 1125 root 9u IPv4 8842 0t0 TCP *:http (LISTEN) +nginx 1126 root 9u IPv4 8842 0t0 TCP *:http (LISTEN) +nginx 1127 root 9u IPv4 8842 0t0 TCP *:http (LISTEN) +nginx 1128 root 9u IPv4 8842 0t0 TCP *:http (LISTEN) +java 9535 root 149u IPv4 148223160 0t0 TCP 192.168.10.110:64885->192.168.10.110:http (CLOSE_WAIT) +``` + +**列名解释** + +> COMMAND:进程名称 +> PID:进程标示符 +> USER:用户 +> FD:文件描述符,应用程序通过文件描述符识别该文件,如cwd、txt等 +> TYPE:文件类型,如DIR +> DEVICE:指定磁盘的名称 +> SIZE/OFF:文件大小 +> NODE:索引节点(文件在磁盘上的标识) +> NAME:打开文件的确切名称 + + +## 查看TCP、UDP的端口和进程情况 + +`netstat -tunlp`用于显示tcp、udp的端口和进程等相关情况。 + +> -t:(tcp)仅显示tcp相关选项 +> +> -u:(udp)仅显示udp相关选项 +> +> -n:拒绝显示别名,能显示数字的全部转换为数字 +> +> -l:仅列出有在 Listen(监听) 的服务状态 +> +> -p:显示建立相关链接的程序名 + +```shell +[root@yw ~]# netstat -tunlp +Active Internet connections (only servers) +Proto Recv-Q Send-Q Local Address Foreign Address State PID/Program name +tcp 0 0 0.0.0.0:445 0.0.0.0:* LISTEN 1179/smbd +tcp 0 0 0.0.0.0:8001 0.0.0.0:* LISTEN 10600/java +``` + +从上面的日志中可以看出端口号以及PID。 + +`netstat -tunlp | grep 80` + +```shell +[root@yw ~]# netstat -tunlp | grep 80 +tcp 0 0 0.0.0.0:80 0.0.0.0:* LISTEN 1124/nginx +``` + + + diff --git "a/linux/\350\231\232\346\213\237\346\234\272\350\256\276\347\275\256\351\235\231\346\200\201ip.md" "b/linux/\350\231\232\346\213\237\346\234\272\350\256\276\347\275\256\351\235\231\346\200\201ip.md" new file mode 100644 index 0000000..2536b13 --- /dev/null +++ "b/linux/\350\231\232\346\213\237\346\234\272\350\256\276\347\275\256\351\235\231\346\200\201ip.md" @@ -0,0 +1,66 @@ +## Linux虚拟机设置静态ip + +### 1、获取网关地址 + +该文档中使用**VMware**创建linux虚拟机,所有我们的网关地址从**VMware**中进行获取。 + +1. 打开VMware主界面 +2. 点击`编辑`->`虚拟网络编辑器` +3. 单击`NAT 模式`后可以查看最下边一行为:子网IP和子网掩码 +4. 单击`NAT 设置`,查看或修改`网关 IP` + +该文档中`网关IP`使用192.168.73.2 + + + +### 2、配置静态ip + +使用`ifconfig`命令查看当前虚拟机的网卡信息 + +``` +ens33: flags=4163 mtu 1500 + inet 192.168.73.151 netmask 255.255.255.0 broadcast 192.168.73.255 + inet6 fe80::e24c:de6e:faf9:312e prefixlen 64 scopeid 0x20 + ether 00:0c:29:b8:66:21 txqueuelen 1000 (Ethernet) + RX packets 254 bytes 24518 (23.9 KiB) + RX errors 0 dropped 0 overruns 0 frame 0 + TX packets 248 bytes 25016 (24.4 KiB) + TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 + +lo: flags=73 mtu 65536 + inet 127.0.0.1 netmask 255.0.0.0 + inet6 ::1 prefixlen 128 scopeid 0x10 + loop txqueuelen 1000 (Local Loopback) + RX packets 0 bytes 0 (0.0 B) + RX errors 0 dropped 0 overruns 0 frame 0 + TX packets 0 bytes 0 (0.0 B) + TX errors 0 dropped 0 overruns 0 carrier 0 collisions 0 +``` + +由上可知当前虚拟机的网卡为`ens33` + +使用`vim`修改`/etc/sysconfig/network-scripts/ifcfg-ens33` + +``` +ONBOOT="yes" +BOOTPROTO="static" +IPADDR="192.168.73.151" +NETMASK="255.255.255.0" +GATEWAY="192.168.73.2" +``` + + + +### 3、配置DNS + +修改`/etc/resolv.conf` + +``` +nameserver 192.168.73.2 +``` + + + +### 4、 使配置生效 + +重启主机或使用`service network restart`即可使配置生效。 \ No newline at end of file diff --git "a/lua/\346\220\255\345\273\272\345\274\200\345\217\221\347\216\257\345\242\203.md" "b/lua/\346\220\255\345\273\272\345\274\200\345\217\221\347\216\257\345\242\203.md" new file mode 100644 index 0000000..332f13b --- /dev/null +++ "b/lua/\346\220\255\345\273\272\345\274\200\345\217\221\347\216\257\345\242\203.md" @@ -0,0 +1,32 @@ +# 搭建Lua开发环境 + +## 下载 Lua + +进入[lua官网](http://www.lua.org)下载windows办二进制包 + +## 配置环境变量 + +将下载的二进制包解压后放入文件夹中,如:`C:\Program Files\Lua`,同时将该路径配置到**path**环境变量中。 + + + +## Sublime配置Lua环境 + +1、打开sublime,选择`Tools -> Build System -> New Build System` + +2、在打开的文件中输入一下内容: + +```json +{ + "cmd": ["lua","$file"], + "file_regex":"^(...*?):([0-9]*):?([0-9]*)", + "selector":"source.lua" +} +``` + +并将该文件保存到`C:\Users\zh\AppData\Roaming\Sublime Text 3\Packages\User`目录下,名称为`WindowsLua.sublime-build` + +3、重启sublime,选择`Tools -> Build System -> WindowsLua`,lua环境完成 + +4、使用**F7**或**F8**可以调试程序 + diff --git a/springboot/images/springboot_1_01.png b/springboot/images/springboot_1_01.png new file mode 100644 index 0000000..fd9e4fc Binary files /dev/null and b/springboot/images/springboot_1_01.png differ diff --git a/springboot/images/springboot_1_02.png b/springboot/images/springboot_1_02.png new file mode 100644 index 0000000..cf2d09a Binary files /dev/null and b/springboot/images/springboot_1_02.png differ diff --git a/springboot/images/springboot_1_03.png b/springboot/images/springboot_1_03.png new file mode 100644 index 0000000..6858d2d Binary files /dev/null and b/springboot/images/springboot_1_03.png differ diff --git a/springboot/images/springboot_2_01.png b/springboot/images/springboot_2_01.png new file mode 100644 index 0000000..35791e4 Binary files /dev/null and b/springboot/images/springboot_2_01.png differ diff --git a/springboot/images/springboot_3_01.png b/springboot/images/springboot_3_01.png new file mode 100644 index 0000000..9c7d1b1 Binary files /dev/null and b/springboot/images/springboot_3_01.png differ diff --git a/springboot/images/springboot_3_02.png b/springboot/images/springboot_3_02.png new file mode 100644 index 0000000..46d1f6d Binary files /dev/null and b/springboot/images/springboot_3_02.png differ diff --git a/springboot/images/springboot_3_03.png b/springboot/images/springboot_3_03.png new file mode 100644 index 0000000..b2e192a Binary files /dev/null and b/springboot/images/springboot_3_03.png differ diff --git a/springboot/images/springboot_3_04.png b/springboot/images/springboot_3_04.png new file mode 100644 index 0000000..62ce96a Binary files /dev/null and b/springboot/images/springboot_3_04.png differ diff --git a/springboot/images/springboot_4_01.png b/springboot/images/springboot_4_01.png new file mode 100644 index 0000000..24fb8cd Binary files /dev/null and b/springboot/images/springboot_4_01.png differ diff --git a/springboot/images/springboot_4_02.png b/springboot/images/springboot_4_02.png new file mode 100644 index 0000000..93c077e Binary files /dev/null and b/springboot/images/springboot_4_02.png differ diff --git a/springboot/images/springboot_4_03.png b/springboot/images/springboot_4_03.png new file mode 100644 index 0000000..57f0970 Binary files /dev/null and b/springboot/images/springboot_4_03.png differ diff --git a/springboot/images/springboot_4_04.png b/springboot/images/springboot_4_04.png new file mode 100644 index 0000000..35aa555 Binary files /dev/null and b/springboot/images/springboot_4_04.png differ diff --git a/springboot/images/springboot_4_05.png b/springboot/images/springboot_4_05.png new file mode 100644 index 0000000..de870b1 Binary files /dev/null and b/springboot/images/springboot_4_05.png differ diff --git a/springboot/images/springboot_4_06.png b/springboot/images/springboot_4_06.png new file mode 100644 index 0000000..71dc6cd Binary files /dev/null and b/springboot/images/springboot_4_06.png differ diff --git a/springboot/images/springboot_4_07.png b/springboot/images/springboot_4_07.png new file mode 100644 index 0000000..95a55ef Binary files /dev/null and b/springboot/images/springboot_4_07.png differ diff --git a/springboot/images/springboot_4_08.png b/springboot/images/springboot_4_08.png new file mode 100644 index 0000000..9b1b409 Binary files /dev/null and b/springboot/images/springboot_4_08.png differ diff --git a/springboot/images/springboot_4_09.png b/springboot/images/springboot_4_09.png new file mode 100644 index 0000000..8813c26 Binary files /dev/null and b/springboot/images/springboot_4_09.png differ diff --git a/springboot/images/springboot_4_10.png b/springboot/images/springboot_4_10.png new file mode 100644 index 0000000..368e33c Binary files /dev/null and b/springboot/images/springboot_4_10.png differ diff --git a/springboot/images/springboot_4_11.png b/springboot/images/springboot_4_11.png new file mode 100644 index 0000000..e6fbf2c Binary files /dev/null and b/springboot/images/springboot_4_11.png differ diff --git a/springboot/images/springboot_4_12.png b/springboot/images/springboot_4_12.png new file mode 100644 index 0000000..5ac2c5a Binary files /dev/null and b/springboot/images/springboot_4_12.png differ diff --git a/springboot/images/springboot_4_13.png b/springboot/images/springboot_4_13.png new file mode 100644 index 0000000..bd44e3a Binary files /dev/null and b/springboot/images/springboot_4_13.png differ diff --git a/springboot/images/springboot_4_14.png b/springboot/images/springboot_4_14.png new file mode 100644 index 0000000..9a234f5 Binary files /dev/null and b/springboot/images/springboot_4_14.png differ diff --git a/springboot/images/springboot_4_15.png b/springboot/images/springboot_4_15.png new file mode 100644 index 0000000..7d89211 Binary files /dev/null and b/springboot/images/springboot_4_15.png differ diff --git a/springboot/images/springboot_5_01.png b/springboot/images/springboot_5_01.png new file mode 100644 index 0000000..e06088a Binary files /dev/null and b/springboot/images/springboot_5_01.png differ diff --git a/springboot/images/springboot_5_02.png b/springboot/images/springboot_5_02.png new file mode 100644 index 0000000..2c2d8e0 Binary files /dev/null and b/springboot/images/springboot_5_02.png differ diff --git a/springboot/images/springboot_5_03.png b/springboot/images/springboot_5_03.png new file mode 100644 index 0000000..8b1c5cd Binary files /dev/null and b/springboot/images/springboot_5_03.png differ diff --git a/springboot/images/springboot_6_01.png b/springboot/images/springboot_6_01.png new file mode 100644 index 0000000..85a8e84 Binary files /dev/null and b/springboot/images/springboot_6_01.png differ diff --git a/springboot/images/springboot_6_02.png b/springboot/images/springboot_6_02.png new file mode 100644 index 0000000..62410d7 Binary files /dev/null and b/springboot/images/springboot_6_02.png differ diff --git a/springboot/images/springboot_7_01.png b/springboot/images/springboot_7_01.png new file mode 100644 index 0000000..d94c0a4 Binary files /dev/null and b/springboot/images/springboot_7_01.png differ diff --git a/springboot/images/springboot_7_02.png b/springboot/images/springboot_7_02.png new file mode 100644 index 0000000..fd7d0e6 Binary files /dev/null and b/springboot/images/springboot_7_02.png differ diff --git "a/springboot/\344\270\212\344\274\240\345\244\247\346\226\207\344\273\266\346\212\245\351\224\231.md" "b/springboot/\344\270\212\344\274\240\345\244\247\346\226\207\344\273\266\346\212\245\351\224\231.md" new file mode 100644 index 0000000..a2b9141 --- /dev/null +++ "b/springboot/\344\270\212\344\274\240\345\244\247\346\226\207\344\273\266\346\212\245\351\224\231.md" @@ -0,0 +1,16 @@ +### 原因 + +SpringBoot 默认配置的上传文件的大小为1MB,超过配置的文件进行上传则会报错。并且SpringBoot2的配置不同于SpringBoot1, SpringBoot2已将上传文件从`http`转换为`servlet`的属性。 + +### 解决方案 + +在`application.yml`中配置如下两条配置 + +```yaml +spring: + servlet: + multipart: + max-file-size: 10MB + max-request-size: 15MB +``` + diff --git "a/springboot/\344\277\256\346\224\271JSON\350\275\254\346\215\242\345\231\250.md" "b/springboot/\344\277\256\346\224\271JSON\350\275\254\346\215\242\345\231\250.md" new file mode 100644 index 0000000..2caa463 --- /dev/null +++ "b/springboot/\344\277\256\346\224\271JSON\350\275\254\346\215\242\345\231\250.md" @@ -0,0 +1,26 @@ +## 修改预定义的JSON转换器 + + springboot2.x版本修改了以前设置转换器的方式 + +### 原方式: + +通过实现`org.springframework.web.servlet.config.annotation.WebMvcConfigurer`接口,实现`configureMessageConverters(List> converters)`方法后,将自定义的转换器添加到`converters`集合的最后面即可。 + +### 现方式: + +现在需要提供一个`org.springframework.boot.autoconfigure.http.HttpMessageConverters`类的bean到spring容器中,用于将预定义的转换器集合替换掉才行。 + +对比以前的方式,现在的方式更加灵活,spring也不需要从源转换器集合中寻找,直接使用用户提供的集合。 + +实现方式 + +```java + @Bean + public HttpMessageConverters httpMessageConverters() { + // 自定义json格式化 + MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter( + JsonUtil.getMapper()); + return new HttpMessageConverters(converter); + } +``` + diff --git "a/springboot/\345\205\245\351\227\250/1_\345\205\245\351\227\250.md" "b/springboot/\345\205\245\351\227\250/1_\345\205\245\351\227\250.md" new file mode 100644 index 0000000..dfa624f --- /dev/null +++ "b/springboot/\345\205\245\351\227\250/1_\345\205\245\351\227\250.md" @@ -0,0 +1,272 @@ +# 一、Spring Boot 入门 + +## 1、Spring Boot 简介 + +> 简化Spring应用开发的一个框架; +> +> 整个Spring技术栈的一个大整合; +> +> J2EE开发的一站式解决方案; + +## 2、微服务 + +2014,martin fowler + +微服务:架构风格(服务微化) + +一个应用应该是一组小型服务;可以通过HTTP的方式进行互通; + +单体应用:ALL IN ONE + +微服务:每一个功能元素最终都是一个可独立替换和独立升级的软件单元; + +[详细参照微服务文档](https://martinfowler.com/articles/microservices.html#MicroservicesAndSoa) + + + +## 3、环境准备 + +环境约束 + +–jdk1.8:Spring Boot 推荐jdk1.7及以上;java version "1.8.0_112" + +–maven3.x:maven 3.3以上版本;Apache Maven 3.3.9 + +–IntelliJIDEA2017:IntelliJ IDEA 2017.2.2 x64、STS + +–SpringBoot 1.5.9.RELEASE:1.5.9; + + + +### 1、MAVEN设置; + +给maven 的settings.xml配置文件的profiles标签添加 + +```xml + + jdk-1.8 + + true + 1.8 + + + 1.8 + 1.8 + 1.8 + + +``` + +### 2、IDEA设置 + +整合maven进来; + +![idea设置](images/springboot_1_01.png) + +![images/](images/springboot_1_02.png) + +## 4、Spring Boot HelloWorld + +一个功能: + +浏览器发送hello请求,服务器接受请求并处理,响应Hello World字符串; + + +### 1、创建一个maven工程;(jar) + +### 2、导入spring boot相关的依赖 + +```xml + + org.springframework.boot + spring-boot-starter-parent + 1.5.9.RELEASE + + + + org.springframework.boot + spring-boot-starter-web + + +``` + +### 3、编写一个主程序;启动Spring Boot应用 + +```java +/** + * @SpringBootApplication 来标注一个主程序类,说明这是一个Spring Boot应用 + */ +@SpringBootApplication +public class HelloWorldMainApplication { + public static void main(String[] args) { + // Spring应用启动起来 + SpringApplication.run(HelloWorldMainApplication.class,args); + } +} +``` + +### 4、编写相关的Controller、Service + +```java +@Controller +public class HelloController { + @ResponseBody + @RequestMapping("/hello") + public String hello(){ + return "Hello World!"; + } +} + +``` + +### 5、运行主程序测试 + +### 6、简化部署 + +```xml + + + + + org.springframework.boot + spring-boot-maven-plugin + + + +``` + +将这个应用打成jar包,直接使用java -jar的命令进行执行; + +## 5、Hello World探究 + +### 1、POM文件 + +#### 1、父项目 + +```xml + + org.springframework.boot + spring-boot-starter-parent + 1.5.9.RELEASE + + +他的父项目是 + + org.springframework.boot + spring-boot-dependencies + 1.5.9.RELEASE + ../../spring-boot-dependencies + +他来真正管理Spring Boot应用里面的所有依赖版本; + +``` + +Spring Boot的版本仲裁中心; + +以后我们导入依赖默认是不需要写版本;(没有在dependencies里面管理的依赖自然需要声明版本号) + +#### 2、启动器 + +```xml + + org.springframework.boot + spring-boot-starter-web + +``` + +**spring-boot-starter**: + +spring-boot-starter:spring-boot场景启动器;帮我们导入了web模块正常运行所依赖的组件; + +Spring Boot将所有的功能场景都抽取出来,做成一个个的starters(启动器),只需要在项目里面引入这些starter相关场景的所有依赖都会导入进来。要用什么功能就导入什么场景的启动器 + +### 2、主程序类,主入口类 + +```java +/** + * @SpringBootApplication 来标注一个主程序类,说明这是一个Spring Boot应用 + */ +@SpringBootApplication +public class HelloWorldMainApplication { + public static void main(String[] args) { + // Spring应用启动起来 + SpringApplication.run(HelloWorldMainApplication.class,args); + } +} + +``` + +@**SpringBootApplication**: Spring Boot应用标注在某个类上说明这个类是SpringBoot的主配置类,SpringBoot就应该运行这个类的main方法来启动SpringBoot应用; + +```java +@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**:Spring Boot的配置类; + +标注在某个类上,表示这是一个Spring Boot的配置类; + +@**Configuration**:配置类上来标注这个注解; + +配置类 ----- 配置文件;配置类也是容器中的一个组件;@Component + +@**EnableAutoConfiguration**:开启自动配置功能; + +以前我们需要配置的东西,Spring Boot帮我们自动配置;@**EnableAutoConfiguration**告诉SpringBoot开启自动配置功能;这样自动配置才能生效; + +```java +@AutoConfigurationPackage +@Import(EnableAutoConfigurationImportSelector.class) +public @interface EnableAutoConfiguration { +``` + +@**AutoConfigurationPackage**:自动配置包 + +@**Import**(AutoConfigurationPackages.Registrar.class): + +Spring的底层注解@Import,给容器中导入一个组件;导入的组件由AutoConfigurationPackages.Registrar.class; + +==将主配置类(@SpringBootApplication标注的类)的所在包及下面所有子包里面的所有组件扫描到Spring容器;== + +@**Import**(EnableAutoConfigurationImportSelector.class); + +给容器中导入组件? + +**EnableAutoConfigurationImportSelector**:导入哪些组件的选择器; + +将所有需要导入的组件以全类名的方式返回;这些组件就会被添加到容器中; + +会给容器中导入非常多的自动配置类(xxxAutoConfiguration);就是给容器中导入这个场景需要的所有组件,并配置好这些组件; ![自动配置类](images/springboot_1_03.png) + +有了自动配置类,免去了我们手动编写配置注入功能组件等的工作; + +SpringFactoriesLoader.loadFactoryNames(EnableAutoConfiguration.class,classLoader); + +==Spring Boot在启动的时候从类路径下的META-INF/spring.factories中获取EnableAutoConfiguration指定的值,将这些值作为自动配置类导入到容器中,自动配置类就生效,帮我们进行自动配置工作;==以前我们需要自己配置的东西,自动配置类都帮我们; + +J2EE的整体整合解决方案和自动配置都在spring-boot-autoconfigure-1.5.9.RELEASE.jar; + +## 6、使用Spring Initializer快速创建Spring Boot项目 + +### 1、IDEA:使用 Spring Initializer快速创建项目 + +IDE都支持使用Spring的项目创建向导快速创建一个Spring Boot项目; + +选择我们需要的模块;向导会联网创建Spring Boot项目; + +默认生成的Spring Boot项目; + +- 主程序已经生成好了,我们只需要我们自己的逻辑 +- resources文件夹中目录结构 + - static:保存所有的静态资源; js css images; + - templates:保存所有的模板页面;(Spring Boot默认jar包使用嵌入式的Tomcat,默认不支持JSP页面);可以使用模板引擎(freemarker、thymeleaf); + - application.properties:Spring Boot应用的配置文件;可以修改一些默认设置; \ No newline at end of file diff --git "a/springboot/\345\205\245\351\227\250/2_\351\205\215\347\275\256.md" "b/springboot/\345\205\245\351\227\250/2_\351\205\215\347\275\256.md" new file mode 100644 index 0000000..ff75170 --- /dev/null +++ "b/springboot/\345\205\245\351\227\250/2_\351\205\215\347\275\256.md" @@ -0,0 +1,580 @@ +# 二、配置文件 + +## 1、配置文件 + +SpringBoot使用一个全局的配置文件,配置文件名是固定的; + +• application.properties + +• application.yml + + +配置文件的作用:修改SpringBoot自动配置的默认值;SpringBoot在底层都给我们自动配置好; + +YAML(YAML Ain't Markup Language) + +* YAML A Markup Language:是一个标记语言 + +* YAML isn't Markup Language:不是一个标记语言; + +标记语言: + +以前的配置文件;大多都使用的是 **xxxx.xml**文件; + +YAML:**以数据为中心**,比json、xml等更适合做配置文件; + +**YAML** + +```yaml +server: + port: 8081 +``` + +**XML**: + +```xml + + 8081 + +``` + +## 2、YAML语法: + +### 1、基本语法 + +k:(空格)v:表示一对键值对(空格必须有); + +以**空格**的缩进来控制层级关系;只要是左对齐的一列数据,都是同一个层级的 + +```yaml +server: + port: 8081 + path: /hello +``` + +属性和值也是大小写敏感; + +### 2、值的写法 + +#### 字面量:普通的值(数字,字符串,布尔) + +k: v:字面直接来写; + +字符串默认不用加上单引号或者双引号; + +"":双引号;不会转义字符串里面的特殊字符;特殊字符会作为本身想表示的意思 + +name: "zhangsan \n lisi":输出;zhangsan 换行 lisi + +'':单引号;会转义特殊字符,特殊字符最终只是一个普通的字符串数据 + +name: ‘zhangsan \n lisi’:输出;zhangsan \n lisi + + +#### 对象、Map(属性和值)(键值对): + +k: v:在下一行来写对象的属性和值的关系;注意缩进 + +对象还是k: v的方式 + +```yaml +friends: + lastName: zhangsan + age: 20 +``` + +行内写法: + +```yaml +friends: {lastName: zhangsan,age: 18} +``` + +#### 数组(List、Set): + +用- 值表示数组中的一个元素 + +```yaml +pets: + - cat + - dog + - pig +``` + +行内写法 + +```yaml +pets: [cat,dog,pig] +``` + +## 3、配置文件值注入 + +配置文件 + +```yaml +person: + lastName: hello + age: 18 + boss: false + birth: 2017/12/12 + maps: {k1: v1,k2: 12} + lists: + - lisi + - zhaoliu + dog: + name: 小狗 + age: 12 +``` + +javaBean: + +```java +/** + * 将配置文件中配置的每一个属性的值,映射到这个组件中 + * @ConfigurationProperties:告诉SpringBoot将本类中的所有属性和配置文件中相关的配置进行绑定; + * prefix = "person":配置文件中哪个下面的所有属性进行一一映射 + * + * 只有这个组件是容器中的组件,才能容器提供的@ConfigurationProperties功能; + * + */ +@Component +@ConfigurationProperties(prefix = "person") +public class Person { + + private String lastName; + private Integer age; + private Boolean boss; + private Date birth; + + private Map maps; + private List lists; + private Dog dog; + +``` + +我们可以导入配置文件处理器,以后编写配置就有提示了 + +```xml + + + org.springframework.boot + spring-boot-configuration-processor + true + +``` + +#### 1、properties配置文件在idea中默认utf-8可能会乱码 + +调整 + +![idea配置乱码](images/springboot_2_01.png) + +#### 2、@Value获取值和@ConfigurationProperties获取值比较 + +| | @ConfigurationProperties | @Value | +| -------------------- | ------------------------ | ---------- | +| 功能 | 批量注入配置文件中的属性 | 一个个指定 | +| 松散绑定(松散语法) | 支持 | 不支持 | +| SpEL | 不支持 | 支持 | +| JSR303数据校验 | 支持 | 不支持 | +| 复杂类型封装 | 支持 | 不支持 | + +配置文件yml还是properties他们都能获取到值; + +如果说,我们只是在某个业务逻辑中需要获取一下配置文件中的某项值,使用@Value; + +如果说,我们专门编写了一个javaBean来和配置文件进行映射,我们就直接使用@ConfigurationProperties; + +#### 3、配置文件注入值数据校验 + +```java +@Component +@ConfigurationProperties(prefix = "person") +@Validated +public class Person { + + /** + * + * + * + */ + + //lastName必须是邮箱格式 + @Email + //@Value("${person.last-name}") + private String lastName; + //@Value("#{11*2}") + private Integer age; + //@Value("true") + private Boolean boss; + + private Date birth; + private Map maps; + private List lists; + private Dog dog; +``` + +#### 4、@PropertySource&@ImportResource&@Bean + +@**PropertySource**:加载指定的配置文件; + +```java +/** + * 将配置文件中配置的每一个属性的值,映射到这个组件中 + * @ConfigurationProperties:告诉SpringBoot将本类中的所有属性和配置文件中相关的配置进行绑定; + * prefix = "person":配置文件中哪个下面的所有属性进行一一映射 + * + * 只有这个组件是容器中的组件,才能容器提供的@ConfigurationProperties功能; + * @ConfigurationProperties(prefix = "person")默认从全局配置文件中获取值; + * + */ +@PropertySource(value = {"classpath:person.properties"}) +@Component +@ConfigurationProperties(prefix = "person") +//@Validated +public class Person { + + /** + * + * + * + */ + + //lastName必须是邮箱格式 + // @Email + //@Value("${person.last-name}") + private String lastName; + //@Value("#{11*2}") + private Integer age; + //@Value("true") + private Boolean boss; + +``` + +@**ImportResource**:导入Spring的配置文件,让配置文件里面的内容生效; + +Spring Boot里面没有Spring的配置文件,我们自己编写的配置文件,也不能自动识别; + +想让Spring的配置文件生效,加载进来;@**ImportResource**标注在一个配置类上 + +```java +@ImportResource(locations = {"classpath:beans.xml"}) +导入Spring的配置文件让其生效 +``` + +不来编写Spring的配置文件 + +```xml + + + + + +``` + +SpringBoot推荐给容器中添加组件的方式;推荐使用全注解的方式 + +1、配置类**@Configuration**------>Spring配置文件 + +2、使用**@Bean**给容器中添加组件 + +```java +/** + * @Configuration:指明当前类是一个配置类;就是来替代之前的Spring配置文件 + * + * 在配置文件中用标签添加组件 + * + */ +@Configuration +public class MyAppConfig { + + //将方法的返回值添加到容器中;容器中这个组件默认的id就是方法名 + @Bean + public HelloService helloService02(){ + System.out.println("配置类@Bean给容器中添加组件了..."); + return new HelloService(); + } +} +``` + +##4、配置文件占位符 + +### 1、随机数 + +```java +${random.value}、${random.int}、${random.long} +${random.int(10)}、${random.int[1024,65536]} + +``` + +### 2、占位符获取之前配置的值,如果没有可以是用:指定默认值 + +```properties +person.last-name=张三${random.uuid} +person.age=${random.int} +person.birth=2017/12/15 +person.boss=false +person.maps.k1=v1 +person.maps.k2=14 +person.lists=a,b,c +person.dog.name=${person.hello:hello}_dog +person.dog.age=15 +``` + +## 5、Profile + +### 1、多Profile文件 + +我们在主配置文件编写的时候,文件名可以是 application-{profile}.properties/yml + +默认使用application.properties的配置; + +### 2、yml支持多文档块方式 + +```yml + +server: + port: 8081 +spring: + profiles: + active: prod + +--- +server: + port: 8083 +spring: + profiles: dev + + +--- + +server: + port: 8084 +spring: + profiles: prod #指定属于哪个环境 +``` + +### 3、激活指定profile + +1、在配置文件中指定 spring.profiles.active=dev + +2、命令行: + +java -jar spring-boot-02-config-0.0.1-SNAPSHOT.jar --spring.profiles.active=dev; + +可以直接在测试的时候,配置传入命令行参数 + +3、虚拟机参数; + +-Dspring.profiles.active=dev + +## 6、配置文件加载位置 + +springboot 启动会扫描以下位置的application.properties或者application.yml文件作为Spring boot的默认配置文件 + +–file:./config/ + +–file:./ + +–classpath:/config/ + +–classpath:/ + +优先级由高到底,高优先级的配置会覆盖低优先级的配置; + +SpringBoot会从这四个位置全部加载主配置文件;**互补配置**; + +==我们还可以通过spring.config.location来改变默认的配置文件位置== + +**项目打包好以后,我们可以使用命令行参数的形式,启动项目的时候来指定配置文件的新位置;指定配置文件和默认加载的这些配置文件共同起作用形成互补配置;** + +java -jar spring-boot-02-config-02-0.0.1-SNAPSHOT.jar --spring.config.location=G:/application.properties + +## 7、外部配置加载顺序 + +**==SpringBoot也可以从以下位置加载配置; 优先级从高到低;高优先级的配置覆盖低优先级的配置,所有的配置会形成互补配置==** + +**1.命令行参数** + +所有的配置都可以在命令行上进行指定 + +java -jar spring-boot-02-config-02-0.0.1-SNAPSHOT.jar --server.port=8087 --server.context-path=/abc + +多个配置用空格分开; --配置项=值 + +2.来自java:comp/env的JNDI属性 + +3.Java系统属性(System.getProperties()) + +4.操作系统环境变量 + +5.RandomValuePropertySource配置的random.*属性值 + +==**由jar包外向jar包内进行寻找;**== + +==**优先加载带profile**== + +**6.jar包外部的application-{profile}.properties或application.yml(带spring.profile)配置文件** + +**7.jar包内部的application-{profile}.properties或application.yml(带spring.profile)配置文件** + +==**再来加载不带profile**== + +**8.jar包外部的application.properties或application.yml(不带spring.profile)配置文件** + +**9.jar包内部的application.properties或application.yml(不带spring.profile)配置文件** + +10.@Configuration注解类上的@PropertySource + +11.通过SpringApplication.setDefaultProperties指定的默认属性 + +所有支持的配置加载来源; + +[参考官方文档](https://docs.spring.io/spring-boot/docs/1.5.9.RELEASE/reference/htmlsingle/#boot-features-external-config) + +## 8、自动配置原理 + +配置文件到底能写什么?怎么写?自动配置原理; + +[配置文件能配置的属性参照](https://docs.spring.io/spring-boot/docs/1.5.9.RELEASE/reference/htmlsingle/#common-application-properties) + +### 1、**自动配置原理:** + +1)、SpringBoot启动的时候加载主配置类,开启了自动配置功能 ==@EnableAutoConfiguration== + +**2)、@EnableAutoConfiguration 作用:** + +- 利用EnableAutoConfigurationImportSelector给容器中导入一些组件? + +- 可以查看selectImports()方法的内容; + +- List configurations = getCandidateConfigurations(annotationMetadata, attributes);获取候选的配置 + +```java + SpringFactoriesLoader.loadFactoryNames() + 扫描所有jar包类路径下 META-INF/spring.factories + 把扫描到的这些文件的内容包装成properties对象 + 从properties中获取到EnableAutoConfiguration.class类(类名)对应的值,然后把他们添加在容器中 +``` + +**==将 类路径下 META-INF/spring.factories 里面配置的所有EnableAutoConfiguration的值加入到了容器中;==** + +```properties +# Auto Configure +org.springframework.boot.autoconfigure.EnableAutoConfiguration=\ +org.springframework.boot.autoconfigure.admin.SpringApplicationAdminJmxAutoConfiguration,\ +... +``` + +每一个这样的 xxxAutoConfiguration类都是容器中的一个组件,都加入到容器中;用他们来做自动配置; + +3)、每一个自动配置类进行自动配置功能; + +4)、以**HttpEncodingAutoConfiguration(Http编码自动配置)**为例解释自动配置原理; + +```java +@Configuration //表示这是一个配置类,以前编写的配置文件一样,也可以给容器中添加组件 +@EnableConfigurationProperties(HttpEncodingProperties.class) //启动指定类的ConfigurationProperties功能;将配置文件中对应的值和HttpEncodingProperties绑定起来;并把HttpEncodingProperties加入到ioc容器中 + +@ConditionalOnWebApplication //Spring底层@Conditional注解(Spring注解版),根据不同的条件,如果满足指定的条件,整个配置类里面的配置就会生效; 判断当前应用是否是web应用,如果是,当前配置类生效 + +@ConditionalOnClass(CharacterEncodingFilter.class) //判断当前项目有没有这个类CharacterEncodingFilter;SpringMVC中进行乱码解决的过滤器; + +@ConditionalOnProperty(prefix = "spring.http.encoding", value = "enabled", matchIfMissing = true) //判断配置文件中是否存在某个配置 spring.http.encoding.enabled;如果不存在,判断也是成立的 +//即使我们配置文件中不配置pring.http.encoding.enabled=true,也是默认生效的; +public class HttpEncodingAutoConfiguration { + + //他已经和SpringBoot的配置文件映射了 + private final HttpEncodingProperties properties; + + //只有一个有参构造器的情况下,参数的值就会从容器中拿 + public HttpEncodingAutoConfiguration(HttpEncodingProperties properties) { + this.properties = properties; + } + + @Bean //给容器中添加一个组件,这个组件的某些值需要从properties中获取 + @ConditionalOnMissingBean(CharacterEncodingFilter.class) //判断容器没有这个组件? + public CharacterEncodingFilter characterEncodingFilter() { + CharacterEncodingFilter filter = new OrderedCharacterEncodingFilter(); + filter.setEncoding(this.properties.getCharset().name()); + filter.setForceRequestEncoding(this.properties.shouldForce(Type.REQUEST)); + filter.setForceResponseEncoding(this.properties.shouldForce(Type.RESPONSE)); + return filter; + } +``` + +根据当前不同的条件判断,决定这个配置类是否生效? + +一但这个配置类生效;这个配置类就会给容器中添加各种组件;这些组件的属性是从对应的properties类中获取的,这些类里面的每一个属性又是和配置文件绑定的; + +5)、所有在配置文件中能配置的属性都是在xxxxProperties类中封装者‘;配置文件能配置什么就可以参照某个功能对应的这个属性类 + +```java +@ConfigurationProperties(prefix = "spring.http.encoding") //从配置文件中获取指定的值和bean的属性进行绑定 +public class HttpEncodingProperties { + + public static final Charset DEFAULT_CHARSET = Charset.forName("UTF-8"); +``` + +**精髓:** + +**1)、SpringBoot启动会加载大量的自动配置类** + +**2)、我们看我们需要的功能有没有SpringBoot默认写好的自动配置类;** + +**3)、我们再来看这个自动配置类中到底配置了哪些组件;(只要我们要用的组件有,我们就不需要再来配置了)** + +**4)、给容器中自动配置类添加组件的时候,会从properties类中获取某些属性。我们就可以在配置文件中指定这些属性的值;** + +xxxxAutoConfigurartion:自动配置类; + +给容器中添加组件 + +xxxxProperties:封装配置文件中相关属性; + +### 2、细节 + +#### 1、@Conditional派生注解(Spring注解版原生的@Conditional作用) + +作用:必须是@Conditional指定的条件成立,才给容器中添加组件,配置配里面的所有内容才生效; + +| @Conditional扩展注解 | 作用(判断是否满足当前指定条件) | +| ------------------------------- | ------------------------------------------------ | +| @ConditionalOnJava | 系统的java版本是否符合要求 | +| @ConditionalOnBean | 容器中存在指定Bean; | +| @ConditionalOnMissingBean | 容器中不存在指定Bean; | +| @ConditionalOnExpression | 满足SpEL表达式指定 | +| @ConditionalOnClass | 系统中有指定的类 | +| @ConditionalOnMissingClass | 系统中没有指定的类 | +| @ConditionalOnSingleCandidate | 容器中只有一个指定的Bean,或者这个Bean是首选Bean | +| @ConditionalOnProperty | 系统中指定的属性是否有指定的值 | +| @ConditionalOnResource | 类路径下是否存在指定资源文件 | +| @ConditionalOnWebApplication | 当前是web环境 | +| @ConditionalOnNotWebApplication | 当前不是web环境 | +| @ConditionalOnJndi | JNDI存在指定项 | + +**自动配置类必须在一定的条件下才能生效;** + +我们怎么知道哪些自动配置类生效; + +**==我们可以通过启用 debug=true属性;来让控制台打印自动配置报告==**,这样我们就可以很方便的知道哪些自动配置类生效; + +```java +========================= +AUTO-CONFIGURATION REPORT +========================= + +Positive matches:(自动配置类启用的) +----------------- + + DispatcherServletAutoConfiguration matched: + - @ConditionalOnClass found required class 'org.springframework.web.servlet.DispatcherServlet'; @ConditionalOnMissingClass did not find unwanted class (OnClassCondition) + +Negative matches:(没有启动,没有匹配成功的自动配置类) +----------------- + + ActiveMQAutoConfiguration: + Did not match: + - @ConditionalOnClass did not find required classes 'javax.jms.ConnectionFactory', 'org.apache.activemq.ActiveMQConnectionFactory' (OnClassCondition) +``` \ No newline at end of file diff --git "a/springboot/\345\205\245\351\227\250/3_\346\227\245\345\277\227.md" "b/springboot/\345\205\245\351\227\250/3_\346\227\245\345\277\227.md" new file mode 100644 index 0000000..9dad6e7 --- /dev/null +++ "b/springboot/\345\205\245\351\227\250/3_\346\227\245\345\277\227.md" @@ -0,0 +1,300 @@ +# 三、日志 + +## 1、日志框架 + + 小张;开发一个大型系统; + + 1、System.out.println("");将关键数据打印在控制台;去掉?写在一个文件? + + 2、框架来记录系统的一些运行时信息;日志框架 ; zhanglogging.jar; + + 3、高大上的几个功能?异步模式?自动归档?xxxx? zhanglogging-good.jar? + + 4、将以前框架卸下来?换上新的框架,重新修改之前相关的API;zhanglogging-prefect.jar; + + 5、JDBC---数据库驱动; + + 写了一个统一的接口层;日志门面(日志的一个抽象层);logging-abstract.jar; + + 给项目中导入具体的日志实现就行了;我们之前的日志框架都是实现的抽象层; + +**市面上的日志框架;** + +JUL、JCL、Jboss-logging、logback、log4j、log4j2、slf4j.... + +| 日志门面 (日志的抽象层) | 日志实现 | +| ------------------------------------------------------------ | ---------------------------------------------------- | +| ~~JCL(Jakarta Commons Logging)~~ SLF4j(Simple Logging Facade for Java) **~~jboss-logging~~** | Log4j JUL(java.util.logging) Log4j2 **Logback** | + +左边选一个门面(抽象层)、右边来选一个实现; + +日志门面: SLF4J; + +日志实现:Logback; + +SpringBoot:底层是Spring框架,Spring框架默认是用JCL;‘ + + **==SpringBoot选用 SLF4j和logback;==** + +## 2、SLF4j使用 + +### 1、如何在系统中使用SLF4j https://www.slf4j.org + +以后开发的时候,日志记录方法的调用,不应该来直接调用日志的实现类,而是调用日志抽象层里面的方法; + +给系统里面导入slf4j的jar和 logback的实现jar + +```java +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +public class HelloWorld { + public static void main(String[] args) { + Logger logger = LoggerFactory.getLogger(HelloWorld.class); + logger.info("Hello World"); + } +} +``` + +图示; + +![images/concrete-bindings.png](images/springboot_3_01.png) + +每一个日志的实现框架都有自己的配置文件。使用slf4j以后,**配置文件还是做成日志实现框架自己本身的配置文件;** + +### 2、遗留问题 + +a(slf4j+logback): Spring(commons-logging)、Hibernate(jboss-logging)、MyBatis、xxxx + +统一日志记录,即使是别的框架和我一起统一使用slf4j进行输出? + +![](images/springboot_3_02.png) + +**如何让系统中所有的日志都统一到slf4j;** + +==1、将系统中其他日志框架先排除出去;== + +==2、用中间包来替换原有的日志框架;== + +==3、我们导入slf4j其他的实现== + +## 3、SpringBoot日志关系 + +```xml + + org.springframework.boot + spring-boot-starter + +``` + +SpringBoot使用它来做日志功能; + +```xml + + org.springframework.boot + spring-boot-starter-logging + +``` + +底层依赖关系 + +![](images/springboot_3_03.png) + +总结: + + 1)、SpringBoot底层也是使用slf4j+logback的方式进行日志记录 + + 2)、SpringBoot也把其他的日志都替换成了slf4j; + + 3)、中间替换包? + +```java +@SuppressWarnings("rawtypes") +public abstract class LogFactory { + + static String UNSUPPORTED_OPERATION_IN_JCL_OVER_SLF4J = "http://www.slf4j.org/codes.html#unsupported_operation_in_jcl_over_slf4j"; + + static LogFactory logFactory = new SLF4JLogFactory(); +``` + +![](images/springboot_3_04.png) + + 4)、如果我们要引入其他框架?一定要把这个框架的默认日志依赖移除掉? + + Spring框架用的是commons-logging; + +```xml + + org.springframework + spring-core + + + commons-logging + commons-logging + + + +``` + +**==SpringBoot能自动适配所有的日志,而且底层使用slf4j+logback的方式记录日志,引入其他框架的时候,只需要把这个框架依赖的日志框架排除掉即可;==** + +## 4、日志使用; + +### 1、默认配置 + +SpringBoot默认帮我们配置好了日志; + +```java + //记录器 + Logger logger = LoggerFactory.getLogger(getClass()); + @Test + public void contextLoads() { + //System.out.println(); + + //日志的级别; + //由低到高 trace + %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n +SpringBoot修改日志的默认配置 + +```properties +logging.level.com.atguigu=trace + +#logging.path= +# 不指定路径在当前项目下生成springboot.log日志 +# 可以指定完整的路径; +#logging.file=G:/springboot.log + +# 在当前磁盘的根路径下创建spring文件夹和里面的log文件夹;使用 spring.log 作为默认文件 +logging.path=/spring/log + +# 在控制台输出的日志的格式 +logging.pattern.console=%d{yyyy-MM-dd} [%thread] %-5level %logger{50} - %msg%n +# 指定文件中日志输出的格式 +logging.pattern.file=%d{yyyy-MM-dd} === [%thread] === %-5level === %logger{50} ==== %msg%n +``` + +| logging.file | logging.path | Example | Description | +| ------------ | ------------ | -------- | ---------------------------------- | +| (none) | (none) | | 只在控制台输出 | +| 指定文件名 | (none) | my.log | 输出日志到my.log文件 | +| (none) | 指定目录 | /var/log | 输出到指定目录的 spring.log 文件中 | + +### 2、指定配置 + +给类路径下放上每个日志框架自己的配置文件即可;SpringBoot就不使用他默认配置的了 + +| Logging System | Customization | +| ----------------------- | ------------------------------------------------------------ | +| Logback | `logback-spring.xml`, `logback-spring.groovy`, `logback.xml` or `logback.groovy` | +| Log4j2 | `log4j2-spring.xml` or `log4j2.xml` | +| JDK (Java Util Logging) | `logging.properties` | + +logback.xml:直接就被日志框架识别了; + +**logback-spring.xml**:日志框架就不直接加载日志的配置项,由SpringBoot解析日志配置,可以使用SpringBoot的高级Profile功能 + +```xml + + + 可以指定某段配置只在某个环境下生效 + + +``` + +如: + +```xml + + + + + %d{yyyy-MM-dd HH:mm:ss.SSS} ----> [%thread] ---> %-5level %logger{50} - %msg%n + + + %d{yyyy-MM-dd HH:mm:ss.SSS} ==== [%thread] ==== %-5level %logger{50} - %msg%n + + + +``` + + + +如果使用logback.xml作为日志配置文件,还要使用profile功能,会有以下错误 + + `no applicable action for [springProfile]` + +## 5、切换日志框架 + +可以按照slf4j的日志适配图,进行相关的切换; + +slf4j+log4j的方式; + +```xml + + org.springframework.boot + spring-boot-starter-web + + + logback-classic + ch.qos.logback + + + log4j-over-slf4j + org.slf4j + + + + + + org.slf4j + slf4j-log4j12 + + +``` + +切换为log4j2 + +```xml + + org.springframework.boot + spring-boot-starter-web + + + spring-boot-starter-logging + org.springframework.boot + + + + + + org.springframework.boot + spring-boot-starter-log4j2 + +``` + diff --git "a/springboot/\345\205\245\351\227\250/4_web.md" "b/springboot/\345\205\245\351\227\250/4_web.md" new file mode 100644 index 0000000..987b9cd --- /dev/null +++ "b/springboot/\345\205\245\351\227\250/4_web.md" @@ -0,0 +1,1846 @@ +# 四、Web开发 + +## 1、简介 + +使用SpringBoot; + +**1)、创建SpringBoot应用,选中我们需要的模块;** + +**2)、SpringBoot已经默认将这些场景配置好了,只需要在配置文件中指定少量配置就可以运行起来** + +**3)、自己编写业务代码;** + + + +**自动配置原理?** + +这个场景SpringBoot帮我们配置了什么?能不能修改?能修改哪些配置?能不能扩展?xxx + +``` +xxxxAutoConfiguration:帮我们给容器中自动配置组件; +xxxxProperties:配置类来封装配置文件的内容; +``` + +## 2、SpringBoot对静态资源的映射规则; + +```java +@ConfigurationProperties(prefix = "spring.resources", ignoreUnknownFields = false) +public class ResourceProperties implements ResourceLoaderAware { + //可以设置和静态资源有关的参数,缓存时间等 +``` + +```java + WebMvcAuotConfiguration: + @Override + public void addResourceHandlers(ResourceHandlerRegistry registry) { + if (!this.resourceProperties.isAddMappings()) { + logger.debug("Default resource handling disabled"); + return; + } + Integer cachePeriod = this.resourceProperties.getCachePeriod(); + if (!registry.hasMappingForPattern("/webjars/**")) { + customizeResourceHandlerRegistration( + registry.addResourceHandler("/webjars/**") + .addResourceLocations( + "classpath:/META-INF/resources/webjars/") + .setCachePeriod(cachePeriod)); + } + String staticPathPattern = this.mvcProperties.getStaticPathPattern(); + //静态资源文件夹映射 + if (!registry.hasMappingForPattern(staticPathPattern)) { + customizeResourceHandlerRegistration( + registry.addResourceHandler(staticPathPattern) + .addResourceLocations( + this.resourceProperties.getStaticLocations()) + .setCachePeriod(cachePeriod)); + } + } + + //配置欢迎页映射 + @Bean + public WelcomePageHandlerMapping welcomePageHandlerMapping( + ResourceProperties resourceProperties) { + return new WelcomePageHandlerMapping(resourceProperties.getWelcomePage(), + this.mvcProperties.getStaticPathPattern()); + } + + //配置喜欢的图标 + @Configuration + @ConditionalOnProperty(value = "spring.mvc.favicon.enabled", matchIfMissing = true) + public static class FaviconConfiguration { + + private final ResourceProperties resourceProperties; + + public FaviconConfiguration(ResourceProperties resourceProperties) { + this.resourceProperties = resourceProperties; + } + + @Bean + public SimpleUrlHandlerMapping faviconHandlerMapping() { + SimpleUrlHandlerMapping mapping = new SimpleUrlHandlerMapping(); + mapping.setOrder(Ordered.HIGHEST_PRECEDENCE + 1); + //所有 **/favicon.ico + mapping.setUrlMap(Collections.singletonMap("**/favicon.ico", + faviconRequestHandler())); + return mapping; + } + + @Bean + public ResourceHttpRequestHandler faviconRequestHandler() { + ResourceHttpRequestHandler requestHandler = new ResourceHttpRequestHandler(); + requestHandler + .setLocations(this.resourceProperties.getFaviconLocations()); + return requestHandler; + } + + } + +``` + +==1)、所有 /webjars/** ,都去 classpath:/META-INF/resources/webjars/ 找资源;== + + webjars:以jar包的方式引入静态资源; + +http://www.webjars.org/ + +![](images/springboot_4_01.png) + +localhost:8080/webjars/jquery/3.3.1/jquery.js + +```xml +在访问的时候只需要写webjars下面资源的名称即可 + + org.webjars + jquery + 3.3.1 + +``` + +==2)、"/**" 访问当前项目的任何资源,都去(静态资源的文件夹)找映射== + +``` +"classpath:/META-INF/resources/", +"classpath:/resources/", +"classpath:/static/", +"classpath:/public/" +"/":当前项目的根路径 +``` + +localhost:8080/abc === 去静态资源文件夹里面找abc + +==3)、欢迎页; 静态资源文件夹下的所有index.html页面;被"/**"映射;== + + localhost:8080/ 找index页面 + +==4)、所有的 **/favicon.ico 都是在静态资源文件下找;== + + + +## 3、模板引擎 + +JSP、Velocity、Freemarker、Thymeleaf + +![](images/springboot_4_02.png) + +SpringBoot推荐的Thymeleaf; + +语法更简单,功能更强大; + + + +### 1、引入thymeleaf; + +```xml + + org.springframework.boot + spring-boot-starter-thymeleaf + 2.1.6 + +切换thymeleaf版本 + + 3.0.9.RELEASE + + + 2.2.2 + +``` + + + +### 2、Thymeleaf使用 + +```java +@ConfigurationProperties(prefix = "spring.thymeleaf") +public class ThymeleafProperties { + + private static final Charset DEFAULT_ENCODING = Charset.forName("UTF-8"); + + private static final MimeType DEFAULT_CONTENT_TYPE = MimeType.valueOf("text/html"); + + public static final String DEFAULT_PREFIX = "classpath:/templates/"; + + public static final String DEFAULT_SUFFIX = ".html"; + // +``` + +只要我们把HTML页面放在classpath:/templates/,thymeleaf就能自动渲染; + +使用: + +1、导入thymeleaf的名称空间 + +```xml + +``` + +2、使用thymeleaf语法; + +```html + + + + + Title + + +

    成功!

    + +
    这是显示欢迎信息
    + + +``` + +### 3、语法规则 + +1)、th:text;改变当前元素里面的文本内容; + + th:任意html属性;来替换原生属性的值 + +![](images/springboot_4_03.png) + +2)、表达式? + +```properties +Simple expressions:(表达式语法) + Variable Expressions: ${...}:获取变量值;OGNL; + 1)、获取对象的属性、调用方法 + 2)、使用内置的基本对象: + #ctx : the context object. + #vars: the context variables. + #locale : the context locale. + #request : (only in Web Contexts) the HttpServletRequest object. + #response : (only in Web Contexts) the HttpServletResponse object. + #session : (only in Web Contexts) the HttpSession object. + #servletContext : (only in Web Contexts) the ServletContext object. + + ${session.foo} + 3)、内置的一些工具对象: +#execInfo : information about the template being processed. +#messages : methods for obtaining externalized messages inside variables expressions, in the same way as they would be obtained using #{…} syntax. +#uris : methods for escaping parts of URLs/URIs +#conversions : methods for executing the configured conversion service (if any). +#dates : methods for java.util.Date objects: formatting, component extraction, etc. +#calendars : analogous to #dates , but for java.util.Calendar objects. +#numbers : methods for formatting numeric objects. +#strings : methods for String objects: contains, startsWith, prepending/appending, etc. +#objects : methods for objects in general. +#bools : methods for boolean evaluation. +#arrays : methods for arrays. +#lists : methods for lists. +#sets : methods for sets. +#maps : methods for maps. +#aggregates : methods for creating aggregates on arrays or collections. +#ids : methods for dealing with id attributes that might be repeated (for example, as a result of an iteration). + + Selection Variable Expressions: *{...}:选择表达式:和${}在功能上是一样; + 补充:配合 th:object="${session.user}: +
    +

    Name: Sebastian.

    +

    Surname: Pepper.

    +

    Nationality: Saturn.

    +
    + + Message Expressions: #{...}:获取国际化内容 + Link URL Expressions: @{...}:定义URL; + @{/order/process(execId=${execId},execType='FAST')} + Fragment Expressions: ~{...}:片段引用表达式 +
    ...
    + +Literals(字面量) + Text literals: 'one text' , 'Another one!' ,… + Number literals: 0 , 34 , 3.0 , 12.3 ,… + Boolean literals: true , false + Null literal: null + Literal tokens: one , sometext , main ,… +Text operations:(文本操作) + String concatenation: + + Literal substitutions: |The name is ${name}| +Arithmetic operations:(数学运算) + Binary operators: + , - , * , / , % + Minus sign (unary operator): - +Boolean operations:(布尔运算) + Binary operators: and , or + Boolean negation (unary operator): ! , not +Comparisons and equality:(比较运算) + Comparators: > , < , >= , <= ( gt , lt , ge , le ) + Equality operators: == , != ( eq , ne ) +Conditional operators:条件运算(三元运算符) + If-then: (if) ? (then) + If-then-else: (if) ? (then) : (else) + Default: (value) ?: (defaultvalue) +Special tokens: + No-Operation: _ +``` + +## 4、SpringMVC自动配置 + +https://docs.spring.io/spring-boot/docs/1.5.10.RELEASE/reference/htmlsingle/#boot-features-developing-web-applications + +### 1. Spring MVC auto-configuration + +Spring Boot 自动配置好了SpringMVC + +以下是SpringBoot对SpringMVC的默认配置:**==(WebMvcAutoConfiguration)==** + +- Inclusion of `ContentNegotiatingViewResolver` and `BeanNameViewResolver` beans. + - 自动配置了ViewResolver(视图解析器:根据方法的返回值得到视图对象(View),视图对象决定如何渲染(转发?重定向?)) + - ContentNegotiatingViewResolver:组合所有的视图解析器的; + - ==如何定制:我们可以自己给容器中添加一个视图解析器;自动的将其组合进来;== + +- Support for serving static resources, including support for WebJars (see below).静态资源文件夹路径,webjars + +- Static `index.html` support. 静态首页访问 + +- Custom `Favicon` support (see below). favicon.ico + +- 自动注册了 of `Converter`, `GenericConverter`, `Formatter` beans. + + - Converter:转换器; public String hello(User user):类型转换使用Converter + - `Formatter` 格式化器; 2017.12.17===Date; + +```java + @Bean + @ConditionalOnProperty(prefix = "spring.mvc", name = "date-format")//在文件中配置日期格式化的规则 + public Formatter dateFormatter() { + return new DateFormatter(this.mvcProperties.getDateFormat());//日期格式化组件 + } +``` + + ==自己添加的格式化器转换器,我们只需要放在容器中即可== + +- Support for `HttpMessageConverters` (see below). + + - HttpMessageConverter:SpringMVC用来转换Http请求和响应的;User---Json; + + - `HttpMessageConverters` 是从容器中确定;获取所有的HttpMessageConverter; + + ==自己给容器中添加HttpMessageConverter,只需要将自己的组件注册容器中(@Bean,@Component)== + +- Automatic registration of `MessageCodesResolver` (see below).定义错误代码生成规则 + +- Automatic use of a `ConfigurableWebBindingInitializer` bean (see below). + + ==我们可以配置一个ConfigurableWebBindingInitializer来替换默认的;(添加到容器)== + + ``` + 初始化WebDataBinder; + 请求数据=====JavaBean; + ``` + +**org.springframework.boot.autoconfigure.web:web的所有自动场景;** + +If you want to keep Spring Boot MVC features, and you just want to add additional [MVC configuration](https://docs.spring.io/spring/docs/4.3.14.RELEASE/spring-framework-reference/htmlsingle#mvc) (interceptors, formatters, view controllers etc.) you can add your own `@Configuration` class of type `WebMvcConfigurerAdapter`, but **without** `@EnableWebMvc`. If you wish to provide custom instances of `RequestMappingHandlerMapping`, `RequestMappingHandlerAdapter` or `ExceptionHandlerExceptionResolver` you can declare a `WebMvcRegistrationsAdapter` instance providing such components. + +If you want to take complete control of Spring MVC, you can add your own `@Configuration` annotated with `@EnableWebMvc`. + +### 2、扩展SpringMVC + +```xml + + + + + + + +``` + +**==编写一个配置类(@Configuration),是WebMvcConfigurerAdapter类型;不能标注@EnableWebMvc==**; + +既保留了所有的自动配置,也能用我们扩展的配置; + +```java +//使用WebMvcConfigurerAdapter可以来扩展SpringMVC的功能 +@Configuration +public class MyMvcConfig extends WebMvcConfigurerAdapter { + + @Override + public void addViewControllers(ViewControllerRegistry registry) { + // super.addViewControllers(registry); + //浏览器发送 /atguigu 请求来到 success + registry.addViewController("/atguigu").setViewName("success"); + } +} +``` + +原理: + + 1)、WebMvcAutoConfiguration是SpringMVC的自动配置类 + + 2)、在做其他自动配置时会导入;@Import(**EnableWebMvcConfiguration**.class) + +```java + @Configuration + public static class EnableWebMvcConfiguration extends DelegatingWebMvcConfiguration { + private final WebMvcConfigurerComposite configurers = new WebMvcConfigurerComposite(); + + //从容器中获取所有的WebMvcConfigurer + @Autowired(required = false) + public void setConfigurers(List configurers) { + if (!CollectionUtils.isEmpty(configurers)) { + this.configurers.addWebMvcConfigurers(configurers); + //一个参考实现;将所有的WebMvcConfigurer相关配置都来一起调用; + @Override + // public void addViewControllers(ViewControllerRegistry registry) { + // for (WebMvcConfigurer delegate : this.delegates) { + // delegate.addViewControllers(registry); + // } + } + } + } +``` + + 3)、容器中所有的WebMvcConfigurer都会一起起作用; + + 4)、我们的配置类也会被调用; + + 效果:SpringMVC的自动配置和我们的扩展配置都会起作用; + +### 3、全面接管SpringMVC; + +SpringBoot对SpringMVC的自动配置不需要了,所有都是我们自己配置;所有的SpringMVC的自动配置都失效了 + +**我们需要在配置类中添加@EnableWebMvc即可;** + +```java +//使用WebMvcConfigurerAdapter可以来扩展SpringMVC的功能 +@EnableWebMvc +@Configuration +public class MyMvcConfig extends WebMvcConfigurerAdapter { + + @Override + public void addViewControllers(ViewControllerRegistry registry) { + // super.addViewControllers(registry); + //浏览器发送 /atguigu 请求来到 success + registry.addViewController("/atguigu").setViewName("success"); + } +} +``` + +原理: + +为什么@EnableWebMvc自动配置就失效了; + +1)@EnableWebMvc的核心 + +```java +@Import(DelegatingWebMvcConfiguration.class) +public @interface EnableWebMvc { +``` + +2)、 + +```java +@Configuration +public class DelegatingWebMvcConfiguration extends WebMvcConfigurationSupport { +``` + +3)、 + +```java +@Configuration +@ConditionalOnWebApplication +@ConditionalOnClass({ Servlet.class, DispatcherServlet.class, + WebMvcConfigurerAdapter.class }) +//容器中没有这个组件的时候,这个自动配置类才生效 +@ConditionalOnMissingBean(WebMvcConfigurationSupport.class) +@AutoConfigureOrder(Ordered.HIGHEST_PRECEDENCE + 10) +@AutoConfigureAfter({ DispatcherServletAutoConfiguration.class, + ValidationAutoConfiguration.class }) +public class WebMvcAutoConfiguration { +``` + +4)、@EnableWebMvc将WebMvcConfigurationSupport组件导入进来; + +5)、导入的WebMvcConfigurationSupport只是SpringMVC最基本的功能; + + + +## 5、如何修改SpringBoot的默认配置 + +模式: + + 1)、SpringBoot在自动配置很多组件的时候,先看容器中有没有用户自己配置的(@Bean、@Component)如果有就用用户配置的,如果没有,才自动配置;如果有些组件可以有多个(ViewResolver)将用户配置的和自己默认的组合起来; + + 2)、在SpringBoot中会有非常多的xxxConfigurer帮助我们进行扩展配置 + + 3)、在SpringBoot中会有很多的xxxCustomizer帮助我们进行定制配置 + +## 6、RestfulCRUD + +### 1)、默认访问首页 + +```java + +//使用WebMvcConfigurerAdapter可以来扩展SpringMVC的功能 +//@EnableWebMvc 不要接管SpringMVC +@Configuration +public class MyMvcConfig extends WebMvcConfigurerAdapter { + + @Override + public void addViewControllers(ViewControllerRegistry registry) { + // super.addViewControllers(registry); + //浏览器发送 /atguigu 请求来到 success + registry.addViewController("/atguigu").setViewName("success"); + } + + //所有的WebMvcConfigurerAdapter组件都会一起起作用 + @Bean //将组件注册在容器 + public WebMvcConfigurerAdapter webMvcConfigurerAdapter(){ + WebMvcConfigurerAdapter adapter = new WebMvcConfigurerAdapter() { + @Override + public void addViewControllers(ViewControllerRegistry registry) { + registry.addViewController("/").setViewName("login"); + registry.addViewController("/index.html").setViewName("login"); + } + }; + return adapter; + } +} + +``` + +### 2)、国际化 + +**1)、编写国际化配置文件;** + +2)、使用ResourceBundleMessageSource管理国际化资源文件 + +3)、在页面使用fmt:message取出国际化内容 + + + +步骤: + +1)、编写国际化配置文件,抽取页面需要显示的国际化消息 + +![](images/springboot_4_04.png) + + + +2)、SpringBoot自动配置好了管理国际化资源文件的组件; + +```java +@ConfigurationProperties(prefix = "spring.messages") +public class MessageSourceAutoConfiguration { + + /** + * Comma-separated list of basenames (essentially a fully-qualified classpath + * location), each following the ResourceBundle convention with relaxed support for + * slash based locations. If it doesn't contain a package qualifier (such as + * "org.mypackage"), it will be resolved from the classpath root. + */ + private String basename = "messages"; + //我们的配置文件可以直接放在类路径下叫messages.properties; + + @Bean + public MessageSource messageSource() { + ResourceBundleMessageSource messageSource = new ResourceBundleMessageSource(); + if (StringUtils.hasText(this.basename)) { + //设置国际化资源文件的基础名(去掉语言国家代码的) + messageSource.setBasenames(StringUtils.commaDelimitedListToStringArray( + StringUtils.trimAllWhitespace(this.basename))); + } + if (this.encoding != null) { + messageSource.setDefaultEncoding(this.encoding.name()); + } + messageSource.setFallbackToSystemLocale(this.fallbackToSystemLocale); + messageSource.setCacheSeconds(this.cacheSeconds); + messageSource.setAlwaysUseMessageFormat(this.alwaysUseMessageFormat); + return messageSource; + } +``` + + + +3)、去页面获取国际化的值; + +![](images/springboot_4_05.png) + + + +```html + + + + + + + + Signin Template for Bootstrap + + + + + + + + + + + + +``` + +效果:根据浏览器语言设置的信息切换了国际化; + +原理: + + 国际化Locale(区域信息对象);LocaleResolver(获取区域信息对象); + +```java + @Bean + @ConditionalOnMissingBean + @ConditionalOnProperty(prefix = "spring.mvc", name = "locale") + public LocaleResolver localeResolver() { + if (this.mvcProperties + .getLocaleResolver() == WebMvcProperties.LocaleResolver.FIXED) { + return new FixedLocaleResolver(this.mvcProperties.getLocale()); + } + AcceptHeaderLocaleResolver localeResolver = new AcceptHeaderLocaleResolver(); + localeResolver.setDefaultLocale(this.mvcProperties.getLocale()); + return localeResolver; + } +默认的就是根据请求头带来的区域信息获取Locale进行国际化 +``` + +4)、点击链接切换国际化 + +```java +/** + * 可以在连接上携带区域信息 + */ +public class MyLocaleResolver implements LocaleResolver { + + @Override + public Locale resolveLocale(HttpServletRequest request) { + String l = request.getParameter("l"); + Locale locale = Locale.getDefault(); + if(!StringUtils.isEmpty(l)){ + String[] split = l.split("_"); + locale = new Locale(split[0],split[1]); + } + return locale; + } + + @Override + public void setLocale(HttpServletRequest request, HttpServletResponse response, Locale locale) { + + } +} + + + @Bean + public LocaleResolver localeResolver(){ + return new MyLocaleResolver(); + } +} + + +``` + +### 3)、登陆 + +开发期间模板引擎页面修改以后,要实时生效 + +1)、禁用模板引擎的缓存 + +``` +# 禁用缓存 +spring.thymeleaf.cache=false +``` + +2)、页面修改完成以后ctrl+f9:重新编译; + +登陆错误消息的显示 + +```html +

    +``` + + + +### 4)、拦截器进行登陆检查 + +拦截器 + +```java + +/** + * 登陆检查, + */ +public class LoginHandlerInterceptor implements HandlerInterceptor { + //目标方法执行之前 + @Override + public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { + Object user = request.getSession().getAttribute("loginUser"); + if(user == null){ + //未登陆,返回登陆页面 + request.setAttribute("msg","没有权限请先登陆"); + request.getRequestDispatcher("/index.html").forward(request,response); + return false; + }else{ + //已登陆,放行请求 + return true; + } + + } + + @Override + public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception { + + } + + @Override + public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception { + + } +} + +``` + + + +注册拦截器 + +```java + //所有的WebMvcConfigurerAdapter组件都会一起起作用 + @Bean //将组件注册在容器 + public WebMvcConfigurerAdapter webMvcConfigurerAdapter(){ + WebMvcConfigurerAdapter adapter = new WebMvcConfigurerAdapter() { + @Override + public void addViewControllers(ViewControllerRegistry registry) { + registry.addViewController("/").setViewName("login"); + registry.addViewController("/index.html").setViewName("login"); + registry.addViewController("/main.html").setViewName("dashboard"); + } + + //注册拦截器 + @Override + public void addInterceptors(InterceptorRegistry registry) { + //super.addInterceptors(registry); + //静态资源; *.css , *.js + //SpringBoot已经做好了静态资源映射 + registry.addInterceptor(new LoginHandlerInterceptor()).addPathPatterns("/**") + .excludePathPatterns("/index.html","/","/user/login"); + } + }; + return adapter; + } +``` + +### 5)、CRUD-员工列表 + +实验要求: + +1)、RestfulCRUD:CRUD满足Rest风格; + +URI: /资源名称/资源标识 HTTP请求方式区分对资源CRUD操作 + +| | 普通CRUD(uri来区分操作) | RestfulCRUD | +| ---- | ------------------------- | ----------------- | +| 查询 | getEmp | emp---GET | +| 添加 | addEmp?xxx | emp---POST | +| 修改 | updateEmp?id=xxx&xxx=xx | emp/{id}---PUT | +| 删除 | deleteEmp?id=1 | emp/{id}---DELETE | + +2)、实验的请求架构; + +| 实验功能 | 请求URI | 请求方式 | +| ------------------------------------ | ------- | -------- | +| 查询所有员工 | emps | GET | +| 查询某个员工(来到修改页面) | emp/1 | GET | +| 来到添加页面 | emp | GET | +| 添加员工 | emp | POST | +| 来到修改页面(查出员工进行信息回显) | emp/1 | GET | +| 修改员工 | emp | PUT | +| 删除员工 | emp/1 | DELETE | + +3)、员工列表: + +#### thymeleaf公共页面元素抽取 + +```html +1、抽取公共片段 +
    +© 2011 The Good Thymes Virtual Grocery +
    + +2、引入公共片段 +
    +~{templatename::selector}:模板名::选择器 +~{templatename::fragmentname}:模板名::片段名 + +3、默认效果: +insert的公共片段在div标签中 +如果使用th:insert等属性进行引入,可以不用写~{}: +行内写法可以加上:[[~{}]];[(~{})]; +``` + + + +三种引入公共片段的th属性: + +**th:insert**:将公共片段整个插入到声明引入的元素中 + +**th:replace**:将声明引入的元素替换为公共片段 + +**th:include**:将被引入的片段的内容包含进这个标签中 + + + +```html +
    +© 2011 The Good Thymes Virtual Grocery +
    + +引入方式 +
    +
    +
    + +效果 +
    +
    + © 2011 The Good Thymes Virtual Grocery +
    +
    + +
    +© 2011 The Good Thymes Virtual Grocery +
    + +
    +© 2011 The Good Thymes Virtual Grocery +
    +``` + + + +引入片段的时候传入参数: + +```html + +