[
  {
    "path": "build.gradle",
    "content": "buildscript {\n\n    repositories {\n        maven {url 'http://maven.aliyun.com/nexus/content/groups/public/'}\n//        maven { url 'http://repo1.maven.apache.org/maven2/' }\n////        maven { url 'http://repo2.maven.org/maven2/' }\n////        maven { url 'http://maven.oschina.net/content/groups/public/' }\n//        google()\n    }\n}\n\nplugins {\n    id 'java'\n}\n\ngroup 'funtester'\nversion '1.0'\n\napply plugin: 'groovy'\napply plugin: 'java'\napply plugin: 'idea'\n//apply plugin: 'distribution'   //打包tar包用到的插件\n\nidea {\n    module {\n        downloadJavadoc = true\n        downloadSources = true\n    }\n}\n\nsourceCompatibility = 1.8\n\nrepositories {\n    mavenLocal()\n    maven {\n        url 'http://maven.aliyun.com/nexus/content/groups/public/'\n    }\n//    maven {\n//        url 'http://repo2.maven.org/maven2/'\n//    }\n//    maven {\n//        url 'http://repo1.maven.apache.org/maven2/'\n//    }\n//    maven { url 'http://maven.oschina.net/content/groups/public/' }\n\n    mavenCentral()\n}\n\ntest {\n    useJUnitPlatform()\n    exclude \"com/test/**\"\n}\n\nsourceSets {\n    main {\n        groovy {\n            srcDirs 'src/main/groovy'\n        }\n    }\n}\ndependencies {\n    compile group: 'org.apache.commons', name: 'commons-lang3', version: '3.7'\n    compile group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.5.5'\n    compile group: 'org.apache.httpcomponents', name: 'httpmime', version: '4.5.5'\n    compile group: 'org.apache.httpcomponents', name: 'httpasyncclient', version: '4.1.4'\n//    compile group: 'commons-beanutils', name: 'commons-beanutils', version: '1.8.0'\n//    compile group: 'commons-codec', name: 'commons-codec', version: '1.9'\n//    compile group: 'com.sun.jna', name: 'jna', version: '3.0.9'\n    compile group: 'com.alibaba', name: 'fastjson', version: '1.2.62'\n//    compile group: 'org.mongodb', name: 'mongo-java-driver', version: '3.9.0'\n    compile group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.12.1'\n    compile group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.12.1'\n//    compile group: 'org.apache.logging.log4j', name: 'log4j-slf4j-impl', version: '2.12.1'\n//    compile group: 'mysql', name: 'mysql-connector-java', version: '8.0.13'\n//    compile group: 'javax.mail', name: 'javax.mail-api', version: '1.6.0'\n//    compile group: 'com.sun.mail', name: 'javax.mail', version: '1.6.0'\n    compile group: 'org.codehaus.groovy', name: 'groovy-all', version: '2.5.7'\n//    compile group: 'redis.clients', name: 'jedis', version: '3.0.1'\n//    compile group: 'com.alibaba', name: 'dubbo', version: '2.5.3'\n//    compile group: 'com.jayway.jsonpath', name: 'json-path', version: '2.4.0'\n//    compile group: 'dom4j', name: 'dom4j', version: '1.6.1'\n//    compile group: 'org.java-websocket', name: 'Java-WebSocket', version: '1.5.1'\n//    compile group: 'io.socket', name: 'socket.io-client', version: '1.0.0'\n}\n\njar {\n    from {\n        //添加依懒到打包文件\n        configurations.compile.collect {it.isDirectory() ? it : zipTree(it)}\n        configurations.runtime.collect {zipTree(it)}\n    }\n    manifest {\n        attributes 'Main-Class': 'com.funtester.main.Blog'\n    }\n}\n\next {\n    if (project.hasProperty('profile')) {\n        profile = project['profile']\n    } else {\n        profile = \"FunTester\"\n    }\n    println \"项目环境:\" + profile\n}\n//因为打包配置,这里会执行\ntask createDirs() {\n    doLast {\n        if (profile != \"FunTester\") {\n            file('build/package/lib').mkdirs()\n            file('build/package/bin').mkdirs()\n            file('build/package/logs').mkdirs()\n            file('build/package/conf').mkdirs()\n            println \"文件夹创建成功!\"\n        }\n    }\n}\ntask copyFun(type: Copy) {\n    from('/Users/fv/Documents/workspace/funtester/build/libs')\n    into('/Users/fv/Library/groovy-2.5.7/lib')\n    println \"拷贝fun.jar包到Groovy依赖成功!\"\n}\ntask copyOkay(type: Copy) {\n    from('/Users/fv/Documents/workspace/okay_test/target/okay_test-1.0-SNAPSHOT.jar')\n    into('/Users/fv/Library/groovy-2.5.7/lib')\n    println \"拷贝okay.jar包到Groovy依赖成功!\"\n}\n\ntask copyJarToGroovy(dependsOn: ['copyFun', 'copyOkay']) {}\n\ntask copyLibs(type: Copy) {\n    doLast {\n        from('build/libs')\n        into('build/package/lib')\n        println \"依赖拷贝成功!\"\n    }\n}\n\ntask copyConf(type: Copy) {\n    doLast {\n        from('src/main/resources/' + profile)\n        into('build/package/conf')\n        println \"从src/main/resources/\" + profile + \"拷贝配置文件\"\n    }\n}\n\ntask copyBin(type: Copy) {\n    doLast {\n        from('src/main/resources/bin')\n        into('build/package/bin')\n        fileMode 0744//可能会失效,检查执行权限\n        println \"依赖脚本,并设置可执行权限成功!\"\n    }\n\n}\n// task 用来复制启动所依赖的jar包\ntask copyDep(type: Copy) {\n    doLast {\n        from configurations.runtime\n        into 'build/package/lib'\n        println \"复制启动所依赖的jar包成功!\"\n    }\n}\n//把上述的task串联起来\ntask prepareFile(dependsOn: [\n        'createDirs',\n        'copyLibs',\n        'copyConf',\n        'copyBin',\n        'copyDep'\n]) {}//如果没有内容的话,可以不需要大括号\n//还有一种写法表示task之间的依赖:prepareFile.dependsOn createDirs,copyLibs,copyConf,copyBin,copyDep\n//指定打包的tar包的名字，以及文件来源目录\n//distributions {\n//    monitor {\n//        baseName = 'azkaban-monitor'\n//        contents {\n//            from {'build/package'}\n//        }\n//    }\n//}\n\n//distribution 插件的特性\n//monitorDistTar.dependsOn 'prepareFile'\n//monitorDistTar.compression = Compression.NONE\n//monitorDistTar.extension = 'tar'\n\n//定义一个task，先build 然后再打包tar包\n//task buildTar(dependsOn: [\n//        'build',\n//        monitorDistTar\n//]) {}"
  },
  {
    "path": "document/7788.markdown",
    "content": "# 7788篇\n\n> **FunTester**，[腾讯云年度作者](https://mp.weixin.qq.com/s/oeTeJZs6h4jJJMRyUunurw)、[Boss直聘签约作者](https://mp.weixin.qq.com/s/CjwFBoS9sew6R74mM9QO4g)，非著名测试开发er，欢迎关注。\n\n* `Gitee`地址*https://gitee.com/fanapi/tester*\n* `GitHub`地址*https://github.com/JunManYuanLong/FunTester*\n\n\n# 无代码合集\n\n## 理论鸡汤\n\n- [写给所有人的编程思维](https://mp.weixin.qq.com/s/Oj33UCnYfbUgzsBzEm2GPQ)\n- [成为杰出Java开发人员的10个步骤](https://mp.weixin.qq.com/s/UCNOTSzzvTXwiUX6xpVlyA)\n- [测试之《代码不朽》脑图](https://mp.weixin.qq.com/s/2aGLK3knUiiSoex-kmi0GA)\n- [为什么选择软件测试作为职业道路？](https://mp.weixin.qq.com/s/o83wYvFUvy17kBPLDO609A)\n- [自动化测试的障碍](https://mp.weixin.qq.com/s/ZIV7uJp7DzVoKhWOh6lvRg)\n- [自动化测试的问题所在](https://mp.weixin.qq.com/s/BhvD7BnkBU8hDBsGUWok6g)\n- [成为优秀自动化测试工程师的7个步骤](https://mp.weixin.qq.com/s/wdw1l4AZnPpdPBZZueCcnw)\n- [优秀软件开发人员的态度](https://mp.weixin.qq.com/s/0uEEeFaR27aTlyp-sm61bA)\n- [如何正确执行功能API测试](https://mp.weixin.qq.com/s/aeGx5O_jK_iTD9KUtylWmA)\n- [未来10年软件测试的新趋势-上](https://mp.weixin.qq.com/s/9XgpIfXQRuKg1Pap-tfqYQ)\n- [未来10年软件测试的新趋势-下](https://mp.weixin.qq.com/s/k2rZaeHoq4AX19CUzjGRVQ)\n- [自动化测试解决了什么问题](https://mp.weixin.qq.com/s/96k2I_OBHayliYGs2xo6OA)\n- [17种软件测试人员常用的高效技能-上](https://mp.weixin.qq.com/s/vrM_LxQMgTSdJxaPnD_CqQ)\n- [17种软件测试人员常用的高效技能-下](https://mp.weixin.qq.com/s/uyWdVm74TYKb62eIRKL7nQ)\n- [手动测试存在的重要原因](https://mp.weixin.qq.com/s/mW5vryoJIkeskZLkBPFe0Q)\n- [编写测试用例的技巧](https://mp.weixin.qq.com/s/zZAh_XXXGOyhlm6ebzs06Q)\n- [成为自动化测试的7种技能](https://mp.weixin.qq.com/s/e-HAGMO0JLR7VBBWLvk0dQ)\n- [功能测试与非功能测试](https://mp.weixin.qq.com/s/oJ6PJs1zO0LOQSTRF6M6WA)\n- [自动化和手动测试，保持平衡！](https://mp.weixin.qq.com/s/mMr_4C98W_FOkks2i2TiCg)\n- [43种常见软件测试分类](https://mp.weixin.qq.com/s/GTMkcEm-xPtVF7_HxXGKDg)\n- [自动化测试生命周期](https://mp.weixin.qq.com/s/SH-vb2RagYQ3sfCY8QM5ew)\n- [代码审查如何保证软件质量](https://mp.weixin.qq.com/s/osRnG09KDqEojiV3kp2nrw)\n- [TDD测试驱动开发的基础](https://mp.weixin.qq.com/s/diW_2HSbWMEsn8G6uQriOg)\n- [如何在DevOps引入自动化测试](https://mp.weixin.qq.com/s/MclK3VvMN1dsiXXJO8g7ig)\n- [自动化的好处](https://mp.weixin.qq.com/s/7MpWQhtozaTrlUMo1oRSBg)\n- [Web端自动化测试失败原因汇总](https://mp.weixin.qq.com/s/qzFth-Q9e8MTms1M8L5TyA)\n- [测试人员如何成为变革的推动者](https://mp.weixin.qq.com/s/0nTZHBOuKG0rewKAeyIqwA)\n- [探索性测试为何如此重要？](https://mp.weixin.qq.com/s/nebHPfKbCO0f-G24qCh9wA)\n- [5种促进业务增长的软件测试策略](https://mp.weixin.qq.com/s/3mB_DQVD2AZLPs84SmsmuA)\n- [如何选择正确的自动化测试工具](https://mp.weixin.qq.com/s/_Ee78UW9CxRpV5MoTrfgCQ)\n- [如何从测试自动化中实现价值](https://mp.weixin.qq.com/s/dj-sJvGjvFMYANfhIVo8jw)\n- [您如何使用Selenium来计算自动化测试的投资回报率？](https://mp.weixin.qq.com/s/DVSEm0DhoAvYfTWIniabJg)\n- [如何在DevOps中实施连续测试](https://mp.weixin.qq.com/s/snPXkH6WEZ2kteYP_-c5_g)\n- [自动化如何选择用例](https://mp.weixin.qq.com/s/1hH5YIle4YQimJr4iGSWlA)\n- [成功实施自动化测试的优点](https://mp.weixin.qq.com/s/UENdSU-NPa5AOVC9ciiy0Q)\n- [测试人员常用借口](https://mp.weixin.qq.com/s/0k_Ciud2sOpRb5PPiVzECw)\n- [测试自动化的边缘DevTestOps](https://mp.weixin.qq.com/s/kCySRYdCS11CA-lF30AtQA)\n- [筛选自动化测试用例的技巧](https://mp.weixin.qq.com/s/SWNopZLwgpj9yYsVEHEspw)\n- [什么阻碍手动测试发挥价值](https://mp.weixin.qq.com/s/t0VAVyA3ywQsHzaqzSILOw)\n- [未来的QA测试工程师](https://mp.weixin.qq.com/s/ngL4sbEjZm7OFAyyWyQ3nQ)\n- [Web安全检查](https://mp.weixin.qq.com/s/SewUV3GMaNKD2P7g64ctYQ)\n- [关于可用性测试](https://mp.weixin.qq.com/s/aUIg40scOWzbRR89ojJWLg)\n- [如何实施DevOps](https://mp.weixin.qq.com/s/UPIL942eOKR1bY0mbC-42w)\n- [黑盒测试和白盒测试](https://mp.weixin.qq.com/s/5kvrYMWG0vFR3vj-aNY49g)\n- [测试用例中的细节](https://mp.weixin.qq.com/s/wvScTliPwuvH9ReIoDQGNQ)\n- [集成测试、单元测试、系统测试](https://mp.weixin.qq.com/s/LRkxMasRvmDYRVb0_aybtA)\n- [集成测试类型和最佳实践](https://mp.weixin.qq.com/s/sSubzrs3cikLV7rmRQaWEA)\n- [软件测试中质量优于数量](https://mp.weixin.qq.com/s/4FxtVFqialRz6R4680rPAw)\n- [DevOps工具](https://mp.weixin.qq.com/s/4r8FoxQyYZ5naowML5Cw-Q)\n- [2020年Tester自我提升](https://mp.weixin.qq.com/s/vuhUp85_6Sbg6ReAN3TTSQ)\n- [DevOps中的测试工程师](https://mp.weixin.qq.com/s/42Ile_T1BAIp7QHleI-c7w)\n- [敏捷团队的回归测试策略](https://mp.weixin.qq.com/s/Z7dzDdfp5_kxvzBVQ3rEDg)\n- [测试自动化与自动化测试：差异很重要](https://mp.weixin.qq.com/s/6HC1bKesOs4mZYb9nOCHjw)\n- [自动化新手要避免的坑（上）](https://mp.weixin.qq.com/s/MjcX40heTRhEgCFhInoqYQ)\n- [自动化新手要避免的坑（下）](https://mp.weixin.qq.com/s/azDUo1IO5JgkJIS9n1CMRg)\n- [如何成为全栈自动化工程师](https://mp.weixin.qq.com/s/j2rQ3COFhg939KLrgKr_bg)\n- [左移测试](https://mp.weixin.qq.com/s/8zXkWV4ils17hUqlXIpXSw)\n- [选择手动测试还是自动化测试？](https://mp.weixin.qq.com/s/4haRrfSIp5Plgm_GN98lRA)\n- [从单元测试标准中学习](https://mp.weixin.qq.com/s/x0TyMAdPBWYL7JSPAmoQsw)\n- [负载测试很重要](https://mp.weixin.qq.com/s/2q7kNQVJuNwB948ks463CA)\n- [白盒测试扫盲](https://mp.weixin.qq.com/s/s_FvGZTC42GEjaWzroz1eA)\n- [自动化测试项目为何失败](https://mp.weixin.qq.com/s/KFJXuLjjs1hii47C1BH8PA)\n- [简化测试用例](https://mp.weixin.qq.com/s/BhwfDqhN9yoa3Iul_Eu5TA)\n- [敏捷测试二三事](https://mp.weixin.qq.com/s/bKkGWJA3JhvdCjgg6-AVEQ)\n- [软件测试中的虚拟化](https://mp.weixin.qq.com/s/zHyJiNFgHIo2ZaPFXsxQMg)\n- [新词：QA-Ops](https://mp.weixin.qq.com/s/detcY6OVYmzOTUxfwN6CFQ)\n- [生产环境中进行自动化测试](https://mp.weixin.qq.com/s/JKEGRLOlgpINUxs-6mohzA)\n- [所谓UI测试](https://mp.weixin.qq.com/s/wDvUy_BhQZCSCqrlC2j1qA)\n- [预上线环境失败的原因](https://mp.weixin.qq.com/s/jva0Jb2OMarERmTn7Kh2Ng)\n- [自动化策略六步走](https://mp.weixin.qq.com/s/He69k8iCKhTKD1j-yV6M5g)\n- [合格的测试经理必备技能](https://mp.weixin.qq.com/s/gFIYksHMn_bHEwAhmgVzjg)\n- [质量保障的拓展实践](https://mp.weixin.qq.com/s/a3sd0dQnjk3TerOhfo-1ng)\n- [敏捷领导者常见误区](https://mp.weixin.qq.com/s/xdq3CZflRjvDBGDLK4tNFQ)\n- [功能自动化测试策略](https://mp.weixin.qq.com/s/qHmcblN4cD4JK6jT7oU4fQ)\n- [性能测试、压力测试和负载测试](https://mp.weixin.qq.com/s/g26lpd7d7EtpN7pkiqkkjg)\n- [如何维护自动化测试](https://mp.weixin.qq.com/s/4eh4AN_MiatMSkoCMtY3UA)\n- [负载测试最佳实践](https://mp.weixin.qq.com/s/hNj7UsCCvv9TdexAcNFUvg)\n- [有关UI测试计划](https://mp.weixin.qq.com/s/D0fMXwJF754a7Mr5ARY5tQ)\n- [软件测试外包](https://mp.weixin.qq.com/s/sYQfb2PiQptcT0o_lLpBqQ)\n- [避免PPT自动化的最佳实践](https://mp.weixin.qq.com/s/5YgYK4_YLZ1wDDhbwMTGlw)\n- [如何优化软件测试成本](https://mp.weixin.qq.com/s/_eXrzDyNDA6yCRR8nPmzGA)\n- [如何从手动测试转到自动化测试](https://mp.weixin.qq.com/s/EBDTX4AMnn2KTEjL88bOhQ)\n- [Selenium自动化测试技巧](https://mp.weixin.qq.com/s/EzrpFaBSVITO2Y2UvYvw0w)\n- [测试为何会错过Bug](https://mp.weixin.qq.com/s/UFHy8OwZjnMkB70roIS-zQ)\n- [测试用例设计——一切测试的基础](https://mp.weixin.qq.com/s/0_ubnlhp2jk-jxHxJ95E9g)\n- [移动应用测试：挑战，类型和最佳实践](https://mp.weixin.qq.com/s/kYxh6xki69evVDsXDxrYKQ)\n- [敏捷测试中面临的挑战](https://mp.weixin.qq.com/s/vmsW56r1J7jWXHSZdcwbPg)\n- [AI如何影响测试行业](https://mp.weixin.qq.com/s/d6c7u1-lAmsiIQz3UvcGKg)\n- [自动测试失败的5个原因](https://mp.weixin.qq.com/s/bTakAHIcx_WyJIo-tsbvvg)\n- [大促前必做的质量检查](https://mp.weixin.qq.com/s/iOku2wKnlr8pSZO0l9Q3Bw)\n- [测试开发工程师工作技巧](https://mp.weixin.qq.com/s/TvrUCisja5Zbq-NIwy_2fQ)\n- [敏捷回归测试](https://mp.weixin.qq.com/s/_bBQFggkZTTEqcb9R_68OA)\n- [制定质量管理计划指南](https://mp.weixin.qq.com/s/ztXYE8EtwlkUdxnk1cjKVg)\n- [质量管理计划的基本要素](https://mp.weixin.qq.com/s/v8lOioYn01S1F0ex4mmljA)\n- [质量保障的方法和实践](https://mp.weixin.qq.com/s/hU_YCaZB-0a09dOCAVgcpw)\n- [为什么测试覆盖率如此重要](https://mp.weixin.qq.com/s/0evyuiU2kdXDgMDnDKjORg)\n- [自动化测试框架](https://mp.weixin.qq.com/s/vu6p_rQd3vFKDYu8JDJ0Rg)\n- [敏捷中的端到端测试](https://mp.weixin.qq.com/s/cdi4xnEzDLpl9ncQguLuAQ)\n- [自动化测试灵魂三问：是什么、为什么和做什么](https://mp.weixin.qq.com/s/geOejJx79-jTwafG9aXwqA)\n- [基于代码的自动化和无代码自动化](https://mp.weixin.qq.com/s/8Dopihqs4XzpU-sN-I94kw)\n- [物联网测试](https://mp.weixin.qq.com/s/B_JI4DANxoOq4HurxZC65Q)\n- [功能测试知多少](https://mp.weixin.qq.com/s/vTxZLwlvlfIBv892Ji-oLQ)\n- [如何选择自动化测试工具](https://mp.weixin.qq.com/s/yJo-d9bAZDs1Lcp8j7ISRg)\n- [连续测试策略](https://mp.weixin.qq.com/s/0aD_0cUW83oPu3sl7sHNnQ)\n- [如何设置质量检查流程](https://mp.weixin.qq.com/s/PQeXxMZzzU15xSfY5wkVgA)\n- [编写干净的代码之变量篇](https://mp.weixin.qq.com/s/J9rGIe8a2xaLlNJq2nVmzw)\n- [高效Mobile DevOps步骤](https://mp.weixin.qq.com/s/-qc-d_zJ1C9H_Uvd8gJiBw)\n- [回归BUG](https://mp.weixin.qq.com/s/00j-acjPeKQ7uap62WpY3w)\n- [处理回归BUG最佳实践](https://mp.weixin.qq.com/s/R3O2NruPAA2gQf4-3R6aAQ)\n- [自动化测试实践清单](https://mp.weixin.qq.com/s/972WruGsYmkRroquBFoqMg)\n- [自动化测试类型](https://mp.weixin.qq.com/s/GRkN8ozZiWNu21Y3KbVOBA)\n- [无脚本测试](https://mp.weixin.qq.com/s/PVBxk4KEwCmWkB6mOXJFlw)\n- [自动化测试转型挑战及其解决方案](https://mp.weixin.qq.com/s/BixS6jRdF5N_nvmW3_OthQ)\n- [无数据驱动自动化测试](https://mp.weixin.qq.com/s/aCYRGxkzMogLbmACYo6ssw)\n- [为什么自动化测试在敏捷开发中很重要](https://mp.weixin.qq.com/s/AP0wUQZ09NvSqme8e09igQ)\n- [测试模型中理解压力测试和负载测试](https://mp.weixin.qq.com/s/smNLx3malzM3avkrn3EJiA)\n- [移动测试工程师职业](https://mp.weixin.qq.com/s/dhtR4TbQNu5fWpmJkXGivw)\n- [远程测试工作挑战](https://mp.weixin.qq.com/s/LK-GEN4OtuWVGDuG8psmOQ)\n- [自动化测试用例的原子性](https://mp.weixin.qq.com/s/jA5WMHwJcu88nHXWoMBAdQ)\n- [可测性经验分享](https://mp.weixin.qq.com/s/iRtUjESYS3sh3YTD-BWjdA)\n- [敏捷中的回归测试的优化【译】](https://mp.weixin.qq.com/s/nDiZZgA1PIiAUCG_xwA2rA)\n- [敏捷的主要优势【译】](https://mp.weixin.qq.com/s/zkI85TLI37XrPFaQ-pZYMA)\n\n## 大咖风采\n\n- [Tcloud 云测平台--集大成者](https://mp.weixin.qq.com/s/29sEO39_NyDiJr-kY5ufdw)\n- [Android App 测试工具及知识大集合](https://mp.weixin.qq.com/s/Xk9rCW8whXOTAQuCfhZqTg)\n- [Android App常规测试内容](https://mp.weixin.qq.com/s/tweeoS5wTqK3k7R2TVuDXA)\n- [JVM的对象和堆](https://mp.weixin.qq.com/s/iNDpTz3gBK3By_bvUnrWOA)\n\n# UI自动化\n\n## UI自动化\n\n- [自动化测试中java多线程的使用实例](https://mp.weixin.qq.com/s/BNSLaIdcTPTNj1tKpGf6fw)\n- [自动化测试中递归函数的应用](https://mp.weixin.qq.com/s/86602zV9zYblhCRMiUlwdA)\n\n## UiAutomator\n\n- [android uiautomator一个画心形图案的方法--代码的浪漫](https://mp.weixin.qq.com/s/byfAKHxD2i83VHnuaNgIZA)\n- [android UiAutomator了解源码解决控件bonds\\[0,0\\]无法点击](https://mp.weixin.qq.com/s/nu2ftXNUSG2_kmZjyhEcVA)\n- [android UiAutomator在清除文本时遇到中文的解决办法](https://mp.weixin.qq.com/s/cNGNCoXsYBSk-MWTWLxF4g)\n- [android UiAutomator获取当前页面某类控件个数的方法](https://mp.weixin.qq.com/s/njb19Sq_Kg4SusAS_eEuug)\n- [android uiautomator自定义监听示例--一个弹出权限设置的监听](https://mp.weixin.qq.com/s/OKKZOf51yq6qY5D6PvN-gg)\n- [如何在Mac OS上使用UiAutomator快速调试类](https://mp.weixin.qq.com/s/jm9d_42jp_BSlv-IW0BpzQ)\n- [UiAutomator测试中如何恢复手机输入法](https://mp.weixin.qq.com/s/o4-zCgbdq6OsHRK9XT14QA)\n- [android UiAutomator基本api的二次封装](https://mp.weixin.qq.com/s/_3jGg3ZYoeyAkjZpy8gWfQ)\n- [android UiAutomator让运行失败的用例重新运行](https://mp.weixin.qq.com/s/tMOPbt1w9tRaKEuIZYKCyg)\n- [利用UiAutomator写一个首页刷新的稳定性测试脚本](https://mp.weixin.qq.com/s/au9hAScsqUdcrh_usu8Pfg)\n- [android UiAutomator长按实现控制按住控件时间的方法](https://mp.weixin.qq.com/s/lOvxAOMh6mmIh3CEV6Fduw)\n- [android UiAutomator自定义快速调试类](https://mp.weixin.qq.com/s/iP2dTOeVkFMzU3dQ06R9GA)\n- [利用UiAutomator写一个自动遍历渠道包关键功能的脚本](https://mp.weixin.qq.com/s/0vg2OlfTy0y4T6sWUG-olA)\n- [android UiAutomator如何根据颜色判断控件的状态](https://mp.weixin.qq.com/s/kldsD3OZ4mJZ5yYQXfXxLw)\n- [android UiAutomator控制多台手机同时运行用例的方法](https://mp.weixin.qq.com/s/z9vgpOQP0wQffmG4C_oBWg)\n- [android UiAutomator使用递归函数写一个让屏幕一闪一闪提醒的方法](https://mp.weixin.qq.com/s/AzXjePdmsgs6QsICZOdPyw)\n- [android UiAutomator获取视频播放进度的方法](https://mp.weixin.qq.com/s/ho070zX9rrLPmh8bZe_HgQ)\n\n\n## Selenium\n\n- [selenium2java截图保存桌面](https://mp.weixin.qq.com/s/OUfwsIo635coGONRNccYlg)\n- [selenium2java调用JavaScript方法封装](https://mp.weixin.qq.com/s/t-Xs2Hr9TM2bjDiOqQX2mA)\n- [selenium2java利用mysq解决向浏览器插入cookies时token过期问题](https://mp.weixin.qq.com/s/oAAkDKUGytQjxJLFkod-AQ)\n- [selenium2java 遇到有三个窗口用例的处理办法](https://mp.weixin.qq.com/s/6AJBanVKYwlsNcvsu_25QQ)\n- [selenium2java通过第三方登录绕过知乎登陆验证码](https://mp.weixin.qq.com/s/A5uTtxlg4l4pru2z7v1cug)\n- [selenium2java使用select处理下拉框示例](https://mp.weixin.qq.com/s/FFor451WzuUzINeclGN-Ng)\n- [selenium2java爬虫示例](https://mp.weixin.qq.com/s/vSZzpzEqsCtASSx6iHqxVA)\n- [selenium2java写一个设置秒杀价的脚本](https://mp.weixin.qq.com/s/1ocIOYt3gdGIJrd9v2shhg)\n- [selenium2java基本方法二次封装](https://mp.weixin.qq.com/s/2GaXigt13wa6JgxJkcef5g)\n- [selenium2java一个弹框上传时间日期大杂烩测试用例](https://mp.weixin.qq.com/s/Z8ZeZ-zFy0q0a-e_epT1Kg)\n- [selenium2java造数据例子](https://mp.weixin.qq.com/s/ACO2O5f7Po4Qn242lopMBg)\n- [selenium2java让浏览器停止加载的方法](https://mp.weixin.qq.com/s/aBQdGYys3Bpyf6yigGOCIA)\n- [selenium2java写一个强制刷新页面的方法](https://mp.weixin.qq.com/s/VWW7cH5WSDmw_eCabUh9LQ)\n- [selenium2java通过接口获取并注入cookies](https://mp.weixin.qq.com/s/luLHWxPWSekuDMbnKsfJvg)\n- [Selenium编写自动化用例的8种技巧](https://mp.weixin.qq.com/s/8wRHc_krXNfWclNeOJDNPg)\n- [JUnit中用于Selenium测试的中实践](https://mp.weixin.qq.com/s/KG4sltQMCfH2MGXkRdtnwA)\n- [您如何使用Selenium来计算自动化测试的投资回报率？](https://mp.weixin.qq.com/s/DVSEm0DhoAvYfTWIniabJg)\n- [Selenium 4 Java的最佳测试框架](https://mp.weixin.qq.com/s/MlNyv-kb03gRTcYllxUreA)\n- [Selenium 4.0 Alpha更新日志](https://mp.weixin.qq.com/s/tU7sm-pcbpRNwDU9D3OVTQ)\n- [Selenium 4.0 Alpha更新实践](https://mp.weixin.qq.com/s/yT9wpO5o5aWBUus494TIHw)\n- [JUnit 5和Selenium基础（一）](https://mp.weixin.qq.com/s/ehBRf7st-OxeuvI_0yW3OQ)\n- [JUnit 5和Selenium基础（二）](https://mp.weixin.qq.com/s/Gt82cPmS2iX-DhKXTXiy8g)\n- [JUnit 5和Selenium基础（三）](https://mp.weixin.qq.com/s/8YkonXTYgAV5-pLs9yEAVw)\n- [如何在跨浏览器测试中提高效率](https://mp.weixin.qq.com/s/MB_Wv7yQ6i9BztAZtL4grA)\n- [Selenium Python使用技巧（一）](https://mp.weixin.qq.com/s/39v8tXG3xig63d-ioEAi8Q)\n- [Selenium Python使用技巧（二）](https://mp.weixin.qq.com/s/uDM3y9zoVjaRmJJJTNs6Vw)\n- [Selenium Python使用技巧（三）](https://mp.weixin.qq.com/s/J7-CO-UDspUGSpB8isjsmQ)\n- [Selenium并行测试基础](https://mp.weixin.qq.com/s/OfXipd7YtqL2AdGAQ5cIMw)\n- [Selenium并行测试最佳实践](https://mp.weixin.qq.com/s/-RsQZaT5pH8DHPvm0L8Hjw)\n- [维护Selenium测试自动化的最佳实践](https://mp.weixin.qq.com/s/EMD1aWuzOSfT7j3KeXhJcA)\n- [Selenium自动化测试技巧](https://mp.weixin.qq.com/s/EzrpFaBSVITO2Y2UvYvw0w)\n- [Selenium自动化：代码测试与无代码测试](https://mp.weixin.qq.com/s/gtmLpQ5FCeuzh1SB5mxuvg)\n- [Selenium处理下拉列表](https://mp.weixin.qq.com/s/E2txSVAmDzYIEZWnyAND4g)\n- [Selenium自动化常见问题](https://mp.weixin.qq.com/s/edoxu-QaD0SOw1VqrhCZWA)\n- [Selenium4 IDE，它终于来了](https://mp.weixin.qq.com/s/XNotlZvFpmBmBQy1pYifOw)\n- [Selenium4 IDE特性：无代码趋势和SIDE Runner](https://mp.weixin.qq.com/s/G0S9K0jHsN0P_jxdMME-cg)\n- [Selenium4 IDE特性：弹性测试、循环和逻辑判断](https://mp.weixin.qq.com/s/o4_jIyi9O7s4S3CbTzl5rQ)\n- [Selenium自动化最佳实践技巧（上）](https://mp.weixin.qq.com/s/lZww1azmncMMMHRY0_yKqA)\n- [Selenium自动化最佳实践技巧（中）](https://mp.weixin.qq.com/s/9D0lUZ-XKHiukNeRqp6zOQ)\n- [Selenium自动化最佳实践技巧（下）](https://mp.weixin.qq.com/s/opVik2ZxmTBurIBoa4yipQ)\n- [Selenium等待：sleep、隐式、显式和Fluent](https://mp.weixin.qq.com/s/73BobMq9M12rYMvzxNhRtA)\n- [Selenium自动化的JUnit参数化实践](https://mp.weixin.qq.com/s/WFu5rJaowxhAIcbEoEatkw)\n- [Selenium异常集锦](https://mp.weixin.qq.com/s/DDkaliSVthX-c_KKG-WwNA)\n- [Selenium自动化测试之前](https://mp.weixin.qq.com/s/DKjSnS9sP0SoHUw4OhOikw)\n\n## APP性能\n\n- [使用monkey测试时，一个控制WiFi状态的多线程类](https://mp.weixin.qq.com/s/P8HVtzHBlj_FcDAAHFKBDg)\n- [java执行和停止Logcat命令及多线程实现](https://mp.weixin.qq.com/s/sUYibRc-muxQoxi48QiaRg)\n- [APP性能测试中获取CPU和PSS数据多线程实现](https://mp.weixin.qq.com/s/NiJSZ8VxpdnarbDJjcJziA)\n- [统计APP启动时间和进入首页时间的多线程类](https://mp.weixin.qq.com/s/IMs6vd3H-HF65Vb-zPwDhw)\n- [如何获取手机性能测试数据FPS](https://mp.weixin.qq.com/s/qZy5AQkNpUXRJk46BHVzaQ)\n- [一个循环启动APP并保持WiFi常开的多线程类](https://mp.weixin.qq.com/s/OgdT4IffDyAdkKmO2SS9iQ)\n\n## 杂乱\n\n- [测试窝，首页抄我七篇原创还拉黑，你们的良心不会痛吗？](https://mp.weixin.qq.com/s/ke5avkknkDMCLMAOGT7wiQ)\n- [如何优雅地屏蔽掉Google搜索结果中视频、新闻、图片等结果](https://mp.weixin.qq.com/s/Iu7pt4Qk3w9sJp3n_UVAeQ)\n- [测试玩梗--欢迎补充](https://mp.weixin.qq.com/s/y_QHbsjFCQVSCfj-A4Usmg)\n- [图解HTTP脑图](https://mp.weixin.qq.com/s/100Vm8FVEuXs0x6rDGTipw)\n- [测试之JVM命令脑图](https://mp.weixin.qq.com/s/qprqyv0j3SCvGw1HMjbaMQ)\n- [好书推荐《Java性能权威指南》](https://mp.weixin.qq.com/s/YWd5Yx6n7887g1lMLTcsWQ)\n- [2019年浏览器市场份额排行榜](https://mp.weixin.qq.com/s/4NmJ_ZCPD5UwaRCtaCfjEg)\n- [JSON基础](https://mp.weixin.qq.com/s/tnQmAFfFbRloYp8J9TYurw)\n- [JMeter吞吐量误差分析](https://mp.weixin.qq.com/s/jHKmFNrLmjpihnoigNNCSg)\n- [JMeter如何模拟不同的网络速度](https://mp.weixin.qq.com/s/1FCwNN2htfTGF6ItdkcCzw)\n- [疫情期间，如何提高远程办公效率](https://mp.weixin.qq.com/s/k_XrdqjGKMshK2Ea-VCNLw)\n- [接口测试视频专题](https://mp.weixin.qq.com/s/4mKpW3QiVRee3kcVOSraog)\n- [Groovy在JMeter中应用专题](https://mp.weixin.qq.com/s/KcxPUDWl7MLQemFRoIV92A)\n- [Java多线程编程在JMeter中应用](https://mp.weixin.qq.com/s/xCnFx5TvIF1SAVNm-aZnxQ)\n- [未来的神器fiddler Everywhere](https://mp.weixin.qq.com/s/-BSuHR6RPkdv8R-iy47MLQ)\n- [Charles报错Failed to install helper解决方案](https://mp.weixin.qq.com/s/LHhMTBhlDM0DrPCvWeU0zA)\n- [测试仓库推介（上）](https://mp.weixin.qq.com/s/zgy6UgNMFcbISD1NhxSAWg)\n- [测试仓库推介（下）](https://mp.weixin.qq.com/s/njnpmRGoEgdxjqkR7c3a6A)\n- [Fiddler Everywhere工具答疑](https://mp.weixin.qq.com/s/2peWMJ-rgDlVjs3STNeS1Q)\n- [Mac上测试Internet Explorer的N种方法](https://mp.weixin.qq.com/s/HeLBPTp2dfs5IlyLMCi90Q)\n- [IntelliJ中基于文本的HTTP客户端](https://mp.weixin.qq.com/s/-9qi_lLVVfxQKEFmcRYFtA)\n- [开源礼节](https://mp.weixin.qq.com/s/EyNules2f9NYdnYAX_NQSw)\n- [弱网测试：最低流畅网速是多少？](https://mp.weixin.qq.com/s/rCji6fZs9yYyk7GyIWvSiA)\n- [接口测试直播回顾](https://mp.weixin.qq.com/s/B8ih9sswaE-OWuVib6C16g)\n- [SpotBugs报错no Groovy library is defined解决办法](https://mp.weixin.qq.com/s/XxvuVS2TmlqT5-b22vObYQ)\n- [推荐好书：不要总是谦卑地弯着腰](https://mp.weixin.qq.com/s/mYNN9jSaikOF5aJEkb-Bug)\n- [2020年FunTester自我总结](https://mp.weixin.qq.com/s/DeDY1JZUTk3cjjQfr3DJRg)\n- [原创打油诗欣赏](https://mp.weixin.qq.com/s/3hPSDjH-3cWu6EVsjU0wOw)\n- [优秀讲师 | 腾讯云+社区权威认证](https://mp.weixin.qq.com/s/oeTeJZs6h4jJJMRyUunurw)\n- [Boss直聘签约作者](https://mp.weixin.qq.com/s/CjwFBoS9sew6R74mM9QO4g)\n- [假期思考题](https://mp.weixin.qq.com/s/3DOnkmYDlwk-XKg4ge3ZUw)\n- [甩锅技能+1](https://mp.weixin.qq.com/s/nMwlfXZoDcRRPHcTKpvfNg)\n"
  },
  {
    "path": "document/api.markdown",
    "content": "# 接口篇\n\n\n##### **FunTester**，[腾讯云年度作者](https://mp.weixin.qq.com/s/oeTeJZs6h4jJJMRyUunurw)、[Boss直聘签约作者](https://mp.weixin.qq.com/s/CjwFBoS9sew6R74mM9QO4g)，非著名测试开发er，欢迎关注。\n\n* `Gitee`地址*https://gitee.com/fanapi/tester*\n* `GitHub`地址*https://github.com/JunManYuanLong/FunTester*\n\n# 接口测试\n\n## 接口功能测试\n\n- [开源测试服务](https://mp.weixin.qq.com/s/ZOs0cp_vt6_iiundHaKk4g)\n- [使用springboot+mybatis数据库存储服务化](https://mp.weixin.qq.com/s/N_5tHW1JJLZlxCaDI2PvyQ)\n- [alertover推送api的java httpclient实现实例](https://mp.weixin.qq.com/s/DJXCBEG3SbybfbT6blO1jA)\n- [接口自动化通用验证类](https://mp.weixin.qq.com/s/fP1clCKkLREfg6POKV5n1A)\n- [将swagger文档自动变成测试代码](https://mp.weixin.qq.com/s/SY8mVenj0zMe5b47GS9VSQ)\n- [httpclient处理多用户同时在线](https://mp.weixin.qq.com/s/Nuc30Fwy6-Qyr-Pc65t1_g)\n- [使用httpclient实现图灵机器人web api调用实例](https://mp.weixin.qq.com/s/dYyxvAhwSmJkNI8N9lYQfg)\n- [groovy如何使用java接口测试框架发送http请求](https://mp.weixin.qq.com/s/KF5lzMT-E2IBOkp_UjuC4g)\n- [httpclient调用京东万象数字营销频道新闻api实例](https://mp.weixin.qq.com/s/kSqgSbPci-q2pfsdcU5Ekw)\n- [httpclient遇到socket closed解决办法](https://mp.weixin.qq.com/s/mDRC7mssKmnvcI6StQWIBQ)\n- [httpclient4.5如何确保资源释放](https://mp.weixin.qq.com/s/373Lx1bv0vi-pIBgWNzC9Q)\n- [httpclient如何处理302重定向](https://mp.weixin.qq.com/s/vg354AjPKhIZsnSu4GZjZg)\n- [基于java的直线型接口测试框架初探](https://mp.weixin.qq.com/s/xhg4exdb1G18-nG5E7exkQ)\n- [利用alertover发送获取响应失败的通知消息](https://mp.weixin.qq.com/s/w6y2UkgL3J20mAxc8fq0tA)\n- [使用httpclient中EntityUtils类解析entity遇到socket closed错误的原因](https://mp.weixin.qq.com/s/RJnuOa2K6aRCElJafkFeug)\n- [httpclient接口测试中重试控制器设置](https://mp.weixin.qq.com/s/hknNdq_ybQ1MoXh_dI3JVA)\n- [拼接GET请求的参数](https://mp.weixin.qq.com/s/EGw_97scexH_3m2Uc8Ye5A)\n- [httpclient上传文件方法的封装](https://mp.weixin.qq.com/s/HIrwl5ullvEmn_UuyLKkRg)\n- [接口批量上传文件的实例](https://mp.weixin.qq.com/s/wZwkWchXXC6iddX1oVEnZQ)\n- [httpclient发送https协议请求以及javax.net.ssl.SSLHandshakeException解决办法](https://mp.weixin.qq.com/s/uSHhKRrL2f9USKpSykkpkQ)\n- [API测试基础](https://mp.weixin.qq.com/s/bkbUEa9CF21xMYSlhPcULw)\n- [拷贝HttpRequestBase对象](https://mp.weixin.qq.com/s/kxB1c0GmSF5OAM15UQJU2Q)\n- [API自动化测试指南](https://mp.weixin.qq.com/s/uy_Vn_ZVUEu3YAI1gW2T_A)\n- [如何统一接口测试的功能、自动化和性能测试用例](https://mp.weixin.qq.com/s/1xqtXNVw7BdUa03nVcsMTg)\n- [如何选择API测试工具](https://mp.weixin.qq.com/s/m2TNJDiqAAWYV9L6UP-29w)\n- [初学者的API测试技巧](https://mp.weixin.qq.com/s/_uk6dw5Q7CfS-gXGH-TZEQ)\n- [压测中测量异步写入接口的延迟](https://mp.weixin.qq.com/s/odvK1iYgg4eRVtOOPbq15w)\n- [多项目登录互踢测试用例](https://mp.weixin.qq.com/s/Nn_CUy_j7j6bUwHSkO0pCQ)\n- [httpclient使用HTTP代理实践](https://mp.weixin.qq.com/s/24IJwJ1TJWHdfj0PzSjmvw)\n- [HTTP异步连接池和多线程实践](https://mp.weixin.qq.com/s/8M348LuHakBe4GAEnnDPxw)\n- [IntelliJ中基于文本的HTTP客户端](https://mp.weixin.qq.com/s/-9qi_lLVVfxQKEFmcRYFtA)\n- [socket接口开发和测试初探](https://mp.weixin.qq.com/s/uhmkbrMp91PP1pQjlEofOQ)\n- [IntelliJ中基于文本的HTTP客户端](https://mp.weixin.qq.com/s/-9qi_lLVVfxQKEFmcRYFtA)\n- [基于WebSocket的client封装](https://mp.weixin.qq.com/s/1lZvsuGEa6hiRHOgOT-Kmg)\n- [基于Socket.IO的Client封装](https://mp.weixin.qq.com/s/Ux90AXI9g85w7R5i3f9idg)\n- [Socket.IO接口多用户测试实践](https://mp.weixin.qq.com/s/aCLaRZQs8zMK_ptJ-PjClw)\n- [Python版Socket.IO接口测试脚本](https://mp.weixin.qq.com/s/oXBP6Sx3yPqlmvV9uCUScw)\n- [命令行如何执行jar包里面的方法](https://mp.weixin.qq.com/s/50oMEmVEnv5Vzlm1HOxuFw)\n- [JSON对象标记语法验证类](https://mp.weixin.qq.com/s/jSXmoEdMF7nWAqQuzJ5GiQ)\n- [Socket接口异步验证实践](https://mp.weixin.qq.com/s/bnjHK3ZmEzHm3y-xaSVkTw)\n- [无数据驱动自动化测试](https://mp.weixin.qq.com/s/aCYRGxkzMogLbmACYo6ssw)\n- [白板点阵数据传输测试初探](https://mp.weixin.qq.com/s/EzFC-hIvgm7j7947TZU6BA)\n- [基于Socket.IO的白板点阵坐标传输接口测试实践](https://mp.weixin.qq.com/s/pDAx4jwYvcRcdld5cKLAUw)\n\n## 接口测试视频\n\n- [FunTester测试框架视频讲解（序）](https://mp.weixin.qq.com/s/CJrHAAniDMyr5oDXYHpPcQ)\n- [获取HTTP请求对象--测试框架视频讲解](https://mp.weixin.qq.com/s/hG89sGf96GcPb2hGnludsw)\n- [发送请求和解析响应—测试框架视频解读](https://mp.weixin.qq.com/s/xUQ8o3YuZOChXZ2UGR1Kyw)\n- [json对象基本操作--视频讲解](https://mp.weixin.qq.com/s/MQtcIGKwWGEMb2XD3zmAIQ)\n- [GET请求实践--测试框架视频讲解](https://mp.weixin.qq.com/s/_ZEDmRPXe4SLjCgdwDtC7A)\n- [POST请求实践--视频演示](https://mp.weixin.qq.com/s/g0mLzMQ4Br2e592m3p68eg)\n- [如何处理header和cookie--视频演示](https://mp.weixin.qq.com/s/MkwzT9VPglSnOxY7geSUiQ)\n- [FunRequest类功能--视频演示](https://mp.weixin.qq.com/s/WGS6ZwAvw7X4MC004Gz4pA)\n- [接口测试业务验证--视频演示](https://mp.weixin.qq.com/s/DH8HDmaritXQnkBIFOadoA)\n- [自动化测试项目基础--视频讲解](https://mp.weixin.qq.com/s/n9zu4OLyj7FbNsV0bYlOYg)\n- [JSONArray基本操作--视频演示](https://mp.weixin.qq.com/s/OosDbRoknMe1riaPc3hhLg)\n- [自动化项目基类实践--视频演示](https://mp.weixin.qq.com/s/IdvSi-GDtE5nqGnR-_4LWA)\n- [模块类和自动化用例实践--视频演示](https://mp.weixin.qq.com/s/Y_A8M7KHmdlJJOD4B4rN4Q)\n- [性能框架多线程基类和执行类--视频讲解](https://mp.weixin.qq.com/s/8Dh-5XfvX8Fm4IqmzbtY6Q)\n- [定时和定量压测模式实现--视频讲解](https://mp.weixin.qq.com/s/l_4wCjVM1fAVRHgEPrcrwg)\n- [基于HTTP请求的多线程实现类--视频讲解](https://mp.weixin.qq.com/s/8SG1xtzq8ArY84Bxm_SNow)\n\n# 单元&白盒\n\n- [Maven和Gradle中配置单元测试框架Spock](https://mp.weixin.qq.com/s/kL5keijAAZwmq_DO1NDBtw)\n- [Groovy单元测试框架spock基础功能Demo](https://mp.weixin.qq.com/s/fQCyIyeQANbu2YP2ML6_8Q)\n- [Groovy单元测试框架spock数据驱动Demo](https://mp.weixin.qq.com/s/uCAB7Mxt1JZW229aKp-uVQ)\n- [人生苦短？试试Groovy进行单元测试](https://mp.weixin.qq.com/s/ahyP-YQTzigeq_5N8byC4g)\n- [模糊断言](https://mp.weixin.qq.com/s/OlJpqHkwpY6-yyELvQ9cIw)\n- [使用WireMock进行更好的集成测试](https://mp.weixin.qq.com/s/oMuVZOOQmuxSygJWH2_QHg)\n- [如何测试这个方法--功能篇](https://mp.weixin.qq.com/s/4zrwkc6ccozUGjOGV563dQ)\n- [如何测试这个方法--性能篇](https://mp.weixin.qq.com/s/QXl9_9Bj5c191oxkXmByUA)\n- [单元测试用例](https://mp.weixin.qq.com/s/UFEXJ1aXOvJUYp49iVLr5w)\n- [关于测试覆盖率](https://mp.weixin.qq.com/s/E15D785fkaWH7-YhiE5gPw)\n- [JUnit 5和Selenium基础（一）](https://mp.weixin.qq.com/s/ehBRf7st-OxeuvI_0yW3OQ)\n- [JUnit 5和Selenium基础（二）](https://mp.weixin.qq.com/s/Gt82cPmS2iX-DhKXTXiy8g)\n- [JUnit 5和Selenium基础（三）](https://mp.weixin.qq.com/s/8YkonXTYgAV5-pLs9yEAVw)\n- [浅谈单元测试](https://mp.weixin.qq.com/s/mJM9qXQepSYQ9vLBnBEs3Q)\n- [Spock 2.0 M1版本初探](https://mp.weixin.qq.com/s/nyYh2QzER03kIk-w9P9GNw)\n- [Java并发BUG基础篇](https://mp.weixin.qq.com/s/NR4vYx81HtgAEqH2Q93k2Q)\n- [Java并发BUG提升篇](https://mp.weixin.qq.com/s/GCRRe8hJpe1QJtxq9VBEhg)\n- [集成测试、单元测试、系统测试](https://mp.weixin.qq.com/s/LRkxMasRvmDYRVb0_aybtA)\n- [从单元测试标准中学习](https://mp.weixin.qq.com/s/x0TyMAdPBWYL7JSPAmoQsw)\n- [白盒测试扫盲](https://mp.weixin.qq.com/s/s_FvGZTC42GEjaWzroz1eA)\n- [Mock System.in和检查System.out](https://mp.weixin.qq.com/s/1ly3uXCZsukmIylN6F5GxQ)\n- [单元测试框架spock和Mockito应用](https://mp.weixin.qq.com/s/s21Lts1UnG9HwOEVvgj-uw)\n- [Mockito框架Mock Void方法](https://mp.weixin.qq.com/s/R95wOMVyeDCHm3_Z0S2kqg)\n- [JsonPath工具类单元测试](https://mp.weixin.qq.com/s/1YtUWGk_sTjn9bHwAeT0Ew)\n- [Intellij静态代码扫描插件SpotBugs](https://mp.weixin.qq.com/s/8ivsMNOmT0LDfvcM06IGMg)\n- [SpotBugs注解SuppressWarnings在Java&Groovy中的应用](https://mp.weixin.qq.com/s/R0JoqmAqhUbRSjIJ61h_tg)\n\n\n## 性能测试\n\n- [Linux性能监控软件netdata中文汉化版](https://mp.weixin.qq.com/s/7VG7gHx7FUvsuNtBTJpjWA)\n- [性能测试框架](https://mp.weixin.qq.com/s/3_09j7-5ex35u30HQRyWug)\n- [性能测试框架第二版](https://mp.weixin.qq.com/s/JPyGQ2DRC6EVBmZkxAoVWA)\n- [性能测试框架第三版](https://mp.weixin.qq.com/s/Mk3PoH7oJX7baFmbeLtl_w)\n- [一个时间计数器timewatch辅助性能测试](https://mp.weixin.qq.com/s/-YZ04n2kyfO0q2QaKHX_0Q)\n- [如何在Linux命令行界面愉快进行性能测试](https://mp.weixin.qq.com/s/fwGqBe1SpA2V0lPfAOd04Q)\n- [Mac+httpclient高并发配置实例](https://mp.weixin.qq.com/s/r4a-vGz0pxeZBPPH3phujw)\n- [单点登录性能测试方案](https://mp.weixin.qq.com/s/sv8FnvIq44dFEq63LpOD2A)\n- [如何对消息队列做性能测试](https://mp.weixin.qq.com/s/MNt22aW3Op9VQ5OoMzPwBw)\n- [如何对修改密码接口进行压测](https://mp.weixin.qq.com/s/9CL_6-uZOlAh7oeo7NOpag)\n- [如何对单行多次update接口进行压测](https://mp.weixin.qq.com/s/Ly1Y4iPGgL6FNRsbOTv0sg)\n- [如何对多行单次update接口进行压测](https://mp.weixin.qq.com/s/Fsqw7vlw6K9EKa_XJwGIgQ)\n- [如何获取JVM堆转储文件](https://mp.weixin.qq.com/s/qCg7nsXVvT1q-9yquQOfWA)\n- [性能测试中标记每个请求](https://mp.weixin.qq.com/s/PokvzoLdVf_y9inlVXHJHQ)\n- [如何对N个接口按比例压测](https://mp.weixin.qq.com/s/GZxbH4GjDkk4BLqnUj1_kw)\n- [如何性能测试中进行业务验证](https://mp.weixin.qq.com/s/OEvRy1bS2Yq_w1kGiidmng)\n- [性能测试中记录每一个耗时请求](https://mp.weixin.qq.com/s/VXcp4uIMm8mRgqe8fVhuCQ)\n- [线程安全类在性能测试中应用](https://mp.weixin.qq.com/s/0-Y63wXqIugVC8RiKldHvg)\n- [利用微基准测试修正压测结果](https://mp.weixin.qq.com/s/dmO33qhOBrTByw_NshS-uA)\n- [性能测试如何减少本机误差](https://mp.weixin.qq.com/s/S6b_wwSowVolp1Uu6sEIOA)\n- [服务端性能优化之异步查询转同步](https://mp.weixin.qq.com/s/okYP2aOPfkWj2FjZcAtQNA)\n- [服务端性能优化之双重检查锁](https://mp.weixin.qq.com/s/-bOyHBcqFlJY3c0PEZaWgQ)\n- [多种登录方式定量性能测试方案](https://mp.weixin.qq.com/s/WuZ2h2rr0rNBgEvQVioacA)\n- [性能测试中图形化输出测试数据](https://mp.weixin.qq.com/s/EMvpYIsszdwBJFPIxztTvA)\n- [压测中测量异步写入接口的延迟](https://mp.weixin.qq.com/s/odvK1iYgg4eRVtOOPbq15w)\n- [手机号验证码登录性能测试](https://mp.weixin.qq.com/s/i-j8fJAdcsJ7v8XPOnPDAw)\n- [绑定手机号性能测试](https://mp.weixin.qq.com/s/K5x1t1dKtIT2NKV6k4v5mw)\n- [终止性能测试并输出报告](https://mp.weixin.qq.com/s/II4-UbKDikctmS_vRT-xLg)\n- [CountDownLatch类在性能测试中应用](https://mp.weixin.qq.com/s/uYBPPOjauR2h81l2uKMANQ)\n- [CyclicBarrier类在性能测试中应用](https://mp.weixin.qq.com/s/kvEHX3t_2xpMke9vwOdWrg)\n- [Phaser类在性能测试中应用](https://mp.weixin.qq.com/s/plxNnQq7yNQvHYEGpyY4uA)\n- [如何同时压测创建和删除接口](https://mp.weixin.qq.com/s/NCeoEF3DkEtpdaaQ365I0Q)\n- [固定QPS压测模式探索](https://mp.weixin.qq.com/s/S2h-zEUoik_CWs60RL6g7Q)\n- [固定QPS压测初试](https://mp.weixin.qq.com/s/ySlJmDIH3fFB4qEnL-ueMg)\n- [命令行如何执行jar包里面的方法](https://mp.weixin.qq.com/s/50oMEmVEnv5Vzlm1HOxuFw)\n- [链路压测中如何记录每一个耗时的请求](https://mp.weixin.qq.com/s/8sb5QZcKbBjTxCaXK5ajXA)\n- [Socket接口异步验证实践](https://mp.weixin.qq.com/s/bnjHK3ZmEzHm3y-xaSVkTw)\n- [性能测试中集合点和多阶段同步问题初探](https://mp.weixin.qq.com/s/NlpD1WyMrcG1V5RYfY0Plg)\n- [性能测试中标记请求参数实践](https://mp.weixin.qq.com/s/2FNMU-k_En26FCqWkYpvhQ)\n- [测试模型中理解压力测试和负载测试](https://mp.weixin.qq.com/s/smNLx3malzM3avkrn3EJiA)\n- [重放浏览器单个请求性能测试实践](https://mp.weixin.qq.com/s/a10hxCrIzS4TV9JwmDSI3Q)\n- [重放浏览器多个请求性能测试实践](https://mp.weixin.qq.com/s/Hm1Kpp1PMrZ5rYFW8l2GlA)\n- [重放浏览器请求多链路性能测试实践](https://mp.weixin.qq.com/s/9YSBLAyHVw8Z6IfK-nJTpQ)\n- [性能测试中异步展示测试进度](https://mp.weixin.qq.com/s/AOERJbEc4ATJqhjvnxgQoA)\n- [ThreadLocal在链路性能测试中实践](https://mp.weixin.qq.com/s/3qhNdHHSStELzNraQSpcew)\n- [Socket接口固定QPS性能测试实践](https://mp.weixin.qq.com/s/I9-14L8THxvtX1NJY0KPfw)\n- [单链路性能测试实践](https://mp.weixin.qq.com/s/4xHLP-GZwrNu5cFKdfsB6g)\n- [链路性能测试中参数多样性方法分享](https://mp.weixin.qq.com/s/I1pm0fulNrj_S-YkNz-gEA)\n- [链路测试中参数流转图](https://mp.weixin.qq.com/s/xyo9HXBLoXgLW6MSFH3V6w)\n- [线程同步类CyclicBarrier在性能测试集合点应用](https://mp.weixin.qq.com/s/K2YySxX9T4v_rzbvIbIHJA)\n"
  },
  {
    "path": "document/article.markdown",
    "content": "# 总目录\n\n> **FunTester**，[腾讯云年度作者](https://mp.weixin.qq.com/s/oeTeJZs6h4jJJMRyUunurw)、[Boss直聘签约作者](https://mp.weixin.qq.com/s/CjwFBoS9sew6R74mM9QO4g)，[GDevOps官方合作媒体](https://mp.weixin.qq.com/s/OnwvRqrgXq_pGp8eOXOTgw)，非著名测试开发er。\n\n* `Gitee`地址*https://gitee.com/fanapi/tester*\n* `GitHub`地址*https://github.com/JunManYuanLong/FunTester*\n\n# 接口测试\n\n## 接口功能测试\n\n- [开源测试服务](https://mp.weixin.qq.com/s/ZOs0cp_vt6_iiundHaKk4g)\n- [使用springboot+mybatis数据库存储服务化](https://mp.weixin.qq.com/s/N_5tHW1JJLZlxCaDI2PvyQ)\n- [alertover推送api的java httpclient实现实例](https://mp.weixin.qq.com/s/DJXCBEG3SbybfbT6blO1jA)\n- [接口自动化通用验证类](https://mp.weixin.qq.com/s/fP1clCKkLREfg6POKV5n1A)\n- [将swagger文档自动变成测试代码](https://mp.weixin.qq.com/s/SY8mVenj0zMe5b47GS9VSQ)\n- [httpclient处理多用户同时在线](https://mp.weixin.qq.com/s/Nuc30Fwy6-Qyr-Pc65t1_g)\n- [使用httpclient实现图灵机器人web api调用实例](https://mp.weixin.qq.com/s/dYyxvAhwSmJkNI8N9lYQfg)\n- [groovy如何使用java接口测试框架发送http请求](https://mp.weixin.qq.com/s/KF5lzMT-E2IBOkp_UjuC4g)\n- [httpclient调用京东万象数字营销频道新闻api实例](https://mp.weixin.qq.com/s/kSqgSbPci-q2pfsdcU5Ekw)\n- [httpclient遇到socket closed解决办法](https://mp.weixin.qq.com/s/mDRC7mssKmnvcI6StQWIBQ)\n- [httpclient4.5如何确保资源释放](https://mp.weixin.qq.com/s/373Lx1bv0vi-pIBgWNzC9Q)\n- [httpclient如何处理302重定向](https://mp.weixin.qq.com/s/vg354AjPKhIZsnSu4GZjZg)\n- [基于java的直线型接口测试框架初探](https://mp.weixin.qq.com/s/xhg4exdb1G18-nG5E7exkQ)\n- [利用alertover发送获取响应失败的通知消息](https://mp.weixin.qq.com/s/w6y2UkgL3J20mAxc8fq0tA)\n- [使用httpclient中EntityUtils类解析entity遇到socket closed错误的原因](https://mp.weixin.qq.com/s/RJnuOa2K6aRCElJafkFeug)\n- [httpclient接口测试中重试控制器设置](https://mp.weixin.qq.com/s/hknNdq_ybQ1MoXh_dI3JVA)\n- [拼接GET请求的参数](https://mp.weixin.qq.com/s/EGw_97scexH_3m2Uc8Ye5A)\n- [httpclient上传文件方法的封装](https://mp.weixin.qq.com/s/HIrwl5ullvEmn_UuyLKkRg)\n- [接口批量上传文件的实例](https://mp.weixin.qq.com/s/wZwkWchXXC6iddX1oVEnZQ)\n- [httpclient发送https协议请求以及javax.net.ssl.SSLHandshakeException解决办法](https://mp.weixin.qq.com/s/uSHhKRrL2f9USKpSykkpkQ)\n- [API测试基础](https://mp.weixin.qq.com/s/bkbUEa9CF21xMYSlhPcULw)\n- [拷贝HttpRequestBase对象](https://mp.weixin.qq.com/s/kxB1c0GmSF5OAM15UQJU2Q)\n- [API自动化测试指南](https://mp.weixin.qq.com/s/uy_Vn_ZVUEu3YAI1gW2T_A)\n- [如何统一接口测试的功能、自动化和性能测试用例](https://mp.weixin.qq.com/s/1xqtXNVw7BdUa03nVcsMTg)\n- [如何选择API测试工具](https://mp.weixin.qq.com/s/m2TNJDiqAAWYV9L6UP-29w)\n- [初学者的API测试技巧](https://mp.weixin.qq.com/s/_uk6dw5Q7CfS-gXGH-TZEQ)\n- [压测中测量异步写入接口的延迟](https://mp.weixin.qq.com/s/odvK1iYgg4eRVtOOPbq15w)\n- [多项目登录互踢测试用例](https://mp.weixin.qq.com/s/Nn_CUy_j7j6bUwHSkO0pCQ)\n- [httpclient使用HTTP代理实践](https://mp.weixin.qq.com/s/24IJwJ1TJWHdfj0PzSjmvw)\n- [HTTP异步连接池和多线程实践](https://mp.weixin.qq.com/s/8M348LuHakBe4GAEnnDPxw)\n- [IntelliJ中基于文本的HTTP客户端](https://mp.weixin.qq.com/s/-9qi_lLVVfxQKEFmcRYFtA)\n- [socket接口开发和测试初探](https://mp.weixin.qq.com/s/uhmkbrMp91PP1pQjlEofOQ)\n- [IntelliJ中基于文本的HTTP客户端](https://mp.weixin.qq.com/s/-9qi_lLVVfxQKEFmcRYFtA)\n- [基于WebSocket的client封装](https://mp.weixin.qq.com/s/1lZvsuGEa6hiRHOgOT-Kmg)\n- [基于Socket.IO的Client封装](https://mp.weixin.qq.com/s/Ux90AXI9g85w7R5i3f9idg)\n- [Socket.IO接口多用户测试实践](https://mp.weixin.qq.com/s/aCLaRZQs8zMK_ptJ-PjClw)\n- [Python版Socket.IO接口测试脚本](https://mp.weixin.qq.com/s/oXBP6Sx3yPqlmvV9uCUScw)\n- [命令行如何执行jar包里面的方法](https://mp.weixin.qq.com/s/50oMEmVEnv5Vzlm1HOxuFw)\n- [JSON对象标记语法验证类](https://mp.weixin.qq.com/s/jSXmoEdMF7nWAqQuzJ5GiQ)\n- [Socket接口异步验证实践](https://mp.weixin.qq.com/s/bnjHK3ZmEzHm3y-xaSVkTw)\n- [无数据驱动自动化测试](https://mp.weixin.qq.com/s/aCYRGxkzMogLbmACYo6ssw)\n- [白板点阵数据传输测试初探](https://mp.weixin.qq.com/s/EzFC-hIvgm7j7947TZU6BA)\n- [基于Socket.IO的白板点阵坐标传输接口测试实践](https://mp.weixin.qq.com/s/pDAx4jwYvcRcdld5cKLAUw)\n\n## 接口测试视频\n\n- [FunTester测试框架视频讲解（序）](https://mp.weixin.qq.com/s/CJrHAAniDMyr5oDXYHpPcQ)\n- [获取HTTP请求对象--测试框架视频讲解](https://mp.weixin.qq.com/s/hG89sGf96GcPb2hGnludsw)\n- [发送请求和解析响应—测试框架视频解读](https://mp.weixin.qq.com/s/xUQ8o3YuZOChXZ2UGR1Kyw)\n- [json对象基本操作--视频讲解](https://mp.weixin.qq.com/s/MQtcIGKwWGEMb2XD3zmAIQ)\n- [GET请求实践--测试框架视频讲解](https://mp.weixin.qq.com/s/_ZEDmRPXe4SLjCgdwDtC7A)\n- [POST请求实践--视频演示](https://mp.weixin.qq.com/s/g0mLzMQ4Br2e592m3p68eg)\n- [如何处理header和cookie--视频演示](https://mp.weixin.qq.com/s/MkwzT9VPglSnOxY7geSUiQ)\n- [FunRequest类功能--视频演示](https://mp.weixin.qq.com/s/WGS6ZwAvw7X4MC004Gz4pA)\n- [接口测试业务验证--视频演示](https://mp.weixin.qq.com/s/DH8HDmaritXQnkBIFOadoA)\n- [自动化测试项目基础--视频讲解](https://mp.weixin.qq.com/s/n9zu4OLyj7FbNsV0bYlOYg)\n- [JSONArray基本操作--视频演示](https://mp.weixin.qq.com/s/OosDbRoknMe1riaPc3hhLg)\n- [自动化项目基类实践--视频演示](https://mp.weixin.qq.com/s/IdvSi-GDtE5nqGnR-_4LWA)\n- [模块类和自动化用例实践--视频演示](https://mp.weixin.qq.com/s/Y_A8M7KHmdlJJOD4B4rN4Q)\n- [性能框架多线程基类和执行类--视频讲解](https://mp.weixin.qq.com/s/8Dh-5XfvX8Fm4IqmzbtY6Q)\n- [定时和定量压测模式实现--视频讲解](https://mp.weixin.qq.com/s/l_4wCjVM1fAVRHgEPrcrwg)\n- [基于HTTP请求的多线程实现类--视频讲解](https://mp.weixin.qq.com/s/8SG1xtzq8ArY84Bxm_SNow)\n\n# 单元&白盒\n\n- [Maven和Gradle中配置单元测试框架Spock](https://mp.weixin.qq.com/s/kL5keijAAZwmq_DO1NDBtw)\n- [Groovy单元测试框架spock基础功能Demo](https://mp.weixin.qq.com/s/fQCyIyeQANbu2YP2ML6_8Q)\n- [Groovy单元测试框架spock数据驱动Demo](https://mp.weixin.qq.com/s/uCAB7Mxt1JZW229aKp-uVQ)\n- [人生苦短？试试Groovy进行单元测试](https://mp.weixin.qq.com/s/ahyP-YQTzigeq_5N8byC4g)\n- [模糊断言](https://mp.weixin.qq.com/s/OlJpqHkwpY6-yyELvQ9cIw)\n- [使用WireMock进行更好的集成测试](https://mp.weixin.qq.com/s/oMuVZOOQmuxSygJWH2_QHg)\n- [如何测试这个方法--功能篇](https://mp.weixin.qq.com/s/4zrwkc6ccozUGjOGV563dQ)\n- [如何测试这个方法--性能篇](https://mp.weixin.qq.com/s/QXl9_9Bj5c191oxkXmByUA)\n- [单元测试用例](https://mp.weixin.qq.com/s/UFEXJ1aXOvJUYp49iVLr5w)\n- [关于测试覆盖率](https://mp.weixin.qq.com/s/E15D785fkaWH7-YhiE5gPw)\n- [JUnit 5和Selenium基础（一）](https://mp.weixin.qq.com/s/ehBRf7st-OxeuvI_0yW3OQ)\n- [JUnit 5和Selenium基础（二）](https://mp.weixin.qq.com/s/Gt82cPmS2iX-DhKXTXiy8g)\n- [JUnit 5和Selenium基础（三）](https://mp.weixin.qq.com/s/8YkonXTYgAV5-pLs9yEAVw)\n- [浅谈单元测试](https://mp.weixin.qq.com/s/mJM9qXQepSYQ9vLBnBEs3Q)\n- [Spock 2.0 M1版本初探](https://mp.weixin.qq.com/s/nyYh2QzER03kIk-w9P9GNw)\n- [Java并发BUG基础篇](https://mp.weixin.qq.com/s/NR4vYx81HtgAEqH2Q93k2Q)\n- [Java并发BUG提升篇](https://mp.weixin.qq.com/s/GCRRe8hJpe1QJtxq9VBEhg)\n- [集成测试、单元测试、系统测试](https://mp.weixin.qq.com/s/LRkxMasRvmDYRVb0_aybtA)\n- [从单元测试标准中学习](https://mp.weixin.qq.com/s/x0TyMAdPBWYL7JSPAmoQsw)\n- [白盒测试扫盲](https://mp.weixin.qq.com/s/s_FvGZTC42GEjaWzroz1eA)\n- [Mock System.in和检查System.out](https://mp.weixin.qq.com/s/1ly3uXCZsukmIylN6F5GxQ)\n- [单元测试框架spock和Mockito应用](https://mp.weixin.qq.com/s/s21Lts1UnG9HwOEVvgj-uw)\n- [Mockito框架Mock Void方法](https://mp.weixin.qq.com/s/R95wOMVyeDCHm3_Z0S2kqg)\n- [JsonPath工具类单元测试](https://mp.weixin.qq.com/s/1YtUWGk_sTjn9bHwAeT0Ew)\n- [Intellij静态代码扫描插件SpotBugs](https://mp.weixin.qq.com/s/8ivsMNOmT0LDfvcM06IGMg)\n- [SpotBugs注解SuppressWarnings在Java&Groovy中的应用](https://mp.weixin.qq.com/s/R0JoqmAqhUbRSjIJ61h_tg)\n\n\n## 性能测试\n\n- [Linux性能监控软件netdata中文汉化版](https://mp.weixin.qq.com/s/7VG7gHx7FUvsuNtBTJpjWA)\n- [性能测试框架](https://mp.weixin.qq.com/s/3_09j7-5ex35u30HQRyWug)\n- [性能测试框架第二版](https://mp.weixin.qq.com/s/JPyGQ2DRC6EVBmZkxAoVWA)\n- [性能测试框架第三版](https://mp.weixin.qq.com/s/Mk3PoH7oJX7baFmbeLtl_w)\n- [一个时间计数器timewatch辅助性能测试](https://mp.weixin.qq.com/s/-YZ04n2kyfO0q2QaKHX_0Q)\n- [如何在Linux命令行界面愉快进行性能测试](https://mp.weixin.qq.com/s/fwGqBe1SpA2V0lPfAOd04Q)\n- [Mac+httpclient高并发配置实例](https://mp.weixin.qq.com/s/r4a-vGz0pxeZBPPH3phujw)\n- [单点登录性能测试方案](https://mp.weixin.qq.com/s/sv8FnvIq44dFEq63LpOD2A)\n- [如何对消息队列做性能测试](https://mp.weixin.qq.com/s/MNt22aW3Op9VQ5OoMzPwBw)\n- [如何对修改密码接口进行压测](https://mp.weixin.qq.com/s/9CL_6-uZOlAh7oeo7NOpag)\n- [如何对单行多次update接口进行压测](https://mp.weixin.qq.com/s/Ly1Y4iPGgL6FNRsbOTv0sg)\n- [如何对多行单次update接口进行压测](https://mp.weixin.qq.com/s/Fsqw7vlw6K9EKa_XJwGIgQ)\n- [如何获取JVM堆转储文件](https://mp.weixin.qq.com/s/qCg7nsXVvT1q-9yquQOfWA)\n- [性能测试中标记每个请求](https://mp.weixin.qq.com/s/PokvzoLdVf_y9inlVXHJHQ)\n- [如何对N个接口按比例压测](https://mp.weixin.qq.com/s/GZxbH4GjDkk4BLqnUj1_kw)\n- [如何性能测试中进行业务验证](https://mp.weixin.qq.com/s/OEvRy1bS2Yq_w1kGiidmng)\n- [性能测试中记录每一个耗时请求](https://mp.weixin.qq.com/s/VXcp4uIMm8mRgqe8fVhuCQ)\n- [线程安全类在性能测试中应用](https://mp.weixin.qq.com/s/0-Y63wXqIugVC8RiKldHvg)\n- [利用微基准测试修正压测结果](https://mp.weixin.qq.com/s/dmO33qhOBrTByw_NshS-uA)\n- [性能测试如何减少本机误差](https://mp.weixin.qq.com/s/S6b_wwSowVolp1Uu6sEIOA)\n- [服务端性能优化之异步查询转同步](https://mp.weixin.qq.com/s/okYP2aOPfkWj2FjZcAtQNA)\n- [服务端性能优化之双重检查锁](https://mp.weixin.qq.com/s/-bOyHBcqFlJY3c0PEZaWgQ)\n- [多种登录方式定量性能测试方案](https://mp.weixin.qq.com/s/WuZ2h2rr0rNBgEvQVioacA)\n- [性能测试中图形化输出测试数据](https://mp.weixin.qq.com/s/EMvpYIsszdwBJFPIxztTvA)\n- [压测中测量异步写入接口的延迟](https://mp.weixin.qq.com/s/odvK1iYgg4eRVtOOPbq15w)\n- [手机号验证码登录性能测试](https://mp.weixin.qq.com/s/i-j8fJAdcsJ7v8XPOnPDAw)\n- [绑定手机号性能测试](https://mp.weixin.qq.com/s/K5x1t1dKtIT2NKV6k4v5mw)\n- [终止性能测试并输出报告](https://mp.weixin.qq.com/s/II4-UbKDikctmS_vRT-xLg)\n- [CountDownLatch类在性能测试中应用](https://mp.weixin.qq.com/s/uYBPPOjauR2h81l2uKMANQ)\n- [CyclicBarrier类在性能测试中应用](https://mp.weixin.qq.com/s/kvEHX3t_2xpMke9vwOdWrg)\n- [Phaser类在性能测试中应用](https://mp.weixin.qq.com/s/plxNnQq7yNQvHYEGpyY4uA)\n- [如何同时压测创建和删除接口](https://mp.weixin.qq.com/s/NCeoEF3DkEtpdaaQ365I0Q)\n- [固定QPS压测模式探索](https://mp.weixin.qq.com/s/S2h-zEUoik_CWs60RL6g7Q)\n- [固定QPS压测初试](https://mp.weixin.qq.com/s/ySlJmDIH3fFB4qEnL-ueMg)\n- [命令行如何执行jar包里面的方法](https://mp.weixin.qq.com/s/50oMEmVEnv5Vzlm1HOxuFw)\n- [链路压测中如何记录每一个耗时的请求](https://mp.weixin.qq.com/s/8sb5QZcKbBjTxCaXK5ajXA)\n- [Socket接口异步验证实践](https://mp.weixin.qq.com/s/bnjHK3ZmEzHm3y-xaSVkTw)\n- [性能测试中集合点和多阶段同步问题初探](https://mp.weixin.qq.com/s/NlpD1WyMrcG1V5RYfY0Plg)\n- [性能测试中标记请求参数实践](https://mp.weixin.qq.com/s/2FNMU-k_En26FCqWkYpvhQ)\n- [测试模型中理解压力测试和负载测试](https://mp.weixin.qq.com/s/smNLx3malzM3avkrn3EJiA)\n- [重放浏览器单个请求性能测试实践](https://mp.weixin.qq.com/s/a10hxCrIzS4TV9JwmDSI3Q)\n- [重放浏览器多个请求性能测试实践](https://mp.weixin.qq.com/s/Hm1Kpp1PMrZ5rYFW8l2GlA)\n- [重放浏览器请求多链路性能测试实践](https://mp.weixin.qq.com/s/9YSBLAyHVw8Z6IfK-nJTpQ)\n- [性能测试中异步展示测试进度](https://mp.weixin.qq.com/s/AOERJbEc4ATJqhjvnxgQoA)\n- [ThreadLocal在链路性能测试中实践](https://mp.weixin.qq.com/s/3qhNdHHSStELzNraQSpcew)\n- [Socket接口固定QPS性能测试实践](https://mp.weixin.qq.com/s/I9-14L8THxvtX1NJY0KPfw)\n- [单链路性能测试实践](https://mp.weixin.qq.com/s/4xHLP-GZwrNu5cFKdfsB6g)\n- [链路性能测试中参数多样性方法分享](https://mp.weixin.qq.com/s/I1pm0fulNrj_S-YkNz-gEA)\n- [链路测试中参数流转图](https://mp.weixin.qq.com/s/xyo9HXBLoXgLW6MSFH3V6w)\n- [线程同步类CyclicBarrier在性能测试集合点应用](https://mp.weixin.qq.com/s/K2YySxX9T4v_rzbvIbIHJA)\n- [链路压测中各接口性能统计](https://mp.weixin.qq.com/s/Deyop0mMpHrRWj9JTHzayw)\n- [性能测试框架中QPS取样器实现](https://mp.weixin.qq.com/s/4-5WhwwE1oRQ7cMUDv7J2w)\n- [链路压测中的支路问题初探](https://mp.weixin.qq.com/s/9iN9XndRPH4vIgc0-jVpUA)\n\n> **FunTester**，[腾讯云年度作者](https://mp.weixin.qq.com/s/oeTeJZs6h4jJJMRyUunurw)、[Boss直聘签约作者](https://mp.weixin.qq.com/s/CjwFBoS9sew6R74mM9QO4g)，[GDevOps官方合作媒体](https://mp.weixin.qq.com/s/OnwvRqrgXq_pGp8eOXOTgw)，非著名测试开发er。\n\n* `Gitee`地址*https://gitee.com/fanapi/tester*\n* `GitHub`地址*https://github.com/JunManYuanLong/FunTester*\n\n\n# 语言合集\n\n## Java\n\n- [java一行代码打印心形](https://mp.weixin.qq.com/s/QPSryoSbViVURpSa9QXtpg)\n- [操作的原子性与线程安全](https://mp.weixin.qq.com/s/QU3llkGLepX2VCch8Y9GKw)\n- [快看，i++真的不安全](https://mp.weixin.qq.com/s/-CdWdROKSEq_ZiLX2kWxzA)\n- [原子操作组合与线程安全](https://mp.weixin.qq.com/s/XB5LXucAF5Bo8EkfLZYRmw)\n- [java利用for循环输出正三角新解](https://mp.weixin.qq.com/s/nnMR2177LLVn4u_9s9Fl4g)\n- [在main方法之前，到底执行了什么？](https://mp.weixin.qq.com/s/jWxiCMfwmvRHrjPdRG8ZyQ)\n- [传参传的到底是什么?](https://mp.weixin.qq.com/s/p_pEQwE6h6q7PprkW-kjbg)\n- [json里面put了null会怎么样？](https://mp.weixin.qq.com/s/gQVROe01I3JzIqNdTSHpDQ)\n- [主线程都结束了，为何进程还在执行](https://mp.weixin.qq.com/s/q2v5JU5dtmNEol7I7IVY-Q)\n- [java测试框架如何执行groovy脚本文件](https://mp.weixin.qq.com/s/0GYt1l3_z5-1qzBNl6_PzA)\n- [java用递归筛选法求N以内的孪生质数（孪生素数）](https://mp.weixin.qq.com/s/PSdCb-DrgMPb4WpJJMexmQ)\n- [从JVM堆内存分析验证深浅拷贝](https://mp.weixin.qq.com/s/SdYDnoau1rjjvPC2SUymBg)\n- [如何学习Java基础](https://mp.weixin.qq.com/s/FCPStkYoJF67NYln4Lc6xg)\n- [如何保存HTTPrequestbase和CloseableHttpResponse](https://mp.weixin.qq.com/s/gRY8HRQHCh0PfyS7Q22IwA)\n- [如何在匿名thread子类中保证线程安全](https://mp.weixin.qq.com/s/GCXx_-ummi0JfZQ7GTIxig)\n- [Java服务端两个常见的并发错误](https://mp.weixin.qq.com/s/5VvCox3eY6sQDsuaKB4ZIw)\n- [Java中interface属性和实例方法](https://mp.weixin.qq.com/s/vrKkM6522tgw3v_cL7R8HA)\n- [服务端性能优化之双重检查锁](https://mp.weixin.qq.com/s/-bOyHBcqFlJY3c0PEZaWgQ)\n- [Java并发BUG基础篇](https://mp.weixin.qq.com/s/NR4vYx81HtgAEqH2Q93k2Q)\n- [Java并发BUG提升篇](https://mp.weixin.qq.com/s/GCRRe8hJpe1QJtxq9VBEhg)\n- [性能测试中图形化输出测试数据](https://mp.weixin.qq.com/s/EMvpYIsszdwBJFPIxztTvA)\n- [超大对象导致Full GC超高的BUG分享](https://mp.weixin.qq.com/s/L15-0JW9WK-E005GeOG9WQ)\n- [利用ThreadLocal解决线程同步问题](https://mp.weixin.qq.com/s/VEm8jt3ZUEUdyyeXPC8VvQ)\n- [线程安全集合类中的对象是安全的么？](https://mp.weixin.qq.com/s/WKSuPEfzZCVwjVTcoD0Dyg)\n- [如何使用“dd MM”解析日期](https://mp.weixin.qq.com/s/v9ooAj3dKu53JXgxB482HA)\n- [Java和Groovy正则使用](https://mp.weixin.qq.com/s/DT3BKE3ZcCKf6TLzGc5wbg)\n- [运行越来越快的Java热点代码](https://mp.weixin.qq.com/s/AP0BcDEjDuaouaB0RXJOoQ)\n- [6个重要的JVM性能参数](https://mp.weixin.qq.com/s/b1QnapiAVn0HD5DQU9JrIw)\n- [ArrayList浅、深拷贝](https://mp.weixin.qq.com/s/kYsBzFsCyDPUssdV3MDqLA)\n- [Java性能测试中两种锁的实现](https://mp.weixin.qq.com/s/j9dGFvYzCJ0AGwYUtTrTsw)\n- [测试如何处理Java异常](https://mp.weixin.qq.com/s/H00GWiATOD8QHJu3UewrBw)\n- [创建Java守护线程](https://mp.weixin.qq.com/s/_UjWdvq8QWYTshr4SeniBg)\n- [Lambda表达式在线程安全Map中应用](https://mp.weixin.qq.com/s/zZjB5aOWh4a_k1eoEsR5ww)\n- [Java程序是如何浪费内存的](https://mp.weixin.qq.com/s/w7VF5m5cc0X7LNvqmwGfvg)\n- [Java中的自定义异常](https://mp.weixin.qq.com/s/nspIdxFP9qEDtagGN4gaMQ)\n- [Java文本块](https://mp.weixin.qq.com/s/GwasvpJsd7uLngvCr6KlQw)\n- [CountDownLatch类在性能测试中应用](https://mp.weixin.qq.com/s/uYBPPOjauR2h81l2uKMANQ)\n- [CyclicBarrier类在性能测试中应用](https://mp.weixin.qq.com/s/kvEHX3t_2xpMke9vwOdWrg)\n- [Phaser类在性能测试中应用](https://mp.weixin.qq.com/s/plxNnQq7yNQvHYEGpyY4uA)\n- [Java压缩/解压缩字符串](https://mp.weixin.qq.com/s/7vHNd5dEN93DPUqgS8od_A)\n- [Java删除空字符：Java8 & Java11](https://mp.weixin.qq.com/s/6dlgYgTFZsHuJ4Eaby5eyg)\n- [Java Stream中map和flatMap方法](https://mp.weixin.qq.com/s/0FG2o7VUAG6z8a_0je-1EQ)\n- [泛型类的正确用法](https://mp.weixin.qq.com/s/1azilraonPIZNCnw_9MB5Q)\n- [Java字符串到数组的转换--最后放大招](https://mp.weixin.qq.com/s/iMUYZYkJ5CjykwWqinNm5g)\n- [Java求数组的并集--最后放大招](https://mp.weixin.qq.com/s/bZ93SGakyiRbaRujhx4nvw)\n- [Java计算数组平均值--最后放大招](https://mp.weixin.qq.com/s/dxQaFHu2PyAbOK6jpEgEUQ)\n- [Math.abs()求绝对值返回负值BUG分享](https://mp.weixin.qq.com/s/RHzExuRqF1XsBtzGKzmgGA)\n- [Java代理模式初探](https://mp.weixin.qq.com/s/SBL_K2PQez3vDHhtAN9NLg)\n- [Socket接口异步验证实践](https://mp.weixin.qq.com/s/bnjHK3ZmEzHm3y-xaSVkTw)\n- [性能测试中异步展示测试进度](https://mp.weixin.qq.com/s/AOERJbEc4ATJqhjvnxgQoA)\n- [Java中的ThreadLocal功能演示](https://mp.weixin.qq.com/s/n92k1JswHKrqT7Y_CD9Q0w)\n- [ThreadLocal在链路性能测试中实践](https://mp.weixin.qq.com/s/3qhNdHHSStELzNraQSpcew)\n- [歪解字符串中连续出现次数最多问题](https://mp.weixin.qq.com/s/xBy4iB4qLd4WQgCsVVuemw)\n- [Java&Groovy下载文件对比](https://mp.weixin.qq.com/s/T9WUynej2yOZhCkDUhaLYw)\n- [线程同步类CyclicBarrier在性能测试集合点应用](https://mp.weixin.qq.com/s/K2YySxX9T4v_rzbvIbIHJA)\n- [Java线程同步三剑客](https://mp.weixin.qq.com/s/cAmd11-HdwXNU3tp4TiDLg)\n\n## Groovy\n\n- [java和groovy混合编程时提示找不到符合错误解决办法](https://mp.weixin.qq.com/s/dLC2W7nIi5zCuK6JTkiA-w)\n- [groovy使用stream语法递归筛选法求N以内的质数](https://mp.weixin.qq.com/s/TsrVn1cuQUrU6wj9OnR-FQ)\n- [使用Groovy进行Bash（shell）操作](https://mp.weixin.qq.com/s/fgCTlZUF3QeNj6jzq1ZgGg)\n- [使用Groovy和Gradle轻松进行数据库操作](https://mp.weixin.qq.com/s/lwmclrnW0csykVRhu7dNTQ)\n- [愉快地使用Groovy Shell](https://mp.weixin.qq.com/s/fJh7fbB3naBFBEiaS62oxw)\n- [Gradle+Groovy基础篇](https://mp.weixin.qq.com/s/c2j7G-PoNtAB3oYYDUhCGw)\n- [Gradle+Groovy提高篇](https://mp.weixin.qq.com/s/yXmYj_1fynLkR0-5FV_Arw)\n- [Groovy重载操作符](https://mp.weixin.qq.com/s/4jW06Q4_vjFR9DovRTTuHg)\n- [用Groovy处理JMeter断言和日志](https://mp.weixin.qq.com/s/Q4yPA4p8dZYAARZ60ZDh9w)\n- [用Groovy处理JMeter变量](https://mp.weixin.qq.com/s/BxtweLrBUptM8r3LxmeM_Q)\n- [用Groovy在JMeter中执行命令行](https://mp.weixin.qq.com/s/VTip7tiLpwBOr1gUoZ0n8A)\n- [用Groovy处理JMeter中的请求参数](https://mp.weixin.qq.com/s/9pCUOXWpMwXR5ynvCMYJ7A)\n- [Java和Groovy正则使用](https://mp.weixin.qq.com/s/DT3BKE3ZcCKf6TLzGc5wbg)\n- [Groovy中的元组](https://mp.weixin.qq.com/s/0-ka0-tv1vyKbiA6m44jRw)\n- [从Java到Groovy的八级进化论](https://mp.weixin.qq.com/s/QTrRHsD3w-zLGbn79y8yUg)\n- [用Groovy在JMeter中使用正则提取赋值](https://mp.weixin.qq.com/s/9riPpnQZCfKGscuzOOpYmQ)\n- [Groovy在JMeter中处理cookie](https://mp.weixin.qq.com/s/DCnDjWaj2aiKv5HVw3-n6A)\n- [Groovy在JMeter中处理header](https://mp.weixin.qq.com/s/juY-1jEWODJ5HHiEsxhIEw)\n- [Groovy的神奇NullObject](https://mp.weixin.qq.com/s/jLGisN_30PrCgNP33Sww0g)\n- [Groovy中的list](https://mp.weixin.qq.com/s/0mUe1_WrUiEm1t6kqCV3eQ)\n- [JMeter参数签名——Groovy脚本形式](https://mp.weixin.qq.com/s/wQN9-xAUQofSqiAVFXdqug)\n- [Groovy中的闭包](https://mp.weixin.qq.com/s/pfcG47gSPfUveAaEfdeo8A)\n- [JMeter参数签名——Groovy工具类形式](https://mp.weixin.qq.com/s/urwU4p9ofv9sU-JFy5Z0iA)\n- [删除List中null的N种方法--最后放大招](https://mp.weixin.qq.com/s/4mfskN781dybyL59dbSbeQ)\n- [混合Java函数和Groovy闭包](https://mp.weixin.qq.com/s/FAIzGgLSX2u7RKbOGs3lGA)\n- [Groovy重载操作符（终极版）](https://mp.weixin.qq.com/s/4oYGJ2B2Y1AqxsIj8v5nZA)\n- [JsonPath工具类单元测试](https://mp.weixin.qq.com/s/1YtUWGk_sTjn9bHwAeT0Ew)\n- [Groovy小记it关键字和IDE报错](https://mp.weixin.qq.com/s/cIMHzkvKtH0a0ewkiBnV8g)\n- [JsonPath验证类既Groovy重载操作符实践](https://mp.weixin.qq.com/s/5gc04CAsBY6pWxe5c2P41w)\n- [Groovy枚举类初始化异常分析](https://mp.weixin.qq.com/s/koFhpBZM1MFYYxCNxUKPyQ)\n- [Java&Groovy下载文件对比](https://mp.weixin.qq.com/s/T9WUynej2yOZhCkDUhaLYw)\n\n## Python\n\n- [python使用filter方法递归筛选法求N以内的质数（素数）--附一行打印心形标记的代码解析](https://mp.weixin.qq.com/s/D8RfpdIi8smCL8TAzBcNpA)\n- [关于python版微信使用经验分享](https://mp.weixin.qq.com/s/19IaI6ETZAm_T4ePPlXqIg)\n- [python用递归筛选法求N以内的孪生质数（孪生素数）](https://mp.weixin.qq.com/s/rVY2pTl8So11WCvA9GrFbA)\n- [利用python wxpy和requests写一个自动应答微信机器人实例](https://mp.weixin.qq.com/s/Fni2kX5BRjdqOQ-glCLjRg)\n- [Python版Socket.IO接口测试脚本](https://mp.weixin.qq.com/s/oXBP6Sx3yPqlmvV9uCUScw)\n\n\n## 测开笔记\n\n- [我的开发日记（一）](https://mp.weixin.qq.com/s/eQgpOKbXsU9vOmxp0Xiklg)\n- [我的开发日记（二）](https://mp.weixin.qq.com/s/XuffL3ZmKKOgHDtH_cEYOw)\n- [我的开发日记（三）](https://mp.weixin.qq.com/s/a-I0agh6nWp8RLlcmbgf5w)\n- [我的开发日记（四）](https://mp.weixin.qq.com/s/QukXd00Mx_dbkgiXys0FNg)\n- [我的开发日记（五）](https://mp.weixin.qq.com/s/6P3nScsVW6MfMcyIqcA1AQ)\n- [我的开发日记（六）](https://mp.weixin.qq.com/s/Gz2QmukONNldSy9Fd29u5w)\n- [我的开发日记（七）](https://mp.weixin.qq.com/s/MjZ-nFXfQkHMsXS0fX1c1w)\n- [我的开发日记（八）](https://mp.weixin.qq.com/s/6ZhNcFm-gR5dhKQjEkE3Rg)\n- [我的开发日记（九）](https://mp.weixin.qq.com/s/VfD2T3orojGxnylr3Q5UeA)\n- [我的开发日记（十）](https://mp.weixin.qq.com/s/6DWth40LGbAraJi05G16Pw)\n- [我的开发日记（十一）](https://mp.weixin.qq.com/s/nsX5A-P6QbePHDN_Pse0_A)\n- [我的开发日记（十二）](https://mp.weixin.qq.com/s/XA1KJXBP3Zl-XFswXxUtvg)\n- [我的开发日记（十三）](https://mp.weixin.qq.com/s/_QPUu5pUlg4A_AlC5wOGkA)\n- [我的开发日记（十四）](https://mp.weixin.qq.com/s/Qy1YKAb3wqW_Ip2FwH7Otw)\n- [我的开发日记（十五）](https://mp.weixin.qq.com/s/bwkvz2t6YItQD0O_BIxpHQ)\n- [这些年，我写过的BUG（一）](https://mp.weixin.qq.com/s/mVTmT1FdwWl1e0BaL7Ne1g)\n- [这些年，我写过的BUG（二）](https://mp.weixin.qq.com/s/NMz5n0ZMf6taGb-gr1BLyw)\n- [FunTester测试框架架构图初探](https://mp.weixin.qq.com/s/bcMbVDkWbHSXjZFDeFyJsQ)\n- [FunTester测试项目架构图初探](https://mp.weixin.qq.com/s/wqb8FXRbEXrhDuZounmNXA)\n\n> **FunTester**，[腾讯云年度作者](https://mp.weixin.qq.com/s/oeTeJZs6h4jJJMRyUunurw)、[Boss直聘签约作者](https://mp.weixin.qq.com/s/CjwFBoS9sew6R74mM9QO4g)，[GDevOps官方合作媒体](https://mp.weixin.qq.com/s/OnwvRqrgXq_pGp8eOXOTgw)，非著名测试开发er。\n\n* `Gitee`地址*https://gitee.com/fanapi/tester*\n* `GitHub`地址*https://github.com/JunManYuanLong/FunTester*\n\n\n# 案例分享\n\n## 测试方案\n\n- [如何对消息队列做性能测试](https://mp.weixin.qq.com/s/MNt22aW3Op9VQ5OoMzPwBw)\n- [如何对修改密码接口进行压测](https://mp.weixin.qq.com/s/9CL_6-uZOlAh7oeo7NOpag)\n- [如何测试概率型业务接口](https://mp.weixin.qq.com/s/kUVffhjae3eYivrGqo6ZMg)\n- [如何测试非固定型概率算法P=p(1+0.1*N)](https://mp.weixin.qq.com/s/sgg8v-Bi-_sUDJXwuTCMGg)\n- [性能测试中标记每个请求](https://mp.weixin.qq.com/s/PokvzoLdVf_y9inlVXHJHQ)\n- [如何对N个接口按比例压测](https://mp.weixin.qq.com/s/GZxbH4GjDkk4BLqnUj1_kw)\n- [多种登录方式定量性能测试方案](https://mp.weixin.qq.com/s/WuZ2h2rr0rNBgEvQVioacA)\n- [压测中测量异步写入接口的延迟](https://mp.weixin.qq.com/s/odvK1iYgg4eRVtOOPbq15w)\n- [绑定手机号性能测试](https://mp.weixin.qq.com/s/K5x1t1dKtIT2NKV6k4v5mw)\n- [手机号验证码登录性能测试](https://mp.weixin.qq.com/s/i-j8fJAdcsJ7v8XPOnPDAw)\n- [重放浏览器单个请求性能测试实践](https://mp.weixin.qq.com/s/a10hxCrIzS4TV9JwmDSI3Q)\n- [重放浏览器多个请求性能测试实践](https://mp.weixin.qq.com/s/Hm1Kpp1PMrZ5rYFW8l2GlA)\n- [重放浏览器请求多链路性能测试实践](https://mp.weixin.qq.com/s/9YSBLAyHVw8Z6IfK-nJTpQ)\n- [Socket接口固定QPS性能测试实践](https://mp.weixin.qq.com/s/I9-14L8THxvtX1NJY0KPfw)\n\n## BUG集锦\n\n- [一个MySQL索引引发的血案](https://mp.weixin.qq.com/s/KLSber-gPg53JVfsCa3Dtw)\n- [微软Zune闰年BUG分析](https://mp.weixin.qq.com/s/zpqAUcNcHaZjWUdUYH_loQ)\n- [“双花”BUG的测试分享](https://mp.weixin.qq.com/s/0dsBsssNfg-seJ_tu9zFaQ)\n- [iOS 11计算器1+2+3=24真的是bug么？](https://mp.weixin.qq.com/s/nokQhe_Hqcq-o7pZJmFlqQ)\n- [不要在遍历的时候删除](https://mp.weixin.qq.com/s/MIczbEpbOrADL0_V7ZUhlg)\n- [连开100年会员会怎样](https://mp.weixin.qq.com/s/mZw-SFIxFFbE-o8UeXhdfg)\n- [异步查询转同步加redis业务实现的BUG分享](https://mp.weixin.qq.com/s/ni3f6QTxw0K-0I3epvEYOA)\n- [Java服务端两个常见的并发错误](https://mp.weixin.qq.com/s/5VvCox3eY6sQDsuaKB4ZIw)\n- [超大对象导致Full GC超高的BUG分享](https://mp.weixin.qq.com/s/L15-0JW9WK-E005GeOG9WQ)\n- [访问权限导致toString返回空BUG分享](https://mp.weixin.qq.com/s/usDOcuJrXOmEKN-mVBzRKg)\n- [异常使用中的BUG](https://mp.weixin.qq.com/s/IG9Ar3IT7CrlSv4d0lCvgA)\n- [Math.abs()求绝对值返回负值BUG分享](https://mp.weixin.qq.com/s/RHzExuRqF1XsBtzGKzmgGA)\n\n## 爬虫实践\n\n- [接口爬虫之网页表单数据提取](https://mp.weixin.qq.com/s/imJ5u67xhYQaEzv-O1in4g)\n- [httpclient爬虫爬取汉字拼音等信息](https://mp.weixin.qq.com/s/w-IvBxAsotmPA3pydpIo1w)\n- [httpclient爬虫爬取电影信息和下载地址实例](https://mp.weixin.qq.com/s/TB49X4S-ioyoW5CrzAnHcw)\n- [httpclient 多线程爬虫实例](https://mp.weixin.qq.com/s/nXL-MP4Y6CN2hgZQefWEeQ)\n- [groovy爬虫练习之——企业信息](https://mp.weixin.qq.com/s/1TisDceIL1-Luqz_wOqAiw)\n- [httpclient 爬虫实例——爬取三级中学名](https://mp.weixin.qq.com/s/Dd7U30aHYauqBFxJdxaiyg)\n- [电子书网站爬虫实践](https://mp.weixin.qq.com/s/KGW0dIS5NTLgxyhSjxDiOw)\n- [groovy爬虫实例——历史上的今天](https://mp.weixin.qq.com/s/5LDUvpU6t_GZ09uhZr224A)\n- [爬取720万条城市历史天气数据](https://mp.weixin.qq.com/s/vOyKpeGlJSJp9bQ8NIMe2A)\n- [记一次失败的爬虫](https://mp.weixin.qq.com/s/SMylrZLXDGw5f1xKI9ObnA)\n- [爬虫实践--CBA历年比赛数据](https://mp.weixin.qq.com/s/mM_QSQddabU5im_O6iVR-Q)\n- [图片爬虫实践](https://mp.weixin.qq.com/s/u5bRSyKsmn3TcjqEEqRJpw)\n\n# 工具合集\n\n## JSON合集\n\n- [JsonPath实践（一）](https://mp.weixin.qq.com/s/Cq0_v_ptbGd4f5y8HIsq7w)\n- [JsonPath实践（二）](https://mp.weixin.qq.com/s/w_iJTiuQahIw6U00CJVJZg)\n- [JsonPath实践（三）](https://mp.weixin.qq.com/s/58A3k0T6dbOkBJ5nRYKDqA)\n- [JsonPath实践（四）](https://mp.weixin.qq.com/s/8ER61qrkMj8bdBpyuq9r6w)\n- [JsonPath实践（五）](https://mp.weixin.qq.com/s/knVLW960WXnckGLstdrOVQ)\n- [JsonPath实践（六）](https://mp.weixin.qq.com/s/ckBCK3t1w68FLBhaw5a7Jw)\n- [JsonPath工具类封装](https://mp.weixin.qq.com/s/KyuCuG5fVEExxBdGJO2LdA)\n- [JsonPath工具类单元测试](https://mp.weixin.qq.com/s/1YtUWGk_sTjn9bHwAeT0Ew)\n- [JsonPath验证类既Groovy重载操作符实践](https://mp.weixin.qq.com/s/5gc04CAsBY6pWxe5c2P41w)\n- [JSON对象标记语法验证类](https://mp.weixin.qq.com/s/jSXmoEdMF7nWAqQuzJ5GiQ)\n- [使用jq处理JSON数据（一）](https://mp.weixin.qq.com/s/45-ztTx2scbNY5u7NQzeIA)\n\n## Jacoco覆盖率\n\n- [接口测试代码覆盖率（jacoco）方案分享](https://mp.weixin.qq.com/s/D73Sq6NLjeRKN8aCpGLOjQ)\n- [jacoco无法读取build.xml配置中源码路径解决办法](https://mp.weixin.qq.com/s/8_x0rVfkIi-uX3y0drx_jw)\n- [使用JaCoCo Maven插件创建代码覆盖率报告](https://mp.weixin.qq.com/s/4Jo05k2WxytiSSNW9WTV-A)\n- [Java 8，Jenkins，Jacoco和Sonar进行持续集成](https://mp.weixin.qq.com/s/dOoXnKnWtQmmC5itClsl4g)\n- [jacoco测试覆盖率过滤非业务类](https://mp.weixin.qq.com/s/7YGe9pCHw3wd87tgOlKjSA)\n\n## arthas诊断工具\n\n- [arthas快速入门视频演示](https://mp.weixin.qq.com/s/Wl5QMD52isGTRuAP4Cpo-A)\n- [arthas进阶thread命令视频演示](https://mp.weixin.qq.com/s/XuF7Nr1sGC3diIn50zlDDQ)\n- [arthas命令jvm,sysprop,sysenv,vmoption视频演示](https://mp.weixin.qq.com/s/87BsTYqnTCnVdG3a_kBcng)\n- [arthas命令logger动态修改日志级别--视频演示](https://mp.weixin.qq.com/s/w724P9B12eTC9rMbavwsMA)\n- [arthas命令sc和sm视频演示](https://mp.weixin.qq.com/s/Ga63sjW_bOKQqfnA5LTb9w)\n- [arthas命令ognl视频演示](https://mp.weixin.qq.com/s/cMCaXFwjp6QHFq40TvP4bQ)\n- [arthas命令redefine实现Java热更新](https://mp.weixin.qq.com/s/2HUXfJhoUfg4yMzSoRHK9w)\n- [arthas命令monitor监控方法执行](https://mp.weixin.qq.com/s/7-oe3UoTY8bzpi89tIKvQQ)\n- [arthas命令watch观察方法调用（上）](https://mp.weixin.qq.com/s/6fMKP7H4Q7ll_0v-wyN19g)\n- [arthas命令watch观察方法调用（下）](https://mp.weixin.qq.com/s/-r2kufxdOjRb2TgF2HPskg)\n- [arthas命令trace追踪方法链路](https://mp.weixin.qq.com/s/bzkdKZugkOl8C-_xTw92YA)\n- [arthas命令tt方法时空隧道](https://mp.weixin.qq.com/s/mDczYmVdSmL5ZbK7bb8i0A)\n\n## moco API\n\n- [解决moco框架API在post请求json参数情况下query失效的问题](https://mp.weixin.qq.com/s/V5lXoepEBtPJrSUHA0Uz5A)\n- [给moco API添加limit功能](https://mp.weixin.qq.com/s/pXJECi15ieNLmA0uIqEqfA)\n- [给moco API添加random功能](https://mp.weixin.qq.com/s/YTcbFbFaWB5arW_fubgTTQ)\n- [解决moco框架API在cycle方法缺失的问题](https://mp.weixin.qq.com/s/YfsPa7eW8WV65CDbPooBPg)\n- [五行代码构建静态博客](https://mp.weixin.qq.com/s/hZnimJOg5OqxRSDyFvuiiQ)\n- [moco API模拟框架视频讲解（上）](https://mp.weixin.qq.com/s/X5-fFXe018_O60WCRdawZg)\n- [moco API模拟框架视频讲解（中）](https://mp.weixin.qq.com/s/g2En-9W9JWYrCLQr_WPEBA)\n- [moco API模拟框架视频讲解（下）](https://mp.weixin.qq.com/s/mz__DiNxMGHwIKCLsjKR8g)\n- [如何mock固定QPS的接口](https://mp.weixin.qq.com/s/yogj9Fni0KJkyQuKuDYlbA)\n- [mock延迟响应的接口](https://mp.weixin.qq.com/s/x_fu0InQpYIUJIQFi9a50g)\n- [moco固定QPS接口升级补偿机制](https://mp.weixin.qq.com/s/zAM91e_REo4edSPTLuHLOw)\n\n## 工具类\n\n- [java网格输出的类](https://mp.weixin.qq.com/s/BJTJu0LGjn7Hc9J1yT04KQ)\n- [java使用poi写入excel文档的一种解决方案](https://mp.weixin.qq.com/s/Ft56gd1B9CPrQs2zq4Cpug)\n- [java使用poi读取excel文档的一种解决方案](https://mp.weixin.qq.com/s/ltZGx9J7E8DTer0D-pfQ2Q)\n- [MongoDB操作类封装](https://mp.weixin.qq.com/s/u-RHOE5XrjOEkelWIxdplw)\n- [java网格输出的类](https://mp.weixin.qq.com/s/QW8nKM2Bz7C75fdkCzSbpw)\n- [将json数据格式化输出到控制台](https://mp.weixin.qq.com/s/2IPwvh-33Ov2jBh0_L8shA)\n- [利用反射根据方法名执行方法的使用示例](https://mp.weixin.qq.com/s/5ntwDo4ZVcTh1PmK4vkNfA)\n- [解决统计出现次数问题的方法类](https://mp.weixin.qq.com/s/gqz4wuKkMWAOIQwMtiupnA)\n- [java利用时间戳来获取UTC时间](https://mp.weixin.qq.com/s/wbDIrwDnxb9_XWkkmP3A_g)\n- [如何遍历执行一个包里面每个类的用例方法](https://mp.weixin.qq.com/s/OJwCOHCJ4TalatsEWbtzIQ)\n- [阿拉伯数字转成汉字](https://mp.weixin.qq.com/s/jNZXIvwMpdxt7jIAlVBgHg)\n- [获取JVM转储文件的Java工具类](https://mp.weixin.qq.com/s/f_TlOb3m8MeR3argBmTzzA)\n- [基于DOM的XML文件解析类](https://mp.weixin.qq.com/s/scRj7OAhvJYL3mx_hCFp4A)\n- [XML文件解析实践（DOM解析）](https://mp.weixin.qq.com/s/V2DG3osaPNUJzFNDQgqM-w)\n- [基于DOM4J的XML文件解析类](https://mp.weixin.qq.com/s/K5R7iMXouTn4g0p14T7iAQ)\n- [将HTTP请求对象转成curl命令行](https://mp.weixin.qq.com/s/861uMAMMWtINjy4Z99WA6w)\n\n## 构建工具\n\n- [java和groovy混编的Maven项目如何用intellij打包执行jar包](https://mp.weixin.qq.com/s/bKexZXlONeo3r6FDhfMltQ)\n- [window系统权限不足导致gradle构建失败的解决办法](https://mp.weixin.qq.com/s/dqiQvmVG1o6glU-pknLDwQ)\n- [使用groovy脚本使gradle灵活加载本地jar包的两种方式](https://mp.weixin.qq.com/s/p3K3ZS7iOUeKO7E94gKFVg)\n- [Java 8，Jenkins，Jacoco和Sonar进行持续集成](https://mp.weixin.qq.com/s/dOoXnKnWtQmmC5itClsl4g)\n- [Gradle如何在任务失败后继续构建](https://mp.weixin.qq.com/s/GcXDzRN7cM_QQpt9ytqoKg)\n- [Gradle+Groovy基础篇](https://mp.weixin.qq.com/s/c2j7G-PoNtAB3oYYDUhCGw)\n- [Gradle+Groovy提高篇](https://mp.weixin.qq.com/s/yXmYj_1fynLkR0-5FV_Arw)\n- [Maven进行增量构建](https://mp.weixin.qq.com/s/ThQ7j6TS93KJZFqlNx8IQg)\n- [SonarQube8.3中的Maven项目的测试覆盖率报告](https://mp.weixin.qq.com/s/Xhp26jyE1c7Auielz48Llw)\n\n## plotly可视化\n\n- [MacOS使用pip安装pandas提示Cannot uninstall 'numpy'解决方案](https://mp.weixin.qq.com/s/fIqMAMXRQvf_vBtS5jDsyg)\n- [Python使用plotly生成本地文件教程](https://mp.weixin.qq.com/s/4dJdIP-g3fF40vX7S31jNg)\n- [Python2.7使用plotly绘制本地散点图和折线图实例](https://mp.weixin.qq.com/s/9QWrA0c-STmrmjSkBYWvbQ)\n- [Python可视化工具plotly从数据库读取数据作图示例](https://mp.weixin.qq.com/s/EUtPidiz_r1rpQBH_kudbA)\n- [利用Python+plotly制作接口请求时间的violin图表](https://mp.weixin.qq.com/s/3GdiLaiVRfkxwM3MOG-U8w)\n- [Python+plotly生成本地饼状图实例](https://mp.weixin.qq.com/s/61Qz9Kz-4ruzC0OvIuElpA)\n- [python plotly处理接口性能测试数据方法封装](https://mp.weixin.qq.com/s/NxVdvYlD7PheNCv8AMYqhg)\n- [利用python+plotly 制作接口响应时间Distplot图表](https://mp.weixin.qq.com/s/yrcUW1fFC18newqHcxhVvw)\n- [利用 python+plotly 制作Contour Plots模拟双波源干涉现象](https://mp.weixin.qq.com/s/vNW80BDeHsyjNQrnaBGk3Q)\n- [利用 python+plotly 制作双波源干涉三维图像](https://mp.weixin.qq.com/s/KSeV8VvQXRIg-bnzYoa5qg)\n- [python plotly制作接口响应耗时的时间序列表（Time Series ）](https://mp.weixin.qq.com/s/U8chcVzCjGTdT3T_X5v4kw)\n- [python使用plotly批量生成图表](https://mp.weixin.qq.com/s/l18WfWz-s6qQ1JKKuh_2AQ)\n\n\n\n> **FunTester**，[腾讯云年度作者](https://mp.weixin.qq.com/s/oeTeJZs6h4jJJMRyUunurw)、[Boss直聘签约作者](https://mp.weixin.qq.com/s/CjwFBoS9sew6R74mM9QO4g)，[GDevOps官方合作媒体](https://mp.weixin.qq.com/s/OnwvRqrgXq_pGp8eOXOTgw)，非著名测试开发er。\n\n* `Gitee`地址*https://gitee.com/fanapi/tester*\n* `GitHub`地址*https://github.com/JunManYuanLong/FunTester*\n\n\n# 无代码合集\n\n## 理论鸡汤\n\n- [写给所有人的编程思维](https://mp.weixin.qq.com/s/Oj33UCnYfbUgzsBzEm2GPQ)\n- [成为杰出Java开发人员的10个步骤](https://mp.weixin.qq.com/s/UCNOTSzzvTXwiUX6xpVlyA)\n- [测试之《代码不朽》脑图](https://mp.weixin.qq.com/s/2aGLK3knUiiSoex-kmi0GA)\n- [为什么选择软件测试作为职业道路？](https://mp.weixin.qq.com/s/o83wYvFUvy17kBPLDO609A)\n- [自动化测试的障碍](https://mp.weixin.qq.com/s/ZIV7uJp7DzVoKhWOh6lvRg)\n- [自动化测试的问题所在](https://mp.weixin.qq.com/s/BhvD7BnkBU8hDBsGUWok6g)\n- [成为优秀自动化测试工程师的7个步骤](https://mp.weixin.qq.com/s/wdw1l4AZnPpdPBZZueCcnw)\n- [优秀软件开发人员的态度](https://mp.weixin.qq.com/s/0uEEeFaR27aTlyp-sm61bA)\n- [如何正确执行功能API测试](https://mp.weixin.qq.com/s/aeGx5O_jK_iTD9KUtylWmA)\n- [未来10年软件测试的新趋势-上](https://mp.weixin.qq.com/s/9XgpIfXQRuKg1Pap-tfqYQ)\n- [未来10年软件测试的新趋势-下](https://mp.weixin.qq.com/s/k2rZaeHoq4AX19CUzjGRVQ)\n- [自动化测试解决了什么问题](https://mp.weixin.qq.com/s/96k2I_OBHayliYGs2xo6OA)\n- [17种软件测试人员常用的高效技能-上](https://mp.weixin.qq.com/s/vrM_LxQMgTSdJxaPnD_CqQ)\n- [17种软件测试人员常用的高效技能-下](https://mp.weixin.qq.com/s/uyWdVm74TYKb62eIRKL7nQ)\n- [手动测试存在的重要原因](https://mp.weixin.qq.com/s/mW5vryoJIkeskZLkBPFe0Q)\n- [编写测试用例的技巧](https://mp.weixin.qq.com/s/zZAh_XXXGOyhlm6ebzs06Q)\n- [成为自动化测试的7种技能](https://mp.weixin.qq.com/s/e-HAGMO0JLR7VBBWLvk0dQ)\n- [功能测试与非功能测试](https://mp.weixin.qq.com/s/oJ6PJs1zO0LOQSTRF6M6WA)\n- [自动化和手动测试，保持平衡！](https://mp.weixin.qq.com/s/mMr_4C98W_FOkks2i2TiCg)\n- [43种常见软件测试分类](https://mp.weixin.qq.com/s/GTMkcEm-xPtVF7_HxXGKDg)\n- [自动化测试生命周期](https://mp.weixin.qq.com/s/SH-vb2RagYQ3sfCY8QM5ew)\n- [代码审查如何保证软件质量](https://mp.weixin.qq.com/s/osRnG09KDqEojiV3kp2nrw)\n- [TDD测试驱动开发的基础](https://mp.weixin.qq.com/s/diW_2HSbWMEsn8G6uQriOg)\n- [如何在DevOps引入自动化测试](https://mp.weixin.qq.com/s/MclK3VvMN1dsiXXJO8g7ig)\n- [自动化的好处](https://mp.weixin.qq.com/s/7MpWQhtozaTrlUMo1oRSBg)\n- [Web端自动化测试失败原因汇总](https://mp.weixin.qq.com/s/qzFth-Q9e8MTms1M8L5TyA)\n- [测试人员如何成为变革的推动者](https://mp.weixin.qq.com/s/0nTZHBOuKG0rewKAeyIqwA)\n- [探索性测试为何如此重要？](https://mp.weixin.qq.com/s/nebHPfKbCO0f-G24qCh9wA)\n- [5种促进业务增长的软件测试策略](https://mp.weixin.qq.com/s/3mB_DQVD2AZLPs84SmsmuA)\n- [如何选择正确的自动化测试工具](https://mp.weixin.qq.com/s/_Ee78UW9CxRpV5MoTrfgCQ)\n- [如何从测试自动化中实现价值](https://mp.weixin.qq.com/s/dj-sJvGjvFMYANfhIVo8jw)\n- [您如何使用Selenium来计算自动化测试的投资回报率？](https://mp.weixin.qq.com/s/DVSEm0DhoAvYfTWIniabJg)\n- [如何在DevOps中实施连续测试](https://mp.weixin.qq.com/s/snPXkH6WEZ2kteYP_-c5_g)\n- [自动化如何选择用例](https://mp.weixin.qq.com/s/1hH5YIle4YQimJr4iGSWlA)\n- [成功实施自动化测试的优点](https://mp.weixin.qq.com/s/UENdSU-NPa5AOVC9ciiy0Q)\n- [测试人员常用借口](https://mp.weixin.qq.com/s/0k_Ciud2sOpRb5PPiVzECw)\n- [测试自动化的边缘DevTestOps](https://mp.weixin.qq.com/s/kCySRYdCS11CA-lF30AtQA)\n- [筛选自动化测试用例的技巧](https://mp.weixin.qq.com/s/SWNopZLwgpj9yYsVEHEspw)\n- [什么阻碍手动测试发挥价值](https://mp.weixin.qq.com/s/t0VAVyA3ywQsHzaqzSILOw)\n- [未来的QA测试工程师](https://mp.weixin.qq.com/s/ngL4sbEjZm7OFAyyWyQ3nQ)\n- [Web安全检查](https://mp.weixin.qq.com/s/SewUV3GMaNKD2P7g64ctYQ)\n- [关于可用性测试](https://mp.weixin.qq.com/s/aUIg40scOWzbRR89ojJWLg)\n- [如何实施DevOps](https://mp.weixin.qq.com/s/UPIL942eOKR1bY0mbC-42w)\n- [黑盒测试和白盒测试](https://mp.weixin.qq.com/s/5kvrYMWG0vFR3vj-aNY49g)\n- [测试用例中的细节](https://mp.weixin.qq.com/s/wvScTliPwuvH9ReIoDQGNQ)\n- [集成测试、单元测试、系统测试](https://mp.weixin.qq.com/s/LRkxMasRvmDYRVb0_aybtA)\n- [集成测试类型和最佳实践](https://mp.weixin.qq.com/s/sSubzrs3cikLV7rmRQaWEA)\n- [软件测试中质量优于数量](https://mp.weixin.qq.com/s/4FxtVFqialRz6R4680rPAw)\n- [DevOps工具](https://mp.weixin.qq.com/s/4r8FoxQyYZ5naowML5Cw-Q)\n- [2020年Tester自我提升](https://mp.weixin.qq.com/s/vuhUp85_6Sbg6ReAN3TTSQ)\n- [DevOps中的测试工程师](https://mp.weixin.qq.com/s/42Ile_T1BAIp7QHleI-c7w)\n- [敏捷团队的回归测试策略](https://mp.weixin.qq.com/s/Z7dzDdfp5_kxvzBVQ3rEDg)\n- [测试自动化与自动化测试：差异很重要](https://mp.weixin.qq.com/s/6HC1bKesOs4mZYb9nOCHjw)\n- [自动化新手要避免的坑（上）](https://mp.weixin.qq.com/s/MjcX40heTRhEgCFhInoqYQ)\n- [自动化新手要避免的坑（下）](https://mp.weixin.qq.com/s/azDUo1IO5JgkJIS9n1CMRg)\n- [如何成为全栈自动化工程师](https://mp.weixin.qq.com/s/j2rQ3COFhg939KLrgKr_bg)\n- [左移测试](https://mp.weixin.qq.com/s/8zXkWV4ils17hUqlXIpXSw)\n- [选择手动测试还是自动化测试？](https://mp.weixin.qq.com/s/4haRrfSIp5Plgm_GN98lRA)\n- [从单元测试标准中学习](https://mp.weixin.qq.com/s/x0TyMAdPBWYL7JSPAmoQsw)\n- [负载测试很重要](https://mp.weixin.qq.com/s/2q7kNQVJuNwB948ks463CA)\n- [白盒测试扫盲](https://mp.weixin.qq.com/s/s_FvGZTC42GEjaWzroz1eA)\n- [自动化测试项目为何失败](https://mp.weixin.qq.com/s/KFJXuLjjs1hii47C1BH8PA)\n- [简化测试用例](https://mp.weixin.qq.com/s/BhwfDqhN9yoa3Iul_Eu5TA)\n- [敏捷测试二三事](https://mp.weixin.qq.com/s/bKkGWJA3JhvdCjgg6-AVEQ)\n- [软件测试中的虚拟化](https://mp.weixin.qq.com/s/zHyJiNFgHIo2ZaPFXsxQMg)\n- [新词：QA-Ops](https://mp.weixin.qq.com/s/detcY6OVYmzOTUxfwN6CFQ)\n- [生产环境中进行自动化测试](https://mp.weixin.qq.com/s/JKEGRLOlgpINUxs-6mohzA)\n- [所谓UI测试](https://mp.weixin.qq.com/s/wDvUy_BhQZCSCqrlC2j1qA)\n- [预上线环境失败的原因](https://mp.weixin.qq.com/s/jva0Jb2OMarERmTn7Kh2Ng)\n- [自动化策略六步走](https://mp.weixin.qq.com/s/He69k8iCKhTKD1j-yV6M5g)\n- [合格的测试经理必备技能](https://mp.weixin.qq.com/s/gFIYksHMn_bHEwAhmgVzjg)\n- [质量保障的拓展实践](https://mp.weixin.qq.com/s/a3sd0dQnjk3TerOhfo-1ng)\n- [敏捷领导者常见误区](https://mp.weixin.qq.com/s/xdq3CZflRjvDBGDLK4tNFQ)\n- [功能自动化测试策略](https://mp.weixin.qq.com/s/qHmcblN4cD4JK6jT7oU4fQ)\n- [性能测试、压力测试和负载测试](https://mp.weixin.qq.com/s/g26lpd7d7EtpN7pkiqkkjg)\n- [如何维护自动化测试](https://mp.weixin.qq.com/s/4eh4AN_MiatMSkoCMtY3UA)\n- [负载测试最佳实践](https://mp.weixin.qq.com/s/hNj7UsCCvv9TdexAcNFUvg)\n- [有关UI测试计划](https://mp.weixin.qq.com/s/D0fMXwJF754a7Mr5ARY5tQ)\n- [软件测试外包](https://mp.weixin.qq.com/s/sYQfb2PiQptcT0o_lLpBqQ)\n- [避免PPT自动化的最佳实践](https://mp.weixin.qq.com/s/5YgYK4_YLZ1wDDhbwMTGlw)\n- [如何优化软件测试成本](https://mp.weixin.qq.com/s/_eXrzDyNDA6yCRR8nPmzGA)\n- [如何从手动测试转到自动化测试](https://mp.weixin.qq.com/s/EBDTX4AMnn2KTEjL88bOhQ)\n- [Selenium自动化测试技巧](https://mp.weixin.qq.com/s/EzrpFaBSVITO2Y2UvYvw0w)\n- [测试为何会错过Bug](https://mp.weixin.qq.com/s/UFHy8OwZjnMkB70roIS-zQ)\n- [测试用例设计——一切测试的基础](https://mp.weixin.qq.com/s/0_ubnlhp2jk-jxHxJ95E9g)\n- [移动应用测试：挑战，类型和最佳实践](https://mp.weixin.qq.com/s/kYxh6xki69evVDsXDxrYKQ)\n- [敏捷测试中面临的挑战](https://mp.weixin.qq.com/s/vmsW56r1J7jWXHSZdcwbPg)\n- [AI如何影响测试行业](https://mp.weixin.qq.com/s/d6c7u1-lAmsiIQz3UvcGKg)\n- [自动测试失败的5个原因](https://mp.weixin.qq.com/s/bTakAHIcx_WyJIo-tsbvvg)\n- [大促前必做的质量检查](https://mp.weixin.qq.com/s/iOku2wKnlr8pSZO0l9Q3Bw)\n- [测试开发工程师工作技巧](https://mp.weixin.qq.com/s/TvrUCisja5Zbq-NIwy_2fQ)\n- [敏捷回归测试](https://mp.weixin.qq.com/s/_bBQFggkZTTEqcb9R_68OA)\n- [制定质量管理计划指南](https://mp.weixin.qq.com/s/ztXYE8EtwlkUdxnk1cjKVg)\n- [质量管理计划的基本要素](https://mp.weixin.qq.com/s/v8lOioYn01S1F0ex4mmljA)\n- [质量保障的方法和实践](https://mp.weixin.qq.com/s/hU_YCaZB-0a09dOCAVgcpw)\n- [为什么测试覆盖率如此重要](https://mp.weixin.qq.com/s/0evyuiU2kdXDgMDnDKjORg)\n- [自动化测试框架](https://mp.weixin.qq.com/s/vu6p_rQd3vFKDYu8JDJ0Rg)\n- [敏捷中的端到端测试](https://mp.weixin.qq.com/s/cdi4xnEzDLpl9ncQguLuAQ)\n- [自动化测试灵魂三问：是什么、为什么和做什么](https://mp.weixin.qq.com/s/geOejJx79-jTwafG9aXwqA)\n- [基于代码的自动化和无代码自动化](https://mp.weixin.qq.com/s/8Dopihqs4XzpU-sN-I94kw)\n- [物联网测试](https://mp.weixin.qq.com/s/B_JI4DANxoOq4HurxZC65Q)\n- [功能测试知多少](https://mp.weixin.qq.com/s/vTxZLwlvlfIBv892Ji-oLQ)\n- [如何选择自动化测试工具](https://mp.weixin.qq.com/s/yJo-d9bAZDs1Lcp8j7ISRg)\n- [连续测试策略](https://mp.weixin.qq.com/s/0aD_0cUW83oPu3sl7sHNnQ)\n- [如何设置质量检查流程](https://mp.weixin.qq.com/s/PQeXxMZzzU15xSfY5wkVgA)\n- [编写干净的代码之变量篇](https://mp.weixin.qq.com/s/J9rGIe8a2xaLlNJq2nVmzw)\n- [高效Mobile DevOps步骤](https://mp.weixin.qq.com/s/-qc-d_zJ1C9H_Uvd8gJiBw)\n- [回归BUG](https://mp.weixin.qq.com/s/00j-acjPeKQ7uap62WpY3w)\n- [处理回归BUG最佳实践](https://mp.weixin.qq.com/s/R3O2NruPAA2gQf4-3R6aAQ)\n- [自动化测试实践清单](https://mp.weixin.qq.com/s/972WruGsYmkRroquBFoqMg)\n- [自动化测试类型](https://mp.weixin.qq.com/s/GRkN8ozZiWNu21Y3KbVOBA)\n- [无脚本测试](https://mp.weixin.qq.com/s/PVBxk4KEwCmWkB6mOXJFlw)\n- [自动化测试转型挑战及其解决方案](https://mp.weixin.qq.com/s/BixS6jRdF5N_nvmW3_OthQ)\n- [无数据驱动自动化测试](https://mp.weixin.qq.com/s/aCYRGxkzMogLbmACYo6ssw)\n- [为什么自动化测试在敏捷开发中很重要](https://mp.weixin.qq.com/s/AP0wUQZ09NvSqme8e09igQ)\n- [测试模型中理解压力测试和负载测试](https://mp.weixin.qq.com/s/smNLx3malzM3avkrn3EJiA)\n- [移动测试工程师职业](https://mp.weixin.qq.com/s/dhtR4TbQNu5fWpmJkXGivw)\n- [远程测试工作挑战](https://mp.weixin.qq.com/s/LK-GEN4OtuWVGDuG8psmOQ)\n- [自动化测试用例的原子性](https://mp.weixin.qq.com/s/jA5WMHwJcu88nHXWoMBAdQ)\n- [可测性经验分享](https://mp.weixin.qq.com/s/iRtUjESYS3sh3YTD-BWjdA)\n- [敏捷中的回归测试的优化【译】](https://mp.weixin.qq.com/s/nDiZZgA1PIiAUCG_xwA2rA)\n- [敏捷的主要优势【译】](https://mp.weixin.qq.com/s/zkI85TLI37XrPFaQ-pZYMA)\n- [2021年自动化测试流行趋势【译】](https://mp.weixin.qq.com/s/dIZxkNT6mjgRukLBy0AJ6Q)\n- [敏捷团队的自动化测试【译】](https://mp.weixin.qq.com/s/5BvzQvdssTyp8voC9J9www)\n\n## 大咖风采\n\n- [Tcloud 云测平台--集大成者](https://mp.weixin.qq.com/s/29sEO39_NyDiJr-kY5ufdw)\n- [Android App 测试工具及知识大集合](https://mp.weixin.qq.com/s/Xk9rCW8whXOTAQuCfhZqTg)\n- [Android App常规测试内容](https://mp.weixin.qq.com/s/tweeoS5wTqK3k7R2TVuDXA)\n- [JVM的对象和堆](https://mp.weixin.qq.com/s/iNDpTz3gBK3By_bvUnrWOA)\n\n# UI自动化\n\n## UI自动化\n\n- [自动化测试中java多线程的使用实例](https://mp.weixin.qq.com/s/BNSLaIdcTPTNj1tKpGf6fw)\n- [自动化测试中递归函数的应用](https://mp.weixin.qq.com/s/86602zV9zYblhCRMiUlwdA)\n- [Appium 2.0速览](https://mp.weixin.qq.com/s/mHHSZKYZXQby8YiQBP57hA)\n\n## UiAutomator\n\n- [android uiautomator一个画心形图案的方法--代码的浪漫](https://mp.weixin.qq.com/s/byfAKHxD2i83VHnuaNgIZA)\n- [android UiAutomator了解源码解决控件bonds\\[0,0\\]无法点击](https://mp.weixin.qq.com/s/nu2ftXNUSG2_kmZjyhEcVA)\n- [android UiAutomator在清除文本时遇到中文的解决办法](https://mp.weixin.qq.com/s/cNGNCoXsYBSk-MWTWLxF4g)\n- [android UiAutomator获取当前页面某类控件个数的方法](https://mp.weixin.qq.com/s/njb19Sq_Kg4SusAS_eEuug)\n- [android uiautomator自定义监听示例--一个弹出权限设置的监听](https://mp.weixin.qq.com/s/OKKZOf51yq6qY5D6PvN-gg)\n- [如何在Mac OS上使用UiAutomator快速调试类](https://mp.weixin.qq.com/s/jm9d_42jp_BSlv-IW0BpzQ)\n- [UiAutomator测试中如何恢复手机输入法](https://mp.weixin.qq.com/s/o4-zCgbdq6OsHRK9XT14QA)\n- [android UiAutomator基本api的二次封装](https://mp.weixin.qq.com/s/_3jGg3ZYoeyAkjZpy8gWfQ)\n- [android UiAutomator让运行失败的用例重新运行](https://mp.weixin.qq.com/s/tMOPbt1w9tRaKEuIZYKCyg)\n- [利用UiAutomator写一个首页刷新的稳定性测试脚本](https://mp.weixin.qq.com/s/au9hAScsqUdcrh_usu8Pfg)\n- [android UiAutomator长按实现控制按住控件时间的方法](https://mp.weixin.qq.com/s/lOvxAOMh6mmIh3CEV6Fduw)\n- [android UiAutomator自定义快速调试类](https://mp.weixin.qq.com/s/iP2dTOeVkFMzU3dQ06R9GA)\n- [利用UiAutomator写一个自动遍历渠道包关键功能的脚本](https://mp.weixin.qq.com/s/0vg2OlfTy0y4T6sWUG-olA)\n- [android UiAutomator如何根据颜色判断控件的状态](https://mp.weixin.qq.com/s/kldsD3OZ4mJZ5yYQXfXxLw)\n- [android UiAutomator控制多台手机同时运行用例的方法](https://mp.weixin.qq.com/s/z9vgpOQP0wQffmG4C_oBWg)\n- [android UiAutomator使用递归函数写一个让屏幕一闪一闪提醒的方法](https://mp.weixin.qq.com/s/AzXjePdmsgs6QsICZOdPyw)\n- [android UiAutomator获取视频播放进度的方法](https://mp.weixin.qq.com/s/ho070zX9rrLPmh8bZe_HgQ)\n\n\n## Selenium\n\n- [selenium2java截图保存桌面](https://mp.weixin.qq.com/s/OUfwsIo635coGONRNccYlg)\n- [selenium2java调用JavaScript方法封装](https://mp.weixin.qq.com/s/t-Xs2Hr9TM2bjDiOqQX2mA)\n- [selenium2java利用mysq解决向浏览器插入cookies时token过期问题](https://mp.weixin.qq.com/s/oAAkDKUGytQjxJLFkod-AQ)\n- [selenium2java 遇到有三个窗口用例的处理办法](https://mp.weixin.qq.com/s/6AJBanVKYwlsNcvsu_25QQ)\n- [selenium2java通过第三方登录绕过知乎登陆验证码](https://mp.weixin.qq.com/s/A5uTtxlg4l4pru2z7v1cug)\n- [selenium2java使用select处理下拉框示例](https://mp.weixin.qq.com/s/FFor451WzuUzINeclGN-Ng)\n- [selenium2java爬虫示例](https://mp.weixin.qq.com/s/vSZzpzEqsCtASSx6iHqxVA)\n- [selenium2java写一个设置秒杀价的脚本](https://mp.weixin.qq.com/s/1ocIOYt3gdGIJrd9v2shhg)\n- [selenium2java基本方法二次封装](https://mp.weixin.qq.com/s/2GaXigt13wa6JgxJkcef5g)\n- [selenium2java一个弹框上传时间日期大杂烩测试用例](https://mp.weixin.qq.com/s/Z8ZeZ-zFy0q0a-e_epT1Kg)\n- [selenium2java造数据例子](https://mp.weixin.qq.com/s/ACO2O5f7Po4Qn242lopMBg)\n- [selenium2java让浏览器停止加载的方法](https://mp.weixin.qq.com/s/aBQdGYys3Bpyf6yigGOCIA)\n- [selenium2java写一个强制刷新页面的方法](https://mp.weixin.qq.com/s/VWW7cH5WSDmw_eCabUh9LQ)\n- [selenium2java通过接口获取并注入cookies](https://mp.weixin.qq.com/s/luLHWxPWSekuDMbnKsfJvg)\n- [Selenium编写自动化用例的8种技巧](https://mp.weixin.qq.com/s/8wRHc_krXNfWclNeOJDNPg)\n- [JUnit中用于Selenium测试的中实践](https://mp.weixin.qq.com/s/KG4sltQMCfH2MGXkRdtnwA)\n- [您如何使用Selenium来计算自动化测试的投资回报率？](https://mp.weixin.qq.com/s/DVSEm0DhoAvYfTWIniabJg)\n- [Selenium 4 Java的最佳测试框架](https://mp.weixin.qq.com/s/MlNyv-kb03gRTcYllxUreA)\n- [Selenium 4.0 Alpha更新日志](https://mp.weixin.qq.com/s/tU7sm-pcbpRNwDU9D3OVTQ)\n- [Selenium 4.0 Alpha更新实践](https://mp.weixin.qq.com/s/yT9wpO5o5aWBUus494TIHw)\n- [JUnit 5和Selenium基础（一）](https://mp.weixin.qq.com/s/ehBRf7st-OxeuvI_0yW3OQ)\n- [JUnit 5和Selenium基础（二）](https://mp.weixin.qq.com/s/Gt82cPmS2iX-DhKXTXiy8g)\n- [JUnit 5和Selenium基础（三）](https://mp.weixin.qq.com/s/8YkonXTYgAV5-pLs9yEAVw)\n- [如何在跨浏览器测试中提高效率](https://mp.weixin.qq.com/s/MB_Wv7yQ6i9BztAZtL4grA)\n- [Selenium Python使用技巧（一）](https://mp.weixin.qq.com/s/39v8tXG3xig63d-ioEAi8Q)\n- [Selenium Python使用技巧（二）](https://mp.weixin.qq.com/s/uDM3y9zoVjaRmJJJTNs6Vw)\n- [Selenium Python使用技巧（三）](https://mp.weixin.qq.com/s/J7-CO-UDspUGSpB8isjsmQ)\n- [Selenium并行测试基础](https://mp.weixin.qq.com/s/OfXipd7YtqL2AdGAQ5cIMw)\n- [Selenium并行测试最佳实践](https://mp.weixin.qq.com/s/-RsQZaT5pH8DHPvm0L8Hjw)\n- [维护Selenium测试自动化的最佳实践](https://mp.weixin.qq.com/s/EMD1aWuzOSfT7j3KeXhJcA)\n- [Selenium自动化测试技巧](https://mp.weixin.qq.com/s/EzrpFaBSVITO2Y2UvYvw0w)\n- [Selenium自动化：代码测试与无代码测试](https://mp.weixin.qq.com/s/gtmLpQ5FCeuzh1SB5mxuvg)\n- [Selenium处理下拉列表](https://mp.weixin.qq.com/s/E2txSVAmDzYIEZWnyAND4g)\n- [Selenium自动化常见问题](https://mp.weixin.qq.com/s/edoxu-QaD0SOw1VqrhCZWA)\n- [Selenium4 IDE，它终于来了](https://mp.weixin.qq.com/s/XNotlZvFpmBmBQy1pYifOw)\n- [Selenium4 IDE特性：无代码趋势和SIDE Runner](https://mp.weixin.qq.com/s/G0S9K0jHsN0P_jxdMME-cg)\n- [Selenium4 IDE特性：弹性测试、循环和逻辑判断](https://mp.weixin.qq.com/s/o4_jIyi9O7s4S3CbTzl5rQ)\n- [Selenium自动化最佳实践技巧（上）](https://mp.weixin.qq.com/s/lZww1azmncMMMHRY0_yKqA)\n- [Selenium自动化最佳实践技巧（中）](https://mp.weixin.qq.com/s/9D0lUZ-XKHiukNeRqp6zOQ)\n- [Selenium自动化最佳实践技巧（下）](https://mp.weixin.qq.com/s/opVik2ZxmTBurIBoa4yipQ)\n- [Selenium等待：sleep、隐式、显式和Fluent](https://mp.weixin.qq.com/s/73BobMq9M12rYMvzxNhRtA)\n- [Selenium自动化的JUnit参数化实践](https://mp.weixin.qq.com/s/WFu5rJaowxhAIcbEoEatkw)\n- [Selenium异常集锦](https://mp.weixin.qq.com/s/DDkaliSVthX-c_KKG-WwNA)\n- [Selenium自动化测试之前](https://mp.weixin.qq.com/s/DKjSnS9sP0SoHUw4OhOikw)\n\n## APP性能\n\n- [使用monkey测试时，一个控制WiFi状态的多线程类](https://mp.weixin.qq.com/s/P8HVtzHBlj_FcDAAHFKBDg)\n- [java执行和停止Logcat命令及多线程实现](https://mp.weixin.qq.com/s/sUYibRc-muxQoxi48QiaRg)\n- [APP性能测试中获取CPU和PSS数据多线程实现](https://mp.weixin.qq.com/s/NiJSZ8VxpdnarbDJjcJziA)\n- [统计APP启动时间和进入首页时间的多线程类](https://mp.weixin.qq.com/s/IMs6vd3H-HF65Vb-zPwDhw)\n- [如何获取手机性能测试数据FPS](https://mp.weixin.qq.com/s/qZy5AQkNpUXRJk46BHVzaQ)\n- [一个循环启动APP并保持WiFi常开的多线程类](https://mp.weixin.qq.com/s/OgdT4IffDyAdkKmO2SS9iQ)\n\n## 杂乱\n\n- [测试窝，首页抄我七篇原创还拉黑，你们的良心不会痛吗？](https://mp.weixin.qq.com/s/ke5avkknkDMCLMAOGT7wiQ)\n- [如何优雅地屏蔽掉Google搜索结果中视频、新闻、图片等结果](https://mp.weixin.qq.com/s/Iu7pt4Qk3w9sJp3n_UVAeQ)\n- [测试玩梗--欢迎补充](https://mp.weixin.qq.com/s/y_QHbsjFCQVSCfj-A4Usmg)\n- [图解HTTP脑图](https://mp.weixin.qq.com/s/100Vm8FVEuXs0x6rDGTipw)\n- [测试之JVM命令脑图](https://mp.weixin.qq.com/s/qprqyv0j3SCvGw1HMjbaMQ)\n- [好书推荐《Java性能权威指南》](https://mp.weixin.qq.com/s/YWd5Yx6n7887g1lMLTcsWQ)\n- [2019年浏览器市场份额排行榜](https://mp.weixin.qq.com/s/4NmJ_ZCPD5UwaRCtaCfjEg)\n- [JSON基础](https://mp.weixin.qq.com/s/tnQmAFfFbRloYp8J9TYurw)\n- [JMeter吞吐量误差分析](https://mp.weixin.qq.com/s/jHKmFNrLmjpihnoigNNCSg)\n- [JMeter如何模拟不同的网络速度](https://mp.weixin.qq.com/s/1FCwNN2htfTGF6ItdkcCzw)\n- [疫情期间，如何提高远程办公效率](https://mp.weixin.qq.com/s/k_XrdqjGKMshK2Ea-VCNLw)\n- [接口测试视频专题](https://mp.weixin.qq.com/s/4mKpW3QiVRee3kcVOSraog)\n- [Groovy在JMeter中应用专题](https://mp.weixin.qq.com/s/KcxPUDWl7MLQemFRoIV92A)\n- [Java多线程编程在JMeter中应用](https://mp.weixin.qq.com/s/xCnFx5TvIF1SAVNm-aZnxQ)\n- [未来的神器fiddler Everywhere](https://mp.weixin.qq.com/s/-BSuHR6RPkdv8R-iy47MLQ)\n- [Charles报错Failed to install helper解决方案](https://mp.weixin.qq.com/s/LHhMTBhlDM0DrPCvWeU0zA)\n- [测试仓库推介（上）](https://mp.weixin.qq.com/s/zgy6UgNMFcbISD1NhxSAWg)\n- [测试仓库推介（下）](https://mp.weixin.qq.com/s/njnpmRGoEgdxjqkR7c3a6A)\n- [Fiddler Everywhere工具答疑](https://mp.weixin.qq.com/s/2peWMJ-rgDlVjs3STNeS1Q)\n- [Mac上测试Internet Explorer的N种方法](https://mp.weixin.qq.com/s/HeLBPTp2dfs5IlyLMCi90Q)\n- [IntelliJ中基于文本的HTTP客户端](https://mp.weixin.qq.com/s/-9qi_lLVVfxQKEFmcRYFtA)\n- [开源礼节](https://mp.weixin.qq.com/s/EyNules2f9NYdnYAX_NQSw)\n- [弱网测试：最低流畅网速是多少？](https://mp.weixin.qq.com/s/rCji6fZs9yYyk7GyIWvSiA)\n- [接口测试直播回顾](https://mp.weixin.qq.com/s/B8ih9sswaE-OWuVib6C16g)\n- [SpotBugs报错no Groovy library is defined解决办法](https://mp.weixin.qq.com/s/XxvuVS2TmlqT5-b22vObYQ)\n- [推荐好书：不要总是谦卑地弯着腰](https://mp.weixin.qq.com/s/mYNN9jSaikOF5aJEkb-Bug)\n- [2020年FunTester自我总结](https://mp.weixin.qq.com/s/DeDY1JZUTk3cjjQfr3DJRg)\n- [原创打油诗欣赏](https://mp.weixin.qq.com/s/3hPSDjH-3cWu6EVsjU0wOw)\n- [优秀讲师 | 腾讯云+社区权威认证](https://mp.weixin.qq.com/s/oeTeJZs6h4jJJMRyUunurw)\n- [Boss直聘签约作者](https://mp.weixin.qq.com/s/CjwFBoS9sew6R74mM9QO4g)\n- [GDevOps官方合作媒体](https://mp.weixin.qq.com/s/OnwvRqrgXq_pGp8eOXOTgw)\n- [假期思考题](https://mp.weixin.qq.com/s/3DOnkmYDlwk-XKg4ge3ZUw)\n- [甩锅技能+1](https://mp.weixin.qq.com/s/nMwlfXZoDcRRPHcTKpvfNg)\n- [不要浪费自己的求知欲](https://mp.weixin.qq.com/s/WO0aQqmhU_xGUpWvYwOqUA)\n"
  },
  {
    "path": "document/base.markdown",
    "content": "# 基础篇\n\n> **FunTester**，[腾讯云年度作者](https://mp.weixin.qq.com/s/oeTeJZs6h4jJJMRyUunurw)、[Boss直聘签约作者](https://mp.weixin.qq.com/s/CjwFBoS9sew6R74mM9QO4g)，非著名测试开发er，欢迎关注。\n\n* `Gitee`地址*https://gitee.com/fanapi/tester*\n* `GitHub`地址*https://github.com/JunManYuanLong/FunTester*\n\n\n# 语言合集\n\n## Java\n\n- [java一行代码打印心形](https://mp.weixin.qq.com/s/QPSryoSbViVURpSa9QXtpg)\n- [操作的原子性与线程安全](https://mp.weixin.qq.com/s/QU3llkGLepX2VCch8Y9GKw)\n- [快看，i++真的不安全](https://mp.weixin.qq.com/s/-CdWdROKSEq_ZiLX2kWxzA)\n- [原子操作组合与线程安全](https://mp.weixin.qq.com/s/XB5LXucAF5Bo8EkfLZYRmw)\n- [java利用for循环输出正三角新解](https://mp.weixin.qq.com/s/nnMR2177LLVn4u_9s9Fl4g)\n- [在main方法之前，到底执行了什么？](https://mp.weixin.qq.com/s/jWxiCMfwmvRHrjPdRG8ZyQ)\n- [传参传的到底是什么?](https://mp.weixin.qq.com/s/p_pEQwE6h6q7PprkW-kjbg)\n- [json里面put了null会怎么样？](https://mp.weixin.qq.com/s/gQVROe01I3JzIqNdTSHpDQ)\n- [主线程都结束了，为何进程还在执行](https://mp.weixin.qq.com/s/q2v5JU5dtmNEol7I7IVY-Q)\n- [java测试框架如何执行groovy脚本文件](https://mp.weixin.qq.com/s/0GYt1l3_z5-1qzBNl6_PzA)\n- [java用递归筛选法求N以内的孪生质数（孪生素数）](https://mp.weixin.qq.com/s/PSdCb-DrgMPb4WpJJMexmQ)\n- [从JVM堆内存分析验证深浅拷贝](https://mp.weixin.qq.com/s/SdYDnoau1rjjvPC2SUymBg)\n- [如何学习Java基础](https://mp.weixin.qq.com/s/FCPStkYoJF67NYln4Lc6xg)\n- [如何保存HTTPrequestbase和CloseableHttpResponse](https://mp.weixin.qq.com/s/gRY8HRQHCh0PfyS7Q22IwA)\n- [如何在匿名thread子类中保证线程安全](https://mp.weixin.qq.com/s/GCXx_-ummi0JfZQ7GTIxig)\n- [Java服务端两个常见的并发错误](https://mp.weixin.qq.com/s/5VvCox3eY6sQDsuaKB4ZIw)\n- [Java中interface属性和实例方法](https://mp.weixin.qq.com/s/vrKkM6522tgw3v_cL7R8HA)\n- [服务端性能优化之双重检查锁](https://mp.weixin.qq.com/s/-bOyHBcqFlJY3c0PEZaWgQ)\n- [Java并发BUG基础篇](https://mp.weixin.qq.com/s/NR4vYx81HtgAEqH2Q93k2Q)\n- [Java并发BUG提升篇](https://mp.weixin.qq.com/s/GCRRe8hJpe1QJtxq9VBEhg)\n- [性能测试中图形化输出测试数据](https://mp.weixin.qq.com/s/EMvpYIsszdwBJFPIxztTvA)\n- [超大对象导致Full GC超高的BUG分享](https://mp.weixin.qq.com/s/L15-0JW9WK-E005GeOG9WQ)\n- [利用ThreadLocal解决线程同步问题](https://mp.weixin.qq.com/s/VEm8jt3ZUEUdyyeXPC8VvQ)\n- [线程安全集合类中的对象是安全的么？](https://mp.weixin.qq.com/s/WKSuPEfzZCVwjVTcoD0Dyg)\n- [如何使用“dd MM”解析日期](https://mp.weixin.qq.com/s/v9ooAj3dKu53JXgxB482HA)\n- [Java和Groovy正则使用](https://mp.weixin.qq.com/s/DT3BKE3ZcCKf6TLzGc5wbg)\n- [运行越来越快的Java热点代码](https://mp.weixin.qq.com/s/AP0BcDEjDuaouaB0RXJOoQ)\n- [6个重要的JVM性能参数](https://mp.weixin.qq.com/s/b1QnapiAVn0HD5DQU9JrIw)\n- [ArrayList浅、深拷贝](https://mp.weixin.qq.com/s/kYsBzFsCyDPUssdV3MDqLA)\n- [Java性能测试中两种锁的实现](https://mp.weixin.qq.com/s/j9dGFvYzCJ0AGwYUtTrTsw)\n- [测试如何处理Java异常](https://mp.weixin.qq.com/s/H00GWiATOD8QHJu3UewrBw)\n- [创建Java守护线程](https://mp.weixin.qq.com/s/_UjWdvq8QWYTshr4SeniBg)\n- [Lambda表达式在线程安全Map中应用](https://mp.weixin.qq.com/s/zZjB5aOWh4a_k1eoEsR5ww)\n- [Java程序是如何浪费内存的](https://mp.weixin.qq.com/s/w7VF5m5cc0X7LNvqmwGfvg)\n- [Java中的自定义异常](https://mp.weixin.qq.com/s/nspIdxFP9qEDtagGN4gaMQ)\n- [Java文本块](https://mp.weixin.qq.com/s/GwasvpJsd7uLngvCr6KlQw)\n- [CountDownLatch类在性能测试中应用](https://mp.weixin.qq.com/s/uYBPPOjauR2h81l2uKMANQ)\n- [CyclicBarrier类在性能测试中应用](https://mp.weixin.qq.com/s/kvEHX3t_2xpMke9vwOdWrg)\n- [Phaser类在性能测试中应用](https://mp.weixin.qq.com/s/plxNnQq7yNQvHYEGpyY4uA)\n- [Java压缩/解压缩字符串](https://mp.weixin.qq.com/s/7vHNd5dEN93DPUqgS8od_A)\n- [Java删除空字符：Java8 & Java11](https://mp.weixin.qq.com/s/6dlgYgTFZsHuJ4Eaby5eyg)\n- [Java Stream中map和flatMap方法](https://mp.weixin.qq.com/s/0FG2o7VUAG6z8a_0je-1EQ)\n- [泛型类的正确用法](https://mp.weixin.qq.com/s/1azilraonPIZNCnw_9MB5Q)\n- [Java字符串到数组的转换--最后放大招](https://mp.weixin.qq.com/s/iMUYZYkJ5CjykwWqinNm5g)\n- [Java求数组的并集--最后放大招](https://mp.weixin.qq.com/s/bZ93SGakyiRbaRujhx4nvw)\n- [Java计算数组平均值--最后放大招](https://mp.weixin.qq.com/s/dxQaFHu2PyAbOK6jpEgEUQ)\n- [Math.abs()求绝对值返回负值BUG分享](https://mp.weixin.qq.com/s/RHzExuRqF1XsBtzGKzmgGA)\n- [Java代理模式初探](https://mp.weixin.qq.com/s/SBL_K2PQez3vDHhtAN9NLg)\n- [Socket接口异步验证实践](https://mp.weixin.qq.com/s/bnjHK3ZmEzHm3y-xaSVkTw)\n- [性能测试中异步展示测试进度](https://mp.weixin.qq.com/s/AOERJbEc4ATJqhjvnxgQoA)\n- [Java中的ThreadLocal功能演示](https://mp.weixin.qq.com/s/n92k1JswHKrqT7Y_CD9Q0w)\n- [ThreadLocal在链路性能测试中实践](https://mp.weixin.qq.com/s/3qhNdHHSStELzNraQSpcew)\n- [歪解字符串中连续出现次数最多问题](https://mp.weixin.qq.com/s/xBy4iB4qLd4WQgCsVVuemw)\n- [Java&Groovy下载文件对比](https://mp.weixin.qq.com/s/T9WUynej2yOZhCkDUhaLYw)\n- [线程同步类CyclicBarrier在性能测试集合点应用](https://mp.weixin.qq.com/s/K2YySxX9T4v_rzbvIbIHJA)\n- [Java线程同步三剑客](https://mp.weixin.qq.com/s/cAmd11-HdwXNU3tp4TiDLg)\n\n## Groovy\n\n- [java和groovy混合编程时提示找不到符合错误解决办法](https://mp.weixin.qq.com/s/dLC2W7nIi5zCuK6JTkiA-w)\n- [groovy使用stream语法递归筛选法求N以内的质数](https://mp.weixin.qq.com/s/TsrVn1cuQUrU6wj9OnR-FQ)\n- [使用Groovy进行Bash（shell）操作](https://mp.weixin.qq.com/s/fgCTlZUF3QeNj6jzq1ZgGg)\n- [使用Groovy和Gradle轻松进行数据库操作](https://mp.weixin.qq.com/s/lwmclrnW0csykVRhu7dNTQ)\n- [愉快地使用Groovy Shell](https://mp.weixin.qq.com/s/fJh7fbB3naBFBEiaS62oxw)\n- [Gradle+Groovy基础篇](https://mp.weixin.qq.com/s/c2j7G-PoNtAB3oYYDUhCGw)\n- [Gradle+Groovy提高篇](https://mp.weixin.qq.com/s/yXmYj_1fynLkR0-5FV_Arw)\n- [Groovy重载操作符](https://mp.weixin.qq.com/s/4jW06Q4_vjFR9DovRTTuHg)\n- [用Groovy处理JMeter断言和日志](https://mp.weixin.qq.com/s/Q4yPA4p8dZYAARZ60ZDh9w)\n- [用Groovy处理JMeter变量](https://mp.weixin.qq.com/s/BxtweLrBUptM8r3LxmeM_Q)\n- [用Groovy在JMeter中执行命令行](https://mp.weixin.qq.com/s/VTip7tiLpwBOr1gUoZ0n8A)\n- [用Groovy处理JMeter中的请求参数](https://mp.weixin.qq.com/s/9pCUOXWpMwXR5ynvCMYJ7A)\n- [Java和Groovy正则使用](https://mp.weixin.qq.com/s/DT3BKE3ZcCKf6TLzGc5wbg)\n- [Groovy中的元组](https://mp.weixin.qq.com/s/0-ka0-tv1vyKbiA6m44jRw)\n- [从Java到Groovy的八级进化论](https://mp.weixin.qq.com/s/QTrRHsD3w-zLGbn79y8yUg)\n- [用Groovy在JMeter中使用正则提取赋值](https://mp.weixin.qq.com/s/9riPpnQZCfKGscuzOOpYmQ)\n- [Groovy在JMeter中处理cookie](https://mp.weixin.qq.com/s/DCnDjWaj2aiKv5HVw3-n6A)\n- [Groovy在JMeter中处理header](https://mp.weixin.qq.com/s/juY-1jEWODJ5HHiEsxhIEw)\n- [Groovy的神奇NullObject](https://mp.weixin.qq.com/s/jLGisN_30PrCgNP33Sww0g)\n- [Groovy中的list](https://mp.weixin.qq.com/s/0mUe1_WrUiEm1t6kqCV3eQ)\n- [JMeter参数签名——Groovy脚本形式](https://mp.weixin.qq.com/s/wQN9-xAUQofSqiAVFXdqug)\n- [Groovy中的闭包](https://mp.weixin.qq.com/s/pfcG47gSPfUveAaEfdeo8A)\n- [JMeter参数签名——Groovy工具类形式](https://mp.weixin.qq.com/s/urwU4p9ofv9sU-JFy5Z0iA)\n- [删除List中null的N种方法--最后放大招](https://mp.weixin.qq.com/s/4mfskN781dybyL59dbSbeQ)\n- [混合Java函数和Groovy闭包](https://mp.weixin.qq.com/s/FAIzGgLSX2u7RKbOGs3lGA)\n- [Groovy重载操作符（终极版）](https://mp.weixin.qq.com/s/4oYGJ2B2Y1AqxsIj8v5nZA)\n- [JsonPath工具类单元测试](https://mp.weixin.qq.com/s/1YtUWGk_sTjn9bHwAeT0Ew)\n- [Groovy小记it关键字和IDE报错](https://mp.weixin.qq.com/s/cIMHzkvKtH0a0ewkiBnV8g)\n- [JsonPath验证类既Groovy重载操作符实践](https://mp.weixin.qq.com/s/5gc04CAsBY6pWxe5c2P41w)\n- [Groovy枚举类初始化异常分析](https://mp.weixin.qq.com/s/koFhpBZM1MFYYxCNxUKPyQ)\n- [Java&Groovy下载文件对比](https://mp.weixin.qq.com/s/T9WUynej2yOZhCkDUhaLYw)\n\n## Python\n\n- [python使用filter方法递归筛选法求N以内的质数（素数）--附一行打印心形标记的代码解析](https://mp.weixin.qq.com/s/D8RfpdIi8smCL8TAzBcNpA)\n- [关于python版微信使用经验分享](https://mp.weixin.qq.com/s/19IaI6ETZAm_T4ePPlXqIg)\n- [python用递归筛选法求N以内的孪生质数（孪生素数）](https://mp.weixin.qq.com/s/rVY2pTl8So11WCvA9GrFbA)\n- [利用python wxpy和requests写一个自动应答微信机器人实例](https://mp.weixin.qq.com/s/Fni2kX5BRjdqOQ-glCLjRg)\n- [Python版Socket.IO接口测试脚本](https://mp.weixin.qq.com/s/oXBP6Sx3yPqlmvV9uCUScw)\n\n\n## 测开笔记\n\n- [我的开发日记（一）](https://mp.weixin.qq.com/s/eQgpOKbXsU9vOmxp0Xiklg)\n- [我的开发日记（二）](https://mp.weixin.qq.com/s/XuffL3ZmKKOgHDtH_cEYOw)\n- [我的开发日记（三）](https://mp.weixin.qq.com/s/a-I0agh6nWp8RLlcmbgf5w)\n- [我的开发日记（四）](https://mp.weixin.qq.com/s/QukXd00Mx_dbkgiXys0FNg)\n- [我的开发日记（五）](https://mp.weixin.qq.com/s/6P3nScsVW6MfMcyIqcA1AQ)\n- [我的开发日记（六）](https://mp.weixin.qq.com/s/Gz2QmukONNldSy9Fd29u5w)\n- [我的开发日记（七）](https://mp.weixin.qq.com/s/MjZ-nFXfQkHMsXS0fX1c1w)\n- [我的开发日记（八）](https://mp.weixin.qq.com/s/6ZhNcFm-gR5dhKQjEkE3Rg)\n- [我的开发日记（九）](https://mp.weixin.qq.com/s/VfD2T3orojGxnylr3Q5UeA)\n- [我的开发日记（十）](https://mp.weixin.qq.com/s/6DWth40LGbAraJi05G16Pw)\n- [我的开发日记（十一）](https://mp.weixin.qq.com/s/nsX5A-P6QbePHDN_Pse0_A)\n- [我的开发日记（十二）](https://mp.weixin.qq.com/s/XA1KJXBP3Zl-XFswXxUtvg)\n- [我的开发日记（十三）](https://mp.weixin.qq.com/s/_QPUu5pUlg4A_AlC5wOGkA)\n- [我的开发日记（十四）](https://mp.weixin.qq.com/s/Qy1YKAb3wqW_Ip2FwH7Otw)\n- [我的开发日记（十五）](https://mp.weixin.qq.com/s/bwkvz2t6YItQD0O_BIxpHQ)\n- [这些年，我写过的BUG（一）](https://mp.weixin.qq.com/s/mVTmT1FdwWl1e0BaL7Ne1g)\n- [这些年，我写过的BUG（二）](https://mp.weixin.qq.com/s/NMz5n0ZMf6taGb-gr1BLyw)\n- [FunTester测试框架架构图初探](https://mp.weixin.qq.com/s/bcMbVDkWbHSXjZFDeFyJsQ)\n- [FunTester测试项目架构图初探](https://mp.weixin.qq.com/s/wqb8FXRbEXrhDuZounmNXA)\n"
  },
  {
    "path": "document/directory.markdown",
    "content": "* 由于文章链接存在敏感词问题,请各位看官移步\n  \n## [GitHub地址](https://github.com/JunManYuanLong/FunTester/blob/okay/document/article.markdown)\n\n\n### 当然也可以关注**FunTester**公众号"
  },
  {
    "path": "document/update.markdown",
    "content": "# 升级篇\n\n> **FunTester**，[腾讯云年度作者](https://mp.weixin.qq.com/s/oeTeJZs6h4jJJMRyUunurw)、[Boss直聘签约作者](https://mp.weixin.qq.com/s/CjwFBoS9sew6R74mM9QO4g)，非著名测试开发er，欢迎关注。\n\n* `Gitee`地址*https://gitee.com/fanapi/tester*\n* `GitHub`地址*https://github.com/JunManYuanLong/FunTester*\n\n\n# 案例分享\n\n## 测试方案\n\n- [如何对消息队列做性能测试](https://mp.weixin.qq.com/s/MNt22aW3Op9VQ5OoMzPwBw)\n- [如何对修改密码接口进行压测](https://mp.weixin.qq.com/s/9CL_6-uZOlAh7oeo7NOpag)\n- [如何测试概率型业务接口](https://mp.weixin.qq.com/s/kUVffhjae3eYivrGqo6ZMg)\n- [如何测试非固定型概率算法P=p(1+0.1*N)](https://mp.weixin.qq.com/s/sgg8v-Bi-_sUDJXwuTCMGg)\n- [性能测试中标记每个请求](https://mp.weixin.qq.com/s/PokvzoLdVf_y9inlVXHJHQ)\n- [如何对N个接口按比例压测](https://mp.weixin.qq.com/s/GZxbH4GjDkk4BLqnUj1_kw)\n- [多种登录方式定量性能测试方案](https://mp.weixin.qq.com/s/WuZ2h2rr0rNBgEvQVioacA)\n- [压测中测量异步写入接口的延迟](https://mp.weixin.qq.com/s/odvK1iYgg4eRVtOOPbq15w)\n- [绑定手机号性能测试](https://mp.weixin.qq.com/s/K5x1t1dKtIT2NKV6k4v5mw)\n- [手机号验证码登录性能测试](https://mp.weixin.qq.com/s/i-j8fJAdcsJ7v8XPOnPDAw)\n- [重放浏览器单个请求性能测试实践](https://mp.weixin.qq.com/s/a10hxCrIzS4TV9JwmDSI3Q)\n- [重放浏览器多个请求性能测试实践](https://mp.weixin.qq.com/s/Hm1Kpp1PMrZ5rYFW8l2GlA)\n- [重放浏览器请求多链路性能测试实践](https://mp.weixin.qq.com/s/9YSBLAyHVw8Z6IfK-nJTpQ)\n- [Socket接口固定QPS性能测试实践](https://mp.weixin.qq.com/s/I9-14L8THxvtX1NJY0KPfw)\n\n## BUG集锦\n\n- [一个MySQL索引引发的血案](https://mp.weixin.qq.com/s/KLSber-gPg53JVfsCa3Dtw)\n- [微软Zune闰年BUG分析](https://mp.weixin.qq.com/s/zpqAUcNcHaZjWUdUYH_loQ)\n- [“双花”BUG的测试分享](https://mp.weixin.qq.com/s/0dsBsssNfg-seJ_tu9zFaQ)\n- [iOS 11计算器1+2+3=24真的是bug么？](https://mp.weixin.qq.com/s/nokQhe_Hqcq-o7pZJmFlqQ)\n- [不要在遍历的时候删除](https://mp.weixin.qq.com/s/MIczbEpbOrADL0_V7ZUhlg)\n- [连开100年会员会怎样](https://mp.weixin.qq.com/s/mZw-SFIxFFbE-o8UeXhdfg)\n- [异步查询转同步加redis业务实现的BUG分享](https://mp.weixin.qq.com/s/ni3f6QTxw0K-0I3epvEYOA)\n- [Java服务端两个常见的并发错误](https://mp.weixin.qq.com/s/5VvCox3eY6sQDsuaKB4ZIw)\n- [超大对象导致Full GC超高的BUG分享](https://mp.weixin.qq.com/s/L15-0JW9WK-E005GeOG9WQ)\n- [访问权限导致toString返回空BUG分享](https://mp.weixin.qq.com/s/usDOcuJrXOmEKN-mVBzRKg)\n- [异常使用中的BUG](https://mp.weixin.qq.com/s/IG9Ar3IT7CrlSv4d0lCvgA)\n- [Math.abs()求绝对值返回负值BUG分享](https://mp.weixin.qq.com/s/RHzExuRqF1XsBtzGKzmgGA)\n\n## 爬虫实践\n\n- [接口爬虫之网页表单数据提取](https://mp.weixin.qq.com/s/imJ5u67xhYQaEzv-O1in4g)\n- [httpclient爬虫爬取汉字拼音等信息](https://mp.weixin.qq.com/s/w-IvBxAsotmPA3pydpIo1w)\n- [httpclient爬虫爬取电影信息和下载地址实例](https://mp.weixin.qq.com/s/TB49X4S-ioyoW5CrzAnHcw)\n- [httpclient 多线程爬虫实例](https://mp.weixin.qq.com/s/nXL-MP4Y6CN2hgZQefWEeQ)\n- [groovy爬虫练习之——企业信息](https://mp.weixin.qq.com/s/1TisDceIL1-Luqz_wOqAiw)\n- [httpclient 爬虫实例——爬取三级中学名](https://mp.weixin.qq.com/s/Dd7U30aHYauqBFxJdxaiyg)\n- [电子书网站爬虫实践](https://mp.weixin.qq.com/s/KGW0dIS5NTLgxyhSjxDiOw)\n- [groovy爬虫实例——历史上的今天](https://mp.weixin.qq.com/s/5LDUvpU6t_GZ09uhZr224A)\n- [爬取720万条城市历史天气数据](https://mp.weixin.qq.com/s/vOyKpeGlJSJp9bQ8NIMe2A)\n- [记一次失败的爬虫](https://mp.weixin.qq.com/s/SMylrZLXDGw5f1xKI9ObnA)\n- [爬虫实践--CBA历年比赛数据](https://mp.weixin.qq.com/s/mM_QSQddabU5im_O6iVR-Q)\n- [图片爬虫实践](https://mp.weixin.qq.com/s/u5bRSyKsmn3TcjqEEqRJpw)\n\n# 工具合集\n\n## JsonPath合集\n\n- [JsonPath实践（一）](https://mp.weixin.qq.com/s/Cq0_v_ptbGd4f5y8HIsq7w)\n- [JsonPath实践（二）](https://mp.weixin.qq.com/s/w_iJTiuQahIw6U00CJVJZg)\n- [JsonPath实践（三）](https://mp.weixin.qq.com/s/58A3k0T6dbOkBJ5nRYKDqA)\n- [JsonPath实践（四）](https://mp.weixin.qq.com/s/8ER61qrkMj8bdBpyuq9r6w)\n- [JsonPath实践（五）](https://mp.weixin.qq.com/s/knVLW960WXnckGLstdrOVQ)\n- [JsonPath实践（六）](https://mp.weixin.qq.com/s/ckBCK3t1w68FLBhaw5a7Jw)\n- [JsonPath工具类封装](https://mp.weixin.qq.com/s/KyuCuG5fVEExxBdGJO2LdA)\n- [JsonPath工具类单元测试](https://mp.weixin.qq.com/s/1YtUWGk_sTjn9bHwAeT0Ew)\n- [JsonPath验证类既Groovy重载操作符实践](https://mp.weixin.qq.com/s/5gc04CAsBY6pWxe5c2P41w)\n- [JSON对象标记语法验证类](https://mp.weixin.qq.com/s/jSXmoEdMF7nWAqQuzJ5GiQ)\n\n## Jacoco覆盖率\n\n- [接口测试代码覆盖率（jacoco）方案分享](https://mp.weixin.qq.com/s/D73Sq6NLjeRKN8aCpGLOjQ)\n- [jacoco无法读取build.xml配置中源码路径解决办法](https://mp.weixin.qq.com/s/8_x0rVfkIi-uX3y0drx_jw)\n- [使用JaCoCo Maven插件创建代码覆盖率报告](https://mp.weixin.qq.com/s/4Jo05k2WxytiSSNW9WTV-A)\n- [Java 8，Jenkins，Jacoco和Sonar进行持续集成](https://mp.weixin.qq.com/s/dOoXnKnWtQmmC5itClsl4g)\n- [jacoco测试覆盖率过滤非业务类](https://mp.weixin.qq.com/s/7YGe9pCHw3wd87tgOlKjSA)\n\n## arthas诊断工具\n\n- [arthas快速入门视频演示](https://mp.weixin.qq.com/s/Wl5QMD52isGTRuAP4Cpo-A)\n- [arthas进阶thread命令视频演示](https://mp.weixin.qq.com/s/XuF7Nr1sGC3diIn50zlDDQ)\n- [arthas命令jvm,sysprop,sysenv,vmoption视频演示](https://mp.weixin.qq.com/s/87BsTYqnTCnVdG3a_kBcng)\n- [arthas命令logger动态修改日志级别--视频演示](https://mp.weixin.qq.com/s/w724P9B12eTC9rMbavwsMA)\n- [arthas命令sc和sm视频演示](https://mp.weixin.qq.com/s/Ga63sjW_bOKQqfnA5LTb9w)\n- [arthas命令ognl视频演示](https://mp.weixin.qq.com/s/cMCaXFwjp6QHFq40TvP4bQ)\n- [arthas命令redefine实现Java热更新](https://mp.weixin.qq.com/s/2HUXfJhoUfg4yMzSoRHK9w)\n- [arthas命令monitor监控方法执行](https://mp.weixin.qq.com/s/7-oe3UoTY8bzpi89tIKvQQ)\n- [arthas命令watch观察方法调用（上）](https://mp.weixin.qq.com/s/6fMKP7H4Q7ll_0v-wyN19g)\n- [arthas命令watch观察方法调用（下）](https://mp.weixin.qq.com/s/-r2kufxdOjRb2TgF2HPskg)\n- [arthas命令trace追踪方法链路](https://mp.weixin.qq.com/s/bzkdKZugkOl8C-_xTw92YA)\n- [arthas命令tt方法时空隧道](https://mp.weixin.qq.com/s/mDczYmVdSmL5ZbK7bb8i0A)\n\n## moco API\n\n- [解决moco框架API在post请求json参数情况下query失效的问题](https://mp.weixin.qq.com/s/V5lXoepEBtPJrSUHA0Uz5A)\n- [给moco API添加limit功能](https://mp.weixin.qq.com/s/pXJECi15ieNLmA0uIqEqfA)\n- [给moco API添加random功能](https://mp.weixin.qq.com/s/YTcbFbFaWB5arW_fubgTTQ)\n- [解决moco框架API在cycle方法缺失的问题](https://mp.weixin.qq.com/s/YfsPa7eW8WV65CDbPooBPg)\n- [五行代码构建静态博客](https://mp.weixin.qq.com/s/hZnimJOg5OqxRSDyFvuiiQ)\n- [moco API模拟框架视频讲解（上）](https://mp.weixin.qq.com/s/X5-fFXe018_O60WCRdawZg)\n- [moco API模拟框架视频讲解（中）](https://mp.weixin.qq.com/s/g2En-9W9JWYrCLQr_WPEBA)\n- [moco API模拟框架视频讲解（下）](https://mp.weixin.qq.com/s/mz__DiNxMGHwIKCLsjKR8g)\n- [如何mock固定QPS的接口](https://mp.weixin.qq.com/s/yogj9Fni0KJkyQuKuDYlbA)\n- [mock延迟响应的接口](https://mp.weixin.qq.com/s/x_fu0InQpYIUJIQFi9a50g)\n- [moco固定QPS接口升级补偿机制](https://mp.weixin.qq.com/s/zAM91e_REo4edSPTLuHLOw)\n\n## 工具类\n\n- [java网格输出的类](https://mp.weixin.qq.com/s/BJTJu0LGjn7Hc9J1yT04KQ)\n- [java使用poi写入excel文档的一种解决方案](https://mp.weixin.qq.com/s/Ft56gd1B9CPrQs2zq4Cpug)\n- [java使用poi读取excel文档的一种解决方案](https://mp.weixin.qq.com/s/ltZGx9J7E8DTer0D-pfQ2Q)\n- [MongoDB操作类封装](https://mp.weixin.qq.com/s/u-RHOE5XrjOEkelWIxdplw)\n- [java网格输出的类](https://mp.weixin.qq.com/s/QW8nKM2Bz7C75fdkCzSbpw)\n- [将json数据格式化输出到控制台](https://mp.weixin.qq.com/s/2IPwvh-33Ov2jBh0_L8shA)\n- [利用反射根据方法名执行方法的使用示例](https://mp.weixin.qq.com/s/5ntwDo4ZVcTh1PmK4vkNfA)\n- [解决统计出现次数问题的方法类](https://mp.weixin.qq.com/s/gqz4wuKkMWAOIQwMtiupnA)\n- [java利用时间戳来获取UTC时间](https://mp.weixin.qq.com/s/wbDIrwDnxb9_XWkkmP3A_g)\n- [如何遍历执行一个包里面每个类的用例方法](https://mp.weixin.qq.com/s/OJwCOHCJ4TalatsEWbtzIQ)\n- [阿拉伯数字转成汉字](https://mp.weixin.qq.com/s/jNZXIvwMpdxt7jIAlVBgHg)\n- [获取JVM转储文件的Java工具类](https://mp.weixin.qq.com/s/f_TlOb3m8MeR3argBmTzzA)\n- [基于DOM的XML文件解析类](https://mp.weixin.qq.com/s/scRj7OAhvJYL3mx_hCFp4A)\n- [XML文件解析实践（DOM解析）](https://mp.weixin.qq.com/s/V2DG3osaPNUJzFNDQgqM-w)\n- [基于DOM4J的XML文件解析类](https://mp.weixin.qq.com/s/K5R7iMXouTn4g0p14T7iAQ)\n- [将HTTP请求对象转成curl命令行](https://mp.weixin.qq.com/s/861uMAMMWtINjy4Z99WA6w)\n\n## 构建工具\n\n- [java和groovy混编的Maven项目如何用intellij打包执行jar包](https://mp.weixin.qq.com/s/bKexZXlONeo3r6FDhfMltQ)\n- [window系统权限不足导致gradle构建失败的解决办法](https://mp.weixin.qq.com/s/dqiQvmVG1o6glU-pknLDwQ)\n- [使用groovy脚本使gradle灵活加载本地jar包的两种方式](https://mp.weixin.qq.com/s/p3K3ZS7iOUeKO7E94gKFVg)\n- [Java 8，Jenkins，Jacoco和Sonar进行持续集成](https://mp.weixin.qq.com/s/dOoXnKnWtQmmC5itClsl4g)\n- [Gradle如何在任务失败后继续构建](https://mp.weixin.qq.com/s/GcXDzRN7cM_QQpt9ytqoKg)\n- [Gradle+Groovy基础篇](https://mp.weixin.qq.com/s/c2j7G-PoNtAB3oYYDUhCGw)\n- [Gradle+Groovy提高篇](https://mp.weixin.qq.com/s/yXmYj_1fynLkR0-5FV_Arw)\n- [Maven进行增量构建](https://mp.weixin.qq.com/s/ThQ7j6TS93KJZFqlNx8IQg)\n- [SonarQube8.3中的Maven项目的测试覆盖率报告](https://mp.weixin.qq.com/s/Xhp26jyE1c7Auielz48Llw)\n\n## plotly可视化\n\n- [MacOS使用pip安装pandas提示Cannot uninstall 'numpy'解决方案](https://mp.weixin.qq.com/s/fIqMAMXRQvf_vBtS5jDsyg)\n- [Python使用plotly生成本地文件教程](https://mp.weixin.qq.com/s/4dJdIP-g3fF40vX7S31jNg)\n- [Python2.7使用plotly绘制本地散点图和折线图实例](https://mp.weixin.qq.com/s/9QWrA0c-STmrmjSkBYWvbQ)\n- [Python可视化工具plotly从数据库读取数据作图示例](https://mp.weixin.qq.com/s/EUtPidiz_r1rpQBH_kudbA)\n- [利用Python+plotly制作接口请求时间的violin图表](https://mp.weixin.qq.com/s/3GdiLaiVRfkxwM3MOG-U8w)\n- [Python+plotly生成本地饼状图实例](https://mp.weixin.qq.com/s/61Qz9Kz-4ruzC0OvIuElpA)\n- [python plotly处理接口性能测试数据方法封装](https://mp.weixin.qq.com/s/NxVdvYlD7PheNCv8AMYqhg)\n- [利用python+plotly 制作接口响应时间Distplot图表](https://mp.weixin.qq.com/s/yrcUW1fFC18newqHcxhVvw)\n- [利用 python+plotly 制作Contour Plots模拟双波源干涉现象](https://mp.weixin.qq.com/s/vNW80BDeHsyjNQrnaBGk3Q)\n- [利用 python+plotly 制作双波源干涉三维图像](https://mp.weixin.qq.com/s/KSeV8VvQXRIg-bnzYoa5qg)\n- [python plotly制作接口响应耗时的时间序列表（Time Series ）](https://mp.weixin.qq.com/s/U8chcVzCjGTdT3T_X5v4kw)\n- [python使用plotly批量生成图表](https://mp.weixin.qq.com/s/l18WfWz-s6qQ1JKKuh_2AQ)"
  },
  {
    "path": "long/1",
    "content": "id=324,23,4,234,2,4,32,4,23\nsize1=9\nsize2=5\nc=5,10,15\na=3,6,9\nb=4,8,12"
  },
  {
    "path": "long/30",
    "content": "309\n163\n240\n173\n169\n114\n158\n87\n106\n107\n143\n131\n175\n112\n236\n230\n357\n83\n255\n191\n314\n120\n316\n81\n195\n107\n141\n163\n151\n123\n171\n237\n172\n109\n128\n206\n206\n149\n83\n104\n191\n198\n90\n105\n157\n91\n118\n148\n214\n238\n148\n175\n191\n276\n161\n158\n74\n345\n115\n134\n154\n154\n139\n96\n187\n174\n124\n105\n160\n70\n77\n134\n303\n82\n127\n169\n370\n174\n147\n233\n128\n198\n480\n140\n95\n231\n189\n237\n279\n170\n108\n146\n240\n165\n109\n236\n181\n228\n150\n108\n340\n207\n141\n221\n161\n97\n183\n134\n136\n111\n155\n90\n123\n192\n187\n130\n134\n128\n155\n330\n127\n324\n281\n197\n214\n193\n146\n120\n76\n121\n254\n157\n164\n186\n120\n145\n192\n132\n317\n154\n195\n144\n121\n185\n138\n97\n120\n287\n251\n221\n126\n599\n130\n208\n137\n261\n165\n100\n179\n218\n159\n102\n109\n175\n176\n124\n99\n225\n268\n161\n105\n98\n179\n136\n170\n201\n100\n98\n342\n231\n111\n292\n180\n239\n85\n145\n155\n232\n159\n195\n121\n307\n164\n203\n102\n122\n235\n203\n172\n280\n336\n177\n226\n318\n114\n146\n137\n204\n293\n138\n188\n366\n77\n131\n202\n174\n175\n131\n120\n253\n147\n183\n123\n208\n140\n274\n184\n118\n117\n140\n335\n162\n164\n132\n142\n183\n220\n126\n221\n216\n157\n94\n177\n115\n267\n174\n178\n80\n113\n152\n86\n177\n194\n184\n91\n129\n171\n356\n144\n159\n143\n205\n237\n156\n198\n90\n198\n130\n107\n137\n140\n148\n93\n239\n281\n162\n229\n301\n460\n142\n173\n271\n67\n168\n434\n116\n139\n227\n257\n98\n158\n249\n192\n201\n130\n191\n92\n83\n102\n82\n413\n168\n223\n272\n114\n302\n256\n154\n127\n231\n224\n100\n114\n148\n106\n175\n285\n127\n201\n167\n229\n138\n96\n170\n133\n104\n104\n102\n230\n237\n224\n241\n129\n165\n314\n182\n175\n136\n270\n175\n130\n132\n123\n162\n354\n184\n100\n207\n181\n354\n120\n97\n196\n171\n99\n107\n301\n190\n169\n191\n150\n80\n157\n183\n101\n171\n148\n100\n180\n132\n234\n235\n148\n184\n100\n117\n121\n163\n290\n399\n130\n296\n74\n164\n208\n90\n307\n167\n97\n123\n134\n127\n249\n225\n250\n208\n233\n91\n83\n244\n411\n162\n90\n237\n161\n109\n112\n154\n126\n98\n179\n96\n168\n195\n350\n162\n271\n271\n112\n379\n175\n218\n153\n120\n91\n118\n90\n83\n87\n237\n196\n246\n186\n126\n106\n127\n142\n162\n411\n57\n94\n118\n163\n121\n99\n152\n194\n318\n361\n135\n82\n121\n175\n119\n263\n251\n146\n181\n131\n196\n74\n299\n119\n133\n102\n65\n118\n87\n341\n177\n112\n290\n133\n120\n146\n94\n241\n110\n271\n190\n239\n243\n160\n167\n266\n223\n456\n251\n173\n116\n597\n176\n277\n213\n159\n103\n129\n122\n170\n217\n466\n206\n118\n257\n142\n111\n155\n153\n297\n127\n225\n124\n236\n192\n194\n223\n203\n106\n302\n401\n244\n112\n178\n157\n186\n151\n124\n159\n118\n203\n153\n358\n108\n209\n333\n247\n154\n114\n72\n143\n113\n102\n144\n96\n183\n114\n128\n119\n230\n410\n234\n187\n160\n157\n266\n64\n228\n162\n121\n219\n250\n129\n289\n141\n112\n133\n128\n178\n97\n126\n293\n263\n245\n94\n301\n271\n104\n239\n223\n116\n168\n156\n330\n93\n165\n106\n139\n107\n139\n85\n173\n200\n290\n146\n199\n128\n193\n109\n195\n224\n329\n149\n153\n316\n137\n111\n319\n114\n144\n133\n213\n145\n201\n245\n134\n214\n255\n178\n164\n197\n112\n306\n132\n240\n145\n152\n110\n95\n170\n89\n130\n106\n199\n182\n153\n131\n232\n293\n178\n167\n182\n145\n174\n128\n152\n192\n172\n106\n171\n94\n286\n183\n195\n568\n237\n267\n152\n263\n144\n119\n124\n152\n159\n84\n169\n149\n159\n249\n440\n223\n298\n161\n152\n136\n269\n275\n188\n124\n185\n134\n379\n244\n177\n115\n105\n103\n207\n269\n123\n130\n109\n130\n269\n151\n124\n125\n223\n142\n95\n184\n528\n201\n232\n152\n141\n189\n179\n261\n202\n203\n93\n273\n133\n155\n276\n150\n101\n119\n253\n396\n252\n162\n141\n121\n206\n119\n198\n125\n161\n154\n120\n116\n226\n202\n426\n156\n205\n191\n108\n135\n93\n99\n379\n152\n140\n141\n226\n180\n146\n141\n144\n273\n68\n216\n306\n95\n180\n128\n155\n127\n166\n87\n167\n407\n128\n157\n178\n103\n166\n152\n221\n147\n66\n113\n135\n209\n289\n217\n265\n136\n247\n98\n436\n131\n259\n246\n125\n150\n189\n153\n238\n101\n155\n82\n105\n137\n288\n179\n238\n141\n418\n194\n105\n122\n207\n128\n170\n264\n289\n174\n121\n190\n203\n111\n148\n266\n334\n102\n214\n177\n307\n118\n108\n228\n168\n176\n345\n92\n159\n106\n292\n184\n192\n168\n184\n191\n226\n135\n121\n198\n86\n327\n327\n165\n119\n117\n158\n136\n270\n110\n201\n192\n104\n182\n275\n92\n262\n195\n231\n165\n102\n160\n161\n241\n122\n207\n135\n112\n169\n98\n155\n315\n118\n138\n259\n178\n248\n201\n293\n158\n315\n201\n187\n144\n143\n226\n207\n117\n149\n191\n153\n277\n166\n112\n124\n227\n195\n162\n135\n164\n286\n95\n188\n113\n132\n117\n126\n403\n122\n288\n141\n106\n146\n347\n267\n121\n137\n334\n119\n144\n345\n228\n115\n171\n187\n99\n92\n120\n131\n173\n166\n132\n351\n133\n204\n208\n117\n180\n262\n176\n119\n248\n179\n149\n160\n198\n104\n200\n220\n145\n206\n146\n246\n126\n249\n240\n196\n368\n160\n196\n365\n219\n205\n122\n122\n224\n143\n240\n171\n251\n171\n149\n97\n408\n251\n127\n172\n154\n217\n281\n118\n357\n145\n126\n207\n242\n216\n134\n171\n109\n139\n177\n131\n142\n201\n123\n124\n147\n80\n166\n93\n120\n83\n156\n111\n115\n422\n132\n154\n107\n249\n102\n107\n174\n140\n218\n147\n84\n283\n79\n262\n202\n314\n235\n174\n83\n222\n138\n143\n193\n184\n314\n101\n199\n185\n129\n148\n154\n182\n289\n82\n363\n134\n103\n161\n218\n327\n101\n191\n111\n174\n276\n294\n179\n163\n167\n296\n212\n142\n196\n152\n103\n288\n285\n105\n150\n119\n86\n153\n140\n178\n216\n179\n164\n382\n88\n467\n291\n104\n308\n247\n416\n97\n259\n158\n158\n152\n206\n163\n283\n157\n191\n119\n132\n121\n156\n269\n152\n133\n177\n330\n165\n150\n194\n105\n161\n199\n86\n269\n127\n109\n105\n117\n148\n393\n176\n206\n316\n130\n103\n205\n106\n422\n167\n185\n203\n123\n136\n145\n138\n259\n143\n98\n143\n117\n300\n239\n106\n196\n101\n174\n373\n158\n171\n373\n106\n190\n105\n221\n399\n208\n199\n108\n100\n144\n272\n76\n226\n259\n135\n71\n171\n313\n110\n232\n103\n162\n155\n221\n112\n105\n193\n146\n173\n147\n351\n145\n105\n223\n303\n255\n239\n246\n104\n82\n192\n137\n116\n127\n403\n336\n120\n203\n115\n161\n116\n313\n253\n141\n209\n326\n146\n163\n342\n101\n134\n234\n178\n290\n173\n179\n155\n329\n241\n172\n241\n136\n226\n140\n186\n245\n128\n193\n191\n109\n68\n214\n118\n113\n167\n172\n180\n118\n244\n175\n411\n52\n302\n97\n121\n131\n147\n171\n174\n188\n203\n163\n180\n125\n175\n89\n287\n309\n119\n145\n390\n140\n236\n104\n132\n322\n89\n113\n231\n152\n364\n141\n316\n114\n205\n148\n232\n228\n266\n233\n185\n204\n183\n205\n319\n251\n174\n160\n152\n95\n270\n98\n159\n258\n290\n115\n131\n114\n122\n169\n155\n122\n104\n123\n106\n366\n202\n75\n115\n122\n170\n147\n286\n247\n181\n122\n199\n136\n217\n399\n206\n119\n100\n126\n116\n427\n156\n178\n225\n380\n126\n168\n166\n153\n79\n156\n195\n241\n175\n174\n145\n84\n260\n223\n203\n198\n183\n108\n154\n162\n140\n130\n91\n200\n397\n486\n100\n157\n190\n185\n128\n145\n118\n339\n238\n107\n112\n270\n163\n315\n147\n256\n217\n163\n125\n159\n145\n328\n134\n243\n239\n136\n246\n111\n285\n473\n100\n87\n80\n200\n245\n210\n215\n91\n239\n77\n171\n118\n121\n111\n95\n326\n86\n146\n175\n356\n155\n244\n111\n231\n138\n275\n147\n185\n156\n190\n170\n129\n177\n209\n227\n161\n206\n120\n261\n383\n220\n79\n221\n189\n138\n134\n135\n82\n230\n367\n165\n106\n179\n225\n129\n74\n171\n178\n137\n81\n195\n122\n144\n162\n224\n110\n187\n155\n269\n171\n250\n286\n156\n166\n140\n134\n106\n296\n143\n102\n285\n308\n94\n163\n163\n97\n119\n91\n160\n135\n201\n135\n402\n111\n162\n266\n374\n113\n130\n295\n303\n213\n93\n512\n188\n292\n161\n63\n265\n88\n165\n238\n138\n285\n362\n153\n93\n113\n204\n521\n427\n166\n85\n145\n92\n116\n348\n246\n155\n276\n122\n226\n93\n328\n86\n177\n261\n94\n265\n216\n209\n123\n133\n302\n199\n136\n139\n282\n157\n173\n208\n395\n95\n199\n166\n139\n105\n310\n125\n91\n174\n102\n99\n128\n134\n93\n137\n165\n240\n170\n254\n165\n138\n208\n146\n175\n232\n97\n191\n144\n204\n76\n233\n100\n154\n149\n156\n191\n380\n113\n225\n228\n195\n391\n337\n202\n178\n107\n243\n219\n185\n164\n218\n267\n304\n240\n159\n168\n155\n109\n136\n84\n265\n280\n81\n106\n91\n116\n220\n352\n224\n191\n140\n132\n131\n96\n418\n219\n142\n228\n167\n312\n132\n122\n370\n101\n168\n328\n312\n149\n136\n360\n205\n170\n114\n171\n139\n238\n110\n189\n347\n166\n102\n74\n336\n158\n214\n249\n79\n131\n291\n124\n105\n140\n97\n190\n270\n367\n210\n285\n173\n159\n115\n173\n233\n84\n144\n151\n131\n103\n286\n136\n148\n301\n140\n124\n203\n118\n158\n181\n130\n311\n299\n145\n159\n156\n108\n138\n156\n248\n323\n254\n140\n111\n195\n150\n153\n200\n396\n177\n143\n127\n220\n228\n181\n114\n93\n201\n67\n347\n180\n134\n174\n96\n163\n127\n159\n148\n147\n174\n105\n157\n225\n206\n348\n333\n311\n113\n313\n274\n181\n151\n150\n107\n108\n182\n170\n225\n147\n151\n267\n264\n135\n178\n162\n154\n109\n114\n95\n153\n158\n107\n169\n80\n84\n173\n262\n214\n168\n373\n200\n325\n171\n179\n192\n153\n193\n116\n211\n254\n243\n119\n148\n185\n152\n171\n286\n396\n129\n296\n165\n396\n173\n184\n112\n302\n165\n157\n347\n297\n233\n94\n119\n120\n177\n359\n132\n188\n230\n209\n102\n147\n250\n204\n149\n158\n197\n67\n86\n487\n198\n252\n176\n84\n253\n106\n233\n119\n163\n186\n97\n275\n238\n245\n325\n214\n185\n162\n193\n206\n102\n277\n89\n145\n102\n190\n155\n389\n135\n87\n185\n80\n219\n164\n242\n187\n138\n118\n187\n219\n107\n129\n274\n155\n159\n120\n115\n283\n260\n147\n190\n159\n222\n212\n293\n114\n119\n200\n173\n232\n169\n230\n134\n116\n352\n337\n107\n207\n195\n136\n109\n90\n252\n206\n221\n384\n307\n180\n115\n119\n161\n218\n100\n212\n286\n336\n205\n92\n220\n66\n204\n183\n253\n128\n166\n256\n228\n75\n79\n371\n109\n222\n181\n214\n126\n145\n82\n105\n112\n214\n160\n104\n150\n275\n124\n169\n413\n122\n224\n218\n175\n117\n185\n116\n115\n252\n132\n280\n235\n266\n170\n139\n80\n73\n257\n200\n308\n80\n147\n129\n234\n145\n95\n415\n271\n128\n111\n289\n371\n228\n142\n90\n183\n136\n285\n129\n115\n329\n427\n184\n171\n172\n189\n248\n175\n201\n283\n95\n168\n165\n165\n274\n186\n209\n193\n157\n218\n100\n514\n277\n127\n144\n228\n244\n373\n183\n205\n131\n145\n230\n89\n78\n162\n153\n266\n197\n68\n78\n66\n337\n137\n128\n473\n182\n181\n224\n246\n117\n157\n167\n230\n249\n665\n131\n267\n414\n130\n113\n85\n275\n152\n108\n227\n392\n194\n82\n206\n142\n136\n118\n213\n104\n152\n116\n129\n188\n190\n134\n122\n133\n246\n130\n133\n153\n168\n301\n199\n365\n221\n103\n240\n343\n222\n223\n169\n224\n80\n243\n193\n144\n240\n219\n175\n273\n165\n174\n132\n313\n188\n223\n156\n128\n107\n242\n140\n546\n97\n112\n181\n176\n132\n168\n314\n137\n179\n141\n129\n87\n127\n171\n171\n97\n146\n117\n122\n86\n214\n88\n83\n366\n201\n105\n374\n124\n181\n194\n97\n69\n122\n124\n150\n149\n325\n137\n236\n145\n203\n80\n160\n299\n164\n192\n93\n193\n326\n148\n199\n158\n177\n105\n74\n255\n354\n159\n215\n144\n119\n80\n149\n209\n272\n333\n230\n287\n149\n235\n121\n215\n246\n86\n163\n206\n206\n155\n160\n146\n189\n169\n191\n123\n387\n283\n166\n91\n169\n233\n223\n229\n68\n235\n371\n354\n242\n398\n189\n65\n224\n315\n88\n179\n113\n106\n199\n436\n106\n189\n135\n232\n255\n96\n382\n101\n97\n280\n90\n195\n67\n87\n55\n379\n117\n294\n169\n164\n166\n196\n169\n177\n119\n151\n157\n614\n362\n176\n174\n381\n370\n130\n285\n94\n102\n170\n161\n142\n280\n89\n84\n111\n126\n224\n119\n153\n332\n183\n231\n246\n112\n234\n177\n204\n134\n155\n240\n174\n179\n196\n258\n314\n161\n139\n88\n134\n227\n233\n178\n142\n275\n194\n123\n141\n94\n135\n105\n144\n193\n134\n468\n177\n92\n135\n345\n390\n360\n145\n358\n67\n192\n111\n144\n129\n191\n191\n202\n97\n185\n112\n107\n205\n140\n164\n369\n438\n88\n100\n174\n67\n67\n66\n72\n304\n143\n170\n179\n81\n226\n91\n219\n246\n295\n139\n166\n171\n167\n215\n203\n248\n278\n174\n219\n117\n240\n200\n116\n285\n126\n143\n203\n126\n170\n218\n146\n148\n139\n250\n315\n67\n145\n112\n137\n137\n136\n157\n150\n85\n126\n249\n220\n94\n213\n194\n257\n424\n111\n177\n201\n317\n212\n170\n258\n165\n279\n240\n161\n323\n246\n263\n182\n200\n125\n149\n258\n255\n304\n285\n124\n226\n245\n219\n438\n74\n287\n275\n274\n144\n257\n144\n176\n74\n207\n141\n111\n106\n237\n116\n155\n86\n60\n58\n73\n393\n243\n338\n193\n158\n193\n250\n136\n185\n105\n153\n231\n258\n151\n317\n154\n134\n94\n156\n403\n284\n143\n174\n166\n206\n171\n177\n235\n113\n80\n187\n118\n231\n246\n102\n84\n84\n130\n152\n295\n140\n168\n504\n208\n209\n468\n222\n263\n122\n334\n153\n200\n116\n191\n126\n87\n152\n110\n170\n195\n148\n181\n116\n178\n141\n155\n159\n100\n352\n238\n354\n204\n242\n111\n579\n525\n216\n102\n167\n86\n253\n174\n183\n191\n77\n146\n93\n83\n195\n113\n85\n111\n222\n234\n104\n139\n253\n93\n58\n78\n430\n73\n332\n209\n128\n185\n111\n199\n241\n163\n221\n350\n120\n246\n157\n329\n316\n111\n212\n114\n121\n133\n247\n135\n82\n211\n212\n204\n260\n272\n216\n312\n264\n273\n105\n278\n93\n150\n115\n191\n184\n217\n161\n92\n171\n536\n201\n139\n243\n103\n297\n232\n237\n145\n208\n190\n236\n181\n125\n510\n230\n134\n130\n203\n285\n119\n289\n141\n549\n158\n138\n169\n256\n212\n82\n257\n147\n95\n334\n140\n168\n108\n181\n161\n149\n140\n126\n163\n248\n245\n151\n83\n231\n61\n56\n61\n72\n67\n54\n136\n316\n199\n232\n128\n172\n123\n121\n109\n127\n214\n171\n273\n209\n436\n633\n477\n417\n81\n260\n215\n77\n152\n167\n146\n82\n121\n121\n136\n166\n219\n155\n84\n250\n142\n149\n213\n241\n107\n215\n102\n168\n244\n89\n123\n96\n259\n290\n293\n245\n234\n109\n300\n397\n242\n155\n235\n165\n154\n195\n130\n137\n212\n102\n184\n127\n289\n271\n123\n152\n448\n196\n288\n246\n120\n376\n191\n108\n168\n133\n122\n214\n170\n312\n134\n177\n339\n121\n371\n80\n142\n255\n134\n234\n65\n60\n101\n64\n51\n74\n112\n383\n75\n222\n153\n223\n167\n283\n268\n136\n104\n354\n218\n112\n330\n128\n175\n320\n188\n230\n260\n205\n249\n179\n98\n121\n148\n96\n232\n216\n190\n233\n290\n306\n127\n178\n168\n306\n102\n199\n259\n163\n237\n269\n305\n305\n180\n207\n129\n119\n224\n91\n122\n136\n103\n139\n181\n349\n175\n241\n175\n160\n186\n283\n148\n266\n291\n179\n441\n177\n206\n140\n105\n144\n285\n314\n434\n129\n104\n96\n194\n179\n218\n125\n221\n124\n170\n156\n159\n181\n291\n280\n83\n197\n64\n64\n84\n65\n63\n67\n130\n328\n303\n131\n163\n136\n134\n154\n165\n130\n240\n128\n176\n198\n149\n220\n137\n123\n429\n173\n199\n157\n349\n145\n181\n294\n288\n178\n230\n104\n158\n125\n314\n364\n211\n153\n77\n230\n286\n189\n123\n283\n179\n178\n118\n124\n829\n269\n239\n227\n106\n183\n116\n123\n214\n119\n161\n94\n80\n88\n141\n178\n121\n164\n410\n147\n125\n242\n182\n250\n134\n239\n220\n170\n196\n475\n229\n174\n400\n256\n356\n252\n144\n72\n294\n139\n128\n117\n237\n236\n149\n83\n112\n91\n212\n101\n89\n62\n64\n77\n139\n507\n367\n233\n116\n366\n82\n276\n135\n179\n110\n262\n232\n208\n381\n390\n107\n370\n107\n226\n155\n125\n126\n96\n159\n202\n213\n285\n208\n168\n131\n377\n84\n270\n144\n203\n252\n242\n276\n204\n150\n247\n417\n74\n404\n383\n221\n151\n136\n320\n149\n133\n130\n67\n90\n264\n112\n118\n246\n223\n203\n145\n206\n248\n269\n311\n336\n112\n330\n142\n152\n459\n309\n101\n101\n140\n169\n194\n258\n181\n256\n260\n74\n73\n127\n139\n141\n111\n174\n71\n118\n228\n67\n61\n60\n68\n55\n52\n60\n117\n51"
  },
  {
    "path": "long/poster.markdown",
    "content": "# FunTester广告位分享\n\n由于公众号资源有限,所以暂定每周一篇头条软文投放,为了方便特写此文档.\n\n日期周一代表当周.\n\nps:有兴趣可以一起聊一聊文末和次条.\n\n转载事宜直接微信联系\n\n[原创汇总,每周更新](https://gitee.com/fanapi/tester/blob/okay/document/directory.markdown) \n\n|日期(周一)|状态|代号|付款|\n|----|----|----|-----|\n|12.7|已预订| L+C|已付款|\n|12.14|已预订|L|已付款|\n|12.21|已预订|N|已付款|\n|12.28|已预订|L|已付款|\n|1.4|已预订|N|被鸽|\n|1.4|已预订|C|未付款|\n|1.11|已预订|M|被鸽|\n|1.11|已预订|M|已付款|\n|1.18|未预定|||\n|1.25|未预定|||\n|2.22|已预定|七七八十一|已付款|\n|3.1|未预定|虚位以待||\n|3.8|已预定|花卷|已付款|\n|3.15|虚位以待||\n|3.22|已预定|花卷|未付款\n|3.29|未预定|虚位以待||\n|4.5|未预定|虚位以待||\n|4.12|已预订|花卷|未付款|\n|4.20|已预订|花卷|未付款|\n|4.26|未预定|虚位以待|未付款|\n\n"
  },
  {
    "path": "long/sql/performance.sql",
    "content": "/*\n Navicat Premium Data Transfer\n\n Source Server         : test\n Source Server Type    : MySQL\n Source Server Version : 50640\n Source Host           : 172.18.4.55:3306\n Source Schema         : okayapi\n\n Target Server Type    : MySQL\n Target Server Version : 50640\n File Encoding         : 65001\n\n Date: 02/01/2020 18:13:16\n*/\n\nSET NAMES utf8mb4;\nSET FOREIGN_KEY_CHECKS = 0;\n\n-- ----------------------------\n-- Table structure for performance\n-- ----------------------------\nDROP TABLE IF EXISTS `performance`;\nCREATE TABLE `performance` (\n  `id` int(10) NOT NULL AUTO_INCREMENT,\n  `threads` int(4) DEFAULT NULL COMMENT '线程数',\n  `rt` int(5) DEFAULT NULL COMMENT '平均响应时间，ms',\n  `qps` double(10,4) DEFAULT NULL COMMENT 'QPS处理能力  /s',\n  `error` double(10,4) DEFAULT NULL COMMENT '错误率',\n  `fail` double(10,4) DEFAULT NULL COMMENT '失败率',\n  `des` varchar(1000) DEFAULT NULL COMMENT '任务描述',\n  `total` int(10) DEFAULT NULL COMMENT '总请求次数',\n  `start_time` timestamp NULL DEFAULT NULL,\n  `end_time` timestamp NULL DEFAULT NULL,\n  `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,\n  PRIMARY KEY (`id`) USING BTREE\n) ENGINE=InnoDB AUTO_INCREMENT=91 DEFAULT CHARSET=utf8;\n\nSET FOREIGN_KEY_CHECKS = 1;\n"
  },
  {
    "path": "long/sql/request.sql",
    "content": "/*\n Navicat Premium Data Transfer\n\n Source Server         : test\n Source Server Type    : MySQL\n Source Server Version : 50640\n Source Host           : 172.18.4.55:3306\n Source Schema         : okayapi\n\n Target Server Type    : MySQL\n Target Server Version : 50640\n File Encoding         : 65001\n\n Date: 02/01/2020 18:14:01\n*/\n\nSET NAMES utf8mb4;\nSET FOREIGN_KEY_CHECKS = 0;\n\n-- ----------------------------\n-- Table structure for request\n-- ----------------------------\nDROP TABLE IF EXISTS `request`;\nCREATE TABLE `request` (\n  `id` int(10) NOT NULL AUTO_INCREMENT,\n  `type` varchar(6) DEFAULT NULL,\n  `method` varchar(6) DEFAULT NULL,\n  `domain` varchar(100) DEFAULT NULL,\n  `api` varchar(100) DEFAULT NULL,\n  `status` int(10) DEFAULT NULL,\n  `code` int(10) DEFAULT NULL,\n  `expend_time` double(10,2) DEFAULT NULL,\n  `data_size` int(6) DEFAULT NULL,\n  `local_ip` varchar(20) DEFAULT NULL,\n  `local_name` varchar(20) DEFAULT NULL,\n  `create_time` varchar(100) DEFAULT NULL,\n  PRIMARY KEY (`id`) USING BTREE\n) ENGINE=InnoDB AUTO_INCREMENT=122167 DEFAULT CHARSET=utf8;\n\nSET FOREIGN_KEY_CHECKS = 1;\n"
  },
  {
    "path": "readme.markdown",
    "content": "# 分支简介\n\n该分支为主分支,其他分支停止更新.\n\n> **FunTester**，[腾讯云年度作者，优秀讲师 | 腾讯云+社区权威认证](https://mp.weixin.qq.com/s/oeTeJZs6h4jJJMRyUunurw)，非著名测试开发，欢迎关注。\n\n\n## [GitHub地址](https://github.com/JunManYuanLong/FunTester)\n\n联系地址：FunTester@88.com\n\n# [**570+原创文章**](/document/directory.markdown)\n# [**接口篇**](/document/api.markdown)\n# [**基础篇**](/document/base.markdown)\n# [**升级篇**](/document/update.markdown)\n# [**7788篇**](/document/7788.markdown)\n\n* 文章链接可能无法访问,各位看官移步GitHub地址即可.\n\n[FunTester测试框架架构图](http://pic.automancloud.com/structure.html)\n\n[FunTester测试项目架构图](http://pic.automancloud.com/project.html)\n\n![FunTester测试框架架构图](http://pic.automancloud.com/structure.png)\n\n![FunTester测试项目架构图](http://pic.automancloud.com/project.png)"
  },
  {
    "path": "settings.gradle",
    "content": "rootProject.name = 'funtester'"
  },
  {
    "path": "src/main/groovy/com/funtester/base/bean/AbstractBean.groovy",
    "content": "package com.funtester.base.bean\n\nimport com.alibaba.fastjson.JSON\nimport com.alibaba.fastjson.JSONObject\nimport com.funtester.config.Constant\nimport com.funtester.frame.Save\nimport com.funtester.frame.SourceCode\nimport org.apache.logging.log4j.LogManager\nimport org.apache.logging.log4j.Logger\n\n/**\n * bean的基类\n */\nabstract class AbstractBean extends Constant{\n\n    static final Logger logger = LogManager.getLogger(AbstractBean.class)\n\n    /**\n     * 将bean转化为json，为了进行数据处理和打印\n     *\n     * @return\n     */\n    JSONObject toJson() {\n        JSONObject.parseObject(JSONObject.toJSONString(this))\n    }\n\n    /**\n     * 文本形式保存\n     */\n    def save() {\n        Save.saveJson(this.toJson(), this.getClass().toString() + SourceCode.getMark());\n    }\n\n    /**\n     * 控制台打印，使用WARN记录，以便查看\n     */\n    def print() {\n        logger.warn(this.getClass().toString() + \"：\" + this.toString());\n    }\n\n    def initFrom(String str) {\n        JSONObject.parseObject(str, this.getClass())\n    }\n\n    def initFrom(Object str) {\n        initFrom(JSON.toJSONString(str))\n    }\n\n    def copyFrom(AbstractBean source) {\n        JSON.parseObject(JSON.toJSONString(source), source.class)\n    }\n\n    def copyTo(AbstractBean target) {\n        JSON.parseObject(JSON.toJSONString(this, target.class))\n    }\n\n    /**\n     * 这里bean的属性必需是可以访问的,不然会返回空json串\n     * @return\n     */\n    @Override\n    String toString() {\n        JSONObject.toJSONString(this)\n    }\n\n    @Override\n    protected Object clone() {\n        initFrom(this)\n    }\n\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/base/bean/PerformanceResultBean.groovy",
    "content": "package com.funtester.base.bean\n\nimport com.funtester.db.mysql.MySqlTest\nimport com.funtester.frame.Output\nimport com.funtester.utils.DecodeEncode\n\n/**\n * 性能测试结果集\n */\nclass PerformanceResultBean extends AbstractBean implements Serializable {\n\n    private static final long serialVersionUID = -1595942562342357L;\n\n    /**\n     * 测试用例描述\n     */\n    String mark\n\n    /**\n     * 开始时间\n     */\n    String startTime\n\n    /**\n     * 结束时间\n     */\n    String endTime\n\n    /**\n     * 表格信息\n     */\n    String table\n\n    /**\n     * 线程数\n     */\n    int threads\n\n    /**\n     * 总请求次数\n     */\n    int total\n\n    /**\n     * 平均响应时间\n     */\n    int rt\n\n    /**\n     * 吞吐量,公式为QPS=Thead/avg(time)\n     */\n    double qps\n\n    /**\n     * 通过QPS=count(r)/T公式计算得到的QPS,在固定QPS模式中,这个值来源于预设QPS\n     */\n    double qps2\n\n    /**\n     * 理论误差,两种统计模式\n     */\n    String deviation\n\n    /**\n     * 错误率\n     */\n    double errorRate\n\n    /**\n     * 失败率\n     */\n    double failRate\n\n    /**\n     * 执行总数\n     */\n    int executeTotal\n\n    PerformanceResultBean(String mark, String startTime, String endTime, int threads, int total, int rt, double qps, double qps2, double errorRate, double failRate, int executeTotal, String table) {\n        this.mark = mark\n        this.startTime = startTime\n        this.endTime = endTime\n        this.threads = threads\n        this.total = total\n        this.rt = rt\n        this.qps = qps\n        this.qps2 = qps2\n        this.errorRate = errorRate\n        this.failRate = failRate\n        this.executeTotal = executeTotal\n        this.table = DecodeEncode.zipBase64(table)\n        this.deviation = com.funtester.frame.SourceCode.getPercent(Math.abs(qps - qps2) * 100 / Math.max(qps, qps2))\n        Output.output(this.toJson())\n        Output.output(table)\n        MySqlTest.savePerformanceBean(this)\n    }\n\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/base/bean/RecordBean.groovy",
    "content": "package com.funtester.base.bean\n\nimport org.apache.logging.log4j.LogManager\nimport org.apache.logging.log4j.Logger\n\n/**\n * 测试记录的bean\n */\nclass RecordBean extends AbstractBean implements Serializable{\n\n    private static final long serialVersionUID = -159594234325649847L;\n\n    static Logger logger = LogManager.getLogger(RecordBean.class)\n\n    String domain;\n\n    String type;\n\n    String api;\n\n    long expend_time;\n\n    int data_size;\n\n    int status;\n\n    int code;\n\n    String method;\n\n    String local_ip;\n\n    String local_name;\n\n    String create_time;\n\n    static RecordBean get() {\n        new RecordBean()\n    }\n\n    RecordBean setDomain(String domain) {\n        this.domain = domain\n        this\n    }\n\n    RecordBean setType(String type) {\n        this.type = type\n        this\n    }\n\n    RecordBean setApi(String api) {\n        this.api = api\n        this\n    }\n\n    RecordBean setExpend_time(long expend_time) {\n        this.expend_time = expend_time\n        this\n    }\n\n    RecordBean setData_size(int data_size) {\n        this.data_size = data_size\n        this\n    }\n\n    RecordBean setStatus(int status) {\n        this.status = status\n        this\n    }\n\n    RecordBean setCode(int code) {\n        this.code = code\n        this\n    }\n\n    RecordBean setMethod(String method) {\n        this.method = method\n        this\n    }\n\n    RecordBean setLocal_ip(String local_ip) {\n        this.local_ip = local_ip\n        this\n    }\n\n    RecordBean setLocal_name(String local_name) {\n        this.local_name = local_name\n        this\n    }\n\n    RecordBean setCreate_time(String create_time) {\n        this.create_time = create_time\n        this\n    }\n\n    @Override\n    def print() {\n        logger.info \"接口：{}，响应时间{}\", api, expend_time\n    }\n}"
  },
  {
    "path": "src/main/groovy/com/funtester/base/bean/RequestInfo.groovy",
    "content": "package com.funtester.base.bean\n\nimport com.alibaba.fastjson.JSONObject\nimport com.funtester.base.interfaces.MarkRequest\nimport com.funtester.config.Constant\nimport com.funtester.config.RequestType\nimport com.funtester.config.SysInit\nimport org.apache.http.Header\nimport org.apache.http.HttpEntity\nimport org.apache.http.client.methods.HttpEntityEnclosingRequestBase\nimport org.apache.http.client.methods.HttpRequestBase\nimport org.apache.http.util.EntityUtils\nimport org.apache.logging.log4j.LogManager\nimport org.apache.logging.log4j.Logger\n\n/**\n * 请求信息封装类\n */\nclass RequestInfo extends AbstractBean implements Serializable {\n\n    private static final long serialVersionUID = 5942566988949859847L;\n\n    private static Logger logger = LogManager.getLogger(RequestInfo.class)\n\n    /**\n     * 请求信息的标记字段,用于日志记录请求\n     */\n    private static MarkRequest mark;\n\n    static void initMark(MarkRequest markRequest) {\n        mark = markRequest;\n    }\n\n    /**\n     * 接口地址\n     */\n    String apiName\n\n    /**\n     * 请求的url\n     */\n    String url\n\n    /**\n     * 请求的uri\n     */\n    String uri\n\n    /**\n     * 方法，get/post\n     */\n    RequestType method\n\n    /**\n     * 域名\n     */\n    String host\n\n    /**\n     * 协议类型\n     */\n    String type\n\n    /**\n     * 参数\n     */\n    String params\n\n    /**\n     * host是否是黑名单\n     */\n    boolean isBlack;\n\n    /**\n     * 所有的请求header,会去重\n     */\n    JSONObject headers\n\n    /**\n     * 存一下\n     */\n    HttpRequestBase request\n\n    /**\n     * 通过request获取请求的相关信息，并输出部分信息\n     *\n     * @param request\n     */\n    RequestInfo(HttpRequestBase request) {\n        this.request = request\n        getRequestInfo()\n    }\n\n    /**\n     * 封装获取请求的各种信息的方法\n     *\n     * @param request 传入请求对象\n     * @return 返回一个map，包含api_name,host_name,type，method，params\n     */\n    private void getRequestInfo() {\n        method = RequestType.getRequestType request.getMethod()\n        uri = request.getURI().toString()// 获取uri\n        getRequestUrl(uri)\n        String one = url.substring(url.indexOf(\"//\") + 2)// 删除掉http://\n        apiName = one.substring(one.indexOf(\"/\"))// 获取接口名\n        host = one.substring(0, one.indexOf(\"/\"))// 获取host地址\n        isBlack = SysInit.isBlack(host)\n        type = url.substring(0, url.indexOf(\"//\") - 1)// 获取协议类型\n        if (method == RequestType.GET) {\n            if (!uri.contains(UNKNOW)) return\n            params = uri.substring(uri.indexOf(UNKNOW) + 1)\n        } else if (method == RequestType.POST) {\n            getPostRequestParams(request)\n        }\n        List<Header> list = Arrays.asList(request.getAllHeaders())\n        headers = new JSONObject() {\n\n            {\n                list.each {\n                    put(it.name, it.value)\n                }\n            }\n        }\n\n    }\n\n    /**\n     * 获取请求url，遇到get请求，先截取\n     *\n     * @param uri\n     */\n    private void getRequestUrl(String uri) {\n        url = uri.contains(UNKNOW) ? uri.substring(0, uri.indexOf(UNKNOW)) : uri\n    }\n\n    /**\n     * 获取响应实体,post path,put方法适用\n     *\n     * @param request\n     */\n    private void getPostRequestParams(HttpEntityEnclosingRequestBase request) {\n        HttpEntity entity = request.getEntity()// 获取实体\n        if (entity == null) return\n        try {\n            params = EntityUtils.toString(entity)// 解析实体\n            EntityUtils.consume(entity)// 确保实体消耗\n        } catch (Exception e) {\n            logger.warn(\"获取post请求参数时异常！\")\n            params = \"entity类型：\" + entity.getClass()\n        }\n    }\n\n    boolean isBlack() {\n        isBlack\n    }\n\n    String mark() {\n        mark == null ? Constant.EMPTY : mark.mark(request)\n    }\n\n\n    @Override\n    String toString() {\n        this.toJson().toString()\n    }\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/base/bean/Result.groovy",
    "content": "package com.funtester.base.bean\n\nimport com.alibaba.fastjson.JSONObject\nimport com.funtester.base.interfaces.ReturnCode\nimport com.funtester.config.Constant\n\n/**\n * 通用的返回体\n * 配合moco框架使用\n * @param < T >\n */\nclass Result<T> extends AbstractBean implements Serializable{\n\n    private static final long serialVersionUID = -196371159847L;\n\n    /**\n     * code码\n     */\n    int code\n    /**\n     * 返回信息\n     */\n    T data\n\n    Result(int code, T data) {\n        this.code = code\n        this.data = data\n    }\n/**\n * 返回简单的响应\n * @param c\n */\n\n    Result(ReturnCode errorCode) {\n        this(errorCode.getCode(), errorCode.getDesc())\n    }\n\n    def Result() {\n    }\n/**\n * 返回成功响应内容\n * @param data\n * @return\n */\n    static <T> Result<T> success(T data) {\n        new Result(0, data)\n    }\n\n    static Result success() {\n        new Result()\n    }\n\n    static Result build(ReturnCode errorCode) {\n        new Result(errorCode)\n    }\n\n    static Result build(int code, String msg) {\n        new Result(code, msg)\n    }\n\n    static Result build(List listData) {\n        success([list: listData] as JSONObject)\n    }\n\n/**\n * 返回通用失败的响应内容\n * @param data\n * @return\n */\n    static <T> Result<T> fail(T data) {\n        new Result<T>(Constant.TEST_ERROR_CODE, data)\n    }\n\n    static Result fail() {\n        new Result(Constant.TEST_ERROR_CODE)\n    }\n\n    static Result fail(ReturnCode errorCode) {\n        new Result(errorCode)\n    }\n\n/**\n * 是否成功响应\n * @return\n */\n    boolean isSuccess() {\n        code == 0\n    }\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/base/bean/VerifyBean.groovy",
    "content": "package com.funtester.base.bean\n\nimport com.alibaba.fastjson.JSON\nimport com.funtester.base.exception.ParamException\nimport com.funtester.config.VerifyType\nimport com.funtester.utils.JsonUtil\nimport com.funtester.utils.Regex\nimport org.apache.logging.log4j.LogManager\nimport org.apache.logging.log4j.Logger\n\n/**\n * 验证对象类\n */\nclass VerifyBean extends AbstractBean implements Serializable, Cloneable {\n\n    private static Logger logger = LogManager.getLogger(VerifyBean.class)\n\n    private static final long serialVersionUID = -1595942567071153982L;\n\n    VerifyType type\n\n    /**\n     * 验证语法\n     */\n    String verify\n    /**\n     * 待验证内容\n     */\n    String value\n\n    String des\n\n    boolean isVerify\n\n    boolean result;\n\n    VerifyBean(String verify, String value, String des) {\n        this.value = value\n        this.des = des\n        def split = verify.split(REG_PART, 2)\n        this.verify = split[1]\n        this.type = VerifyType.getRequestType(split[0])\n    }\n\n    /**\n     * 用于进行自身验证,会进行isverify和result记录\n     * @return\n     */\n    boolean verify() {\n        if (isVerify && result) return result\n        isVerify = true\n        result = verify(value)\n        result\n    }\n\n    /**\n     * 用于进行对象外String验证,不会修改对象属性\n     * @param val\n     * @return\n     */\n    boolean verify(String val) {\n        boolean res\n        try {\n            switch (type) {\n                case VerifyType.CONTAIN:\n                    res = val.contains(verify)\n                    return res\n                case VerifyType.REGEX:\n                    res = Regex.isRegex(val, verify)\n                    return res\n                case VerifyType.JSONPATH:\n                    def split = verify.split(REG_PART, 2)\n                    def path = split[0]\n                    def v = split[1]\n                    def instance = JsonUtil.getInstance(JSON.parseObject(val))\n                    res = instance.getVerify(path).fit(v)\n                    return res\n                case VerifyType.HANDLE:\n                    def sp = verify.split(REG_PART, 2)\n                    def path = sp[0]\n                    def ve = sp[1]\n                    def instance = JsonUtil.getInstance(JSON.parseObject(val))\n                    res = instance.getVerify(path).fitFun(ve)\n                    return res\n                default:\n                    ParamException.fail(\"验证类型参数错误!\")\n            }\n        } catch (Exception e) {\n            logger.warn(\"验证出现问题: {}\", e.getMessage())\n            res = false\n        } finally {\n            /*这里Groovy可以这么写,但是Java不能这么写,因为需要有返回值*/\n            logger.info(\"verify对象 {} ,验证结果: {}\", verify, res)\n        }\n    }\n\n    @Override\n    def print() {\n        logger.info(\"{} 验证结果: {}\", des, result)\n    }\n\n    @Override\n    VerifyBean clone() {\n        new VerifyBean(this.verify, this.value, this.des)\n    }\n\n\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/base/constaint/CaseBase.java",
    "content": "package com.funtester.base.constaint;\n\nimport com.alibaba.fastjson.JSONObject;\nimport com.funtester.db.mysql.MySqlTest;\nimport com.funtester.frame.SourceCode;\n\n/**\n * 用例虚拟类\n */\npublic abstract class CaseBase extends SourceCode {\n\n    /**\n     * 保存测试用例的执行结果\n     *\n     * @param label  测试用例的标签\n     * @param result 测试用例结果\n     */\n    public void saveResult(String label, JSONObject result) {\n        MySqlTest.saveTestResult(label, result);\n    }\n\n    /**\n     * 前置处理\n     *\n     * @return\n     */\n    public abstract boolean before();\n\n    /**\n     * 后置处理\n     *\n     * @return\n     */\n    public abstract boolean after();\n\n    /**\n     * 初始化\n     *\n     * @return\n     */\n    public abstract boolean init();\n\n\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/base/constaint/FixedQpsThread.java",
    "content": "package com.funtester.base.constaint;\n\nimport com.funtester.base.interfaces.MarkThread;\nimport com.funtester.config.HttpClientConstant;\nimport com.funtester.frame.execute.FixedQpsConcurrent;\nimport com.funtester.utils.Time;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\npublic abstract class FixedQpsThread<F> extends ThreadBase<F> {\n\n    private static Logger logger = LogManager.getLogger(FixedQpsThread.class);\n\n    public int qps;\n\n    /**\n     * 根据属性isTimesMode判断,次数或者时间(单位ms)\n     */\n    public int limit;\n\n    public boolean isTimesMode;\n\n    public FixedQpsThread(F f, int limit, int qps, MarkThread markThread, boolean isTimesMode) {\n        this.limit = limit;\n        this.qps = qps;\n        this.mark = markThread;\n        this.f = f;\n        this.isTimesMode = isTimesMode;\n    }\n\n\n    protected FixedQpsThread() {\n        super();\n    }\n\n    @Override\n    public void run() {\n        try {\n            before();\n            threadmark = this.mark == null ? EMPTY : this.mark.mark(this);\n            long s = Time.getTimeStamp();\n            doing();\n            long e = Time.getTimeStamp();\n            FixedQpsConcurrent.executeTimes.getAndIncrement();\n            int diff = (int) (e - s);\n            FixedQpsConcurrent.allTimes.add(diff);\n            if (diff > HttpClientConstant.MAX_ACCEPT_TIME)\n                FixedQpsConcurrent.marks.add(diff + CONNECTOR + threadmark + CONNECTOR + Time.getNow());\n        } catch (Exception e) {\n            FixedQpsConcurrent.errorTimes.getAndIncrement();\n            logger.warn(\"执行任务失败！,标记:{}\", threadmark, e);\n        } finally {\n            after();\n        }\n    }\n\n    @Override\n    public void before() {\n\n    }\n\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/base/constaint/ThreadBase.java",
    "content": "package com.funtester.base.constaint;\n\nimport com.funtester.base.interfaces.MarkThread;\nimport com.funtester.frame.SourceCode;\nimport com.funtester.httpclient.FunLibrary;\n\nimport java.io.Serializable;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.concurrent.CountDownLatch;\nimport java.util.stream.Collectors;\n\n/**\n * 多线程任务基类，可单独使用\n *\n * @param <T> 必需实现Serializable\n */\npublic abstract class ThreadBase<F> extends SourceCode implements Runnable, Serializable {\n\n    private static final long serialVersionUID = -1282879464717720145L;\n\n    /**\n     * 全局的时间终止开关,true表示终止,false表示不终止.\n     */\n    private static boolean ABORT = false;\n\n    /**\n     * 线程的名字\n     */\n    public String threadName;\n\n    /**\n     * 线程标记对象,用户标记请求或者单次执行任务的\n     */\n    public String threadmark;\n\n    /**\n     * 错误数\n     * <p>这里注意使用{@link FunLibrary#getHttpResponse(org.apache.http.client.methods.HttpRequestBase)}方法获取响应的功能封装方法,即使报错也不会抛异常.这样会导致errorNum错误数为零</p>\n     */\n    public int errorNum;\n\n    /**\n     * 执行数,一般与响应时间记录数量相同\n     */\n    public int executeNum;\n\n    /**\n     * 计数锁\n     * <p>\n     * 会在concurrent类里面根据线程数自动设定\n     * </p>\n     */\n    protected CountDownLatch countDownLatch;\n\n    /**\n     * 标记对象\n     */\n    public MarkThread mark;\n\n    /**\n     * 用于设置访问资源,用于闭包中无法访问包外实例对象的情况,这里还有一个用处就是在标记线程对象的时候,用到了这个t(参数标记模式中)\n     *\n     * @since 2020年10月19日, 统一用来设置HTTPrequestbase对象.同样可以用于执行SQL和redis查询语句或者对象, 暂未使用dubbo尝试\n     */\n    public F f;\n\n    protected ThreadBase() {\n    }\n\n    /**\n     * 记录所有超时的请求标记\n     */\n    public List<String> marks = new ArrayList<>();\n\n    /**\n     * 用于存储请求耗时集合\n     * 2021年03月16日,将统计集合提取为对象属性,用于外部访问,可用于取样器实现\n     */\n    public List<Integer> costs = new ArrayList<>();\n\n    /**\n     * 运行待测方法的之前的准备\n     */\n    public void before() {\n        ABORT = false;\n    }\n\n    /**\n     * 待测方法\n     *\n     * @throws Exception 抛出异常后记录错误次数,一般在性能测试的时候重置重试控制器不再重试\n     */\n    protected abstract void doing() throws Exception;\n\n    /**\n     * 运行待测方法后的处理\n     */\n    protected void after() {\n        costs = new ArrayList<>();\n        marks = new ArrayList<>();\n        if (countDownLatch != null)\n            countDownLatch.countDown();\n    }\n\n    /**\n     * 设置计数器\n     *\n     * @param countDownLatch\n     */\n    public void setCountDownLatch(CountDownLatch countDownLatch) {\n        this.countDownLatch = countDownLatch;\n    }\n\n    /**\n     * 拷贝对象方法,用于统计单一对象多线程调用时候的请求数和成功数,对于<T>的复杂情况,需要将T类型也重写clone方法\n     *\n     * <p>\n     * 此处若具体实现类而非虚拟类建议自己写clone方法,子类重写需注意{@link ThreadBase#initBase()}方法调用\n     * </p>\n     *\n     * @return\n     */\n    @Override\n    public abstract ThreadBase clone();\n\n    /**\n     * 用于对象拷贝之后,清空存储列表\n     */\n    public void initBase() {\n        this.costs = new ArrayList<>();\n        this.marks = new ArrayList<>();\n    }\n\n    /**\n     * 线程任务是否需要提前关闭,默认返回false\n     * <p>\n     * 一般用于单线程错误率过高的情况\n     * </p>\n     *\n     * @return\n     */\n    public boolean status() {\n        return false;\n    }\n\n    /**\n     * Groovy乘法调用方法\n     *\n     * @param num\n     * @return\n     */\n    public List<ThreadBase> multiply(int num) {\n        return range(num).mapToObj(x -> this.clone()).collect(Collectors.toList());\n    }\n\n\n    /**\n     * 用于在某些情况下提前终止测试\n     */\n    public static void stop() {\n        ABORT = true;\n    }\n\n    /**\n     * true表示终止,false表示不终止.\n     *\n     * @return\n     */\n    public static boolean needAbort() {\n        return ABORT;\n    }\n\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/base/constaint/ThreadLimitTimeCount.java",
    "content": "package com.funtester.base.constaint;\n\nimport com.funtester.base.interfaces.MarkThread;\nimport com.funtester.config.HttpClientConstant;\nimport com.funtester.frame.execute.Concurrent;\nimport com.funtester.httpclient.GCThread;\nimport com.funtester.utils.Time;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * 请求时间限制的多线程类,限制每个线程执行的时间\n * <p>\n * 通常在测试某项用例固定时间的场景下使用,可以提前终止测试用例\n * </p>\n *\n * @param <T> 闭包参数传递使用,Groovy脚本会有一些兼容问题,部分对象需要tostring获取参数值\n */\npublic abstract class ThreadLimitTimeCount<F> extends ThreadBase<F> {\n\n    private static final long serialVersionUID = -7017995186493855741L;\n\n    private static final Logger logger = LogManager.getLogger(ThreadLimitTimeCount.class);\n\n    public List<String> marks = new ArrayList<>();\n\n    /**\n     * 任务请求执行时间,单位是ms秒\n     */\n    public int time;\n\n    public ThreadLimitTimeCount(F f, int time, MarkThread markThread) {\n        this.time = time * 1000;\n        this.f = f;\n        this.mark = markThread;\n    }\n\n    protected ThreadLimitTimeCount() {\n        super();\n    }\n\n    @Override\n    public void run() {\n        try {\n            before();\n            long ss = Time.getTimeStamp();\n            while (true) {\n                try {\n                    threadmark = mark == null ? EMPTY : this.mark.mark(this);\n                    long s = Time.getTimeStamp();\n                    doing();\n                    long et = Time.getTimeStamp();\n                    executeNum++;\n                    int diff =(int) (et - s);\n                    costs.add(diff);\n                    if (diff > HttpClientConstant.MAX_ACCEPT_TIME)\n                        marks.add(diff + CONNECTOR + threadmark + CONNECTOR + Time.getNow());\n                    if ((et - ss) > time || status() || ThreadBase.needAbort()) break;\n                } catch (Exception e) {\n                    logger.warn(\"执行任务失败！\", e);\n                    logger.warn(\"执行失败对象的标记:{}\", threadmark);\n                    errorNum++;\n                }\n            }\n            long ee = Time.getTimeStamp();\n            logger.info(\"线程:{},执行次数：{}, 失败次数: {},总耗时: {} s\", threadName, executeNum, errorNum, (ee - ss) / 1000.0);\n            Concurrent.allTimes.addAll(costs);\n            Concurrent.requestMark.addAll(marks);\n        } catch (Exception e) {\n            logger.warn(\"执行任务失败！\", e);\n        } finally {\n            after();\n        }\n\n    }\n\n\n\n    public boolean status() {\n        return errorNum > 10;\n    }\n\n    /**\n     * 运行待测方法的之前的准备\n     */\n    @Override\n    public void before() {\n        super.before();\n    }\n\n    @Override\n    protected void after() {\n        super.after();\n        GCThread.stop();\n    }\n\n\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/base/constaint/ThreadLimitTimesCount.java",
    "content": "package com.funtester.base.constaint;\n\nimport com.funtester.base.interfaces.MarkThread;\nimport com.funtester.config.HttpClientConstant;\nimport com.funtester.frame.execute.Concurrent;\nimport com.funtester.httpclient.GCThread;\nimport com.funtester.utils.Time;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\n/**\n * 请求时间限制的多线程类,限制每个线程执行的次数\n *\n * <p>\n * 通常在测试某项用例固定时间的场景下使用,可以提前终止测试用例\n * </p>\n *\n * @param <F> 闭包参数传递使用,Groovy脚本会有一些兼容问题,部分对象需要tostring获取参数值\n */\npublic abstract class ThreadLimitTimesCount<F> extends ThreadBase<F> {\n\n    private static final long serialVersionUID = -4617192188292407063L;\n\n    private static final Logger logger = LogManager.getLogger(ThreadLimitTimesCount.class);\n\n\n    /**\n     * 任务请求执行次数\n     */\n    public int times;\n\n    public ThreadLimitTimesCount(F f, int times, MarkThread markThread) {\n        this.times = times;\n        this.f = f;\n        this.mark = markThread;\n    }\n\n    protected ThreadLimitTimesCount() {\n        super();\n    }\n\n    @Override\n    public void run() {\n        try {\n            before();\n            long ss = Time.getTimeStamp();\n            for (int i = 0; i < times; i++) {\n                try {\n                    threadmark = mark == null ? EMPTY : this.mark.mark(this);\n                    long s = Time.getTimeStamp();\n                    doing();\n                    long e = Time.getTimeStamp();\n                    executeNum++;\n                    int diff =(int) (e - s);\n                    costs.add(diff);\n                    if (diff > HttpClientConstant.MAX_ACCEPT_TIME)\n                        marks.add(diff + CONNECTOR + threadmark + CONNECTOR + Time.getNow());\n                    if (status() || ThreadBase.needAbort()) break;\n                } catch (Exception e) {\n                    logger.warn(\"执行任务失败！\", e);\n                    logger.warn(\"执行失败对象的标记:{}\", threadmark);\n                    errorNum++;\n                }\n            }\n            long ee = Time.getTimeStamp();\n            logger.info(\"线程:{},执行次数：{}，错误次数: {},总耗时：{} s\", threadName, times, errorNum, (ee - ss) / 1000.0);\n            Concurrent.allTimes.addAll(costs);\n            Concurrent.requestMark.addAll(marks);\n        } catch (Exception e) {\n            logger.warn(\"执行任务失败！\", e);\n        } finally {\n            after();\n        }\n    }\n\n    /**\n     * 运行待测方法的之前的准备\n     */\n    @Override\n    public void before() {\n        super.before();\n    }\n\n    @Override\n    public boolean status() {\n        return errorNum > 10;\n    }\n\n\n    @Override\n    protected void after() {\n        super.after();\n        GCThread.stop();\n    }\n\n\n\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/base/exception/FailException.java",
    "content": "package com.funtester.base.exception;\n\nimport com.funtester.config.Constant;\n\n/**\n * 自定义异常基类\n */\npublic class FailException extends RuntimeException {\n\n    private static final long serialVersionUID = -7041169491254546905L;\n\n    public FailException() {\n        super(Constant.DEFAULT_STRING);\n    }\n\n    protected FailException(String message) {\n        super(message);\n    }\n\n    public static void fail(String message) {\n        throw new FailException(message);\n    }\n\n    /**\n     * 默认抛异常,多用于调试\n     */\n    public static void fail() {\n        throw new FailException();\n    }\n\n    /**\n     * 将检查异常修改为运行异常\n     *\n     * @param e\n     */\n    public static void fail(Exception e) {\n        throw new FailException(e.getMessage());\n    }\n\n\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/base/exception/LoginException.java",
    "content": "package com.funtester.base.exception;\n\nimport com.alibaba.fastjson.JSONObject;\n\n/**\n * 处理项目中的登录异常\n */\npublic class LoginException extends FailException {\n\n    private static final long serialVersionUID = 8674617502387938483L;\n\n    private LoginException() {\n        super();\n    }\n\n    private LoginException(String message) {\n        super(message);\n    }\n\n    public static void fail(String message) {\n        throw new LoginException(message);\n    }\n\n    /**\n     * 用于处理记录登录响应结果的抛异常方法\n     *\n     * @param response 登录接口响应结果\n     */\n    public static void fail(JSONObject response) {\n        fail(response.toJSONString());\n    }\n\n\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/base/exception/ParamException.java",
    "content": "package com.funtester.base.exception;\n\nimport com.alibaba.fastjson.JSONObject;\n\n/**\n * 参数错误运行异常类\n */\npublic class ParamException extends FailException {\n\n    private static final long serialVersionUID = -5079364420579956243L;\n\n    private ParamException() {\n        super();\n    }\n\n    private ParamException(String message) {\n        super(message);\n    }\n\n    public static void fail(String message) {\n        throw new ParamException(message);\n    }\n\n    public static void fail(JSONObject message) {\n        throw new ParamException(message.toJSONString());\n    }\n\n\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/base/exception/RequestException.java",
    "content": "package com.funtester.base.exception;\n\nimport org.apache.http.client.methods.HttpRequestBase;\n\n/**\n * 用于处理请求异常\n */\npublic class RequestException extends FailException {\n\n    private static final long serialVersionUID = 7916010541762451964L;\n\n    private RequestException() {\n        super();\n    }\n\n    private RequestException(HttpRequestBase request) {\n        super(request.toString());\n    }\n\n    public RequestException(String message) {\n        super(message);\n    }\n\n    public static void fail(HttpRequestBase base) {\n        throw new RequestException(base);\n    }\n\n    public static void fail(String message) {\n        throw new RequestException(message);\n    }\n\n\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/base/exception/VerifyException.java",
    "content": "package com.funtester.base.exception;\n\nimport com.alibaba.fastjson.JSONObject;\nimport com.funtester.frame.SourceCode;\nimport com.funtester.httpclient.FunRequest;\nimport org.apache.http.client.methods.HttpRequestBase;\n\n/**\n * 用于处理验证过程中的异常\n */\npublic class VerifyException extends FailException {\n\n    private static final long serialVersionUID = 7916010541762451964L;\n\n    private VerifyException() {\n        super();\n    }\n\n    private VerifyException(HttpRequestBase request) {\n        super(request.toString());\n    }\n\n    private VerifyException(String message) {\n        super(message);\n    }\n\n\n    public static void fail(String message) {\n        SourceCode.getiMessage().sendBusinessMessage();\n        throw new VerifyException(message);\n    }\n\n    public static void fail(JSONObject message) {\n        SourceCode.getiMessage().sendBusinessMessage();\n        fail(message.toJSONString());\n    }\n\n    public static void fail(HttpRequestBase request) {\n        SourceCode.getiMessage().sendBusinessMessage();\n        fail(FunRequest.initFromRequest(request).toString());\n    }\n\n\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/base/interfaces/IBase.java",
    "content": "package com.funtester.base.interfaces;\n\nimport com.alibaba.fastjson.JSONObject;\nimport com.funtester.base.bean.RequestInfo;\nimport org.apache.http.client.methods.CloseableHttpResponse;\nimport org.apache.http.client.methods.HttpGet;\nimport org.apache.http.client.methods.HttpPost;\nimport org.apache.http.client.methods.HttpRequestBase;\n\nimport java.io.File;\n\n/**\n * 每个项目需要重写的方法\n */\npublic interface IBase {\n\n    /**\n     * 获取get请求对象\n     *\n     * @param url\n     * @return\n     */\n    HttpGet getGet(String url);\n\n    /**\n     * 获取get请求对象\n     *\n     * @param url\n     * @param arg\n     * @return\n     */\n    HttpGet getGet(String url, JSONObject arg);\n\n    /**\n     * 获取post请求对象\n     *\n     * @param url\n     * @return\n     */\n    HttpPost getPost(String url);\n\n    /**\n     * 获取post请求对象\n     *\n     * @param url\n     * @param params\n     * @return\n     */\n    HttpPost getPost(String url, JSONObject params);\n\n    /**\n     * 获取post请求对象\n     *\n     * @param url\n     * @param params\n     * @param file\n     * @return\n     */\n    HttpPost getPost(String url, JSONObject params, File file);\n\n    /**\n     * 获取响应\n     *\n     * @param request\n     * @return\n     */\n    JSONObject getResponse(HttpRequestBase request);\n\n    /**\n     * 获取响应\n     *\n     * @param url\n     * @return\n     */\n    JSONObject getGetResponse(String url);\n\n    /**\n     * 获取响应\n     *\n     * @param url\n     * @param args\n     * @return\n     */\n    JSONObject getGetResponse(String url, JSONObject args);\n\n    /**\n     * 获取响应\n     *\n     * @param url\n     * @return\n     */\n    JSONObject getPostResponse(String url);\n\n    /**\n     * 获取响应\n     *\n     * @param url\n     * @param params\n     * @return\n     */\n    JSONObject getPostResponse(String url, JSONObject params);\n\n    /**\n     * 获取响应\n     *\n     * @param url\n     * @param params\n     * @param file\n     * @return\n     */\n    JSONObject getPostResponse(String url, JSONObject params, File file);\n\n    /**\n     * 校验响应正确性\n     * <p>\n     * 用于处理响应结果，一般校验json的必要层级和响应码\n     * </p>\n     *\n     * @param response\n     * @return\n     */\n    boolean isRight(JSONObject response);\n\n    /**\n     * 检查响应是否符合标准\n     * <p>\n     * 会在FunLibrary类使用，如果没有ibase对象，会默认返回test_error_code\n     * requestinfo主要用于校验该请求是否需要校验，黑名单有配置black_host提供\n     * </p>\n     *\n     * @param response    响应json\n     * @param requestInfo 请求info\n     * @return\n     */\n    int checkCode(JSONObject response, RequestInfo requestInfo);\n\n    /**\n     * 登录\n     */\n    void login();\n\n    /**\n     * 设置header\n     */\n    void setHeaders(HttpRequestBase request);\n\n    /**\n     * 处理响应结果\n     *\n     * @param response\n     */\n    void handleResponseHeader(JSONObject response);\n\n    /**\n     * 获取公共的登录参数\n     *\n     * @return\n     */\n    JSONObject getParams();\n\n    /**\n     * 初始化对象，从json数据中，一般指cookie\n     * <p>\n     * 主要用于new了新的对象之后，然后赋值的操作，场景是从另外一个服务的对象拷贝到现在的对象，区别于clone，因为可能还会涉及其他的验证，所以单独写出一个方法，极少用到\n     * </p>\n     */\n    void init(JSONObject info);\n\n\n    /**\n     * 记录请求\n     */\n    void recordRequest(HttpRequestBase base);\n\n    /**\n     * 获取请求,用于并发\n     *\n     * @return\n     */\n    HttpRequestBase getRequest();\n\n    /**\n     * 输出JSON格式的响应结果,用于统一屏蔽打印或者不打印响应内容\n     *\n     * @param response\n     */\n    public void print(JSONObject response);\n\n\n    /**\n     * 打印所有的请求header,此处功能与print响应类似,需要用一个开关控制\n     *\n     * @param request\n     */\n    public void printHeader(HttpRequestBase request);\n\n    /**\n     * 打印所有的响应header,此处功能与print响应类似,需要用一个开关控制\n     *\n     * @param response\n     */\n    public void printHeader(CloseableHttpResponse response);\n\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/base/interfaces/IMessage.java",
    "content": "package com.funtester.base.interfaces;\n\npublic interface IMessage {\n    /**\n     * 发送系统异常\n     */\n    public void sendSystemMessage();\n\n    /**\n     * 发送功能异常\n     */\n    public void sendFunctionMessage();\n\n    /**\n     * 发送业务异常\n     */\n    public void sendBusinessMessage();\n\n    /**\n     * 发送程序异常\n     */\n    public void sendCodeMessage();\n\n    /**\n     * 提醒推送\n     */\n    public void sendRemindMessage();\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/base/interfaces/IMySqlBasic.java",
    "content": "package com.funtester.base.interfaces;\n\nimport java.sql.ResultSet;\n\n/**\n * 项目数据库执行类接口\n */\npublic interface IMySqlBasic {\n    /**\n     * 执行查询sql\n     *\n     * @param sql\n     * @return\n     */\n    ResultSet executeQuerySql(String sql);\n\n    /**\n     * 执行查询sql\n     *\n     * @param database\n     * @param sql\n     * @return\n     */\n    ResultSet executeQuerySql(String database, String sql);\n\n    /**\n     * 执行修改sql\n     *\n     * @param sql\n     */\n    void executeUpdateSql(String sql);\n\n    /**\n     * 执行查询sql\n     *\n     * @param database\n     * @param sql\n     */\n    void executeUpdateSql(String database, String sql);\n\n    /**\n     * 关闭数据库连接\n     */\n    void mySqlOver();\n\n    /**\n     * 初始化数据库连接\n     *\n     * @param database\n     */\n    void getConnection(String database);\n\n    /**\n     * 初始化数据库连接\n     */\n    void getConnection();\n}"
  },
  {
    "path": "src/main/groovy/com/funtester/base/interfaces/ISocketClient.java",
    "content": "package com.funtester.base.interfaces;\n\nimport com.alibaba.fastjson.JSONObject;\nimport com.funtester.socket.ScoketIOFunClient;\nimport com.funtester.socket.WebSocketFunClient;\n\nimport java.util.List;\n\n/**\n * 对于基类base拓展Socket功能,暂时分成WebSocket和Socket.IO\n * {@link ScoketIOFunClient}\n * {@link WebSocketFunClient}\n */\npublic interface ISocketClient {\n\n    /**\n     * 连接\n     */\n    void connect();\n\n    /**\n     * 初始化\n     */\n    void init();\n\n    /**\n     * 发送消息\n     *\n     * @param mgs\n     */\n    void send(JSONObject mgs);\n\n    /**\n     * 发送消息\n     *\n     * @param mgs\n     */\n    void send(String mgs);\n\n    /**\n     * 关闭\n     */\n    void close();\n\n    /**\n     * 克隆对象,性能测试中需要\n     */\n    ISocketClient clone();\n\n    /**\n     * 是否已连接\n     *\n     * @return\n     */\n    boolean isConnect();\n\n    /**\n     * 获取记录的消息,用于验证响应,请注意需要返回副本\n     *\n     * @return\n     */\n    List<String> getMsgs();\n\n    /**\n     * 用于保存收到的信息,不同于Client的saveMsg,此方法需要将对象存储的消息全都存到long_path目录下,是否需要清空Client对象中的msgs信息,需要视情况而定.\n     */\n    void savaMsg();\n\n\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/base/interfaces/ISocketVerify.java",
    "content": "package com.funtester.base.interfaces;\n\nimport com.funtester.base.bean.VerifyBean;\n\nimport java.util.List;\n\n/**\n * Socket接口通用验证接口,暂时无用\n */\npublic interface ISocketVerify extends Runnable {\n\n    /**\n     * 初始化消息,某些场景下需要将消息转成固定对象,进行验证,如json\n     *\n     * @param msg\n     */\n    public void initMsg(List<String> msg);\n\n    /**\n     * 执行一次现有消息的全部验证,是否有匹配\n     *\n     * @return\n     */\n    public boolean verify();\n\n    /**\n     * 往验证队列中添加verify对象\n     *\n     * @param bean\n     */\n    public void addVerify(VerifyBean bean);\n\n    /**\n     * 清除verify,验证通过的verify可以从队列中清除\n     *\n     * @param bean\n     */\n    public void remoreVerify(VerifyBean bean);\n\n    /**\n     * 清除所有验证对象,通常是未验证通过,可以区分未通过和已通过\n     */\n    public void removeAllVerify();\n\n    /**\n     * 保存verify队列的测试结果\n     */\n    public void saveResult();\n\n\n}"
  },
  {
    "path": "src/main/groovy/com/funtester/base/interfaces/MarkRequest.java",
    "content": "package com.funtester.base.interfaces;\n\nimport org.apache.http.client.methods.HttpRequestBase;\n\n/**\n * 专门用来标记HTTP请求的接口\n */\npublic interface MarkRequest extends MarkThread {\n\n    /**\n     * 标记请求对象\n     *\n     * @param requestBase\n     * @return\n     */\n    public String mark(HttpRequestBase requestBase);\n\n\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/base/interfaces/MarkThread.java",
    "content": "package com.funtester.base.interfaces;\n\nimport com.funtester.base.constaint.ThreadBase;\n\n/**\n * 用来标记thread,为了记录超时的请求\n */\npublic interface MarkThread {\n\n    /**\n     * 标记线程任务\n     *\n     * @param threadBase\n     * @return\n     */\n    public String mark(ThreadBase threadBase);\n\n    public MarkThread clone();\n\n\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/base/interfaces/ReturnCode.java",
    "content": "package com.funtester.base.interfaces;\n\npublic interface ReturnCode {\n\n    int getCode();\n\n    String getDesc();\n\n\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/config/Constant.java",
    "content": "package com.funtester.config;\n\nimport com.funtester.utils.FileUtil;\nimport com.funtester.utils.Time;\nimport org.apache.http.Consts;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.io.File;\nimport java.net.InetAddress;\nimport java.net.UnknownHostException;\nimport java.nio.charset.Charset;\nimport java.util.List;\nimport java.util.Properties;\n\n/**\n * 常量类\n */\npublic class Constant {\n\n    private static Logger logger = LogManager.getLogger(Constant.class);\n\n    /*常用的常量*/\n    public static final String LINE = \"\\r\\n\";\n\n    public static final String TAB = \"\\t\";\n\n    public static final String EMPTY = \"\";\n\n    public static final String COMMA = \",\";\n\n    public static final String UNKNOW = \"?\";\n\n    public static final String OR = \"/\";\n\n    public static final String PART = \"|\";\n\n    /**\n     * 正则表达式中用到的{@link Constant#PART}\n     */\n    public static final String REG_PART = \"\\\\|\";\n\n    public static final String SPACE_1 = \" \";\n\n    public static final String CONNECTOR = \"_\";\n\n    public static final String QUOTE_DOUBLE = \"\\\"\";\n\n    public static final String QUOTE_SINGLE = \"\\'\";\n\n    private static final String[] PERCENT = {SPACE_1, \"▁\", \"▂\", \"▃\", \"▄\", \"▅\", \"▅\", \"▇\", \"█\"};\n\n    /**\n     * 此处前七处等高,第八个元素不等高,不能正常使用\n     */\n    private static final String[] PARTS = {SPACE_1, \"▏\", \"▎\", \"▍\", \"▌\", \"▋\", \"▊\", \"▉\", \"█\"};\n\n    /**\n     * 统计性能数据的分桶数\n     */\n    public static final int BUCKET_SIZE = 32;\n\n    /**\n     * 读写配置文件过滤的文本\n     */\n    public static final String FILTER = \"##\";\n\n    public static final String DEFAULT_STRING = \"FunTester\";\n\n    public static final String RESPONSE_CODE = \"code\";\n\n    public static final String RESPONSE_CONTENT = \"content\";\n\n    public static final int TEST_ERROR_CODE = -2;\n\n    public static final long DEFAULT_LONG = 0L;\n\n    public static final Properties SYSTEM_INFO = getSysInfo();\n\n    private static Properties getSysInfo() {\n        return System.getProperties();\n    }\n\n    /**\n     * 校验IP+port的正确性\n     */\n    public static final String HOST_REGEX = \"((25[0-5]|2[0-4]\\\\d|((1\\\\d{2})|([1-9]?\\\\d)))\\\\.){3}(25[0-5]|2[0-4]\\\\d|((1\\\\d{2})|([1-9]?\\\\d))):([0-9]|[1-9]\\\\d{1,3}|[1-5]\\\\d{4}|6[0-4]\\\\d{4}|65[0-4]\\\\d{2}|655[0-2]\\\\d|6553[0-5])\";\n\n    /**\n     * UTF-8字符编码格式\n     */\n    public static final Charset UTF_8 = Consts.UTF_8;\n\n    /**\n     * gb2312编码格式\n     */\n    public static final Charset GB2312 = Charset.forName(\"gb2312\");\n\n    /**\n     * Unicode编码格式\n     */\n    public static final Charset UNICODE = Charset.forName(\"Unicode\");\n\n    /**\n     * utf-16字符集\n     */\n    public static final Charset UTF_16 = Charset.forName(\"UTF-16\");\n\n    /**\n     * ISO-8859-1编码格式\n     */\n    public static final Charset ISO_8859_1 = Charset.forName(\"ISO-8859-1\");\n\n    /**\n     * GBK编码格式\n     */\n    public static final Charset GBK = Charset.forName(\"GBK\");\n\n    /**\n     * 默认字符集\n     */\n    public static Charset DEFAULT_CHARSET = UTF_8;\n\n    /**\n     * 当前工作目录\n     */\n    public static final String WORK_SPACE = new File(EMPTY).getAbsolutePath() + \"/\";\n\n    /**\n     * 测试数据存储目录\n     */\n    public static final String LONG_Path = WORK_SPACE + \"long/\";\n\n    /**\n     * 日志存存储目录\n     */\n    public static final String LOG_Path = WORK_SPACE + \"log/\";\n\n    /**\n     * request日志记录目录\n     */\n    public static final String REQUEST_Path = LONG_Path + \"request/\";\n\n    /**\n     * 标记请求地址\n     */\n    public static final String MARK_Path = LONG_Path + \"mark/\";\n\n    /**\n     * 压测数据存放地址\n     */\n    public static final String DATA_Path = LONG_Path + \"data/\";\n\n    /**\n     * 毫秒数\n     */\n    public static final long DAY = 86400000;\n\n    /**\n     * 反射方法执行用例时间间隔\n     */\n    public static final int EXECUTE_GAP_TIME = 10;\n\n    /**\n     * 本机ip，程序初始化会赋值\n     */\n    public static final String LOCAL_IP = getLocalIp();\n\n    /**\n     * 本机用户名，程序初始化会赋值\n     */\n    public static final String COMPUTER_USER_NAME = SYSTEM_INFO.getOrDefault(\"user.name\", DEFAULT_STRING).toString();\n\n    public static final String JAVA_VERSION = SYSTEM_INFO.get(\"java.version\").toString();\n\n    public static final String SYS_ENCODING = SYSTEM_INFO.get(\"file.encoding\").toString();\n\n    public static final String SYS_VERSION = SYSTEM_INFO.get(\"os.version\").toString();\n\n    public static final String SYS_NAME = SYSTEM_INFO.get(\"os.name\").toString();\n\n\n    /**\n     * 获取本机IP\n     *\n     * @return\n     */\n    public static String getLocalIp() {\n        try {\n            return InetAddress.getLocalHost().getHostAddress();\n        } catch (UnknownHostException e) {\n            logger.warn(\"获取本机IP失败！\", e);\n            return EMPTY;\n        }\n    }\n\n    /**\n     * 直接获取long目录下的文件\n     *\n     * @param fileName\n     * @return\n     */\n    public static String getLongFile(String fileName) {\n        return LONG_Path + fileName;\n    }\n\n    public static String getPercent(int i) {\n        return PERCENT[i % 9];\n    }\n\n    public static String getPart(int i) {\n        return PARTS[i % 9];\n    }\n\n    /**\n     * 创建日志文件夹和数据存储文件夹\n     */\n    static {\n        new File(LOG_Path).mkdir();\n        new File(LONG_Path).mkdir();\n        File file = new File(REQUEST_Path);\n        File mark = new File(MARK_Path);\n        File data = new File(DATA_Path);\n        file.mkdir();\n        mark.mkdir();\n        data.mkdir();\n        List<String> allFile = FileUtil.getAllFile(DATA_Path);\n        allFile.addAll(FileUtil.getAllFile(MARK_Path));\n        allFile.addAll(FileUtil.getAllFile(REQUEST_Path));\n        allFile.stream().map(y -> new File(y)).forEach(x -> {\n            if (Time.getTimeStamp() - x.lastModified() > 3 * DAY) x.delete();\n        });\n        logger.info(\"当前用户：{}，IP：{}，工作目录：{},系统编码格式:{},系统{}版本:{}\", COMPUTER_USER_NAME, LOCAL_IP, WORK_SPACE, SYS_ENCODING, SYS_NAME, SYS_VERSION);\n    }\n\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/config/EmailConstant.java",
    "content": "package com.funtester.config;\n\npublic class EmailConstant {\n\n    /**\n     * QQ邮箱发件服务器\n     */\n    public static final String QQ_HOST = \"smtp.qq.com\";\n\n    /**\n     * QQ发件邮箱\n     */\n    public static final String QQ_USERNAME = \"1009329307@qq.com\";\n\n    /**\n     * 授权码\n     */\n    public static final String QQ_PASSWORD = \"nhkmsrcucjpgbbcj\";\n\n    /**\n     * email加密传输类型\n     */\n   public static final String SSL_FACTORY = \"javax.net.ssl.SSLSocketFactory\";\n\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/config/HttpClientConstant.java",
    "content": "package com.funtester.config;\n\n\nimport org.apache.http.Header;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\n\nimport static com.funtester.config.Constant.DEFAULT_CHARSET;\nimport static com.funtester.httpclient.FunLibrary.getHeader;\n\n/**\n *\n */\npublic class HttpClientConstant {\n\n    static PropertyUtils.Property propertyUtils = PropertyUtils.getProperties(\"http\");\n\n    static String getProperty(String name) {\n        return propertyUtils.getProperty(name);\n    }\n\n    /**\n     * 默认user_agent\n     */\n    public static Header USER_AGENT = getHeader(\"User-Agent\", getProperty(\"User-Agent\"));\n\n    /**\n     * 从连接目标url最大超时 单位：毫秒\n     */\n    public static int CONNECT_REQUEST_TIMEOUT = propertyUtils.getPropertyInt(\"TIMEOUT\") * 1000;\n\n    /**\n     * 连接池中获取可用连接最大超时时间 单位：毫秒\n     */\n    public static int CONNECT_TIMEOUT = CONNECT_REQUEST_TIMEOUT;\n\n    /**\n     * 等待响应（读数据）最大超时 单位：毫秒\n     */\n    public static int SOCKET_TIMEOUT = CONNECT_REQUEST_TIMEOUT;\n\n    /**\n     * 记录\n     */\n    public static int MAX_ACCEPT_TIME = propertyUtils.getPropertyInt(\"MAX_ACCEPT_TIME\") * 1000;\n\n    /**\n     * 连接池最大连接数\n     */\n    public static int MAX_TOTAL_CONNECTION = 5000;\n\n    /**\n     * 每个路由最大连接数\n     */\n    public static int MAX_PER_ROUTE_CONNECTION = 2000;\n\n    /**\n     * 最大header数\n     */\n    public static int MAX_HEADER_COUNT = 100;\n\n    /**\n     * 消息最大长度\n     */\n    public static int MAX_LINE_LENGTH = 10000;\n\n    /**\n     * 设置的本机ip\n     */\n    public static String IP = SysInit.getRandomIP();\n\n    /**\n     * 连接header设置\n     */\n    public static Header CONNECTION = getHeader(\"Connection\", getProperty(\"Connection\"));\n\n    public static Header CLIENT_IP = getHeader(\"Client-Ip\", IP);\n\n    public static Header HTTP_X_FORWARDED_FOR = getHeader(\"HTTP_X_FORWARDED_FOR\", IP);\n\n    public static Header WL_Proxy_Client_IP = getHeader(\"WL-Proxy-Client-IP\", IP);\n\n    public static Header Proxy_Client_IP = getHeader(\"Proxy-Client-IP\", IP);\n\n    public static Header X_FORWARDED_FOR = getHeader(\"X-FORWARDED-FOR\", IP);\n\n    public static Header ContentType_JSON = getHeader(\"Content-Type\", \"application/json; charset=\" + DEFAULT_CHARSET.toString());\n\n    public static Header ContentType_FORM = getHeader(\"Content-Type\", \"application/x-www-form-urlencoded; charset=\" + DEFAULT_CHARSET.toString());\n\n    public static Header ContentType_TEXT = getHeader(\"Content-Type\", \"text/plain; charset=\" + DEFAULT_CHARSET.toString());\n\n    public static Header X_Requested_KWith = getHeader(\"X-Requested-With\", \"XMLHttpRequest\");\n\n    /**\n     * 重试次数\n     */\n    public static int TRY_TIMES = propertyUtils.getPropertyInt(\"TRY_TIMES\");\n\n    /**\n     * 关闭超时的链接\n     */\n    public static int IDLE_TIMEOUT = 5;\n\n    /**\n     * 在设置请求contenttype参数，表示请求以io流发送数据\n     */\n    public static String CONTENTTYPE_MULTIPART_FORM = \"multipart/form-data\";\n\n    /**\n     * 在设置请求contenttype参数，表示请求以文本发送数据\n     */\n    public static String CONTENTTYPE_TEXT = \"text/plain\";\n\n    /**\n     * 请求头，cookie\n     */\n    public static String COOKIE = \"cookie\";\n\n    /**\n     * SSL版本\n     */\n    public static String SSL_VERSION = getProperty(\"ssl_v\");\n\n    /**\n     * 域名黑名单\n     */\n    public static List<String> BLACK_HOSTS = new ArrayList<>();\n\n    /**\n     * 通用循环间隔时间,单位s\n     */\n    public static final int LOOP_INTERVAL = 5;\n\n    /**\n     * 线程池,线程最大空闲时间\n     */\n    public static final int THREAD_ALIVE_TIME = 3;\n\n    /**\n     * 线程池核心线程数\n     */\n    public static final int THREADPOOL_CORE = 20;\n\n    /**\n     * 线程池最大线程数\n     */\n    public static final int THREADPOOL_MAX = 500;\n\n    /**\n     * 关闭线程池最大等待时间\n     */\n    public static final int WAIT_TERMINATION_TIMEOUT = 10;\n\n    /**\n     * 添加黑名单\n     *\n     * @param host\n     */\n    public static void addBlackHost(String host) {\n        BLACK_HOSTS.add(host);\n    }\n\n    static {\n        BLACK_HOSTS.addAll(Arrays.asList(getProperty(\"black_host\").split(\",\")));\n    }\n\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/config/PropertyUtils.groovy",
    "content": "package com.funtester.config\n\nimport com.alibaba.fastjson.JSONObject\nimport com.funtester.utils.RWUtil\nimport com.funtester.frame.SourceCode\nimport org.apache.logging.log4j.LogManager\nimport org.apache.logging.log4j.Logger\n\nimport java.util.stream.Stream\n\n/**\n * 读取配置工具\n */\nclass PropertyUtils extends SourceCode {\n\n    private static Logger logger = LogManager.getLogger(PropertyUtils.class)\n\n    /**\n     * 获取指定.properties配置文件中所以的数据\n     * @param propertyName\n     *        调用方式：\n     *            1.配置文件放在resource源包下，不用加后缀\n     *              PropertiesUtil.getAllMessage(\"message\")\n     *            2.放在包里面的\n     *              PropertiesUtil.getAllMessage(\"com.test.message\")\n     * @return\n     */\n    static Property getProperties(String propertyName) {\n        logger.debug(\"读取配置文件：{}\", propertyName)\n        try {\n            new Property(ResourceBundle.getBundle(propertyName.trim()))\n        } catch (MissingResourceException e) {\n            getLocalProperties(WORK_SPACE + propertyName + \".properties\")\n        }\n    }\n\n    /**\n     * 获取指定路径下的文件配置,过滤掉{@link com.funtester.config.Constant#FILTER}\n     * @param filePath\n     * @return\n     */\n    static Property getLocalProperties(String filePath) {\n        logger.debug(\"读取配置文件：{}\", filePath)\n        try {\n            new Property(RWUtil.readTxtByJson(filePath, FILTER))\n        } catch (MissingResourceException e) {\n            logger.warn(\"找不到配置文件\", e)\n            new Property()\n        }\n    }\n\n    /**\n     * 获取当前项目下的文件配置,过滤掉{@link com.funtester.config.Constant#FILTER}\n     * @param propertyName\n     * @return\n     */\n    static Property getPropertiesByFile(String propertyName) {\n        getLocalProperties(WORK_SPACE + propertyName)\n    }\n\n    /**\n     * 配置项\n     */\n    static class Property {\n\n        Map<String, String> properties = new HashMap<>()\n\n        def Property(ResourceBundle resourceBundle) {\n            def set = resourceBundle.keySet()\n            for (def key in set) {\n                properties.put key, resourceBundle.getString(key)\n            }\n        }\n\n        def Property(JSONObject json) {\n            properties.putAll(json)\n        }\n\n        /**\n         * 获取string类型\n         * @param name\n         * @return\n         */\n        String getProperty(String name) {\n            PropertyUtils.logger.debug(\"获取配置项：{}\", name)\n            if (contain(name)) properties.get(name)\n        }\n\n        /**\n         * 获取int值\n         * @param name\n         * @return\n         */\n        int getPropertyInt(String name) {\n            changeStringToInt(properties.get(name))\n        }\n        /**\n         * 获取long值\n         * @param name\n         * @return\n         */\n        int getPropertyLong(String name) {\n            Long.valueOf(properties.get(name))\n        }\n\n        /**\n         * 获取boolean值\n         * @param name\n         * @return\n         */\n        boolean getPropertyBoolean(String name) {\n            changeStringToBoolean(properties.get(name))\n        }\n\n        /**\n         * 获取数组\n         * @param name\n         * @return\n         */\n        String[] getArrays(String name) {\n            getProperty(name).split(COMMA)\n        }\n\n        /**\n         * 获取数字类型的数组\n         * @param name\n         * @return\n         */\n        Integer[] getIntArray(String name) {\n            def split = getProperty(name).split(COMMA)\n            Stream.of(split).map { x -> x as Integer }.toArray()\n        }\n\n        /**\n         * 返回配置文件的配置项的大小\n         * @return\n         */\n        int size() {\n            properties.size()\n        }\n\n        /**\n         * 输出所以配置项\n         * @return\n         */\n        def printAll() {\n            output properties\n        }\n\n        /**\n         * 是否有配置项\n         * @param key\n         * @return\n         */\n        boolean contain(def key) {\n            boolean var = properties.containsKey key asBoolean()\n            if (!var) PropertyUtils.logger.error(\"配置{}未发现！\", key)\n            var\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/config/RequestType.java",
    "content": "package com.funtester.config;\n\n\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\n/**\n * 请求枚举类，fun备用，暂时无用,通过其他方式区分了post请求的参数格式\n */\npublic enum RequestType {\n\n    GET(\"get\"), POST(\"post\"), FUN(\"fun\");\n\n    static Logger logger = LogManager.getLogger(RequestType.class);\n\n    String name;\n\n    private RequestType(String name) {\n        this.name = name;\n    }\n\n    public static RequestType getRequestType(String name) {\n        logger.debug(\"验证请求方式：{}\", name);\n        for (RequestType requestType : RequestType.values()) {\n            if (requestType.name.equalsIgnoreCase(name)) {\n                return requestType;\n            }\n        }\n        return FUN;\n    }\n\n    /**\n     * 获取名字\n     *\n     * @return\n     */\n    public String getName() {\n        return name;\n    }\n\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/config/SocketConstant.java",
    "content": "package com.funtester.config;\n\n\n/**\n * socket测试相关的配置\n */\npublic class SocketConstant {\n\n    /* WebSocket独享配置     */\n\n    /**\n     * 最大等待次数,超过次数*时间就是连接失败\n     */\n    public static int MAX_WATI_TIMES = 3;\n\n    /* 共享配置    */\n\n    /**\n     * 默认连接间隔\n     */\n    public static int WAIT_INTERVAL = 3;\n\n    /**\n     * 默认最大的保存响应消息的数量\n     */\n    public static int MAX_MSG_SIZE = 200;\n\n    /*Socket.IO独享配置*/\n\n    /**\n     * Socket.IO独享配置\n     */\n    public static int MAX_RETRY = 3;\n\n    /**\n     * 重试延迟\n     */\n    public static int RETRY_DELAY = 1000;\n\n    /**\n     * 请求超时\n     */\n    public static int TIMEOUT = 10000;\n\n    public static String[] transports = {\"websocket\"};\n\n\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/config/SqlConstant.java",
    "content": "package com.funtester.config;\n\n\n/**\n *\n */\npublic class SqlConstant {\n\n    static PropertyUtils.Property propertyUtils = PropertyUtils.getProperties(\"mysql\");\n\n    static String getProperty(String name) {\n        return propertyUtils.getProperty(name);\n    }\n\n    /**\n     * 驱动名称\n     */\n    public static final String DRIVE = \"com.mysql.cj.jdbc.Driver\";\n\n    /**\n     * 数据库默认连接设置\n     */\n    public static final String SQLARGS = \"?useUnicode=true&characterEncoding=utf-8&useOldAliasMetadataBehavior=true\";\n\n    /**\n     * 测试数据库\n     */\n    public static final String TEST_SQL_URL = getProperty(\"test_mysql_url\") + SQLARGS;\n\n    public static final String TEST_USER = getProperty(\"user\");\n\n    public static final String TEST_PASS_WORD = getProperty(\"password\");\n\n    /**\n     * 数据库账号\n     */\n    public static final String FUN_SQL_URL = \"jdbc:mysql://ip/database\" + SQLARGS;\n\n    /**\n     * 数据库存储服务接口地址\n     */\n    public static final String MYSQL_SERVER_PATH = getProperty(\"mysql_server_path\");\n\n    /**\n     * 数据库连接重连间隔\n     */\n    public static final int MYSQL_RECONNECTION_GAP = 250;\n\n    /**\n     * 数据库存储任务每个线程最大等待数量\n     */\n    public static final int MYSQL_WORK_PER_THREAD = 30;\n\n    /**\n     * 最大等待数量，超过上限不再创建新的线程\n     */\n    public static final int MYSQL_MAX_WAIT_WORK = MYSQL_WORK_PER_THREAD * 50;\n\n    /**\n     * 获取数据库存储任务的超时时间，单位毫秒\n     */\n    public static final int MYSQLWORK_TIMEOUT = 200;\n\n    /**\n     * 默认request表名\n     */\n    public static String REQUEST_TABLE;\n\n    /**\n     * 默认result表名\n     */\n    public static String RESULT_TABLE;\n\n    /**\n     * 默认class表名\n     */\n    public static String CLASS_TABLE;\n\n    /**\n     * 默认性能测试表名\n     */\n    public static String PERFORMANCE_TABLE;\n\n    /**\n     * 默认的alertover表格\n     */\n    public static String ALERTOVER_TABLE;\n\n    /**\n     * 是否保存所有的请求到数据库\n     */\n    public static boolean flag = propertyUtils.getPropertyBoolean(\"flag\");\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/config/SysInit.java",
    "content": "package com.funtester.config;\n\nimport com.funtester.frame.SourceCode;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\n/**\n * 存放一些系统初始化的方法，可被外部调用\n */\npublic class SysInit extends SourceCode{\n\n    private static Logger logger = LogManager.getLogger(SysInit.class);\n\n    /**\n     * 是否是黑名单的host\n     * <p>先检验fv1314和本地local还有10.10.的地址，然后校验配置文件中的host name</p>\n     *\n     * @param name\n     * @return\n     */\n    public static boolean isBlack(String name) {\n        return name.contains(\"10.10\") || name.contains(\"local\") || HttpClientConstant.BLACK_HOSTS.contains(name);\n    }\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/config/VerifyType.groovy",
    "content": "package com.funtester.config;\n\nimport com.funtester.base.exception.ParamException;\nimport org.apache.commons.lang3.StringUtils\nimport org.apache.logging.log4j.LogManager\nimport org.apache.logging.log4j.Logger;\n\n/**\n * 通用验证类型,包含,正则,JsonPath,handle四项\n */\nenum VerifyType {\n\n    CONTAIN(\"contain\"), REGEX(\"regex\"), JSONPATH(\"jsonpath\"), HANDLE(\"handle\");\n\n    String vname;\n\n    VerifyType(String vname) {\n        this.vname = vname;\n    }\n\n    private static Logger logger = LogManager.getLogger(VerifyType.class);\n\n    /**\n     * 获取验证类型,不区分大小写\n     *\n     * @param name\n     * @return\n     */\n    static VerifyType getRequestType(String name) {\n        logger.debug(\"验证校验方式方式：{}\", name);\n        if (StringUtils.isEmpty(name)) ParamException.fail(\"参数不能为空!\");\n        name = name.toLowerCase();\n        switch (name) {\n            case CONTAIN.getVname():\n                return CONTAIN;\n            case REGEX.getVname():\n                return REGEX;\n            case JSONPATH.getVname():\n                return JSONPATH;\n            case HANDLE.getVname():\n                return HANDLE;\n            default:\n                ParamException.fail(name + \"参数错误!\");\n        }\n    }\n\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/db/mongodb/MongoBase.java",
    "content": "package com.funtester.db.mongodb;\n\nimport com.funtester.frame.SourceCode;\n\n/**\n * mongo操作类的基础类\n */\n@SuppressWarnings(\"all\")\npublic class MongoBase extends SourceCode {\n//\n//    /**\n//     * 获取服务地址list\n//     *\n//     * @param addresses\n//     * @return\n//     */\n//    public static List<ServerAddress> getServers(ServerAddress... addresses) {\n//        return Arrays.asList(addresses);\n//    }\n//\n//    /**\n//     * 获取服务地址\n//     *\n//     * @param host\n//     * @param port\n//     * @return\n//     */\n//    public static ServerAddress getServerAdress(String host, int port) {\n//        return new ServerAddress(host, port);\n//    }\n//\n//    /**\n//     * 获取认证list\n//     *\n//     * @param credentials\n//     * @return\n//     */\n//    public static List<MongoCredential> getCredentials(MongoCredential... credentials) {\n//        return Arrays.asList(credentials);\n//    }\n//\n//    /**\n//     * 获取验证\n//     *\n//     * @param userName\n//     * @param database\n//     * @param password\n//     * @return\n//     */\n//    public static MongoCredential getMongoCredential(String userName, String database, String password) {\n//        return MongoCredential.createCredential(userName, database, password.toCharArray());\n//    }\n//\n//    /**\n//     * 获取mongo客户端\n//     *\n//     * @param addresses\n//     * @param credentials\n//     * @return\n//     */\n//    public static MongoClient getMongoClient(List<ServerAddress> addresses, List<MongoCredential> credentials) {\n//        return new MongoClient(addresses, credentials);\n//    }\n//\n//    /**\n//     * 连接mongo数据库\n//     *\n//     * @param mongoClient\n//     * @param databaseName\n//     * @return\n//     */\n//    public static MongoDatabase getMongoDatabase(MongoClient mongoClient, String databaseName) {\n//        return mongoClient.getDatabase(databaseName);\n//    }\n//\n//    /**\n//     * 连接mongo集\n//     *\n//     * @param mongoDatabase\n//     * @param collectionName\n//     * @return\n//     */\n//    public static MongoCollection<Document> getMongoCollection(MongoDatabase mongoDatabase, String collectionName) {\n//        return mongoDatabase.getCollection(collectionName);\n//    }\n//\n//    /**\n//     * 关闭数据库连接\n//     *\n//     * @param mongoClient\n//     */\n//    public static void over(MongoClient mongoClient) {\n//        mongoClient.close();\n//    }\n//\n//    /**\n//     * 获取mongo客户端对象，通过servers和credentials对象创建\n//     *\n//     * @param mongoObject\n//     * @return\n//     */\n//    public static MongoClient getMongoClient(MongoObject mongoObject) {\n//        MongoClient mongoClient = new MongoClient(getServers(getServerAdress(mongoObject.host, mongoObject.port)), getCredentials(getMongoCredential(mongoObject.user, mongoObject.database, mongoObject.password)));\n//        return mongoClient;\n//    }\n//\n//    /**\n//     * 获取mongo客户端对象,通过uri方式连接\n//     *\n//     * @param mongoObject\n//     * @return\n//     */\n//    public static MongoClient getMongoClientOnline(MongoObject mongoObject) {\n//        String format = String.format(\"mongodb://%s:%s@%s:%d/%s\", mongoObject.user, mongoObject.password, mongoObject.host, mongoObject.port, mongoObject.database);\n//        return new MongoClient(new MongoClientURI(format));\n//    }\n//\n//    /**\n//     * 获取collection对象\n//     *\n//     * @param mongoObject\n//     * @return\n//     */\n//    public static MongoCollection<Document> getCollection(MongoObject mongoObject, String collectionName) {\n//        return getMongoClient(mongoObject).getDatabase(mongoObject.database).getCollection(collectionName);\n//    }\n//\n//    /**\n//     * 获取collection对象\n//     *\n//     * @param mongoObject\n//     * @return\n//     */\n//    public static MongoCollection<Document> getCollectionOnline(MongoObject mongoObject, String collectionName) {\n//        return getMongoClientOnline(mongoObject).getDatabase(mongoObject.database).getCollection(collectionName);\n//    }\n\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/db/mongodb/MongoObject.java",
    "content": "package com.funtester.db.mongodb;\n\n\n/**\n * mongo数据库配置对象，针对单个数据服务，单个身份验证\n */\npublic class MongoObject extends MongoBase {\n//\n//    String host;\n//\n//    int port;\n//\n//    String user;\n//\n//    String password;\n//\n//    String database;\n//\n//    MongoClient mongoClient;\n//\n//    /**\n//     * 创建测试数据连接\n//     *\n//     * @param host\n//     * @param port\n//     * @param user\n//     * @param password\n//     * @param database\n//     */\n//    public MongoObject(String host, int port, String user, String password, String database) {\n//        this.host = host;\n//        this.port = port;\n//        this.user = user;\n//        this.password = password;\n//        this.database = database;\n//        this.mongoClient = getMongoClient(this);\n//    }\n//\n//    /**\n//     * 创建线上数据库连接\n//     *\n//     * @param port\n//     * @param host\n//     * @param user\n//     * @param password\n//     * @param database\n//     */\n//    public MongoObject(int port, String host, String user, String password, String database) {\n//        this.host = host;\n//        this.port = port;\n//        this.user = user;\n//        this.password = password;\n//        this.database = database;\n//        this.mongoClient = getMongoClientOnline(this);\n//    }\n//\n//    /**\n//     * 获取colletion对象\n//     *\n//     * @param collectionName\n//     * @return\n//     */\n//    public MongoCollection<Document> getMongoCollection(String collectionName) {\n//        MongoClient mongoClientOnline = getMongoClientOnline(this);\n//        return mongoClientOnline.getDatabase(database).getCollection(collectionName);\n//    }\n//\n//\n//    /**\n//     * 关闭连接\n//     */\n//    public void over() {\n//        over(this.mongoClient);\n//    }\n//\n//    @Override\n//    public MongoObject clone() {\n//        return new MongoObject(this.host, this.port, this.user, this.password, this.database);\n//    }\n//\n//    public MongoObject clone2() {\n//        return new MongoObject(this.port, this.host, this.user, this.password, this.database);\n//    }\n\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/db/mysql/AidThread.java",
    "content": "package com.funtester.db.mysql;\n\nimport com.funtester.frame.SourceCode;\n\n/**\n * mysql辅助线程，当任务数太满的时候启用\n * <p>已经启用，单独写了基于springboot的sql存储服务</p>\n */\n@Deprecated\npublic class AidThread extends SourceCode {\n//public class AidThread extends SourceCode implements Runnable {\n\n//    private static Logger logger = LogManager.getLogger(AidThread.class);\n//\n//    @Override\n//    public void run() {\n//        MySqlObject object = new MySqlObject();\n//        MySqlObject.threadNum.incrementAndGet();\n//        while (true) {\n//            if (object.statement == null || MySqlTest.getWaitWorkNum() < SqlConstant.MYSQL_WORK_PER_THREAD) break;\n//            String sql = MySqlTest.getWork();\n//            if (sql == null) break;\n//            logger.info(\"辅助线程执行SQL：{}\", sql);\n//            object.executeUpdateSql(sql);\n//        }\n//        MySqlObject.threadNum.decrementAndGet();\n//        object.close();\n//    }\n\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/db/mysql/MySqlFun.java",
    "content": "package com.funtester.db.mysql;\n\n/**\n * mysql操作的基础类\n * <p>用于存储数据，多用于爬虫</p>\n */\n@Deprecated\npublic class MySqlFun extends SqlBase {\n//public class MySqlFun extends SqlBase implements IMySqlBasic {\n\n//    String url;\n//\n//    String database;\n//\n//    String user;\n//\n//    String password;\n//\n//    Connection connection;\n//\n//    Statement statement;\n//\n//    /**\n//     * 私有构造方法\n//     */\n//    public MySqlFun(String url, String database, String user, String password) {\n//        this.url = url;\n//        this.database = database;\n//        this.user = user;\n//        this.password = password;\n//        getConnection(database);\n//    }\n//\n//    /**\n//     * 初始化连接\n//     */\n//    @Override\n//    public void getConnection() {\n//        getConnection(EMPTY);\n//    }\n//\n//    /**\n//     * 执行sql语句，非query语句，并不关闭连接\n//     *\n//     * @param sql\n//     */\n//    @Override\n//    public void executeUpdateSql(String sql) {\n//        executeUpdateSql(EMPTY, sql);\n//    }\n//\n//    /**\n//     * 执行sql语句，非query语句，并不关闭连接\n//     *\n//     * @param database\n//     * @param sql\n//     */\n//    @Override\n//    public void executeUpdateSql(String database, String sql) {\n//        getConnection(database);\n//        SqlBase.executeUpdateSql(connection, statement, sql);\n//    }\n//\n//    /**\n//     * 查询功能\n//     *\n//     * @param sql\n//     * @return\n//     */\n//    @Override\n//    public ResultSet executeQuerySql(String sql) {\n//        return SqlBase.executeQuerySql(connection, statement, sql);\n//    }\n//\n//    @Override\n//    public ResultSet executeQuerySql(String database, String sql) {\n//        getConnection(database);\n//        return executeQuerySql(sql);\n//    }\n//\n//    /**\n//     * 关闭query连接\n//     */\n//    @Override\n//    public void mySqlOver() {\n//        SqlBase.mySqlOver(connection, statement);\n//    }\n//\n//    @Override\n//    public void getConnection(String database) {\n//        connection = SqlBase.getConnection(SqlConstant.FUN_SQL_URL.replace(\"ip\", url).replace(\"database\", database), user, password);\n//        statement = SqlBase.getStatement(connection);\n//    }\n\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/db/mysql/MySqlObject.java",
    "content": "package com.funtester.db.mysql;\n\n/**\n * 辅助线程，处理sql任务\n * <p>不再使用该方式存储数据库数据</p>\n */\n@Deprecated\npublic class MySqlObject {\n//    /**\n//     * 标记多少辅助线程存活数量\n//     */\n//    public static AtomicInteger threadNum = new AtomicInteger(0);\n//    /**\n//     * 标记连接使用\n//     */\n//    int updateTime;\n//    Connection connection;\n//    Statement statement;\n//\n//    /**\n//     * 初始化连接方法\n//     */\n//    public MySqlObject() {\n//        getConnection();\n//    }\n//\n//    /**\n//     * 获取当前辅助线程数\n//     *\n//     * @return\n//     */\n//    public static int getThreadNum() {\n//        return threadNum.get();\n//    }\n//\n//    /**\n//     * 更新连接使用标记\n//     */\n//    void updateLastUpdate() {\n//        updateTime = SourceCode.getMark();\n//    }\n//\n//    /**\n//     * 执行sql方法\n//     *\n//     * @param sql\n//     */\n//    void executeUpdateSql(String sql) {\n//        getConnection();\n//        SqlBase.executeUpdateSql(connection, statement, sql);\n//        updateLastUpdate();\n//    }\n//\n//    /**\n//     * 获取数据库连接\n//     */\n//    void getConnection() {\n//        try {\n//            if (SourceCode.getMark() - updateTime > SqlConstant.MYSQL_RECONNECTION_GAP || connection == null || connection.isClosed()) {\n//                connection = TestConnectionManage.getConnection(SqlConstant.TEST_SQL_URL, SqlConstant.TEST_USER, SqlConstant.TEST_PASS_WORD);\n//                statement = TestConnectionManage.getStatement(connection);\n//            }\n//        } catch (SQLException e) {\n//            Output.output(\"数据库连接获取失败！\", e);\n//        } finally {\n//            updateLastUpdate();\n//        }\n//    }\n//\n//    /**\n//     * 关闭对象方法\n//     */\n//    void close() {\n//        SqlBase.mySqlOver(connection, statement);\n//    }\n}"
  },
  {
    "path": "src/main/groovy/com/funtester/db/mysql/MySqlTest.java",
    "content": "package com.funtester.db.mysql;\n\nimport com.alibaba.fastjson.JSONObject;\nimport com.funtester.base.bean.PerformanceResultBean;\nimport com.funtester.base.bean.RecordBean;\nimport com.funtester.base.bean.RequestInfo;\nimport com.funtester.config.SqlConstant;\nimport org.apache.commons.lang3.StringUtils;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.sql.Connection;\nimport java.sql.Statement;\nimport java.util.concurrent.LinkedBlockingQueue;\nimport java.util.concurrent.atomic.AtomicInteger;\n\n/**\n * 数据库读写类\n * <p>\n * 用来存储接口请求信息的mysql数据库类\n * 打印请求信息的方法写在这里面，数据库服务的队列也在这里（可不用），暂时才用直接抛出sql语句完成记录功能\n * </p>\n */\npublic class MySqlTest extends SqlBase {\n\n    private static Logger logger = LogManager.getLogger(MySqlTest.class);\n\n    /**\n     * 控台statement1和statement均衡\n     */\n    static AtomicInteger key = new AtomicInteger(0);\n\n    /**\n     * 存放数据库存储任务\n     */\n    static LinkedBlockingQueue<String> sqls = new LinkedBlockingQueue<>();\n\n    public static Connection getConnection0() {\n        return connection0;\n    }\n\n    public static Statement getStatement0() {\n        return statement0;\n    }\n\n    /**\n     * 用于查询\n     */\n    static Connection connection0;\n\n    /**\n     * 用于写入\n     */\n    static Connection connection1;\n\n    /**\n     * 用于写入\n     */\n    static Connection connection2;\n\n    static Statement statement0;\n\n    static Statement statement1;\n\n    static Statement statement2;\n\n\n    /**\n     * 新方法，报错requestinfo对象\n     *\n     * @param requestInfo  请求信息\n     * @param data_size\n     * @param expend_time\n     * @param status\n     * @param mark\n     * @param code\n     * @param localIP\n     * @param computerName\n     */\n    public static void saveApiTestDate(RequestInfo requestInfo, int data_size, long expend_time, int status, int mark, int code, String localIP, String computerName) {\n        logger.info(\"请求uri：{},耗时：{} ms, {}\", requestInfo.getUri(), expend_time, requestInfo.mark());\n//        if (StringUtils.isEmpty(SqlConstant.REQUEST_TABLE) || SysInit.isBlack(requestInfo.getHost())) return;\n//        String sql = String.format(\"INSERT INTO \" + SqlConstant.REQUEST_TABLE + \" (domain,api,data_size,expend_time,status,type,method,code,local_ip,local_name,create_time) VALUES ('%s','%s',%d,%d,%d,'%s','%s',%d,'%s','%s','%s');\", requestInfo.getHost(), requestInfo.getApiName(), data_size, expend_time, status, requestInfo.getType(), requestInfo.getMethod().getName(), code, localIP, computerName, Time.getDate());\n//        RecordBean requestBean = new RecordBean();\n//        requestBean.setApi(requestInfo.getApiName());\n//        requestBean.setDomain(requestInfo.getHost());\n//        requestBean.setType(requestInfo.getType());\n//        requestBean.setExpend_time(expend_time);\n//        requestBean.setData_size(data_size);\n//        requestBean.setStatus(status);\n//        requestBean.setMethod(requestInfo.getMethod().getName());\n//        requestBean.setCode(code);\n//        requestBean.setLocal_ip(localIP);\n//        requestBean.setLocal_name(computerName);\n//        requestBean.setCreate_time(Time.getDate());\n//        RecordBean.get().setApi(requestInfo.getApiName()).setDomain(requestInfo.getHost()).setType(requestInfo.getType()).setExpend_time(expend_time).setData_size(data_size).setStatus(status).setMethod(requestInfo.getMethod().getName()).setCode(code).setLocal_ip(localIP).setLocal_name(computerName).setCreate_time(Time.getDate());\n//        sendWork(sql);\n    }\n\n    /**\n     * 保存性能测试结果的方法\n     *\n     * @param bean\n     */\n    public static void savePerformanceBean(PerformanceResultBean bean) {\n        if (!StringUtils.isNoneEmpty(SqlConstant.PERFORMANCE_TABLE)) return;\n        String sql = String.format(\"INSERT INTO \" + SqlConstant.PERFORMANCE_TABLE + \"(threads,total,rt,qps,error,fail,des,start_time,end_time) VALUES (%d,%d,%d,%f,%f,%f,'%s','%s','%s');\", bean.getThreads(), bean.getTotal(), bean.getRt(), bean.getQps(), bean.getErrorRate(), bean.getFailRate(), bean.getMark(), bean.getStartTime(), bean.getEndTime());\n        sendWork(sql);\n    }\n\n    /**\n     * 保存测试结果\n     *\n     * @param label  测试标记\n     * @param result 测试结果\n     */\n    public static void saveTestResult(String label, JSONObject result) {\n//        if (SqlConstant.RESULT_TABLE == null) return;\n//        String data = result.toString();\n//        Iterator<String> iterator = result.keySet().iterator();\n//        int abc = 1;\n//        while (iterator.hasNext() && abc == 1) {\n//            String key = iterator.next().toString();\n//            String value = result.getString(key);\n//            if (value.equals(\"false\")) abc = 2;\n//        }\n//        if (abc != 1) new AlertOver(\"用例失败！\", label + \"测试结果：\" + abc + LINE + data).sendBusinessMessage();\n//        logger.info(label + LINE + \"测试结果：\" + (abc == 1 ? \"通过\" : \"失败\") + LINE + data);\n//        String sql = String.format(\"INSERT INTO \" + SqlConstant.RESULT_TABLE + \" (result,label,params,local_ip,computer_name,create_time) VALUES (%d,'%s','%s','%s','%s','%s')\", abc, label, data, LOCAL_IP, COMPUTER_USER_NAME, Time.getDate());\n//        sendWork(sql);\n    }\n\n    /**\n     * 记录alertover警告\n     *\n     * @param requestInfo\n     * @param type\n     * @param title\n     * @param localIP\n     * @param computerName\n     */\n    public static void saveAlertOverMessage(RequestInfo requestInfo, String type, String title, String localIP, String computerName) {\n//        String host_name = requestInfo.getHost();\n//        if (SysInit.isBlack(host_name) || SqlConstant.ALERTOVER_TABLE == null) return;\n//        String sql = String.format(\"INSERT INTO alertover (type,title,host_name,api_name,local_ip,computer_name,create_time) VALUES('%s','%s','%s','%s','%s','%s','%s');\", type, title, host_name, requestInfo.getApiName(), localIP, computerName, Time.getDate());\n//        sendWork(sql);\n    }\n\n    /**\n     * 获取所有有效的用例类\n     *\n     * @return\n     */\n//    public static List<String> getAllCaseName() {\n//        List<String> list = new ArrayList<>();\n//        if (SqlConstant.CLASS_TABLE == null) return list;\n//        String sql = \"SELECT * FROM \" + SqlConstant.CLASS_TABLE + \" WHERE flag = 1 ORDER BY create_time DESC;\";\n//        TestConnectionManage.getQueryConnection();\n//        ResultSet resultSet = executeQuerySql(connection0, statement0, sql);\n//        try {\n//            while (resultSet != null && resultSet.next()) {\n//                String className = resultSet.getString(\"class\");\n//                list.add(className);\n//            }\n//        } catch (SQLException e) {\n//            logger.warn(sql, e);\n//        }\n//        return list;\n//    }\n\n    /**\n     * 获取用例状态\n     *\n     * @param name\n     * @return\n     */\n//    public static boolean getCaseStatus(String name) {\n//        if (SqlConstant.CLASS_TABLE == null) return false;\n//        String sql = \"SELECT flag FROM \" + SqlConstant.CLASS_TABLE + \" WHERE class = \\\"\" + name + \"\\\";\";\n//        TestConnectionManage.getQueryConnection();\n//        ResultSet resultSet = executeQuerySql(connection0, statement0, sql);\n//        try {\n//            if (resultSet != null && resultSet.next()) {\n//                int flag = resultSet.getInt(1);\n//                return flag == 1 ? true : false;\n//            }\n//        } catch (SQLException e) {\n//            logger.warn(sql, e);\n//        }\n//        return false;\n//    }\n\n\n    /**\n     * 确保所有的储存任务都结束\n     */\n//    private static void check() {\n//        while (sqls.size() != 0) {\n//            sleep(100);\n//        }\n//        TestConnectionManage.stopAllThread();\n//    }\n\n    /**\n     * 执行sql语句，非query语句，并不关闭连接\n     *\n     * @param sql\n     * @param key\n     */\n//    static void executeUpdateSql(String sql, boolean key) {\n//        int size = getWaitWorkNum();\n//        if (size % 3 == 1 && size > MySqlObject.getThreadNum() * (SqlConstant.MYSQL_WORK_PER_THREAD + 1) && size < SqlConstant.MYSQL_MAX_WAIT_WORK)\n//            new Thread(new AidThread()).start();\n//        if (key) {\n//            TestConnectionManage.getUpdateConnection1();\n//            executeUpdateSql(connection1, statement1, sql);\n//            TestConnectionManage.updateLastUpdate1();\n//        } else {\n//            TestConnectionManage.getUpdateConnection2();\n//            executeUpdateSql(connection2, statement2, sql);\n//            TestConnectionManage.updateLastUpdate2();\n//        }\n//    }\n\n    /**\n     * 发送数据库任务，暂时用请求服务器接口\n     *\n     * @param sql\n     * @return\n     */\n    public static void sendWork(String sql) {\n//        if (!SqlConstant.flag) return;\n//        logger.debug(\"记录SQL：{}\", sql);\n//        FunLibrary.noHeader();\n//        JSONObject argss = new JSONObject();\n//        argss.put(\"sql\", DecodeEncode.urlEncoderText(sql));\n//        FunLibrary.getHttpResponse(FunLibrary.getHttpPost(SqlConstant.MYSQL_SERVER_PATH, argss));\n    }\n\n    /**\n     * 添加请求记录\n     *\n     * @param requestBean\n     */\n    public static void sendWork(RecordBean requestBean) {\n//        FunLibrary.noHeader();\n//        if (SqlConstant.flag)\n//            FunLibrary.getHttpResponse(FunLibrary.getHttpPost(SqlConstant.MYSQL_SERVER_PATH, requestBean.toJson()));\n    }\n\n    /**\n     * 添加存储任务，数据库存储服务用\n     *\n     * @param sql\n     * @return\n     */\n    public static boolean addWork(String sql) {\n//        try {\n//            sqls.put(sql);\n//        } catch (InterruptedException e) {\n//            logger.warn(\"添加数据库存储任务失败！\", e);\n//            return false;\n//        }\n        return true;\n    }\n\n    /**\n     * 从任务池里面获取任务\n     *\n     * @return\n     */\n    static String getWork() {\n//        String sql = null;\n//        try {\n//            sql = sqls.poll(SqlConstant.MYSQLWORK_TIMEOUT, TimeUnit.MILLISECONDS);\n//        } catch (InterruptedException e) {\n//            logger.warn(\"获取存储任务失败！\", e);\n//        } finally {\n//            return sql;\n//        }\n        return EMPTY;\n    }\n\n    /**\n     * 获取等待任务数\n     *\n     * @return\n     */\n    public static int getWaitWorkNum() {\n        return sqls.size();\n    }\n\n    /**\n     * 提供外部查询功能\n     *\n     * @param sql\n     * @return\n     */\n//    public static ResultSet executeQuerySql(String sql) {\n//        TestConnectionManage.getQueryConnection();\n//        return executeQuerySql(connection0, statement0, sql);\n//    }\n\n\n    /**\n     * 关闭数据库链接的方法，供外部使用\n     */\n//    public static void mySqlOver() {\n//        mySqlQueryOver();\n//    }\n\n    /**\n     * 关闭update连接\n     */\n//    static void mySqlUpdateOver() {\n//        check();\n//        mySqlOver(connection1, statement1);\n//        mySqlOver(connection2, statement2);\n//    }\n\n    /**\n     * 关闭query连接\n     */\n//    public static void mySqlQueryOver() {\n//        mySqlOver(connection0, statement0);\n//    }\n\n\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/db/mysql/SqlBase.java",
    "content": "package com.funtester.db.mysql;\n\nimport com.funtester.frame.SourceCode;\n\n/**\n * 数据库基础类，主要公共的获取连接和操作对象\n */\npublic class SqlBase extends SourceCode {\n\n//    private static Logger logger = LogManager.getLogger(SqlBase.class);\n//\n//    /**\n//     * 获取数据库连接\n//     *\n//     * @param url      地址，包括端口\n//     * @param user     用户名\n//     * @param passowrd 密码\n//     * @return\n//     */\n//    public static Connection getConnection(String url, String user, String passowrd) {\n//        logger.debug(\"连接数据库url：{}，user：{}，password：{}\", url, user, passowrd);\n//        try {\n//            Class.forName(SqlConstant.DRIVE);\n//        } catch (ClassNotFoundException e) {\n//            logger.warn(\"加载驱动程序失败！\", e);\n//        }\n//        try {\n//            return DriverManager.getConnection(url, user, passowrd);\n//        } catch (SQLException e) {\n//            logger.warn(\"数据库连接失败！\", e);\n//        }\n//        return null;\n//    }\n//\n//    /**\n//     * 获取statement对象\n//     *\n//     * @param connection\n//     * @return\n//     */\n//    public static Statement getStatement(Connection connection) {\n//        try {\n//            return connection.createStatement();\n//        } catch (SQLException e) {\n//            logger.warn(\"获取数据库连接失败！\", e);\n//        } catch (ExceptionInInitializerError e) {\n//            logger.warn(\"初始化失败!\", e);\n//        }\n//        return null;\n//    }\n//\n//    /**\n//     * 执行sql语句,查询语句，返回ResultSet，并不关闭连接\n//     *\n//     * @param connection\n//     * @param statement\n//     * @param sql\n//     * @return\n//     */\n//    public static ResultSet executeQuerySql(Connection connection, Statement statement, String sql) {\n//        logger.debug(\"执行的SQL：{}\", sql);\n//        try {\n//            if (connection != null && !connection.isClosed()) {\n//                ResultSet resultSet = statement.executeQuery(sql);\n//                return resultSet;\n//            }\n//        } catch (SQLException e) {\n//            logger.warn(sql, e);\n//        }\n//        return null;\n//    }\n//\n//    /**\n//     * 执行sql语句，非query语句，不关闭连接\n//     *\n//     * @param connection\n//     * @param statement\n//     * @param sql\n//     */\n//    public static void executeUpdateSql(Connection connection, Statement statement, String sql) {\n//        logger.debug(\"执行的SQL：{}\", sql);\n//        try {\n//            if (!connection.isClosed()) statement.executeUpdate(sql);\n//        } catch (SQLException e) {\n//            logger.warn(sql, e);\n//        }\n//    }\n//\n//    /**\n//     * 关闭数据库资源\n//     *\n//     * @param connection\n//     * @param statement\n//     */\n//    public static void mySqlOver(Connection connection, Statement statement) {\n//        try {\n//            if (connection == null || connection.isClosed()) return;\n//            statement.close();\n//            connection.close();\n//        } catch (SQLException e) {\n//            logger.warn(\"关闭数据库链接失败！\", e);\n//        }\n//    }\n\n\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/db/mysql/TestConnectionManage.java",
    "content": "package com.funtester.db.mysql;\n\n/**\n * 测试数据存储数据库连接管理类\n * <p>放弃使用该方式存储，换成springboot数据库服务</p>\n */\n@Deprecated\npublic class TestConnectionManage extends SqlBase {\n\n//    static Logger logger = LogManager.getLogger(TestConnectionManage.class);\n//\n//    public static ExecuteThread executeThread1 = new ExecuteThread(true);\n//\n//\n//    public static ExecuteThread executeThread2 = new ExecuteThread(false);\n//\n//\n//    /**\n//     * 记录query的最后调用时间时间\n//     */\n//    private static int lastQuery;\n//\n//    /**\n//     * 记录update的最后调用时间\n//     */\n//    private static int lastUpdate1;\n//\n//    /**\n//     * 记录update的最后调用时间\n//     */\n//    private static int lastUpdate2;\n//\n//    public static void start() {\n//        getUpdateConnection1();\n//        getUpdateConnection2();\n//        executeThread1.start();\n//        executeThread2.start();\n//    }\n//\n//    static void getQueryConnection() {\n//        try {\n//            if (getMark() - lastQuery > SqlConstant.MYSQL_RECONNECTION_GAP || MySqlTest.connection0 == null || MySqlTest.connection0.isClosed())\n//                MySqlTestInitQuery();\n//        } catch (SQLException e) {\n//            logger.warn(\"数据库连接获取失败！\", e);\n//        }\n//    }\n//\n//    public static void getUpdateConnection1() {\n//        try {\n//            if (getMark() - lastUpdate1 > SqlConstant.MYSQL_RECONNECTION_GAP || MySqlTest.connection1 == null || MySqlTest.connection1.isClosed())\n//                MySqlTestInitUpdate(true);\n//        } catch (SQLException e) {\n//            logger.warn(\"数据库连接获取失败！\", e);\n//        }\n//    }\n//\n//    public static void getUpdateConnection2() {\n//        try {\n//            if (getMark() - lastUpdate2 > SqlConstant.MYSQL_RECONNECTION_GAP || MySqlTest.connection2 == null || MySqlTest.connection2.isClosed())\n//                MySqlTestInitUpdate(false);\n//        } catch (SQLException e) {\n//            logger.warn(\"数据库连接获取失败！\", e);\n//        }\n//    }\n//\n//    static void updateLastQuery() {\n//        lastQuery = getMark();\n//    }\n//\n//    static void updateLastUpdate1() {\n//        lastUpdate1 = getMark();\n//    }\n//\n//    static void updateLastUpdate2() {\n//        lastUpdate2 = getMark();\n//    }\n//\n//    /**\n//     * 连接初始化，last自动赋值\n//     */\n//    private static void MySqlTestInitQuery() {\n//        updateLastQuery();\n//        MySqlTest.mySqlQueryOver();\n//        MySqlTest.connection0 = getConnection(SqlConstant.TEST_SQL_URL, SqlConstant.TEST_USER, SqlConstant.TEST_PASS_WORD);\n//        MySqlTest.statement0 = getStatement(MySqlTest.connection0);\n//    }\n//\n//\n//    /**\n//     * 连接初始化，last自动赋值\n//     */\n//    private static void MySqlTestInitUpdate(boolean key) {\n//        if (key) {\n//            updateLastUpdate1();\n//            MySqlTest.connection1 = getConnection(SqlConstant.TEST_SQL_URL, SqlConstant.TEST_USER, SqlConstant.TEST_PASS_WORD);\n//            MySqlTest.statement1 = getStatement(MySqlTest.connection1);\n//        } else {\n//            updateLastUpdate2();\n//            MySqlTest.connection2 = getConnection(SqlConstant.TEST_SQL_URL, SqlConstant.TEST_USER, SqlConstant.TEST_PASS_WORD);\n//            MySqlTest.statement2 = getStatement(MySqlTest.connection2);\n//        }\n//    }\n//\n//\n//    /**\n//     * 结束所有sql任务线程\n//     */\n//    public static void stopAllThread() {\n//        ExecuteThread.threadKey = true;\n//    }\n//\n//}\n//\n///**\n// * 多线程类，用于消耗mysqltest里sqls中的数据库任务\n// */\n//@Deprecated\n//class ExecuteThread extends Thread {\n//\n//    /**\n//     * 分配连接\n//     */\n//    boolean key;\n//\n//    /**\n//     * 结束标志\n//     */\n//    static boolean threadKey = false;\n//\n//    ExecuteThread(boolean key) {\n//        this.key = key;\n//    }\n//\n//    @Override\n//    public void run() {\n//        while (true) {\n//            if (threadKey) break;\n//            String sql = MySqlTest.getWork();\n//            if (sql == null) continue;\n//            TestConnectionManage.logger.info(\"辅助线程执行SQL：{}\", sql);\n//            MySqlTest.executeUpdateSql(sql, key);\n//        }\n//    }\n\n}"
  },
  {
    "path": "src/main/groovy/com/funtester/db/redis/RedisPool.java",
    "content": "package com.funtester.db.redis;\n\nimport com.funtester.frame.SourceCode;\n\n/**\n * redis连接池\n */\npublic class RedisPool extends SourceCode {\n\n//    static Logger logger = LogManager.getLogger(RedisPool.class);\n//\n//    static PropertyUtils.Property property = PropertyUtils.getProperties(\"redis\");\n//\n//    private static String IP = property.getProperty(\"ip\");\n//\n//    private static int PORT = property.getPropertyInt(\"port\");\n//\n//    /**\n//     * 最大连接数\n//     */\n//    private static int MAX_TOTAL = property.getPropertyInt(\"max_total\");\n//\n//    /**\n//     * 在jedispool中最大的idle状态(空闲的)的jedis实例的个数\n//     */\n//    private static int MAX_IDLE = property.getPropertyInt(\"max_idle\");\n//\n//    /**\n//     * 在jedispool中最小的idle状态(空闲的)的jedis实例的个数\n//     */\n//    private static int MIN_IDLE = property.getPropertyInt(\"min_idle\");\n//\n//    /**\n//     * 获取实例的最大等待时间\n//     */\n//    private static long MAX_WAIT = property.getPropertyLong(\"max_wait\");\n//\n//    /**\n//     * redis连接的超时时间\n//     */\n//    private static int TIMEOUT = property.getPropertyInt(\"timeout\");\n//\n//    /**\n//     * 在borrow一个jedis实例的时候，是否要进行验证操作，如果赋值true。则得到的jedis实例肯定是可以用的\n//     */\n//    private static boolean testOnBorrow = true;\n//\n//    /**\n//     * 在return一个jedis实例的时候，是否要进行验证操作，如果赋值true。则放回jedispool的jedis实例肯定是可以用的。\n//     */\n//    private static boolean testOnReturn = true;\n//\n//    /**\n//     * 连接耗尽的时候，是否阻塞，false会抛出异常，true阻塞直到超时。默认为true\n//     */\n//    private static boolean blockWhenExhausted = true;\n//\n//    private static JedisPoolConfig config = getConfig();\n//\n//    private static JedisPool pool = initPool();\n//\n//    /**\n//     * 初始化连接池\n//     */\n//    private static JedisPool initPool() {\n//        logger.debug(\"redis连接池IP：{}，端口：{}，超时设置：{}\", IP, PORT, TIMEOUT);\n//        return new JedisPool(config, IP, PORT, TIMEOUT);\n//    }\n//\n//    /**\n//     * 默认连接池配置\n//     *\n//     * @return\n//     */\n//    private static JedisPoolConfig getConfig() {\n//        JedisPoolConfig config = new JedisPoolConfig();\n//        config.setMaxTotal(MAX_TOTAL);\n//        config.setMaxIdle(MAX_IDLE);\n//        config.setMinIdle(MIN_IDLE);\n//        config.setTestOnBorrow(testOnBorrow);\n//        config.setTestOnReturn(testOnReturn);\n//        config.setBlockWhenExhausted(blockWhenExhausted);\n//        config.setMaxWaitMillis(MAX_WAIT);\n//        logger.debug(\"连接redis配置：{}\", JSONObject.toJSONString(config));\n//        return config;\n//    }\n//\n//    /**\n//     * 获取连接池\n//     *\n//     * @return\n//     */\n//    public static JedisPool getPool() {\n//        return pool;\n//    }\n//\n//    /**\n//     * 关闭连接池资源\n//     */\n//    public static void close() {\n//        pool.close();\n//    }\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/db/redis/RedisUtil.java",
    "content": "package com.funtester.db.redis;\n\npublic class RedisUtil extends RedisPool {\n\n//    public static int getIndex() {\n//        return index;\n//    }\n//\n//    public static void setIndex(int index) {\n//        RedisUtil.index = index;\n//    }\n//\n//    /**\n//     * redis数据库索引，默认0\n//     */\n//    private static int index;\n//\n//    private static Logger logger = LogManager.getLogger(RedisUtil.class);\n//\n//    /**\n//     * 获取jedis操作对象，回收资源方法close，3.0以后废弃了其他方法，默认连接第一个数据库\n//     *\n//     * @return\n//     */\n//    public static Jedis getJedis() {\n//        return RedisPool.getPool().getResource();\n//    }\n//\n//    /**\n//     * 获取某一个database的操作连接\n//     *\n//     * @param index\n//     * @return\n//     */\n//    public static Jedis getJedis(int index) {\n//        Jedis jedis = getJedis();\n//        jedis.select(index);\n//        return jedis;\n//    }\n//\n//    /**\n//     * 设置key的有效期，单位是秒\n//     *\n//     * @param key\n//     * @param exTime\n//     * @return\n//     */\n//    public static boolean expire(String key, int exTime) {\n//        try (Jedis jedis = getJedis()) {\n//            return jedis.expire(key, exTime) == 1;\n//        } catch (Exception e) {\n//            logger.error(\"expire key:{} error\", key, e);\n//            return false;\n//        }\n//    }\n//\n//    /**\n//     * 设置key-value，过期时间\n//     *\n//     * @param key\n//     * @param value\n//     * @param expiredTime\n//     * @return\n//     */\n//    public static String set(String key, String value, int expiredTime) {\n//        try (Jedis jedis = getJedis()) {\n//            return jedis.setex(key, expiredTime, value);\n//        } catch (Exception e) {\n//            logger.error(\"setex key:{} value:{} error\", key, value, e);\n//            return EMPTY;\n//        }\n//    }\n//\n//    /**\n//     * 设置redis内容\n//     *\n//     * @param key\n//     * @param value\n//     * @return\n//     */\n//    public static String set(String key, String value) {\n//        try (Jedis jedis = getJedis()) {\n//            return jedis.set(key, value);\n//        } catch (Exception e) {\n//            logger.error(\"set key:{} value:{} error\", key, value, e);\n//            return EMPTY;\n//        }\n//    }\n//\n//    /**\n//     * 获取value\n//     *\n//     * @param key\n//     * @return\n//     */\n//    public static String get(String key) {\n//        try (Jedis jedis = getJedis()) {\n//            return jedis.get(key);\n//        } catch (Exception e) {\n//            logger.error(\"get key:{} error\", key, e);\n//            return EMPTY;\n//        }\n//    }\n//\n//    /**\n//     * 是否存在key\n//     *\n//     * @param key\n//     * @return\n//     */\n//    public static boolean exists(String key) {\n//        try (Jedis jedis = getJedis()) {\n//            return jedis.exists(key);\n//        } catch (Exception e) {\n//            logger.error(\"exists key:{} error\", key, e);\n//            return false;\n//        }\n//    }\n//\n//    /**\n//     * 删除key\n//     * jedis返回值1表示成功，0表示失败，可能是不存在的key\n//     *\n//     * @param key\n//     * @return\n//     */\n//    public static boolean del(String key) {\n//        try (Jedis jedis = getJedis()) {\n//            return jedis.del(key) == 1;\n//        } catch (Exception e) {\n//            logger.error(\"del key:{} error\", key, e);\n//            return false;\n//        }\n//    }\n//\n//    /**\n//     * 获取key对应value的类型\n//     *\n//     * @param key\n//     * @return\n//     */\n//    public static String type(String key) {\n//        try (Jedis jedis = getJedis()) {\n//            return jedis.type(key);\n//        } catch (Exception e) {\n//            logger.error(\"type key:{} error\", key, e);\n//            return EMPTY;\n//        }\n//    }\n//\n//    /**\n//     * 获取符合条件的key集合\n//     *\n//     * @param pattern\n//     * @return\n//     */\n//    public static Set<String> getKeys(String pattern) {\n//        try (Jedis jedis = getJedis()) {\n//            return jedis.keys(pattern);\n//        } catch (Exception e) {\n//            logger.error(\"type key:{} error\", pattern, e);\n//            return new HashSet<String>();\n//        }\n//    }\n//\n//    /**\n//     * 获取符合条件的key集合\n//     *\n//     * @param key\n//     * @param content\n//     * @return\n//     */\n//    public static boolean append(String key, String content) {\n//        try (Jedis jedis = getJedis()) {\n//            return jedis.append(key, content) > 0;\n//        } catch (Exception e) {\n//            logger.error(\"append key:{} ,content:{},error\", key, content, e);\n//            return false;\n//        }\n//    }\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/dubbo/DubboBase.java",
    "content": "package com.funtester.dubbo;\n\npublic class DubboBase {\n\n//    private ApplicationConfig applicationConfig = new ApplicationConfig();\n//\n//    private RegistryConfig registryConfig = new RegistryConfig();\n//\n//    private String version;\n//\n//    private String registryAddress;\n//\n//    ReferenceConfig<GenericService> referenceConfig;\n//\n//    ReferenceConfigCache configCache;\n//\n//    public DubboBase(String propertyName) {\n//        PropertyUtils.Property properties = PropertyUtils.getProperties(propertyName);\n//        this.registryAddress = properties.getProperty(\"address\");\n//        this.version = properties.getProperty(\"version\");\n//        RegistryConfig registryConfig = new RegistryConfig();\n//        registryConfig.setAddress(registryAddress);\n//        applicationConfig.setName(properties.getProperty(\"name\"));\n//    }\n//\n//    /**\n//     * 不依赖配置文件\n//     *\n//     * @param adress\n//     * @param version\n//     * @param name\n//     */\n//    public DubboBase(String adress, String version, String name) {\n//        this.registryAddress = adress;\n//        this.version = version;\n//        RegistryConfig registryConfig = new RegistryConfig();\n//        registryConfig.setAddress(registryAddress);\n//        applicationConfig.setName(name);\n//    }\n//\n//    /**\n//     * ReferenceConfig实例很重，封装了与注册中心的连接以及与提供者的连接，\n//     * 需要缓存，否则重复生成ReferenceConfig可能造成性能问题并且会有内存和连接泄漏。\n//     * API方式编程时，容易忽略此问题。\n//     * 这里使用dubbo内置的简单缓存工具类进行缓存\n//     *\n//     * @param interfaceClass\n//     * @return\n//     */\n//    public GenericService getGenericService(String interfaceClass) {\n//        if (referenceConfig == null) {\n//            referenceConfig = new ReferenceConfig<GenericService>();\n//            referenceConfig.setApplication(applicationConfig);\n//            referenceConfig.setRegistry(registryConfig);\n//            referenceConfig.setVersion(version);\n//            // 弱类型接口名\n//            referenceConfig.setInterface(interfaceClass);\n//            // 声明为泛化接口\n//            referenceConfig.setGeneric(true);\n//        }\n//        configCache = ReferenceConfigCache.getCache(StringUtil.getChinese(5));\n//        return configCache.get(referenceConfig);\n//    }\n//\n//    /**\n//     * 释放资源\n//     */\n//    public void over() {\n//        if (null != configCache) configCache.destroy(referenceConfig);\n//    }\n//\n\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/dubbo/DubboInvokeParams.groovy",
    "content": "package com.funtester.dubbo\n\n\nclass DubboInvokeParams {\n//\n//    int length\n//\n//    String[] types = new String[length]\n//\n//    Object[] values = new Object[length]\n//\n//    DubboInvokeParams(int length) {\n//        this.length = length;\n//    }\n\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/dubbo/DubboParamBase.groovy",
    "content": "package com.funtester.dubbo\n\nclass DubboParamBase {\n//\n//    String type\n//\n//    Object value\n//\n//    DubboParamBase(Class type, Object value) {\n//        this.type = type.getTypeName()\n//        this.value = value\n//    }\n\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/dubbo/DubboUtil.java",
    "content": "package com.funtester.dubbo;\n\nimport com.funtester.frame.SourceCode;\n\n\n/**\n * dubbo泛化调用的方法\n */\npublic class DubboUtil extends SourceCode {\n//\n//    public static Logger logger = LogManager.getLogger(DubboUtil.class);\n//\n//    public static DubboInvokeParams initDubboInvokeParams(DubboParamBase... params) {\n//        DubboInvokeParams invokeParams = new DubboInvokeParams(params.length);\n//        range(invokeParams.getLength()).forEach(x ->\n//                {\n//                    DubboParamBase param = params[x];\n//                    invokeParams.getTypes()[x] = param.getType();\n//                    invokeParams.getValues()[x] = param.getValue();\n//                }\n//        );\n//        return invokeParams;\n//    }\n//\n//    public static Object getResponse(GenericService genericService, String methodName, DubboInvokeParams params) {\n//        return genericService.$invoke(methodName, params.getTypes(), params.getValues());\n//    }\n//\n//    public static void main(String[] args) {\n//        Map<String, Object> getDataRequest = new HashMap<String, Object>();\n//        getDataRequest.put(\"orgId\", \"119\");\n//        getDataRequest.put(\"orgType\", \"2\");\n//        getDataRequest.put(\"reqId\", \"123456789\");\n//        DubboInvokeParams dubboInvokeParams = initDubboInvokeParams(new DubboParamBase(Object.class, getDataRequest));\n//        DubboBase dubbo = new DubboBase(\"dubbo\");\n//        GenericService genericService = dubbo.getGenericService(\"com.noriental.usersvr.service.okuser.SchoolYearService\");\n//\n//        Object response = getResponse(genericService, \"findFutureYear\", dubboInvokeParams);\n//        output(response.toString());\n//\n//    }\n\n\n}"
  },
  {
    "path": "src/main/groovy/com/funtester/frame/JsonVerify.groovy",
    "content": "package com.funtester.frame\n\nimport com.alibaba.fastjson.JSON\nimport com.alibaba.fastjson.JSONArray\nimport com.alibaba.fastjson.JSONException\nimport com.alibaba.fastjson.JSONObject\nimport com.funtester.base.exception.ParamException\nimport com.funtester.utils.JsonUtil\nimport org.apache.logging.log4j.LogManager\nimport org.apache.logging.log4j.Logger\n\n/**\n * 操作符重写类,用于匹配JSonpath验证语法,基本重载的方法以及各种比较方法,每个方法重载三次,参数为double,String,verify\n * 数字统一采用double类型,无法操作的String对象的方法返回empty\n * 操作符现在支持['>', '<', '=']三种,暂无增加计划\n */\nclass JsonVerify extends SourceCode implements Comparable {\n\n    private static Logger logger = LogManager.getLogger(JsonVerify.class)\n\n    /**\n     * 验证文本\n     */\n    String extra\n\n    /**\n     * 验证数字格式\n     */\n    double num\n\n    /**\n     * 构造方法,暂时写着,尽量使用jsonutil创造verify对象\n     *\n     * @param json\n     * @param path\n     */\n    private JsonVerify(JSONObject json, String path) {\n        this(JsonUtil.getInstance(json).getString(path))\n        if (isNumber()) num = changeStringToDouble(extra)\n    }\n\n    private JsonVerify(String value) {\n        extra = value\n        if (isNumber()) num = changeStringToDouble(extra)\n    }\n\n    /**\n     * 获取实例方法\n     * @param json\n     * @param path\n     * @return\n     */\n    static JsonVerify getInstance(JSONObject json, String path) {\n        new JsonVerify(json, path)\n    }\n\n    static JsonVerify getInstance(String str) {\n        new JsonVerify(str)\n    }\n\n    /**\n     * 加法重载\n     * @param i\n     * @return\n     */\n    def plus(double i) {\n        isNumber() ? num + i : extra + i.toString()\n    }\n\n    /**\n     * 加法重载,string类型\n     * @param s\n     * @return\n     */\n    def plus(String s) {\n        isNumber() && isNumber(s) ? num + changeStringToDouble(s) : extra + s\n    }\n\n    /**\n     * 加法重载,verify类型\n     * @param s\n     * @return\n     */\n    def plus(JsonVerify v) {\n        isNumber() && v.isNumber() ? this + (v.num) : extra + v.extra\n    }\n\n    /**\n     * 减法重载\n     * @param i\n     * @return\n     */\n    def minus(double i) {\n        isNumber() ? num - i : extra - i.toString()\n    }\n\n    /**\n     * 减法重载,string类型\n     * @param s\n     * @return\n     */\n    def minus(String s) {\n        extra - s\n    }\n    /**\n     * 减法重载\n     * @param v\n     * @return\n     */\n    def minus(JsonVerify v) {\n        if (isNumber() && v.isNumber()) this - v.num\n        else extra - v.extra\n    }\n\n    /**\n     * extra * i 这里会去强转double为int,调用intvalue()方法\n     * @param i\n     * @return\n     */\n    def multiply(double i) {\n        if (isNumber()) num * i\n        else extra * i\n    }\n\n    def multiply(String s) {\n        isNumber() ? isNumber(s) ? num * changeStringToDouble(s) : s * num : isNumber(s) ? extra * changeStringToDouble(s) : EMPTY\n    }\n\n    def multiply(JsonVerify v) {\n        this * v.extra\n    }\n\n    /**\n     * 除法重载\n     * @param i\n     * @return\n     */\n    def div(int i) {\n        if (isNumber()) num / i\n    }\n\n    def div(String s) {\n        if (isNumber() && isNumber(s)) num / changeStringToDouble(s)\n    }\n\n    def div(JsonVerify v) {\n        if (isNumber() && v.isNumber()) num / v.num\n    }\n\n    def mod(int i) {\n        if (isNumber()) (int) (num % i * 10000) * 1.0 / 10000\n    }\n\n    /**\n     * 直接取值,用于数组类型\n     * @param i\n     * @return\n     */\n    def getAt(int i) {\n        try {\n            JSONArray.parseArray(extra)[i]\n        } catch (JSONException e) {\n            i >= extra.length() ? EMPTY : extra[i]\n        }\n    }\n\n    /**\n     * 直接取值,用户json类型\n     * @param i\n     * @return\n     */\n    def getAt(String s) {\n        try {\n            JSON.parseObject(extra)[s]\n        } catch (JSONException e) {\n            isNumber(s) ? extra.charAt(changeStringToInt(s)) : null\n        }\n    }\n\n    /**\n     * if (a implements Comparable) { a.compareTo(b) == 0 } else { a.equals(b) }* @param a\n     * @return\n     */\n    boolean equals(JsonVerify verify) {\n        extra == verify.extra\n    }\n\n    boolean equals(Number n) {\n        num.toString() == n.toString()\n    }\n\n    /**\n     * 此方法存在缺陷,在其他项目引入jar时,调用==会直接调用Java的\n     * @param s\n     * @return\n     */\n    boolean equals(String s) {\n        extra == s\n    }\n\n    @Override\n    boolean equals(Object o) {\n        this == o.toString()\n    }\n\n    /**\n     * a <=> b  a.compareTo(b)\n     * a>b      a.compareTo(b) > 0\n     * a>=b     a.compareTo(b) >= 0\n     * a<b      a.compareTo(b) < 0\n     * a<=b     a.compareTo(b) <= 0\n     * @param o\n     * @return\n     */\n    @Override\n    int compareTo(Object o) {\n        if (isNumber() && (o instanceof Number || isNumber(o.toString()))) {\n            return num.compareTo(o.toString() as Double)\n        } else {\n            extra.length().compareTo(o.toString().length())\n        }\n    }\n\n    /**\n     * 类型转换,用于as关键字\n     * @param tClass\n     * @return\n     */\n    def <T> T asType(Class<T> tClass) {\n        logger.debug(\"强转类型:{}\", tClass.toString())\n        if (tClass == Integer) num.intValue()\n        else if (tClass == Double) num\n        else if (tClass == Long) num.longValue()\n        else if (tClass == String) extra\n        else if (tClass == JsonVerify) new JsonVerify(extra)\n        else if (tClass == Boolean) changeStringToBoolean(extra)\n    }\n\n    /**\n     * 用户正则匹配\n     * @param regex\n     * @return\n     */\n    def regex(String regex) {\n        extra ==~ regex\n    }\n\n    /**\n     * 是否是数字\n     * @return\n     */\n    def isNumber() {\n        isNumber(extra)\n    }\n\n    /**\n     * 是否为boolean类型\n     * @return\n     */\n    def isBoolean() {\n        extra ==~ (\"false|true\")\n    }\n\n    @Override\n    String toString() {\n        extra\n    }\n\n\n    /**\n     * 使用与switch-case方法判断\n     * @param o\n     * @return\n     */\n    boolean isCase(Object o) {\n        this == o\n    }\n\n    /**\n     * 判断是否符合期望\n     * @param str\n     * @return\n     */\n    boolean fit(String str) {\n        logger.info(\"verify对象: {},匹配的字符串: {}\", extra, str)\n        OPS o = OPS.getInstance(str.charAt(0))\n        def res = str.substring(1)\n        switch (o) {\n            case OPS.GREATER:\n                return this > res\n            case OPS.LESS:\n                return this < res\n            case OPS.EQUAL:\n                return extra == res\n            case OPS.REGEX:\n                return extra ==~ res\n            default:\n                ParamException.fail(\"判断字符串参数错误!\")\n        }\n    }\n\n    /**\n     * 判断是否符合操作后期望,通过{@link com.funtester.config.Constant#REG_PART}分隔\n     * @param str\n     * @return\n     */\n    boolean fitFun(String str) {\n        def split = str.split(REG_PART, 2)\n        def handle = split[0]\n        def ops = split[1]\n        HPS h = HPS.getInstance(handle.charAt(0))\n        def hr = handle.substring(1)\n        switch (h) {\n            case HPS.PLUS:\n                def n = getInstance((this + hr) as String)\n                return n.fit(ops)\n            case HPS.MINUS:\n                def n = getInstance((this - hr) as String)\n                return n.fit(ops)\n            case HPS.MUL:\n                def n = getInstance((this * hr) as String)\n                return n.fit(ops)\n            case HPS.DIV:\n                def n = getInstance((this / hr) as String)\n                return n.fit(ops)\n            default:\n                ParamException.fail(\"运算操作字符串参数错误!\")\n        }\n\n    }\n\n    /**\n     * 支持的判断类型的操作符枚举类\n     */\n    static enum OPS {\n\n        GREATER((char) '>'), LESS((char) '<'), EQUAL((char) '='), REGEX((char) '~')\n\n        char name\n\n        OPS(char name) {\n            this.name = name\n        }\n\n        static OPS getInstance(char c) {\n            switch (c) {\n                case GREATER.getName():\n                    return GREATER;\n                case LESS.getName():\n                    return LESS;\n                case EQUAL.getName():\n                    return EQUAL;\n                case REGEX.getName():\n                    return REGEX\n                default:\n                    ParamException.fail(\"判断操作符参数错误!\")\n            }\n        }\n    }\n\n    /**\n     * 支持的运算类型的操作符枚举类\n     */\n    static enum HPS {\n\n        PLUS((char) '+'), MINUS((char) '-'), MUL((char) '*'), DIV((char) '/')\n\n        char name\n\n        HPS(char name) {\n            this.name = name\n        }\n\n        static HPS getInstance(char c) {\n            switch (c) {\n                case PLUS.getName():\n                    return PLUS\n                case MINUS.getName():\n                    return MINUS\n                case MUL.getName():\n                    return MUL\n                case DIV.getName():\n                    return DIV\n                default:\n                    ParamException.fail(\"运算操作符参数错误!\")\n            }\n        }\n    }\n\n\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/frame/Output.java",
    "content": "package com.funtester.frame;\n\nimport com.alibaba.fastjson.JSON;\nimport com.alibaba.fastjson.JSONArray;\nimport com.alibaba.fastjson.JSONException;\nimport com.alibaba.fastjson.JSONObject;\nimport com.funtester.base.bean.AbstractBean;\nimport com.funtester.config.Constant;\nimport com.funtester.utils.StringUtil;\nimport org.apache.commons.lang3.ArrayUtils;\nimport org.apache.commons.lang3.StringUtils;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.util.*;\nimport java.util.stream.Collectors;\nimport java.util.stream.IntStream;\n\n@SuppressWarnings(\"all\")\npublic class Output extends Constant {\n\n    private static Logger logger = LogManager.getLogger(Output.class);\n\n    private static final String UP = \"~☢~~☢~~☢~~☢~~☢~~☢~~☢~~☢~~☢~~☢~\";\n\n    private static final String DOWN = \"~☢~~☢~~☢~~☢~~☢~~☢~~☢~~☢~~☢~~☢~\";\n\n    public static void output(AbstractBean bean) {\n        output(bean.toJson());\n    }\n\n    /**\n     * 输出，自带log方法,排除root用户使用输出\n     *\n     * @param text\n     */\n    public static void output(String text) {\n        logger.info(text);\n    }\n\n    /**\n     * 输出，针对各种不同情况做兼容\n     * <p>\n     * 在处理两个对象，默认情况第一个是说明文字，第二个是list内容\n     * </p>\n     *\n     * @param object\n     */\n    public static void output(Object... object) {\n        if (ArrayUtils.isEmpty(object)) {\n            logger.warn(\"怎么空了呢！\");\n        } else if (object.length == 1) {\n            if (object[0] instanceof List) {\n                output((List) object[0]);\n            } else {\n                output(object[0].toString());\n            }\n        } else if (object.length == 2) {\n            output(object[0]);\n            if (object[1] instanceof List) {\n                output((List) object[1]);\n            } else {\n                output(object[1]);\n            }\n        } else if (object.getClass().isArray()) {\n            output(Arrays.asList(object));\n        }\n    }\n\n    public static void output(List list) {\n        list.forEach(x -> output(\"第\" + (list.indexOf(x) + 1) + \"个：\" + x.toString()));\n    }\n\n    public static void output(Iterator its) {\n        its.forEachRemaining(x -> output(x.toString()));\n    }\n\n    /**\n     * 输出无序集合\n     *\n     * @param its\n     */\n    public static void output(Iterable its) {\n        its.forEach(x -> output(x.toString()));\n    }\n\n    public static void output(Map map) {\n        if (map == null || map.size() == 0) {\n            logger.warn(\"怎么空了呢！\");\n        } else {\n            show(map);\n        }\n    }\n\n    /**\n     * 输出json数组\n     *\n     * @param jsonArray\n     */\n    public static void output(JSONArray jsonArray) {\n        if (jsonArray == null || jsonArray.isEmpty()) {\n            output(\"jsonarray对象为空!\");\n            return;\n        }\n        jsonArray.forEach(x -> {\n            try {\n                output(JSON.parseObject(x.toString()));\n            } catch (JSONException e) {\n                output(x);\n            }\n        });\n    }\n\n    /**\n     * 输出数组\n     *\n     * @param arrays\n     */\n    public static <T extends Number> void output(T[] nums) {\n        if (ArrayUtils.isEmpty(nums)) return;\n        Arrays.asList(nums).forEach(x -> output(x));\n    }\n\n    /**\n     * 泛型做输出数字对象\n     *\n     * @param x\n     * @param <T>\n     */\n    public static <T extends Number> void output(T x) {\n        output(x.toString());\n    }\n\n    public static void output(Object o) {\n        if (o == null) logger.warn(\"怎么空了呢！\");\n        else output(o.toString());\n    }\n\n    public static void output(int[] nums) {\n        output(ArrayUtils.toObject(nums));\n    }\n\n    public static void output(long[] nums) {\n        output(ArrayUtils.toObject(nums));\n    }\n\n    public static void output(double[] nums) {\n        output(ArrayUtils.toObject(nums));\n    }\n\n    /**\n     * 输出json\n     *\n     * @param jsonObject json格式响应实体\n     */\n    public static JSONObject output(JSONObject jsonObject) {\n        if (jsonObject == null || jsonObject.size() == 0) {\n            output(\"json 对象是空的！\");\n            return jsonObject;\n        }\n        String jsonStr = jsonObject.toString();// 先将json对象转化为string对象\n        jsonStr = jsonStr.replaceAll(\"\\\\\\\\/\", OR);\n        int level = 0;// 用户标记层级\n        StringBuffer jsonResultStr = new StringBuffer(\"＞  \");// 新建stringbuffer对象，用户接收转化好的string字符串\n        int length = jsonStr.length();\n        for (int i = 0; i < length; i++) {// 循环遍历每一个字符\n            char piece = jsonStr.charAt(i);// 获取当前字符\n            // 如果上一个字符是断行，则在本行开始按照level数值添加标记符，排除第一行\n            if ('\\n' == jsonResultStr.charAt(jsonResultStr.length() - 1)) {\n                jsonResultStr.append(StringUtil.getSerialEmoji(level) + \" . \");\n                IntStream.range(0, level - 1).forEach(x -> jsonResultStr.append(\". . \"));//没有采用sourcecode的getmanystring\n            }\n            char last = i == 0 ? '{' : jsonStr.charAt(i - 1);\n            char next = i < length - 1 ? jsonStr.charAt(i + 1) : '}';\n            switch (piece) {\n                case ',':\n                    // 如果是“,”，则断行\n                    jsonResultStr.append(piece + ((\"\\\"0123456789le]}\".contains(last + EMPTY) && \"\\\"[{\".contains(next + EMPTY)) ? LINE : EMPTY));\n                    break;\n                case '{':\n                case '[':\n                    // 如果字符是{或者[，则断行，level加1\n                    jsonResultStr.append(piece + (\":[{,\".contains(last + EMPTY) && \",[{}]\\\"0123456789le\".contains(next + EMPTY) ? LINE : EMPTY));\n                    if (last != '[') level++;//解决jsonarray:[{\n                    break;\n                case '}':\n                case ']':\n                    // 如果是}或者]，则断行，level减1\n//                    jsonResultStr.append(LINE);\n                    jsonResultStr.append((\"\\\"0123456789le]}{[,\".contains(last + EMPTY) && \"}],\".contains(next + EMPTY) ? LINE : EMPTY));\n                    if (next != ']') level--;//解决jsonarray:[{\n                    jsonResultStr.append(level == 0 ? \"\" : StringUtil.getSerialEmoji(level) + \" . \");\n                    IntStream.range(0, level - 1).forEach(x -> jsonResultStr.append(\". . \"));//没有采用sourcecode的getmanystring\n                    jsonResultStr.append(piece);\n                    break;\n                default:\n                    jsonResultStr.append(piece);\n                    break;\n            }\n        }\n        output(LINE + UP + \" JSON \" + UP + LINE + jsonResultStr.toString().replaceAll(LINE, LINE + \"＞  \") + LINE + DOWN + \" JSON \" + DOWN);\n        return jsonObject;\n    }\n\n    public static void show(Map map) {\n        new ConsoleTable(map);\n    }\n\n    public static void show(List<List<String>> rows) {\n        new ConsoleTable(rows);\n    }\n\n    /**\n     * 打印可能的json数据\n     *\n     * @param content\n     */\n    public static void showStr(String content) {\n        try {\n            if (content.contains(\"&\")) output(SourceCode.getJson(content.split(\"&\")));\n            else output(JSONObject.parseObject(content));\n        } catch (JSONException e) {\n            output(content);\n        }\n    }\n\n    static class ConsoleTable extends SourceCode {\n\n        List<Integer> rowLength = new ArrayList<>();\n\n        public static void show(Map map) {\n            new ConsoleTable(map);\n        }\n\n        /**\n         * 输出map\n         *\n         * @param map\n         */\n        private ConsoleTable(Map map) {\n            Set set = map.keySet();\n            int asInt0 = set.stream().mapToInt(key -> key.toString().length()).max().getAsInt();\n            rowLength.add(asInt0 + 2);\n            List<String> values = new ArrayList<>();\n            set.forEach(key -> values.add(map.get(key).toString()));\n            int asInt1 = values.stream().mapToInt(value -> value.length()).max().getAsInt();\n            rowLength.add(asInt1 + 2);\n            StringBuffer stringBuffer = new StringBuffer(LINE + getHeader());\n            map.forEach((k, v) -> {\n                stringBuffer.append(getCel(0, k.toString()));\n                stringBuffer.append(getCel(1, v.toString()));\n            });\n            output(stringBuffer.append(LINE + getHeader()).toString());\n        }\n\n        /**\n         * 输出list\n         *\n         * @param rows\n         */\n        private ConsoleTable(List<List<String>> rows) {\n            for (int i = 0; i < rows.size(); i++) {\n                List<String> line = rows.get(i);\n                for (int j = 0; j < line.size(); j++) {\n                    String s = line.get(j);\n                    if (rowLength.size() <= j) rowLength.add(0);\n                    if (rowLength.get(j) < s.length()) rowLength.set(j, s.length());\n                }\n            }\n            rowLength = rowLength.stream().map(n -> n + 2).collect(Collectors.toList());\n            StringBuffer stringBuffer = new StringBuffer(LINE + getHeader());\n            for (int i = 0; i < rows.size(); i++) {\n                List<String> line = rows.get(i);\n                for (int j = 0; j < rowLength.size(); j++) {\n                    stringBuffer.append(getCel(j, j < line.size() ? line.get(j) : EMPTY));\n                }\n            }\n            output(stringBuffer.append(LINE + getHeader()).toString());\n        }\n\n\n        /**\n         * 获取每一格的string\n         *\n         * @param colum   列\n         * @param content 格内容\n         * @return\n         */\n        public String getCel(int colum, String content) {\n            return (colum == 0 ? LINE + PART : PART) + StringUtil.center(content, rowLength.get(colum)) + (rowLength.size() - colum == 1 ? PART : EMPTY);\n        }\n\n        /**\n         * 获取头尾行\n         *\n         * @return\n         */\n        private String getHeader() {\n            List<String> collect = rowLength.stream().map(size -> getManyString(\"-\", size)).collect(Collectors.toList());\n            return \"+\" + StringUtils.join(collect.toArray(), \"+\") + \"+\";\n        }\n\n\n    }\n\n\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/frame/ResponseVerify.java",
    "content": "package com.funtester.frame;\n\nimport com.alibaba.fastjson.JSONObject;\nimport com.funtester.httpclient.FunLibrary;\nimport com.funtester.utils.Regex;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\n\n/**\n * 通用验证方法封装\n */\npublic class ResponseVerify extends SourceCode {\n\n    private static Logger logger = LogManager.getLogger(ResponseVerify.class);\n\n    /**\n     * 断言的json对象\n     */\n    private JSONObject verifyJson;\n\n    /**\n     * 断言的code码\n     */\n    private int code;\n\n    /**\n     * 获取所有lines\n     *\n     * @return\n     */\n    public List<String> getLines() {\n        return lines;\n    }\n\n    /**\n     * 断言的json对象分行解析\n     */\n    private List<String> lines = new ArrayList<>();\n\n    public ResponseVerify(JSONObject jsonObject) {\n        this.verifyJson = jsonObject;\n        this.lines = parseJsonLines(jsonObject);\n    }\n\n    /**\n     * 获取 code\n     * <p>这里的requestinfo主要的目的是为了拦截一些不必要的checkcode验证的，主要有black_host名单提供，在使用时，注意requestinfo的非空校验</p>\n     *\n     * @return\n     */\n    public int getCode() {\n        return FunLibrary.getiBase().checkCode(verifyJson, null);\n    }\n\n\n    /**\n     * 校验code码是否正确,==0\n     *\n     * @return\n     */\n    public boolean isRight() {\n        return 0 == this.getCode();\n    }\n\n    /**\n     * 获取节点值\n     *\n     * @param key 节点\n     * @return 返回节点值\n     */\n    public String getValue(String key) {\n        int size = lines.size();\n        for (int i = 0; i < size; i++) {\n            String line = lines.get(i);\n            if (line.startsWith(key + \":\"))\n                return line.replaceFirst(key + \":\", EMPTY);\n        }\n        return EMPTY;\n    }\n\n    /**\n     * 校验是否包含文本\n     *\n     * @param text 需要校验的文本\n     * @return 返回 Boolean 值\n     */\n    public boolean isContains(String... text) {\n        boolean result = true;\n        String content = verifyJson.toString();\n        int length = text.length;\n        for (int i = 0; i < length; i++) {\n            if (!result) break;\n            result = content.contains(text[i]) & result;\n        }\n        return result;\n    }\n\n    /**\n     * 校验节点值为数字\n     *\n     * @param value 节点名\n     * @return 返回 Boolean 值\n     */\n    public boolean isNum(String... value) {\n        boolean result = true;\n        int length = value.length;\n        for (int i = 0; i < length; i++) {\n            String key = value[i] + \":\";\n            if (!verifyJson.toString().contains(value[i]) || !result)\n                return false;\n            for (int k = 0; k < lines.size(); k++) {\n                String line = lines.get(k);\n                if (line.startsWith(key)) {\n                    String lineValue = line.replaceFirst(key, EMPTY);\n                    result = isNumber(lineValue) & result;\n                }\n            }\n        }\n        return result;\n    }\n\n    /**\n     * 校验节点值不为空\n     *\n     * @param keys 节点名\n     * @return 返回 Boolean 值，为空返回false，不为空返回true\n     */\n    public boolean notNull(String... keys) {\n        boolean result = true;\n        for (int i = 0; i < keys.length; i++) {\n            String key = keys[i] + \":\";\n            if (!verifyJson.toString().contains(keys[i]) || !result)\n                return false;\n            for (int k = 0; k < lines.size(); k++) {\n                String line = lines.get(k);\n                if (line.startsWith(key)) {\n                    String lineValue = line.replaceFirst(key, EMPTY);\n                    result = lineValue != null & !lineValue.isEmpty() & result;\n                }\n            }\n        }\n        return result;\n    }\n\n    /**\n     * 验证是否为列表，根据字段后面的符号是否是[\n     *\n     * @param key 返回体的字段值\n     * @return\n     */\n    public boolean isArray(String key) {\n        String json = verifyJson.toString();\n        int index = json.indexOf(key);\n        char a = json.charAt(index + key.length() + 2);\n        return a == '[';\n    }\n\n    /**\n     * 验证是否是json，根据后面跟的符号是否是{\n     *\n     * @param key 返回体的字段值\n     * @return\n     */\n    public boolean isJson(String key) {\n        String json = verifyJson.toString();\n        int index = json.indexOf(key);\n        char a = json.charAt(index + key.length() + 2);\n        if (a == '{')\n            return true;\n        return false;\n    }\n\n    /**\n     * 是否是Boolean值\n     *\n     * @return\n     */\n    public boolean isBoolean(String... value) {\n        boolean result = true;\n        int length = value.length;\n        for (int i = 0; i < length; i++) {\n            String key = value[i] + \":\";\n            if (!verifyJson.toString().contains(value[i]) || !result)\n                return false;\n            for (int k = 0; k < lines.size(); k++) {\n                String line = lines.get(k);\n                if (line.startsWith(key)) {\n                    String lineValue = line.replaceFirst(key, EMPTY);\n                    result = Regex.isRegex(lineValue, \"^(false)|(true)$\") & result;\n                }\n            }\n        }\n        return result;\n    }\n\n    /**\n     * 验证正则匹配结果\n     *\n     * @param regex\n     * @return\n     */\n    public boolean isRegex(String regex) {\n        String text = verifyJson.toString();\n        return Regex.isRegex(text, regex);\n    }\n\n    /**\n     * 解析json信息\n     *\n     * @param response json格式的响应实体\n     * @return json每个字段和值，key:value形式\n     */\n    public static List<String> parseJsonLines(JSONObject response) {\n        String jsonStr = response.toString();// 先将json对象转化为string对象\n        jsonStr = jsonStr.replaceAll(\",\", LINE);\n        jsonStr = jsonStr.replaceAll(\"\\\"\", EMPTY);\n        jsonStr = jsonStr.replaceAll(\"\\\\\\\\/\", OR);\n        jsonStr = jsonStr.replaceAll(\"\\\\{\", LINE);\n        jsonStr = jsonStr.replaceAll(\"\\\\[\", LINE);\n        jsonStr = jsonStr.replaceAll(\"}\", LINE);\n        jsonStr = jsonStr.replaceAll(\"]\", LINE);\n        List<String> jsonLines = Arrays.asList(jsonStr.split(LINE));\n        return jsonLines;\n    }\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/frame/Save.java",
    "content": "package com.funtester.frame;\n\nimport com.funtester.base.exception.FailException;\nimport com.funtester.utils.RWUtil;\nimport com.alibaba.fastjson.JSONObject;\nimport org.apache.commons.lang3.StringUtils;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.io.File;\nimport java.util.ArrayList;\nimport java.util.Collection;\nimport java.util.List;\n\nimport static com.funtester.config.Constant.*;\n\n/**\n * 用来保存数据的类，如果文件已经存在会删除原来的文件\n */\npublic class Save {\n\n    private static Logger logger = LogManager.getLogger(Save.class);\n\n    /**\n     * 保存信息，每次回删除文件，默认当前工作空间\n     *\n     * @param content 内容\n     */\n    public static void info(String content) {\n        info(\"long\", content);\n    }\n\n    public static void info(String name, String content) {\n        File dirFile = new File(LONG_Path + name);\n        if (dirFile.exists()) {\n            boolean delete = dirFile.delete();\n            if (!delete) FailException.fail(\"删除文件失败!\" + name);\n        }\n        RWUtil.writeText(dirFile, content);\n        logger.info(\"数据保存成功！文件名：{}{}\", LONG_Path, name);\n    }\n\n    /**\n     * 保存list数据到本地文件\n     */\n    public static void saveLongList(Collection<Long> data, Object name) {\n        List<String> list = new ArrayList<>();\n        data.forEach(num -> list.add(num.toString()));\n        saveStringList(list, name.toString());\n    }\n\n    /**\n     * 保存list数据到本地文件\n     */\n    public static void saveIntegerList(Collection<Integer> data, String name) {\n        List<String> list = new ArrayList<>();\n        data.forEach(num -> list.add(num.toString()));\n        saveStringList(list, name);\n    }\n\n    /**\n     * 保存list数据到本地文件\n     */\n    public static void saveDoubleList(Collection<Double> data, String name) {\n        List<String> list = new ArrayList<>();\n        data.forEach(num -> list.add(num.toString()));\n        saveStringList(list, name);\n    }\n\n    /**\n     * 保存list数据，long类型无法覆盖\n     *\n     * @param data\n     * @param name\n     */\n    public static void saveList(Collection<Object> data, String name) {\n        List<String> list = new ArrayList<>();\n        data.forEach(num -> list.add(num.toString()));\n        saveStringList(list, name);\n    }\n\n    /**\n     * 保存list数据到本地文件\n     */\n    public static void saveStringList(Collection<String> data, String name) {\n        String join = StringUtils.join(data, LINE);\n        info(name, join);\n    }\n\n    /**\n     * 保存json数据到本地文件\n     */\n    public static void saveJson(JSONObject data, String name) {\n        StringBuffer buffer = new StringBuffer();\n        data.keySet().forEach(x -> buffer.append(LINE + x.toString() + PART + data.getString(x.toString())));\n        /*处理\\n\\t(LINE)*/\n        if (buffer.length() > 2) info(name, buffer.substring(2));\n    }\n\n    /**\n     * 同步save数据,用于匿名类多线程保存测试数据\n     *\n     * @param data\n     * @param name\n     */\n    public static void saveStringListSync(Collection<String> data, String name) {\n        synchronized (Save.class) {\n            if (data.isEmpty()) return;\n            saveStringList(data, name);\n        }\n    }\n\n\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/frame/SourceCode.java",
    "content": "package com.funtester.frame;\n\n\nimport com.alibaba.fastjson.JSONObject;\nimport com.funtester.base.exception.FailException;\nimport com.funtester.base.exception.ParamException;\nimport com.funtester.base.interfaces.IMessage;\nimport com.funtester.utils.Regex;\nimport com.funtester.utils.Time;\nimport org.apache.commons.lang3.ArrayUtils;\nimport org.apache.commons.lang3.StringUtils;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.io.*;\nimport java.text.DecimalFormat;\nimport java.util.Arrays;\nimport java.util.List;\nimport java.util.Random;\nimport java.util.Scanner;\nimport java.util.concurrent.TimeUnit;\nimport java.util.stream.Collectors;\nimport java.util.stream.IntStream;\n\n\npublic class SourceCode extends Output {\n\n    private static Logger logger = LogManager.getLogger(SourceCode.class);\n\n    private static Scanner scanner;\n\n    private static IMessage iMessage;\n\n    public static IMessage getiMessage() {\n        return iMessage;\n    }\n\n    public static void setiMessage(IMessage iMessage) {\n        SourceCode.iMessage = iMessage;\n    }\n\n    private static ThreadLocal<Random> random = new ThreadLocal() {\n        @Override\n        protected Random initialValue() {\n            return new Random();\n        }\n\n    };\n\n    /**\n     * 获取当前时间戳10位int 类型的数据\n     *\n     * @return\n     */\n    public static int getMark() {\n        return (int) (Time.getTimeStamp() / 1000);\n    }\n\n    /**\n     * 获取纳秒的时间标记\n     *\n     * @return\n     */\n    public static long getNanoMark() {\n        return System.nanoTime();\n    }\n\n    /**\n     * 等待方法，用sacnner类，控制台输出字符key时会跳出循环\n     * <p>如何执行close方法，只能用一次</p>\n     *\n     * @param key\n     */\n    public static void waitForKey(Object key) {\n        logger.warn(\"请输入“{}”继续运行！\", key.toString());\n        long start = Time.getTimeStamp();\n        scanner = new Scanner(System.in, DEFAULT_CHARSET.name());\n        while (scanner.hasNext()) {\n            String next = scanner.next();\n            if (next.equalsIgnoreCase(key.toString())) break;\n            logger.warn(\"输入：{}错误！\", next);\n        }\n        long end = Time.getTimeStamp();\n        double timeDiffer = Time.getTimeDiffer(start, end);\n        logger.info(\"本次共等待：\" + timeDiffer + \"秒！\");\n    }\n\n    /**\n     * 获取屏幕输入内容\n     * <p>如何执行close方法，只能用一次</p>\n     *\n     * @return\n     */\n    public static String getInput() {\n        scanner = new Scanner(System.in, DEFAULT_CHARSET.name());\n        String next = scanner.next();\n        logger.debug(\"输、入内容：{}\", next);\n        return next;\n    }\n\n    /**\n     * 关闭scanner，解决无法多次使用wait的BUG\n     */\n    public static void closeScanner() {\n        scanner.close();\n    }\n\n    /**\n     * 将数组变成json对象，使用split方法\n     * <p>\n     * split方法默认limit=2\n     * </p>\n     * int和double使用数字类型,其他使用字符串类型\n     *\n     * @param objects\n     * @param regex   分隔的regex表达式\n     * @return\n     */\n    public static JSONObject changeArraysToJson(Object[] objects, String regex) {\n        JSONObject args = new JSONObject();\n        Arrays.stream(objects).forEach(x -> {\n            String[] split = x.toString().split(regex, 2);\n            args.put(split[0], isInteger(split[1]) ? changeStringToInt(split[1]) : isDouble(split[1]) ? changeStringToDouble(split[1]) : split[1]);\n        });\n        return args;\n    }\n\n    /**\n     * 获取一个简单的json对象\n     * <p>\n     * 使用“=”号作为分隔符，limit=2\n     * </p>\n     *\n     * @param content\n     * @return\n     */\n    public static JSONObject getJson(String... content) {\n        if (StringUtils.isAnyEmpty(content)) ParamException.fail(\"转换成json格式参数错误!\");\n        return changeArraysToJson(content, \"=\");\n    }\n\n    /**\n     * 获取一个简单的JSON对象\n     *\n     * @param key\n     * @param value\n     * @return\n     */\n    public static JSONObject getSimpleJson(String key, Object value) {\n        return StringUtils.isBlank(key) ? null : new JSONObject(1) {{\n            put(key, value);\n        }};\n    }\n\n    /**\n     * 获取text复制拼接的string\n     *\n     * @param text\n     * @param time 次数\n     * @return\n     */\n    public static String getManyString(String text, int time) {\n        return IntStream.range(0, time).mapToObj(x -> text).collect(Collectors.joining());\n    }\n\n    /**\n     * 获取一个百分比，两位小数\n     *\n     * @param total 总数\n     * @param piece 成功数\n     * @return 百分比\n     */\n    public static double getPercent(int total, int piece) {\n        if (total == 0) return 0.00;\n        int s = (int) (piece * 1.0 / total * 10000);\n        return s * 1.0 / 100;\n    }\n\n    /**\n     * 获取百分比,string类型,拼接%符合,两位小数\n     *\n     * @param percent 这里传的需要计算好的百分比,实际比例*100,而不是比例\n     * @return\n     */\n    public static String getPercent(double percent) {\n        return formatDouble(percent) + \"%\";\n    }\n\n    /**\n     * 格式化数字格式，使用千分号\n     *\n     * @param number\n     * @return\n     */\n    public static String formatLong(Number number) {\n        return formatNumber(number, \"#,###\");\n    }\n\n    /**\n     * 格式化数字格式,保留两位有效数字,使用去尾法\n     *\n     * @param number\n     * @return\n     */\n    public static String formatDouble(Number number) {\n        return formatNumber(number, \"#.##\");\n    }\n\n    /**\n     * 格式化数字格式\n     *\n     * @param number\n     * @param pattern\n     * @return\n     */\n    public static String formatNumber(Number number, String pattern) {\n        DecimalFormat format = new DecimalFormat(pattern);\n        return format.format(number);\n    }\n\n    /**\n     * 获取随机IP地址\n     *\n     * @return\n     */\n    public static String getRandomIP() {\n        return getRandomInt(255) + \".\" + getRandomInt(255) + \".\" + getRandomInt(255) + \".\" + getRandomInt(255);\n    }\n\n    /**\n     * 把string类型转化为int\n     *\n     * @param text 需要转化的文本\n     * @return\n     */\n    public static int changeStringToInt(String text) {\n        logger.debug(\"需要转化成的文本：{}\", text);\n        try {\n            return Integer.parseInt(text);\n        } catch (NumberFormatException e) {\n            logger.warn(\"转化int类型失败！\", e);\n            return TEST_ERROR_CODE;\n        }\n    }\n\n    /**\n     * 把string类型转化为long\n     *\n     * @param text 需要转化的文本\n     * @return\n     */\n    public static long changeStringToLong(String text) {\n        logger.debug(\"需要转化成的文本：{}\", text);\n        try {\n            return Long.parseLong(text);\n        } catch (NumberFormatException e) {\n            logger.warn(\"转化int类型失败！\", e);\n            return TEST_ERROR_CODE;\n        }\n    }\n\n    /**\n     * 将string转换成boolean，失败返回null，待修改\n     *\n     * @param text\n     * @return\n     */\n    public static boolean changeStringToBoolean(String text) {\n        logger.debug(\"需要转化成的文本：{}\", text);\n        if (text == null || !Regex.isMatch(text.toLowerCase(), \"false|ture\")) return false;\n        return text.equalsIgnoreCase(\"true\");\n    }\n\n    /**\n     * string转化为double\n     *\n     * @param text\n     * @return\n     */\n    public static double changeStringToDouble(String text) {\n        logger.debug(\"需要转化成的文本：{}\", text);\n        try {\n            return Double.parseDouble(text);\n        } catch (NumberFormatException e) {\n            logger.warn(\"转化double类型失败！\", e);\n            return TEST_ERROR_CODE * 1.0;\n        }\n    }\n\n    /**\n     * 是否是数字，000不算,0.0也算,-0和-0.0不算\n     *\n     * @param text\n     * @return\n     */\n    public static boolean isNumber(String text) {\n        logger.debug(\"需要判断的文本：{}\", text);\n        if (StringUtils.isEmpty(text) || text.equals(\"-0\")) return false;\n        if (text.equals(\"0\")) return true;\n        return Regex.isMatch(text, \"-{0,1}(([1-9][0-9]*)|0)(.\\\\d+){0,1}\");\n    }\n\n    public static boolean isInteger(String str) {\n        return isNumber(str) && !str.contains(\".\") && str.length() < 11;\n    }\n\n    public static boolean isDouble(String str) {\n        return isNumber(str) && str.contains(\".\");\n    }\n\n    /**\n     * 线程休眠,单位是秒\n     *\n     * @param second 秒，可以是小数\n     */\n    public static void sleep(int second) {\n        if (second > 100) FailException.fail(\"休眠时间过长,请更换其他方式!\");\n        try {\n            Thread.sleep(second * 1000);\n        } catch (InterruptedException e) {\n            logger.warn(\"sleep发生错误！\", e);\n        }\n    }\n\n    /**\n     * 睡眠,提供更精准的休眠功能\n     *\n     * @param time 单位s\n     */\n    public static void sleep(double time) {\n        if (time > 100) FailException.fail(\"休眠时间过长,请更换其他方式!\");\n        try {\n            Thread.sleep((long) (time * 1000));\n        } catch (InterruptedException e) {\n            logger.warn(\"sleep发生错误！\", e);\n        }\n    }\n\n    /**\n     * 线程休眠,以纳秒为单位\n     *\n     * @param nanosec\n     */\n    public static void sleep(long nanosec) {\n        if (nanosec < 1_000_000) return;\n        try {\n            TimeUnit.NANOSECONDS.sleep(nanosec);\n        } catch (InterruptedException e) {\n            logger.warn(\"sleep发生错误！\", e);\n        }\n    }\n\n    /**\n     * 获取随机数，获取1~num 的数字，包含 num\n     *\n     * @param num 随机数上限\n     * @return 随机数\n     */\n    public static int getRandomInt(int num) {\n        return random.get().nextInt(num) + 1;\n    }\n\n    /**\n     * 随机范围int,取头不取尾\n     *\n     * @param start\n     * @param end\n     * @return\n     */\n    public static int getRandomIntRange(int start, int end) {\n        if (end <= start) return TEST_ERROR_CODE;\n        return random.get().nextInt(end - start) + start;\n    }\n\n    /**\n     * 随机选择某一个值\n     *\n     * @param fs\n     * @param <F>\n     * @return\n     */\n    public static <F extends Number> F random(F... fs) {\n        return fs[getRandomInt(fs.length) - 1];\n    }\n\n    /**\n     * 随机选择某一个字符串\n     *\n     * @param fs\n     * @return\n     */\n    public static String random(String... fs) {\n        if (ArrayUtils.isEmpty(fs)) ParamException.fail(\"数组不能为空!\");\n        return fs[getRandomInt(fs.length) - 1];\n    }\n\n    /**\n     * 随机选择某一个对象\n     *\n     * @param list\n     * @param <F>\n     * @return\n     */\n    public static <F extends Object> F random(List<F> list) {\n        if (list == null || list.isEmpty()) ParamException.fail(\"数组不能为空!\");\n        return list.get(getRandomInt(list.size()) - 1);\n    }\n\n    /**\n     * 获取一定范围内的随机值\n     *\n     * @param start 初始值\n     * @param range 随机范围\n     * @return\n     */\n    public static double getRandomRange(double start, double range) {\n        return start - range + getRandomDouble() * range * 2;\n    }\n\n    /**\n     * 获取随机数，获取0-1 的数字\n     *\n     * @return 随机数\n     */\n    public static double getRandomDouble() {\n        return random.get().nextDouble();\n    }\n\n    /**\n     * 获取一个intsteam\n     *\n     * @param start\n     * @param end\n     * @return\n     */\n    public static IntStream range(int start, int end) {\n        return IntStream.range(start, end);\n    }\n\n    /**\n     * 获取一个intsteam，默认从0开始,num为止,不包含num\n     *\n     * @param num\n     * @return\n     */\n    public static IntStream range(int num) {\n        return IntStream.range(0, num);\n    }\n\n    /**\n     * 通用的终止运行的方法,用于脚本调试等场景\n     */\n    public static void fail() {\n        throw new FailException();\n    }\n\n    /**\n     * 通过将对象序列化成数据流实现深层拷贝的方法\n     * <p>\n     * 将该对象序列化成流,因为写在流里的是对象的一个拷贝，而原对象仍然存在于JVM里面。所以利用这个特性可以实现对象的深拷贝\n     * </p>\n     *\n     * @param t   需要被拷贝的对象,必需实现 Serializable接口,不然会报错\n     * @param <T> 需要拷贝对象的类型\n     * @return\n     */\n    public static <T> T deepClone(T t) {\n        try {\n            ByteArrayOutputStream baos = new ByteArrayOutputStream();\n            ObjectOutputStream oos = new ObjectOutputStream(baos);\n            oos.writeObject(t);\n            // 将流序列化成对象\n            ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray());\n            ObjectInputStream ois = new ObjectInputStream(bais);\n            return (T) ois.readObject();\n        } catch (IOException e) {\n            logger.error(\"线程任务拷贝失败!\", e);\n        } catch (ClassNotFoundException e) {\n            logger.error(\"未找到对应类!\", e);\n        }\n        return null;\n    }\n\n\n}"
  },
  {
    "path": "src/main/groovy/com/funtester/frame/execute/Concurrent.java",
    "content": "package com.funtester.frame.execute;\n\nimport com.funtester.base.bean.PerformanceResultBean;\nimport com.funtester.base.constaint.ThreadBase;\nimport com.funtester.config.Constant;\nimport com.funtester.frame.Save;\nimport com.funtester.frame.SourceCode;\nimport com.funtester.utils.Time;\nimport com.funtester.utils.RWUtil;\nimport org.apache.commons.lang3.StringUtils;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Vector;\nimport java.util.concurrent.CountDownLatch;\nimport java.util.concurrent.ExecutorService;\n\nimport static java.util.stream.Collectors.toList;\n\n/**\n * 并发类，用于启动压力脚本\n */\npublic class Concurrent extends SourceCode {\n\n    private static Logger logger = LogManager.getLogger(Concurrent.class);\n\n    /**\n     * 开始时间\n     */\n    private long startTime;\n\n    /**\n     * 结束时间\n     */\n    private long endTime;\n\n    /**\n     * 任务描述\n     */\n    public String desc;\n\n    /**\n     * 任务集\n     */\n    public List<ThreadBase> threads = new ArrayList<>();\n\n    /**\n     * 线程数\n     */\n    public int threadNum;\n\n    /**\n     * 执行失败总数\n     */\n    private int errorTotal;\n\n    /**\n     * 任务执行失败总数\n     */\n    private int failTotal;\n\n    /**\n     * 执行总数\n     */\n    private int executeTotal;\n\n    /**\n     * 用于记录所有请求时间\n     */\n    public static Vector<Integer> allTimes = new Vector<>();\n\n    /**\n     * 记录所有markrequest的信息\n     */\n    public static Vector<String> requestMark = new Vector<>();\n\n    /**\n     * 线程池\n     */\n    ExecutorService executorService;\n\n    /**\n     * 计数器\n     */\n    CountDownLatch countDownLatch;\n\n    /**\n     * @param thread    线程任务\n     * @param threadNum 线程数\n     * @param desc      任务描述\n     */\n    public Concurrent(ThreadBase thread, int threadNum, String desc) {\n        this(threadNum, desc);\n        range(threadNum).forEach(x -> threads.add(thread.clone()));\n    }\n\n    /**\n     * @param threads 线程组\n     * @param desc    任务描述\n     */\n    public Concurrent(List<ThreadBase> threads, String desc) {\n        this(threads.size(), desc);\n        this.threads = threads;\n    }\n\n    private Concurrent(int threadNum, String desc) {\n        this.threadNum = threadNum;\n        this.desc = StatisticsUtil.getFileName(desc);\n        executorService = ThreadPoolUtil.createFixedPool(threadNum);\n        countDownLatch = new CountDownLatch(threadNum);\n    }\n\n    private Concurrent() {\n\n    }\n\n    /**\n     * 执行多线程任务\n     * 默认取list中thread对象,丢入线程池,完成多线程执行,如果没有threadname,name默认采用desc+线程数作为threadname,去除末尾的日期\n     */\n    public PerformanceResultBean start() {\n        Progress progress = new Progress(threads, StatisticsUtil.getTrueName(desc));\n        new Thread(progress).start();\n        startTime = Time.getTimeStamp();\n        for (int i = 0; i < threadNum; i++) {\n            ThreadBase thread = threads.get(i);\n            if (StringUtils.isBlank(thread.threadName)) thread.threadName = StatisticsUtil.getTrueName(desc) + i;\n            thread.setCountDownLatch(countDownLatch);\n            executorService.execute(thread);\n        }\n        shutdownService(executorService, countDownLatch);\n        endTime = Time.getTimeStamp();\n        progress.stop();\n        threads.forEach(x -> {\n            if (x.status()) failTotal++;\n            errorTotal += x.errorNum;\n            executeTotal += x.executeNum;\n        });\n        logger.info(\"总计{}个线程，共用时：{} s,执行总数:{},错误数:{},失败数:{}\", threadNum, Time.getTimeDiffer(startTime, endTime), executeTotal, errorTotal, failTotal);\n        return over();\n    }\n\n    /**\n     * 关闭任务相关资源\n     *\n     * @param executorService 线程池\n     * @param countDownLatch  计数器\n     */\n    private static void shutdownService(ExecutorService executorService, CountDownLatch countDownLatch) {\n        try {\n            countDownLatch.await();\n            executorService.shutdown();\n        } catch (InterruptedException e) {\n            logger.warn(\"线程池关闭失败！\", e);\n        }\n    }\n\n    private PerformanceResultBean over() {\n        Save.saveIntegerList(allTimes, DATA_Path.replace(LONG_Path, EMPTY) + StatisticsUtil.getFileName(threadNum, desc));\n        Save.saveStringListSync(Concurrent.requestMark, MARK_Path.replace(LONG_Path, EMPTY) + desc);\n        allTimes = new Vector<>();\n        requestMark = new Vector<>();\n        return countQPS(threadNum, desc, Time.getTimeByTimestamp(startTime), Time.getTimeByTimestamp(endTime));\n    }\n\n\n    /**\n     * 计算结果\n     * <p>此结果仅供参考</p>\n     * 此处因为start和end的不准确问题,所以采用改计算方法,与fixQPS有区别\n     *\n     * @param name 线程数\n     */\n    public PerformanceResultBean countQPS(int name, String desc, String start, String end) {\n        List<String> strings = RWUtil.readTxtFileByLine(Constant.DATA_Path + StatisticsUtil.getFileName(name, desc));\n        int size = strings.size();\n        List<Integer> data = strings.stream().map(x -> changeStringToInt(x)).collect(toList());\n        int sum = data.stream().mapToInt(x -> x).sum();\n        String statistics = StatisticsUtil.statistics(data, desc, threadNum);\n        int rt = sum / size;\n        double qps = 1000.0 * name / rt;\n        double qps2 = (executeTotal + errorTotal) * 1000.0 / (endTime - startTime);\n        return new PerformanceResultBean(desc, start, end, name, size, rt, qps, qps2, getPercent(executeTotal, errorTotal), getPercent(threadNum, failTotal), executeTotal, statistics);\n    }\n\n\n    /**\n     * 用于做后期的计算\n     *\n     * @param name\n     * @param desc\n     * @return\n     */\n    public PerformanceResultBean countQPS(int name, String desc) {\n        return countQPS(name, desc, Time.getDate(), Time.getDate());\n    }\n\n\n}"
  },
  {
    "path": "src/main/groovy/com/funtester/frame/execute/ExecuteGroovy.java",
    "content": "package com.funtester.frame.execute;\n\nimport com.funtester.frame.SourceCode;\nimport com.funtester.utils.FileUtil;\nimport groovy.lang.GroovyClassLoader;\nimport groovy.lang.GroovyObject;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.io.File;\nimport java.io.IOException;\nimport java.lang.reflect.Method;\nimport java.util.ArrayList;\nimport java.util.List;\n\n/**\n * groovy脚本执行类，用户执行上传的groovy脚本，功能简单，使用未做封装，将就用一下\n */\npublic class ExecuteGroovy extends SourceCode {\n\n    private static Logger logger = LogManager.getLogger(ExecuteSource.class);\n\n    /**\n     * 路径\n     */\n    private String path;\n\n    /**\n     * 文件名\n     */\n    private String name;\n\n    /**\n     * 所有的脚本文件\n     */\n    private List<String> files = new ArrayList<>();\n\n    /**\n     * Groovy类加载器\n     */\n    private GroovyClassLoader loader = new GroovyClassLoader(getClass().getClassLoader());\n\n    /**\n     * Groovy对象\n     */\n    private GroovyObject groovyObject;\n\n    /**\n     * 加载类\n     */\n    private Class<?> groovyClass;\n\n\n    public ExecuteGroovy(String path, String name) {\n        this.path = path;\n        this.name = name;\n        getGroovyObject();\n    }\n\n    /**\n     * 执行一个类的所有方法\n     */\n    public void executeAllMethod(String path) {\n        FileUtil.getAllFile(path);\n        if (files == null)\n            return;\n        files.forEach((file) -> new ExecuteGroovy(file, EMPTY).executeMethodByPath());\n    }\n\n    /**\n     * 执行某个类的方法，需要做过滤\n     *\n     * @return\n     */\n    public void executeMethodByName() {\n        if (new File(path).isDirectory()) {\n            logger.warn(\"文件类型错误!\");\n        }\n        try {\n            groovyObject.invokeMethod(name, null);\n        } catch (Exception e) {\n            logger.warn(\"执行\" + name + \"失败!\", e);\n        }\n    }\n\n    /**\n     * 根据path执行相关方法\n     */\n    public void executeMethodByPath() {\n        Method[] methods = groovyClass.getDeclaredMethods();//获取类方法，此处方法比较多，需过滤\n        for (Method method : methods) {\n            String methodName = method.getName();\n            if (methodName.contains(\"test\") || methodName.equals(\"main\")) {\n                groovyObject.invokeMethod(methodName, null);\n            }\n        }\n    }\n\n    /**\n     * 获取groovy对象和执行类\n     */\n    public void getGroovyObject() {\n        try {\n            groovyClass = loader.parseClass(new File(path));//创建类\n        } catch (IOException e) {\n            logger.warn(\"groovy类加载失败！\", e);\n        }\n        try {\n            groovyObject = (GroovyObject) groovyClass.newInstance();//创建类对象\n        } catch (InstantiationException e) {\n            logger.warn(\"创建对象失败！\", e);\n        } catch (IllegalAccessException e) {\n            logger.warn(\"非法异常！\", e);\n        }\n    }\n\n    /**\n     * 获取文件下所有的groovy脚本,不支持递归查询\n     *\n     * @return\n     */\n    public List<String> getAllGroovyFile(String path) {\n        File file = new File(path);\n        if (file.isFile()) {\n            files.add(path);\n            return files;\n        }\n        File[] files1 = file.listFiles();\n        int size = files1.length;\n        for (int i = 0; i < size; i++) {\n            String name = files1[i].getAbsolutePath();\n            if (name.endsWith(\".groovy\"))\n                files.add(name);\n        }\n        return files;\n    }\n\n}"
  },
  {
    "path": "src/main/groovy/com/funtester/frame/execute/ExecuteSource.java",
    "content": "package com.funtester.frame.execute;\n\nimport com.alibaba.fastjson.JSON;\nimport com.funtester.base.exception.FailException;\nimport com.funtester.config.Constant;\nimport com.funtester.frame.SourceCode;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.io.File;\nimport java.lang.reflect.InvocationTargetException;\nimport java.lang.reflect.Method;\nimport java.net.URL;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.List;\n\npublic class ExecuteSource extends SourceCode {\n\n    private static Logger logger = LogManager.getLogger(ExecuteSource.class);\n\n    /**\n     * 执行包内所有类的非 main 方法\n     *\n     * @param packageName\n     */\n    public static void executeAllMethodInPackage(String packageName) {\n        List<String> classNames = getClassName(packageName);\n        if (classNames != null) {\n            for (String className : classNames) {\n                String path = packageName + \".\" + className;\n                executeAllMethod(path);// 执行所有方法\n            }\n        }\n    }\n\n\n    /**\n     * 执行一个类的方法内所有的方法，非 main，执行带参方法的代码过滤\n     *\n     * @param path 类名\n     */\n    public static void executeAllMethod(String path) {\n        Class<?> c = null;\n        Object object = null;\n        try {\n            c = Class.forName(path);\n            object = c.newInstance();\n        } catch (Exception e) {\n            e.printStackTrace();\n        }\n        Method[] methods = c.getDeclaredMethods();\n        for (Method method : methods) {\n            try {\n                method.invoke(object);\n            } catch (IllegalAccessException e) {\n                logger.warn(\"非法访问导致反射方法执行失败！\", e);\n            } catch (InvocationTargetException e) {\n                logger.warn(\"反射调用目标异常导致方法执行失败！\", e);\n            } catch (Exception e) {\n                logger.warn(\"反射方法执行失败！\", e);\n            } finally {\n                sleep(Constant.EXECUTE_GAP_TIME);\n            }\n        }\n    }\n\n    /**\n     * 提供给命令行main方法使用\n     * <p>防止编译报错,用list绕一圈</p>\n     *\n     * @param params\n     */\n    public static void executeMethod(List<String> params) {\n        Object[] objects = params.subList(1, params.size()).toArray();\n        executeMethod(params.get(0), objects);\n    }\n\n    /**\n     * 提供给命令行main方法使用\n     * <p>防止编译报错,用list绕一圈</p>\n     *\n     * @param params\n     */\n    public static void executeMethod(String[] params) {\n        executeMethod(Arrays.asList(params));\n    }\n\n    /**\n     * 执行具体的某一个方法,提供内部方法调用\n     *\n     * @param path\n     */\n    public static void executeMethod(String path, Object... paramsTpey) {\n        int length = paramsTpey.length;\n        if (length % 2 == 1) FailException.fail(\"参数个数错误,应该是偶数\");\n        String className = path.substring(0, path.lastIndexOf(\".\"));\n        String methodname = path.substring(className.length() + 1);\n        Class<?> c = null;\n        Object object = null;\n        try {\n            c = Class.forName(className);\n            object = c.newInstance();\n        } catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) {\n            logger.warn(\"创建实例对象时错误:{}\", className, e);\n        }\n        Method[] methods = c.getDeclaredMethods();\n        for (Method method : methods) {\n            if (!method.getName().equalsIgnoreCase(methodname)) continue;\n            try {\n                Class[] classs = new Class[length / 2];\n                for (int i = 0; i < paramsTpey.length; i = +2) {\n                    classs[i / 2] = Class.forName(paramsTpey[i].toString());//此处基础数据类型的参数会导致报错,但不影响下面的调用\n                }\n                method = c.getMethod(method.getName(), classs);\n            } catch (NoSuchMethodException | ClassNotFoundException e) {\n                logger.warn(\"方法属性处理错误!\", e);\n            }\n            try {\n                Object[] ps = new Object[length / 2];\n                for (int i = 1; i < paramsTpey.length; i = +2) {\n                    String name = paramsTpey[i - 1].toString();\n                    String param = paramsTpey[i].toString();\n                    Object p = param;\n                    if (name.contains(\"Integer\")) {\n                        p = Integer.parseInt(param);\n                    } else if (name.contains(\"JSON\")) {\n                        p = JSON.parseObject(param);\n                    }\n                    ps[i / 2] = p;\n                }\n                method.invoke(object, ps);\n            } catch (IllegalAccessException | InvocationTargetException e) {\n                logger.warn(\"反射执行方法失败:{}\", path, e);\n            }\n            break;\n        }\n    }\n\n    /**\n     * 获取当前类的所有用例方法名\n     *\n     * @param path\n     * @return\n     */\n    public static List<String> getAllMethodName(String path) {\n        List<String> methods = new ArrayList<>();\n        Class<?> c = null;\n        Object object = null;\n        try {\n            c = Class.forName(path);\n            object = c.newInstance();\n        } catch (Exception e) {\n            FailException.fail(\"初始化对象失败:\" + path);\n        }\n        Method[] all = c.getDeclaredMethods();\n        for (int i = 0; i < all.length; i++) {\n            String str = all[i].getName();\n            methods.add(str);\n        }\n        return methods;\n    }\n\n    /**\n     * 获取某包下所有类\n     *\n     * @param packageName 包名\n     * @return 类的完整名称\n     */\n    public static List<String> getClassName(String packageName) {\n        List<String> fileNames = new ArrayList<>();\n        ClassLoader loader = Thread.currentThread().getContextClassLoader();// 获取当前位置\n        String packagePath = packageName.replace(\".\", Constant.OR);// 转化路径，Linux 系统\n        URL url = loader.getResource(packagePath);// 具体路径\n        if (url == null || !\"file\".equals(url.getProtocol())) {\n            FailException.fail(\"获取包路径失败!\");\n        }\n        File file = new File(url.getPath());\n        File[] childFiles = file.listFiles();\n        for (File childFile : childFiles) {\n            String path = childFile.getPath();\n            if (path.endsWith(\".class\")) {\n                path = path.substring(path.lastIndexOf(OR) + 1, path.lastIndexOf(\".\"));\n                fileNames.add(path);\n            }\n        }\n        return fileNames;\n    }\n\n\n}"
  },
  {
    "path": "src/main/groovy/com/funtester/frame/execute/FixedQpsConcurrent.java",
    "content": "package com.funtester.frame.execute;\n\nimport com.funtester.base.bean.PerformanceResultBean;\nimport com.funtester.base.constaint.FixedQpsThread;\nimport com.funtester.config.Constant;\nimport com.funtester.config.HttpClientConstant;\nimport com.funtester.frame.Save;\nimport com.funtester.frame.SourceCode;\nimport com.funtester.httpclient.GCThread;\nimport com.funtester.utils.Time;\nimport com.funtester.utils.RWUtil;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Vector;\nimport java.util.concurrent.ExecutorService;\nimport java.util.concurrent.TimeUnit;\nimport java.util.concurrent.atomic.AtomicInteger;\n\nimport static java.util.stream.Collectors.toList;\n\n/**\n * 并发类，用于启动压力脚本\n */\npublic class FixedQpsConcurrent extends SourceCode {\n\n    private static Logger logger = LogManager.getLogger(FixedQpsConcurrent.class);\n\n    public static boolean key = false;\n\n    public static AtomicInteger executeTimes = new AtomicInteger(0);\n\n    public static AtomicInteger errorTimes = new AtomicInteger(0);\n\n    public static Vector<String> marks = new Vector<>();\n\n    /**\n     * 基础任务对象\n     */\n    public FixedQpsThread baseThread;\n\n    /**\n     * 用于记录所有请求时间\n     */\n    public static Vector<Integer> allTimes = new Vector<>();\n\n    /**\n     * 开始时间\n     */\n    public long startTime;\n\n    /**\n     * 结束时间\n     */\n    public long endTime;\n\n    /**\n     * 任务队列的长度,因为会循环去那队列的任务\n     */\n    public int queueLength;\n\n    /**\n     * 任务描述\n     */\n    public String desc;\n\n    /**\n     * 任务集\n     */\n    public List<FixedQpsThread> threads = new ArrayList<>();\n\n    /**\n     * 线程池\n     */\n    ExecutorService executorService;\n\n    /**\n     * @param thread 线程任务\n     * @param desc   任务描述\n     */\n    public FixedQpsConcurrent(FixedQpsThread thread, String desc) {\n        this(desc);\n        this.queueLength = 1;\n        threads.add(thread);\n        baseThread = thread;\n    }\n\n    /**\n     * @param threads 线程组\n     * @param desc    任务描述\n     */\n    public FixedQpsConcurrent(List<FixedQpsThread> threads, String desc) {\n        this(desc);\n        this.threads = threads;\n        baseThread = threads.get(0);\n        this.queueLength = threads.size();\n    }\n\n    /**\n     * 初始化连接池\n     */\n    private FixedQpsConcurrent(String desc) {\n        this.desc = StatisticsUtil.getFileName(desc);\n        executorService = ThreadPoolUtil.createPool(HttpClientConstant.THREADPOOL_CORE, HttpClientConstant.THREADPOOL_MAX, HttpClientConstant.THREAD_ALIVE_TIME);\n    }\n\n    private FixedQpsConcurrent() {\n\n    }\n\n    /**\n     * 重置连接池,用以改变并发能力\n     *\n     * @param core\n     * @param max\n     */\n    public void initPool(int core, int max) {\n        executorService = ThreadPoolUtil.createPool(core, max, HttpClientConstant.THREAD_ALIVE_TIME);\n    }\n\n    /**\n     * 执行多线程任务\n     * 默认取list中thread对象,丢入线程池,完成多线程执行,如果没有threadname,name默认采用desc+线程数作为threadname,去除末尾的日期\n     */\n    public PerformanceResultBean start() {\n        key = false;\n        Progress progress = new Progress(threads, StatisticsUtil.getTrueName(desc), executeTimes);\n        new Thread(progress).start();\n        boolean isTimesMode = baseThread.isTimesMode;\n        int limit = baseThread.limit;\n        int qps = baseThread.qps;\n        long interval = 1_000_000_000 / qps;//此处单位1s=1000ms,1ms=1000000ns\n        startTime = Time.getTimeStamp();\n        AidThread aidThread = new AidThread();\n        new Thread(aidThread).start();\n        while (true) {\n            executorService.execute(threads.get(limit-- % queueLength).clone());\n            if (key ? true : isTimesMode ? limit < 1 : Time.getTimeStamp() - startTime > limit) break;\n            sleep(interval);\n        }\n        endTime = Time.getTimeStamp();\n        aidThread.stop();\n        progress.stop();\n        GCThread.stop();\n        try {\n            executorService.shutdown();\n            executorService.awaitTermination(HttpClientConstant.WAIT_TERMINATION_TIMEOUT, TimeUnit.SECONDS);//此方法需要在shutdown方法执行之后执行\n        } catch (InterruptedException e) {\n            logger.error(\"线程池等待任务结束失败!\", e);\n        }\n        logger.info(\"总计执行 {} ，共用时：{} s,执行总数:{},错误数:{}!\", baseThread.isTimesMode ? baseThread.limit + \"次任务\" : \"秒\", Time.getTimeDiffer(startTime, endTime), executeTimes, errorTimes);\n        return over();\n    }\n\n    private PerformanceResultBean over() {\n        key = true;\n        Save.saveIntegerList(allTimes, DATA_Path.replace(LONG_Path, EMPTY) + StatisticsUtil.getFileName(queueLength, desc));\n        Save.saveStringListSync(marks, MARK_Path.replace(LONG_Path, EMPTY) + desc);\n        allTimes = new Vector<>();\n        marks = new Vector<>();\n        int executeNum = executeTimes.getAndSet(0);\n        int errorNum = errorTimes.getAndSet(0);\n        return countQPS(queueLength, desc, startTime, endTime, executeNum, errorNum);\n    }\n\n    /**\n     * 计算结果\n     * <p>此结果仅供参考</p>\n     * 由于fixQPS模型没有固定线程数,所以智能采取QPS=Q/T的计算,与concurrent有区别\n     *\n     * @param name 线程数\n     */\n    public PerformanceResultBean countQPS(int name, String desc, long start, long end, int executeNum, int errorNum) {\n        List<String> strings = RWUtil.readTxtFileByLine(Constant.DATA_Path + StatisticsUtil.getFileName(name, desc));\n        int size = strings.size();\n        List<Integer> data = strings.stream().map(x -> changeStringToInt(x)).collect(toList());\n        int sum = data.stream().mapToInt(x -> x).sum();\n        String statistics = StatisticsUtil.statistics(data, desc, name);\n        double qps = executeNum * 1000.0 / (end - start);\n        int qps2 = baseThread.qps;\n        return new PerformanceResultBean(desc, Time.getTimeByTimestamp(start), Time.getTimeByTimestamp(end), name, size, sum / size, qps, qps2, getPercent(executeNum, errorNum), 0, executeNum, statistics);\n    }\n\n    /**\n     * 补偿线程,如果超过一半QPS量,才会进行补偿,补偿速率为每秒20个\n     */\n    class AidThread implements Runnable {\n\n        private boolean key = true;\n\n        int i;\n\n        public AidThread() {\n\n        }\n\n        @Override\n        public void run() {\n            logger.info(\"补偿线程开始!\");\n            try {\n                while (key) {\n                    sleep(HttpClientConstant.LOOP_INTERVAL);\n                    int actual = executeTimes.get();\n                    int qps = baseThread.qps;\n                    long expect = (Time.getTimeStamp() - FixedQpsConcurrent.this.startTime) / 2000 * qps;\n                    if (expect > actual + qps) {\n                        logger.info(\"期望执行数:{},实际执行数:{},设置QPS:{}\", expect, actual, qps);\n                        range((int) expect - actual).forEach(x -> {\n                            sleep(0.05);\n                            if (!executorService.isShutdown()) {\n                                executorService.execute(threads.get(this.i++ % queueLength).clone());\n                            }\n                        });\n                    }\n                }\n                logger.info(\"补偿线程结束!\");\n            } catch (Exception e) {\n                logger.error(\"补偿线程发生错误!\", e);\n            }\n        }\n\n        public void stop() {\n            key = false;\n        }\n\n\n    }\n\n\n}"
  },
  {
    "path": "src/main/groovy/com/funtester/frame/execute/Progress.java",
    "content": "package com.funtester.frame.execute;\n\nimport com.funtester.base.constaint.FixedQpsThread;\nimport com.funtester.base.constaint.ThreadBase;\nimport com.funtester.base.constaint.ThreadLimitTimeCount;\nimport com.funtester.base.constaint.ThreadLimitTimesCount;\nimport com.funtester.base.exception.ParamException;\nimport com.funtester.config.HttpClientConstant;\nimport com.funtester.frame.SourceCode;\nimport com.funtester.utils.StringUtil;\nimport com.funtester.utils.Time;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.concurrent.atomic.AtomicInteger;\nimport java.util.stream.Collectors;\n\n/**\n * 用于异步展示性能测试进度的多线程类\n *\n * @param <F> 多线程任务{@link ThreadBase}对象的实现子类\n */\npublic class Progress<F extends ThreadBase> extends SourceCode implements Runnable {\n\n    private static Logger logger = LogManager.getLogger(Progress.class);\n\n    /**\n     * 会长\n     */\n    private static final String SUFFIX = \"QPS变化曲线\";\n\n    /**\n     * 记录每一次获取QPS的值,可能用于结果展示\n     */\n    public List<Integer> qs = new ArrayList<>();\n\n    /**\n     * 多线程任务类对象\n     */\n    private List<F> threads;\n\n    /**\n     * 线程数,用于计算实时QPS\n     */\n    private int threadNum;\n\n    /**\n     * 进度条的长度\n     */\n    private static final int LENGTH = 67;\n\n    /**\n     * 标志符号\n     */\n    private static final String ONE = getPart(3);\n\n    /**\n     * 总开关,是否运行,默认true\n     */\n    private boolean st = true;\n\n    /**\n     * 是否次数模型\n     */\n    private boolean isTimesMode;\n\n    /**\n     * 用于区分固定QPS请求模型,这里不计算固定QPS模型中的实时QPS\n     */\n    private boolean canCount;\n\n    /**\n     * 多线程任务基类对象,本类中不处理,只用来获取值,若使用的话请调用clone()方法\n     */\n    private F base;\n\n    /**\n     * 在固定QPS模式中使用\n     */\n    private AtomicInteger excuteNum;\n\n    /**\n     * 限制条件\n     */\n    private int limit;\n\n    /**\n     * 非精确时间,误差可以忽略\n     */\n    private long startTime = Time.getTimeStamp();\n\n    /**\n     * 描述\n     */\n    private String taskDesc;\n\n    /**\n     * 固定线程模型\n     *\n     * @param threads\n     * @param desc\n     */\n    public Progress(final List<F> threads, String desc) {\n        this.threads = threads;\n        this.threadNum = threads.size();\n        this.taskDesc = desc;\n        this.base = threads.get(0);\n        init();\n    }\n\n    /**\n     * 适配固定QPS模型\n     *\n     * @param threads\n     * @param desc\n     * @param excuteNum\n     */\n    public Progress(final List<F> threads, String desc, final AtomicInteger excuteNum) {\n        this.threads = threads;\n        this.threadNum = threads.size();\n        this.taskDesc = desc;\n        this.base = threads.get(0);\n        init();\n    }\n\n    /**\n     * 初始化对象,对istimesMode和limit赋值\n     */\n    private void init() {\n        if (base instanceof ThreadLimitTimeCount) {\n            this.isTimesMode = false;\n            this.canCount = true;\n            this.limit = ((ThreadLimitTimeCount) base).time;\n        } else if (base instanceof ThreadLimitTimesCount) {\n            this.isTimesMode = true;\n            this.canCount = true;\n            this.limit = ((ThreadLimitTimesCount) base).times;\n        } else if (base instanceof FixedQpsThread) {\n            FixedQpsThread fix = (FixedQpsThread) base;\n            this.canCount = false;\n            this.isTimesMode = fix.isTimesMode;\n            this.limit = fix.limit;\n        } else {\n            ParamException.fail(\"创建进度条对象失败!\");\n        }\n    }\n\n    @Override\n    public void run() {\n        double pro = 0;\n        while (st) {\n            sleep(HttpClientConstant.LOOP_INTERVAL);\n            pro = isTimesMode ? base.executeNum == 0 ? FixedQpsConcurrent.executeTimes.get() * 1.0 / limit : base.executeNum * 1.0 / limit : (Time.getTimeStamp() - startTime) * 1.0 / limit;\n            if (pro > 0.95) break;\n            if (st)\n                logger.info(\"{}进度:{}  {} ,当前QPS: {}\", taskDesc, getManyString(ONE, (int) (pro * LENGTH)), getPercent(pro * 100), getQPS());\n        }\n    }\n\n    /**\n     * 获取某一个时刻的QPS\n     *\n     * @return\n     */\n    private int getQPS() {\n        int qps = 0;\n        if (canCount) {\n            List<Integer> times = new ArrayList<>();\n            for (int i = 0; i < threadNum; i++) {\n                List<Integer> costs = threads.get(i).costs;\n                int size = costs.size();\n                if (size < 3) continue;\n                times.add(costs.get(size - 1));\n                times.add(costs.get(size - 2));\n            }\n            qps = times.isEmpty() ? 0 : (int) (1000 * threadNum / (times.stream().collect(Collectors.summarizingInt(x -> x)).getAverage()));\n        } else {\n            qps = excuteNum.get() / (int) (Time.getTimeStamp() - startTime);\n        }\n        qs.add(qps);\n        return qps;\n    }\n\n    /**\n     * 关闭线程,防止死循环\n     */\n    public void stop() {\n        st = false;\n        logger.info(\"{}进度:{}  {}\", taskDesc, getManyString(ONE, LENGTH), \"100%\");\n        printQPS();\n    }\n\n    /**\n     * 打印QPS变化曲线\n     */\n    private void printQPS() {\n        int size = qs.size();\n        if (size < 5) return;\n        if (size <= BUCKET_SIZE) {\n            output(StatisticsUtil.draw(qs, StringUtil.center(taskDesc + SUFFIX, size * 3)));\n        } else {\n            double v = size * 1.0 / BUCKET_SIZE;\n            List<Integer> qpss = range(BUCKET_SIZE).mapToObj(x -> qs.get((int) (x * v))).collect(Collectors.toList());\n            output(StatisticsUtil.draw(qpss, StringUtil.center(taskDesc + SUFFIX, BUCKET_SIZE * 3)));\n        }\n    }\n\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/frame/execute/StatisticsUtil.java",
    "content": "package com.funtester.frame.execute;\n\nimport com.funtester.config.Constant;\nimport com.funtester.utils.StringUtil;\nimport com.funtester.utils.Time;\nimport org.apache.commons.lang3.ArrayUtils;\nimport org.apache.commons.lang3.StringUtils;\n\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Collections;\nimport java.util.List;\nimport java.util.stream.Collectors;\n\nimport static com.funtester.frame.SourceCode.getManyString;\nimport static com.funtester.frame.SourceCode.range;\nimport static java.util.stream.Collectors.toList;\n\n/**\n * 用于性能测试数据统计工具类,主要是统计常用指标QPS,rt,时间,以及图形化输出测试结果\n *\n * <p>                  Demo\n *\n *     \t\t\t\tFunTester300线程\n *\n * \t>>响应时间分布图,横轴排序分成桶的序号,纵轴每个桶的中位数<<\n * \t\t--<中位数数据最小值为:55 ms,最大值:1255 ms>--\n *                                                                   ██\n *                                                                   ██\n *                                                                   ██\n *                                                                   ██\n *                                                                   ██\n *                                                                ▃▃ ██\n *                                                       ▃▃ ▅▅ ▇▇ ██ ██\n *                                                    ▇▇ ██ ██ ██ ██ ██\n *                                                 ▃▃ ██ ██ ██ ██ ██ ██\n *                                              ▁▁ ██ ██ ██ ██ ██ ██ ██\n *                                           ▁▁ ██ ██ ██ ██ ██ ██ ██ ██\n *                                           ██ ██ ██ ██ ██ ██ ██ ██ ██\n *                                        ▇▇ ██ ██ ██ ██ ██ ██ ██ ██ ██\n *                                     ▇▇ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██\n *                                     ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██\n *                               ▃▃ ▇▇ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██\n *                            ▃▃ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██\n *                         ▂▂ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██\n *                         ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██\n *                      ▅▅ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██\n *                   ▃▃ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██\n *       ▁▁ ▂▂ ▃▃ ▄▄ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██\n * ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██\n * </p>\n */\npublic class StatisticsUtil extends Constant {\n\n    /**\n     * 将性能测试数据图表展示,需要等宽字体显示\n     *\n     * <p>\n     * 将数据排序,然后按照循序分桶,选择桶中中位数作代码,通过二维数组转化成柱状图\n     * </p>\n     * 生成统计结果数组大小{@link com.funtester.config.Constant#BUCKET_SIZE},小于{@link com.funtester.config.Constant#BUCKET_SIZE}平方的数据量不予以统计\n     *\n     * @param data 性能测试数据,也可以其他统计数据\n     * @return\n     */\n    public static String statistics(List<Integer> data, String title, int threadNum) {\n        if (data.size() < BUCKET_SIZE * BUCKET_SIZE) return \"数据量太少,无法绘图!\";//过滤少量数据\n        List<Integer> nums = batchNums(data);\n        return draw(nums, StringUtil.center(((StringUtils.isEmpty(title)) ? DEFAULT_STRING : getTrueName(title) + SPACE_1 + (threadNum == 0 ? EMPTY : threadNum) + \" thread\"), BUCKET_SIZE * 3) + LINE + LINE + StringUtil.center(\"Response Time: x-serial num, y-median\", BUCKET_SIZE * 3) + LINE + StringUtil.center(\"min median:\" + nums.get(0) + \" ms,max:\" + nums.get(BUCKET_SIZE - 1) + \" ms\", BUCKET_SIZE * 3));\n    }\n\n    /**\n     * 根据数组画图,无序亦可\n     * <p>\n     * 此处注意title处理调用center方法的时候,需要data.size()乘以3才是正确的长度,一个长度包含一个空格和两个特殊字符\n     * </p>\n     *\n     * @param data\n     * @param title\n     * @return\n     */\n    public static String draw(int[] data, String title) {\n        List<Integer> integers = Arrays.asList(ArrayUtils.toObject(data));\n        return draw(integers, title);\n    }\n\n    /**\n     * 根据数组画图,无序亦可\n     * <p>\n     * 此处注意title处理调用center方法的时候,需要data.size()乘以3才是正确的长度,一个长度包含一个空格和两个特殊字符\n     * </p>\n     *\n     * @param data\n     * @param title\n     * @return\n     */\n    public static String draw(List<Integer> data, String title) {\n        int largest = Collections.max(data);\n        int buket = data.size();\n        String[][] map = data.stream().map(x -> getPercent(x, largest, buket)).collect(toList()).toArray(new String[buket][buket]);//转换成string二维数组\n        String[][] result = new String[buket][buket];\n        /*将二维数组反转成竖排*/\n        for (int i = 0; i < buket; i++) {\n            for (int j = 0; j < buket; j++) {\n                result[i][j] = getManyString(map[j][buket - 1 - i], 2) + SPACE_1;\n            }\n        }\n        StringBuffer table = new StringBuffer(LINE + StringUtil.center(title, buket) + LINE + LINE);\n        range(buket).forEach(x -> table.append(Arrays.asList(result[x]).stream().collect(Collectors.joining()) + LINE));\n        return table.toString();\n    }\n\n    /**\n     * 分割数组,变成可以图形化的数组\n     *\n     * @param data\n     * @return\n     */\n    public static List<Integer> batchNums(List<Integer> data) {\n        int size = data.size();//获取总数据量大小\n        Collections.sort(data);\n        return new ArrayList<Integer>() {{\n            range(1, BUCKET_SIZE + 1).forEach(x -> add(data.get(size * x / BUCKET_SIZE - size / BUCKET_SIZE / 2)));\n        }};\n    }\n\n    /**\n     * 将数据转化成string数组\n     *\n     * @param part   数据\n     * @param total  基准数据,默认最大的中位数\n     * @param length\n     * @return\n     */\n    private static String[] getPercent(int part, int total, int length) {\n        int i = part * 8 * length / total;\n        int prefix = i / 8;\n        int suffix = i % 8;\n        String s = getManyString(getPercent(8), prefix) + (prefix == length ? EMPTY : getPercent(suffix) + getManyString(SPACE_1, length - prefix - 1));\n        return s.split(EMPTY);\n    }\n\n\n    /**\n     * 统一处理压测原始数据保存文件名\n     *\n     * @param thread\n     * @param desc\n     * @return\n     */\n    public static String getFileName(int thread, String desc) {\n        return desc + CONNECTOR + thread;\n    }\n\n    /**\n     * 用于处理初始化concurrent的desc,主要处理后缀\n     *\n     * @param desc\n     * @return\n     */\n    public static String getFileName(String desc) {\n        return desc + Time.markDate();\n    }\n\n    /**\n     * 用于处理title,除去标记数字\n     *\n     * @param desc\n     * @return\n     */\n    public static String getTrueName(String desc) {\n        if (StringUtils.isEmpty(desc)) return EMPTY;\n        return desc.replaceAll(\"\\\\d{6}$\", EMPTY);\n    }\n\n\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/frame/execute/ThreadPoolUtil.groovy",
    "content": "package com.funtester.frame.execute;\n\nimport java.util.concurrent.*;\n\n/**\n * Java线程池Demo\n */\nclass ThreadPoolUtil {\n\n\n    /**\n     * 重建可变线程池\n     * corePoolSize：核心池的大小，这个参数跟后面讲述的线程池的实现原理有非常大的关系。在创建了线程池后，默认情况下，线程池中并没有任何线程，而是等待有任务到来才创建线程去执行任务，除非调用了prestartAllCoreThreads()或者prestartCoreThread()方法，从这2个方法的名字就可以看出，是预创建线程的意思，即在没有任务到来之前就创建corePoolSize个线程或者一个线程。默认情况下，在创建了线程池后，线程池中的线程数为0，当有任务来之后，就会创建一个线程去执行任务，当线程池中的线程数目达到corePoolSize后，就会把到达的任务放到缓存队列当中；\n     * maximumPoolSize：线程池最大线程数，这个参数也是一个非常重要的参数，它表示在线程池中最多能创建多少个线程；\n     * keepAliveTime：表示线程没有任务执行时最多保持多久时间会终止。默认情况下，只有当线程池中的线程数大于corePoolSize时，keepAliveTime才会起作用，直到线程池中的线程数不大于corePoolSize，即当线程池中的线程数大于corePoolSize时，如果一个线程空闲的时间达到keepAliveTime，则会终止，直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法，在线程池中的线程数不大于corePoolSize时，keepAliveTime参数也会起作用，直到线程池中的线程数为0；\n     * unit：参数keepAliveTime的时间单位，有7种取值，在TimeUnit类中有7种静态属性：\n     * workQueue：一个阻塞队列，用来存储等待执行的任务，这个参数的选择也很重要，会对线程池的运行过程产生重大影响，一般来说，这里的阻塞队列有以下几种选择：ArrayBlockingQueue;LinkedBlockingQueue; SynchronousQueue;\n     * 　　ArrayBlockingQueue和PriorityBlockingQueue使用较少，一般使用LinkedBlockingQueue和Synchronous。线程池的排队策略与BlockingQueue有关。\n     * threadFactory：线程工厂，主要用来创建线程；\n     * handler：表示当拒绝处理任务时的策略，有以下四种取值：\n     * ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。\n     * ThreadPoolExecutor.DiscardPolicy：也是丢弃任务，但是不抛出异常。\n     * ThreadPoolExecutor.DiscardOldestPolicy：丢弃队列最前面的任务，然后重新尝试执行任务（重复此过程）\n     * ThreadPoolExecutor.CallerRunsPolicy：由调用线程处理该任务\n     *\n     * @param core 核心线程数\n     * @param max 最大线程数\n     * @param liveTime 空闲时间\n     * @return\n     */\n    static ThreadPoolExecutor createPool(int core = 5, int max = 20, int liveTime = 5) {\n        return new ThreadPoolExecutor(core, max, liveTime, TimeUnit.SECONDS, new LinkedBlockingDeque<Runnable>(1000));\n\n    }\n\n    /**\n     * 定长的线程池\n     *\n     * @param size\n     * @return\n     */\n    static ExecutorService createFixedPool(int size = 10) {\n        return Executors.newFixedThreadPool(size);\n    }\n\n    /**\n     * 缓存线程池,无限长度\n     *\n     * @return\n     */\n    static ExecutorService createCachePool() {\n        return Executors.newCachedThreadPool();\n    }\n\n/*获取线程安全的单例的线程池\nstatic ThreadPoolExecutor getSingleThreadPoolExecutor(AtomicInteger atomicInteger) {\n        if (singleThreadPoolExecutor == null){\n            synchronized (objectLock){\n                if (singleThreadPoolExecutor == null){\n                    singleThreadPoolExecutor = new ThreadPoolExecutor(1, 1, 5,\n                            TimeUnit.SECONDS, new LinkedBlockingQueue<Runnable>(100),\n                            new ThreadFactory() {\n                                @Override\n                                Thread newThread(Runnable runnable) {\n                                    Thread thread = new Thread(runnable);\n                                    thread.setName(\"UserCenter-business-\" + atomicInteger.getAndIncrement());\n                                    return thread;\n                                }\n                            },\n                        new ThreadPoolExecutor.CallerRunsPolicy());\n                }\n            }\n        }\n        return singleThreadPoolExecutor;\n    }\n    */\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/frame/thread/FixedQpsHeaderMark.groovy",
    "content": "package com.funtester.frame.thread\n\nimport com.funtester.base.constaint.ThreadBase\nimport com.funtester.base.exception.ParamException\nimport com.funtester.base.interfaces.MarkRequest\nimport com.funtester.frame.SourceCode\nimport org.apache.http.client.methods.HttpRequestBase\n\nimport java.util.concurrent.atomic.AtomicInteger\n\n/**\n * 针对固定QPS模式的多线程对象的标记类\n */\nclass FixedQpsHeaderMark extends SourceCode implements MarkRequest, Cloneable, Serializable {\n\n    private static final long serialVersionUID = -158942567078477L;\n\n    private static volatile AtomicInteger num = new AtomicInteger(10000);\n\n    String headerName;\n\n    @Override\n    String mark(ThreadBase threadBase) {\n        if (threadBase instanceof RequestTimesFixedQps) {\n            RequestTimesFixedQps req = (RequestTimesFixedQps) threadBase;\n            return mark(req.t);\n        } else if (threadBase instanceof RequestTimeFixedQps) {\n            RequestThreadTimes req = (RequestTimeFixedQps) threadBase;\n            return mark(req.t);\n        } else {\n            ParamException.fail(threadBase.getClass().toString());\n        }\n        EMPTY;\n    }\n\n    /**\n     * 标记请求\n     *\n     * @param base\n     * @return\n     */\n    @Override\n    String mark(HttpRequestBase base) {\n        base.removeHeaders(headerName);\n        String value = 8 + EMPTY + num.getAndIncrement();\n        base.addHeader(headerName, value);\n        value;\n    }\n\n    @Override\n    FixedQpsHeaderMark clone() {\n        new FixedQpsHeaderMark(headerName);\n    }\n\n    FixedQpsHeaderMark(String headerName) {\n        this.headerName = headerName;\n    }\n\n    private FixedQpsHeaderMark() {\n\n    }\n\n}"
  },
  {
    "path": "src/main/groovy/com/funtester/frame/thread/FixedQpsParamMark.java",
    "content": "package com.funtester.frame.thread;\n\nimport com.funtester.base.constaint.ThreadBase;\nimport com.funtester.base.interfaces.MarkThread;\nimport com.funtester.frame.SourceCode;\n\nimport java.io.Serializable;\nimport java.util.concurrent.atomic.AtomicInteger;\n\n/**\n * 用于非单纯的http请求以及非HTTP请求,没有httprequestbase对象的标记方法,自己实现的虚拟类,可用户标记header固定字段或者随机参数,使用T作为参数载体,目前只能使用在T为string类才行\n */\npublic class FixedQpsParamMark extends SourceCode implements MarkThread, Cloneable, Serializable {\n\n    private static final long serialVersionUID = 2135701056209833015L;\n\n    public static AtomicInteger num = new AtomicInteger(10000);\n\n    /**\n     * 用于标记执行线程\n     */\n    String name;\n\n    @Override\n    public String mark(ThreadBase threadBase) {\n        return name + num.getAndIncrement();\n    }\n\n    @Override\n    public FixedQpsParamMark clone() {\n        FixedQpsParamMark paramMark = new FixedQpsParamMark(this.name);\n        return paramMark;\n    }\n\n    private FixedQpsParamMark() {\n        name = EMPTY;\n    }\n\n    public FixedQpsParamMark(String name) {\n        this();\n        this.name = name;\n    }\n\n\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/frame/thread/HeaderMark.java",
    "content": "package com.funtester.frame.thread;\n\nimport com.funtester.base.constaint.ThreadBase;\nimport com.funtester.base.exception.ParamException;\nimport com.funtester.base.interfaces.MarkRequest;\nimport com.funtester.frame.SourceCode;\nimport com.funtester.utils.StringUtil;\nimport org.apache.http.client.methods.HttpRequestBase;\n\nimport java.io.Serializable;\nimport java.util.concurrent.atomic.AtomicInteger;\n\npublic class HeaderMark extends SourceCode implements MarkRequest, Cloneable, Serializable {\n\n    private static final long serialVersionUID = -1595942567071153477L;\n\n    public static AtomicInteger threadName = new AtomicInteger(getRandomIntRange(1000, 9000));\n\n    String headerName;\n\n    private String m;\n\n    int num = getRandomIntRange(100, 999) * 1000;\n\n    @Override\n    public String mark(ThreadBase threadBase) {\n        if (threadBase instanceof RequestThreadTime) {\n            RequestThreadTime req = (RequestThreadTime) threadBase;\n            return mark(req.f);\n        } else if (threadBase instanceof RequestThreadTimes) {\n            RequestThreadTimes req = (RequestThreadTimes) threadBase;\n            return mark(req.f);\n        } else {\n            ParamException.fail(threadBase.getClass().toString());\n        }\n        return EMPTY;\n    }\n\n    /**\n     * 标记请求\n     *\n     * @param base\n     * @return\n     */\n    @Override\n    public String mark(HttpRequestBase base) {\n        base.removeHeaders(headerName);\n        String value = m + num++;\n        base.addHeader(headerName, value);\n        return value;\n    }\n\n    @Override\n    public HeaderMark clone() {\n        return new HeaderMark(headerName);\n    }\n\n    public HeaderMark(String headerName) {\n        this.headerName = headerName;\n        this.m = DEFAULT_STRING + StringUtil.getStringWithoutNum(4) + threadName.getAndIncrement();\n    }\n\n    public HeaderMark() {\n\n    }\n\n\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/frame/thread/ParamMark.java",
    "content": "package com.funtester.frame.thread;\n\nimport com.funtester.base.constaint.ThreadBase;\nimport com.funtester.base.interfaces.MarkThread;\nimport com.funtester.frame.SourceCode;\n\nimport java.io.Serializable;\nimport java.util.concurrent.atomic.AtomicInteger;\n\n/**\n * 用于非单纯的http请求以及非HTTP请求,没有httprequestbase对象的标记方法,自己实现的虚拟类,可用户标记header固定字段或者随机参数,使用T作为参数载体,目前只能使用在T为string类才行\n */\npublic class ParamMark extends SourceCode implements MarkThread, Cloneable, Serializable {\n\n    private static final long serialVersionUID = -5532592151245141262L;\n\n    public static AtomicInteger threadName = new AtomicInteger(getRandomIntRange(1000, 9000));\n\n    /**\n     * 用于标记执行线程\n     */\n    String name;\n\n    int num = getRandomIntRange(100, 999) * 1000;\n\n    @Override\n    public String mark(ThreadBase threadBase) {\n        return name + num++;\n    }\n\n    @Override\n    public ParamMark clone() {\n        ParamMark paramMark = new ParamMark();\n        return paramMark;\n    }\n\n    public ParamMark() {\n        this.name = threadName.getAndIncrement() + EMPTY;\n    }\n\n    public ParamMark(String name) {\n        this();\n        this.name = name;\n    }\n\n\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/frame/thread/QuerySqlThread.java",
    "content": "package com.funtester.frame.thread;\n\nimport com.funtester.base.constaint.ThreadBase;\nimport com.funtester.base.constaint.ThreadLimitTimesCount;\nimport com.funtester.base.interfaces.IMySqlBasic;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.sql.SQLException;\n\n/**\n * 数据库多线程类，query方法类，区别于updatethread\n */\npublic class QuerySqlThread extends ThreadLimitTimesCount {\n\n    private static final long serialVersionUID = 879371247008746883L;\n\n    private static Logger logger = LogManager.getLogger(QuerySqlThread.class);\n\n    String sql;\n\n    IMySqlBasic base;\n\n    public QuerySqlThread(IMySqlBasic base, String sql, int times) {\n        this.times = times;\n        this.sql = sql;\n        this.base = base;\n    }\n\n    @Override\n    public void before() {\n        base.getConnection();\n    }\n\n    @Override\n    protected void doing() throws SQLException {\n        base.executeQuerySql(sql);\n    }\n\n    @Override\n    protected void after() {\n        super.after();\n        base.mySqlOver();\n    }\n\n    @Override\n    public ThreadBase clone() {\n        return null;\n    }\n\n\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/frame/thread/RequestThreadTime.java",
    "content": "package com.funtester.frame.thread;\n\nimport com.funtester.base.constaint.ThreadLimitTimeCount;\nimport com.funtester.base.interfaces.MarkThread;\nimport com.funtester.httpclient.FunLibrary;\nimport com.funtester.httpclient.FunRequest;\nimport com.funtester.httpclient.GCThread;\nimport org.apache.http.client.methods.HttpRequestBase;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\n/**\n * http请求多线程类\n */\npublic class RequestThreadTime extends ThreadLimitTimeCount<HttpRequestBase> {\n\n    private static final long serialVersionUID = -6554503654885966097L;\n\n    private static Logger logger = LogManager.getLogger(RequestThreadTime.class);\n\n    /**\n     * 单请求多线程多次任务构造方法\n     *\n     * @param request 被执行的请求\n     * @param time    每个线程运行的次数\n     */\n    public RequestThreadTime(HttpRequestBase request, int time) {\n        super(request, time, null);\n    }\n\n    /**\n     * @param request 被执行的请求\n     * @param time    执行时间\n     * @param mark    标记类对象\n     */\n    public RequestThreadTime(HttpRequestBase request, int time, MarkThread mark) {\n        super(request, time, mark);\n    }\n\n    protected RequestThreadTime() {\n        super();\n    }\n\n    @Override\n    public void before() {\n        super.before();\n        GCThread.starts();\n    }\n\n    @Override\n    protected void doing() throws Exception {\n        FunLibrary.executeSimlple(f);\n    }\n\n\n    @Override\n    public RequestThreadTime clone() {\n        RequestThreadTime threadTime = new RequestThreadTime();\n        threadTime.time = this.time;\n        threadTime.f = FunRequest.cloneRequest(f);\n        threadTime.mark = mark == null ? null : mark.clone();\n        return threadTime;\n    }\n\n\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/frame/thread/RequestThreadTimes.java",
    "content": "package com.funtester.frame.thread;\n\nimport com.funtester.base.constaint.ThreadLimitTimesCount;\nimport com.funtester.base.interfaces.MarkThread;\nimport com.funtester.httpclient.FunLibrary;\nimport com.funtester.httpclient.FunRequest;\nimport com.funtester.httpclient.GCThread;\nimport org.apache.http.client.methods.HttpRequestBase;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\n/**\n * http请求多线程类\n */\npublic class RequestThreadTimes extends ThreadLimitTimesCount<HttpRequestBase> {\n\n    private static final long serialVersionUID = 84690314667174004L;\n\n    private static Logger logger = LogManager.getLogger(RequestThreadTimes.class);\n\n    /**\n     * 单请求多线程多次任务构造方法\n     *\n     * @param request 被执行的请求\n     * @param times   每个线程运行的次数\n     */\n    public RequestThreadTimes(HttpRequestBase request, int times) {\n        super(request, times, null);\n    }\n\n    /**\n     * 应对对每个请求进行标记的情况\n     *\n     * @param request\n     * @param times\n     * @param mark\n     */\n    public RequestThreadTimes(HttpRequestBase request, int times, MarkThread mark) {\n        super(request, times, mark);\n    }\n\n    protected RequestThreadTimes() {\n        super();\n    }\n\n    @Override\n    public void before() {\n        super.before();\n        GCThread.starts();\n    }\n\n    /**\n     * @throws Exception\n     */\n    @Override\n    protected void doing() throws Exception {\n        FunLibrary.executeSimlple(f);\n    }\n\n    @Override\n    public RequestThreadTimes clone() {\n        RequestThreadTimes threadTimes = new RequestThreadTimes();\n        threadTimes.times = this.times;\n        threadTimes.f = FunRequest.cloneRequest(f);\n        threadTimes.mark = mark == null ? null : mark.clone();\n        return threadTimes;\n    }\n\n\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/frame/thread/RequestTimeFixedQps.java",
    "content": "package com.funtester.frame.thread;\n\nimport com.funtester.base.constaint.FixedQpsThread;\nimport com.funtester.base.interfaces.MarkRequest;\nimport com.funtester.httpclient.FunLibrary;\nimport com.funtester.httpclient.FunRequest;\nimport com.funtester.httpclient.GCThread;\nimport org.apache.http.client.methods.HttpRequestBase;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\npublic class RequestTimeFixedQps extends FixedQpsThread<HttpRequestBase> {\n\n    private static final long serialVersionUID = -64206522585960792L;\n\n    private static Logger logger = LogManager.getLogger(RequestTimeFixedQps.class);\n\n    private RequestTimeFixedQps() {\n\n    }\n\n    public RequestTimeFixedQps(int qps, int time, MarkRequest markRequest, HttpRequestBase request) {\n        super(request, time, qps, markRequest, false);\n    }\n\n    @Override\n    public void before() {\n        super.before();\n        GCThread.starts();\n    }\n\n    @Override\n    protected void doing() throws Exception {\n        FunLibrary.executeSimlple(f);\n    }\n\n    @Override\n    public RequestTimeFixedQps clone() {\n        RequestTimeFixedQps newone = new RequestTimeFixedQps();\n        newone.f = FunRequest.cloneRequest(this.f);\n        newone.mark = this.mark == null ? null : this.mark.clone();\n        newone.qps = this.qps;\n        newone.isTimesMode = this.isTimesMode;\n        newone.limit = this.limit;\n        return newone;\n    }\n\n\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/frame/thread/RequestTimesFixedQps.java",
    "content": "package com.funtester.frame.thread;\n\nimport com.funtester.base.constaint.FixedQpsThread;\nimport com.funtester.base.interfaces.MarkRequest;\nimport com.funtester.httpclient.FunLibrary;\nimport com.funtester.httpclient.FunRequest;\nimport com.funtester.httpclient.GCThread;\nimport org.apache.http.client.methods.HttpRequestBase;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\npublic class RequestTimesFixedQps extends FixedQpsThread<HttpRequestBase> {\n\n    private static final long serialVersionUID = 679065222134424087L;\n\n    private static Logger logger = LogManager.getLogger(RequestTimesFixedQps.class);\n\n    private RequestTimesFixedQps() {\n\n    }\n\n    public RequestTimesFixedQps(int qps, int times, MarkRequest markRequest, HttpRequestBase request) {\n        super(request, times, qps, markRequest, true);\n    }\n\n    @Override\n    public void before() {\n        super.before();\n        GCThread.starts();\n    }\n\n    @Override\n    protected void doing() throws Exception {\n        FunLibrary.executeSimlple(f);\n    }\n\n    @Override\n    public RequestTimesFixedQps clone() {\n        RequestTimesFixedQps newone = new RequestTimesFixedQps();\n        newone.f = FunRequest.cloneRequest(this.f);\n        newone.mark = this.mark == null ? null : this.mark.clone();\n        newone.qps = this.qps;\n        newone.isTimesMode = this.isTimesMode;\n        newone.limit = this.limit;\n        return newone;\n    }\n\n\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/frame/thread/UpdateSqlThread.java",
    "content": "package com.funtester.frame.thread;\n\nimport com.funtester.base.constaint.ThreadBase;\nimport com.funtester.base.constaint.ThreadLimitTimesCount;\nimport com.funtester.base.interfaces.IMySqlBasic;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\n/**\n * 数据库多线程类,update方法类，区别于querythread\n */\npublic class UpdateSqlThread extends ThreadLimitTimesCount<String> {\n\n    private static final long serialVersionUID = 5808571085138930143L;\n\n    private static Logger logger = LogManager.getLogger(UpdateSqlThread.class);\n\n    IMySqlBasic base;\n\n    public UpdateSqlThread(IMySqlBasic base, String sql, int times) {\n        this.times = times;\n        this.f = sql;\n        this.base = base;\n    }\n\n    @Override\n    protected void doing() {\n        base.executeUpdateSql(f);\n    }\n\n    @Override\n    protected void after() {\n        super.after();\n        base.mySqlOver();\n    }\n\n    @Override\n    public ThreadBase clone() {\n        return null;\n    }\n\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/httpclient/ClientManage.java",
    "content": "package com.funtester.httpclient;\n\nimport com.funtester.base.exception.FailException;\nimport com.funtester.config.Constant;\nimport com.funtester.config.HttpClientConstant;\nimport com.funtester.utils.Regex;\nimport org.apache.commons.lang3.StringUtils;\nimport org.apache.http.HttpEntityEnclosingRequest;\nimport org.apache.http.HttpHost;\nimport org.apache.http.NoHttpResponseException;\nimport org.apache.http.client.HttpRequestRetryHandler;\nimport org.apache.http.client.config.CookieSpecs;\nimport org.apache.http.client.config.RequestConfig;\nimport org.apache.http.client.methods.HttpUriRequest;\nimport org.apache.http.client.protocol.HttpClientContext;\nimport org.apache.http.config.ConnectionConfig;\nimport org.apache.http.config.MessageConstraints;\nimport org.apache.http.config.Registry;\nimport org.apache.http.config.RegistryBuilder;\nimport org.apache.http.conn.socket.ConnectionSocketFactory;\nimport org.apache.http.conn.socket.PlainConnectionSocketFactory;\nimport org.apache.http.conn.ssl.SSLConnectionSocketFactory;\nimport org.apache.http.impl.client.CloseableHttpClient;\nimport org.apache.http.impl.client.HttpClients;\nimport org.apache.http.impl.conn.PoolingHttpClientConnectionManager;\nimport org.apache.http.impl.nio.client.CloseableHttpAsyncClient;\nimport org.apache.http.impl.nio.client.HttpAsyncClients;\nimport org.apache.http.impl.nio.conn.PoolingNHttpClientConnectionManager;\nimport org.apache.http.impl.nio.reactor.DefaultConnectingIOReactor;\nimport org.apache.http.impl.nio.reactor.IOReactorConfig;\nimport org.apache.http.nio.reactor.ConnectingIOReactor;\nimport org.apache.http.nio.reactor.IOReactorException;\nimport org.apache.http.protocol.HttpContext;\nimport org.apache.http.protocol.HttpCoreContext;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport javax.net.ssl.SSLContext;\nimport javax.net.ssl.SSLException;\nimport javax.net.ssl.TrustManager;\nimport javax.net.ssl.X509TrustManager;\nimport java.io.IOException;\nimport java.io.InterruptedIOException;\nimport java.net.SocketException;\nimport java.net.UnknownHostException;\nimport java.nio.charset.CodingErrorAction;\nimport java.security.KeyManagementException;\nimport java.security.NoSuchAlgorithmException;\nimport java.security.cert.CertificateException;\nimport java.util.concurrent.TimeUnit;\n\n/**\n * 连接池管理类\n */\npublic class ClientManage {\n\n    private static Logger logger = LogManager.getLogger(ClientManage.class);\n\n    /**\n     * ssl验证\n     */\n    private static SSLContext sslContext = createIgnoreVerifySSL();\n\n    /**\n     * 请求超时控制器\n     */\n    private static RequestConfig requestConfig = getRequestConfig();\n\n    /**\n     * 请求重试管理器\n     */\n    private static HttpRequestRetryHandler httpRequestRetryHandler = getHttpRequestRetryHandler();\n\n    /**\n     * 连接池\n     */\n    private static PoolingHttpClientConnectionManager connManager = getPool();\n\n    /**\n     * 异步连接池\n     */\n    private static PoolingNHttpClientConnectionManager NconnManager = getNPool();\n\n    /**\n     * httpclient对象\n     */\n    public static CloseableHttpClient httpsClient = getCloseableHttpsClients();\n\n    /**\n     * 异步连接池\n     */\n    public static CloseableHttpAsyncClient httpAsyncClient = getCloseableHttpAsyncClient();\n\n    /**\n     * 获取连接池\n     *\n     * @return\n     */\n    private static PoolingHttpClientConnectionManager getPool() {\n        PoolingHttpClientConnectionManager connManager = null;\n        // 采用绕过验证的方式处理https请求\n        // 设置协议http和https对应的处理socket链接工厂的对象\n        Registry<ConnectionSocketFactory> socketFactoryRegistry = RegistryBuilder.<ConnectionSocketFactory>create().register(\"http\", PlainConnectionSocketFactory.INSTANCE).register(\"https\", new SSLConnectionSocketFactory(sslContext)).build();\n        connManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry);\n        // 消息约束\n        MessageConstraints messageConstraints = MessageConstraints.custom().setMaxHeaderCount(HttpClientConstant.MAX_HEADER_COUNT).setMaxLineLength(HttpClientConstant.MAX_LINE_LENGTH).build();\n        // 连接设置\n        ConnectionConfig connectionConfig = ConnectionConfig.custom().setMalformedInputAction(CodingErrorAction.IGNORE).setUnmappableInputAction(CodingErrorAction.IGNORE).setCharset(Constant.DEFAULT_CHARSET).setMessageConstraints(messageConstraints).build();\n        connManager.setDefaultConnectionConfig(connectionConfig);\n        connManager.setMaxTotal(HttpClientConstant.MAX_TOTAL_CONNECTION);\n        connManager.setDefaultMaxPerRoute(HttpClientConstant.MAX_PER_ROUTE_CONNECTION);\n        return connManager;\n    }\n\n    /**\n     * 获取异步连接池\n     *\n     * @return\n     */\n    private static PoolingNHttpClientConnectionManager getNPool() {\n        IOReactorConfig ioReactorConfig = IOReactorConfig.custom().setIoThreadCount(Runtime.getRuntime().availableProcessors()).setConnectTimeout(HttpClientConstant.CONNECT_REQUEST_TIMEOUT).setSoTimeout(HttpClientConstant.SOCKET_TIMEOUT).build();\n        ConnectingIOReactor ioReactor = null;\n        try {\n            ioReactor = new DefaultConnectingIOReactor(ioReactorConfig);\n        } catch (IOReactorException e) {\n            logger.error(\"创建连接响应器失败!\", e);\n        }\n        MessageConstraints messageConstraints = MessageConstraints.custom().setMaxHeaderCount(HttpClientConstant.MAX_HEADER_COUNT).setMaxLineLength(HttpClientConstant.MAX_LINE_LENGTH).build();\n        PoolingNHttpClientConnectionManager connManager = new PoolingNHttpClientConnectionManager(ioReactor);\n        ConnectionConfig connectionConfig = ConnectionConfig.custom().setMalformedInputAction(CodingErrorAction.IGNORE).setUnmappableInputAction(CodingErrorAction.IGNORE).setCharset(Constant.DEFAULT_CHARSET).setMessageConstraints(messageConstraints).build();\n        connManager.setDefaultConnectionConfig(connectionConfig);\n        connManager.setMaxTotal(HttpClientConstant.MAX_TOTAL_CONNECTION);\n        connManager.setDefaultMaxPerRoute(HttpClientConstant.MAX_PER_ROUTE_CONNECTION);\n        return connManager;\n    }\n\n    /**\n     * 获取SSL套接字对象 重点重点：设置tls协议的版本\n     *\n     * @return\n     */\n    private static SSLContext createIgnoreVerifySSL() {\n        SSLContext sslContext = null;// 创建套接字对象\n        try {\n            sslContext = SSLContext.getInstance(HttpClientConstant.SSL_VERSION);// 指定TLS版本\n        } catch (NoSuchAlgorithmException e) {\n            FailException.fail(\"创建套接字失败！\" + e.getMessage());\n        }\n        // 实现X509TrustManager接口，用于绕过验证\n        X509TrustManager trustManager = new X509TrustManager() {\n            @Override\n            public void checkClientTrusted(java.security.cert.X509Certificate[] paramArrayOfX509Certificate,\n                                           String paramString) throws CertificateException {\n            }\n\n            @Override\n            public void checkServerTrusted(java.security.cert.X509Certificate[] paramArrayOfX509Certificate,\n                                           String paramString) throws CertificateException {\n            }\n\n            @Override\n            public java.security.cert.X509Certificate[] getAcceptedIssuers() {\n                return null;\n            }\n        };\n        try {\n            sslContext.init(null, new TrustManager[]{trustManager}, null);// 初始化sslContext对象\n        } catch (KeyManagementException e) {\n            logger.warn(\"初始化套接字失败！\", e);\n        }\n        return sslContext;\n    }\n\n    /**\n     * 获取重试控制器\n     *\n     * @return\n     */\n    private static HttpRequestRetryHandler getHttpRequestRetryHandler() {\n        return new HttpRequestRetryHandler() {\n            public boolean retryRequest(IOException exception, int executionCount, HttpContext context) {\n                boolean log = log(exception, executionCount, context);\n                if (log) logger.warn(\"请求发生重试! 次数: {}\", executionCount);\n                return log;\n            }\n\n            /**绕一圈,记录重试信息,避免错误日志影响观感\n             * @param exception\n             * @param executionCount\n             * @param context\n             * @return\n             */\n            private boolean log(IOException exception, int executionCount, HttpContext context) {\n                if (executionCount + 1 > HttpClientConstant.TRY_TIMES) return false;\n                logger.warn(\"请求发生错误:{}\", exception.getMessage());\n                HttpClientContext clientContext = HttpClientContext.adapt(context);\n                final Object request = clientContext.getAttribute(HttpCoreContext.HTTP_REQUEST);\n                if (request instanceof HttpUriRequest) {\n                    HttpUriRequest uriRequest = (HttpUriRequest) request;\n                    logger.warn(\"请求失败接口URI:{}\", uriRequest.getURI().toString());\n                }\n                if (exception instanceof NoHttpResponseException) {\n                    return false;\n                } else if (exception instanceof InterruptedIOException) {\n                    return true;\n                } else if (exception instanceof UnknownHostException) {\n                    return false;\n                } else if (exception instanceof SSLException) {\n                    return false;\n                } else if (exception instanceof SocketException) {\n                    return false;\n                } else {\n                    logger.warn(\"未记录的请求异常:{}\", exception.getClass().getName());\n                }\n                // 如果请求是幂等的，则不重试,HttpEntityEnclosingRequest类以及子类都是非幂等性的\n                if (!(request instanceof HttpEntityEnclosingRequest)) {\n                    return false;\n                }\n                return true;\n            }\n        };\n    }\n\n    /**\n     * 通过连接池获取https协议请求对象\n     * <p>\n     * 增加默认的请求控制器，和请求配置，连接控制器，取消了cookiestore，单独解析响应set-cookie和发送请求的header，适配多用户同时在线的情况\n     * </p>\n     *\n     * @return\n     */\n    private static CloseableHttpAsyncClient getCloseableHttpAsyncClient() {\n        return HttpAsyncClients.custom().setConnectionManager(NconnManager).setSSLHostnameVerifier(SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER).setSSLContext(sslContext).build();\n    }\n\n    private static CloseableHttpClient getCloseableHttpsClients() {\n        return HttpClients.custom().setConnectionManager(connManager).setRetryHandler(httpRequestRetryHandler).setDefaultRequestConfig(requestConfig).build();\n    }\n\n    /**\n     * 获取请求超时控制器\n     * <p>\n     * cookieSpec:即cookie策略。参数为cookiespecs的一些字段。作用：\n     * 1、如果网站header中有set-cookie字段时，采用默认方式可能会被cookie reject，无法写入cookie。将此属性设置成CookieSpecs.STANDARD_STRICT可避免此情况。\n     * 2、如果要想忽略cookie访问，则将此属性设置成CookieSpecs.IGNORE_COOKIES。\n     * </p>\n     *\n     * @return\n     */\n    private static RequestConfig getRequestConfig() {\n        return RequestConfig.custom().setConnectionRequestTimeout(HttpClientConstant.CONNECT_REQUEST_TIMEOUT).setConnectTimeout(HttpClientConstant.CONNECT_TIMEOUT).setSocketTimeout(HttpClientConstant.SOCKET_TIMEOUT).setCookieSpec(CookieSpecs.IGNORE_COOKIES).setRedirectsEnabled(false).build();\n    }\n\n    /**\n     * 获取代理配置项\n     *\n     * @param ip\n     * @param port\n     * @return\n     */\n    public static RequestConfig getProxyRequestConfig(String ip, int port) {\n        return RequestConfig.custom().setConnectionRequestTimeout(HttpClientConstant.CONNECT_REQUEST_TIMEOUT).setConnectTimeout(HttpClientConstant.CONNECT_TIMEOUT).setSocketTimeout(HttpClientConstant.SOCKET_TIMEOUT).setCookieSpec(CookieSpecs.IGNORE_COOKIES).setRedirectsEnabled(false).setProxy(new HttpHost(ip, port)).build();\n    }\n\n\n    /**\n     * 回收资源方法，关闭过期连接，关闭超时连接，用于另起线程回收连接池连接\n     */\n    public static void recyclingConnection() {\n        connManager.closeExpiredConnections();\n        connManager.closeIdleConnections(HttpClientConstant.IDLE_TIMEOUT, TimeUnit.MILLISECONDS);\n    }\n\n    /**\n     * 重新初始化连接池,用于临时改变超时和超时标准线的重置\n     * <p>\n     * 会重置请求控制器,重置连接池和重试控制器\n     * </p>\n     * 时间单位s,默认配置单位ms,自动乘以1000\n     *\n     * @param timeout\n     * @param accepttime\n     * @param retrytimes\n     * @param ip\n     * @param port\n     */\n    public static void init(int timeout, int accepttime, int retrytimes, String ip, int port) {\n        HttpClientConstant.CONNECT_REQUEST_TIMEOUT = timeout * 1000;\n        HttpClientConstant.CONNECT_TIMEOUT = timeout * 1000;\n        HttpClientConstant.SOCKET_TIMEOUT = timeout * 1000;\n        HttpClientConstant.MAX_ACCEPT_TIME = accepttime * 1000;\n        HttpClientConstant.TRY_TIMES = retrytimes < 1 ? Constant.TEST_ERROR_CODE : retrytimes;\n        requestConfig = StringUtils.isNoneBlank(ip) && Regex.isMatch(ip + \":\" + port, Constant.HOST_REGEX) ? getProxyRequestConfig(ip, port) : getRequestConfig();\n        httpsClient = getCloseableHttpsClients();\n        httpRequestRetryHandler = getHttpRequestRetryHandler();\n    }\n\n\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/httpclient/FunLibrary.java",
    "content": "package com.funtester.httpclient;\n\nimport com.alibaba.fastjson.JSONException;\nimport com.alibaba.fastjson.JSONObject;\nimport com.funtester.base.bean.RequestInfo;\nimport com.funtester.base.exception.ParamException;\nimport com.funtester.base.exception.RequestException;\nimport com.funtester.base.interfaces.IBase;\nimport com.funtester.config.Constant;\nimport com.funtester.config.HttpClientConstant;\nimport com.funtester.db.mysql.MySqlTest;\nimport com.funtester.frame.SourceCode;\nimport com.funtester.utils.DecodeEncode;\nimport com.funtester.utils.Regex;\nimport com.funtester.utils.Time;\nimport com.funtester.utils.message.AlertOver;\nimport org.apache.commons.lang3.StringUtils;\nimport org.apache.http.*;\nimport org.apache.http.client.config.RequestConfig;\nimport org.apache.http.client.entity.UrlEncodedFormEntity;\nimport org.apache.http.client.methods.CloseableHttpResponse;\nimport org.apache.http.client.methods.HttpGet;\nimport org.apache.http.client.methods.HttpPost;\nimport org.apache.http.client.methods.HttpRequestBase;\nimport org.apache.http.entity.ContentType;\nimport org.apache.http.entity.StringEntity;\nimport org.apache.http.entity.mime.MultipartEntityBuilder;\nimport org.apache.http.entity.mime.content.StringBody;\nimport org.apache.http.message.BasicHeader;\nimport org.apache.http.message.BasicNameValuePair;\nimport org.apache.http.util.EntityUtils;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.io.*;\nimport java.util.ArrayList;\nimport java.util.Arrays;\nimport java.util.Iterator;\nimport java.util.List;\nimport java.util.concurrent.ExecutionException;\nimport java.util.concurrent.Future;\nimport java.util.stream.Collectors;\n\n/**\n * 请求相关类，采用统一的静态方法，在登录后台管理页面是自动化设置cookie，其他公参由各自的base类实现header\n */\npublic class FunLibrary extends SourceCode {\n\n    private static Logger logger = LogManager.getLogger(FunLibrary.class);\n\n    /**\n     * ibase实现类，需要用来校验响应是否正确的响应体，获取响应的code码，code码默认-2，对于不同的项目ibase的isright方法不一样\n     */\n    private static IBase iBase;\n\n    /**\n     * 最近发送的请求\n     */\n    private static HttpRequestBase lastRequest;\n\n    /**\n     * 是否保存请求和响应\n     */\n    public static boolean SAVE_KEY = false;\n\n    /**\n     * 方法已重载，获取get对象\n     * <p>方法重载，主要区别参数，会自动进行urlencode操作</p>\n     *\n     * @param url  表示请求地址\n     * @param args 表示传入数据\n     * @return 返回get对象\n     */\n    public static HttpGet getHttpGet(String url, JSONObject args) {\n        if (args == null || args.size() == 0) return getHttpGet(url);\n        String uri = url + changeJsonToArguments(args);\n        return getHttpGet(uri);\n    }\n\n    /**\n     * 方法已重载，获取get对象\n     * <p>方法重载，主要区别参数，会自动进行urlencode操作</p>\n     *\n     * @param url 表示请求地址\n     * @return 返回get对象\n     */\n    public static HttpGet getHttpGet(String url) {\n        return new HttpGet(url.replace(SPACE_1, EMPTY));\n    }\n\n    /**\n     * 获取post对象，以form表单提交数据\n     * <p>方法重载，文字信息form表单提交，文件信息二进制流提交，具体参照文件上传的方法主食，post请求可以不需要参数，暂时不支持其他参数类型，如果是公参需要在url里面展示，需要传一个json对象，一般默认args为get公参，params为post请求参数</p>\n     * 请求header参数类型为{@link HttpClientConstant#ContentType_FORM}\n     *\n     * @param url    请求地址\n     * @param params 请求数据，form表单形式设置请求实体\n     * @return 返回post对象\n     */\n    public static HttpPost getHttpPost(String url, JSONObject params) {\n        HttpPost httpPost = getHttpPost(url);\n        setFormHttpEntity(httpPost, params);\n        httpPost.addHeader(HttpClientConstant.ContentType_FORM);\n        return httpPost;\n    }\n\n    /**\n     * 获取httppost对象，没有参数设置\n     * <p>方法重载，文字信息form表单提交，文件信息二进制流提交，具体参照文件上传的方法主食，post请求可以不需要参数，暂时不支持其他参数类型，如果是公参需要在url里面展示，需要传一个json对象，一般默认args为get公参，params为post请求参数</p>\n     *\n     * @param url\n     * @return\n     */\n    public static HttpPost getHttpPost(String url) {\n        return new HttpPost(url.replace(SPACE_1, EMPTY));\n    }\n\n    /**\n     * 获取httppost对象，json格式对象，传参时手动tostring\n     * <p>新重载方法，适应post请求json传参，估计utf-8编码格式</p>\n     * 请求header参数类型为{@link HttpClientConstant#ContentType_JSON}\n     *\n     * @param url\n     * @param params\n     * @return\n     */\n    public static HttpPost getHttpPost(String url, String params) {\n        HttpPost httpPost = getHttpPost(url);\n        httpPost.setEntity(new StringEntity(params, DEFAULT_CHARSET.toString()));\n        httpPost.addHeader(HttpClientConstant.ContentType_JSON);\n        return httpPost;\n    }\n\n    /**\n     * * 获取httppost对象，json格式对象，传参时手动tostring\n     * <p>新重载方法，适应post请求json传参</p>\n     * 请求header参数类型为{@link HttpClientConstant#ContentType_JSON}\n     *\n     * @param url\n     * @param args\n     * @return\n     */\n    public static HttpPost getHttpPost(String url, JSONObject args, String params) {\n        return getHttpPost(url + changeJsonToArguments(args), params);\n    }\n\n    /**\n     * 获取 httppost 请求对象\n     * <p>方法重载，文字信息form表单提交，文件信息二进制流提交，具体参照文件上传的方法主食，post请求可以不需要参数，暂时不支持其他参数类型，如果是公参需要在url里面展示，需要传一个json对象，一般默认args为get公参，params为post请求参数</p>\n     *\n     * @param url    请求地址\n     * @param args   请求地址参数\n     * @param params 请求参数\n     * @return\n     */\n    public static HttpPost getHttpPost(String url, JSONObject args, JSONObject params) {\n        return getHttpPost(url + changeJsonToArguments(args), params);\n    }\n\n\n    /**\n     * 获取 httpPost 对象\n     * <p>方法重载，文字信息form表单提交，文件信息二进制流提交，具体参照文件上传的方法主食，post请求可以不需要参数，暂时不支持其他参数类型，如果是公参需要在url里面展示，需要传一个json对象，一般默认args为get公参，params为post请求参数</p>\n     *\n     * @param url    请求地址\n     * @param args   请求通用参数\n     * @param params 请求参数，其中二进制流必须是 file\n     * @param file   文件\n     * @return\n     */\n    public static HttpPost getHttpPost(String url, JSONObject args, JSONObject params, File file) {\n        return getHttpPost(url + changeJsonToArguments(args), params, file);\n    }\n\n    /**\n     * 获取 httpPost 对象\n     * <p>方法重载，文字信息form表单提交，文件信息二进制流提交，具体参照文件上传的方法主食，post请求可以不需要参数，暂时不支持其他参数类型，如果是公参需要在url里面展示，需要传一个json对象，一般默认args为get公参，params为post请求参数</p>\n     *\n     * @param url    请求地址\n     * @param params 请求参数，其中二进制流必须是 file\n     * @param file   文件\n     * @return\n     */\n    public static HttpPost getHttpPost(String url, JSONObject params, File file) {\n        HttpPost httpPost = getHttpPost(url);\n        setMultipartEntityEntity(httpPost, params, file);\n        return httpPost;\n    }\n\n    /**\n     * 设置二进制流实体，params 里面参数值为 file\n     *\n     * @param httpPost httpPsot 请求\n     * @param params   请求参数\n     * @param file     文件\n     */\n    private static void setMultipartEntityEntity(HttpPost httpPost, JSONObject params, File file) {\n        logger.debug(\"上传文件名：{}\", file.getAbsolutePath());\n        String fileName = file.getName();\n        InputStream inputStream = null;\n        try {\n            inputStream = new FileInputStream(file);\n        } catch (FileNotFoundException e) {\n            logger.warn(\"读取文件失败！\", e);\n        }\n        Iterator<String> keys = params.keySet().iterator();// 遍历 params 参数和值\n        MultipartEntityBuilder builder = MultipartEntityBuilder.create();// 新建MultipartEntityBuilder对象\n        while (keys.hasNext()) {\n            String key = keys.next();\n            String value = params.getString(key);\n            if (value.equals(\"file\")) {\n                builder.addBinaryBody(key, inputStream, ContentType.create(HttpClientConstant.CONTENTTYPE_MULTIPART_FORM), fileName);// 设置流参数\n            } else {\n                StringBody body = new StringBody(value, ContentType.create(HttpClientConstant.CONTENTTYPE_TEXT, DEFAULT_CHARSET));// 设置普通参数\n                builder.addPart(key, body);\n            }\n        }\n        HttpEntity entity = builder.build();\n        httpPost.setEntity(entity);\n    }\n\n    /**\n     * 发送请求之前，目前修改为止增加一个connection请求头\n     *\n     * @param request\n     */\n    protected static void beforeRequest(HttpRequestBase request) {\n        request.addHeader(HttpClientConstant.CONNECTION);\n    }\n\n    /**\n     * 响应结束之后，处理响应头信息，如set-cookien内容\n     *\n     * @param response 响应内容\n     * @return\n     */\n    private static JSONObject afterResponse(CloseableHttpResponse response) {\n        JSONObject cookies = new JSONObject();\n        List<Header> headers = Arrays.asList(response.getHeaders(\"Set-Cookie\"));\n        if (headers.size() == 0) return cookies;\n        headers.forEach(x -> {\n            String[] split = x.getValue().split(\";\")[0].split(\"=\", 2);\n            cookies.put(split[0], split[1]);\n        });\n        return cookies;\n    }\n\n    /**\n     * 根据解析好的content，转化json对象\n     *\n     * @param content\n     * @return\n     */\n    private static JSONObject getJsonResponse(String content, JSONObject cookies) {\n        JSONObject jsonObject = new JSONObject();\n        try {\n            if (StringUtils.isEmpty(content)) ParamException.fail(\"响应为空!\");\n            jsonObject = JSONObject.parseObject(content);\n        } catch (JSONException e) {\n            jsonObject = new JSONObject() {{\n                put(RESPONSE_CONTENT, content);\n                put(RESPONSE_CODE, TEST_ERROR_CODE);\n            }};\n            logger.warn(\"响应体非json格式，已经自动转换成json格式！\");\n        } finally {\n            if (cookies != null && !cookies.isEmpty()) jsonObject.put(HttpClientConstant.COOKIE, cookies);\n            return jsonObject;\n        }\n    }\n\n\n    /**\n     * 根据响应获取响应实体\n     *\n     * @param response\n     * @return\n     */\n    public static String getContent(HttpResponse response) {\n        HttpEntity entity = response.getEntity();// 获取响应实体\n        String content = EMPTY;\n        try {\n            content = EntityUtils.toString(entity, DEFAULT_CHARSET);// 用string接收响应实体\n            EntityUtils.consume(entity);// 消耗响应实体，并关闭相关资源占用\n        } catch (Exception e1) {\n            logger.warn(\"解析响应实体异常！\", e1);\n        }\n        return content;\n    }\n\n    /**\n     * 获取响应状态，处理重定向的url\n     *\n     * @param response\n     * @param res\n     * @return\n     */\n    public static int getStatus(CloseableHttpResponse response, JSONObject res) {\n        int status = response.getStatusLine().getStatusCode();\n        if (status != HttpStatus.SC_OK) logger.warn(\"响应状态码错误：{}\", status);\n        if (status == HttpStatus.SC_MOVED_TEMPORARILY)\n            res.put(\"location\", response.getFirstHeader(\"Location\").getValue());\n        return status;\n    }\n\n    /**\n     * 获取响应实体\n     * <p>会自动设置cookie，但是需要各个项目再自行实现cookie管理</p>\n     * <p>该方法只会处理文本信息，对于文件处理可以调用两个过期的方法解决</p>\n     *\n     * @param request 请求对象\n     * @return 返回json类型的对象\n     */\n    public static JSONObject getHttpResponse(HttpRequestBase request) {\n        if (!isRightRequest(request)) RequestException.fail(request);\n        beforeRequest(request);\n        JSONObject res = new JSONObject();\n        RequestInfo requestInfo = new RequestInfo(request);\n        long start = Time.getTimeStamp();\n        try (CloseableHttpResponse response = ClientManage.httpsClient.execute(request)) {\n            long end = Time.getTimeStamp();\n            long elapsed_time = end - start;\n            int status = getStatus(response, res);\n            JSONObject setCookies = afterResponse(response);\n            String content = getContent(response);\n            int data_size = content.length();\n            res.putAll(getJsonResponse(content, setCookies));\n            int code = iBase == null ? -2 : iBase.checkCode(res, requestInfo);\n//            if (iBase != null && !iBase.isRight(res))\n//                new AlertOver(\"响应状态码错误：\" + status, \"状态码错误：\" + status, requestInfo.getUrl(), requestInfo).sendSystemMessage();\n            MySqlTest.saveApiTestDate(requestInfo, data_size, elapsed_time, status, getMark(), code, LOCAL_IP, COMPUTER_USER_NAME);\n        } catch (Exception e) {\n            logger.warn(\"获取请求相应失败！请求内容:{}\", FunRequest.initFromRequest(request).toString(), e);\n            if (!requestInfo.isBlack())\n                new AlertOver(\"接口请求失败\", requestInfo.toString(), requestInfo.getUrl(), requestInfo).sendSystemMessage();\n        } finally {\n            if (!requestInfo.isBlack()) {\n                lastRequest = request;\n            }\n        }\n        return res;\n    }\n\n    /**\n     * 判断请求是否是正确的，目前主要过滤一些不完整的请求和超长的url\n     *\n     * @param request\n     * @return\n     */\n    private static boolean isRightRequest(HttpRequestBase request) {\n        String url = request.getURI().toString().toLowerCase();\n        return StringUtils.isNoneEmpty(url) && url.startsWith(\"http\");\n    }\n\n    /**\n     * 设置post接口上传表单，默认的编码格式\n     * 默认编码格式{@link Constant#DEFAULT_CHARSET}\n     *\n     * @param httpPost post请求\n     * @param params   参数\n     */\n    private static void setFormHttpEntity(HttpPost httpPost, JSONObject params) {\n        List<NameValuePair> formparams = new ArrayList<NameValuePair>();\n        params.keySet().forEach(x -> formparams.add(new BasicNameValuePair(x, params.getString(x))));\n        UrlEncodedFormEntity entity = new UrlEncodedFormEntity(formparams, DEFAULT_CHARSET);\n        httpPost.setEntity(entity);\n    }\n\n    /**\n     * 解析response，使用char数组，注意编码格式\n     * <p>自定义解析响应实体的方法，暂不采用</p>\n     *\n     * @param response 传入的response，非closedresponse\n     * @return string类型的response\n     */\n    @Deprecated\n    private static String parseResponeEntityByChar(HttpResponse response) {\n        StringBuffer buffer = new StringBuffer();// 创建并实例化stringbuffer，存放响应信息\n        try (InputStream input = response.getEntity().getContent(); InputStreamReader reader = new InputStreamReader(input, DEFAULT_CHARSET)) {\n            char[] buff = new char[1024];// 创建并实例化字符数组\n            int length = 0;// 声明变量length，表示读取长度\n            while ((length = reader.read(buff)) != -1) {// 循环读取字符输入流\n                String x = new String(buff, 0, length);// 获取读取到的有效内容\n                buffer.append(x);// 将读取到的内容添加到stringbuffer中\n            }\n        } catch (IOException e) {\n            logger.warn(\"解析响应实体失败！\", e);\n        }\n        return buffer.toString();\n    }\n\n    /**\n     * 从响应解析到文件\n     *\n     * @param response\n     * @param file\n     */\n    @Deprecated\n    private static void parseResponeByFile(HttpResponse response, File file) {\n        int bytesum = 0;// 这个用来统计需要写入byte数组的长度\n        int byteread = 0;// 这个用来接收read()方法的返回值，表示读取内容的长度\n        try (InputStream inputStream = response.getEntity().getContent(); FileOutputStream fileOutputStream = new FileOutputStream(file);) {\n            byte[] buffer = new byte[1024];// 新建读取文件所用的数组\n            // 此处用while循环每次按buffer读取文件直到读取完成\n            while ((byteread = inputStream.read(buffer)) != -1) {// 如何读取到文件末尾\n                bytesum += byteread;// 此处计算读取长度，byteread表示每次读取的长度\n                fileOutputStream.write(buffer, 0, byteread);// 此方法第一个参数是byte数组，第二次参数是开始位置，第三个参数是长度\n            }\n        } catch (IOException e) {\n            logger.warn(\"解析响应实体失败！\", e);\n        }\n    }\n\n    /**\n     * 把json数据转化为参数，为get请求和post请求stringentity的时候使用\n     *\n     * @param argument 请求参数，json数据类型，map类型，可转化\n     * @return 返回拼接参数后的地址\n     */\n    public static String changeJsonToArguments(JSONObject argument) {\n        return argument == null || argument.isEmpty() ? EMPTY : argument.keySet().stream().map(x -> x.toString() + \"=\" + DecodeEncode.urlEncoderText(argument.getString(x.toString()))).collect(Collectors.joining(\"&\", UNKNOW, EMPTY)).toString();\n    }\n\n    /**\n     * 通过json对象信息，生成cookie的header\n     *\n     * @param cookies\n     * @return\n     */\n    public static Header getCookies(JSONObject cookies) {\n        return getHeader(HttpClientConstant.COOKIE, cookies.keySet().stream().map(x -> x.toString() + \"=\" + cookies.get(x).toString()).collect(Collectors.joining(\";\")).toString());\n    }\n\n    /**\n     * 生成header\n     *\n     * @param name\n     * @param value\n     * @return\n     */\n    public static Header getHeader(String name, String value) {\n        return new BasicHeader(name, value);\n    }\n\n    public static IBase getiBase() {\n        return iBase;\n    }\n\n    public static void setiBase(IBase iBase) {\n        FunLibrary.iBase = iBase;\n    }\n\n    /**\n     * 将header转成json对象\n     *\n     * @param headers\n     * @return\n     */\n    public static JSONObject header2Json(List<Header> headers) {\n        return new JSONObject() {{\n            headers.forEach(x -> put(x.getName(), x.getValue()));\n        }};\n    }\n\n    /**\n     * 简单发送请求\n     *\n     * @param request\n     */\n    public static String executeSimlple(HttpRequestBase request) throws IOException {\n        try (CloseableHttpResponse response = ClientManage.httpsClient.execute(request);) {\n            if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK)\n                RequestException.fail(\"响应状态码错误:\" + response.getStatusLine().getStatusCode());\n            return getContent(response);\n        }\n    }\n\n    /**\n     * 设置代理请求\n     *\n     * @param request\n     * @param adress\n     */\n    public static void setProxy(HttpRequestBase request, String adress) {\n        request.setConfig(getProxyConfig(adress));\n    }\n\n    /**\n     * 设置代理请求\n     *\n     * @param request\n     * @param ip\n     * @param port\n     */\n    public static void setProxy(HttpRequestBase request, String ip, int port) {\n        setProxy(request, ip + \":\" + port);\n    }\n\n    /**\n     * 通过IP和端口获取代理配置对象\n     *\n     * @param adress\n     * @return\n     */\n    public static RequestConfig getProxyConfig(String adress) {\n        if (StringUtils.isBlank(adress) || !Regex.isMatch(adress, Constant.HOST_REGEX))\n            ParamException.fail(\"adress格式错误:\" + adress);\n        String[] split = adress.split(\":\", 2);\n        return ClientManage.getProxyRequestConfig(split[0], changeStringToInt(split[1]));\n    }\n\n    /**\n     * 异步发送请求\n     *\n     * @param request\n     */\n    public static void executeSync(HttpRequestBase request) {\n        if (!ClientManage.httpAsyncClient.isRunning()) ClientManage.httpAsyncClient.start();\n        ClientManage.httpAsyncClient.execute(request, null);\n    }\n\n    /**\n     * 异步发送请求获取影响Demo\n     * <p>经过测试没卵用</p>\n     *\n     * @param request\n     * @throws ExecutionException\n     * @throws InterruptedException\n     */\n    public static JSONObject executeSyncWithResponse(HttpRequestBase request) {\n        if (!ClientManage.httpAsyncClient.isRunning()) ClientManage.httpAsyncClient.start();\n        Future<HttpResponse> execute = ClientManage.httpAsyncClient.execute(request, null);\n        try {\n            HttpResponse httpResponse = execute.get();\n            String content = getContent(httpResponse);\n            return getJsonResponse(content, null);\n        } catch (Exception e) {\n            logger.error(\"异步请求获取响应失败!\", e);\n        }\n        return new JSONObject();\n    }\n\n    /**\n     * 获取最后一个发出的请求,用于进行性能测试用的,也可以由基类对象{@link IBase}实现\n     *\n     * @return\n     */\n    public static HttpRequestBase getLastRequest() {\n        return lastRequest;\n    }\n\n    /**\n     * 结束测试，关闭连接池\n     */\n    public static void testOver() {\n        try {\n            ClientManage.httpsClient.close();\n            ClientManage.httpAsyncClient.close();\n        } catch (Exception e) {\n            logger.warn(\"连接池关闭失败！\", e);\n        }\n    }\n\n    /**\n     * 初始化连接池和各类管理器\n     *\n     * @param timeout\n     * @param accepttime\n     * @param retrytimes\n     */\n    public synchronized static void init(int timeout, int accepttime, int retrytimes) {\n        ClientManage.init(timeout, accepttime, retrytimes, null, 0);\n    }\n\n    public synchronized static void init(int timeout, int accepttime, int retrytimes, String ip, int port) {\n        ClientManage.init(timeout, accepttime, retrytimes, ip, port);\n    }\n\n\n}"
  },
  {
    "path": "src/main/groovy/com/funtester/httpclient/FunRequest.groovy",
    "content": "package com.funtester.httpclient\n\nimport com.alibaba.fastjson.JSONObject\nimport com.funtester.base.bean.RequestInfo\nimport com.funtester.base.exception.RequestException\nimport com.funtester.config.HttpClientConstant\nimport com.funtester.config.RequestType\nimport com.funtester.frame.Save\nimport com.funtester.frame.SourceCode\nimport com.funtester.utils.Time\nimport org.apache.commons.lang3.StringUtils\nimport org.apache.http.Header\nimport org.apache.http.HttpEntity\nimport org.apache.http.client.methods.HttpPost\nimport org.apache.http.client.methods.HttpRequestBase\nimport org.apache.http.client.methods.RequestBuilder\nimport org.apache.http.util.EntityUtils\nimport org.apache.logging.log4j.LogManager\nimport org.apache.logging.log4j.Logger\n\n/**\n * 重写FunLibrary，使用面对对象思想,不用轻易使用set属性方法,可能存在BUG\n */\nclass FunRequest extends SourceCode implements Serializable, Cloneable {\n\n    private static final long serialVersionUID = -4153600036943378727L\n\n    private static Logger logger = LogManager.getLogger(FunRequest.class)\n\n    /**\n     * 请求类型，true为get，false为post\n     */\n    RequestType requestType\n\n    /**\n     * 请求对象\n     */\n    HttpRequestBase request\n\n    /**\n     * host地址\n     */\n    String host = EMPTY\n\n    /**\n     * 接口地址\n     */\n    String apiName = EMPTY\n\n    /**\n     * 请求地址,如果为空则由host和apiname拼接\n     */\n    String uri = EMPTY\n\n    /**\n     * header集合\n     */\n    List<Header> headers = new ArrayList<>()\n\n    /**\n     * get参数\n     */\n    JSONObject args = new JSONObject()\n\n    /**\n     * post参数,表单\n     */\n    JSONObject params = new JSONObject()\n\n    /**\n     * json参数\n     */\n    JSONObject json = new JSONObject()\n\n    /**\n     * 响应,若没有这个参数,从将funrequest对象转换成json对象时会自动调用getresponse方法\n     */\n    JSONObject response = new JSONObject()\n\n    /**\n     * 构造方法\n     *\n     * @param requestType\n     */\n    private FunRequest(RequestType requestType) {\n        this.requestType = requestType\n    }\n\n    /**\n     * 获取get对象\n     *\n     * @return\n     */\n    static FunRequest isGet() {\n        new FunRequest(RequestType.GET)\n    }\n\n    /**\n     * 获取post对象\n     *\n     * @return\n     */\n    static FunRequest isPost() {\n        new FunRequest(RequestType.POST)\n    }\n\n    /**\n     * 设置host\n     *\n     * @param host\n     * @return\n     */\n    FunRequest setHost(String host) {\n        this.host = host\n        this\n    }\n\n    /**\n     * 设置接口地址\n     *\n     * @param apiName\n     * @return\n     */\n    FunRequest setApiName(String apiName) {\n        this.apiName = apiName\n        this\n    }\n\n    /**\n     * 设置uri\n     *\n     * @param uri\n     * @return\n     */\n    FunRequest setUri(String uri) {\n        this.uri = uri\n        this\n    }\n\n    /**\n     * 添加get参数\n     *\n     * @param key\n     * @param value\n     * @return\n     */\n    FunRequest addArgs(Object key, Object value) {\n        args.put(key, value)\n        this\n    }\n\n    /**\n     * 添加post参数\n     *\n     * @param key\n     * @param value\n     * @return\n     */\n    FunRequest addParam(Object key, Object value) {\n        params.put(key, value)\n        this\n    }\n\n    /**\n     * 添加json参数\n     *\n     * @param key\n     * @param value\n     * @return\n     */\n    FunRequest addJson(Object key, Object value) {\n        json.put(key, value)\n        this\n    }\n\n    /**\n     * 添加header\n     *\n     * @param key\n     * @param value\n     * @return\n     */\n    FunRequest addHeader(Object key, Object value) {\n        headers << getHeader(key.toString(), value.toString())\n        this\n    }\n\n    /**\n     * 添加header\n     *\n     * @param header\n     * @return\n     */\n    FunRequest addHeader(Header header) {\n        headers.add(header)\n        this\n    }\n\n    /**\n     * 批量添加header\n     *\n     * @param header\n     * @return\n     */\n    FunRequest addHeader(List<Header> header) {\n        header.each {h -> headers << h}\n        this\n    }\n\n    /**\n     * 增加header中cookies\n     *\n     * @param cookies\n     * @return\n     */\n    FunRequest addCookies(JSONObject cookies) {\n        headers << getCookies(cookies)\n        this\n    }\n\n    FunRequest addHeaders(List<Header> headers) {\n        this.headers.addAll(headers)\n        this\n    }\n\n    FunRequest addHeaders(JSONObject headers) {\n        headers.each {x ->\n            this.headers.add(getHeader(x.getKey().toString(), x.getValue().toString()))\n        }\n        this\n    }\n\n    FunRequest addArgs(JSONObject args) {\n        this.args.putAll(args)\n        this\n    }\n\n    FunRequest addParams(JSONObject params) {\n        this.params.putAll(params)\n        this\n    }\n\n    FunRequest addJson(JSONObject json) {\n        this.json.putAll(json)\n        this\n    }\n\n    /**\n     * 获取请求响应，兼容相关参数方法，不包括file\n     *\n     * @return\n     */\n    JSONObject getResponse() {\n        response = response.isEmpty() ? FunLibrary.getHttpResponse(request == null ? getRequest() : request) : response\n        response\n    }\n\n\n    /**\n     * 获取请求对象\n     *\n     * @return\n     */\n    HttpRequestBase getRequest() {\n        if (request != null) request\n        if (StringUtils.isEmpty(uri))\n            uri = host + apiName\n        switch (requestType) {\n            case RequestType.GET:\n                request = FunLibrary.getHttpGet(uri, args)\n                break\n            case RequestType.POST:\n                request = !params.isEmpty() ? FunLibrary.getHttpPost(uri + FunLibrary.changeJsonToArguments(args), params) : !json.isEmpty() ? FunLibrary.getHttpPost(uri + FunLibrary.changeJsonToArguments(args), json.toString()) : FunLibrary.getHttpPost(uri + FunLibrary.changeJsonToArguments(args))\n                break\n        }\n        for (Header header in headers) {\n            request.addHeader(header)\n        }\n        logger.debug(\"请求信息：{}\", new RequestInfo(this.request).toString())\n        request\n    }\n\n    FunRequest setHeaders(List<Header> headers) {\n        this.headers = headers\n        this\n    }\n\n    FunRequest setArgs(JSONObject args) {\n        this.args = args\n        this\n    }\n\n    FunRequest setParams(JSONObject params) {\n        this.params = params\n        this\n    }\n\n    FunRequest setJson(JSONObject json) {\n        this.json = json\n        this\n    }\n\n    @Override\n    FunRequest clone() {\n        initFromRequest(this.getRequest())\n    }\n\n    @Override\n    String toString() {\n        return \"{\" +\n                \"requestType='\" + requestType.getName() + '\\'' +\n                \", host='\" + host + '\\'' +\n                \", apiName='\" + apiName + '\\'' +\n                \", uri='\" + uri + '\\'' +\n                \", headers=\" + FunLibrary.header2Json(headers).toString() +\n                \", args=\" + args.toString() +\n                \", params=\" + params.toString() +\n                \", json=\" + json.toString() +\n                \", response=\" + response.toString() +\n                '}'\n    }\n\n    /**\n     * 将请求对象转成curl命令行\n     * @return\n     */\n    String toCurl() {\n        StringBuffer curl = new StringBuffer(\"curl -w HTTPcode%{http_code}:代理返回code%{http_connect}:数据类型%{content_type}:DNS解析时间%{time_namelookup}:%{time_redirect}:连接建立完成时间%{time_pretransfer}:连接时间%{time_connect}:开始传输时间%{time_starttransfer}:总时间%{time_total}:下载速度%{speed_download}:speed_upload%{speed_upload} \")\n        if (requestType == RequestType.GET) curl << \" -G\"\n        headers.each {\n            curl << \" -H '${it.getName()}:${it.getValue().replace(SPACE_1, EMPTY)}'\"\n        }\n        switch (requestType) {\n            case RequestType.GET:\n                args.each {\n                    curl << \" -d '${it.key}=${it.value}'\"\n                }\n                break\n            case RequestType.POST:\n                if (!params.isEmpty()) {\n                    curl << \" -H Content-Type:application/x-www-form-urlencoded\"\n                    params.each {\n                        curl << \" -F '${it.key}=${it.value}'\"\n                    }\n                }\n                if (!json.isEmpty()) {\n                    curl << \" -H \\\"Content-Type:application/json\\\"\" //此处多余,防止从外部构建curl命令\n                    json.each {\n                        curl << \" -d '${it.key}=${it.value}'\"\n                    }\n                }\n                break\n            default:\n                break\n        }\n        curl << \" ${uri}\"\n        //        curl << \" --compressed\" //这里防止生成多个curl请求,批量生成有用\n        curl.toString()\n    }\n\n    /**\n     * 将请求对象转成curl命令行\n     * @param requestBase\n     * @return\n     */\n    static String reqToCurl(HttpRequestBase requestBase) {\n        initFromRequest(requestBase).toCurl()\n    }\n\n    /**\n     * 从requestbase对象从初始化funrequest\n     * @param base\n     * @return\n     */\n    static FunRequest initFromRequest(HttpRequestBase base) {\n        FunRequest request = null\n        String method = base.getMethod()\n        RequestType requestType = RequestType.getRequestType(method)\n        String uri = base.getURI().toString()\n        List<Header> headers = Arrays.asList(base.getAllHeaders())\n        if (requestType == requestType.GET) {\n            request = isGet().setUri(uri).addHeaders(headers)\n        } else if (requestType == RequestType.POST) {\n            HttpPost post = (HttpPost) base\n            HttpEntity entity = post.getEntity()\n            String value = entity.getContentType().getValue()\n            String content = null\n            try {\n                content = EntityUtils.toString(entity)\n            } catch (IOException e) {\n                logger.error(\"解析响应失败!\", e)\n                fail()\n            }\n            if (value.equalsIgnoreCase(HttpClientConstant.ContentType_TEXT.getValue()) || value.equalsIgnoreCase(HttpClientConstant.ContentType_JSON.getValue())) {\n                request = isPost().setUri(uri).addHeaders(headers).addJson(JSONObject.parseObject(content))\n            } else if (value.equalsIgnoreCase(HttpClientConstant.ContentType_FORM.getValue())) {\n                request = isPost().setUri(uri).addHeaders(headers).addParams(getJson(content.split(\"&\")))\n            }\n        } else {\n            RequestException.fail(\"不支持的请求类型!\")\n        }\n        return request\n    }\n\n    static HttpRequestBase doCopy(HttpRequestBase base) {\n        (HttpRequestBase) RequestBuilder.copy(base).build()\n    }\n\n    /**\n     * 拷贝HttpRequestBase对象\n     * @param base\n     * @return\n     */\n    static HttpRequestBase cloneRequest(HttpRequestBase base) {\n        initFromRequest(base).getRequest()\n    }\n\n    /**\n     * 保存请求和响应\n     * @param base\n     * @param response\n     */\n    static void save(HttpRequestBase base, JSONObject response) {\n        FunRequest request = initFromRequest(base)\n        request.setResponse(response)\n        Save.info(\"/request/\" + Time.getDate().substring(8) + SPACE_1 + request.getUri().replace(OR, CONNECTOR).replaceAll(\"https*:_+\", EMPTY), request.toString())\n    }\n\n\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/httpclient/GCThread.java",
    "content": "package com.funtester.httpclient;\n\nimport com.funtester.config.HttpClientConstant;\n\nimport static com.funtester.frame.SourceCode.sleep;\n\n/**\n * 从连接池中回收连接的多线程类\n */\npublic class GCThread implements Runnable {\n\n    /**\n     * 资源回收线程\n     */\n    private static volatile Thread gc = init();\n\n    /**\n     * 增加了线程状态的判断,同一进程多次运行HTTP请求的压测功能\n     */\n    public synchronized static void starts() {\n        if (gc.getState() == Thread.State.NEW) gc.start();\n        else if (gc.getState() == Thread.State.TERMINATED) gc = init();\n    }\n\n    /**\n     * 初始化方法,获取新的gc线程对象\n     *\n     * @return\n     */\n    public static synchronized Thread init() {\n        FLAG = true;\n        return new Thread(new GCThread());\n    }\n\n    private GCThread() {\n    }\n\n    /**\n     * 线程结束标志\n     */\n    private static boolean FLAG = true;\n\n    @Override\n    public void run() {\n        while (FLAG) {\n            sleep(HttpClientConstant.LOOP_INTERVAL);\n            ClientManage.recyclingConnection();\n        }\n    }\n\n    /**\n     * 结束线程方法\n     */\n    public static void stop() {\n        FLAG = false;\n    }\n\n\n}"
  },
  {
    "path": "src/main/groovy/com/funtester/main/ExecuteMethod.java",
    "content": "package com.funtester.main;\n\nimport com.funtester.frame.SourceCode;\nimport com.funtester.frame.execute.ExecuteSource;\nimport org.apache.commons.lang3.ArrayUtils;\n\npublic class ExecuteMethod extends SourceCode {\n\n    public static void main(String[] args) {\n        if (ArrayUtils.isEmpty(args)) args = new String[]{\"com.funtester.ztest.java.T.test\", \"java.lang.Integer\", \"1\"};\n        ExecuteSource.executeMethod(args);\n    }\n\n\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/main/PerformanceFromFile.groovy",
    "content": "package com.funtester.main\n\nimport com.funtester.frame.SourceCode\nimport com.funtester.frame.execute.Concurrent\nimport com.funtester.frame.thread.RequestThreadTimes\nimport com.funtester.httpclient.FunLibrary\nimport com.funtester.utils.request.RequestFile\nimport org.apache.http.client.methods.HttpRequestBase\n/**\n * 从文本配置中读取request，进行压测的类\n */\nclass PerformanceFromFile extends SourceCode {\n    static void main(String[] args) {\n        FunLibrary.setSocketTimeOut(30)\n        def size = args.size();\n        List<HttpRequestBase> list = new ArrayList<>()\n        for (int i = 0; i < size - 1; i += 2) {\n            def name = args[i]\n            int thread = changeStringToInt(args[i + 1])\n            def request = new RequestFile(name).getRequest()\n            for (int j = 0; j < thread; j++) {\n                list.add(request)\n            }\n        }\n        int perTimes = changeStringToInt(args[size - 1])\n        List<RequestThreadTimes> thread = new ArrayList<>()\n        for (int i = 0; i < list.size(); i++) {\n            def get = list.get(i)\n            def thread1 = new RequestThreadTimes(get, perTimes)\n            thread.add(thread1)\n        }\n        def concurrent = new Concurrent(thread)\n        concurrent.start()\n        FunLibrary.testOver()\n    }\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/socket/ScoketIOFunClient.java",
    "content": "package com.funtester.socket;\n\n/**\n * 基于Socket.IO的Client封装对象\n */\npublic class ScoketIOFunClient {\n//public class ScoketIOFunClient extends SourceCode implements Serializable {\n\n//    private static final long serialVersionUID = -7229704711068396512L;\n//\n//    private static Logger logger = LogManager.getLogger(ScoketIOFunClient.class);\n//\n//    public static ThreadLocal<IO.Options> options = new ThreadLocal() {\n//\n//        /**\n//         * 通用配置,初始化连接选项的方法,默认采取重置\n//         *\n//         * @return\n//         */\n//        @Override\n//        public IO.Options initialValue() {\n//            IO.Options options = new IO.Options();\n//            options.transports = SocketConstant.transports;\n//            //失败重试次数\n//            options.reconnectionAttempts = SocketConstant.MAX_RETRY;\n//            //失败重连的时间间隔\n//            options.reconnectionDelay = SocketConstant.RETRY_DELAY;\n//            //连接超时时间(ms)\n//            options.timeout = SocketConstant.TIMEOUT;\n//            return options;\n//        }\n//\n//    };\n//\n//    /**\n//     * 所有的客户端\n//     */\n//    public static Vector<ScoketIOFunClient> clients = new Vector<>();\n//\n//    /**\n//     * 记录的消息\n//     */\n//    public LinkedList<String> msgs = new LinkedList<>();\n//\n//    /**\n//     * 客户端名称\n//     */\n//    private String cname;\n//\n//    /**\n//     * 连接的URL\n//     */\n//    private String url;\n//\n//    /**\n//     * Socket对象\n//     */\n//    public Socket socket;\n//\n//    /**\n//     * 监听事件记录,此处使用量很小,故而不考虑线程安全\n//     */\n//    public Set<String> events = new HashSet<>();\n//\n//\n//    private ScoketIOFunClient(String url, Socket socket) {\n//        this.url = url;\n//        this.socket = socket;\n//        clients.add(this);\n//    }\n//\n//    /**\n//     * 获取socketClient实例\n//     *\n//     * @param url\n//     * @param cname\n//     * @return\n//     */\n//    public static ScoketIOFunClient getInstance(String url, String cname) {\n//        logger.info(\"Socket 连接: {},客户端名称: {}\", url, cname);\n//        ScoketIOFunClient client = null;\n//        try {\n//            client = new ScoketIOFunClient(url, IO.socket(url, options.get()));\n//            client.setCname(cname);\n//        } catch (URISyntaxException e) {\n//            FailException.fail(e);\n//        }\n//        return client;\n//    }\n//\n//    /**\n//     * 注册通用的事件监听,需要脚本自己注册改监听\n//     * {@link io.socket.client.Socket}\n//     */\n//    public void init() {\n//        this.socket.on(Socket.EVENT_CONNECTING, objects -> {\n//            logger.info(\"{} 正在连接...信息:{}\", cname, initMsg(objects));\n//        });\n//        events.add(Socket.EVENT_CONNECTING);\n//        this.socket.on(Socket.EVENT_ERROR, objects -> {\n//            logger.info(\"{} 收到错误信息:{}\", cname, initMsg(objects));\n//        });\n//        events.add(Socket.EVENT_ERROR);\n//        this.socket.on(Socket.EVENT_CONNECT_TIMEOUT, objects -> {\n//            logger.info(\"{} 连接超时!,url:{},信息:{}\", cname, url, initMsg(objects));\n//        });\n//        events.add(Socket.EVENT_CONNECT_TIMEOUT);\n//        this.socket.on(Socket.EVENT_CONNECT_ERROR, objects -> {\n//            logger.info(\"{} 连接错误,信息:{}\", cname, initMsg(objects));\n//        });\n//        events.add(Socket.EVENT_CONNECT_ERROR);\n//        this.socket.on(Socket.EVENT_PING, objects -> {\n//            logger.info(\"{} ping消息:{}\", cname, initMsg(objects));\n//        });\n//        events.add(Socket.EVENT_PING);\n//        this.socket.on(Socket.EVENT_PONG, objects -> {\n//            logger.info(\"{} ping消息:{}\", cname, initMsg(objects));\n//        });\n//        events.add(Socket.EVENT_PONG);\n//        /*此处统一的message做记录*/\n//        this.socket.on(Socket.EVENT_MESSAGE, objects -> {\n//            String msg = initMsg(objects);\n//            saveMsg(msg);\n//            logger.info(\"{} 收到 {} 事件,信息:{}\", cname, Socket.EVENT_MESSAGE, msg);\n//        });\n//    }\n//\n//    /**\n//     * 开始建立socket连接\n//     */\n//    public void connect() {\n//        logger.info(\"{} 开始连接...\", cname);\n//        this.socket.connect();\n//        int a = 0;\n//        while (true) {\n//            this.socket.connect();\n//            if (this.socket.connected()) break;\n//            if ((a++ > SocketConstant.MAX_RETRY)) FailException.fail(cname + \"连接重试失败!\");\n//            SourceCode.sleep(SocketConstant.WAIT_INTERVAL);\n//        }\n//        logger.info(\"{} 连接成功!\", cname);\n//    }\n//\n//    /**\n//     * 添加监听事件\n//     *\n//     * @param event\n//     * @param fn\n//     */\n//    public void addEventListener(String event, Emitter.Listener fn) {\n//        events.add(event);\n//        this.socket.on(event, fn);\n//    }\n//\n//    /**\n//     * 发送消息,暂不重载\n//     *\n//     * @param event\n//     * @param objects\n//     */\n//    public void send(String event, Object... objects) {\n//        events.add(event);\n//        this.socket.emit(event, objects);\n//    }\n//\n//    /**\n//     * 关闭SocketClient\n//     */\n//    public void close() {\n//        logger.info(\"{} socket链接关闭!\", cname);\n//        this.socket.close();\n//    }\n//\n//    /**\n//     * 初始化收到的信息\n//     *\n//     * @param objects\n//     * @return\n//     */\n//    public static String initMsg(Object... objects) {\n//        if (ArrayUtils.isEmpty(objects)) return EMPTY;\n//        return Arrays.toString(objects);\n//    }\n//\n//    /**\n//     * 该方法用于性能测试中,clone多线程对象\n//     *\n//     * @return\n//     */\n//    @Override\n//    public ScoketIOFunClient clone() {\n//        return getInstance(this.url, this.cname + StringUtil.getString(4));\n//    }\n//\n//    /**\n//     * 设置cname,多用于性能测试clone()之后\n//     *\n//     * @param cname\n//     */\n//    public void setCname(String cname) {\n//        this.cname = cname;\n//    }\n//\n//    public String getCname() {\n//        return cname;\n//    }\n//\n//    public String getUrl() {\n//        return url;\n//    }\n//\n//    public void setUrl(String url) {\n//        this.url = url;\n//    }\n//\n//    /**\n//     * 保存收到的信息,只保留最近的{@link SocketConstant}条\n//     *\n//     * @param msg\n//     */\n//    public void saveMsg(String msg) {\n//        synchronized (msgs) {\n//            if (msgs.size() > SocketConstant.MAX_MSG_SIZE) msgs.remove();\n//            msgs.add(msg);\n//        }\n//    }\n//\n//    /**\n//     * 关闭所有socketclient\n//     */\n//    public static void closeAll() {\n//        clients.forEach(x ->\n//                {\n//                    if (x != null && x.socket.connected()) x.close();\n//                }\n//        );\n//        clients.clear();\n//        logger.info(\"关闭所有Socket客户端!\");\n//    }\n\n\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/socket/WebSocketFunClient.java",
    "content": "package com.funtester.socket;\n\n/**\n * socket客户端代码,限于WebSocket协议的测试\n */\npublic class WebSocketFunClient {\n//public class WebSocketFunClient extends WebSocketClient {\n\n//    private static Logger logger = LogManager.getLogger(WebSocketFunClient.class);\n//\n//    public static Vector<WebSocketFunClient> clients = new Vector<>();\n//\n//    /**\n//     * 存储收到的消息\n//     */\n//    public LinkedList<String> msgs = new LinkedList<>();\n//\n//    /**\n//     * 连接的url\n//     */\n//    private String url;\n//\n//    /**\n//     * 客户端名称\n//     */\n//    private String cname;\n//\n//    private WebSocketFunClient(String url, String cname) throws URISyntaxException {\n//        super(new URI(url));\n//        this.cname = cname;\n//        this.url = url;\n//        clients.add(this);\n//    }\n//\n//    /**\n//     * 获取socketclient实例\n//     *\n//     * @param url\n//     * @return\n//     */\n//    public static WebSocketFunClient getInstance(String url) {\n//        return getInstance(url, Constant.DEFAULT_STRING + StringUtil.getString(4));\n//    }\n//\n//    /**\n//     * 获取socketclient实例\n//     *\n//     * @param url\n//     * @param cname\n//     * @return\n//     */\n//    public static WebSocketFunClient getInstance(String url, String cname) {\n//        WebSocketFunClient client = null;\n//        try {\n//            client = new WebSocketFunClient(url, cname);\n//        } catch (URISyntaxException e) {\n//            ParamException.fail(cname + \"创建socket client 失败! 原因:\" + e.getMessage());\n//        }\n//        return client;\n//    }\n//\n//    @Override\n//    public void onOpen(ServerHandshake handshakedata) {\n//        logger.info(\"{} 正在建立socket连接...\", cname);\n//        handshakedata.iterateHttpFields().forEachRemaining(x -> logger.info(\"握手信息key: {} ,value: {}\", x, handshakedata.getFieldValue(x)));\n//    }\n//\n//    /**\n//     * 收到消息时候调用的方法\n//     *\n//     * @param message\n//     */\n//    @Override\n//    public void onMessage(String message) {\n//        saveMsg(message);\n//        logger.info(\"{}收到: {}\", cname, message);\n//    }\n//\n//    /**\n//     * 关闭\n//     *\n//     * @param code   关闭code码,详情查看 {@link org.java_websocket.framing.CloseFrame}\n//     * @param reason 关闭原因\n//     * @param remote\n//     */\n//    @Override\n//    public void onClose(int code, String reason, boolean remote) {\n//        logger.info(\"{} socket 连接关闭,URL: {} ,code码:{},原因:{},是否由远程服务关闭:{}\", cname, url, code, reason, remote);\n//    }\n//\n//    /**\n//     * 关闭socketclient\n//     */\n//    @Override\n//    public void close() {\n//        logger.warn(\"{}:socket连接关闭!\", cname);\n//        super.close();\n//    }\n//\n//    /**\n//     * 出错时候调用\n//     *\n//     * @param e\n//     */\n//    @Override\n//    public void onError(Exception e) {\n//        logger.error(\"{} socket异常,URL: {}\", cname, url, e);\n//    }\n//\n//    /**\n//     * 发送消息\n//     *\n//     * @param text\n//     */\n//    @Override\n//    public void send(String text) {\n//        logger.debug(\"{} 发送:{}\", cname, text);\n//        super.send(text);\n//    }\n//\n//    /**\n//     * 简历socket连接\n//     */\n//    @Override\n//    public void connect() {\n//        logger.info(\"{} 开始连接...\", cname);\n//        super.connect();\n//        int a = 0;\n//        while (true) {\n//            if (this.getReadyState() == ReadyState.OPEN) break;\n//            if ((a++ > SocketConstant.MAX_WATI_TIMES)) FailException.fail(cname + \"连接重试失败!\");\n//            SourceCode.sleep(SocketConstant.WAIT_INTERVAL);\n//        }\n//        logger.info(\"{} 连接成功!\", cname);\n//    }\n//\n//    /**\n//     * 发送非默认编码格式的文字\n//     *\n//     * @param text\n//     * @param charset\n//     */\n//    public void send(String text, Charset charset) {\n//        send(new String(text.getBytes(), charset));\n//    }\n//\n//    /**\n//     * 发送json信息\n//     *\n//     * @param json\n//     */\n//    public void send(JSONObject json) {\n//        send(json.toJSONString());\n//    }\n//\n//    /**\n//     * 发送bean\n//     *\n//     * @param bean\n//     */\n//    public void send(AbstractBean bean) {\n//        send(bean.toString());\n//    }\n//\n//    /**\n//     * 重置连接\n//     */\n//    @Override\n//    public void reconnect() {\n//        logger.info(\"{}重置连接并尝试重新连接!\", cname);\n//        super.reconnect();\n//    }\n//\n//    /**\n//     * 设置cname,多用于性能测试clone()之后\n//     *\n//     * @param cname\n//     */\n//    public void setCname(String cname) {\n//        this.cname = cname;\n//    }\n//\n//    public String getCname() {\n//        return cname;\n//    }\n//\n//    public String getUrl() {\n//        return url;\n//    }\n//\n//    public void setUrl(String url) {\n//        this.url = url;\n//    }\n//\n//    /**\n//     * 该方法用于性能测试中,clone多线程对象\n//     *\n//     * @return\n//     */\n//    @Override\n//    public WebSocketFunClient clone() {\n//        return getInstance(this.url, this.cname + StringUtil.getString(4));\n//    }\n//\n//    /**\n//     * 保存收到的信息,只保留最近的{@link SocketConstant}条\n//     *\n//     * @param msg\n//     */\n//    public void saveMsg(String msg) {\n//        synchronized (msgs) {\n//            if (msgs.size() > SocketConstant.MAX_MSG_SIZE) msgs.remove();\n//            msgs.add(msg);\n//        }\n//    }\n//\n//    /**\n//     * 关闭所有socketclient\n//     */\n//    public static void closeAll() {\n//        clients.forEach(x ->\n//                {\n//                    if (x != null && !x.isClosed()) x.close();\n//                }\n//        );\n//        clients.clear();\n//        logger.info(\"关闭所有Socket客户端!\");\n//    }\n\n\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/utils/ArgsUtil.java",
    "content": "package com.funtester.utils;\n\nimport com.alibaba.fastjson.JSON;\nimport com.alibaba.fastjson.JSONObject;\nimport com.funtester.frame.SourceCode;\n\nimport java.io.File;\n\npublic class ArgsUtil extends SourceCode {\n\n    String[] all;\n\n    public ArgsUtil(String[] args) {\n        all = (String[]) args.clone();\n    }\n\n    /**\n     * 获取int参数\n     *\n     * @param i 获取的参数索引\n     * @param k 默认值\n     * @return\n     */\n    public int getIntOrdefault(int i, int k) {\n        return i >= all.length ? k : changeStringToInt(all[i]);\n    }\n\n    /**\n     * 获取boolean参数\n     *\n     * @param i\n     * @param k\n     * @return\n     */\n    public boolean getBooleanOrdefault(int i, boolean k) {\n        return i >= all.length ? k : changeStringToBoolean(all[i]);\n    }\n\n\n    /**\n     * @param i\n     * @param k\n     * @return\n     */\n    public String getStringOrdefault(int i, String k) {\n        return i >= all.length ? k : all[i];\n    }\n\n\n    /**\n     * @param i\n     * @param path\n     * @return\n     */\n    public File getFileOrDefault(int i, String path) {\n        return i >= all.length ? new File(path) : new File(all[i]);\n    }\n\n    /**\n     * @param i\n     * @param json\n     * @return\n     */\n    public JSONObject getJsonOrDefault(int i, String json) {\n        return i >= all.length ? JSON.parseObject(json) : JSON.parseObject(all[i]);\n    }\n\n\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/utils/CMD.java",
    "content": "package com.funtester.utils;\n\nimport com.funtester.config.Constant;\nimport org.apache.commons.lang3.StringUtils;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.io.*;\nimport java.nio.charset.Charset;\n\n/**\n * 执行命令的类\n */\npublic class CMD extends Constant {\n\n    private static Logger logger = LogManager.getLogger(CMD.class);\n\n    /**\n     * 执行cmd命令，控制台信息编码方式\n     *\n     * @param cmd 需要执行的命令\n     */\n    public static int execCmd(String cmd) {\n        return execCmd(cmd, DEFAULT_CHARSET);\n    }\n\n    /**\n     * 执行cmd命令，注意Mac 系统添加环境路径\n     *\n     * @param cmd 需要执行的命令\n     */\n    public static int execCmd(String cmd, Charset charset) {\n        return execCmd(cmd, charset, false, EMPTY);\n    }\n\n    public static int execCmd(String cmd, boolean filter, String mark) {\n        return execCmd(cmd, DEFAULT_CHARSET, false, EMPTY);\n    }\n\n    public static int execCmd(String cmd, Charset charset, boolean filter, String mark) {\n        logger.info(\"执行命令：{}\", cmd);\n        Process p = null;// 通过runtime类执行cmd命令\n        try {\n            p = Runtime.getRuntime().exec(cmd);\n        } catch (IOException e) {\n            logger.error(\"cmd：{}命令错误\", e);\n            return 1;\n        }\n        try (InputStream input = p.getInputStream();\n             InputStreamReader inputStreamReader = new InputStreamReader(input, charset);\n             BufferedReader reader = new BufferedReader(inputStreamReader);\n             InputStream errorInput = p.getErrorStream();\n             InputStreamReader streamReader = new InputStreamReader(errorInput, charset.name());\n             BufferedReader errorReader = new BufferedReader(streamReader)) {\n            String line = EMPTY;\n            while ((line = reader.readLine()) != null) {// 循环读取\n                if (!filter || (line.contains(mark))) logger.info(line);\n            }\n            String eline = EMPTY;\n            while ((eline = errorReader.readLine()) != null) {// 循环读取\n                logger.info(eline);// 输出\n            }\n            return 0;\n        } catch (IOException e) {\n            logger.warn(\"执行命令:{}失败！\", cmd, e);\n            p.destroy();\n            return 1;\n        }\n    }\n\n    /**\n     * 获取文本信息的最后几行，用户查看日志\n     *\n     * @param path\n     * @param num\n     * @return\n     */\n    public static String catFile(String path, int num) {\n        logger.info(\"查询的文件：{}\", path);\n        if (StringUtils.isEmpty(path)) return EMPTY;\n        File file = new File(path);\n        if (!file.exists() || file.isDirectory()) return EMPTY;\n        StringBuffer stringBuffer = new StringBuffer();\n        String command = \"tail -n \" + num + SPACE_1 + path;\n        logger.debug(\"执行命令：{}\", command);\n        try (InputStream input = Runtime.getRuntime().exec(command).getInputStream();\n             InputStreamReader inputStreamReader = new InputStreamReader(input, DEFAULT_CHARSET);\n             BufferedReader reader = new BufferedReader(inputStreamReader);) {\n            String line = EMPTY;\n            while ((line = reader.readLine()) != null) {// 循环读取\n                stringBuffer.append(line + LINE);\n            }\n        } catch (IOException e) {\n            logger.error(\"获取：{}文件信息失败！\", path, e);\n        } finally {\n            return stringBuffer.toString();\n        }\n    }\n\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/utils/CountUtil.groovy",
    "content": "package com.funtester.utils\n\nimport org.apache.logging.log4j.LogManager\nimport org.apache.logging.log4j.Logger\n\nimport java.util.stream.Collectors\n\n/**\n * 统计出现次数相关类\n */\nclass CountUtil {\n\n    private static Logger logger = LogManager.getLogger(CountUtil.class)\n\n    /**\n     * 统计数据出现的次数\n     *\n     * @param counts 统计的 jsonobject 对象\n     * @param object 需要统计的数据\n     */\n    static def count(Map counts, Object object) {\n        count(counts, object, 1)\n    }\n\n    /**\n     * 统计数据出现的次数\n     *\n     * @param counts 统计的 jsonobject 对象\n     * @param object 需要统计的数据\n     * @param num 增加值\n     */\n    static def count(Map counts, Object object, int num) {\n        counts.put(object, Integer.valueOf(counts.getOrDefault(object, 0) + num))\n    }\n\n    /**\n     * 统计某个list里面某个元素出现的次数\n     * @param list\n     * @param str\n     * @return\n     */\n    static def count(List list, def str) {\n        list.count {s -> s.toString().equals(str.toString())}\n    }\n\n    /**\n     * 统计某个list里面各个元素出现的次数\n     * collect,是一个map<object,integer>对象\n     * @param list\n     * @return\n     */\n    static def count(List list) {\n        list.stream().collect(Collectors.groupingBy {x -> x}).each {\n            it.setValue(it.value.size())\n            logger.info(\"元素：${it.key}，次数：${it.value}\")\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/utils/CurlUtil.groovy",
    "content": "package com.funtester.utils\n\nimport com.alibaba.fastjson.JSONObject\nimport com.funtester.config.Constant\nimport com.funtester.config.RequestType\nimport com.funtester.frame.SourceCode\nimport com.funtester.httpclient.FunLibrary\nimport com.funtester.httpclient.FunRequest\nimport org.apache.http.Header\nimport org.apache.http.client.methods.HttpRequestBase\n\n/**\n * 通过将浏览器中复制的curl文本信息转化成HTTPrequestbase对象工具类\n */\nclass CurlUtil {\n\n    private static def filterWords = [\".js\", \".png\", \".gif\", \".css\", \".ico\", \"list_unread\", \".svg\", \".htm\", \".jpeg\", \".ashx\"]\n\n    /**\n     * 从curl复制结果中获取请求\n     * @param path\n     * @return\n     */\n    static List<HttpRequestBase> getRequests(String path) {\n        def fileinfo = RWUtil.readTxtFileByLine(path.contains(Constant.OR) ? path : Constant.LONG_Path + path).stream().map {it.trim()}\n        List<HttpRequestBase> requests = []\n        def base = new CurlRequestBase()\n        fileinfo.each {\n            if (it.startsWith(\"curl\")) {\n                def split = it.split(\" \", 2)\n                def type = split[0]\n                def value = split[1]\n                base.url = value.substring(value.indexOf('h'), value.lastIndexOf(\"'\"))\n            } else if (it.startsWith(\"-H\")) {\n                def split = it.split(\" \", 2)[1].split(\": \")\n                base.headers << FunLibrary.getHeader(split[0].substring(1), split[1].substring(0, split[1].lastIndexOf(\"'\")))\n            } else if (it.startsWith(\"--data-raw\")) {\n                base.params = SourceCode.getJson(it.substring(it.indexOf(\"'\") + 1, it.lastIndexOf(\"'\")).split(\"&\"))\n                base.type = RequestType.POST\n            } else if (it.startsWith(\"--compressed\")) {\n                requests << getRequest(base)\n                base = new CurlRequestBase()\n            }\n        }\n        requests.findAll {\n            it != null && it.getFirstHeader(\"accept\").getValue().contains(\"application/json\")\n        }\n    }\n\n    /**\n     * 将curlrequestbase对象转换成HTTPrequestbase\n     * @param base\n     * @return\n     */\n    static HttpRequestBase getRequest(CurlRequestBase base) {\n        if (filterWords.any {\n            base.url.contains(it)\n        }) return\n        base.type == RequestType.GET ? FunRequest.isGet().setUri(base.url).addHeader(base.headers).getRequest() : FunRequest.isPost().setUri(base.url).addHeader(base.headers).addParams(base.params).getRequest()\n    }\n\n    /**\n     * 添加URL过滤词汇\n     * @param w\n     */\n    static void addFilterWord(String w) {\n        filterWords << w\n    }\n\n    /**\n     * 用于存储每一个请求的详情\n     */\n    static class CurlRequestBase {\n\n        String url\n\n        RequestType type = RequestType.GET\n\n        List<Header> headers = new ArrayList<>()\n\n        JSONObject params = new JSONObject()\n\n    }\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/utils/DecodeEncode.java",
    "content": "package com.funtester.utils;\n\nimport com.funtester.base.exception.FailException;\nimport com.funtester.config.Constant;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.io.ByteArrayOutputStream;\nimport java.io.IOException;\nimport java.io.OutputStream;\nimport java.io.UnsupportedEncodingException;\nimport java.nio.charset.Charset;\nimport java.security.MessageDigest;\nimport java.security.NoSuchAlgorithmException;\nimport java.util.Base64;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\nimport java.util.zip.DeflaterOutputStream;\nimport java.util.zip.InflaterOutputStream;\n\n/**\n * 编码格式转码解码类\n */\npublic class DecodeEncode extends Constant{\n\n    private static Logger logger = LogManager.getLogger(DecodeEncode.class);\n\n    /**\n     * url进行转码，常用于网络请求\n     *\n     * @param text 需要转码的文本\n     * @return 返回转码后的文本\n     */\n    public static String urlEncoderText(String text) {\n        return urlEncoderText(text, UTF_8);\n    }\n\n    /**\n     * url进行转码，常用于网络请求\n     *\n     * @param text 需要转码的文本\n     * @return 返回转码后的文本\n     */\n    public static String urlEncoderText(String text, Charset charset) {\n        String result = EMPTY;\n        try {\n            result = java.net.URLEncoder.encode(text, charset.toString());\n        } catch (UnsupportedEncodingException e) {\n            logger.warn(\"数据格式错误！\", e);\n        }\n        return result;\n    }\n\n    /**\n     * url进行解码，常用于解析响应，默认是UTF-8字符集\n     *\n     * @param text 需要解码的文本\n     * @return 解码后的文本\n     */\n    public static String urlDecoderText(String text, Charset charset) {\n        String result = EMPTY;\n        try {\n            result = java.net.URLDecoder.decode(text, charset.toString());\n        } catch (UnsupportedEncodingException e) {\n            logger.warn(\"数据格式错误！\", e);\n        }\n        return result;\n    }\n\n    /**\n     * url进行解码，常用于解析响应，默认是UTF-8字符集\n     *\n     * @param text 需要解码的文本\n     * @return 解码后的文本\n     */\n    public static String urlDecoderText(String text) {\n        return urlDecoderText(text, UTF_8);\n    }\n\n    /**\n     * 对本文进行base64解码，方法默认UTF_8\n     *\n     * @param text\n     * @return\n     */\n    public static String base64Decode(String text) {\n        return base64Decode(text, UTF_8);\n    }\n\n    /**\n     * 对字符串进行解码,使用编码格式参数\n     *\n     * @param text\n     * @param charset\n     * @return\n     */\n    public static String base64Decode(String text, Charset charset) {\n        return new String(base64Byte(text.getBytes(charset)));\n    }\n\n    /**\n     * 转换\n     *\n     * @param text\n     * @return\n     */\n    public static byte[] base64Byte(byte[] text) {\n        return Base64.getDecoder().decode(text);\n    }\n\n    /**\n     * 获取字符串的字节数组\n     *\n     * @param text\n     * @return\n     */\n    public static byte[] base64Byte(String text) {\n        return base64Byte(text.getBytes());\n    }\n\n    /**\n     * 压缩字符串,默认梳utf-8\n     *\n     * @param text\n     * @return\n     */\n    public static String zipBase64(String text) {\n        try (ByteArrayOutputStream out = new ByteArrayOutputStream()) {\n            try (DeflaterOutputStream deflaterOutputStream = new DeflaterOutputStream(out)) {\n                deflaterOutputStream.write(text.getBytes(Constant.UTF_8));\n            }\n            return DecodeEncode.base64Encode(out.toByteArray());\n        } catch (IOException e) {\n            logger.error(\"压缩文本失败:{}\", text, e);\n        }\n        return EMPTY;\n    }\n\n    /**\n     * 解压字符串,默认utf-8\n     *\n     * @param text\n     * @return\n     */\n    public static String unzipBase64(String text) {\n        try (ByteArrayOutputStream os = new ByteArrayOutputStream()) {\n            try (OutputStream outputStream = new InflaterOutputStream(os)) {\n                outputStream.write(DecodeEncode.base64Byte(text));\n            }\n            return new String(os.toByteArray(), Constant.UTF_8);\n        } catch (IOException e) {\n            logger.error(\"解压文本失败:{}\", text, e);\n        }\n        return EMPTY;\n    }\n\n    /**\n     * 对本文进行base64转码，方法默认了utf8\n     *\n     * @param text\n     * @return\n     */\n    public static String base64Encode(String text) {\n        return base64Encode(text, UTF_8);\n    }\n\n    /**\n     * 对本文进行base64转码，编码格式自定义\n     *\n     * @param text\n     * @param charset\n     * @return\n     */\n    public static String base64Encode(String text, Charset charset) {\n        try {\n            return new String(Base64.getEncoder().encode(text.getBytes(charset)));\n        } catch (Exception e) {\n            logger.warn(\"base64转码失败！\", e);\n            return EMPTY;\n        }\n    }\n\n    public static String base64Encode(byte[] data) {\n        try {\n            return new String(Base64.getEncoder().encode(data));\n        } catch (Exception e) {\n            logger.warn(\"base64转码失败！\", e);\n            return EMPTY;\n        }\n    }\n\n    /**\n     * 使用md5加密数据\n     *\n     * @param text\n     * @return\n     */\n    public static String encodeByMd5(String text) {\n        byte[] date = null;\n        try {\n            date = text.getBytes(\"utf-8\");\n        } catch (UnsupportedEncodingException e) {\n            FailException.fail(\"utf-8格式错误！\" + e.getMessage());\n        }\n        MessageDigest message = null;\n        try {\n            message = MessageDigest.getInstance(\"md5\");\n        } catch (NoSuchAlgorithmException e) {\n            FailException.fail(\"md5加密失败！\" + e.getMessage());\n        }\n        message.update(date);\n        byte[] result = message.digest();\n        StringBuffer stringBuffer = new StringBuffer();\n        for (int offset = 0; offset < result.length; offset++) {\n            int i = result[offset];\n            if (i < 0)// 如果负数\n                i += 256;// 变成正数，0xff & i 也可以\n            if (i < 16)// 如果小于16，则加上0小于16，转换之后就是一位缺少一位故要加0\n                stringBuffer.append(\"0\");\n            stringBuffer.append(Integer.toHexString(i));\n        }\n        return stringBuffer.toString();\n    }\n\n    /**\n     * MD5加盐加密\n     *\n     * @param text\n     * @param salt\n     * @return\n     */\n    public static String encodeByMd5(String text, String salt) {\n        return encodeByMd5(text + salt);\n    }\n\n    /**\n     * 处理Unicode码转(\\u6210\\u529f)\n     *\n     * @param str\n     * @return\n     */\n    public static String unicodeToString(String str) {\n        Pattern pattern = Pattern.compile(\"(\\\\\\\\u(\\\\p{XDigit}{4}))\");\n        Matcher matcher = pattern.matcher(str);\n        char ch;\n        while (matcher.find()) {\n            String group = matcher.group(2);\n            ch = (char) Integer.parseInt(group, 16);\n            String group1 = matcher.group(1);\n            str = str.replace(group1, ch + EMPTY);\n        }\n        return str;\n    }\n\n    /**\n     * 处理Unicode码转成(\\xe6\\x88\\x90\\xe5\\x8a\\x9f\")\n     *\n     * @param str\n     * @return\n     */\n    public static String unicodeToStringX(String str) {\n        str = str.replaceAll(\"\\\\\\\\x\", \"%\");\n        return urlDecoderText(str, DEFAULT_CHARSET);\n    }\n\n\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/utils/FileUtil.groovy",
    "content": "package com.funtester.utils\n\nimport com.funtester.config.Constant\nimport org.apache.logging.log4j.LogManager\nimport org.apache.logging.log4j.Logger\n\n/**\n * 文件读写类,与{@link RWUtil}有功能上的重合,原因在与Java和Groovy的不兼容问题.\n */\nclass FileUtil extends Constant {\n\n    private static Logger logger = LogManager.getLogger(FileUtil.class)\n\n    /**\n     * 拷贝文件\n     * @param source\n     * @param target\n     * @return\n     */\n    static def copy(String source, String target) {\n        def s = new File(source)\n        def t = new File(target)\n        if (s.exists() && s.isFile()) t.newOutputStream() << s.newInputStream()\n    }\n\n    /**\n     * 重命名一个文件\n     * @param oldPath\n     * @param newPath\n     * @return\n     */\n    static def rename(String oldPath, String newPath) {\n        if (new File(oldPath).renameTo(newPath)) logger.error(\"rename file error!，old:{},new:{}\", oldPath, newPath)\n    }\n\n    /**\n     * 从url下载文件\n     * @param url\n     * @param name\n     * @return\n     */\n    static def down(String url, String name) {\n        new File(name) << new URL(url).openStream()\n    }\n\n    /**\n     * 下载文件,目前只要针对图片\n     * @param url\n     * @return\n     */\n    static def down(String url) {\n        def tuple = handlePicName(url)\n        down(tuple.first, tuple.second);\n    }\n\n    /**\n     * 获取文件夹下所有文件的绝对路径的方法，递归，排除了Linux系统的隐藏文件\n     *\n     * @param path\n     * @return\n     */\n    static List<String> getAllFile(String path) {\n        List<String> list = new ArrayList<>()\n        File file = new File(path)\n        if (!file.exists() || file.isFile()) return list\n        File[] files = file.listFiles()\n        int length = files.length\n        if (length == 0) return list\n        for (int i in 0..length - 1) {\n            File file1 = files[i]\n            if (file1.isDirectory()) {\n                List<String> allFile = getAllFile(file1.getAbsolutePath())\n                list.addAll(allFile)\n                continue\n            }\n            String path1 = file1.getAbsolutePath()\n            if (path1.contains(\"/.\")) continue\n            list.add(path1)\n        }\n        return list\n    }\n\n    /**\n     * 处理下载网络图片的时候明文件的问题\n     * @param name\n     * @return\n     */\n    static Tuple2 handlePicName(String url) {\n        url -= \".webp\"\n        String name = url.substring(url.lastIndexOf(\"/\") + 1);\n        if (name.contains(UNKNOW)) name = name.substring(0, name.indexOf(UNKNOW))\n        return new Tuple2<String, String>(url, name)\n    }\n\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/utils/HeapDumper.java",
    "content": "package com.funtester.utils;\n\nimport com.sun.management.HotSpotDiagnosticMXBean;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport javax.management.MBeanServer;\nimport java.lang.management.ManagementFactory;\n\n/**\n * 获取JVM内存转储文件的工具类\n */\npublic class HeapDumper {\n\n    private static Logger logger = LogManager.getLogger(HeapDumper.class);\n\n    /**\n     * 这是HotSpot Diagnostic MBean的名称\n     */\n    private static final String HOTSPOT_BEAN_NAME = \"com.sun.management:type=HotSpotDiagnostic\";\n\n    /**\n     * 用于存储热点诊断MBean的字段\n     */\n    private static volatile HotSpotDiagnosticMXBean hotspotMBean;\n\n    /**\n     * 下载内存转储文件\n     *\n     * @param fileName 文件名,例如:heap.bin,不兼容路径,会在当前目录下生成\n     * @param live\n     */\n    public static void dumpHeap(String fileName, boolean live) {\n        initHotspotMBean();\n        try {\n            hotspotMBean.dumpHeap(fileName, live);\n        } catch (Exception e) {\n            logger.error(\"生成内存转储文件失败!\", e);\n        }\n    }\n\n    /**\n     * 初始化热点诊断MBean\n     */\n    private static void initHotspotMBean() {\n        if (hotspotMBean == null) {\n            synchronized (HeapDumper.class) {\n                if (hotspotMBean == null) {\n                    try {\n                        MBeanServer server = ManagementFactory.getPlatformMBeanServer();\n                        hotspotMBean = ManagementFactory.newPlatformMXBeanProxy(server, HOTSPOT_BEAN_NAME, HotSpotDiagnosticMXBean.class);\n                    } catch (Exception e) {\n                        logger.error(\"初始化mbean失败!\", e);\n                    }\n                }\n            }\n        }\n    }\n\n\n}"
  },
  {
    "path": "src/main/groovy/com/funtester/utils/Join.java",
    "content": "package com.funtester.utils;\n\nimport org.apache.commons.lang3.ArrayUtils;\nimport org.apache.commons.lang3.StringUtils;\n\npublic class Join {\n\n    /**\n     * 把字符串每个字符用分隔器连接起来\n     *\n     * @param text\n     * @param separator\n     * @return\n     */\n    public static String join(String text, String separator) {\n        return StringUtils.join(ArrayUtils.toObject(text.toCharArray()), separator);\n    }\n\n    /**\n     * 把字符串每个字符用分隔器连接起来\n     *\n     * @param text\n     * @param separator\n     * @return\n     */\n    public static String join(String text, String separator, String prefix, String suffix) {\n        return prefix + join(text, separator) + suffix;\n    }\n\n    /**\n     * 把Iterable用分隔器连接起来\n     *\n     * @param iterable\n     * @param separator\n     * @param prefix\n     * @param suffix\n     * @return\n     */\n    public static String join(Iterable<?> iterable, String separator, String prefix, String suffix) {\n        return prefix + join(iterable, separator) + suffix;\n    }\n\n    /**\n     * 把Iterable用分隔器连接起来，没有前后缀\n     *\n     * @param iterable\n     * @param separator\n     * @return\n     */\n    public static String join(Iterable<?> iterable, String separator) {\n        return StringUtils.join(iterable, separator);\n    }\n\n    /**\n     * 把数组添加用间隔符连接\n     *\n     * @param objects\n     * @param separator\n     * @param prefix    前缀\n     * @param suffix    后缀\n     * @return\n     */\n    public static String join(Object[] objects, String separator, String prefix, String suffix) {\n        return prefix + join(objects, separator) + suffix;\n    }\n\n    /**\n     * 把数组添加用间隔符连接\n     *\n     * @param objects\n     * @param separator 间隔\n     * @return\n     */\n    public static String join(Object[] objects, String separator) {\n        return StringUtils.join(objects, separator);\n    }\n\n\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/utils/JsonUtil.groovy",
    "content": "package com.funtester.utils\n/**下面是例子,官方文档地址:https://github.com/json-path/JsonPath/blob/master/README.md\n * $.store.book[*].author\tThe authors of all books\n * $..author\tAll authors\n * $.store.*\tAll things, both books and bicycles\n * $.store..price\tThe price of everything\n * $..book[2]\tThe third book\n * $..book[-2]\tThe second to last book\n * $..book[0,1]\tThe first two books\n * $..book[:2]\tAll books from index 0 (inclusive) until index 2 (exclusive)\n * $..book[1:2]\tAll books from index 1 (inclusive) until index 2 (exclusive)\n * $..book[-2:]\tLast two books\n * $..book[2:]\tBook number two from tail\n * $..book[?(@.isbn)]\tAll books with an ISBN number\n * $.store.book[?(@.price < 10)]\tAll books in store cheaper than 10\n * $..book[?(@.price <= $['expensive'])]\tAll books in store that are not \"expensive\"\n * $..book[?(@.author =~ /.*REES/i)]\tAll books matching regex (ignore case)\n * $..*\tGive me every thing\n * $..book.length()\tThe number of books\n *\n *\n * min()\tProvides the min value of an array of numbers\tDouble\n * max()\tProvides the max value of an array of numbers\tDouble\n * avg()\tProvides the average value of an array of numbers\tDouble\n * stddev()\tProvides the standard deviation value of an array of numbers\tDouble\n * length()\tProvides the length of an array\tInteger\n * sum()\tProvides the sum value of an array of numbers\tDouble\n * min()\t最小值\tDouble\n * max()\t最大值\tDouble\n * avg()\t平均值\tDouble\n * stddev()\t标准差\tDouble\n * length()\t数组长度\tInteger\n * sum()\t数组之和\tDouble\n * ==\tleft is equal to right (note that 1 is not equal to '1')\n * !=\tleft is not equal to right\n * <\tleft is less than right\n * <=\tleft is less or equal to right\n * >\tleft is greater than right\n * >=\tleft is greater than or equal to right\n * =~\tleft matches regular expression [?(@.name =~ /foo.*?/i)]\n * in\tleft exists in right [?(@.size in ['S', 'M'])]\n * nin\tleft does not exists in right\n * subsetof\t子集 [?(@.sizes subsetof ['S', 'M', 'L'])]\n * anyof\tleft has an intersection with right [?(@.sizes anyof ['M', 'L'])]\n * noneof\tleft has no intersection with right [?(@.sizes noneof ['M', 'L'])]\n * size\tsize of left (array or string) should match right\n * empty\tleft (array or string) should be empty\n *\n * groovy需要  \\$  Java不需要直接  $\n *\n */\nclass JsonUtil {\n\n//    private static Logger logger = LogManager.getLogger(JsonUtil.class)\n//\n//    /**\n//     * 用户构建对象,获取verify对象\n//     */\n//    private JSONObject json\n//\n//    private JsonUtil(JSONObject json) {\n//        this.json = json\n//    }\n//\n//    static JsonUtil getInstance(JSONObject json) {\n//        new JsonUtil(json)\n//    }\n//\n//    JsonVerify getVerify(String path) {\n//        JsonVerify.getInstance(this.json, path)\n//    }\n//\n//    /**\n//     * 获取string对象\n//     * @param path\n//     * @return\n//     */\n//    String getString(String path) {\n//        def object = get(path)\n//        object == null ? Constant.EMPTY : object.toString()\n//    }\n//\n//\n//    /**\n//     * 获取int类型\n//     * @param path\n//     * @return\n//     */\n//    int getInt(String path) {\n//        SourceCode.changeStringToInt(getString ( path))\n//    }\n//\n//    /**\n//     * 获取boolean类型\n//     * @param path\n//     * @return\n//     */\n//    int getBoolean(String path) {\n//        SourceCode.changeStringToBoolean(getString(path))\n//    }\n//\n//    /**\n//     * 获取long类型\n//     * @param path\n//     * @return\n//     */\n//    int getLong(String path) {\n//        SourceCode.changeStringToLong(getString(path))\n//    }\n//\n//    /**\n//     * 获取double类型\n//     * @param path\n//     * @return\n//     */\n//    double getDouble(String path) {\n//        SourceCode.changeStringToDouble(getString(path))\n//    }\n//\n//    /**\n//     * 获取list对象\n//     * @param path\n//     * @return\n//     */\n//    List getList(String path) {\n//        get(path) as List\n//    }\n//\n//    /**\n//     * 获取匹配对象,类型传参\n//     * 这里不加public  IDE会报错\n//     * @param path\n//     * @param tClass\n//     * @return\n//     */\n//    public <T> T getT(String path, Class<T> tClass) {\n//        try {\n//            get(path) as T\n//        } catch (ClassCastException e) {\n//            logger.warn(\"类型转换失败!\", e)\n//        }\n//    }\n//\n//    /**\n//     * 获取匹配对象,这里如果使用模糊路径匹配失败,返回\"[]\",如果绝对路径报错\n//     * @param path\n//     * @return\n//     */\n//    def get(String path) {\n//        logger.debug(\"匹配对象:{},表达式:{}\", json.toString(), path)\n//        if (json == null || json.isEmpty()) ParamException.fail(\"json为空或者null,参数错误!\")\n//        try {\n//            JsonPath.read(this.json, path)\n//        } catch (JsonPathException e) {\n//            logger.warn(\"json: {} 解析失败,path: {}\", json.toString(), path, e)\n//        }\n//    }\n//\n\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/utils/RWUtil.java",
    "content": "package com.funtester.utils;\n\nimport com.alibaba.fastjson.JSONObject;\nimport com.funtester.base.exception.FailException;\nimport com.funtester.base.exception.ParamException;\nimport com.funtester.config.Constant;\nimport com.funtester.frame.SourceCode;\nimport groovy.lang.Tuple2;\nimport org.apache.commons.lang3.StringUtils;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.io.*;\nimport java.net.URL;\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.stream.Collectors;\n\n/**\n * 文件读写类,与{@link FileUtil}有功能上的重合,原因在与Java和Groovy的不兼容问题.\n */\npublic class RWUtil extends Constant {\n\n    private static Logger logger = LogManager.getLogger(RWUtil.class);\n\n    /**\n     * 读取文件信息，返回json数据\n     *\n     * @param filePath\n     * @return\n     */\n    public static JSONObject readTxtByJson(String filePath) {\n        if (StringUtils.isEmpty(filePath) || !new File(filePath).exists() || new File(filePath).isDirectory())\n            ParamException.fail(\"配置文件信息错误!\" + filePath);\n        logger.debug(\"读取文件名：{}\", filePath);\n        List<String> lines = readTxtFileByLine(filePath);\n        JSONObject info = new JSONObject();\n        lines.forEach(line -> {\n            String[] split = line.split(\"=\", 2);\n            info.put(split[0], split[1]);\n        });\n        return info;\n    }\n\n    public static JSONObject readTxtByJson(String filePath, String filter) {\n        if (StringUtils.isEmpty(filePath) || !new File(filePath).exists() || new File(filePath).isDirectory())\n            ParamException.fail(\"配置文件信息错误!\" + filePath);\n        logger.debug(\"读取文件名：{}\", filePath);\n        List<String> lines = readTxtFileByLine(filePath, filter, false);\n        JSONObject info = new JSONObject();\n        lines.forEach(line -> {\n            String[] split = line.split(\"=\", 2);\n            info.put(split[0], split[1]);\n        });\n        return info;\n    }\n\n    /**\n     * 通过文件信息，返回string\n     *\n     * @param filePath\n     * @return\n     */\n    public static String readTextByString(String filePath) {\n        if (StringUtils.isEmpty(filePath) || !new File(filePath).exists() || new File(filePath).isDirectory())\n            ParamException.fail(\"配置文件信息错误!\" + filePath);\n        logger.debug(\"读取文件名：{}\", filePath);\n        List<String> list = readTxtFileByLine(filePath);\n        StringBuffer all = new StringBuffer();\n        list.forEach(line -> all.append(line + Constant.LINE));\n        return all.toString();\n    }\n\n    /**\n     * 分行读取txt文档，默认使用utf-8编码格式\n     *\n     * @param filePath 文件路径\n     * @return 返回list数组\n     */\n    public static List<String> readTxtFileByLine(String filePath) {\n        return readTxtFileByLine(filePath, Constant.EMPTY, true);\n    }\n\n    /**\n     * 分行读取txt文档，默认使用utf-8编码格式\n     * <p>line.contains(content) == key</p>\n     *\n     * @param filePath 文件路径\n     * @param content  过滤文本\n     * @param key      是否包含\n     * @return 返回list数组\n     */\n    public static List<String> readTxtFileByLine(String filePath, String content, boolean key) {\n        if (StringUtils.isEmpty(filePath) || !new File(filePath).exists() || new File(filePath).isDirectory())\n            ParamException.fail(\"文件信息错误!\" + filePath);\n        logger.debug(\"读取文件名：{}\", filePath);\n        List<String> lines = new ArrayList<>();\n        try {\n            String encoding = Constant.UTF_8.toString();\n            File file = new File(filePath);\n            if (file.isFile() && file.exists()) { // 判断文件是否存在\n                FileInputStream fileInputStream = new FileInputStream(file);\n                InputStreamReader read = new InputStreamReader(fileInputStream, encoding);// 考虑到编码格式\n                BufferedReader bufferedReader = new BufferedReader(read);\n                String line = null;\n                while ((line = bufferedReader.readLine()) != null) {\n                    if (line.contains(content) == key)\n                        lines.add(line);\n                }\n                bufferedReader.close();\n                read.close();\n                fileInputStream.close();\n            } else {\n                logger.warn(\"找不到指定的文件：{}\", filePath);\n            }\n        } catch (Exception e) {\n            logger.warn(\"读取文件内容出错\", e);\n        }\n        return lines;\n    }\n\n    /**\n     * 从配置文件中读取数字信息\n     *\n     * @param filePath\n     * @return\n     */\n    public static List<Integer> readTxtFileByNumLine(String filePath) {\n        return readTxtFileByLine(filePath, Constant.EMPTY, true).stream().map(x -> SourceCode.changeStringToInt(x)).collect(Collectors.toList());\n    }\n\n    /**\n     * 从配置文件中读取数字信息\n     *\n     * @param filePath\n     * @return\n     */\n    public static List<Double> readTxtFileByDoubleLine(String filePath) {\n        return readTxtFileByLine(filePath, Constant.EMPTY, true).stream().map(x -> SourceCode.changeStringToDouble(x)).collect(Collectors.toList());\n    }\n\n\n    /**\n     * 下载文件,目前只要针对图片\n     *\n     * @param url\n     */\n    public static void down(String url) {\n        Tuple2 tuple2 = FileUtil.handlePicName(url);\n        down(tuple2.getFirst().toString(), tuple2.getSecond().toString());\n    }\n\n    /**\n     * 通过url下载图片\n     *\n     * @param url\n     * @param name\n     */\n    public static void down(String url, String name) {\n        File file = new File(name);\n        logger.info(\"下载链接：{}，存储文件名：{}\", url, file.getAbsolutePath());\n        if (!file.exists())\n            try {\n                file.createNewFile();\n            } catch (IOException e) {\n                logger.warn(\"创建文件失败！\", e);\n            }\n        try (InputStream is = new URL(url).openStream(); OutputStream os = new FileOutputStream(file)) {\n            int bytesRead = 0;\n            byte[] buffer = new byte[1024];\n            while ((bytesRead = is.read(buffer)) != -1) {\n                os.write(buffer, 0, bytesRead);\n            }\n        } catch (IOException e) {\n            logger.warn(\"下载文件失败！\", e);\n        }\n    }\n\n    /**\n     * 复制文件\n     *\n     * @param oldPath 旧路径\n     * @param newPath 新路径\n     */\n    public static void copyFile(String oldPath, String newPath) {\n        logger.debug(\"源文件名：{}，目标文件名：{}\", oldPath, newPath);\n        int bytesum = 0;// 这个用来统计需要写入byte数组的长度\n        int byteread = 0;// 这个用来接收read()方法的返回值，表示读取内容的长度\n        File oldfile = new File(oldPath);// 获取源文件的file对象\n        if (oldfile.exists()) {// 文件存在时\n            try (InputStream inputStream = new FileInputStream(oldPath); FileOutputStream fileOutputStream = new FileOutputStream(newPath);) {\n                byte[] buffer = new byte[1024];// 新建读取文件所用的数组\n                // 此处用while循环每次按buffer读取文件直到读取完成\n                while ((byteread = inputStream.read(buffer)) != -1) {// 如何读取到文件末尾\n                    bytesum += byteread;// 此处计算读取长度，byteread表示每次读取的长度\n                    fileOutputStream.write(buffer, 0, byteread);// 此方法第一个参数是byte数组，第二次参数是开始位置，第三个参数是长度\n                }\n                logger.info(\"文件：{}，总大小是：\", oldfile, SourceCode.formatLong(bytesum));// 输出读取的总长度\n                fileOutputStream.flush();// 强制缓存输出，防止数据丢失\n            } catch (IOException e) {\n                FailException.fail(\"复制文件出错!\" + e.getMessage());\n            }\n        } else {\n            logger.warn(\"文件不存在！\");\n        }\n        // File oldfile2 = new File(oldPath);\n        // oldfile2.delete();\n    }\n\n    /**\n     * 写入文本信息，会自动新建文件\n     *\n     * @param file file对象，必须是存在的路径\n     * @param text 写入的内容，如果file存在，续写\n     */\n    public static void writeText(File file, String text) {\n        logger.debug(\"写入文件名：{}\", file);\n        if (!file.exists())\n            try {\n                file.createNewFile();\n            } catch (IOException e) {\n                logger.error(\"文件创建失败！\", e);\n            }\n        try {\n            FileWriter fileWriter = new FileWriter(file, true);\n            BufferedWriter bw1 = new BufferedWriter(fileWriter);\n            bw1.write(text);// 将内容写到文件中\n            bw1.flush();// 强制输出缓冲区内容\n            bw1.close();// 关闭流\n        } catch (IOException e) {\n            logger.warn(\"写入文件失败！\", e);\n        }\n    }\n\n\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/utils/Regex.java",
    "content": "package com.funtester.utils;\n\nimport com.funtester.base.exception.ParamException;\nimport com.funtester.frame.SourceCode;\nimport org.apache.commons.lang3.StringUtils;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.regex.Matcher;\nimport java.util.regex.Pattern;\n\n/**\n * 正则验证的封装\n */\npublic class Regex extends SourceCode {\n\n    private static Logger logger = LogManager.getLogger(Regex.class);\n\n    /**\n     * 正则校验文本是否匹配\n     *\n     * @param text  需要匹配的文本\n     * @param regex 正则表达式\n     * @return\n     */\n    public static boolean isRegex(String text, String regex) {\n        return matcher(text, regex).find();\n    }\n\n    /**\n     * 正则校验文本是否完全匹配，不包含其他杂项，相当于加上了^和$\n     *\n     * @param text  需要匹配的文本\n     * @param regex 正则表达式\n     * @return\n     */\n    public static boolean isMatch(String text, String regex) {\n        return matcher(text, regex).matches();\n    }\n\n    /**\n     * 获取匹配对象\n     *\n     * @param text\n     * @param regex\n     * @return\n     */\n    private static Matcher matcher(String text, String regex) {\n        if (StringUtils.isAnyBlank(text, regex)) ParamException.fail(\"正则参数错误!\");\n        return Pattern.compile(regex).matcher(text);\n    }\n\n    /**\n     * 返回所有匹配项\n     *\n     * @param text  需要匹配的文本\n     * @param regex 正则表达式\n     * @return\n     */\n    public static List<String> regexAll(String text, String regex) {\n        Matcher matcher = matcher(text, regex);\n        List<String> result = new ArrayList<>();\n        while (matcher.find()) {\n            result.add(matcher.group());\n        }\n        return result;\n    }\n\n    /**\n     * 获取第一个匹配对象\n     *\n     * @param text\n     * @param regex\n     * @return\n     */\n    public static String findFirst(String text, String regex) {\n        Matcher matcher = matcher(text, regex);\n        if (matcher.find()) return matcher.group();\n        return EMPTY;\n    }\n\n    /**\n     * 获取匹配项，不包含文字信息，会删除regex的内容\n     * <p>不保证完全正确</p>\n     *\n     * @param text\n     * @param regex\n     * @return\n     */\n    @Deprecated\n    public static String getRegex(String text, String regex) {\n        if (StringUtils.isAnyBlank(text, regex)) ParamException.fail(\"正则参数错误!\");\n        String result = EMPTY;\n        try {\n            result = regexAll(text, regex).get(0);\n            String[] split = regex.split(\"(\\\\.|\\\\+|\\\\*|\\\\?)\");\n            for (int i = 0; i < split.length; i++) {\n                String s1 = split[i];\n                if (!s1.isEmpty())\n                    result = result.replaceAll(s1, EMPTY);\n            }\n        } catch (Exception e) {\n            logger.warn(\"获取匹配对象失败！\", e);\n        } finally {\n            return result;\n        }\n    }\n\n\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/utils/StringUtil.groovy",
    "content": "package com.funtester.utils\n\nimport com.funtester.frame.SourceCode\n\nimport java.util.stream.Collectors\n\n/**\n * 处理各种字符串的工具类\n */\nclass StringUtil extends SourceCode {\n\n    private static final char[] chars = ['0', '1', '2', '3', '4', '5', '6', '7', '8', '9', (char) 65, (char) 66, (char) 67, (char) 68, (char) 69, (char) 70, (char) 71, (char) 72, (char) 73, (char) 74, (char) 75, (char) 76, (char) 77, (char) 78, (char) 79, (char) 80, (char) 81, (char) 82, (char) 83, (char) 84, (char) 85, (char) 86, (char) 87, (char) 88, (char) 89, (char) 90, (char) 97, (char) 98, (char) 99, (char) 100, (char) 101, (char) 102, (char) 103, (char) 104, (char) 105, (char) 106, (char) 107, (char) 108, (char) 109, (char) 110, (char) 111, (char) 112, (char) 113, (char) 114, (char) 115, (char) 116, (char) 117, (char) 118, (char) 119, (char) 120, (char) 121, (char) 122]\n\n    /**\n     * emoji表情\n     */\n    private static final String[] EMOJIS = [\"☢\", \"💸\", \"💷\", \"💶\", \"💵\", \"💼\", \"💻\", \"💰\", \"💮\", \"💴\", \"💳\", \"💨\", \"💧\", \"💦\", \"💪\", \"💡\", \"📘\", \"📗\", \"📖\", \"📕\", \"📜\", \"📚\", \"📙\", \"📐\", \"📏\", \"📎\", \"📍\", \"📔\", \"📓\", \"📑\", \"📈\", \"📇\", \"📆\", \"📅\", \"📌\", \"📋\", \"📊\", \"📉\", \"📀\", \"💿\", \"💾\", \"💽\", \"📄\", \"📃\", \"📂\", \"📁\", \"📷\", \"📼\", \"📻\", \"📺\", \"📹\", \"📰\", \"📯\", \"📮\", \"📭\", \"📲\", \"📱\", \"📨\", \"📧\", \"📦\", \"📥\", \"📬\", \"📫\", \"📪\", \"📩\", \"📠\", \"📟\", \"📞\", \"📝\", \"📤\", \"📡\", \"🔗\", \"🔖\", \"🔐\", \"🔏\", \"🔎\", \"🔍\", \"🔓\", \"🔒\", \"🔑\", \"🔌\", \"🔋\", \"🔮\", \"🔭\", \"🔨\", \"🔧\", \"🔦\", \"🔥\", \"🔬\", \"🔫\", \"🔪\", \"🔩\", \"🦋\", \"😘\", \"😗\", \"😖\", \"😕\", \"😜\", \"😛\", \"😚\", \"😙\", \"😐\", \"😏\", \"😎\", \"😍\", \"😔\", \"😓\", \"😒\", \"😑\", \"😇\", \"😆\", \"😅\", \"😌\", \"😋\", \"😊\", \"😉\", \"😀\", \"😄\", \"😃\", \"😂\", \"😁\", \"😸\", \"😷\", \"😶\", \"😵\", \"😼\", \"😻\", \"😺\", \"😹\", \"😰\", \"😯\", \"😮\", \"😭\", \"😴\", \"😳\", \"😲\", \"😱\", \"😨\", \"😧\", \"😦\", \"😥\", \"😬\", \"😫\", \"😪\", \"😠\", \"😟\", \"😞\", \"😝\", \"😤\", \"😣\", \"😢\", \"😡\", \"🙏\", \"🙎\", \"🙍\", \"🙈\", \"🙇\", \"🙆\", \"🙅\", \"🙌\", \"🙋\", \"🙊\", \"🙉\", \"🙀\", \"😿\", \"😾\", \"😽\", \"🚘\", \"🚗\", \"🚖\", \"🚕\", \"🚜\", \"🚛\", \"🚚\", \"✏✒\", \"🚐\", \"🚏\", \"🚎\", \"🚍\", \"🚔\", \"🚓\", \"🚒\", \"🚑\", \"🚈\", \"🚌\", \"🚋\", \"🚊\", \"🚉\", \"☀\", \"☁\", \"🚶\", \"🚵\", \"🚴\", \"🚲\", \"☎\", \"🚨\", \"🚥\", \"🚬\", \"☔\", \"☕\", \"🚪\", \"🚩\", \"🚞\", \"🚝\", \"🚣\", \"🛀\", \"🚿\", \"🚽\", \"🛁\", \"🌗\", \"🌖\", \"🌕\", \"🌔\", \"🌛\", \"🌚\", \"🌙\", \"🌘\", \"♈\", \"🌏\", \"♉\", \"🌎\", \"♊\", \"🌍\", \"♋\", \"♌\", \"🌓\", \"♍\", \"🌒\", \"♎\", \"🌑\", \"♏\", \"🌐\", \"♐\", \"♑\", \"♒\", \"♓\", \"🌊\", \"🌂\", \"🌷\", \"🌵\", \"🌴\", \"🌻\", \"🌺\", \"🌹\", \"🌸\", \"🌳\", \"🌲\", \"🌱\", \"🌰\", \"🌟\", \"🌞\", \"🌝\", \"🌜\", \"🌠\", \"🍗\", \"🍖\", \"🍕\", \"🍔\", \"🍛\", \"🍚\", \"🍙\", \"🍘\", \"🍏\", \"🍎\", \"🍍\", \"🍌\", \"🍓\", \"🍒\", \"🍑\", \"🍐\", \"🍇\", \"🍆\", \"🍅\", \"🍄\", \"🍋\", \"🍊\", \"🍉\", \"🍈\", \"🌿\", \"🌾\", \"🌽\", \"🌼\", \"🍃\", \"🍂\", \"🍁\", \"🍀\", \"🍷\", \"🍶\", \"⚡\", \"🍵\", \"🍴\", \"🍻\", \"🍺\", \"🍹\", \"🍸\", \"🍯\", \"🍮\", \"🍭\", \"🍬\", \"🍳\", \"🍲\", \"🍱\", \"🍰\", \"🍧\", \"🍦\", \"🍥\", \"🍤\", \"🍫\", \"🍪\", \"🍩\", \"🍨\", \"🍟\", \"🍞\", \"🍝\", \"🍜\", \"🍣\", \"🍢\", \"⚽\", \"🍡\", \"⚾\", \"🍠\", \"⛅\", \"🎏\", \"🎎\", \"🎌\", \"🎓\", \"🎒\", \"⛎\", \"🎐\", \"🎅\", \"🎊\", \"🎉\", \"🎈\", \"🍼\", \"🎂\", \"🎁\", \"🎀\", \"🎷\", \"🎻\", \"🎺\", \"🎸\", \"🎯\", \"🎮\", \"🎭\", \"🎬\", \"🎳\", \"🎲\", \"🎱\", \"🎰\", \"🎥\", \"🎫\", \"🎪\", \"🎩\", \"🎨\", \"🎣\", \"✂\", \"✉\", \"✊\", \"✋\", \"✌\", \"🏇\", \"🏆\", \"🏄\", \"🏊\", \"🏉\", \"🏈\", \"🎿\", \"🎾\", \"🎽\", \"⌚\", \"⌛\", \"🏃\", \"🏂\", \"🏀\", \"🏮\", \"❄\", \"⭐\", \"🐘\", \"🐗\", \"🐖\", \"🐕\", \"🐜\", \"🐛\", \"🐚\", \"🐙\", \"🐐\", \"🐏\", \"🐎\", \"🐍\", \"🐔\", \"🐓\", \"🐒\", \"🐑\", \"🐈\", \"🐇\", \"🐆\", \"🐅\", \"🐌\", \"🐋\", \"🐊\", \"🐉\", \"🐀\", \"🐄\", \"🐃\", \"🐂\", \"🐁\", \"🐸\", \"🐷\", \"🐶\", \"🐵\", \"🐼\", \"🐻\", \"🐺\", \"🐹\", \"🐰\", \"🐯\", \"🐮\", \"🐭\", \"🐴\", \"🐳\", \"🐲\", \"🐱\", \"🐨\", \"🐧\", \"🐦\", \"🐥\", \"🐬\", \"🐫\", \"🐪\", \"🐩\", \"🐠\", \"🐟\", \"🐞\", \"🐝\", \"🐤\", \"🐣\", \"🐢\", \"🐡\", \"👘\", \"👗\", \"👖\", \"👕\", \"👜\", \"👛\", \"👚\", \"👙\", \"👐\", \"👏\", \"👎\", \"👍\", \"👔\", \"👓\", \"👒\", \"👑\", \"👈\", \"👇\", \"👆\", \"👅\", \"👌\", \"👋\", \"👊\", \"👉\", \"👀\", \"🐾\", \"🐽\", \"👄\", \"👃\", \"👂\", \"👸\", \"👷\", \"👶\", \"👵\", \"👼\", \"👰\", \"👯\", \"👮\", \"👭\", \"👴\", \"👳\", \"👲\", \"👱\", \"👨\", \"👧\", \"👦\", \"👥\", \"👬\", \"👫\", \"👪\", \"👩\", \"👠\", \"👟\", \"👞\", \"👝\", \"👤\", \"👣\", \"👢\", \"👡\", \"💐\", \"💏\", \"💎\", \"💍\", \"💑\", \"⏰\", \"💈\", \"💇\", \"💆\", \"💅\", \"⏳\", \"💌\", \"💋\", \"💊\", \"💉\", \"💄\", \"💃\", \"💂\", \"💁\"]\n\n    /**\n     * 序号\n     */\n    private static final String[] SERIAL = [\"⓪\", \"①\", \"②\", \"③\", \"④\", \"⑤\", \"⑥\", \"⑦\", \"⑧\", \"⑨\", \"⑩\", \"⑪\", \"⑫\", \"⑬\", \"⑭\", \"⑮\", \"⑯\", \"⑰\", \"⑱\", \"⑲\", \"⑳\"]\n\n    /**\n     * 小写汉字数字\n     */\n    private static final String[] chineses = [\"〇\", \"一\", \"二\", \"三\", \"四\", \"五\", \"六\", \"七\", \"八\", \"九\"]\n\n    /**\n     * 大写汉字数字\n     */\n    private static final String[] capeChineses = [\"零\", \"壹\", \"贰\", \"叁\", \"肆\", \"伍\", \"陆\", \"柒\", \"捌\", \"玖\"]\n\n    /**\n     * 获取随机字符串\n     * @param i\n     * @return\n     */\n    static String getString(int i) {\n        def re = new StringBuffer()\n        if (i < 1) return re\n        for (int j in 1..i) {\n            re << getChar()\n        }\n        re.toString()\n    }\n\n    /**\n     * 获取随机字符\n     * @return\n     */\n    static char getChar() {\n        chars[getRandomInt(62) - 1]\n    }\n\n    /**\n     * 获取随机字母，区分大小写\n     *\n     * @return\n     */\n    static char getWord() {\n        chars[getRandomInt(52) + 9];\n    }\n\n    /**\n     * 获取随机字符串，没有数字\n     * @param i\n     * @return\n     */\n    static String getStringWithoutNum(int i) {\n        def re = new StringBuffer()\n        if (i < 1) return re\n        for (int j in 1..i) {\n            re << getWord()\n        }\n        re.toString()\n    }\n\n    /**\n     * 获取所有小写字母\n     * @return\n     */\n    static String getLowWords() {\n        \"abcdefghijklmnopqrstuvwxyz\"\n    }\n\n    /**\n     * 获取所有大写字母\n     * @return\n     */\n    static String getUpWords() {\n        \"ABCDEFGHIJKLMNOPQRSTUVWXYZ\"\n    }\n\n    /**\n     * 获取所有的数字\n     * @return\n     */\n    static String getNumbers() {\n        \"0123456789\"\n    }\n\n    /**\n     * 将int类型转化为汉子数字，对于3位数的数字自动补零\n     * @param i\n     * @return\n     */\n    static String getChinese(int i) {\n        if (i <= 0) return \"〇〇〇\"\n        String num = (i + EMPTY).collect { x -> chineses[changeStringToInt(x)] }.join()\n        num.length() > 2 ? num : getManyString(chineses[0] + EMPTY, 3 - num.length()) + num\n    }\n\n    /**\n     * 将int类型转化汉字大写数字表示，对于3位数的数字自动补零\n     * @param i\n     * @return\n     */\n    static String getCapeChinese(int i) {\n        if (i <= 0) return \"零零零\"\n        def num = (i + EMPTY).collect { x -> capeChineses[changeStringToInt(x)] }.join()\n        num.length() > 2 ? num : getManyString(capeChineses[0] + EMPTY, 3 - num.length()) + num\n    }\n\n    /**\n     * 随机获取emoji表情数\n     *\n     * @param size\n     * @return\n     */\n    static String getEmojis(int size) {\n        range(size).map { x -> getEmojis() }.collect(Collectors.toString());\n    }\n\n    /**\n     * 获取序号符号\n     *\n     * @param i\n     * @return\n     */\n    static String getSerialEmoji(int i) {\n        (i < 0 || i > 20) ? EMOJIS[0] : SERIAL[i];\n    }\n\n    /**\n     * 随机获取emoji表情\n     *\n     * @return\n     */\n    static String getEmojis() {\n        EMOJIS[getRandomInt(EMOJIS.length - 1)];\n    }\n\n\n    /**\n     * 返回一个居中的字符串\n     * @param str\n     * @param size\n     * @return\n     */\n    static String center(String str, int size) {\n        str.center(size)\n    }\n\n    /**\n     * 返回一个居左的文本\n     * @param str\n     * @param size\n     * @return\n     */\n    static String left(String str, int size) {\n        str.padLeft(size)\n    }\n\n    /**\n     * 返回一个居右的文本\n     * @param str\n     * @param size\n     * @return\n     */\n    static String right(String str, int size) {\n        str.padRight(size)\n    }\n\n\n//这个是添加新的的emoji表情的方法\n//    static void main(String[] args) {\n//        String aa = \"\";\n//        String aaa = EMPTY;\n//        for (int i = 0; i < aa.length(); i += 2) {\n//            String abc = aa.substring(i, i + 2);\n//            aaa = aaa + \"\\\"\" + aa.substring(i, i + 2) + \"\\\",\";\n//        }\n//        output(aaa);\n//        aaa = EMPTY;\n//        int length = EMOJIS.length;\n//        HashSet<String> strings = new HashSet<>(Arrays.asList(EMOJIS));\n//        for (String string : strings) {\n//            aaa = aaa + \"\\\"\" + string + \"\\\",\";\n//        }\n//        output(aaa);\n//        output(length, strings.size());\n//    }\n}"
  },
  {
    "path": "src/main/groovy/com/funtester/utils/Time.java",
    "content": "package com.funtester.utils;\n\nimport com.funtester.config.Constant;\nimport com.funtester.frame.SourceCode;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\nimport java.text.ParseException;\nimport java.text.SimpleDateFormat;\nimport java.util.Calendar;\nimport java.util.Date;\n\n/**\n * 时间相关功能工具类\n */\npublic class Time extends SourceCode {\n\n    private static Logger logger = LogManager.getLogger(Time.class);\n\n    /**\n     * 默认的日志显示格式\n     */\n    private static ThreadLocal<SimpleDateFormat> DEFAULT_FORMAT = new ThreadLocal() {\n        @Override\n        protected SimpleDateFormat initialValue() {\n            return new SimpleDateFormat(\"yyyy-MM-dd HH:mm:ss\");\n        }\n    };\n\n    /**\n     * 纯数字的日期格式\n     */\n    private static ThreadLocal<SimpleDateFormat> NUM_FORMAT = new ThreadLocal() {\n        @Override\n        protected SimpleDateFormat initialValue() {\n            return new SimpleDateFormat(\"yyyyMMddHHmmss\");\n        }\n    };\n\n    /**\n     * 标记日期格式,选用ddHHmm\n     */\n    private static ThreadLocal<SimpleDateFormat> MARK_FORMAT = new ThreadLocal() {\n        @Override\n        protected SimpleDateFormat initialValue() {\n            return new SimpleDateFormat(\"ddHHmm\");\n        }\n    };\n\n    /**\n     * 获取calendar类对象，默认UTC时间\n     *\n     * @return\n     */\n    private static ThreadLocal<Calendar> calendar = new ThreadLocal() {\n        @Override\n        protected Calendar initialValue() {\n            Calendar calendar = Calendar.getInstance();\n            calendar.setTime(new Date());\n            return calendar;\n        }\n    };\n\n    /**\n     * 获取时间戳，13位long类型\n     *\n     * @return\n     */\n    public static long getTimeStamp() {\n        return System.currentTimeMillis();\n    }\n\n    /**\n     * 获取一天开始，utc\n     *\n     * @return\n     */\n    public static String getStartOfDay() {\n        return getUtcDate() + \" 00:00:00\";\n    }\n\n    /**\n     * 获取一天结束，utc\n     *\n     * @return\n     */\n    public static String getEndOfDay() {\n        return getUtcDate() + \" 23:55:55\";\n    }\n\n    /**\n     * 获取当天日期，utc\n     *\n     * @return\n     */\n    public static String getUtcDate() {\n        int month = getMonth();\n        int day = getDay();\n        return getYear() + \"-\" + (month < 10 ? \"0\" + month : month) + \"-\" + (day < 10 ? \"0\" + day : day);\n    }\n\n    /**\n     * 获取时间戳\n     *\n     * @param time 传入时间，纯数字\n     * @return 返回时间戳，毫秒\n     */\n    public static long getUtcTimestamp(String time) {\n        long timestamp = getTimeStamp(time);\n        long utc = timestamp - Calendar.getInstance().getTimeZone().getRawOffset();\n        return utc;\n    }\n\n    /**\n     * 获取UTC时间戳\n     *\n     * @param time 纯数字日期\n     * @return\n     */\n    public static long getUtcTimestamp(long time) {\n        return getUtcTimestamp(time + EMPTY);\n    }\n\n    /**\n     * 获取当前星期数（按年）\n     *\n     * @return\n     */\n    public static int getWeeksNum() {\n        return calendar.get().get(Calendar.WEEK_OF_YEAR);\n    }\n\n    /**\n     * 获取月份,获取值+1,索引从0开始的\n     *\n     * @return\n     */\n    public static int getMonth() {\n        return calendar.get().get(Calendar.MONTH) + 1;\n    }\n\n    /**\n     * 获取当前是当月的第几天\n     *\n     * @return\n     */\n    public static int getDay() {\n        return calendar.get().get(Calendar.DAY_OF_MONTH);\n    }\n\n    /**\n     * 获取年份\n     *\n     * @return\n     */\n    public static int getYear() {\n        return calendar.get().get(Calendar.YEAR);\n    }\n\n    public static int getHour() {\n        return calendar.get().get(Calendar.HOUR_OF_DAY);\n    }\n\n    public static int getMinute() {\n        return calendar.get().get(Calendar.MINUTE);\n    }\n\n    public static int getSecond() {\n        return calendar.get().get(Calendar.SECOND);\n    }\n\n    /**\n     * 获取当前时间\n     *\n     * @return 返回当前时间\n     */\n    public static String getNow() {\n        return getNow(NUM_FORMAT.get());\n    }\n\n    public static String markDate() {\n        return getNow(MARK_FORMAT.get());\n    }\n\n    public static String getNow(String format) {\n        return getNow(new SimpleDateFormat(format));\n    }\n\n    public static String getNow(SimpleDateFormat now) {\n        return now.format(new Date());\n    }\n\n    /**\n     * 获取时间戳,会替换掉所有非数字的字符\n     * 默认返回{@link Constant#DEFAULT_LONG}\n     *\n     * @param time 传入时间，纯数字组成的时间\n     * @return 返回时间戳，毫秒\n     */\n    public static long getTimeStamp(String time) {\n        time = time.replaceAll(\"\\\\D*\", EMPTY);\n        try {\n            return NUM_FORMAT.get().parse(time).getTime();\n        } catch (ParseException e) {\n            logger.warn(\"时间格式错误！\", e);\n        }\n        return DEFAULT_LONG;\n    }\n\n    /**\n     * 根据时间戳返回对应的时间，并且输出\n     *\n     * @param time long 时间戳\n     * @return 返回时间\n     */\n    public static String getTimeByTimestamp(long time) {\n        Date now = new Date(time);\n        String nowTime = DEFAULT_FORMAT.get().format(now);\n        return nowTime;\n    }\n\n    /**\n     * 获取时间差，以秒为单位\n     *\n     * @param start 开始时间\n     * @param end   结束时间\n     * @return\n     */\n    @Deprecated\n    public static double getTimeDiffer(Date start, Date end) {\n        return getTimeDiffer(start.getTime(), end.getTime());\n    }\n\n    /**\n     * 重载，用long类型取代date\n     *\n     * @param start\n     * @param end\n     * @return\n     */\n    public static double getTimeDiffer(long start, long end) {\n        return (end - start) / 1000.0;\n    }\n\n    /**\n     * 获取当前时间，返回date类型\n     *\n     * @return\n     */\n    public static String getDate() {\n        return getNow(DEFAULT_FORMAT.get());\n    }\n\n\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/utils/TimeWatch.groovy",
    "content": "package com.funtester.utils\n\n\nimport com.funtester.frame.SourceCode\nimport org.apache.logging.log4j.LogManager\nimport org.apache.logging.log4j.Logger\n\nimport static com.funtester.config.Constant.LINE\nimport static com.funtester.config.Constant.TAB\nimport static com.funtester.frame.SourceCode.formatLong\nimport static com.funtester.frame.SourceCode.getNanoMark\n/**\n * 时间观察者类，用于简单记录执行时间\n */\nclass TimeWatch implements Serializable {\n\n    private static final long serialVersionUID = -4156600036913348727L;\n\n    private static Logger logger = LogManager.getLogger(TimeWatch.class)\n\n    /**\n     * 默认的名称\n     */\n    def name = \"default\"\n\n    /**\n     * 纳秒\n     */\n    def startNano\n    /**\n     * 标记集合\n     */\n\n    def marks = new HashMap<String, Mark>()\n\n    /**\n     * 毫秒\n     */\n    def startMillis\n\n    /**\n     * 无参创建方法，默认名称\n     * @return\n     */\n    static TimeWatch create() {\n        final TimeWatch timeWatch = new TimeWatch()\n        timeWatch.start()\n    }\n\n    /**\n     * 创建方法\n     * @param name\n     * @return\n     */\n    static TimeWatch create(def name) {\n        final TimeWatch timeWatch = new TimeWatch()\n        timeWatch.start()\n    }\n\n\n    private TimeWatch() {\n    }\n\n    /**\n     * 开始记录\n     * @return\n     */\n    def start() {\n        reset()\n    }\n\n    /**\n     * 重置\n     */\n    def reset() {\n        startNano = SourceCode.getNanoMark()\n        startMillis = Time.getTimeStamp()\n        this\n    }\n\n    /**\n     * 标记\n     * @param name\n     * @return\n     */\n    String mark(String name) {\n        marks.put name, new Mark(name)\n        name\n    }\n\n    /**\n     * 标记\n     * @return\n     */\n    String mark() {\n        mark(name)\n    }\n\n    /**\n     * 获取标记时间\n     * @return\n     */\n    def getMarkTime() {\n        if (marks.containsKey(name)) {\n            def diff = Time.getTimeStamp() - marks.get(name).getStartMillis()\n            logger.info(LINE + \"观察者：{}的标记：{}记录时间：{} ms\", name, name, formatLong(diff))\n        } else {\n            logger.warn(\"没有默认标记！\")\n        }\n    }\n\n    /**\n     * 获取标记时间\n     * @return\n     */\n    def getMarkNanoTime() {\n        if (marks.containsKey(name)) {\n            def diff = getNanoMark() - marks.get(name).getStartNano()\n            logger.info(LINE + \"观察者：{}的标记：{}记录时间：{} ns\", name, name, formatLong(diff))\n        } else {\n            logger.warn(\"没有默认标记！\")\n        }\n    }\n\n\n    /**\n     * 获取某个标记的记录时间\n     * @param name\n     * @return\n     */\n    def getMarkTime(String name) {\n        if (marks.containsKey(name)) {\n            def diff = Time.getTimeStamp() - marks.get(name).getStartMillis()\n            logger.info(LINE + \"观察者：{}的标记：{}记录时间：{} ms\", name, name, formatLong(diff))\n        } else {\n            logger.warn(\"没有{}标记！\", name)\n        }\n    }\n\n    /**\n     * 获取某个标记的记录时间\n     * @param name\n     * @return\n     */\n    def getMarkNanoTime(String name) {\n        if (marks.containsKey(name)) {\n            def diff = getNanoMark() - marks.get(name).getStartNano()\n            logger.info(LINE + \"观察者：{}的标记：{}记录时间：{} ns\", name, name, formatLong(diff))\n        } else {\n            logger.warn(\"没有{}标记！\", name)\n        }\n    }\n\n\n    /**\n     * 获取记录时间\n     * @return\n     */\n    def getTime() {\n        def diff = Time.getTimeStamp() - startMillis\n        logger.info(LINE + \"观察者：{}，记录时间：{} ms\", getName(), formatLong(diff))\n        diff\n    }\n\n    /**\n     * 获取记录时间纳秒\n     * @return\n     */\n    def getNanoTime() {\n        long diff = getNanoMark() - startNano\n        logger.info(LINE + \"观察者：{}，记录时间：{} ns\", getName(), formatLong(diff))\n        diff\n    }\n\n    /**\n     * 获取标记与观察者的时间差\n     * @param name\n     * @return\n     */\n    def getDiffTime(String name) {\n        if (marks.containsKey(name)) {\n            def diff = marks.get(name).getStartMillis() - this.getStartMillis()\n            logger.info(LINE + \"观察者：{}和标记：{}记录时间差：{} ms\", name, name, formatLong(diff))\n        } else {\n            logger.warn(\"没有{}标记！\", name)\n        }\n    }\n\n    /**\n     * 获取标记与观察者的时间差\n     * @param name\n     * @return\n     */\n    def getDiffNanoTime(String name) {\n        if (marks.containsKey(name)) {\n            def diff = marks.get(name).getStartNano() - this.getStartNano()\n            logger.info(LINE + \"观察者：{}和标记：{}记录时间差：{} ns\", name, name, formatLong(diff > 0 ? diff : -diff))\n        } else {\n            logger.warn(\"没有{}标记！\", name)\n        }\n    }\n\n    /**\n     * 获取两个标记的时间差\n     * @param first\n     * @param second\n     * @return\n     */\n    def getDiffTime(String first, String second) {\n        if (marks.containsKey(first) && marks.containsKey(second)) {\n            def diff = marks.get(second).getStartMillis() - marks.get(first).getStartMillis()\n            logger.info(LINE + \"标记：{}和标记：{}记录时间差：{} ms\", first, second, diff)\n        } else {\n            logger.warn(\"没有{}标记！\", first + TAB + second)\n        }\n    }\n\n\n    /**\n     * 获取两个标记的时间差\n     * @param first\n     * @param second\n     * @return\n     */\n    def getDiffNanoTime(String first, String second) {\n        if (marks.containsKey(first) && marks.containsKey(second)) {\n            def diff = marks.get(second).getStartNano() - marks.get(first).getStartNano()\n            logger.info(LINE + \"标记：{}和标记：{}记录时间差：{} ns\", first, second, formatLong(diff))\n        } else {\n            logger.warn(\"没有{}标记！\", first + TAB + second)\n        }\n    }\n\n    @Override\n    String toString() {\n        return \"时间观察者：\" + this.name\n    }\n\n    @Override\n    TimeWatch clone() {\n        TimeWatch watch = new TimeWatch()\n        watch.name = getName() + \"_c\"\n        watch.startMillis = this.getStartMillis()\n        watch.startNano = this.getStartNano()\n        watch\n    }\n    /**\n     * 标记类\n     */\n    class Mark implements Serializable {\n\n        private static final long serialVersionUID = -41564036913335727L;\n\n        Mark(def name) {\n            this.name = name\n            reset()\n        }\n\n        def name\n\n        def startNano\n\n        def startMillis\n\n        def lastNano\n\n        def lastMills\n\n        def reset() {\n            this.startNano = getNanoMark()\n            this.startMillis = Time.getTimeStamp()\n        }\n    }\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/utils/WriteHtml.java",
    "content": "package com.funtester.utils;\n\nimport java.util.List;\n\n/**\n * 生成表格封装类\n */\npublic class WriteHtml {\n\n    /**\n     * 获取表格头部信息\n     *\n     * @param list\n     * @return\n     */\n    private static String getTable(List<Object> list) {\n        StringBuffer start = new StringBuffer(\"<table align=\\\"center\\\"  border='1' style='table-layout:fixed;font-size:16px;'><thead><tr style='word-wrap:break-word;word-break:break-all'>\");\n        start.append(\"<td>序号</td>\");\n        for (int i = 0; i < list.size(); i++) {\n            start.append(\"<td>\" + list.get(i).toString() + \"</td>\");\n        }\n        String end = \"</tr></thead><tbody>\";\n        return start + end;\n    }\n\n    /**\n     * 拼接整个页面\n     *\n     * @param result\n     * @param title\n     * @return\n     */\n    public static String createWebReport(List<List<Object>> result, String title) {\n        String starttext = \"<html><head><meta http-equiv='Content-Type' content='text/html; charset=UTF-8'></head><h1 style='text-align:center'>\" + title + \"</h1>\" + getTable(result.get(0));\n        String endtext = \"<script src=\\\"http://blog.fv1314.xyz/blog/js/bubbly.js\\\"></script><script src=\\\"http://blog.fv1314.xyz/blog/js/home.js\\\"></script></tbody></table></body></html>\";\n        StringBuffer sheet = new StringBuffer(starttext);\n        for (int i = 1; i < result.size(); i++) {\n            List<Object> objects = result.get(i);\n            sheet.append(\"<tr>\");\n            sheet.append(\"<td style='word-wrap:break-word;word-break:break-all'>\" + i + \"</td>\");\n            sheet.append(getTr(objects));\n            sheet.append(\"</tr>\");\n        }\n        sheet.append(endtext);\n        return sheet.toString();\n    }\n\n    /**\n     * 获取行信息\n     *\n     * @param tr\n     * @return\n     */\n    private static StringBuffer getTr(List<Object> tr) {\n        StringBuffer body = new StringBuffer();\n        for (int i = 0; i < tr.size(); i++) {\n            body.append(\"<td style='word-wrap:break-word;word-break:break-all'>\" + tr.get(i).toString() + \"</td>\");\n        }\n        return body;\n    }\n\n\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/utils/message/AlertOver.java",
    "content": "package com.funtester.utils.message;\n\nimport com.funtester.base.bean.RequestInfo;\nimport com.funtester.base.interfaces.IMessage;\nimport com.funtester.httpclient.FunLibrary;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\npublic class AlertOver extends FunLibrary implements IMessage {\n\n    private static Logger logger = LogManager.getLogger(AlertOver.class);\n\n    String title;\n\n    String content;\n\n    String murl;\n\n    RequestInfo requestInfo;\n\n    private static String system = \"s-7e93ec02-1308-480c-bc11-a7260c14\";//系统异常\n\n    private static String function = \"s-7e3b7ea5-b4b0-4479-a0e3-bce6c830\";//功能异常\n\n    private static String business = \"s-466a191a-cbb8-4164-b8be-9779bb88\";//业务异常\n\n    private static String remind = \"s-f49ac5bc-008b-4b11-890e-6715ef89\";//提醒推送\n\n    private static String code = \"s-490d0fc6-35cc-4430-9f87-09cdeb05\";//程序异常\n\n    private static final String testGroup = \"g-4eefc0ad-19af-4b1c-9d0b-ef87be15\";\n\n    public AlertOver() {\n        this(\"test title\", \"test content!\");\n    }\n\n    public AlertOver(String title, String content) {\n        this.title = title;\n        this.content = content + LINE + \"发送源：\" + COMPUTER_USER_NAME;\n    }\n\n    public AlertOver(String title, String content, String url) {\n        this(title, content);\n        this.murl = url;\n    }\n\n    public AlertOver(String title, String content, String url, RequestInfo requestInfo) {\n        this(title, content);\n        this.murl = url;\n        this.requestInfo = requestInfo;\n    }\n\n    /**\n     * 发送系统异常\n     */\n    public void sendSystemMessage() {\n//        if (SysInit.isBlack(murl)) return;\n//        sendMessage(system);\n//        MySqlTest.saveAlertOverMessage(requestInfo, \"system\", title, LOCAL_IP, COMPUTER_USER_NAME);\n//        logger.info(\"发送系统错误提醒，title：{}，ip：{}，computer：{}\", title, LOCAL_IP, COMPUTER_USER_NAME);\n    }\n\n    /**\n     * 发送功能异常\n     */\n    public void sendFunctionMessage() {\n        sendMessage(function);\n    }\n\n    /**\n     * 发送业务异常\n     */\n    public void sendBusinessMessage() {\n        sendMessage(business);\n    }\n\n    /**\n     * 发送程序异常\n     */\n    public void sendCodeMessage() {\n        sendMessage(code);\n    }\n\n    /**\n     * 提醒推送\n     */\n    public void sendRemindMessage() {\n        sendMessage(remind);\n    }\n\n    /**\n     * 发送消息\n     *\n     * @return\n     */\n    public void sendMessage(String source) {\n//        if (SysInit.isBlack(murl)) return;\n//        String url = \"https://api.alertover.com/v1/alert\";\n//        String receiver = testGroup;//测试组ID\n//        JSONObject jsonObject = new JSONObject();// 新建json数组\n//        jsonObject.put(\"frame\", source);// 添加发送源id\n//        jsonObject.put(\"receiver\", receiver);// 添加接收组id\n//        jsonObject.put(\"content\", content);// 发送内容\n//        jsonObject.put(\"title\", title);// 发送标题\n//        jsonObject.put(\"url\", murl);// 发送标题\n//        jsonObject.put(\"sound\", \"pianobar\");// 发送声音\n//        logger.debug(\"消息详情：{}\", jsonObject.toString());\n//        HttpPost httpPost = getHttpPost(url, jsonObject);\n        /*取消发送*/\n//        getHttpResponse(httpPost);\n    }\n\n\n}"
  },
  {
    "path": "src/main/groovy/com/funtester/utils/message/EmailUtil.java",
    "content": "package com.funtester.utils.message;\n\nimport com.funtester.frame.SourceCode;\n\n/**\n * 发送邮件的类，暂时使用静态方法，默认使用QQ邮箱发送\n */\npublic class EmailUtil extends SourceCode {\n\n//    private static Logger logger = LogManager.getLogger(EmailUtil.class);\n//\n//    private static Session session;\n//\n//    private static void instance() {\n//        Security.addProvider(new Provider());\n//        //设置邮件会话参数\n//        Properties props = new Properties();\n//        //邮箱的发送服务器地址\n//        props.setProperty(\"mail.smtp.host\", EmailConstant.QQ_HOST);\n//        props.setProperty(\"mail.smtp.socketFactory.class\", EmailConstant.SSL_FACTORY);\n//        props.setProperty(\"mail.smtp.socketFactory.fallback\", \"false\");\n//        //邮箱发送服务器端口,这里设置为465端口\n//        props.setProperty(\"mail.smtp.port\", \"465\");\n//        props.setProperty(\"mail.smtp.socketFactory.port\", \"465\");\n//        props.put(\"mail.smtp.auth\", \"true\");\n//        //获取到邮箱会话,利用匿名内部类的方式,将发送者邮箱用户名和密码授权给jvm\n//        session = Session.getDefaultInstance(props, new Authenticator() {\n//            protected PasswordAuthentication getPasswordAuthentication() {\n//                return new PasswordAuthentication(EmailConstant.QQ_USERNAME, EmailConstant.QQ_PASSWORD);\n//            }\n//        });\n//    }\n//\n//    /**\n//     * 向邮箱发送邮件\n//     *\n//     * @param email   对方的邮件地址\n//     * @param title   邮件的标题\n//     * @param content 邮件的内容\n//     * @return\n//     */\n//    public static boolean sendEmail(String email, String title, String content) {\n//        //多线程优化\n//        if (session == null) {\n//            synchronized (EmailUtil.class) {\n//                if (session == null)\n//                    instance();\n//            }\n//        }\n//        try {\n//            Message msg = new MimeMessage(session);\n//            //设置发件人\n//            msg.setFrom(new InternetAddress(EmailConstant.QQ_USERNAME));\n//            //设置收件人,to为收件人,cc为抄送,bcc为密送\n//            msg.setRecipients(Message.RecipientType.TO, InternetAddress.parse(email, false));\n//            msg.setRecipients(Message.RecipientType.CC, InternetAddress.parse(email, false));\n//            msg.setRecipients(Message.RecipientType.BCC, InternetAddress.parse(email, false));\n//            msg.setSubject(title);\n//            //设置邮件消息\n//            msg.setText(content);\n//            //设置发送的日期\n//            msg.setSentDate(new Date());\n//            //调用Transport的send方法去发送邮件\n//            Transport.send(msg);\n//            return true;\n//        } catch (MessagingException e) {\n//            logger.error(e.getMessage());\n//            return false;\n//        }\n//    }\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/utils/request/Request.java",
    "content": "package com.funtester.utils.request;\n\nimport com.alibaba.fastjson.JSONObject;\nimport com.funtester.config.RequestType;\nimport com.funtester.frame.SourceCode;\nimport com.funtester.utils.Regex;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Set;\n\n/**\n * 从swagger文档中读取到的一个请求的所有信息\n */\npublic class Request extends SourceCode {\n\n    /**\n     * 请求的url\n     */\n    private String url;\n\n    public String getUrl() {\n        return url;\n    }\n\n    public RequestType getType() {\n        return type;\n    }\n\n    public String getApiName() {\n        return apiName;\n    }\n\n    public String getDesc() {\n        return desc;\n    }\n\n    public JSONObject getArgs() {\n        return args;\n    }\n\n    public JSONObject getParams() {\n        return params;\n    }\n\n    public StringBuffer getStringBuffer() {\n        return stringBuffer;\n    }\n\n    /**\n     * 请求类型\n     */\n    RequestType type;\n\n    public void setUrl(String url) {\n        this.url = url;\n    }\n\n    public void setType(RequestType type) {\n        this.type = type;\n    }\n\n    public void setApiName(String apiName) {\n        this.apiName = apiName;\n    }\n\n    public void setDesc(String desc) {\n        this.desc = desc;\n    }\n\n    public void setArgs(JSONObject args) {\n        this.args = args;\n    }\n\n    public void setParams(JSONObject params) {\n        this.params = params;\n    }\n\n    public void setStringBuffer(StringBuffer stringBuffer) {\n        this.stringBuffer = stringBuffer;\n    }\n\n    public void setCode(StringBuffer code) {\n        this.code = code;\n    }\n\n    /**\n     * 接口名称\n     */\n    private String apiName;\n\n    /**\n     * 接口描述\n     */\n    private String desc;\n\n    /**\n     * restful参数\n     */\n    List<String> restfulArgs = new ArrayList<>();\n\n    /**\n     * query参数\n     */\n    JSONObject args = new JSONObject();\n\n    /**\n     * formdata参数\n     */\n    JSONObject params = new JSONObject();\n\n    /**\n     * 参数替换字符串，用户想方法里面添加参数\n     */\n    StringBuffer stringBuffer = new StringBuffer();\n\n    /**\n     * 代码文本\n     */\n    StringBuffer code = new StringBuffer();\n\n    /**\n     * 如果遇到post请求，fromdata参数为空时，url里面直接拼接请求字符串\n     */\n    boolean postNoParams = false;\n\n\n    /**\n     * 拼接json参数\n     *\n     * @param i 0：get请求；1：post请求\n     */\n    private void spliceArgs(int i) {\n        String type = i == 1 ? \"params\" : \"args\";\n        this.code.append(LINE + TAB + TAB + \"JSONObject \" + type + \" = new JSONObject();\");\n        Set keySet = i == 0 ? args.keySet() : params.keySet();\n        keySet.forEach(key -> {\n            collectArgs(key.toString(), params.getString(key.toString()));\n            this.code.append(LINE + TAB + TAB + type + \".put(\\\"\" + key.toString() + \"\\\", \" + key.toString() + \");\");\n        });\n    }\n\n\n    /**\n     * 收集参数，拼接往json传参的代码行\n     *\n     * @param key\n     * @param value\n     */\n    private void collectArgs(String key, String value) {\n        if (value.equals(\"string\")) this.stringBuffer.append(\"String \" + key.toString() + \",\");\n        if (value.equals(\"integer\")) this.stringBuffer.append(\"int \" + key.toString() + \",\");\n    }\n\n    /**\n     * 收集restful参数，处理url\n     */\n    private void collectRestfulArgs() {\n        if (this.url.contains(\"{\")) {//restful公参处理，并提取到restfulargs里面\n            List<String> regexAll = Regex.regexAll(this.url, \"\\\\{[^}]+\\\\}\");\n            regexAll.forEach(regex -> {\n                regex = regex.replace(\"{\", EMPTY).replace(\"}\", EMPTY);\n                this.restfulArgs.add(regex);\n            });\n        }\n    }\n\n    /**\n     * 拼接url\n     *\n     * @return\n     */\n    private String spliceUrl() {\n        collectRestfulArgs();\n        this.url = this.url.contains(\"{\") ? this.url : this.url + \"\\\"\";\n        this.url = \"\\\"\" + this.url.replace(\"}/{\", \"+OR+\").replace(\"{\", \"\\\"+\").replace(\"}\", EMPTY);\n        return TAB + TAB + \"String url = HOST + \" + this.url + \";\";\n    }\n\n    /**\n     * 拼接get请求\n     */\n    private void spliceGet() {\n        if (!this.args.isEmpty()) {\n            spliceArgs(0);\n            this.code.append(LINE + TAB + TAB + \"HttpGet httpGet = getHttpGet(url, args);\");//拼接获取请求方法\n        } else {\n            this.code.append(LINE + TAB + TAB + \"HttpGet httpGet = getHttpGet(url);\");//拼接获取请求方法\n        }\n        this.code.append(LINE + TAB + TAB + \"JSONObject response = getHttpResponseEntityByJson(httpGet);\");//拼接发送请求获取响应的方法\n    }\n\n    /**\n     * 拼接post请求\n     */\n    private void splicePost() {\n        if (!this.args.isEmpty()) spliceArgs(0);\n        if (!this.params.isEmpty()) spliceArgs(1);\n        if (this.args.isEmpty()) {//处理为空的情况\n            if (!this.params.isEmpty()) {\n                this.code.append(LINE + TAB + TAB + \"HttpPost httpPost = getHttpPost(url, params);\");\n            } else {\n                this.code.append(LINE + TAB + TAB + \"HttpPost httpPost = getHttpPost(url);\");\n            }\n        } else {\n            if (!this.params.isEmpty()) {\n                this.code.append(LINE + TAB + TAB + \"HttpPost httpPost = getHttpPost(url, args, params);\");\n            } else {\n                this.postNoParams = true;\n            }\n        }\n        this.code.append(LINE + TAB + TAB + \"JSONObject response = getHttpResponseEntityByJson(httpPost);\");\n    }\n\n    /**\n     * 拼接响应后代码行\n     *\n     * @return\n     */\n    private String spliceEnd() {\n        restfulArgs.forEach(key -> stringBuffer.append(\"int \" + key.toString() + \",\"));//在方法中添加参数类型的名称\n        this.code.append(LINE + TAB + TAB + \"output(response);\");//拼接输出响应\n        this.code.append(LINE + TAB + TAB + \"return response;\");//返回响应\n        this.code.append(LINE + TAB + \"}\");\n        return this.code.toString().replace(\"() {\", \"(\" + stringBuffer.toString() + \") {\").replace(\",)\", \")\");//替换参数类型和名称\n    }\n\n    /**\n     * 把request对象变成代码的方法\n     *\n     * @return\n     */\n    public String magic() {\n        this.code.append(TAB + \"/**\\n\\t * \" + desc + \"\\n\\t *\\n\\t * @return\\n\\t */\" + LINE);\n        this.code.append(TAB + \"public JSONObject \" + apiName + \"() {\" + LINE);//新建方法行\n        String urlLine = spliceUrl();\n        this.code.append(urlLine);\n        if (restfulArgs.size() > 0) restfulArgs.forEach(arg -> args.remove(arg));//将公参从args里面删除\n        if (this.type == RequestType.GET) spliceGet();\n        if (this.type == RequestType.POST) splicePost();\n        String finalCode = spliceEnd();\n        if (this.postNoParams)\n            finalCode = finalCode.replace(urlLine, urlLine.replace(\";\", EMPTY) + \" + changeJsonToArguments(args)\");\n        return finalCode;\n    }\n\n\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/utils/request/RequestFile.java",
    "content": "package com.funtester.utils.request;\n\nimport com.funtester.config.Constant;\nimport com.funtester.config.RequestType;\nimport com.funtester.httpclient.FunLibrary;\nimport com.funtester.utils.RWUtil;\nimport com.alibaba.fastjson.JSONObject;\nimport org.apache.http.client.methods.HttpRequestBase;\nimport org.apache.logging.log4j.LogManager;\nimport org.apache.logging.log4j.Logger;\n\n\n/**\n * 从文件中读取接口相关参数，用来发送请求，实现接口请求的配置化\n * <p>从当前路径下获取后缀为.log的文件，以文件名为准读取文件内容</p>\n */\npublic class RequestFile extends FunLibrary {\n\n    private static Logger logger = LogManager.getLogger(RequestFile.class);\n\n    String url;\n\n    /**\n     * get对应get请求，post对应post请求表单参数，其他对应post请求json参数\n     */\n    JSONObject headers;\n\n    RequestType requestType;\n\n    String name;\n\n    JSONObject info;\n\n    JSONObject params;\n\n    /**\n     * @param name\n     */\n    public RequestFile(String name) {\n        this.name = name;\n        getInfo();\n        this.url = this.info.getString(\"url\");\n        requestType = RequestType.getRequestType(this.info.getString(\"requestType\"));\n        getParams();\n        headers = JSONObject.parseObject(this.info.getString(\"headers\"));\n    }\n\n    /**\n     * 获取当前目录下的配置文件，以数字开头，后缀是.log的\n     *\n     * @param i\n     */\n    public RequestFile(int i) {\n        this(i + Constant.EMPTY);\n    }\n\n    /**\n     * 从配置文件中读取信息，组成一个json对象\n     */\n    private void getInfo() {\n        String filePath = Constant.WORK_SPACE + this.name;\n        logger.info(\"配置文件地址：\" + filePath);\n        this.info = RWUtil.readTxtByJson(filePath);\n    }\n\n    /**\n     * 获取请求参数\n     */\n    private void getParams() {\n        params = JSONObject.parseObject(info.getString(\"params\"));\n    }\n\n\n    /**\n     * 根据info组成请求\n     *\n     * @return\n     */\n    public HttpRequestBase getRequest() {\n        HttpRequestBase requestBase;\n        switch (this.requestType) {\n            case GET:\n                requestBase = getHttpGet(this.url, this.params);\n                break;\n            case POST:\n                requestBase = getHttpPost(this.url, this.params);\n                break;\n            default:\n                requestBase = getHttpPost(this.url, this.params.toString());\n                break;\n        }\n        this.headers.keySet().forEach(x -> requestBase.addHeader(getHeader(x.toString(), headers.getString(x.toString()))));\n        output(getHttpResponse(requestBase));\n        return requestBase;\n    }\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/utils/request/Swagger.java",
    "content": "package com.funtester.utils.request;\n\nimport com.alibaba.fastjson.JSONArray;\nimport com.alibaba.fastjson.JSONObject;\nimport com.funtester.config.RequestType;\nimport com.funtester.httpclient.FunLibrary;\nimport com.funtester.utils.Regex;\n\nimport java.util.ArrayList;\nimport java.util.List;\nimport java.util.Set;\n\n/**\n * swagger文档解析类\n */\npublic class Swagger extends FunLibrary {\n\n    /**\n     * 关键字，用于url前\n     */\n    String key;\n\n    /**\n     * swagger文档地址\n     */\n    String swaggerPath;\n\n    /**\n     * 构造方法中接口地址类别\n     */\n    String name;\n\n    /**\n     * swagger地址所有类别\n     */\n    List<String> names = new ArrayList<>();\n\n    /**\n     * 某类别所有接口地址\n     */\n    List<String> urls = new ArrayList<>();\n\n    /**\n     * swagger文档转换成的json对象\n     */\n    JSONObject swagger = new JSONObject();\n\n    /**\n     * 所有接口地址的json对象\n     */\n    JSONObject paths = new JSONObject();\n\n    /**\n     * 对应构造方法中url的request对象\n     */\n    Request request = new Request();\n\n    public Request getRequest() {\n        return request;\n    }\n\n    public List<Request> getAllRequests() {\n        return allRequests;\n    }\n\n    /**\n     * 对应构造方法中name的所有request对象\n     */\n    List<Request> allRequests = new ArrayList<>();\n\n    /**\n     * 获取某一类的接口的request对象\n     *\n     * @param swaggerPath\n     * @param name\n     */\n    public Swagger(String swaggerPath, String name) {\n        this.swaggerPath = swaggerPath;\n        this.name = name;\n        build();\n    }\n\n    /**\n     * 获取某一类的某一个接口的request对象\n     *\n     * @param swaggerPath\n     * @param name\n     * @param url\n     */\n    public Swagger(String swaggerPath, String name, String url) {\n        this.swaggerPath = swaggerPath;\n        this.name = name;\n        build();\n        request = getRequest(url);\n    }\n\n\n    public String getKey() {\n        this.key = Regex.regexAll(this.swaggerPath, \"/((?!/).)*/swagger.json\").get(0);\n        this.key = this.key.replace(OR, EMPTY).replace(\"swagger.json\", EMPTY);\n        if (this.key.contains(\":\")) this.key = EMPTY;\n        return this.key;\n    }\n\n    /**\n     * 获取name下所有接口的request对象\n     */\n    private void getRequests() {\n        this.urls.forEach(url -> {\n            Request request = getRequest(url);\n            if (request != null) allRequests.add(request);\n        });\n    }\n\n    /**\n     * 初始化处理方法\n     */\n    public void build() {\n        swagger = getHttpResponse(getHttpGet(this.swaggerPath));\n        getKey();\n        getNames();\n        getPaths();\n        getUrls();\n        getRequests();\n\n    }\n\n    /**\n     * 获取某一个url地址的请求request对象\n     *\n     * @param url 接口地址\n     * @return\n     */\n    private Request getRequest(String url) {\n        Request request = new Request();\n        request.setUrl((OR + key + url).replace(\"//\", \"/\"));\n        JSONObject json1 = paths.getJSONObject(url);\n        JSONObject json2 = new JSONObject();\n        if (json1.containsKey(\"get\")) {\n            request.setType(RequestType.GET);\n            json2 = json1.getJSONObject(\"get\");\n        } else if (json1.containsKey(\"post\")) {\n            request.setType(RequestType.POST);\n            json2 = json1.getJSONObject(\"post\");\n        }\n        String tags = json2.get(\"tags\").toString();\n        if (!tags.contains(name)) return null;\n        String apiName = json2.getString(\"operationId\");\n        request.setApiName(apiName);\n        String desc = json2.getString(\"summary\");\n        request.setDesc(desc);\n        JSONArray json3 = json2.getJSONArray(\"parameters\");\n        JSONObject json5 = new JSONObject();\n        JSONObject json6 = new JSONObject();\n        json3.forEach(json -> {//获取参数，区分query和formdata\n            JSONObject json4 = (JSONObject) json;\n            String in = json4.getString(\"in\");\n            if (in.equals(\"query\")) {\n                boolean required = json4.getBoolean(\"required\");\n                if (required) {\n                    String format = json4.getString(\"type\");\n                    String name = json4.getString(\"name\");\n                    json5.put(name, format);\n                }\n            } else if (in.equals(\"formData\")) {\n                boolean required = json4.getBoolean(\"required\");\n                if (required) {\n                    String format = json4.getString(\"type\");\n                    String name = json4.getString(\"name\");\n                    json6.put(name, format);\n                }\n            }\n        });\n        request.setArgs(json5);\n        request.setParams(json6);\n        return request;\n    }\n\n    /**\n     * 获取name下所有接口的地址\n     */\n    private void getUrls() {\n        Set keySet = paths.keySet();\n        keySet.forEach(key -> urls.add(key.toString()));\n    }\n\n\n    /**\n     * 获取所有name\n     */\n    private void getNames() {\n        JSONArray tags = swagger.getJSONArray(\"tags\");\n        tags.forEach(info -> {\n            JSONObject name = (JSONObject) info;\n            names.add(name.getString(\"name\"));\n        });\n    }\n\n    /**\n     * 获取所有的接口地址\n     */\n    private void getPaths() {\n        paths = swagger.getJSONObject(\"paths\");\n    }\n\n\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/utils/xml/Attr.groovy",
    "content": "package com.funtester.utils.xml\n/**\n * 节点属性信息\n */\nclass Attr {\n//class Attr extends AbstractBean implements Serializable{\n\n//    private static final long serialVersionUID = -35484487563215649L\n//\n//    String name\n//\n//    String value\n//\n//    Attr(String name, String value) {\n//        this.name = name\n//        this.value = value\n//    }\n//\n\n}"
  },
  {
    "path": "src/main/groovy/com/funtester/utils/xml/NodeInfo.groovy",
    "content": "package com.funtester.utils.xml\n/**\n * 节点信息\n */\nclass NodeInfo {\n//class NodeInfo extends AbstractBean implements Serializable{\n\n//    private static final long serialVersionUID = 568896512159847L\n//\n//    List<Attr> attrs\n//\n//    List<NodeInfo> children\n\n\n}\n\n"
  },
  {
    "path": "src/main/groovy/com/funtester/utils/xml/XMLUtil.groovy",
    "content": "package com.funtester.utils.xml\n/**\n * 基于dom解析xml文件工具类\n */\nclass XMLUtil {\n\n//    private static Logger logger = LogManager.getLogger(XMLUtil.class)\n//\n//    /**\n//     *  解析某个节点(根节点)信息\n//     * @param path 绝对路径或者URL\n//     * @param root\n//     * @return\n//     */\n//    static List<NodeInfo> parseRoot(String path, String root) {\n//        DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance()\n//        try {\n//            DocumentBuilder db = dbf.newDocumentBuilder()\n//            Document document = db.parse(path.startsWith(\"http\") ? new URI(path) : new File(path))\n//            NodeList nodeList = document.getElementsByTagName(root)\n//            return range(nodeList.getLength()).mapToObj {x -> parseNode(nodeList.item(x))}.collect() as List\n//        } catch (ParserConfigurationException e) {\n//            logger.error(\"解析配置错误!\", e)\n//        } catch (IOException e) {\n//            logger.error(\"IO错误!\", e)\n//        } catch (SAXException e) {\n//            logger.error(\"SAX错误!\", e)\n//        }\n//        FailException.fail(\"解析文件:${path}中${root}节点出错!\")\n//    }\n//\n//    /**\n//     * 解析某个节点信息\n//     * @param node\n//     * @return\n//     */\n//    static NodeInfo parseNode(Node node) {\n//        if (node.getNodeType() != Node.ELEMENT_NODE) return null\n//        NodeInfo nodeInfo = new NodeInfo()\n//        NamedNodeMap attrs = node.getAttributes()\n//        List<Attr> nodeAttr = new ArrayList<>()\n//        range(attrs.getLength()).each {\n//            Node attr = attrs.item(it)\n//            nodeAttr << new Attr(attr.getNodeName(), attr.getNodeValue())\n//        }\n//        nodeInfo.attrs = nodeAttr\n//        NodeList childNodes = node.getChildNodes()\n//        List<NodeInfo> children = new ArrayList<>()\n//        range(childNodes.getLength()).each {children.add(parseNode(childNodes.item(it)))}\n//        nodeInfo.children = children.findAll {it != null}\n//        return nodeInfo\n//    }\n\n\n}\n"
  },
  {
    "path": "src/main/groovy/com/funtester/utils/xml/XMLUtil2.groovy",
    "content": "package com.funtester.utils.xml\n/**\n * 基于dom4j解析xml工具类\n */\nclass XMLUtil2 {\n\n//    private static Logger logger = LogManager.getLogger(XMLUtil2.class)\n//\n//    /**\n//     * 解析xml文件\n//     * @param path 绝对路径或者URL\n//     * @return\n//     */\n//    static List<NodeInfo> parse(String path) {\n//        SAXReader reader = new SAXReader();\n//        try {\n//            Document document = reader.read(path.startsWith(\"http\") ? new URL(path) : new File(path));\n//            Element rootElement = document.getRootElement();\n//            def iterator = rootElement.elementIterator()\n//            List<NodeInfo> info = new ArrayList<>()\n//            while (iterator.hasNext()) {\n//                info << parseNode(iterator.next() as Element)\n//            }\n//            return info;\n//        } catch (DocumentException e) {\n//            logger.error(\"解析文件${path}失败!\", e)\n//        }\n//        FailException.fail(\"解析文件${path}失败!\")\n//    }\n//\n//    /**\n//     * 解析节点信息\n//     * @param e\n//     * @return\n//     */\n//    static NodeInfo parseNode(Element e) {\n//        if (e.getNodeType() != Node.ELEMENT_NODE) return null;\n//        def info = new NodeInfo()\n//        List<Attribute> attributes = e.attributes();\n//        List<Attr> attrs = new ArrayList<>()\n//        attributes.each {\n//            attrs << new Attr(it.name, it.value)\n//        }\n//        info.setAttrs(attrs)\n//        List<NodeInfo> children = new ArrayList<>()\n//        def iterator = e.elementIterator()\n//        if (iterator.hasNext()) {\n//            children << parseNode(iterator.next() as Element)\n//        }\n//        info.setChildren(children)\n//        return info;\n//    }\n}\n"
  },
  {
    "path": "src/main/resources/http.properties",
    "content": "ssl_v=TLSv1.2\nUser-Agent=Mozilla/5.0 (Macintosh; Intel Mac OS X 10_14_4) AppleWebKit/537.36 (KHTML, like Gecko) Chrome/74.0.3729.108 Safari/537.36\nConnection=keep-alive\nblack_host=wblçog.fv1314.xyz:8082,172.18.4.55:8888\nTIMEOUT=10\nTRY_TIMES=3\nMAX_ACCEPT_TIME=5"
  },
  {
    "path": "src/main/resources/log4j2.xml",
    "content": "<?xml version=\"1.0\" encoding=\"UTF-8\"?>\n<!--日志级别以及优先级排序: OFF > FATAL > ERROR > WARN > INFO > DEBUG > TRACE > ALL -->\n<!--Configuration后面的status，这个用于设置log4j2自身内部的信息输出，可以不设置，当设置成trace时，你会看到log4j2内部各种详细输出-->\n<!--monitorInterval：Log4j能够自动检测修改配置 文件和重新配置本身，设置间隔秒数-->\n<configuration status=\"WARN\" monitorInterval=\"30\">\n    <!--先定义所有的appender-->\n    <appenders>\n        <!--这个输出控制台的配置-->\n        <console name=\"Console\" target=\"SYSTEM_OUT\">\n            <!--输出日志的格式-->\n            <PatternLayout pattern=\"%-4p-> %m%n\"/>\n            <!--            <PatternLayout pattern=\"[%-4p] %d - %t %L:%M %m %n\"/>-->\n            <!--            <PatternLayout pattern=\"%d %-4p (%F:%L) - %m%n\"/>-->\n        </console>\n        <!--文件会打印出所有信息，这个log每次运行程序会自动清空，由append属性决定，这个也挺有用的，适合临时测试用-->\n        <!--        <File name=\"log\" fileName=\"log/fun.log\" append=\"false\">-->\n        <!--            <PatternLayout pattern=\"%d{HH:mm:ss.SSS} %-5level %class{36} %L %M - %msg%xEx%n\"/>-->\n        <!--        </File>-->\n        <!-- 这个会打印出所有的info及以下级别的信息，每次大小超过size，则这size大小的日志会自动存入按年份-月份建立的文件夹下面并进行压缩，作为存档-->\n        <RollingFile name=\"RollingFileInfo\"\n                     fileName=\"log/info.log\"\n                     filePattern=\"log/$${date:yyyy-MM}/info-%d{yyyy-MM-dd}-%i.log\">\n            <!--控制台只输出level及以上级别的信息（onMatch），其他的直接拒绝（onMismatch）-->\n            <ThresholdFilter level=\"info\" onMatch=\"ACCEPT\" onMismatch=\"DENY\"/>\n            <PatternLayout pattern=\"%d{dd HH:mm:ss} %t %-4level %l - %m%n\"/>\n            <Policies>\n                <TimeBasedTriggeringPolicy/>\n                <SizeBasedTriggeringPolicy size=\"8 MB\"/>\n            </Policies>\n            <!--            <CronTriggeringPolicy schedule=\"* * * * * ?\"/>-->\n            <!-- DefaultRolloverStrategy属性如不设置，则默认为最多同一文件夹下7个文件，这里设置了20 -->\n            <DefaultRolloverStrategy max=\"10\">\n                <Delete basePath=\"log/$${date:yyyy-MM}/\" maxDepth=\"2\">\n                    <IfFileName glob=\"info*.log\"/>\n                    <!--!Note: 这里的age必须和filePattern协调, 后者是精确到HH, 这里就要写成xH, xd就不起作用\n                    另外, 数字最好>2, 否则可能造成删除的时候, 最近的文件还处于被占用状态,导致删除不成功!-->\n                    <!--7天-->\n                    <IfLastModified age=\"3d\"/>\n                </Delete>\n            </DefaultRolloverStrategy>\n        </RollingFile>\n\n\n        <RollingFile name=\"RollingFileWarn\" fileName=\"log/warn.log\"\n                     filePattern=\"log/$${date:yyyy-MM}/warn-%d{yyyy-MM-dd}-%i.log\">\n            <ThresholdFilter level=\"warn\" onMatch=\"ACCEPT\" onMismatch=\"DENY\"/>\n            <PatternLayout pattern=\"%d{dd HH:mm:ss} %t %-4level %l - %m%n\"/>\n            <Policies>\n                <TimeBasedTriggeringPolicy/>\n                <SizeBasedTriggeringPolicy size=\"5 MB\"/>\n            </Policies>\n            <!-- DefaultRolloverStrategy属性如不设置，则默认为最多同一文件夹下7个文件，这里设置了20 -->\n            <DefaultRolloverStrategy max=\"10\">\n                <Delete basePath=\"log/$${date:yyyy-MM}/\" maxDepth=\"2\">\n                    <IfFileName glob=\"warn*.log\"/>\n                    <!--!Note: 这里的age必须和filePattern协调, 后者是精确到HH, 这里就要写成xH, xd就不起作用\n                    另外, 数字最好>2, 否则可能造成删除的时候, 最近的文件还处于被占用状态,导致删除不成功!-->\n                    <!--7天-->\n                    <IfLastModified age=\"3d\"/>\n                </Delete>\n            </DefaultRolloverStrategy>\n        </RollingFile>\n    </appenders>\n    <!--然后定义logger，只有定义了logger并引入的appender，appender才会生效-->\n    <loggers>\n        <root level=\"info\">\n            <appender-ref ref=\"Console\"/>\n            <appender-ref ref=\"RollingFileInfo\"/>\n            <appender-ref ref=\"RollingFileWarn\"/>\n        </root>\n    </loggers>\n    <!--    异步日志记录的配置-->\n    <!--    <Appenders>-->\n    <!--        &lt;!&ndash; Async Loggers will auto-flush in batches, so switch off immediateFlush. &ndash;&gt;-->\n    <!--        <RandomAccessFile name=\"RandomAccessFile\" fileName=\"asyncWithLocation.log\"-->\n    <!--                          immediateFlush=\"false\" append=\"false\">-->\n    <!--            <PatternLayout>-->\n    <!--                <Pattern>%d %p %class{1.} [%t] %location %m %ex%n</Pattern>-->\n    <!--            </PatternLayout>-->\n    <!--        </RandomAccessFile>-->\n    <!--    </Appenders>-->\n    <!--    <Loggers>-->\n    <!--        &lt;!&ndash; pattern layout actually uses location, so we need to include it &ndash;&gt;-->\n    <!--        <AsyncLogger name=\"com.foo.Bar\" level=\"trace\" includeLocation=\"true\">-->\n    <!--            <AppenderRef ref=\"RandomAccessFile\"/>-->\n    <!--        </AsyncLogger>-->\n    <!--        <Root level=\"info\" includeLocation=\"true\">-->\n    <!--            <AppenderRef ref=\"RandomAccessFile\"/>-->\n    <!--        </Root>-->\n    <!--    </Loggers>-->\n</configuration>"
  },
  {
    "path": "src/main/resources/mysql.properties",
    "content": "test_mysql_url=jdbc:mysql://172.18.4.55:3306/okayapi\nuser=root\npassword=dsjw2016\nmysql_server_path=http://172.18.4.55:8888/mysql\nflag=false"
  },
  {
    "path": "src/main/resources/redis.properties",
    "content": "ip=58.87.70.151\nport=6379\nmax_total=10\nmax_idle=5\nmin_idle=2\nmax_wait=60000\ntimeout=60000"
  }
]