Repository: JunManYuanLong/FunTester Branch: okay Commit: 87858304b866 Files: 119 Total size: 428.1 KB Directory structure: gitextract_d43vbshl/ ├── build.gradle ├── document/ │ ├── 7788.markdown │ ├── api.markdown │ ├── article.markdown │ ├── base.markdown │ ├── directory.markdown │ └── update.markdown ├── long/ │ ├── 1 │ ├── 30 │ ├── poster.markdown │ └── sql/ │ ├── performance.sql │ └── request.sql ├── readme.markdown ├── settings.gradle └── src/ └── main/ ├── groovy/ │ └── com/ │ └── funtester/ │ ├── base/ │ │ ├── bean/ │ │ │ ├── AbstractBean.groovy │ │ │ ├── PerformanceResultBean.groovy │ │ │ ├── RecordBean.groovy │ │ │ ├── RequestInfo.groovy │ │ │ ├── Result.groovy │ │ │ └── VerifyBean.groovy │ │ ├── constaint/ │ │ │ ├── CaseBase.java │ │ │ ├── FixedQpsThread.java │ │ │ ├── ThreadBase.java │ │ │ ├── ThreadLimitTimeCount.java │ │ │ └── ThreadLimitTimesCount.java │ │ ├── exception/ │ │ │ ├── FailException.java │ │ │ ├── LoginException.java │ │ │ ├── ParamException.java │ │ │ ├── RequestException.java │ │ │ └── VerifyException.java │ │ └── interfaces/ │ │ ├── IBase.java │ │ ├── IMessage.java │ │ ├── IMySqlBasic.java │ │ ├── ISocketClient.java │ │ ├── ISocketVerify.java │ │ ├── MarkRequest.java │ │ ├── MarkThread.java │ │ └── ReturnCode.java │ ├── config/ │ │ ├── Constant.java │ │ ├── EmailConstant.java │ │ ├── HttpClientConstant.java │ │ ├── PropertyUtils.groovy │ │ ├── RequestType.java │ │ ├── SocketConstant.java │ │ ├── SqlConstant.java │ │ ├── SysInit.java │ │ └── VerifyType.groovy │ ├── db/ │ │ ├── mongodb/ │ │ │ ├── MongoBase.java │ │ │ └── MongoObject.java │ │ ├── mysql/ │ │ │ ├── AidThread.java │ │ │ ├── MySqlFun.java │ │ │ ├── MySqlObject.java │ │ │ ├── MySqlTest.java │ │ │ ├── SqlBase.java │ │ │ └── TestConnectionManage.java │ │ └── redis/ │ │ ├── RedisPool.java │ │ └── RedisUtil.java │ ├── dubbo/ │ │ ├── DubboBase.java │ │ ├── DubboInvokeParams.groovy │ │ ├── DubboParamBase.groovy │ │ └── DubboUtil.java │ ├── frame/ │ │ ├── JsonVerify.groovy │ │ ├── Output.java │ │ ├── ResponseVerify.java │ │ ├── Save.java │ │ ├── SourceCode.java │ │ ├── execute/ │ │ │ ├── Concurrent.java │ │ │ ├── ExecuteGroovy.java │ │ │ ├── ExecuteSource.java │ │ │ ├── FixedQpsConcurrent.java │ │ │ ├── Progress.java │ │ │ ├── StatisticsUtil.java │ │ │ └── ThreadPoolUtil.groovy │ │ └── thread/ │ │ ├── FixedQpsHeaderMark.groovy │ │ ├── FixedQpsParamMark.java │ │ ├── HeaderMark.java │ │ ├── ParamMark.java │ │ ├── QuerySqlThread.java │ │ ├── RequestThreadTime.java │ │ ├── RequestThreadTimes.java │ │ ├── RequestTimeFixedQps.java │ │ ├── RequestTimesFixedQps.java │ │ └── UpdateSqlThread.java │ ├── httpclient/ │ │ ├── ClientManage.java │ │ ├── FunLibrary.java │ │ ├── FunRequest.groovy │ │ └── GCThread.java │ ├── main/ │ │ ├── ExecuteMethod.java │ │ └── PerformanceFromFile.groovy │ ├── socket/ │ │ ├── ScoketIOFunClient.java │ │ └── WebSocketFunClient.java │ └── utils/ │ ├── ArgsUtil.java │ ├── CMD.java │ ├── CountUtil.groovy │ ├── CurlUtil.groovy │ ├── DecodeEncode.java │ ├── FileUtil.groovy │ ├── HeapDumper.java │ ├── Join.java │ ├── JsonUtil.groovy │ ├── RWUtil.java │ ├── Regex.java │ ├── StringUtil.groovy │ ├── Time.java │ ├── TimeWatch.groovy │ ├── WriteHtml.java │ ├── message/ │ │ ├── AlertOver.java │ │ └── EmailUtil.java │ ├── request/ │ │ ├── Request.java │ │ ├── RequestFile.java │ │ └── Swagger.java │ └── xml/ │ ├── Attr.groovy │ ├── NodeInfo.groovy │ ├── XMLUtil.groovy │ └── XMLUtil2.groovy └── resources/ ├── http.properties ├── log4j2.xml ├── mysql.properties └── redis.properties ================================================ FILE CONTENTS ================================================ ================================================ FILE: build.gradle ================================================ buildscript { repositories { maven {url 'http://maven.aliyun.com/nexus/content/groups/public/'} // maven { url 'http://repo1.maven.apache.org/maven2/' } //// maven { url 'http://repo2.maven.org/maven2/' } //// maven { url 'http://maven.oschina.net/content/groups/public/' } // google() } } plugins { id 'java' } group 'funtester' version '1.0' apply plugin: 'groovy' apply plugin: 'java' apply plugin: 'idea' //apply plugin: 'distribution' //打包tar包用到的插件 idea { module { downloadJavadoc = true downloadSources = true } } sourceCompatibility = 1.8 repositories { mavenLocal() maven { url 'http://maven.aliyun.com/nexus/content/groups/public/' } // maven { // url 'http://repo2.maven.org/maven2/' // } // maven { // url 'http://repo1.maven.apache.org/maven2/' // } // maven { url 'http://maven.oschina.net/content/groups/public/' } mavenCentral() } test { useJUnitPlatform() exclude "com/test/**" } sourceSets { main { groovy { srcDirs 'src/main/groovy' } } } dependencies { compile group: 'org.apache.commons', name: 'commons-lang3', version: '3.7' compile group: 'org.apache.httpcomponents', name: 'httpclient', version: '4.5.5' compile group: 'org.apache.httpcomponents', name: 'httpmime', version: '4.5.5' compile group: 'org.apache.httpcomponents', name: 'httpasyncclient', version: '4.1.4' // compile group: 'commons-beanutils', name: 'commons-beanutils', version: '1.8.0' // compile group: 'commons-codec', name: 'commons-codec', version: '1.9' // compile group: 'com.sun.jna', name: 'jna', version: '3.0.9' compile group: 'com.alibaba', name: 'fastjson', version: '1.2.62' // compile group: 'org.mongodb', name: 'mongo-java-driver', version: '3.9.0' compile group: 'org.apache.logging.log4j', name: 'log4j-core', version: '2.12.1' compile group: 'org.apache.logging.log4j', name: 'log4j-api', version: '2.12.1' // compile group: 'org.apache.logging.log4j', name: 'log4j-slf4j-impl', version: '2.12.1' // compile group: 'mysql', name: 'mysql-connector-java', version: '8.0.13' // compile group: 'javax.mail', name: 'javax.mail-api', version: '1.6.0' // compile group: 'com.sun.mail', name: 'javax.mail', version: '1.6.0' compile group: 'org.codehaus.groovy', name: 'groovy-all', version: '2.5.7' // compile group: 'redis.clients', name: 'jedis', version: '3.0.1' // compile group: 'com.alibaba', name: 'dubbo', version: '2.5.3' // compile group: 'com.jayway.jsonpath', name: 'json-path', version: '2.4.0' // compile group: 'dom4j', name: 'dom4j', version: '1.6.1' // compile group: 'org.java-websocket', name: 'Java-WebSocket', version: '1.5.1' // compile group: 'io.socket', name: 'socket.io-client', version: '1.0.0' } jar { from { //添加依懒到打包文件 configurations.compile.collect {it.isDirectory() ? it : zipTree(it)} configurations.runtime.collect {zipTree(it)} } manifest { attributes 'Main-Class': 'com.funtester.main.Blog' } } ext { if (project.hasProperty('profile')) { profile = project['profile'] } else { profile = "FunTester" } println "项目环境:" + profile } //因为打包配置,这里会执行 task createDirs() { doLast { if (profile != "FunTester") { file('build/package/lib').mkdirs() file('build/package/bin').mkdirs() file('build/package/logs').mkdirs() file('build/package/conf').mkdirs() println "文件夹创建成功!" } } } task copyFun(type: Copy) { from('/Users/fv/Documents/workspace/funtester/build/libs') into('/Users/fv/Library/groovy-2.5.7/lib') println "拷贝fun.jar包到Groovy依赖成功!" } task copyOkay(type: Copy) { from('/Users/fv/Documents/workspace/okay_test/target/okay_test-1.0-SNAPSHOT.jar') into('/Users/fv/Library/groovy-2.5.7/lib') println "拷贝okay.jar包到Groovy依赖成功!" } task copyJarToGroovy(dependsOn: ['copyFun', 'copyOkay']) {} task copyLibs(type: Copy) { doLast { from('build/libs') into('build/package/lib') println "依赖拷贝成功!" } } task copyConf(type: Copy) { doLast { from('src/main/resources/' + profile) into('build/package/conf') println "从src/main/resources/" + profile + "拷贝配置文件" } } task copyBin(type: Copy) { doLast { from('src/main/resources/bin') into('build/package/bin') fileMode 0744//可能会失效,检查执行权限 println "依赖脚本,并设置可执行权限成功!" } } // task 用来复制启动所依赖的jar包 task copyDep(type: Copy) { doLast { from configurations.runtime into 'build/package/lib' println "复制启动所依赖的jar包成功!" } } //把上述的task串联起来 task prepareFile(dependsOn: [ 'createDirs', 'copyLibs', 'copyConf', 'copyBin', 'copyDep' ]) {}//如果没有内容的话,可以不需要大括号 //还有一种写法表示task之间的依赖:prepareFile.dependsOn createDirs,copyLibs,copyConf,copyBin,copyDep //指定打包的tar包的名字,以及文件来源目录 //distributions { // monitor { // baseName = 'azkaban-monitor' // contents { // from {'build/package'} // } // } //} //distribution 插件的特性 //monitorDistTar.dependsOn 'prepareFile' //monitorDistTar.compression = Compression.NONE //monitorDistTar.extension = 'tar' //定义一个task,先build 然后再打包tar包 //task buildTar(dependsOn: [ // 'build', // monitorDistTar //]) {} ================================================ FILE: document/7788.markdown ================================================ # 7788篇 > **FunTester**,[腾讯云年度作者](https://mp.weixin.qq.com/s/oeTeJZs6h4jJJMRyUunurw)、[Boss直聘签约作者](https://mp.weixin.qq.com/s/CjwFBoS9sew6R74mM9QO4g),非著名测试开发er,欢迎关注。 * `Gitee`地址*https://gitee.com/fanapi/tester* * `GitHub`地址*https://github.com/JunManYuanLong/FunTester* # 无代码合集 ## 理论鸡汤 - [写给所有人的编程思维](https://mp.weixin.qq.com/s/Oj33UCnYfbUgzsBzEm2GPQ) - [成为杰出Java开发人员的10个步骤](https://mp.weixin.qq.com/s/UCNOTSzzvTXwiUX6xpVlyA) - [测试之《代码不朽》脑图](https://mp.weixin.qq.com/s/2aGLK3knUiiSoex-kmi0GA) - [为什么选择软件测试作为职业道路?](https://mp.weixin.qq.com/s/o83wYvFUvy17kBPLDO609A) - [自动化测试的障碍](https://mp.weixin.qq.com/s/ZIV7uJp7DzVoKhWOh6lvRg) - [自动化测试的问题所在](https://mp.weixin.qq.com/s/BhvD7BnkBU8hDBsGUWok6g) - [成为优秀自动化测试工程师的7个步骤](https://mp.weixin.qq.com/s/wdw1l4AZnPpdPBZZueCcnw) - [优秀软件开发人员的态度](https://mp.weixin.qq.com/s/0uEEeFaR27aTlyp-sm61bA) - [如何正确执行功能API测试](https://mp.weixin.qq.com/s/aeGx5O_jK_iTD9KUtylWmA) - [未来10年软件测试的新趋势-上](https://mp.weixin.qq.com/s/9XgpIfXQRuKg1Pap-tfqYQ) - [未来10年软件测试的新趋势-下](https://mp.weixin.qq.com/s/k2rZaeHoq4AX19CUzjGRVQ) - [自动化测试解决了什么问题](https://mp.weixin.qq.com/s/96k2I_OBHayliYGs2xo6OA) - [17种软件测试人员常用的高效技能-上](https://mp.weixin.qq.com/s/vrM_LxQMgTSdJxaPnD_CqQ) - [17种软件测试人员常用的高效技能-下](https://mp.weixin.qq.com/s/uyWdVm74TYKb62eIRKL7nQ) - [手动测试存在的重要原因](https://mp.weixin.qq.com/s/mW5vryoJIkeskZLkBPFe0Q) - [编写测试用例的技巧](https://mp.weixin.qq.com/s/zZAh_XXXGOyhlm6ebzs06Q) - [成为自动化测试的7种技能](https://mp.weixin.qq.com/s/e-HAGMO0JLR7VBBWLvk0dQ) - [功能测试与非功能测试](https://mp.weixin.qq.com/s/oJ6PJs1zO0LOQSTRF6M6WA) - [自动化和手动测试,保持平衡!](https://mp.weixin.qq.com/s/mMr_4C98W_FOkks2i2TiCg) - [43种常见软件测试分类](https://mp.weixin.qq.com/s/GTMkcEm-xPtVF7_HxXGKDg) - [自动化测试生命周期](https://mp.weixin.qq.com/s/SH-vb2RagYQ3sfCY8QM5ew) - [代码审查如何保证软件质量](https://mp.weixin.qq.com/s/osRnG09KDqEojiV3kp2nrw) - [TDD测试驱动开发的基础](https://mp.weixin.qq.com/s/diW_2HSbWMEsn8G6uQriOg) - [如何在DevOps引入自动化测试](https://mp.weixin.qq.com/s/MclK3VvMN1dsiXXJO8g7ig) - [自动化的好处](https://mp.weixin.qq.com/s/7MpWQhtozaTrlUMo1oRSBg) - [Web端自动化测试失败原因汇总](https://mp.weixin.qq.com/s/qzFth-Q9e8MTms1M8L5TyA) - [测试人员如何成为变革的推动者](https://mp.weixin.qq.com/s/0nTZHBOuKG0rewKAeyIqwA) - [探索性测试为何如此重要?](https://mp.weixin.qq.com/s/nebHPfKbCO0f-G24qCh9wA) - [5种促进业务增长的软件测试策略](https://mp.weixin.qq.com/s/3mB_DQVD2AZLPs84SmsmuA) - [如何选择正确的自动化测试工具](https://mp.weixin.qq.com/s/_Ee78UW9CxRpV5MoTrfgCQ) - [如何从测试自动化中实现价值](https://mp.weixin.qq.com/s/dj-sJvGjvFMYANfhIVo8jw) - [您如何使用Selenium来计算自动化测试的投资回报率?](https://mp.weixin.qq.com/s/DVSEm0DhoAvYfTWIniabJg) - [如何在DevOps中实施连续测试](https://mp.weixin.qq.com/s/snPXkH6WEZ2kteYP_-c5_g) - [自动化如何选择用例](https://mp.weixin.qq.com/s/1hH5YIle4YQimJr4iGSWlA) - [成功实施自动化测试的优点](https://mp.weixin.qq.com/s/UENdSU-NPa5AOVC9ciiy0Q) - [测试人员常用借口](https://mp.weixin.qq.com/s/0k_Ciud2sOpRb5PPiVzECw) - [测试自动化的边缘DevTestOps](https://mp.weixin.qq.com/s/kCySRYdCS11CA-lF30AtQA) - [筛选自动化测试用例的技巧](https://mp.weixin.qq.com/s/SWNopZLwgpj9yYsVEHEspw) - [什么阻碍手动测试发挥价值](https://mp.weixin.qq.com/s/t0VAVyA3ywQsHzaqzSILOw) - [未来的QA测试工程师](https://mp.weixin.qq.com/s/ngL4sbEjZm7OFAyyWyQ3nQ) - [Web安全检查](https://mp.weixin.qq.com/s/SewUV3GMaNKD2P7g64ctYQ) - [关于可用性测试](https://mp.weixin.qq.com/s/aUIg40scOWzbRR89ojJWLg) - [如何实施DevOps](https://mp.weixin.qq.com/s/UPIL942eOKR1bY0mbC-42w) - [黑盒测试和白盒测试](https://mp.weixin.qq.com/s/5kvrYMWG0vFR3vj-aNY49g) - [测试用例中的细节](https://mp.weixin.qq.com/s/wvScTliPwuvH9ReIoDQGNQ) - [集成测试、单元测试、系统测试](https://mp.weixin.qq.com/s/LRkxMasRvmDYRVb0_aybtA) - [集成测试类型和最佳实践](https://mp.weixin.qq.com/s/sSubzrs3cikLV7rmRQaWEA) - [软件测试中质量优于数量](https://mp.weixin.qq.com/s/4FxtVFqialRz6R4680rPAw) - [DevOps工具](https://mp.weixin.qq.com/s/4r8FoxQyYZ5naowML5Cw-Q) - [2020年Tester自我提升](https://mp.weixin.qq.com/s/vuhUp85_6Sbg6ReAN3TTSQ) - [DevOps中的测试工程师](https://mp.weixin.qq.com/s/42Ile_T1BAIp7QHleI-c7w) - [敏捷团队的回归测试策略](https://mp.weixin.qq.com/s/Z7dzDdfp5_kxvzBVQ3rEDg) - [测试自动化与自动化测试:差异很重要](https://mp.weixin.qq.com/s/6HC1bKesOs4mZYb9nOCHjw) - [自动化新手要避免的坑(上)](https://mp.weixin.qq.com/s/MjcX40heTRhEgCFhInoqYQ) - [自动化新手要避免的坑(下)](https://mp.weixin.qq.com/s/azDUo1IO5JgkJIS9n1CMRg) - [如何成为全栈自动化工程师](https://mp.weixin.qq.com/s/j2rQ3COFhg939KLrgKr_bg) - [左移测试](https://mp.weixin.qq.com/s/8zXkWV4ils17hUqlXIpXSw) - [选择手动测试还是自动化测试?](https://mp.weixin.qq.com/s/4haRrfSIp5Plgm_GN98lRA) - [从单元测试标准中学习](https://mp.weixin.qq.com/s/x0TyMAdPBWYL7JSPAmoQsw) - [负载测试很重要](https://mp.weixin.qq.com/s/2q7kNQVJuNwB948ks463CA) - [白盒测试扫盲](https://mp.weixin.qq.com/s/s_FvGZTC42GEjaWzroz1eA) - [自动化测试项目为何失败](https://mp.weixin.qq.com/s/KFJXuLjjs1hii47C1BH8PA) - [简化测试用例](https://mp.weixin.qq.com/s/BhwfDqhN9yoa3Iul_Eu5TA) - [敏捷测试二三事](https://mp.weixin.qq.com/s/bKkGWJA3JhvdCjgg6-AVEQ) - [软件测试中的虚拟化](https://mp.weixin.qq.com/s/zHyJiNFgHIo2ZaPFXsxQMg) - [新词:QA-Ops](https://mp.weixin.qq.com/s/detcY6OVYmzOTUxfwN6CFQ) - [生产环境中进行自动化测试](https://mp.weixin.qq.com/s/JKEGRLOlgpINUxs-6mohzA) - [所谓UI测试](https://mp.weixin.qq.com/s/wDvUy_BhQZCSCqrlC2j1qA) - [预上线环境失败的原因](https://mp.weixin.qq.com/s/jva0Jb2OMarERmTn7Kh2Ng) - [自动化策略六步走](https://mp.weixin.qq.com/s/He69k8iCKhTKD1j-yV6M5g) - [合格的测试经理必备技能](https://mp.weixin.qq.com/s/gFIYksHMn_bHEwAhmgVzjg) - [质量保障的拓展实践](https://mp.weixin.qq.com/s/a3sd0dQnjk3TerOhfo-1ng) - [敏捷领导者常见误区](https://mp.weixin.qq.com/s/xdq3CZflRjvDBGDLK4tNFQ) - [功能自动化测试策略](https://mp.weixin.qq.com/s/qHmcblN4cD4JK6jT7oU4fQ) - [性能测试、压力测试和负载测试](https://mp.weixin.qq.com/s/g26lpd7d7EtpN7pkiqkkjg) - [如何维护自动化测试](https://mp.weixin.qq.com/s/4eh4AN_MiatMSkoCMtY3UA) - [负载测试最佳实践](https://mp.weixin.qq.com/s/hNj7UsCCvv9TdexAcNFUvg) - [有关UI测试计划](https://mp.weixin.qq.com/s/D0fMXwJF754a7Mr5ARY5tQ) - [软件测试外包](https://mp.weixin.qq.com/s/sYQfb2PiQptcT0o_lLpBqQ) - [避免PPT自动化的最佳实践](https://mp.weixin.qq.com/s/5YgYK4_YLZ1wDDhbwMTGlw) - [如何优化软件测试成本](https://mp.weixin.qq.com/s/_eXrzDyNDA6yCRR8nPmzGA) - [如何从手动测试转到自动化测试](https://mp.weixin.qq.com/s/EBDTX4AMnn2KTEjL88bOhQ) - [Selenium自动化测试技巧](https://mp.weixin.qq.com/s/EzrpFaBSVITO2Y2UvYvw0w) - [测试为何会错过Bug](https://mp.weixin.qq.com/s/UFHy8OwZjnMkB70roIS-zQ) - [测试用例设计——一切测试的基础](https://mp.weixin.qq.com/s/0_ubnlhp2jk-jxHxJ95E9g) - [移动应用测试:挑战,类型和最佳实践](https://mp.weixin.qq.com/s/kYxh6xki69evVDsXDxrYKQ) - [敏捷测试中面临的挑战](https://mp.weixin.qq.com/s/vmsW56r1J7jWXHSZdcwbPg) - [AI如何影响测试行业](https://mp.weixin.qq.com/s/d6c7u1-lAmsiIQz3UvcGKg) - [自动测试失败的5个原因](https://mp.weixin.qq.com/s/bTakAHIcx_WyJIo-tsbvvg) - [大促前必做的质量检查](https://mp.weixin.qq.com/s/iOku2wKnlr8pSZO0l9Q3Bw) - [测试开发工程师工作技巧](https://mp.weixin.qq.com/s/TvrUCisja5Zbq-NIwy_2fQ) - [敏捷回归测试](https://mp.weixin.qq.com/s/_bBQFggkZTTEqcb9R_68OA) - [制定质量管理计划指南](https://mp.weixin.qq.com/s/ztXYE8EtwlkUdxnk1cjKVg) - [质量管理计划的基本要素](https://mp.weixin.qq.com/s/v8lOioYn01S1F0ex4mmljA) - [质量保障的方法和实践](https://mp.weixin.qq.com/s/hU_YCaZB-0a09dOCAVgcpw) - [为什么测试覆盖率如此重要](https://mp.weixin.qq.com/s/0evyuiU2kdXDgMDnDKjORg) - [自动化测试框架](https://mp.weixin.qq.com/s/vu6p_rQd3vFKDYu8JDJ0Rg) - [敏捷中的端到端测试](https://mp.weixin.qq.com/s/cdi4xnEzDLpl9ncQguLuAQ) - [自动化测试灵魂三问:是什么、为什么和做什么](https://mp.weixin.qq.com/s/geOejJx79-jTwafG9aXwqA) - [基于代码的自动化和无代码自动化](https://mp.weixin.qq.com/s/8Dopihqs4XzpU-sN-I94kw) - [物联网测试](https://mp.weixin.qq.com/s/B_JI4DANxoOq4HurxZC65Q) - [功能测试知多少](https://mp.weixin.qq.com/s/vTxZLwlvlfIBv892Ji-oLQ) - [如何选择自动化测试工具](https://mp.weixin.qq.com/s/yJo-d9bAZDs1Lcp8j7ISRg) - [连续测试策略](https://mp.weixin.qq.com/s/0aD_0cUW83oPu3sl7sHNnQ) - [如何设置质量检查流程](https://mp.weixin.qq.com/s/PQeXxMZzzU15xSfY5wkVgA) - [编写干净的代码之变量篇](https://mp.weixin.qq.com/s/J9rGIe8a2xaLlNJq2nVmzw) - [高效Mobile DevOps步骤](https://mp.weixin.qq.com/s/-qc-d_zJ1C9H_Uvd8gJiBw) - [回归BUG](https://mp.weixin.qq.com/s/00j-acjPeKQ7uap62WpY3w) - [处理回归BUG最佳实践](https://mp.weixin.qq.com/s/R3O2NruPAA2gQf4-3R6aAQ) - [自动化测试实践清单](https://mp.weixin.qq.com/s/972WruGsYmkRroquBFoqMg) - [自动化测试类型](https://mp.weixin.qq.com/s/GRkN8ozZiWNu21Y3KbVOBA) - [无脚本测试](https://mp.weixin.qq.com/s/PVBxk4KEwCmWkB6mOXJFlw) - [自动化测试转型挑战及其解决方案](https://mp.weixin.qq.com/s/BixS6jRdF5N_nvmW3_OthQ) - [无数据驱动自动化测试](https://mp.weixin.qq.com/s/aCYRGxkzMogLbmACYo6ssw) - [为什么自动化测试在敏捷开发中很重要](https://mp.weixin.qq.com/s/AP0wUQZ09NvSqme8e09igQ) - [测试模型中理解压力测试和负载测试](https://mp.weixin.qq.com/s/smNLx3malzM3avkrn3EJiA) - [移动测试工程师职业](https://mp.weixin.qq.com/s/dhtR4TbQNu5fWpmJkXGivw) - [远程测试工作挑战](https://mp.weixin.qq.com/s/LK-GEN4OtuWVGDuG8psmOQ) - [自动化测试用例的原子性](https://mp.weixin.qq.com/s/jA5WMHwJcu88nHXWoMBAdQ) - [可测性经验分享](https://mp.weixin.qq.com/s/iRtUjESYS3sh3YTD-BWjdA) - [敏捷中的回归测试的优化【译】](https://mp.weixin.qq.com/s/nDiZZgA1PIiAUCG_xwA2rA) - [敏捷的主要优势【译】](https://mp.weixin.qq.com/s/zkI85TLI37XrPFaQ-pZYMA) ## 大咖风采 - [Tcloud 云测平台--集大成者](https://mp.weixin.qq.com/s/29sEO39_NyDiJr-kY5ufdw) - [Android App 测试工具及知识大集合](https://mp.weixin.qq.com/s/Xk9rCW8whXOTAQuCfhZqTg) - [Android App常规测试内容](https://mp.weixin.qq.com/s/tweeoS5wTqK3k7R2TVuDXA) - [JVM的对象和堆](https://mp.weixin.qq.com/s/iNDpTz3gBK3By_bvUnrWOA) # UI自动化 ## UI自动化 - [自动化测试中java多线程的使用实例](https://mp.weixin.qq.com/s/BNSLaIdcTPTNj1tKpGf6fw) - [自动化测试中递归函数的应用](https://mp.weixin.qq.com/s/86602zV9zYblhCRMiUlwdA) ## UiAutomator - [android uiautomator一个画心形图案的方法--代码的浪漫](https://mp.weixin.qq.com/s/byfAKHxD2i83VHnuaNgIZA) - [android UiAutomator了解源码解决控件bonds\[0,0\]无法点击](https://mp.weixin.qq.com/s/nu2ftXNUSG2_kmZjyhEcVA) - [android UiAutomator在清除文本时遇到中文的解决办法](https://mp.weixin.qq.com/s/cNGNCoXsYBSk-MWTWLxF4g) - [android UiAutomator获取当前页面某类控件个数的方法](https://mp.weixin.qq.com/s/njb19Sq_Kg4SusAS_eEuug) - [android uiautomator自定义监听示例--一个弹出权限设置的监听](https://mp.weixin.qq.com/s/OKKZOf51yq6qY5D6PvN-gg) - [如何在Mac OS上使用UiAutomator快速调试类](https://mp.weixin.qq.com/s/jm9d_42jp_BSlv-IW0BpzQ) - [UiAutomator测试中如何恢复手机输入法](https://mp.weixin.qq.com/s/o4-zCgbdq6OsHRK9XT14QA) - [android UiAutomator基本api的二次封装](https://mp.weixin.qq.com/s/_3jGg3ZYoeyAkjZpy8gWfQ) - [android UiAutomator让运行失败的用例重新运行](https://mp.weixin.qq.com/s/tMOPbt1w9tRaKEuIZYKCyg) - [利用UiAutomator写一个首页刷新的稳定性测试脚本](https://mp.weixin.qq.com/s/au9hAScsqUdcrh_usu8Pfg) - [android UiAutomator长按实现控制按住控件时间的方法](https://mp.weixin.qq.com/s/lOvxAOMh6mmIh3CEV6Fduw) - [android UiAutomator自定义快速调试类](https://mp.weixin.qq.com/s/iP2dTOeVkFMzU3dQ06R9GA) - [利用UiAutomator写一个自动遍历渠道包关键功能的脚本](https://mp.weixin.qq.com/s/0vg2OlfTy0y4T6sWUG-olA) - [android UiAutomator如何根据颜色判断控件的状态](https://mp.weixin.qq.com/s/kldsD3OZ4mJZ5yYQXfXxLw) - [android UiAutomator控制多台手机同时运行用例的方法](https://mp.weixin.qq.com/s/z9vgpOQP0wQffmG4C_oBWg) - [android UiAutomator使用递归函数写一个让屏幕一闪一闪提醒的方法](https://mp.weixin.qq.com/s/AzXjePdmsgs6QsICZOdPyw) - [android UiAutomator获取视频播放进度的方法](https://mp.weixin.qq.com/s/ho070zX9rrLPmh8bZe_HgQ) ## Selenium - [selenium2java截图保存桌面](https://mp.weixin.qq.com/s/OUfwsIo635coGONRNccYlg) - [selenium2java调用JavaScript方法封装](https://mp.weixin.qq.com/s/t-Xs2Hr9TM2bjDiOqQX2mA) - [selenium2java利用mysq解决向浏览器插入cookies时token过期问题](https://mp.weixin.qq.com/s/oAAkDKUGytQjxJLFkod-AQ) - [selenium2java 遇到有三个窗口用例的处理办法](https://mp.weixin.qq.com/s/6AJBanVKYwlsNcvsu_25QQ) - [selenium2java通过第三方登录绕过知乎登陆验证码](https://mp.weixin.qq.com/s/A5uTtxlg4l4pru2z7v1cug) - [selenium2java使用select处理下拉框示例](https://mp.weixin.qq.com/s/FFor451WzuUzINeclGN-Ng) - [selenium2java爬虫示例](https://mp.weixin.qq.com/s/vSZzpzEqsCtASSx6iHqxVA) - [selenium2java写一个设置秒杀价的脚本](https://mp.weixin.qq.com/s/1ocIOYt3gdGIJrd9v2shhg) - [selenium2java基本方法二次封装](https://mp.weixin.qq.com/s/2GaXigt13wa6JgxJkcef5g) - [selenium2java一个弹框上传时间日期大杂烩测试用例](https://mp.weixin.qq.com/s/Z8ZeZ-zFy0q0a-e_epT1Kg) - [selenium2java造数据例子](https://mp.weixin.qq.com/s/ACO2O5f7Po4Qn242lopMBg) - [selenium2java让浏览器停止加载的方法](https://mp.weixin.qq.com/s/aBQdGYys3Bpyf6yigGOCIA) - [selenium2java写一个强制刷新页面的方法](https://mp.weixin.qq.com/s/VWW7cH5WSDmw_eCabUh9LQ) - [selenium2java通过接口获取并注入cookies](https://mp.weixin.qq.com/s/luLHWxPWSekuDMbnKsfJvg) - [Selenium编写自动化用例的8种技巧](https://mp.weixin.qq.com/s/8wRHc_krXNfWclNeOJDNPg) - [JUnit中用于Selenium测试的中实践](https://mp.weixin.qq.com/s/KG4sltQMCfH2MGXkRdtnwA) - [您如何使用Selenium来计算自动化测试的投资回报率?](https://mp.weixin.qq.com/s/DVSEm0DhoAvYfTWIniabJg) - [Selenium 4 Java的最佳测试框架](https://mp.weixin.qq.com/s/MlNyv-kb03gRTcYllxUreA) - [Selenium 4.0 Alpha更新日志](https://mp.weixin.qq.com/s/tU7sm-pcbpRNwDU9D3OVTQ) - [Selenium 4.0 Alpha更新实践](https://mp.weixin.qq.com/s/yT9wpO5o5aWBUus494TIHw) - [JUnit 5和Selenium基础(一)](https://mp.weixin.qq.com/s/ehBRf7st-OxeuvI_0yW3OQ) - [JUnit 5和Selenium基础(二)](https://mp.weixin.qq.com/s/Gt82cPmS2iX-DhKXTXiy8g) - [JUnit 5和Selenium基础(三)](https://mp.weixin.qq.com/s/8YkonXTYgAV5-pLs9yEAVw) - [如何在跨浏览器测试中提高效率](https://mp.weixin.qq.com/s/MB_Wv7yQ6i9BztAZtL4grA) - [Selenium Python使用技巧(一)](https://mp.weixin.qq.com/s/39v8tXG3xig63d-ioEAi8Q) - [Selenium Python使用技巧(二)](https://mp.weixin.qq.com/s/uDM3y9zoVjaRmJJJTNs6Vw) - [Selenium Python使用技巧(三)](https://mp.weixin.qq.com/s/J7-CO-UDspUGSpB8isjsmQ) - [Selenium并行测试基础](https://mp.weixin.qq.com/s/OfXipd7YtqL2AdGAQ5cIMw) - [Selenium并行测试最佳实践](https://mp.weixin.qq.com/s/-RsQZaT5pH8DHPvm0L8Hjw) - [维护Selenium测试自动化的最佳实践](https://mp.weixin.qq.com/s/EMD1aWuzOSfT7j3KeXhJcA) - [Selenium自动化测试技巧](https://mp.weixin.qq.com/s/EzrpFaBSVITO2Y2UvYvw0w) - [Selenium自动化:代码测试与无代码测试](https://mp.weixin.qq.com/s/gtmLpQ5FCeuzh1SB5mxuvg) - [Selenium处理下拉列表](https://mp.weixin.qq.com/s/E2txSVAmDzYIEZWnyAND4g) - [Selenium自动化常见问题](https://mp.weixin.qq.com/s/edoxu-QaD0SOw1VqrhCZWA) - [Selenium4 IDE,它终于来了](https://mp.weixin.qq.com/s/XNotlZvFpmBmBQy1pYifOw) - [Selenium4 IDE特性:无代码趋势和SIDE Runner](https://mp.weixin.qq.com/s/G0S9K0jHsN0P_jxdMME-cg) - [Selenium4 IDE特性:弹性测试、循环和逻辑判断](https://mp.weixin.qq.com/s/o4_jIyi9O7s4S3CbTzl5rQ) - [Selenium自动化最佳实践技巧(上)](https://mp.weixin.qq.com/s/lZww1azmncMMMHRY0_yKqA) - [Selenium自动化最佳实践技巧(中)](https://mp.weixin.qq.com/s/9D0lUZ-XKHiukNeRqp6zOQ) - [Selenium自动化最佳实践技巧(下)](https://mp.weixin.qq.com/s/opVik2ZxmTBurIBoa4yipQ) - [Selenium等待:sleep、隐式、显式和Fluent](https://mp.weixin.qq.com/s/73BobMq9M12rYMvzxNhRtA) - [Selenium自动化的JUnit参数化实践](https://mp.weixin.qq.com/s/WFu5rJaowxhAIcbEoEatkw) - [Selenium异常集锦](https://mp.weixin.qq.com/s/DDkaliSVthX-c_KKG-WwNA) - [Selenium自动化测试之前](https://mp.weixin.qq.com/s/DKjSnS9sP0SoHUw4OhOikw) ## APP性能 - [使用monkey测试时,一个控制WiFi状态的多线程类](https://mp.weixin.qq.com/s/P8HVtzHBlj_FcDAAHFKBDg) - [java执行和停止Logcat命令及多线程实现](https://mp.weixin.qq.com/s/sUYibRc-muxQoxi48QiaRg) - [APP性能测试中获取CPU和PSS数据多线程实现](https://mp.weixin.qq.com/s/NiJSZ8VxpdnarbDJjcJziA) - [统计APP启动时间和进入首页时间的多线程类](https://mp.weixin.qq.com/s/IMs6vd3H-HF65Vb-zPwDhw) - [如何获取手机性能测试数据FPS](https://mp.weixin.qq.com/s/qZy5AQkNpUXRJk46BHVzaQ) - [一个循环启动APP并保持WiFi常开的多线程类](https://mp.weixin.qq.com/s/OgdT4IffDyAdkKmO2SS9iQ) ## 杂乱 - [测试窝,首页抄我七篇原创还拉黑,你们的良心不会痛吗?](https://mp.weixin.qq.com/s/ke5avkknkDMCLMAOGT7wiQ) - [如何优雅地屏蔽掉Google搜索结果中视频、新闻、图片等结果](https://mp.weixin.qq.com/s/Iu7pt4Qk3w9sJp3n_UVAeQ) - [测试玩梗--欢迎补充](https://mp.weixin.qq.com/s/y_QHbsjFCQVSCfj-A4Usmg) - [图解HTTP脑图](https://mp.weixin.qq.com/s/100Vm8FVEuXs0x6rDGTipw) - [测试之JVM命令脑图](https://mp.weixin.qq.com/s/qprqyv0j3SCvGw1HMjbaMQ) - [好书推荐《Java性能权威指南》](https://mp.weixin.qq.com/s/YWd5Yx6n7887g1lMLTcsWQ) - [2019年浏览器市场份额排行榜](https://mp.weixin.qq.com/s/4NmJ_ZCPD5UwaRCtaCfjEg) - [JSON基础](https://mp.weixin.qq.com/s/tnQmAFfFbRloYp8J9TYurw) - [JMeter吞吐量误差分析](https://mp.weixin.qq.com/s/jHKmFNrLmjpihnoigNNCSg) - [JMeter如何模拟不同的网络速度](https://mp.weixin.qq.com/s/1FCwNN2htfTGF6ItdkcCzw) - [疫情期间,如何提高远程办公效率](https://mp.weixin.qq.com/s/k_XrdqjGKMshK2Ea-VCNLw) - [接口测试视频专题](https://mp.weixin.qq.com/s/4mKpW3QiVRee3kcVOSraog) - [Groovy在JMeter中应用专题](https://mp.weixin.qq.com/s/KcxPUDWl7MLQemFRoIV92A) - [Java多线程编程在JMeter中应用](https://mp.weixin.qq.com/s/xCnFx5TvIF1SAVNm-aZnxQ) - [未来的神器fiddler Everywhere](https://mp.weixin.qq.com/s/-BSuHR6RPkdv8R-iy47MLQ) - [Charles报错Failed to install helper解决方案](https://mp.weixin.qq.com/s/LHhMTBhlDM0DrPCvWeU0zA) - [测试仓库推介(上)](https://mp.weixin.qq.com/s/zgy6UgNMFcbISD1NhxSAWg) - [测试仓库推介(下)](https://mp.weixin.qq.com/s/njnpmRGoEgdxjqkR7c3a6A) - [Fiddler Everywhere工具答疑](https://mp.weixin.qq.com/s/2peWMJ-rgDlVjs3STNeS1Q) - [Mac上测试Internet Explorer的N种方法](https://mp.weixin.qq.com/s/HeLBPTp2dfs5IlyLMCi90Q) - [IntelliJ中基于文本的HTTP客户端](https://mp.weixin.qq.com/s/-9qi_lLVVfxQKEFmcRYFtA) - [开源礼节](https://mp.weixin.qq.com/s/EyNules2f9NYdnYAX_NQSw) - [弱网测试:最低流畅网速是多少?](https://mp.weixin.qq.com/s/rCji6fZs9yYyk7GyIWvSiA) - [接口测试直播回顾](https://mp.weixin.qq.com/s/B8ih9sswaE-OWuVib6C16g) - [SpotBugs报错no Groovy library is defined解决办法](https://mp.weixin.qq.com/s/XxvuVS2TmlqT5-b22vObYQ) - [推荐好书:不要总是谦卑地弯着腰](https://mp.weixin.qq.com/s/mYNN9jSaikOF5aJEkb-Bug) - [2020年FunTester自我总结](https://mp.weixin.qq.com/s/DeDY1JZUTk3cjjQfr3DJRg) - [原创打油诗欣赏](https://mp.weixin.qq.com/s/3hPSDjH-3cWu6EVsjU0wOw) - [优秀讲师 | 腾讯云+社区权威认证](https://mp.weixin.qq.com/s/oeTeJZs6h4jJJMRyUunurw) - [Boss直聘签约作者](https://mp.weixin.qq.com/s/CjwFBoS9sew6R74mM9QO4g) - [假期思考题](https://mp.weixin.qq.com/s/3DOnkmYDlwk-XKg4ge3ZUw) - [甩锅技能+1](https://mp.weixin.qq.com/s/nMwlfXZoDcRRPHcTKpvfNg) ================================================ FILE: document/api.markdown ================================================ # 接口篇 ##### **FunTester**,[腾讯云年度作者](https://mp.weixin.qq.com/s/oeTeJZs6h4jJJMRyUunurw)、[Boss直聘签约作者](https://mp.weixin.qq.com/s/CjwFBoS9sew6R74mM9QO4g),非著名测试开发er,欢迎关注。 * `Gitee`地址*https://gitee.com/fanapi/tester* * `GitHub`地址*https://github.com/JunManYuanLong/FunTester* # 接口测试 ## 接口功能测试 - [开源测试服务](https://mp.weixin.qq.com/s/ZOs0cp_vt6_iiundHaKk4g) - [使用springboot+mybatis数据库存储服务化](https://mp.weixin.qq.com/s/N_5tHW1JJLZlxCaDI2PvyQ) - [alertover推送api的java httpclient实现实例](https://mp.weixin.qq.com/s/DJXCBEG3SbybfbT6blO1jA) - [接口自动化通用验证类](https://mp.weixin.qq.com/s/fP1clCKkLREfg6POKV5n1A) - [将swagger文档自动变成测试代码](https://mp.weixin.qq.com/s/SY8mVenj0zMe5b47GS9VSQ) - [httpclient处理多用户同时在线](https://mp.weixin.qq.com/s/Nuc30Fwy6-Qyr-Pc65t1_g) - [使用httpclient实现图灵机器人web api调用实例](https://mp.weixin.qq.com/s/dYyxvAhwSmJkNI8N9lYQfg) - [groovy如何使用java接口测试框架发送http请求](https://mp.weixin.qq.com/s/KF5lzMT-E2IBOkp_UjuC4g) - [httpclient调用京东万象数字营销频道新闻api实例](https://mp.weixin.qq.com/s/kSqgSbPci-q2pfsdcU5Ekw) - [httpclient遇到socket closed解决办法](https://mp.weixin.qq.com/s/mDRC7mssKmnvcI6StQWIBQ) - [httpclient4.5如何确保资源释放](https://mp.weixin.qq.com/s/373Lx1bv0vi-pIBgWNzC9Q) - [httpclient如何处理302重定向](https://mp.weixin.qq.com/s/vg354AjPKhIZsnSu4GZjZg) - [基于java的直线型接口测试框架初探](https://mp.weixin.qq.com/s/xhg4exdb1G18-nG5E7exkQ) - [利用alertover发送获取响应失败的通知消息](https://mp.weixin.qq.com/s/w6y2UkgL3J20mAxc8fq0tA) - [使用httpclient中EntityUtils类解析entity遇到socket closed错误的原因](https://mp.weixin.qq.com/s/RJnuOa2K6aRCElJafkFeug) - [httpclient接口测试中重试控制器设置](https://mp.weixin.qq.com/s/hknNdq_ybQ1MoXh_dI3JVA) - [拼接GET请求的参数](https://mp.weixin.qq.com/s/EGw_97scexH_3m2Uc8Ye5A) - [httpclient上传文件方法的封装](https://mp.weixin.qq.com/s/HIrwl5ullvEmn_UuyLKkRg) - [接口批量上传文件的实例](https://mp.weixin.qq.com/s/wZwkWchXXC6iddX1oVEnZQ) - [httpclient发送https协议请求以及javax.net.ssl.SSLHandshakeException解决办法](https://mp.weixin.qq.com/s/uSHhKRrL2f9USKpSykkpkQ) - [API测试基础](https://mp.weixin.qq.com/s/bkbUEa9CF21xMYSlhPcULw) - [拷贝HttpRequestBase对象](https://mp.weixin.qq.com/s/kxB1c0GmSF5OAM15UQJU2Q) - [API自动化测试指南](https://mp.weixin.qq.com/s/uy_Vn_ZVUEu3YAI1gW2T_A) - [如何统一接口测试的功能、自动化和性能测试用例](https://mp.weixin.qq.com/s/1xqtXNVw7BdUa03nVcsMTg) - [如何选择API测试工具](https://mp.weixin.qq.com/s/m2TNJDiqAAWYV9L6UP-29w) - [初学者的API测试技巧](https://mp.weixin.qq.com/s/_uk6dw5Q7CfS-gXGH-TZEQ) - [压测中测量异步写入接口的延迟](https://mp.weixin.qq.com/s/odvK1iYgg4eRVtOOPbq15w) - [多项目登录互踢测试用例](https://mp.weixin.qq.com/s/Nn_CUy_j7j6bUwHSkO0pCQ) - [httpclient使用HTTP代理实践](https://mp.weixin.qq.com/s/24IJwJ1TJWHdfj0PzSjmvw) - [HTTP异步连接池和多线程实践](https://mp.weixin.qq.com/s/8M348LuHakBe4GAEnnDPxw) - [IntelliJ中基于文本的HTTP客户端](https://mp.weixin.qq.com/s/-9qi_lLVVfxQKEFmcRYFtA) - [socket接口开发和测试初探](https://mp.weixin.qq.com/s/uhmkbrMp91PP1pQjlEofOQ) - [IntelliJ中基于文本的HTTP客户端](https://mp.weixin.qq.com/s/-9qi_lLVVfxQKEFmcRYFtA) - [基于WebSocket的client封装](https://mp.weixin.qq.com/s/1lZvsuGEa6hiRHOgOT-Kmg) - [基于Socket.IO的Client封装](https://mp.weixin.qq.com/s/Ux90AXI9g85w7R5i3f9idg) - [Socket.IO接口多用户测试实践](https://mp.weixin.qq.com/s/aCLaRZQs8zMK_ptJ-PjClw) - [Python版Socket.IO接口测试脚本](https://mp.weixin.qq.com/s/oXBP6Sx3yPqlmvV9uCUScw) - [命令行如何执行jar包里面的方法](https://mp.weixin.qq.com/s/50oMEmVEnv5Vzlm1HOxuFw) - [JSON对象标记语法验证类](https://mp.weixin.qq.com/s/jSXmoEdMF7nWAqQuzJ5GiQ) - [Socket接口异步验证实践](https://mp.weixin.qq.com/s/bnjHK3ZmEzHm3y-xaSVkTw) - [无数据驱动自动化测试](https://mp.weixin.qq.com/s/aCYRGxkzMogLbmACYo6ssw) - [白板点阵数据传输测试初探](https://mp.weixin.qq.com/s/EzFC-hIvgm7j7947TZU6BA) - [基于Socket.IO的白板点阵坐标传输接口测试实践](https://mp.weixin.qq.com/s/pDAx4jwYvcRcdld5cKLAUw) ## 接口测试视频 - [FunTester测试框架视频讲解(序)](https://mp.weixin.qq.com/s/CJrHAAniDMyr5oDXYHpPcQ) - [获取HTTP请求对象--测试框架视频讲解](https://mp.weixin.qq.com/s/hG89sGf96GcPb2hGnludsw) - [发送请求和解析响应—测试框架视频解读](https://mp.weixin.qq.com/s/xUQ8o3YuZOChXZ2UGR1Kyw) - [json对象基本操作--视频讲解](https://mp.weixin.qq.com/s/MQtcIGKwWGEMb2XD3zmAIQ) - [GET请求实践--测试框架视频讲解](https://mp.weixin.qq.com/s/_ZEDmRPXe4SLjCgdwDtC7A) - [POST请求实践--视频演示](https://mp.weixin.qq.com/s/g0mLzMQ4Br2e592m3p68eg) - [如何处理header和cookie--视频演示](https://mp.weixin.qq.com/s/MkwzT9VPglSnOxY7geSUiQ) - [FunRequest类功能--视频演示](https://mp.weixin.qq.com/s/WGS6ZwAvw7X4MC004Gz4pA) - [接口测试业务验证--视频演示](https://mp.weixin.qq.com/s/DH8HDmaritXQnkBIFOadoA) - [自动化测试项目基础--视频讲解](https://mp.weixin.qq.com/s/n9zu4OLyj7FbNsV0bYlOYg) - [JSONArray基本操作--视频演示](https://mp.weixin.qq.com/s/OosDbRoknMe1riaPc3hhLg) - [自动化项目基类实践--视频演示](https://mp.weixin.qq.com/s/IdvSi-GDtE5nqGnR-_4LWA) - [模块类和自动化用例实践--视频演示](https://mp.weixin.qq.com/s/Y_A8M7KHmdlJJOD4B4rN4Q) - [性能框架多线程基类和执行类--视频讲解](https://mp.weixin.qq.com/s/8Dh-5XfvX8Fm4IqmzbtY6Q) - [定时和定量压测模式实现--视频讲解](https://mp.weixin.qq.com/s/l_4wCjVM1fAVRHgEPrcrwg) - [基于HTTP请求的多线程实现类--视频讲解](https://mp.weixin.qq.com/s/8SG1xtzq8ArY84Bxm_SNow) # 单元&白盒 - [Maven和Gradle中配置单元测试框架Spock](https://mp.weixin.qq.com/s/kL5keijAAZwmq_DO1NDBtw) - [Groovy单元测试框架spock基础功能Demo](https://mp.weixin.qq.com/s/fQCyIyeQANbu2YP2ML6_8Q) - [Groovy单元测试框架spock数据驱动Demo](https://mp.weixin.qq.com/s/uCAB7Mxt1JZW229aKp-uVQ) - [人生苦短?试试Groovy进行单元测试](https://mp.weixin.qq.com/s/ahyP-YQTzigeq_5N8byC4g) - [模糊断言](https://mp.weixin.qq.com/s/OlJpqHkwpY6-yyELvQ9cIw) - [使用WireMock进行更好的集成测试](https://mp.weixin.qq.com/s/oMuVZOOQmuxSygJWH2_QHg) - [如何测试这个方法--功能篇](https://mp.weixin.qq.com/s/4zrwkc6ccozUGjOGV563dQ) - [如何测试这个方法--性能篇](https://mp.weixin.qq.com/s/QXl9_9Bj5c191oxkXmByUA) - [单元测试用例](https://mp.weixin.qq.com/s/UFEXJ1aXOvJUYp49iVLr5w) - [关于测试覆盖率](https://mp.weixin.qq.com/s/E15D785fkaWH7-YhiE5gPw) - [JUnit 5和Selenium基础(一)](https://mp.weixin.qq.com/s/ehBRf7st-OxeuvI_0yW3OQ) - [JUnit 5和Selenium基础(二)](https://mp.weixin.qq.com/s/Gt82cPmS2iX-DhKXTXiy8g) - [JUnit 5和Selenium基础(三)](https://mp.weixin.qq.com/s/8YkonXTYgAV5-pLs9yEAVw) - [浅谈单元测试](https://mp.weixin.qq.com/s/mJM9qXQepSYQ9vLBnBEs3Q) - [Spock 2.0 M1版本初探](https://mp.weixin.qq.com/s/nyYh2QzER03kIk-w9P9GNw) - [Java并发BUG基础篇](https://mp.weixin.qq.com/s/NR4vYx81HtgAEqH2Q93k2Q) - [Java并发BUG提升篇](https://mp.weixin.qq.com/s/GCRRe8hJpe1QJtxq9VBEhg) - [集成测试、单元测试、系统测试](https://mp.weixin.qq.com/s/LRkxMasRvmDYRVb0_aybtA) - [从单元测试标准中学习](https://mp.weixin.qq.com/s/x0TyMAdPBWYL7JSPAmoQsw) - [白盒测试扫盲](https://mp.weixin.qq.com/s/s_FvGZTC42GEjaWzroz1eA) - [Mock System.in和检查System.out](https://mp.weixin.qq.com/s/1ly3uXCZsukmIylN6F5GxQ) - [单元测试框架spock和Mockito应用](https://mp.weixin.qq.com/s/s21Lts1UnG9HwOEVvgj-uw) - [Mockito框架Mock Void方法](https://mp.weixin.qq.com/s/R95wOMVyeDCHm3_Z0S2kqg) - [JsonPath工具类单元测试](https://mp.weixin.qq.com/s/1YtUWGk_sTjn9bHwAeT0Ew) - [Intellij静态代码扫描插件SpotBugs](https://mp.weixin.qq.com/s/8ivsMNOmT0LDfvcM06IGMg) - [SpotBugs注解SuppressWarnings在Java&Groovy中的应用](https://mp.weixin.qq.com/s/R0JoqmAqhUbRSjIJ61h_tg) ## 性能测试 - [Linux性能监控软件netdata中文汉化版](https://mp.weixin.qq.com/s/7VG7gHx7FUvsuNtBTJpjWA) - [性能测试框架](https://mp.weixin.qq.com/s/3_09j7-5ex35u30HQRyWug) - [性能测试框架第二版](https://mp.weixin.qq.com/s/JPyGQ2DRC6EVBmZkxAoVWA) - [性能测试框架第三版](https://mp.weixin.qq.com/s/Mk3PoH7oJX7baFmbeLtl_w) - [一个时间计数器timewatch辅助性能测试](https://mp.weixin.qq.com/s/-YZ04n2kyfO0q2QaKHX_0Q) - [如何在Linux命令行界面愉快进行性能测试](https://mp.weixin.qq.com/s/fwGqBe1SpA2V0lPfAOd04Q) - [Mac+httpclient高并发配置实例](https://mp.weixin.qq.com/s/r4a-vGz0pxeZBPPH3phujw) - [单点登录性能测试方案](https://mp.weixin.qq.com/s/sv8FnvIq44dFEq63LpOD2A) - [如何对消息队列做性能测试](https://mp.weixin.qq.com/s/MNt22aW3Op9VQ5OoMzPwBw) - [如何对修改密码接口进行压测](https://mp.weixin.qq.com/s/9CL_6-uZOlAh7oeo7NOpag) - [如何对单行多次update接口进行压测](https://mp.weixin.qq.com/s/Ly1Y4iPGgL6FNRsbOTv0sg) - [如何对多行单次update接口进行压测](https://mp.weixin.qq.com/s/Fsqw7vlw6K9EKa_XJwGIgQ) - [如何获取JVM堆转储文件](https://mp.weixin.qq.com/s/qCg7nsXVvT1q-9yquQOfWA) - [性能测试中标记每个请求](https://mp.weixin.qq.com/s/PokvzoLdVf_y9inlVXHJHQ) - [如何对N个接口按比例压测](https://mp.weixin.qq.com/s/GZxbH4GjDkk4BLqnUj1_kw) - [如何性能测试中进行业务验证](https://mp.weixin.qq.com/s/OEvRy1bS2Yq_w1kGiidmng) - [性能测试中记录每一个耗时请求](https://mp.weixin.qq.com/s/VXcp4uIMm8mRgqe8fVhuCQ) - [线程安全类在性能测试中应用](https://mp.weixin.qq.com/s/0-Y63wXqIugVC8RiKldHvg) - [利用微基准测试修正压测结果](https://mp.weixin.qq.com/s/dmO33qhOBrTByw_NshS-uA) - [性能测试如何减少本机误差](https://mp.weixin.qq.com/s/S6b_wwSowVolp1Uu6sEIOA) - [服务端性能优化之异步查询转同步](https://mp.weixin.qq.com/s/okYP2aOPfkWj2FjZcAtQNA) - [服务端性能优化之双重检查锁](https://mp.weixin.qq.com/s/-bOyHBcqFlJY3c0PEZaWgQ) - [多种登录方式定量性能测试方案](https://mp.weixin.qq.com/s/WuZ2h2rr0rNBgEvQVioacA) - [性能测试中图形化输出测试数据](https://mp.weixin.qq.com/s/EMvpYIsszdwBJFPIxztTvA) - [压测中测量异步写入接口的延迟](https://mp.weixin.qq.com/s/odvK1iYgg4eRVtOOPbq15w) - [手机号验证码登录性能测试](https://mp.weixin.qq.com/s/i-j8fJAdcsJ7v8XPOnPDAw) - [绑定手机号性能测试](https://mp.weixin.qq.com/s/K5x1t1dKtIT2NKV6k4v5mw) - [终止性能测试并输出报告](https://mp.weixin.qq.com/s/II4-UbKDikctmS_vRT-xLg) - [CountDownLatch类在性能测试中应用](https://mp.weixin.qq.com/s/uYBPPOjauR2h81l2uKMANQ) - [CyclicBarrier类在性能测试中应用](https://mp.weixin.qq.com/s/kvEHX3t_2xpMke9vwOdWrg) - [Phaser类在性能测试中应用](https://mp.weixin.qq.com/s/plxNnQq7yNQvHYEGpyY4uA) - [如何同时压测创建和删除接口](https://mp.weixin.qq.com/s/NCeoEF3DkEtpdaaQ365I0Q) - [固定QPS压测模式探索](https://mp.weixin.qq.com/s/S2h-zEUoik_CWs60RL6g7Q) - [固定QPS压测初试](https://mp.weixin.qq.com/s/ySlJmDIH3fFB4qEnL-ueMg) - [命令行如何执行jar包里面的方法](https://mp.weixin.qq.com/s/50oMEmVEnv5Vzlm1HOxuFw) - [链路压测中如何记录每一个耗时的请求](https://mp.weixin.qq.com/s/8sb5QZcKbBjTxCaXK5ajXA) - [Socket接口异步验证实践](https://mp.weixin.qq.com/s/bnjHK3ZmEzHm3y-xaSVkTw) - [性能测试中集合点和多阶段同步问题初探](https://mp.weixin.qq.com/s/NlpD1WyMrcG1V5RYfY0Plg) - [性能测试中标记请求参数实践](https://mp.weixin.qq.com/s/2FNMU-k_En26FCqWkYpvhQ) - [测试模型中理解压力测试和负载测试](https://mp.weixin.qq.com/s/smNLx3malzM3avkrn3EJiA) - [重放浏览器单个请求性能测试实践](https://mp.weixin.qq.com/s/a10hxCrIzS4TV9JwmDSI3Q) - [重放浏览器多个请求性能测试实践](https://mp.weixin.qq.com/s/Hm1Kpp1PMrZ5rYFW8l2GlA) - [重放浏览器请求多链路性能测试实践](https://mp.weixin.qq.com/s/9YSBLAyHVw8Z6IfK-nJTpQ) - [性能测试中异步展示测试进度](https://mp.weixin.qq.com/s/AOERJbEc4ATJqhjvnxgQoA) - [ThreadLocal在链路性能测试中实践](https://mp.weixin.qq.com/s/3qhNdHHSStELzNraQSpcew) - [Socket接口固定QPS性能测试实践](https://mp.weixin.qq.com/s/I9-14L8THxvtX1NJY0KPfw) - [单链路性能测试实践](https://mp.weixin.qq.com/s/4xHLP-GZwrNu5cFKdfsB6g) - [链路性能测试中参数多样性方法分享](https://mp.weixin.qq.com/s/I1pm0fulNrj_S-YkNz-gEA) - [链路测试中参数流转图](https://mp.weixin.qq.com/s/xyo9HXBLoXgLW6MSFH3V6w) - [线程同步类CyclicBarrier在性能测试集合点应用](https://mp.weixin.qq.com/s/K2YySxX9T4v_rzbvIbIHJA) ================================================ FILE: document/article.markdown ================================================ # 总目录 > **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。 * `Gitee`地址*https://gitee.com/fanapi/tester* * `GitHub`地址*https://github.com/JunManYuanLong/FunTester* # 接口测试 ## 接口功能测试 - [开源测试服务](https://mp.weixin.qq.com/s/ZOs0cp_vt6_iiundHaKk4g) - [使用springboot+mybatis数据库存储服务化](https://mp.weixin.qq.com/s/N_5tHW1JJLZlxCaDI2PvyQ) - [alertover推送api的java httpclient实现实例](https://mp.weixin.qq.com/s/DJXCBEG3SbybfbT6blO1jA) - [接口自动化通用验证类](https://mp.weixin.qq.com/s/fP1clCKkLREfg6POKV5n1A) - [将swagger文档自动变成测试代码](https://mp.weixin.qq.com/s/SY8mVenj0zMe5b47GS9VSQ) - [httpclient处理多用户同时在线](https://mp.weixin.qq.com/s/Nuc30Fwy6-Qyr-Pc65t1_g) - [使用httpclient实现图灵机器人web api调用实例](https://mp.weixin.qq.com/s/dYyxvAhwSmJkNI8N9lYQfg) - [groovy如何使用java接口测试框架发送http请求](https://mp.weixin.qq.com/s/KF5lzMT-E2IBOkp_UjuC4g) - [httpclient调用京东万象数字营销频道新闻api实例](https://mp.weixin.qq.com/s/kSqgSbPci-q2pfsdcU5Ekw) - [httpclient遇到socket closed解决办法](https://mp.weixin.qq.com/s/mDRC7mssKmnvcI6StQWIBQ) - [httpclient4.5如何确保资源释放](https://mp.weixin.qq.com/s/373Lx1bv0vi-pIBgWNzC9Q) - [httpclient如何处理302重定向](https://mp.weixin.qq.com/s/vg354AjPKhIZsnSu4GZjZg) - [基于java的直线型接口测试框架初探](https://mp.weixin.qq.com/s/xhg4exdb1G18-nG5E7exkQ) - [利用alertover发送获取响应失败的通知消息](https://mp.weixin.qq.com/s/w6y2UkgL3J20mAxc8fq0tA) - [使用httpclient中EntityUtils类解析entity遇到socket closed错误的原因](https://mp.weixin.qq.com/s/RJnuOa2K6aRCElJafkFeug) - [httpclient接口测试中重试控制器设置](https://mp.weixin.qq.com/s/hknNdq_ybQ1MoXh_dI3JVA) - [拼接GET请求的参数](https://mp.weixin.qq.com/s/EGw_97scexH_3m2Uc8Ye5A) - [httpclient上传文件方法的封装](https://mp.weixin.qq.com/s/HIrwl5ullvEmn_UuyLKkRg) - [接口批量上传文件的实例](https://mp.weixin.qq.com/s/wZwkWchXXC6iddX1oVEnZQ) - [httpclient发送https协议请求以及javax.net.ssl.SSLHandshakeException解决办法](https://mp.weixin.qq.com/s/uSHhKRrL2f9USKpSykkpkQ) - [API测试基础](https://mp.weixin.qq.com/s/bkbUEa9CF21xMYSlhPcULw) - [拷贝HttpRequestBase对象](https://mp.weixin.qq.com/s/kxB1c0GmSF5OAM15UQJU2Q) - [API自动化测试指南](https://mp.weixin.qq.com/s/uy_Vn_ZVUEu3YAI1gW2T_A) - [如何统一接口测试的功能、自动化和性能测试用例](https://mp.weixin.qq.com/s/1xqtXNVw7BdUa03nVcsMTg) - [如何选择API测试工具](https://mp.weixin.qq.com/s/m2TNJDiqAAWYV9L6UP-29w) - [初学者的API测试技巧](https://mp.weixin.qq.com/s/_uk6dw5Q7CfS-gXGH-TZEQ) - [压测中测量异步写入接口的延迟](https://mp.weixin.qq.com/s/odvK1iYgg4eRVtOOPbq15w) - [多项目登录互踢测试用例](https://mp.weixin.qq.com/s/Nn_CUy_j7j6bUwHSkO0pCQ) - [httpclient使用HTTP代理实践](https://mp.weixin.qq.com/s/24IJwJ1TJWHdfj0PzSjmvw) - [HTTP异步连接池和多线程实践](https://mp.weixin.qq.com/s/8M348LuHakBe4GAEnnDPxw) - [IntelliJ中基于文本的HTTP客户端](https://mp.weixin.qq.com/s/-9qi_lLVVfxQKEFmcRYFtA) - [socket接口开发和测试初探](https://mp.weixin.qq.com/s/uhmkbrMp91PP1pQjlEofOQ) - [IntelliJ中基于文本的HTTP客户端](https://mp.weixin.qq.com/s/-9qi_lLVVfxQKEFmcRYFtA) - [基于WebSocket的client封装](https://mp.weixin.qq.com/s/1lZvsuGEa6hiRHOgOT-Kmg) - [基于Socket.IO的Client封装](https://mp.weixin.qq.com/s/Ux90AXI9g85w7R5i3f9idg) - [Socket.IO接口多用户测试实践](https://mp.weixin.qq.com/s/aCLaRZQs8zMK_ptJ-PjClw) - [Python版Socket.IO接口测试脚本](https://mp.weixin.qq.com/s/oXBP6Sx3yPqlmvV9uCUScw) - [命令行如何执行jar包里面的方法](https://mp.weixin.qq.com/s/50oMEmVEnv5Vzlm1HOxuFw) - [JSON对象标记语法验证类](https://mp.weixin.qq.com/s/jSXmoEdMF7nWAqQuzJ5GiQ) - [Socket接口异步验证实践](https://mp.weixin.qq.com/s/bnjHK3ZmEzHm3y-xaSVkTw) - [无数据驱动自动化测试](https://mp.weixin.qq.com/s/aCYRGxkzMogLbmACYo6ssw) - [白板点阵数据传输测试初探](https://mp.weixin.qq.com/s/EzFC-hIvgm7j7947TZU6BA) - [基于Socket.IO的白板点阵坐标传输接口测试实践](https://mp.weixin.qq.com/s/pDAx4jwYvcRcdld5cKLAUw) ## 接口测试视频 - [FunTester测试框架视频讲解(序)](https://mp.weixin.qq.com/s/CJrHAAniDMyr5oDXYHpPcQ) - [获取HTTP请求对象--测试框架视频讲解](https://mp.weixin.qq.com/s/hG89sGf96GcPb2hGnludsw) - [发送请求和解析响应—测试框架视频解读](https://mp.weixin.qq.com/s/xUQ8o3YuZOChXZ2UGR1Kyw) - [json对象基本操作--视频讲解](https://mp.weixin.qq.com/s/MQtcIGKwWGEMb2XD3zmAIQ) - [GET请求实践--测试框架视频讲解](https://mp.weixin.qq.com/s/_ZEDmRPXe4SLjCgdwDtC7A) - [POST请求实践--视频演示](https://mp.weixin.qq.com/s/g0mLzMQ4Br2e592m3p68eg) - [如何处理header和cookie--视频演示](https://mp.weixin.qq.com/s/MkwzT9VPglSnOxY7geSUiQ) - [FunRequest类功能--视频演示](https://mp.weixin.qq.com/s/WGS6ZwAvw7X4MC004Gz4pA) - [接口测试业务验证--视频演示](https://mp.weixin.qq.com/s/DH8HDmaritXQnkBIFOadoA) - [自动化测试项目基础--视频讲解](https://mp.weixin.qq.com/s/n9zu4OLyj7FbNsV0bYlOYg) - [JSONArray基本操作--视频演示](https://mp.weixin.qq.com/s/OosDbRoknMe1riaPc3hhLg) - [自动化项目基类实践--视频演示](https://mp.weixin.qq.com/s/IdvSi-GDtE5nqGnR-_4LWA) - [模块类和自动化用例实践--视频演示](https://mp.weixin.qq.com/s/Y_A8M7KHmdlJJOD4B4rN4Q) - [性能框架多线程基类和执行类--视频讲解](https://mp.weixin.qq.com/s/8Dh-5XfvX8Fm4IqmzbtY6Q) - [定时和定量压测模式实现--视频讲解](https://mp.weixin.qq.com/s/l_4wCjVM1fAVRHgEPrcrwg) - [基于HTTP请求的多线程实现类--视频讲解](https://mp.weixin.qq.com/s/8SG1xtzq8ArY84Bxm_SNow) # 单元&白盒 - [Maven和Gradle中配置单元测试框架Spock](https://mp.weixin.qq.com/s/kL5keijAAZwmq_DO1NDBtw) - [Groovy单元测试框架spock基础功能Demo](https://mp.weixin.qq.com/s/fQCyIyeQANbu2YP2ML6_8Q) - [Groovy单元测试框架spock数据驱动Demo](https://mp.weixin.qq.com/s/uCAB7Mxt1JZW229aKp-uVQ) - [人生苦短?试试Groovy进行单元测试](https://mp.weixin.qq.com/s/ahyP-YQTzigeq_5N8byC4g) - [模糊断言](https://mp.weixin.qq.com/s/OlJpqHkwpY6-yyELvQ9cIw) - [使用WireMock进行更好的集成测试](https://mp.weixin.qq.com/s/oMuVZOOQmuxSygJWH2_QHg) - [如何测试这个方法--功能篇](https://mp.weixin.qq.com/s/4zrwkc6ccozUGjOGV563dQ) - [如何测试这个方法--性能篇](https://mp.weixin.qq.com/s/QXl9_9Bj5c191oxkXmByUA) - [单元测试用例](https://mp.weixin.qq.com/s/UFEXJ1aXOvJUYp49iVLr5w) - [关于测试覆盖率](https://mp.weixin.qq.com/s/E15D785fkaWH7-YhiE5gPw) - [JUnit 5和Selenium基础(一)](https://mp.weixin.qq.com/s/ehBRf7st-OxeuvI_0yW3OQ) - [JUnit 5和Selenium基础(二)](https://mp.weixin.qq.com/s/Gt82cPmS2iX-DhKXTXiy8g) - [JUnit 5和Selenium基础(三)](https://mp.weixin.qq.com/s/8YkonXTYgAV5-pLs9yEAVw) - [浅谈单元测试](https://mp.weixin.qq.com/s/mJM9qXQepSYQ9vLBnBEs3Q) - [Spock 2.0 M1版本初探](https://mp.weixin.qq.com/s/nyYh2QzER03kIk-w9P9GNw) - [Java并发BUG基础篇](https://mp.weixin.qq.com/s/NR4vYx81HtgAEqH2Q93k2Q) - [Java并发BUG提升篇](https://mp.weixin.qq.com/s/GCRRe8hJpe1QJtxq9VBEhg) - [集成测试、单元测试、系统测试](https://mp.weixin.qq.com/s/LRkxMasRvmDYRVb0_aybtA) - [从单元测试标准中学习](https://mp.weixin.qq.com/s/x0TyMAdPBWYL7JSPAmoQsw) - [白盒测试扫盲](https://mp.weixin.qq.com/s/s_FvGZTC42GEjaWzroz1eA) - [Mock System.in和检查System.out](https://mp.weixin.qq.com/s/1ly3uXCZsukmIylN6F5GxQ) - [单元测试框架spock和Mockito应用](https://mp.weixin.qq.com/s/s21Lts1UnG9HwOEVvgj-uw) - [Mockito框架Mock Void方法](https://mp.weixin.qq.com/s/R95wOMVyeDCHm3_Z0S2kqg) - [JsonPath工具类单元测试](https://mp.weixin.qq.com/s/1YtUWGk_sTjn9bHwAeT0Ew) - [Intellij静态代码扫描插件SpotBugs](https://mp.weixin.qq.com/s/8ivsMNOmT0LDfvcM06IGMg) - [SpotBugs注解SuppressWarnings在Java&Groovy中的应用](https://mp.weixin.qq.com/s/R0JoqmAqhUbRSjIJ61h_tg) ## 性能测试 - [Linux性能监控软件netdata中文汉化版](https://mp.weixin.qq.com/s/7VG7gHx7FUvsuNtBTJpjWA) - [性能测试框架](https://mp.weixin.qq.com/s/3_09j7-5ex35u30HQRyWug) - [性能测试框架第二版](https://mp.weixin.qq.com/s/JPyGQ2DRC6EVBmZkxAoVWA) - [性能测试框架第三版](https://mp.weixin.qq.com/s/Mk3PoH7oJX7baFmbeLtl_w) - [一个时间计数器timewatch辅助性能测试](https://mp.weixin.qq.com/s/-YZ04n2kyfO0q2QaKHX_0Q) - [如何在Linux命令行界面愉快进行性能测试](https://mp.weixin.qq.com/s/fwGqBe1SpA2V0lPfAOd04Q) - [Mac+httpclient高并发配置实例](https://mp.weixin.qq.com/s/r4a-vGz0pxeZBPPH3phujw) - [单点登录性能测试方案](https://mp.weixin.qq.com/s/sv8FnvIq44dFEq63LpOD2A) - [如何对消息队列做性能测试](https://mp.weixin.qq.com/s/MNt22aW3Op9VQ5OoMzPwBw) - [如何对修改密码接口进行压测](https://mp.weixin.qq.com/s/9CL_6-uZOlAh7oeo7NOpag) - [如何对单行多次update接口进行压测](https://mp.weixin.qq.com/s/Ly1Y4iPGgL6FNRsbOTv0sg) - [如何对多行单次update接口进行压测](https://mp.weixin.qq.com/s/Fsqw7vlw6K9EKa_XJwGIgQ) - [如何获取JVM堆转储文件](https://mp.weixin.qq.com/s/qCg7nsXVvT1q-9yquQOfWA) - [性能测试中标记每个请求](https://mp.weixin.qq.com/s/PokvzoLdVf_y9inlVXHJHQ) - [如何对N个接口按比例压测](https://mp.weixin.qq.com/s/GZxbH4GjDkk4BLqnUj1_kw) - [如何性能测试中进行业务验证](https://mp.weixin.qq.com/s/OEvRy1bS2Yq_w1kGiidmng) - [性能测试中记录每一个耗时请求](https://mp.weixin.qq.com/s/VXcp4uIMm8mRgqe8fVhuCQ) - [线程安全类在性能测试中应用](https://mp.weixin.qq.com/s/0-Y63wXqIugVC8RiKldHvg) - [利用微基准测试修正压测结果](https://mp.weixin.qq.com/s/dmO33qhOBrTByw_NshS-uA) - [性能测试如何减少本机误差](https://mp.weixin.qq.com/s/S6b_wwSowVolp1Uu6sEIOA) - [服务端性能优化之异步查询转同步](https://mp.weixin.qq.com/s/okYP2aOPfkWj2FjZcAtQNA) - [服务端性能优化之双重检查锁](https://mp.weixin.qq.com/s/-bOyHBcqFlJY3c0PEZaWgQ) - [多种登录方式定量性能测试方案](https://mp.weixin.qq.com/s/WuZ2h2rr0rNBgEvQVioacA) - [性能测试中图形化输出测试数据](https://mp.weixin.qq.com/s/EMvpYIsszdwBJFPIxztTvA) - [压测中测量异步写入接口的延迟](https://mp.weixin.qq.com/s/odvK1iYgg4eRVtOOPbq15w) - [手机号验证码登录性能测试](https://mp.weixin.qq.com/s/i-j8fJAdcsJ7v8XPOnPDAw) - [绑定手机号性能测试](https://mp.weixin.qq.com/s/K5x1t1dKtIT2NKV6k4v5mw) - [终止性能测试并输出报告](https://mp.weixin.qq.com/s/II4-UbKDikctmS_vRT-xLg) - [CountDownLatch类在性能测试中应用](https://mp.weixin.qq.com/s/uYBPPOjauR2h81l2uKMANQ) - [CyclicBarrier类在性能测试中应用](https://mp.weixin.qq.com/s/kvEHX3t_2xpMke9vwOdWrg) - [Phaser类在性能测试中应用](https://mp.weixin.qq.com/s/plxNnQq7yNQvHYEGpyY4uA) - [如何同时压测创建和删除接口](https://mp.weixin.qq.com/s/NCeoEF3DkEtpdaaQ365I0Q) - [固定QPS压测模式探索](https://mp.weixin.qq.com/s/S2h-zEUoik_CWs60RL6g7Q) - [固定QPS压测初试](https://mp.weixin.qq.com/s/ySlJmDIH3fFB4qEnL-ueMg) - [命令行如何执行jar包里面的方法](https://mp.weixin.qq.com/s/50oMEmVEnv5Vzlm1HOxuFw) - [链路压测中如何记录每一个耗时的请求](https://mp.weixin.qq.com/s/8sb5QZcKbBjTxCaXK5ajXA) - [Socket接口异步验证实践](https://mp.weixin.qq.com/s/bnjHK3ZmEzHm3y-xaSVkTw) - [性能测试中集合点和多阶段同步问题初探](https://mp.weixin.qq.com/s/NlpD1WyMrcG1V5RYfY0Plg) - [性能测试中标记请求参数实践](https://mp.weixin.qq.com/s/2FNMU-k_En26FCqWkYpvhQ) - [测试模型中理解压力测试和负载测试](https://mp.weixin.qq.com/s/smNLx3malzM3avkrn3EJiA) - [重放浏览器单个请求性能测试实践](https://mp.weixin.qq.com/s/a10hxCrIzS4TV9JwmDSI3Q) - [重放浏览器多个请求性能测试实践](https://mp.weixin.qq.com/s/Hm1Kpp1PMrZ5rYFW8l2GlA) - [重放浏览器请求多链路性能测试实践](https://mp.weixin.qq.com/s/9YSBLAyHVw8Z6IfK-nJTpQ) - [性能测试中异步展示测试进度](https://mp.weixin.qq.com/s/AOERJbEc4ATJqhjvnxgQoA) - [ThreadLocal在链路性能测试中实践](https://mp.weixin.qq.com/s/3qhNdHHSStELzNraQSpcew) - [Socket接口固定QPS性能测试实践](https://mp.weixin.qq.com/s/I9-14L8THxvtX1NJY0KPfw) - [单链路性能测试实践](https://mp.weixin.qq.com/s/4xHLP-GZwrNu5cFKdfsB6g) - [链路性能测试中参数多样性方法分享](https://mp.weixin.qq.com/s/I1pm0fulNrj_S-YkNz-gEA) - [链路测试中参数流转图](https://mp.weixin.qq.com/s/xyo9HXBLoXgLW6MSFH3V6w) - [线程同步类CyclicBarrier在性能测试集合点应用](https://mp.weixin.qq.com/s/K2YySxX9T4v_rzbvIbIHJA) - [链路压测中各接口性能统计](https://mp.weixin.qq.com/s/Deyop0mMpHrRWj9JTHzayw) - [性能测试框架中QPS取样器实现](https://mp.weixin.qq.com/s/4-5WhwwE1oRQ7cMUDv7J2w) - [链路压测中的支路问题初探](https://mp.weixin.qq.com/s/9iN9XndRPH4vIgc0-jVpUA) > **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。 * `Gitee`地址*https://gitee.com/fanapi/tester* * `GitHub`地址*https://github.com/JunManYuanLong/FunTester* # 语言合集 ## Java - [java一行代码打印心形](https://mp.weixin.qq.com/s/QPSryoSbViVURpSa9QXtpg) - [操作的原子性与线程安全](https://mp.weixin.qq.com/s/QU3llkGLepX2VCch8Y9GKw) - [快看,i++真的不安全](https://mp.weixin.qq.com/s/-CdWdROKSEq_ZiLX2kWxzA) - [原子操作组合与线程安全](https://mp.weixin.qq.com/s/XB5LXucAF5Bo8EkfLZYRmw) - [java利用for循环输出正三角新解](https://mp.weixin.qq.com/s/nnMR2177LLVn4u_9s9Fl4g) - [在main方法之前,到底执行了什么?](https://mp.weixin.qq.com/s/jWxiCMfwmvRHrjPdRG8ZyQ) - [传参传的到底是什么?](https://mp.weixin.qq.com/s/p_pEQwE6h6q7PprkW-kjbg) - [json里面put了null会怎么样?](https://mp.weixin.qq.com/s/gQVROe01I3JzIqNdTSHpDQ) - [主线程都结束了,为何进程还在执行](https://mp.weixin.qq.com/s/q2v5JU5dtmNEol7I7IVY-Q) - [java测试框架如何执行groovy脚本文件](https://mp.weixin.qq.com/s/0GYt1l3_z5-1qzBNl6_PzA) - [java用递归筛选法求N以内的孪生质数(孪生素数)](https://mp.weixin.qq.com/s/PSdCb-DrgMPb4WpJJMexmQ) - [从JVM堆内存分析验证深浅拷贝](https://mp.weixin.qq.com/s/SdYDnoau1rjjvPC2SUymBg) - [如何学习Java基础](https://mp.weixin.qq.com/s/FCPStkYoJF67NYln4Lc6xg) - [如何保存HTTPrequestbase和CloseableHttpResponse](https://mp.weixin.qq.com/s/gRY8HRQHCh0PfyS7Q22IwA) - [如何在匿名thread子类中保证线程安全](https://mp.weixin.qq.com/s/GCXx_-ummi0JfZQ7GTIxig) - [Java服务端两个常见的并发错误](https://mp.weixin.qq.com/s/5VvCox3eY6sQDsuaKB4ZIw) - [Java中interface属性和实例方法](https://mp.weixin.qq.com/s/vrKkM6522tgw3v_cL7R8HA) - [服务端性能优化之双重检查锁](https://mp.weixin.qq.com/s/-bOyHBcqFlJY3c0PEZaWgQ) - [Java并发BUG基础篇](https://mp.weixin.qq.com/s/NR4vYx81HtgAEqH2Q93k2Q) - [Java并发BUG提升篇](https://mp.weixin.qq.com/s/GCRRe8hJpe1QJtxq9VBEhg) - [性能测试中图形化输出测试数据](https://mp.weixin.qq.com/s/EMvpYIsszdwBJFPIxztTvA) - [超大对象导致Full GC超高的BUG分享](https://mp.weixin.qq.com/s/L15-0JW9WK-E005GeOG9WQ) - [利用ThreadLocal解决线程同步问题](https://mp.weixin.qq.com/s/VEm8jt3ZUEUdyyeXPC8VvQ) - [线程安全集合类中的对象是安全的么?](https://mp.weixin.qq.com/s/WKSuPEfzZCVwjVTcoD0Dyg) - [如何使用“dd MM”解析日期](https://mp.weixin.qq.com/s/v9ooAj3dKu53JXgxB482HA) - [Java和Groovy正则使用](https://mp.weixin.qq.com/s/DT3BKE3ZcCKf6TLzGc5wbg) - [运行越来越快的Java热点代码](https://mp.weixin.qq.com/s/AP0BcDEjDuaouaB0RXJOoQ) - [6个重要的JVM性能参数](https://mp.weixin.qq.com/s/b1QnapiAVn0HD5DQU9JrIw) - [ArrayList浅、深拷贝](https://mp.weixin.qq.com/s/kYsBzFsCyDPUssdV3MDqLA) - [Java性能测试中两种锁的实现](https://mp.weixin.qq.com/s/j9dGFvYzCJ0AGwYUtTrTsw) - [测试如何处理Java异常](https://mp.weixin.qq.com/s/H00GWiATOD8QHJu3UewrBw) - [创建Java守护线程](https://mp.weixin.qq.com/s/_UjWdvq8QWYTshr4SeniBg) - [Lambda表达式在线程安全Map中应用](https://mp.weixin.qq.com/s/zZjB5aOWh4a_k1eoEsR5ww) - [Java程序是如何浪费内存的](https://mp.weixin.qq.com/s/w7VF5m5cc0X7LNvqmwGfvg) - [Java中的自定义异常](https://mp.weixin.qq.com/s/nspIdxFP9qEDtagGN4gaMQ) - [Java文本块](https://mp.weixin.qq.com/s/GwasvpJsd7uLngvCr6KlQw) - [CountDownLatch类在性能测试中应用](https://mp.weixin.qq.com/s/uYBPPOjauR2h81l2uKMANQ) - [CyclicBarrier类在性能测试中应用](https://mp.weixin.qq.com/s/kvEHX3t_2xpMke9vwOdWrg) - [Phaser类在性能测试中应用](https://mp.weixin.qq.com/s/plxNnQq7yNQvHYEGpyY4uA) - [Java压缩/解压缩字符串](https://mp.weixin.qq.com/s/7vHNd5dEN93DPUqgS8od_A) - [Java删除空字符:Java8 & Java11](https://mp.weixin.qq.com/s/6dlgYgTFZsHuJ4Eaby5eyg) - [Java Stream中map和flatMap方法](https://mp.weixin.qq.com/s/0FG2o7VUAG6z8a_0je-1EQ) - [泛型类的正确用法](https://mp.weixin.qq.com/s/1azilraonPIZNCnw_9MB5Q) - [Java字符串到数组的转换--最后放大招](https://mp.weixin.qq.com/s/iMUYZYkJ5CjykwWqinNm5g) - [Java求数组的并集--最后放大招](https://mp.weixin.qq.com/s/bZ93SGakyiRbaRujhx4nvw) - [Java计算数组平均值--最后放大招](https://mp.weixin.qq.com/s/dxQaFHu2PyAbOK6jpEgEUQ) - [Math.abs()求绝对值返回负值BUG分享](https://mp.weixin.qq.com/s/RHzExuRqF1XsBtzGKzmgGA) - [Java代理模式初探](https://mp.weixin.qq.com/s/SBL_K2PQez3vDHhtAN9NLg) - [Socket接口异步验证实践](https://mp.weixin.qq.com/s/bnjHK3ZmEzHm3y-xaSVkTw) - [性能测试中异步展示测试进度](https://mp.weixin.qq.com/s/AOERJbEc4ATJqhjvnxgQoA) - [Java中的ThreadLocal功能演示](https://mp.weixin.qq.com/s/n92k1JswHKrqT7Y_CD9Q0w) - [ThreadLocal在链路性能测试中实践](https://mp.weixin.qq.com/s/3qhNdHHSStELzNraQSpcew) - [歪解字符串中连续出现次数最多问题](https://mp.weixin.qq.com/s/xBy4iB4qLd4WQgCsVVuemw) - [Java&Groovy下载文件对比](https://mp.weixin.qq.com/s/T9WUynej2yOZhCkDUhaLYw) - [线程同步类CyclicBarrier在性能测试集合点应用](https://mp.weixin.qq.com/s/K2YySxX9T4v_rzbvIbIHJA) - [Java线程同步三剑客](https://mp.weixin.qq.com/s/cAmd11-HdwXNU3tp4TiDLg) ## Groovy - [java和groovy混合编程时提示找不到符合错误解决办法](https://mp.weixin.qq.com/s/dLC2W7nIi5zCuK6JTkiA-w) - [groovy使用stream语法递归筛选法求N以内的质数](https://mp.weixin.qq.com/s/TsrVn1cuQUrU6wj9OnR-FQ) - [使用Groovy进行Bash(shell)操作](https://mp.weixin.qq.com/s/fgCTlZUF3QeNj6jzq1ZgGg) - [使用Groovy和Gradle轻松进行数据库操作](https://mp.weixin.qq.com/s/lwmclrnW0csykVRhu7dNTQ) - [愉快地使用Groovy Shell](https://mp.weixin.qq.com/s/fJh7fbB3naBFBEiaS62oxw) - [Gradle+Groovy基础篇](https://mp.weixin.qq.com/s/c2j7G-PoNtAB3oYYDUhCGw) - [Gradle+Groovy提高篇](https://mp.weixin.qq.com/s/yXmYj_1fynLkR0-5FV_Arw) - [Groovy重载操作符](https://mp.weixin.qq.com/s/4jW06Q4_vjFR9DovRTTuHg) - [用Groovy处理JMeter断言和日志](https://mp.weixin.qq.com/s/Q4yPA4p8dZYAARZ60ZDh9w) - [用Groovy处理JMeter变量](https://mp.weixin.qq.com/s/BxtweLrBUptM8r3LxmeM_Q) - [用Groovy在JMeter中执行命令行](https://mp.weixin.qq.com/s/VTip7tiLpwBOr1gUoZ0n8A) - [用Groovy处理JMeter中的请求参数](https://mp.weixin.qq.com/s/9pCUOXWpMwXR5ynvCMYJ7A) - [Java和Groovy正则使用](https://mp.weixin.qq.com/s/DT3BKE3ZcCKf6TLzGc5wbg) - [Groovy中的元组](https://mp.weixin.qq.com/s/0-ka0-tv1vyKbiA6m44jRw) - [从Java到Groovy的八级进化论](https://mp.weixin.qq.com/s/QTrRHsD3w-zLGbn79y8yUg) - [用Groovy在JMeter中使用正则提取赋值](https://mp.weixin.qq.com/s/9riPpnQZCfKGscuzOOpYmQ) - [Groovy在JMeter中处理cookie](https://mp.weixin.qq.com/s/DCnDjWaj2aiKv5HVw3-n6A) - [Groovy在JMeter中处理header](https://mp.weixin.qq.com/s/juY-1jEWODJ5HHiEsxhIEw) - [Groovy的神奇NullObject](https://mp.weixin.qq.com/s/jLGisN_30PrCgNP33Sww0g) - [Groovy中的list](https://mp.weixin.qq.com/s/0mUe1_WrUiEm1t6kqCV3eQ) - [JMeter参数签名——Groovy脚本形式](https://mp.weixin.qq.com/s/wQN9-xAUQofSqiAVFXdqug) - [Groovy中的闭包](https://mp.weixin.qq.com/s/pfcG47gSPfUveAaEfdeo8A) - [JMeter参数签名——Groovy工具类形式](https://mp.weixin.qq.com/s/urwU4p9ofv9sU-JFy5Z0iA) - [删除List中null的N种方法--最后放大招](https://mp.weixin.qq.com/s/4mfskN781dybyL59dbSbeQ) - [混合Java函数和Groovy闭包](https://mp.weixin.qq.com/s/FAIzGgLSX2u7RKbOGs3lGA) - [Groovy重载操作符(终极版)](https://mp.weixin.qq.com/s/4oYGJ2B2Y1AqxsIj8v5nZA) - [JsonPath工具类单元测试](https://mp.weixin.qq.com/s/1YtUWGk_sTjn9bHwAeT0Ew) - [Groovy小记it关键字和IDE报错](https://mp.weixin.qq.com/s/cIMHzkvKtH0a0ewkiBnV8g) - [JsonPath验证类既Groovy重载操作符实践](https://mp.weixin.qq.com/s/5gc04CAsBY6pWxe5c2P41w) - [Groovy枚举类初始化异常分析](https://mp.weixin.qq.com/s/koFhpBZM1MFYYxCNxUKPyQ) - [Java&Groovy下载文件对比](https://mp.weixin.qq.com/s/T9WUynej2yOZhCkDUhaLYw) ## Python - [python使用filter方法递归筛选法求N以内的质数(素数)--附一行打印心形标记的代码解析](https://mp.weixin.qq.com/s/D8RfpdIi8smCL8TAzBcNpA) - [关于python版微信使用经验分享](https://mp.weixin.qq.com/s/19IaI6ETZAm_T4ePPlXqIg) - [python用递归筛选法求N以内的孪生质数(孪生素数)](https://mp.weixin.qq.com/s/rVY2pTl8So11WCvA9GrFbA) - [利用python wxpy和requests写一个自动应答微信机器人实例](https://mp.weixin.qq.com/s/Fni2kX5BRjdqOQ-glCLjRg) - [Python版Socket.IO接口测试脚本](https://mp.weixin.qq.com/s/oXBP6Sx3yPqlmvV9uCUScw) ## 测开笔记 - [我的开发日记(一)](https://mp.weixin.qq.com/s/eQgpOKbXsU9vOmxp0Xiklg) - [我的开发日记(二)](https://mp.weixin.qq.com/s/XuffL3ZmKKOgHDtH_cEYOw) - [我的开发日记(三)](https://mp.weixin.qq.com/s/a-I0agh6nWp8RLlcmbgf5w) - [我的开发日记(四)](https://mp.weixin.qq.com/s/QukXd00Mx_dbkgiXys0FNg) - [我的开发日记(五)](https://mp.weixin.qq.com/s/6P3nScsVW6MfMcyIqcA1AQ) - [我的开发日记(六)](https://mp.weixin.qq.com/s/Gz2QmukONNldSy9Fd29u5w) - [我的开发日记(七)](https://mp.weixin.qq.com/s/MjZ-nFXfQkHMsXS0fX1c1w) - [我的开发日记(八)](https://mp.weixin.qq.com/s/6ZhNcFm-gR5dhKQjEkE3Rg) - [我的开发日记(九)](https://mp.weixin.qq.com/s/VfD2T3orojGxnylr3Q5UeA) - [我的开发日记(十)](https://mp.weixin.qq.com/s/6DWth40LGbAraJi05G16Pw) - [我的开发日记(十一)](https://mp.weixin.qq.com/s/nsX5A-P6QbePHDN_Pse0_A) - [我的开发日记(十二)](https://mp.weixin.qq.com/s/XA1KJXBP3Zl-XFswXxUtvg) - [我的开发日记(十三)](https://mp.weixin.qq.com/s/_QPUu5pUlg4A_AlC5wOGkA) - [我的开发日记(十四)](https://mp.weixin.qq.com/s/Qy1YKAb3wqW_Ip2FwH7Otw) - [我的开发日记(十五)](https://mp.weixin.qq.com/s/bwkvz2t6YItQD0O_BIxpHQ) - [这些年,我写过的BUG(一)](https://mp.weixin.qq.com/s/mVTmT1FdwWl1e0BaL7Ne1g) - [这些年,我写过的BUG(二)](https://mp.weixin.qq.com/s/NMz5n0ZMf6taGb-gr1BLyw) - [FunTester测试框架架构图初探](https://mp.weixin.qq.com/s/bcMbVDkWbHSXjZFDeFyJsQ) - [FunTester测试项目架构图初探](https://mp.weixin.qq.com/s/wqb8FXRbEXrhDuZounmNXA) > **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。 * `Gitee`地址*https://gitee.com/fanapi/tester* * `GitHub`地址*https://github.com/JunManYuanLong/FunTester* # 案例分享 ## 测试方案 - [如何对消息队列做性能测试](https://mp.weixin.qq.com/s/MNt22aW3Op9VQ5OoMzPwBw) - [如何对修改密码接口进行压测](https://mp.weixin.qq.com/s/9CL_6-uZOlAh7oeo7NOpag) - [如何测试概率型业务接口](https://mp.weixin.qq.com/s/kUVffhjae3eYivrGqo6ZMg) - [如何测试非固定型概率算法P=p(1+0.1*N)](https://mp.weixin.qq.com/s/sgg8v-Bi-_sUDJXwuTCMGg) - [性能测试中标记每个请求](https://mp.weixin.qq.com/s/PokvzoLdVf_y9inlVXHJHQ) - [如何对N个接口按比例压测](https://mp.weixin.qq.com/s/GZxbH4GjDkk4BLqnUj1_kw) - [多种登录方式定量性能测试方案](https://mp.weixin.qq.com/s/WuZ2h2rr0rNBgEvQVioacA) - [压测中测量异步写入接口的延迟](https://mp.weixin.qq.com/s/odvK1iYgg4eRVtOOPbq15w) - [绑定手机号性能测试](https://mp.weixin.qq.com/s/K5x1t1dKtIT2NKV6k4v5mw) - [手机号验证码登录性能测试](https://mp.weixin.qq.com/s/i-j8fJAdcsJ7v8XPOnPDAw) - [重放浏览器单个请求性能测试实践](https://mp.weixin.qq.com/s/a10hxCrIzS4TV9JwmDSI3Q) - [重放浏览器多个请求性能测试实践](https://mp.weixin.qq.com/s/Hm1Kpp1PMrZ5rYFW8l2GlA) - [重放浏览器请求多链路性能测试实践](https://mp.weixin.qq.com/s/9YSBLAyHVw8Z6IfK-nJTpQ) - [Socket接口固定QPS性能测试实践](https://mp.weixin.qq.com/s/I9-14L8THxvtX1NJY0KPfw) ## BUG集锦 - [一个MySQL索引引发的血案](https://mp.weixin.qq.com/s/KLSber-gPg53JVfsCa3Dtw) - [微软Zune闰年BUG分析](https://mp.weixin.qq.com/s/zpqAUcNcHaZjWUdUYH_loQ) - [“双花”BUG的测试分享](https://mp.weixin.qq.com/s/0dsBsssNfg-seJ_tu9zFaQ) - [iOS 11计算器1+2+3=24真的是bug么?](https://mp.weixin.qq.com/s/nokQhe_Hqcq-o7pZJmFlqQ) - [不要在遍历的时候删除](https://mp.weixin.qq.com/s/MIczbEpbOrADL0_V7ZUhlg) - [连开100年会员会怎样](https://mp.weixin.qq.com/s/mZw-SFIxFFbE-o8UeXhdfg) - [异步查询转同步加redis业务实现的BUG分享](https://mp.weixin.qq.com/s/ni3f6QTxw0K-0I3epvEYOA) - [Java服务端两个常见的并发错误](https://mp.weixin.qq.com/s/5VvCox3eY6sQDsuaKB4ZIw) - [超大对象导致Full GC超高的BUG分享](https://mp.weixin.qq.com/s/L15-0JW9WK-E005GeOG9WQ) - [访问权限导致toString返回空BUG分享](https://mp.weixin.qq.com/s/usDOcuJrXOmEKN-mVBzRKg) - [异常使用中的BUG](https://mp.weixin.qq.com/s/IG9Ar3IT7CrlSv4d0lCvgA) - [Math.abs()求绝对值返回负值BUG分享](https://mp.weixin.qq.com/s/RHzExuRqF1XsBtzGKzmgGA) ## 爬虫实践 - [接口爬虫之网页表单数据提取](https://mp.weixin.qq.com/s/imJ5u67xhYQaEzv-O1in4g) - [httpclient爬虫爬取汉字拼音等信息](https://mp.weixin.qq.com/s/w-IvBxAsotmPA3pydpIo1w) - [httpclient爬虫爬取电影信息和下载地址实例](https://mp.weixin.qq.com/s/TB49X4S-ioyoW5CrzAnHcw) - [httpclient 多线程爬虫实例](https://mp.weixin.qq.com/s/nXL-MP4Y6CN2hgZQefWEeQ) - [groovy爬虫练习之——企业信息](https://mp.weixin.qq.com/s/1TisDceIL1-Luqz_wOqAiw) - [httpclient 爬虫实例——爬取三级中学名](https://mp.weixin.qq.com/s/Dd7U30aHYauqBFxJdxaiyg) - [电子书网站爬虫实践](https://mp.weixin.qq.com/s/KGW0dIS5NTLgxyhSjxDiOw) - [groovy爬虫实例——历史上的今天](https://mp.weixin.qq.com/s/5LDUvpU6t_GZ09uhZr224A) - [爬取720万条城市历史天气数据](https://mp.weixin.qq.com/s/vOyKpeGlJSJp9bQ8NIMe2A) - [记一次失败的爬虫](https://mp.weixin.qq.com/s/SMylrZLXDGw5f1xKI9ObnA) - [爬虫实践--CBA历年比赛数据](https://mp.weixin.qq.com/s/mM_QSQddabU5im_O6iVR-Q) - [图片爬虫实践](https://mp.weixin.qq.com/s/u5bRSyKsmn3TcjqEEqRJpw) # 工具合集 ## JSON合集 - [JsonPath实践(一)](https://mp.weixin.qq.com/s/Cq0_v_ptbGd4f5y8HIsq7w) - [JsonPath实践(二)](https://mp.weixin.qq.com/s/w_iJTiuQahIw6U00CJVJZg) - [JsonPath实践(三)](https://mp.weixin.qq.com/s/58A3k0T6dbOkBJ5nRYKDqA) - [JsonPath实践(四)](https://mp.weixin.qq.com/s/8ER61qrkMj8bdBpyuq9r6w) - [JsonPath实践(五)](https://mp.weixin.qq.com/s/knVLW960WXnckGLstdrOVQ) - [JsonPath实践(六)](https://mp.weixin.qq.com/s/ckBCK3t1w68FLBhaw5a7Jw) - [JsonPath工具类封装](https://mp.weixin.qq.com/s/KyuCuG5fVEExxBdGJO2LdA) - [JsonPath工具类单元测试](https://mp.weixin.qq.com/s/1YtUWGk_sTjn9bHwAeT0Ew) - [JsonPath验证类既Groovy重载操作符实践](https://mp.weixin.qq.com/s/5gc04CAsBY6pWxe5c2P41w) - [JSON对象标记语法验证类](https://mp.weixin.qq.com/s/jSXmoEdMF7nWAqQuzJ5GiQ) - [使用jq处理JSON数据(一)](https://mp.weixin.qq.com/s/45-ztTx2scbNY5u7NQzeIA) ## Jacoco覆盖率 - [接口测试代码覆盖率(jacoco)方案分享](https://mp.weixin.qq.com/s/D73Sq6NLjeRKN8aCpGLOjQ) - [jacoco无法读取build.xml配置中源码路径解决办法](https://mp.weixin.qq.com/s/8_x0rVfkIi-uX3y0drx_jw) - [使用JaCoCo Maven插件创建代码覆盖率报告](https://mp.weixin.qq.com/s/4Jo05k2WxytiSSNW9WTV-A) - [Java 8,Jenkins,Jacoco和Sonar进行持续集成](https://mp.weixin.qq.com/s/dOoXnKnWtQmmC5itClsl4g) - [jacoco测试覆盖率过滤非业务类](https://mp.weixin.qq.com/s/7YGe9pCHw3wd87tgOlKjSA) ## arthas诊断工具 - [arthas快速入门视频演示](https://mp.weixin.qq.com/s/Wl5QMD52isGTRuAP4Cpo-A) - [arthas进阶thread命令视频演示](https://mp.weixin.qq.com/s/XuF7Nr1sGC3diIn50zlDDQ) - [arthas命令jvm,sysprop,sysenv,vmoption视频演示](https://mp.weixin.qq.com/s/87BsTYqnTCnVdG3a_kBcng) - [arthas命令logger动态修改日志级别--视频演示](https://mp.weixin.qq.com/s/w724P9B12eTC9rMbavwsMA) - [arthas命令sc和sm视频演示](https://mp.weixin.qq.com/s/Ga63sjW_bOKQqfnA5LTb9w) - [arthas命令ognl视频演示](https://mp.weixin.qq.com/s/cMCaXFwjp6QHFq40TvP4bQ) - [arthas命令redefine实现Java热更新](https://mp.weixin.qq.com/s/2HUXfJhoUfg4yMzSoRHK9w) - [arthas命令monitor监控方法执行](https://mp.weixin.qq.com/s/7-oe3UoTY8bzpi89tIKvQQ) - [arthas命令watch观察方法调用(上)](https://mp.weixin.qq.com/s/6fMKP7H4Q7ll_0v-wyN19g) - [arthas命令watch观察方法调用(下)](https://mp.weixin.qq.com/s/-r2kufxdOjRb2TgF2HPskg) - [arthas命令trace追踪方法链路](https://mp.weixin.qq.com/s/bzkdKZugkOl8C-_xTw92YA) - [arthas命令tt方法时空隧道](https://mp.weixin.qq.com/s/mDczYmVdSmL5ZbK7bb8i0A) ## moco API - [解决moco框架API在post请求json参数情况下query失效的问题](https://mp.weixin.qq.com/s/V5lXoepEBtPJrSUHA0Uz5A) - [给moco API添加limit功能](https://mp.weixin.qq.com/s/pXJECi15ieNLmA0uIqEqfA) - [给moco API添加random功能](https://mp.weixin.qq.com/s/YTcbFbFaWB5arW_fubgTTQ) - [解决moco框架API在cycle方法缺失的问题](https://mp.weixin.qq.com/s/YfsPa7eW8WV65CDbPooBPg) - [五行代码构建静态博客](https://mp.weixin.qq.com/s/hZnimJOg5OqxRSDyFvuiiQ) - [moco API模拟框架视频讲解(上)](https://mp.weixin.qq.com/s/X5-fFXe018_O60WCRdawZg) - [moco API模拟框架视频讲解(中)](https://mp.weixin.qq.com/s/g2En-9W9JWYrCLQr_WPEBA) - [moco API模拟框架视频讲解(下)](https://mp.weixin.qq.com/s/mz__DiNxMGHwIKCLsjKR8g) - [如何mock固定QPS的接口](https://mp.weixin.qq.com/s/yogj9Fni0KJkyQuKuDYlbA) - [mock延迟响应的接口](https://mp.weixin.qq.com/s/x_fu0InQpYIUJIQFi9a50g) - [moco固定QPS接口升级补偿机制](https://mp.weixin.qq.com/s/zAM91e_REo4edSPTLuHLOw) ## 工具类 - [java网格输出的类](https://mp.weixin.qq.com/s/BJTJu0LGjn7Hc9J1yT04KQ) - [java使用poi写入excel文档的一种解决方案](https://mp.weixin.qq.com/s/Ft56gd1B9CPrQs2zq4Cpug) - [java使用poi读取excel文档的一种解决方案](https://mp.weixin.qq.com/s/ltZGx9J7E8DTer0D-pfQ2Q) - [MongoDB操作类封装](https://mp.weixin.qq.com/s/u-RHOE5XrjOEkelWIxdplw) - [java网格输出的类](https://mp.weixin.qq.com/s/QW8nKM2Bz7C75fdkCzSbpw) - [将json数据格式化输出到控制台](https://mp.weixin.qq.com/s/2IPwvh-33Ov2jBh0_L8shA) - [利用反射根据方法名执行方法的使用示例](https://mp.weixin.qq.com/s/5ntwDo4ZVcTh1PmK4vkNfA) - [解决统计出现次数问题的方法类](https://mp.weixin.qq.com/s/gqz4wuKkMWAOIQwMtiupnA) - [java利用时间戳来获取UTC时间](https://mp.weixin.qq.com/s/wbDIrwDnxb9_XWkkmP3A_g) - [如何遍历执行一个包里面每个类的用例方法](https://mp.weixin.qq.com/s/OJwCOHCJ4TalatsEWbtzIQ) - [阿拉伯数字转成汉字](https://mp.weixin.qq.com/s/jNZXIvwMpdxt7jIAlVBgHg) - [获取JVM转储文件的Java工具类](https://mp.weixin.qq.com/s/f_TlOb3m8MeR3argBmTzzA) - [基于DOM的XML文件解析类](https://mp.weixin.qq.com/s/scRj7OAhvJYL3mx_hCFp4A) - [XML文件解析实践(DOM解析)](https://mp.weixin.qq.com/s/V2DG3osaPNUJzFNDQgqM-w) - [基于DOM4J的XML文件解析类](https://mp.weixin.qq.com/s/K5R7iMXouTn4g0p14T7iAQ) - [将HTTP请求对象转成curl命令行](https://mp.weixin.qq.com/s/861uMAMMWtINjy4Z99WA6w) ## 构建工具 - [java和groovy混编的Maven项目如何用intellij打包执行jar包](https://mp.weixin.qq.com/s/bKexZXlONeo3r6FDhfMltQ) - [window系统权限不足导致gradle构建失败的解决办法](https://mp.weixin.qq.com/s/dqiQvmVG1o6glU-pknLDwQ) - [使用groovy脚本使gradle灵活加载本地jar包的两种方式](https://mp.weixin.qq.com/s/p3K3ZS7iOUeKO7E94gKFVg) - [Java 8,Jenkins,Jacoco和Sonar进行持续集成](https://mp.weixin.qq.com/s/dOoXnKnWtQmmC5itClsl4g) - [Gradle如何在任务失败后继续构建](https://mp.weixin.qq.com/s/GcXDzRN7cM_QQpt9ytqoKg) - [Gradle+Groovy基础篇](https://mp.weixin.qq.com/s/c2j7G-PoNtAB3oYYDUhCGw) - [Gradle+Groovy提高篇](https://mp.weixin.qq.com/s/yXmYj_1fynLkR0-5FV_Arw) - [Maven进行增量构建](https://mp.weixin.qq.com/s/ThQ7j6TS93KJZFqlNx8IQg) - [SonarQube8.3中的Maven项目的测试覆盖率报告](https://mp.weixin.qq.com/s/Xhp26jyE1c7Auielz48Llw) ## plotly可视化 - [MacOS使用pip安装pandas提示Cannot uninstall 'numpy'解决方案](https://mp.weixin.qq.com/s/fIqMAMXRQvf_vBtS5jDsyg) - [Python使用plotly生成本地文件教程](https://mp.weixin.qq.com/s/4dJdIP-g3fF40vX7S31jNg) - [Python2.7使用plotly绘制本地散点图和折线图实例](https://mp.weixin.qq.com/s/9QWrA0c-STmrmjSkBYWvbQ) - [Python可视化工具plotly从数据库读取数据作图示例](https://mp.weixin.qq.com/s/EUtPidiz_r1rpQBH_kudbA) - [利用Python+plotly制作接口请求时间的violin图表](https://mp.weixin.qq.com/s/3GdiLaiVRfkxwM3MOG-U8w) - [Python+plotly生成本地饼状图实例](https://mp.weixin.qq.com/s/61Qz9Kz-4ruzC0OvIuElpA) - [python plotly处理接口性能测试数据方法封装](https://mp.weixin.qq.com/s/NxVdvYlD7PheNCv8AMYqhg) - [利用python+plotly 制作接口响应时间Distplot图表](https://mp.weixin.qq.com/s/yrcUW1fFC18newqHcxhVvw) - [利用 python+plotly 制作Contour Plots模拟双波源干涉现象](https://mp.weixin.qq.com/s/vNW80BDeHsyjNQrnaBGk3Q) - [利用 python+plotly 制作双波源干涉三维图像](https://mp.weixin.qq.com/s/KSeV8VvQXRIg-bnzYoa5qg) - [python plotly制作接口响应耗时的时间序列表(Time Series )](https://mp.weixin.qq.com/s/U8chcVzCjGTdT3T_X5v4kw) - [python使用plotly批量生成图表](https://mp.weixin.qq.com/s/l18WfWz-s6qQ1JKKuh_2AQ) > **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。 * `Gitee`地址*https://gitee.com/fanapi/tester* * `GitHub`地址*https://github.com/JunManYuanLong/FunTester* # 无代码合集 ## 理论鸡汤 - [写给所有人的编程思维](https://mp.weixin.qq.com/s/Oj33UCnYfbUgzsBzEm2GPQ) - [成为杰出Java开发人员的10个步骤](https://mp.weixin.qq.com/s/UCNOTSzzvTXwiUX6xpVlyA) - [测试之《代码不朽》脑图](https://mp.weixin.qq.com/s/2aGLK3knUiiSoex-kmi0GA) - [为什么选择软件测试作为职业道路?](https://mp.weixin.qq.com/s/o83wYvFUvy17kBPLDO609A) - [自动化测试的障碍](https://mp.weixin.qq.com/s/ZIV7uJp7DzVoKhWOh6lvRg) - [自动化测试的问题所在](https://mp.weixin.qq.com/s/BhvD7BnkBU8hDBsGUWok6g) - [成为优秀自动化测试工程师的7个步骤](https://mp.weixin.qq.com/s/wdw1l4AZnPpdPBZZueCcnw) - [优秀软件开发人员的态度](https://mp.weixin.qq.com/s/0uEEeFaR27aTlyp-sm61bA) - [如何正确执行功能API测试](https://mp.weixin.qq.com/s/aeGx5O_jK_iTD9KUtylWmA) - [未来10年软件测试的新趋势-上](https://mp.weixin.qq.com/s/9XgpIfXQRuKg1Pap-tfqYQ) - [未来10年软件测试的新趋势-下](https://mp.weixin.qq.com/s/k2rZaeHoq4AX19CUzjGRVQ) - [自动化测试解决了什么问题](https://mp.weixin.qq.com/s/96k2I_OBHayliYGs2xo6OA) - [17种软件测试人员常用的高效技能-上](https://mp.weixin.qq.com/s/vrM_LxQMgTSdJxaPnD_CqQ) - [17种软件测试人员常用的高效技能-下](https://mp.weixin.qq.com/s/uyWdVm74TYKb62eIRKL7nQ) - [手动测试存在的重要原因](https://mp.weixin.qq.com/s/mW5vryoJIkeskZLkBPFe0Q) - [编写测试用例的技巧](https://mp.weixin.qq.com/s/zZAh_XXXGOyhlm6ebzs06Q) - [成为自动化测试的7种技能](https://mp.weixin.qq.com/s/e-HAGMO0JLR7VBBWLvk0dQ) - [功能测试与非功能测试](https://mp.weixin.qq.com/s/oJ6PJs1zO0LOQSTRF6M6WA) - [自动化和手动测试,保持平衡!](https://mp.weixin.qq.com/s/mMr_4C98W_FOkks2i2TiCg) - [43种常见软件测试分类](https://mp.weixin.qq.com/s/GTMkcEm-xPtVF7_HxXGKDg) - [自动化测试生命周期](https://mp.weixin.qq.com/s/SH-vb2RagYQ3sfCY8QM5ew) - [代码审查如何保证软件质量](https://mp.weixin.qq.com/s/osRnG09KDqEojiV3kp2nrw) - [TDD测试驱动开发的基础](https://mp.weixin.qq.com/s/diW_2HSbWMEsn8G6uQriOg) - [如何在DevOps引入自动化测试](https://mp.weixin.qq.com/s/MclK3VvMN1dsiXXJO8g7ig) - [自动化的好处](https://mp.weixin.qq.com/s/7MpWQhtozaTrlUMo1oRSBg) - [Web端自动化测试失败原因汇总](https://mp.weixin.qq.com/s/qzFth-Q9e8MTms1M8L5TyA) - [测试人员如何成为变革的推动者](https://mp.weixin.qq.com/s/0nTZHBOuKG0rewKAeyIqwA) - [探索性测试为何如此重要?](https://mp.weixin.qq.com/s/nebHPfKbCO0f-G24qCh9wA) - [5种促进业务增长的软件测试策略](https://mp.weixin.qq.com/s/3mB_DQVD2AZLPs84SmsmuA) - [如何选择正确的自动化测试工具](https://mp.weixin.qq.com/s/_Ee78UW9CxRpV5MoTrfgCQ) - [如何从测试自动化中实现价值](https://mp.weixin.qq.com/s/dj-sJvGjvFMYANfhIVo8jw) - [您如何使用Selenium来计算自动化测试的投资回报率?](https://mp.weixin.qq.com/s/DVSEm0DhoAvYfTWIniabJg) - [如何在DevOps中实施连续测试](https://mp.weixin.qq.com/s/snPXkH6WEZ2kteYP_-c5_g) - [自动化如何选择用例](https://mp.weixin.qq.com/s/1hH5YIle4YQimJr4iGSWlA) - [成功实施自动化测试的优点](https://mp.weixin.qq.com/s/UENdSU-NPa5AOVC9ciiy0Q) - [测试人员常用借口](https://mp.weixin.qq.com/s/0k_Ciud2sOpRb5PPiVzECw) - [测试自动化的边缘DevTestOps](https://mp.weixin.qq.com/s/kCySRYdCS11CA-lF30AtQA) - [筛选自动化测试用例的技巧](https://mp.weixin.qq.com/s/SWNopZLwgpj9yYsVEHEspw) - [什么阻碍手动测试发挥价值](https://mp.weixin.qq.com/s/t0VAVyA3ywQsHzaqzSILOw) - [未来的QA测试工程师](https://mp.weixin.qq.com/s/ngL4sbEjZm7OFAyyWyQ3nQ) - [Web安全检查](https://mp.weixin.qq.com/s/SewUV3GMaNKD2P7g64ctYQ) - [关于可用性测试](https://mp.weixin.qq.com/s/aUIg40scOWzbRR89ojJWLg) - [如何实施DevOps](https://mp.weixin.qq.com/s/UPIL942eOKR1bY0mbC-42w) - [黑盒测试和白盒测试](https://mp.weixin.qq.com/s/5kvrYMWG0vFR3vj-aNY49g) - [测试用例中的细节](https://mp.weixin.qq.com/s/wvScTliPwuvH9ReIoDQGNQ) - [集成测试、单元测试、系统测试](https://mp.weixin.qq.com/s/LRkxMasRvmDYRVb0_aybtA) - [集成测试类型和最佳实践](https://mp.weixin.qq.com/s/sSubzrs3cikLV7rmRQaWEA) - [软件测试中质量优于数量](https://mp.weixin.qq.com/s/4FxtVFqialRz6R4680rPAw) - [DevOps工具](https://mp.weixin.qq.com/s/4r8FoxQyYZ5naowML5Cw-Q) - [2020年Tester自我提升](https://mp.weixin.qq.com/s/vuhUp85_6Sbg6ReAN3TTSQ) - [DevOps中的测试工程师](https://mp.weixin.qq.com/s/42Ile_T1BAIp7QHleI-c7w) - [敏捷团队的回归测试策略](https://mp.weixin.qq.com/s/Z7dzDdfp5_kxvzBVQ3rEDg) - [测试自动化与自动化测试:差异很重要](https://mp.weixin.qq.com/s/6HC1bKesOs4mZYb9nOCHjw) - [自动化新手要避免的坑(上)](https://mp.weixin.qq.com/s/MjcX40heTRhEgCFhInoqYQ) - [自动化新手要避免的坑(下)](https://mp.weixin.qq.com/s/azDUo1IO5JgkJIS9n1CMRg) - [如何成为全栈自动化工程师](https://mp.weixin.qq.com/s/j2rQ3COFhg939KLrgKr_bg) - [左移测试](https://mp.weixin.qq.com/s/8zXkWV4ils17hUqlXIpXSw) - [选择手动测试还是自动化测试?](https://mp.weixin.qq.com/s/4haRrfSIp5Plgm_GN98lRA) - [从单元测试标准中学习](https://mp.weixin.qq.com/s/x0TyMAdPBWYL7JSPAmoQsw) - [负载测试很重要](https://mp.weixin.qq.com/s/2q7kNQVJuNwB948ks463CA) - [白盒测试扫盲](https://mp.weixin.qq.com/s/s_FvGZTC42GEjaWzroz1eA) - [自动化测试项目为何失败](https://mp.weixin.qq.com/s/KFJXuLjjs1hii47C1BH8PA) - [简化测试用例](https://mp.weixin.qq.com/s/BhwfDqhN9yoa3Iul_Eu5TA) - [敏捷测试二三事](https://mp.weixin.qq.com/s/bKkGWJA3JhvdCjgg6-AVEQ) - [软件测试中的虚拟化](https://mp.weixin.qq.com/s/zHyJiNFgHIo2ZaPFXsxQMg) - [新词:QA-Ops](https://mp.weixin.qq.com/s/detcY6OVYmzOTUxfwN6CFQ) - [生产环境中进行自动化测试](https://mp.weixin.qq.com/s/JKEGRLOlgpINUxs-6mohzA) - [所谓UI测试](https://mp.weixin.qq.com/s/wDvUy_BhQZCSCqrlC2j1qA) - [预上线环境失败的原因](https://mp.weixin.qq.com/s/jva0Jb2OMarERmTn7Kh2Ng) - [自动化策略六步走](https://mp.weixin.qq.com/s/He69k8iCKhTKD1j-yV6M5g) - [合格的测试经理必备技能](https://mp.weixin.qq.com/s/gFIYksHMn_bHEwAhmgVzjg) - [质量保障的拓展实践](https://mp.weixin.qq.com/s/a3sd0dQnjk3TerOhfo-1ng) - [敏捷领导者常见误区](https://mp.weixin.qq.com/s/xdq3CZflRjvDBGDLK4tNFQ) - [功能自动化测试策略](https://mp.weixin.qq.com/s/qHmcblN4cD4JK6jT7oU4fQ) - [性能测试、压力测试和负载测试](https://mp.weixin.qq.com/s/g26lpd7d7EtpN7pkiqkkjg) - [如何维护自动化测试](https://mp.weixin.qq.com/s/4eh4AN_MiatMSkoCMtY3UA) - [负载测试最佳实践](https://mp.weixin.qq.com/s/hNj7UsCCvv9TdexAcNFUvg) - [有关UI测试计划](https://mp.weixin.qq.com/s/D0fMXwJF754a7Mr5ARY5tQ) - [软件测试外包](https://mp.weixin.qq.com/s/sYQfb2PiQptcT0o_lLpBqQ) - [避免PPT自动化的最佳实践](https://mp.weixin.qq.com/s/5YgYK4_YLZ1wDDhbwMTGlw) - [如何优化软件测试成本](https://mp.weixin.qq.com/s/_eXrzDyNDA6yCRR8nPmzGA) - [如何从手动测试转到自动化测试](https://mp.weixin.qq.com/s/EBDTX4AMnn2KTEjL88bOhQ) - [Selenium自动化测试技巧](https://mp.weixin.qq.com/s/EzrpFaBSVITO2Y2UvYvw0w) - [测试为何会错过Bug](https://mp.weixin.qq.com/s/UFHy8OwZjnMkB70roIS-zQ) - [测试用例设计——一切测试的基础](https://mp.weixin.qq.com/s/0_ubnlhp2jk-jxHxJ95E9g) - [移动应用测试:挑战,类型和最佳实践](https://mp.weixin.qq.com/s/kYxh6xki69evVDsXDxrYKQ) - [敏捷测试中面临的挑战](https://mp.weixin.qq.com/s/vmsW56r1J7jWXHSZdcwbPg) - [AI如何影响测试行业](https://mp.weixin.qq.com/s/d6c7u1-lAmsiIQz3UvcGKg) - [自动测试失败的5个原因](https://mp.weixin.qq.com/s/bTakAHIcx_WyJIo-tsbvvg) - [大促前必做的质量检查](https://mp.weixin.qq.com/s/iOku2wKnlr8pSZO0l9Q3Bw) - [测试开发工程师工作技巧](https://mp.weixin.qq.com/s/TvrUCisja5Zbq-NIwy_2fQ) - [敏捷回归测试](https://mp.weixin.qq.com/s/_bBQFggkZTTEqcb9R_68OA) - [制定质量管理计划指南](https://mp.weixin.qq.com/s/ztXYE8EtwlkUdxnk1cjKVg) - [质量管理计划的基本要素](https://mp.weixin.qq.com/s/v8lOioYn01S1F0ex4mmljA) - [质量保障的方法和实践](https://mp.weixin.qq.com/s/hU_YCaZB-0a09dOCAVgcpw) - [为什么测试覆盖率如此重要](https://mp.weixin.qq.com/s/0evyuiU2kdXDgMDnDKjORg) - [自动化测试框架](https://mp.weixin.qq.com/s/vu6p_rQd3vFKDYu8JDJ0Rg) - [敏捷中的端到端测试](https://mp.weixin.qq.com/s/cdi4xnEzDLpl9ncQguLuAQ) - [自动化测试灵魂三问:是什么、为什么和做什么](https://mp.weixin.qq.com/s/geOejJx79-jTwafG9aXwqA) - [基于代码的自动化和无代码自动化](https://mp.weixin.qq.com/s/8Dopihqs4XzpU-sN-I94kw) - [物联网测试](https://mp.weixin.qq.com/s/B_JI4DANxoOq4HurxZC65Q) - [功能测试知多少](https://mp.weixin.qq.com/s/vTxZLwlvlfIBv892Ji-oLQ) - [如何选择自动化测试工具](https://mp.weixin.qq.com/s/yJo-d9bAZDs1Lcp8j7ISRg) - [连续测试策略](https://mp.weixin.qq.com/s/0aD_0cUW83oPu3sl7sHNnQ) - [如何设置质量检查流程](https://mp.weixin.qq.com/s/PQeXxMZzzU15xSfY5wkVgA) - [编写干净的代码之变量篇](https://mp.weixin.qq.com/s/J9rGIe8a2xaLlNJq2nVmzw) - [高效Mobile DevOps步骤](https://mp.weixin.qq.com/s/-qc-d_zJ1C9H_Uvd8gJiBw) - [回归BUG](https://mp.weixin.qq.com/s/00j-acjPeKQ7uap62WpY3w) - [处理回归BUG最佳实践](https://mp.weixin.qq.com/s/R3O2NruPAA2gQf4-3R6aAQ) - [自动化测试实践清单](https://mp.weixin.qq.com/s/972WruGsYmkRroquBFoqMg) - [自动化测试类型](https://mp.weixin.qq.com/s/GRkN8ozZiWNu21Y3KbVOBA) - [无脚本测试](https://mp.weixin.qq.com/s/PVBxk4KEwCmWkB6mOXJFlw) - [自动化测试转型挑战及其解决方案](https://mp.weixin.qq.com/s/BixS6jRdF5N_nvmW3_OthQ) - [无数据驱动自动化测试](https://mp.weixin.qq.com/s/aCYRGxkzMogLbmACYo6ssw) - [为什么自动化测试在敏捷开发中很重要](https://mp.weixin.qq.com/s/AP0wUQZ09NvSqme8e09igQ) - [测试模型中理解压力测试和负载测试](https://mp.weixin.qq.com/s/smNLx3malzM3avkrn3EJiA) - [移动测试工程师职业](https://mp.weixin.qq.com/s/dhtR4TbQNu5fWpmJkXGivw) - [远程测试工作挑战](https://mp.weixin.qq.com/s/LK-GEN4OtuWVGDuG8psmOQ) - [自动化测试用例的原子性](https://mp.weixin.qq.com/s/jA5WMHwJcu88nHXWoMBAdQ) - [可测性经验分享](https://mp.weixin.qq.com/s/iRtUjESYS3sh3YTD-BWjdA) - [敏捷中的回归测试的优化【译】](https://mp.weixin.qq.com/s/nDiZZgA1PIiAUCG_xwA2rA) - [敏捷的主要优势【译】](https://mp.weixin.qq.com/s/zkI85TLI37XrPFaQ-pZYMA) - [2021年自动化测试流行趋势【译】](https://mp.weixin.qq.com/s/dIZxkNT6mjgRukLBy0AJ6Q) - [敏捷团队的自动化测试【译】](https://mp.weixin.qq.com/s/5BvzQvdssTyp8voC9J9www) ## 大咖风采 - [Tcloud 云测平台--集大成者](https://mp.weixin.qq.com/s/29sEO39_NyDiJr-kY5ufdw) - [Android App 测试工具及知识大集合](https://mp.weixin.qq.com/s/Xk9rCW8whXOTAQuCfhZqTg) - [Android App常规测试内容](https://mp.weixin.qq.com/s/tweeoS5wTqK3k7R2TVuDXA) - [JVM的对象和堆](https://mp.weixin.qq.com/s/iNDpTz3gBK3By_bvUnrWOA) # UI自动化 ## UI自动化 - [自动化测试中java多线程的使用实例](https://mp.weixin.qq.com/s/BNSLaIdcTPTNj1tKpGf6fw) - [自动化测试中递归函数的应用](https://mp.weixin.qq.com/s/86602zV9zYblhCRMiUlwdA) - [Appium 2.0速览](https://mp.weixin.qq.com/s/mHHSZKYZXQby8YiQBP57hA) ## UiAutomator - [android uiautomator一个画心形图案的方法--代码的浪漫](https://mp.weixin.qq.com/s/byfAKHxD2i83VHnuaNgIZA) - [android UiAutomator了解源码解决控件bonds\[0,0\]无法点击](https://mp.weixin.qq.com/s/nu2ftXNUSG2_kmZjyhEcVA) - [android UiAutomator在清除文本时遇到中文的解决办法](https://mp.weixin.qq.com/s/cNGNCoXsYBSk-MWTWLxF4g) - [android UiAutomator获取当前页面某类控件个数的方法](https://mp.weixin.qq.com/s/njb19Sq_Kg4SusAS_eEuug) - [android uiautomator自定义监听示例--一个弹出权限设置的监听](https://mp.weixin.qq.com/s/OKKZOf51yq6qY5D6PvN-gg) - [如何在Mac OS上使用UiAutomator快速调试类](https://mp.weixin.qq.com/s/jm9d_42jp_BSlv-IW0BpzQ) - [UiAutomator测试中如何恢复手机输入法](https://mp.weixin.qq.com/s/o4-zCgbdq6OsHRK9XT14QA) - [android UiAutomator基本api的二次封装](https://mp.weixin.qq.com/s/_3jGg3ZYoeyAkjZpy8gWfQ) - [android UiAutomator让运行失败的用例重新运行](https://mp.weixin.qq.com/s/tMOPbt1w9tRaKEuIZYKCyg) - [利用UiAutomator写一个首页刷新的稳定性测试脚本](https://mp.weixin.qq.com/s/au9hAScsqUdcrh_usu8Pfg) - [android UiAutomator长按实现控制按住控件时间的方法](https://mp.weixin.qq.com/s/lOvxAOMh6mmIh3CEV6Fduw) - [android UiAutomator自定义快速调试类](https://mp.weixin.qq.com/s/iP2dTOeVkFMzU3dQ06R9GA) - [利用UiAutomator写一个自动遍历渠道包关键功能的脚本](https://mp.weixin.qq.com/s/0vg2OlfTy0y4T6sWUG-olA) - [android UiAutomator如何根据颜色判断控件的状态](https://mp.weixin.qq.com/s/kldsD3OZ4mJZ5yYQXfXxLw) - [android UiAutomator控制多台手机同时运行用例的方法](https://mp.weixin.qq.com/s/z9vgpOQP0wQffmG4C_oBWg) - [android UiAutomator使用递归函数写一个让屏幕一闪一闪提醒的方法](https://mp.weixin.qq.com/s/AzXjePdmsgs6QsICZOdPyw) - [android UiAutomator获取视频播放进度的方法](https://mp.weixin.qq.com/s/ho070zX9rrLPmh8bZe_HgQ) ## Selenium - [selenium2java截图保存桌面](https://mp.weixin.qq.com/s/OUfwsIo635coGONRNccYlg) - [selenium2java调用JavaScript方法封装](https://mp.weixin.qq.com/s/t-Xs2Hr9TM2bjDiOqQX2mA) - [selenium2java利用mysq解决向浏览器插入cookies时token过期问题](https://mp.weixin.qq.com/s/oAAkDKUGytQjxJLFkod-AQ) - [selenium2java 遇到有三个窗口用例的处理办法](https://mp.weixin.qq.com/s/6AJBanVKYwlsNcvsu_25QQ) - [selenium2java通过第三方登录绕过知乎登陆验证码](https://mp.weixin.qq.com/s/A5uTtxlg4l4pru2z7v1cug) - [selenium2java使用select处理下拉框示例](https://mp.weixin.qq.com/s/FFor451WzuUzINeclGN-Ng) - [selenium2java爬虫示例](https://mp.weixin.qq.com/s/vSZzpzEqsCtASSx6iHqxVA) - [selenium2java写一个设置秒杀价的脚本](https://mp.weixin.qq.com/s/1ocIOYt3gdGIJrd9v2shhg) - [selenium2java基本方法二次封装](https://mp.weixin.qq.com/s/2GaXigt13wa6JgxJkcef5g) - [selenium2java一个弹框上传时间日期大杂烩测试用例](https://mp.weixin.qq.com/s/Z8ZeZ-zFy0q0a-e_epT1Kg) - [selenium2java造数据例子](https://mp.weixin.qq.com/s/ACO2O5f7Po4Qn242lopMBg) - [selenium2java让浏览器停止加载的方法](https://mp.weixin.qq.com/s/aBQdGYys3Bpyf6yigGOCIA) - [selenium2java写一个强制刷新页面的方法](https://mp.weixin.qq.com/s/VWW7cH5WSDmw_eCabUh9LQ) - [selenium2java通过接口获取并注入cookies](https://mp.weixin.qq.com/s/luLHWxPWSekuDMbnKsfJvg) - [Selenium编写自动化用例的8种技巧](https://mp.weixin.qq.com/s/8wRHc_krXNfWclNeOJDNPg) - [JUnit中用于Selenium测试的中实践](https://mp.weixin.qq.com/s/KG4sltQMCfH2MGXkRdtnwA) - [您如何使用Selenium来计算自动化测试的投资回报率?](https://mp.weixin.qq.com/s/DVSEm0DhoAvYfTWIniabJg) - [Selenium 4 Java的最佳测试框架](https://mp.weixin.qq.com/s/MlNyv-kb03gRTcYllxUreA) - [Selenium 4.0 Alpha更新日志](https://mp.weixin.qq.com/s/tU7sm-pcbpRNwDU9D3OVTQ) - [Selenium 4.0 Alpha更新实践](https://mp.weixin.qq.com/s/yT9wpO5o5aWBUus494TIHw) - [JUnit 5和Selenium基础(一)](https://mp.weixin.qq.com/s/ehBRf7st-OxeuvI_0yW3OQ) - [JUnit 5和Selenium基础(二)](https://mp.weixin.qq.com/s/Gt82cPmS2iX-DhKXTXiy8g) - [JUnit 5和Selenium基础(三)](https://mp.weixin.qq.com/s/8YkonXTYgAV5-pLs9yEAVw) - [如何在跨浏览器测试中提高效率](https://mp.weixin.qq.com/s/MB_Wv7yQ6i9BztAZtL4grA) - [Selenium Python使用技巧(一)](https://mp.weixin.qq.com/s/39v8tXG3xig63d-ioEAi8Q) - [Selenium Python使用技巧(二)](https://mp.weixin.qq.com/s/uDM3y9zoVjaRmJJJTNs6Vw) - [Selenium Python使用技巧(三)](https://mp.weixin.qq.com/s/J7-CO-UDspUGSpB8isjsmQ) - [Selenium并行测试基础](https://mp.weixin.qq.com/s/OfXipd7YtqL2AdGAQ5cIMw) - [Selenium并行测试最佳实践](https://mp.weixin.qq.com/s/-RsQZaT5pH8DHPvm0L8Hjw) - [维护Selenium测试自动化的最佳实践](https://mp.weixin.qq.com/s/EMD1aWuzOSfT7j3KeXhJcA) - [Selenium自动化测试技巧](https://mp.weixin.qq.com/s/EzrpFaBSVITO2Y2UvYvw0w) - [Selenium自动化:代码测试与无代码测试](https://mp.weixin.qq.com/s/gtmLpQ5FCeuzh1SB5mxuvg) - [Selenium处理下拉列表](https://mp.weixin.qq.com/s/E2txSVAmDzYIEZWnyAND4g) - [Selenium自动化常见问题](https://mp.weixin.qq.com/s/edoxu-QaD0SOw1VqrhCZWA) - [Selenium4 IDE,它终于来了](https://mp.weixin.qq.com/s/XNotlZvFpmBmBQy1pYifOw) - [Selenium4 IDE特性:无代码趋势和SIDE Runner](https://mp.weixin.qq.com/s/G0S9K0jHsN0P_jxdMME-cg) - [Selenium4 IDE特性:弹性测试、循环和逻辑判断](https://mp.weixin.qq.com/s/o4_jIyi9O7s4S3CbTzl5rQ) - [Selenium自动化最佳实践技巧(上)](https://mp.weixin.qq.com/s/lZww1azmncMMMHRY0_yKqA) - [Selenium自动化最佳实践技巧(中)](https://mp.weixin.qq.com/s/9D0lUZ-XKHiukNeRqp6zOQ) - [Selenium自动化最佳实践技巧(下)](https://mp.weixin.qq.com/s/opVik2ZxmTBurIBoa4yipQ) - [Selenium等待:sleep、隐式、显式和Fluent](https://mp.weixin.qq.com/s/73BobMq9M12rYMvzxNhRtA) - [Selenium自动化的JUnit参数化实践](https://mp.weixin.qq.com/s/WFu5rJaowxhAIcbEoEatkw) - [Selenium异常集锦](https://mp.weixin.qq.com/s/DDkaliSVthX-c_KKG-WwNA) - [Selenium自动化测试之前](https://mp.weixin.qq.com/s/DKjSnS9sP0SoHUw4OhOikw) ## APP性能 - [使用monkey测试时,一个控制WiFi状态的多线程类](https://mp.weixin.qq.com/s/P8HVtzHBlj_FcDAAHFKBDg) - [java执行和停止Logcat命令及多线程实现](https://mp.weixin.qq.com/s/sUYibRc-muxQoxi48QiaRg) - [APP性能测试中获取CPU和PSS数据多线程实现](https://mp.weixin.qq.com/s/NiJSZ8VxpdnarbDJjcJziA) - [统计APP启动时间和进入首页时间的多线程类](https://mp.weixin.qq.com/s/IMs6vd3H-HF65Vb-zPwDhw) - [如何获取手机性能测试数据FPS](https://mp.weixin.qq.com/s/qZy5AQkNpUXRJk46BHVzaQ) - [一个循环启动APP并保持WiFi常开的多线程类](https://mp.weixin.qq.com/s/OgdT4IffDyAdkKmO2SS9iQ) ## 杂乱 - [测试窝,首页抄我七篇原创还拉黑,你们的良心不会痛吗?](https://mp.weixin.qq.com/s/ke5avkknkDMCLMAOGT7wiQ) - [如何优雅地屏蔽掉Google搜索结果中视频、新闻、图片等结果](https://mp.weixin.qq.com/s/Iu7pt4Qk3w9sJp3n_UVAeQ) - [测试玩梗--欢迎补充](https://mp.weixin.qq.com/s/y_QHbsjFCQVSCfj-A4Usmg) - [图解HTTP脑图](https://mp.weixin.qq.com/s/100Vm8FVEuXs0x6rDGTipw) - [测试之JVM命令脑图](https://mp.weixin.qq.com/s/qprqyv0j3SCvGw1HMjbaMQ) - [好书推荐《Java性能权威指南》](https://mp.weixin.qq.com/s/YWd5Yx6n7887g1lMLTcsWQ) - [2019年浏览器市场份额排行榜](https://mp.weixin.qq.com/s/4NmJ_ZCPD5UwaRCtaCfjEg) - [JSON基础](https://mp.weixin.qq.com/s/tnQmAFfFbRloYp8J9TYurw) - [JMeter吞吐量误差分析](https://mp.weixin.qq.com/s/jHKmFNrLmjpihnoigNNCSg) - [JMeter如何模拟不同的网络速度](https://mp.weixin.qq.com/s/1FCwNN2htfTGF6ItdkcCzw) - [疫情期间,如何提高远程办公效率](https://mp.weixin.qq.com/s/k_XrdqjGKMshK2Ea-VCNLw) - [接口测试视频专题](https://mp.weixin.qq.com/s/4mKpW3QiVRee3kcVOSraog) - [Groovy在JMeter中应用专题](https://mp.weixin.qq.com/s/KcxPUDWl7MLQemFRoIV92A) - [Java多线程编程在JMeter中应用](https://mp.weixin.qq.com/s/xCnFx5TvIF1SAVNm-aZnxQ) - [未来的神器fiddler Everywhere](https://mp.weixin.qq.com/s/-BSuHR6RPkdv8R-iy47MLQ) - [Charles报错Failed to install helper解决方案](https://mp.weixin.qq.com/s/LHhMTBhlDM0DrPCvWeU0zA) - [测试仓库推介(上)](https://mp.weixin.qq.com/s/zgy6UgNMFcbISD1NhxSAWg) - [测试仓库推介(下)](https://mp.weixin.qq.com/s/njnpmRGoEgdxjqkR7c3a6A) - [Fiddler Everywhere工具答疑](https://mp.weixin.qq.com/s/2peWMJ-rgDlVjs3STNeS1Q) - [Mac上测试Internet Explorer的N种方法](https://mp.weixin.qq.com/s/HeLBPTp2dfs5IlyLMCi90Q) - [IntelliJ中基于文本的HTTP客户端](https://mp.weixin.qq.com/s/-9qi_lLVVfxQKEFmcRYFtA) - [开源礼节](https://mp.weixin.qq.com/s/EyNules2f9NYdnYAX_NQSw) - [弱网测试:最低流畅网速是多少?](https://mp.weixin.qq.com/s/rCji6fZs9yYyk7GyIWvSiA) - [接口测试直播回顾](https://mp.weixin.qq.com/s/B8ih9sswaE-OWuVib6C16g) - [SpotBugs报错no Groovy library is defined解决办法](https://mp.weixin.qq.com/s/XxvuVS2TmlqT5-b22vObYQ) - [推荐好书:不要总是谦卑地弯着腰](https://mp.weixin.qq.com/s/mYNN9jSaikOF5aJEkb-Bug) - [2020年FunTester自我总结](https://mp.weixin.qq.com/s/DeDY1JZUTk3cjjQfr3DJRg) - [原创打油诗欣赏](https://mp.weixin.qq.com/s/3hPSDjH-3cWu6EVsjU0wOw) - [优秀讲师 | 腾讯云+社区权威认证](https://mp.weixin.qq.com/s/oeTeJZs6h4jJJMRyUunurw) - [Boss直聘签约作者](https://mp.weixin.qq.com/s/CjwFBoS9sew6R74mM9QO4g) - [GDevOps官方合作媒体](https://mp.weixin.qq.com/s/OnwvRqrgXq_pGp8eOXOTgw) - [假期思考题](https://mp.weixin.qq.com/s/3DOnkmYDlwk-XKg4ge3ZUw) - [甩锅技能+1](https://mp.weixin.qq.com/s/nMwlfXZoDcRRPHcTKpvfNg) - [不要浪费自己的求知欲](https://mp.weixin.qq.com/s/WO0aQqmhU_xGUpWvYwOqUA) ================================================ FILE: document/base.markdown ================================================ # 基础篇 > **FunTester**,[腾讯云年度作者](https://mp.weixin.qq.com/s/oeTeJZs6h4jJJMRyUunurw)、[Boss直聘签约作者](https://mp.weixin.qq.com/s/CjwFBoS9sew6R74mM9QO4g),非著名测试开发er,欢迎关注。 * `Gitee`地址*https://gitee.com/fanapi/tester* * `GitHub`地址*https://github.com/JunManYuanLong/FunTester* # 语言合集 ## Java - [java一行代码打印心形](https://mp.weixin.qq.com/s/QPSryoSbViVURpSa9QXtpg) - [操作的原子性与线程安全](https://mp.weixin.qq.com/s/QU3llkGLepX2VCch8Y9GKw) - [快看,i++真的不安全](https://mp.weixin.qq.com/s/-CdWdROKSEq_ZiLX2kWxzA) - [原子操作组合与线程安全](https://mp.weixin.qq.com/s/XB5LXucAF5Bo8EkfLZYRmw) - [java利用for循环输出正三角新解](https://mp.weixin.qq.com/s/nnMR2177LLVn4u_9s9Fl4g) - [在main方法之前,到底执行了什么?](https://mp.weixin.qq.com/s/jWxiCMfwmvRHrjPdRG8ZyQ) - [传参传的到底是什么?](https://mp.weixin.qq.com/s/p_pEQwE6h6q7PprkW-kjbg) - [json里面put了null会怎么样?](https://mp.weixin.qq.com/s/gQVROe01I3JzIqNdTSHpDQ) - [主线程都结束了,为何进程还在执行](https://mp.weixin.qq.com/s/q2v5JU5dtmNEol7I7IVY-Q) - [java测试框架如何执行groovy脚本文件](https://mp.weixin.qq.com/s/0GYt1l3_z5-1qzBNl6_PzA) - [java用递归筛选法求N以内的孪生质数(孪生素数)](https://mp.weixin.qq.com/s/PSdCb-DrgMPb4WpJJMexmQ) - [从JVM堆内存分析验证深浅拷贝](https://mp.weixin.qq.com/s/SdYDnoau1rjjvPC2SUymBg) - [如何学习Java基础](https://mp.weixin.qq.com/s/FCPStkYoJF67NYln4Lc6xg) - [如何保存HTTPrequestbase和CloseableHttpResponse](https://mp.weixin.qq.com/s/gRY8HRQHCh0PfyS7Q22IwA) - [如何在匿名thread子类中保证线程安全](https://mp.weixin.qq.com/s/GCXx_-ummi0JfZQ7GTIxig) - [Java服务端两个常见的并发错误](https://mp.weixin.qq.com/s/5VvCox3eY6sQDsuaKB4ZIw) - [Java中interface属性和实例方法](https://mp.weixin.qq.com/s/vrKkM6522tgw3v_cL7R8HA) - [服务端性能优化之双重检查锁](https://mp.weixin.qq.com/s/-bOyHBcqFlJY3c0PEZaWgQ) - [Java并发BUG基础篇](https://mp.weixin.qq.com/s/NR4vYx81HtgAEqH2Q93k2Q) - [Java并发BUG提升篇](https://mp.weixin.qq.com/s/GCRRe8hJpe1QJtxq9VBEhg) - [性能测试中图形化输出测试数据](https://mp.weixin.qq.com/s/EMvpYIsszdwBJFPIxztTvA) - [超大对象导致Full GC超高的BUG分享](https://mp.weixin.qq.com/s/L15-0JW9WK-E005GeOG9WQ) - [利用ThreadLocal解决线程同步问题](https://mp.weixin.qq.com/s/VEm8jt3ZUEUdyyeXPC8VvQ) - [线程安全集合类中的对象是安全的么?](https://mp.weixin.qq.com/s/WKSuPEfzZCVwjVTcoD0Dyg) - [如何使用“dd MM”解析日期](https://mp.weixin.qq.com/s/v9ooAj3dKu53JXgxB482HA) - [Java和Groovy正则使用](https://mp.weixin.qq.com/s/DT3BKE3ZcCKf6TLzGc5wbg) - [运行越来越快的Java热点代码](https://mp.weixin.qq.com/s/AP0BcDEjDuaouaB0RXJOoQ) - [6个重要的JVM性能参数](https://mp.weixin.qq.com/s/b1QnapiAVn0HD5DQU9JrIw) - [ArrayList浅、深拷贝](https://mp.weixin.qq.com/s/kYsBzFsCyDPUssdV3MDqLA) - [Java性能测试中两种锁的实现](https://mp.weixin.qq.com/s/j9dGFvYzCJ0AGwYUtTrTsw) - [测试如何处理Java异常](https://mp.weixin.qq.com/s/H00GWiATOD8QHJu3UewrBw) - [创建Java守护线程](https://mp.weixin.qq.com/s/_UjWdvq8QWYTshr4SeniBg) - [Lambda表达式在线程安全Map中应用](https://mp.weixin.qq.com/s/zZjB5aOWh4a_k1eoEsR5ww) - [Java程序是如何浪费内存的](https://mp.weixin.qq.com/s/w7VF5m5cc0X7LNvqmwGfvg) - [Java中的自定义异常](https://mp.weixin.qq.com/s/nspIdxFP9qEDtagGN4gaMQ) - [Java文本块](https://mp.weixin.qq.com/s/GwasvpJsd7uLngvCr6KlQw) - [CountDownLatch类在性能测试中应用](https://mp.weixin.qq.com/s/uYBPPOjauR2h81l2uKMANQ) - [CyclicBarrier类在性能测试中应用](https://mp.weixin.qq.com/s/kvEHX3t_2xpMke9vwOdWrg) - [Phaser类在性能测试中应用](https://mp.weixin.qq.com/s/plxNnQq7yNQvHYEGpyY4uA) - [Java压缩/解压缩字符串](https://mp.weixin.qq.com/s/7vHNd5dEN93DPUqgS8od_A) - [Java删除空字符:Java8 & Java11](https://mp.weixin.qq.com/s/6dlgYgTFZsHuJ4Eaby5eyg) - [Java Stream中map和flatMap方法](https://mp.weixin.qq.com/s/0FG2o7VUAG6z8a_0je-1EQ) - [泛型类的正确用法](https://mp.weixin.qq.com/s/1azilraonPIZNCnw_9MB5Q) - [Java字符串到数组的转换--最后放大招](https://mp.weixin.qq.com/s/iMUYZYkJ5CjykwWqinNm5g) - [Java求数组的并集--最后放大招](https://mp.weixin.qq.com/s/bZ93SGakyiRbaRujhx4nvw) - [Java计算数组平均值--最后放大招](https://mp.weixin.qq.com/s/dxQaFHu2PyAbOK6jpEgEUQ) - [Math.abs()求绝对值返回负值BUG分享](https://mp.weixin.qq.com/s/RHzExuRqF1XsBtzGKzmgGA) - [Java代理模式初探](https://mp.weixin.qq.com/s/SBL_K2PQez3vDHhtAN9NLg) - [Socket接口异步验证实践](https://mp.weixin.qq.com/s/bnjHK3ZmEzHm3y-xaSVkTw) - [性能测试中异步展示测试进度](https://mp.weixin.qq.com/s/AOERJbEc4ATJqhjvnxgQoA) - [Java中的ThreadLocal功能演示](https://mp.weixin.qq.com/s/n92k1JswHKrqT7Y_CD9Q0w) - [ThreadLocal在链路性能测试中实践](https://mp.weixin.qq.com/s/3qhNdHHSStELzNraQSpcew) - [歪解字符串中连续出现次数最多问题](https://mp.weixin.qq.com/s/xBy4iB4qLd4WQgCsVVuemw) - [Java&Groovy下载文件对比](https://mp.weixin.qq.com/s/T9WUynej2yOZhCkDUhaLYw) - [线程同步类CyclicBarrier在性能测试集合点应用](https://mp.weixin.qq.com/s/K2YySxX9T4v_rzbvIbIHJA) - [Java线程同步三剑客](https://mp.weixin.qq.com/s/cAmd11-HdwXNU3tp4TiDLg) ## Groovy - [java和groovy混合编程时提示找不到符合错误解决办法](https://mp.weixin.qq.com/s/dLC2W7nIi5zCuK6JTkiA-w) - [groovy使用stream语法递归筛选法求N以内的质数](https://mp.weixin.qq.com/s/TsrVn1cuQUrU6wj9OnR-FQ) - [使用Groovy进行Bash(shell)操作](https://mp.weixin.qq.com/s/fgCTlZUF3QeNj6jzq1ZgGg) - [使用Groovy和Gradle轻松进行数据库操作](https://mp.weixin.qq.com/s/lwmclrnW0csykVRhu7dNTQ) - [愉快地使用Groovy Shell](https://mp.weixin.qq.com/s/fJh7fbB3naBFBEiaS62oxw) - [Gradle+Groovy基础篇](https://mp.weixin.qq.com/s/c2j7G-PoNtAB3oYYDUhCGw) - [Gradle+Groovy提高篇](https://mp.weixin.qq.com/s/yXmYj_1fynLkR0-5FV_Arw) - [Groovy重载操作符](https://mp.weixin.qq.com/s/4jW06Q4_vjFR9DovRTTuHg) - [用Groovy处理JMeter断言和日志](https://mp.weixin.qq.com/s/Q4yPA4p8dZYAARZ60ZDh9w) - [用Groovy处理JMeter变量](https://mp.weixin.qq.com/s/BxtweLrBUptM8r3LxmeM_Q) - [用Groovy在JMeter中执行命令行](https://mp.weixin.qq.com/s/VTip7tiLpwBOr1gUoZ0n8A) - [用Groovy处理JMeter中的请求参数](https://mp.weixin.qq.com/s/9pCUOXWpMwXR5ynvCMYJ7A) - [Java和Groovy正则使用](https://mp.weixin.qq.com/s/DT3BKE3ZcCKf6TLzGc5wbg) - [Groovy中的元组](https://mp.weixin.qq.com/s/0-ka0-tv1vyKbiA6m44jRw) - [从Java到Groovy的八级进化论](https://mp.weixin.qq.com/s/QTrRHsD3w-zLGbn79y8yUg) - [用Groovy在JMeter中使用正则提取赋值](https://mp.weixin.qq.com/s/9riPpnQZCfKGscuzOOpYmQ) - [Groovy在JMeter中处理cookie](https://mp.weixin.qq.com/s/DCnDjWaj2aiKv5HVw3-n6A) - [Groovy在JMeter中处理header](https://mp.weixin.qq.com/s/juY-1jEWODJ5HHiEsxhIEw) - [Groovy的神奇NullObject](https://mp.weixin.qq.com/s/jLGisN_30PrCgNP33Sww0g) - [Groovy中的list](https://mp.weixin.qq.com/s/0mUe1_WrUiEm1t6kqCV3eQ) - [JMeter参数签名——Groovy脚本形式](https://mp.weixin.qq.com/s/wQN9-xAUQofSqiAVFXdqug) - [Groovy中的闭包](https://mp.weixin.qq.com/s/pfcG47gSPfUveAaEfdeo8A) - [JMeter参数签名——Groovy工具类形式](https://mp.weixin.qq.com/s/urwU4p9ofv9sU-JFy5Z0iA) - [删除List中null的N种方法--最后放大招](https://mp.weixin.qq.com/s/4mfskN781dybyL59dbSbeQ) - [混合Java函数和Groovy闭包](https://mp.weixin.qq.com/s/FAIzGgLSX2u7RKbOGs3lGA) - [Groovy重载操作符(终极版)](https://mp.weixin.qq.com/s/4oYGJ2B2Y1AqxsIj8v5nZA) - [JsonPath工具类单元测试](https://mp.weixin.qq.com/s/1YtUWGk_sTjn9bHwAeT0Ew) - [Groovy小记it关键字和IDE报错](https://mp.weixin.qq.com/s/cIMHzkvKtH0a0ewkiBnV8g) - [JsonPath验证类既Groovy重载操作符实践](https://mp.weixin.qq.com/s/5gc04CAsBY6pWxe5c2P41w) - [Groovy枚举类初始化异常分析](https://mp.weixin.qq.com/s/koFhpBZM1MFYYxCNxUKPyQ) - [Java&Groovy下载文件对比](https://mp.weixin.qq.com/s/T9WUynej2yOZhCkDUhaLYw) ## Python - [python使用filter方法递归筛选法求N以内的质数(素数)--附一行打印心形标记的代码解析](https://mp.weixin.qq.com/s/D8RfpdIi8smCL8TAzBcNpA) - [关于python版微信使用经验分享](https://mp.weixin.qq.com/s/19IaI6ETZAm_T4ePPlXqIg) - [python用递归筛选法求N以内的孪生质数(孪生素数)](https://mp.weixin.qq.com/s/rVY2pTl8So11WCvA9GrFbA) - [利用python wxpy和requests写一个自动应答微信机器人实例](https://mp.weixin.qq.com/s/Fni2kX5BRjdqOQ-glCLjRg) - [Python版Socket.IO接口测试脚本](https://mp.weixin.qq.com/s/oXBP6Sx3yPqlmvV9uCUScw) ## 测开笔记 - [我的开发日记(一)](https://mp.weixin.qq.com/s/eQgpOKbXsU9vOmxp0Xiklg) - [我的开发日记(二)](https://mp.weixin.qq.com/s/XuffL3ZmKKOgHDtH_cEYOw) - [我的开发日记(三)](https://mp.weixin.qq.com/s/a-I0agh6nWp8RLlcmbgf5w) - [我的开发日记(四)](https://mp.weixin.qq.com/s/QukXd00Mx_dbkgiXys0FNg) - [我的开发日记(五)](https://mp.weixin.qq.com/s/6P3nScsVW6MfMcyIqcA1AQ) - [我的开发日记(六)](https://mp.weixin.qq.com/s/Gz2QmukONNldSy9Fd29u5w) - [我的开发日记(七)](https://mp.weixin.qq.com/s/MjZ-nFXfQkHMsXS0fX1c1w) - [我的开发日记(八)](https://mp.weixin.qq.com/s/6ZhNcFm-gR5dhKQjEkE3Rg) - [我的开发日记(九)](https://mp.weixin.qq.com/s/VfD2T3orojGxnylr3Q5UeA) - [我的开发日记(十)](https://mp.weixin.qq.com/s/6DWth40LGbAraJi05G16Pw) - [我的开发日记(十一)](https://mp.weixin.qq.com/s/nsX5A-P6QbePHDN_Pse0_A) - [我的开发日记(十二)](https://mp.weixin.qq.com/s/XA1KJXBP3Zl-XFswXxUtvg) - [我的开发日记(十三)](https://mp.weixin.qq.com/s/_QPUu5pUlg4A_AlC5wOGkA) - [我的开发日记(十四)](https://mp.weixin.qq.com/s/Qy1YKAb3wqW_Ip2FwH7Otw) - [我的开发日记(十五)](https://mp.weixin.qq.com/s/bwkvz2t6YItQD0O_BIxpHQ) - [这些年,我写过的BUG(一)](https://mp.weixin.qq.com/s/mVTmT1FdwWl1e0BaL7Ne1g) - [这些年,我写过的BUG(二)](https://mp.weixin.qq.com/s/NMz5n0ZMf6taGb-gr1BLyw) - [FunTester测试框架架构图初探](https://mp.weixin.qq.com/s/bcMbVDkWbHSXjZFDeFyJsQ) - [FunTester测试项目架构图初探](https://mp.weixin.qq.com/s/wqb8FXRbEXrhDuZounmNXA) ================================================ FILE: document/directory.markdown ================================================ * 由于文章链接存在敏感词问题,请各位看官移步 ## [GitHub地址](https://github.com/JunManYuanLong/FunTester/blob/okay/document/article.markdown) ### 当然也可以关注**FunTester**公众号 ================================================ FILE: document/update.markdown ================================================ # 升级篇 > **FunTester**,[腾讯云年度作者](https://mp.weixin.qq.com/s/oeTeJZs6h4jJJMRyUunurw)、[Boss直聘签约作者](https://mp.weixin.qq.com/s/CjwFBoS9sew6R74mM9QO4g),非著名测试开发er,欢迎关注。 * `Gitee`地址*https://gitee.com/fanapi/tester* * `GitHub`地址*https://github.com/JunManYuanLong/FunTester* # 案例分享 ## 测试方案 - [如何对消息队列做性能测试](https://mp.weixin.qq.com/s/MNt22aW3Op9VQ5OoMzPwBw) - [如何对修改密码接口进行压测](https://mp.weixin.qq.com/s/9CL_6-uZOlAh7oeo7NOpag) - [如何测试概率型业务接口](https://mp.weixin.qq.com/s/kUVffhjae3eYivrGqo6ZMg) - [如何测试非固定型概率算法P=p(1+0.1*N)](https://mp.weixin.qq.com/s/sgg8v-Bi-_sUDJXwuTCMGg) - [性能测试中标记每个请求](https://mp.weixin.qq.com/s/PokvzoLdVf_y9inlVXHJHQ) - [如何对N个接口按比例压测](https://mp.weixin.qq.com/s/GZxbH4GjDkk4BLqnUj1_kw) - [多种登录方式定量性能测试方案](https://mp.weixin.qq.com/s/WuZ2h2rr0rNBgEvQVioacA) - [压测中测量异步写入接口的延迟](https://mp.weixin.qq.com/s/odvK1iYgg4eRVtOOPbq15w) - [绑定手机号性能测试](https://mp.weixin.qq.com/s/K5x1t1dKtIT2NKV6k4v5mw) - [手机号验证码登录性能测试](https://mp.weixin.qq.com/s/i-j8fJAdcsJ7v8XPOnPDAw) - [重放浏览器单个请求性能测试实践](https://mp.weixin.qq.com/s/a10hxCrIzS4TV9JwmDSI3Q) - [重放浏览器多个请求性能测试实践](https://mp.weixin.qq.com/s/Hm1Kpp1PMrZ5rYFW8l2GlA) - [重放浏览器请求多链路性能测试实践](https://mp.weixin.qq.com/s/9YSBLAyHVw8Z6IfK-nJTpQ) - [Socket接口固定QPS性能测试实践](https://mp.weixin.qq.com/s/I9-14L8THxvtX1NJY0KPfw) ## BUG集锦 - [一个MySQL索引引发的血案](https://mp.weixin.qq.com/s/KLSber-gPg53JVfsCa3Dtw) - [微软Zune闰年BUG分析](https://mp.weixin.qq.com/s/zpqAUcNcHaZjWUdUYH_loQ) - [“双花”BUG的测试分享](https://mp.weixin.qq.com/s/0dsBsssNfg-seJ_tu9zFaQ) - [iOS 11计算器1+2+3=24真的是bug么?](https://mp.weixin.qq.com/s/nokQhe_Hqcq-o7pZJmFlqQ) - [不要在遍历的时候删除](https://mp.weixin.qq.com/s/MIczbEpbOrADL0_V7ZUhlg) - [连开100年会员会怎样](https://mp.weixin.qq.com/s/mZw-SFIxFFbE-o8UeXhdfg) - [异步查询转同步加redis业务实现的BUG分享](https://mp.weixin.qq.com/s/ni3f6QTxw0K-0I3epvEYOA) - [Java服务端两个常见的并发错误](https://mp.weixin.qq.com/s/5VvCox3eY6sQDsuaKB4ZIw) - [超大对象导致Full GC超高的BUG分享](https://mp.weixin.qq.com/s/L15-0JW9WK-E005GeOG9WQ) - [访问权限导致toString返回空BUG分享](https://mp.weixin.qq.com/s/usDOcuJrXOmEKN-mVBzRKg) - [异常使用中的BUG](https://mp.weixin.qq.com/s/IG9Ar3IT7CrlSv4d0lCvgA) - [Math.abs()求绝对值返回负值BUG分享](https://mp.weixin.qq.com/s/RHzExuRqF1XsBtzGKzmgGA) ## 爬虫实践 - [接口爬虫之网页表单数据提取](https://mp.weixin.qq.com/s/imJ5u67xhYQaEzv-O1in4g) - [httpclient爬虫爬取汉字拼音等信息](https://mp.weixin.qq.com/s/w-IvBxAsotmPA3pydpIo1w) - [httpclient爬虫爬取电影信息和下载地址实例](https://mp.weixin.qq.com/s/TB49X4S-ioyoW5CrzAnHcw) - [httpclient 多线程爬虫实例](https://mp.weixin.qq.com/s/nXL-MP4Y6CN2hgZQefWEeQ) - [groovy爬虫练习之——企业信息](https://mp.weixin.qq.com/s/1TisDceIL1-Luqz_wOqAiw) - [httpclient 爬虫实例——爬取三级中学名](https://mp.weixin.qq.com/s/Dd7U30aHYauqBFxJdxaiyg) - [电子书网站爬虫实践](https://mp.weixin.qq.com/s/KGW0dIS5NTLgxyhSjxDiOw) - [groovy爬虫实例——历史上的今天](https://mp.weixin.qq.com/s/5LDUvpU6t_GZ09uhZr224A) - [爬取720万条城市历史天气数据](https://mp.weixin.qq.com/s/vOyKpeGlJSJp9bQ8NIMe2A) - [记一次失败的爬虫](https://mp.weixin.qq.com/s/SMylrZLXDGw5f1xKI9ObnA) - [爬虫实践--CBA历年比赛数据](https://mp.weixin.qq.com/s/mM_QSQddabU5im_O6iVR-Q) - [图片爬虫实践](https://mp.weixin.qq.com/s/u5bRSyKsmn3TcjqEEqRJpw) # 工具合集 ## JsonPath合集 - [JsonPath实践(一)](https://mp.weixin.qq.com/s/Cq0_v_ptbGd4f5y8HIsq7w) - [JsonPath实践(二)](https://mp.weixin.qq.com/s/w_iJTiuQahIw6U00CJVJZg) - [JsonPath实践(三)](https://mp.weixin.qq.com/s/58A3k0T6dbOkBJ5nRYKDqA) - [JsonPath实践(四)](https://mp.weixin.qq.com/s/8ER61qrkMj8bdBpyuq9r6w) - [JsonPath实践(五)](https://mp.weixin.qq.com/s/knVLW960WXnckGLstdrOVQ) - [JsonPath实践(六)](https://mp.weixin.qq.com/s/ckBCK3t1w68FLBhaw5a7Jw) - [JsonPath工具类封装](https://mp.weixin.qq.com/s/KyuCuG5fVEExxBdGJO2LdA) - [JsonPath工具类单元测试](https://mp.weixin.qq.com/s/1YtUWGk_sTjn9bHwAeT0Ew) - [JsonPath验证类既Groovy重载操作符实践](https://mp.weixin.qq.com/s/5gc04CAsBY6pWxe5c2P41w) - [JSON对象标记语法验证类](https://mp.weixin.qq.com/s/jSXmoEdMF7nWAqQuzJ5GiQ) ## Jacoco覆盖率 - [接口测试代码覆盖率(jacoco)方案分享](https://mp.weixin.qq.com/s/D73Sq6NLjeRKN8aCpGLOjQ) - [jacoco无法读取build.xml配置中源码路径解决办法](https://mp.weixin.qq.com/s/8_x0rVfkIi-uX3y0drx_jw) - [使用JaCoCo Maven插件创建代码覆盖率报告](https://mp.weixin.qq.com/s/4Jo05k2WxytiSSNW9WTV-A) - [Java 8,Jenkins,Jacoco和Sonar进行持续集成](https://mp.weixin.qq.com/s/dOoXnKnWtQmmC5itClsl4g) - [jacoco测试覆盖率过滤非业务类](https://mp.weixin.qq.com/s/7YGe9pCHw3wd87tgOlKjSA) ## arthas诊断工具 - [arthas快速入门视频演示](https://mp.weixin.qq.com/s/Wl5QMD52isGTRuAP4Cpo-A) - [arthas进阶thread命令视频演示](https://mp.weixin.qq.com/s/XuF7Nr1sGC3diIn50zlDDQ) - [arthas命令jvm,sysprop,sysenv,vmoption视频演示](https://mp.weixin.qq.com/s/87BsTYqnTCnVdG3a_kBcng) - [arthas命令logger动态修改日志级别--视频演示](https://mp.weixin.qq.com/s/w724P9B12eTC9rMbavwsMA) - [arthas命令sc和sm视频演示](https://mp.weixin.qq.com/s/Ga63sjW_bOKQqfnA5LTb9w) - [arthas命令ognl视频演示](https://mp.weixin.qq.com/s/cMCaXFwjp6QHFq40TvP4bQ) - [arthas命令redefine实现Java热更新](https://mp.weixin.qq.com/s/2HUXfJhoUfg4yMzSoRHK9w) - [arthas命令monitor监控方法执行](https://mp.weixin.qq.com/s/7-oe3UoTY8bzpi89tIKvQQ) - [arthas命令watch观察方法调用(上)](https://mp.weixin.qq.com/s/6fMKP7H4Q7ll_0v-wyN19g) - [arthas命令watch观察方法调用(下)](https://mp.weixin.qq.com/s/-r2kufxdOjRb2TgF2HPskg) - [arthas命令trace追踪方法链路](https://mp.weixin.qq.com/s/bzkdKZugkOl8C-_xTw92YA) - [arthas命令tt方法时空隧道](https://mp.weixin.qq.com/s/mDczYmVdSmL5ZbK7bb8i0A) ## moco API - [解决moco框架API在post请求json参数情况下query失效的问题](https://mp.weixin.qq.com/s/V5lXoepEBtPJrSUHA0Uz5A) - [给moco API添加limit功能](https://mp.weixin.qq.com/s/pXJECi15ieNLmA0uIqEqfA) - [给moco API添加random功能](https://mp.weixin.qq.com/s/YTcbFbFaWB5arW_fubgTTQ) - [解决moco框架API在cycle方法缺失的问题](https://mp.weixin.qq.com/s/YfsPa7eW8WV65CDbPooBPg) - [五行代码构建静态博客](https://mp.weixin.qq.com/s/hZnimJOg5OqxRSDyFvuiiQ) - [moco API模拟框架视频讲解(上)](https://mp.weixin.qq.com/s/X5-fFXe018_O60WCRdawZg) - [moco API模拟框架视频讲解(中)](https://mp.weixin.qq.com/s/g2En-9W9JWYrCLQr_WPEBA) - [moco API模拟框架视频讲解(下)](https://mp.weixin.qq.com/s/mz__DiNxMGHwIKCLsjKR8g) - [如何mock固定QPS的接口](https://mp.weixin.qq.com/s/yogj9Fni0KJkyQuKuDYlbA) - [mock延迟响应的接口](https://mp.weixin.qq.com/s/x_fu0InQpYIUJIQFi9a50g) - [moco固定QPS接口升级补偿机制](https://mp.weixin.qq.com/s/zAM91e_REo4edSPTLuHLOw) ## 工具类 - [java网格输出的类](https://mp.weixin.qq.com/s/BJTJu0LGjn7Hc9J1yT04KQ) - [java使用poi写入excel文档的一种解决方案](https://mp.weixin.qq.com/s/Ft56gd1B9CPrQs2zq4Cpug) - [java使用poi读取excel文档的一种解决方案](https://mp.weixin.qq.com/s/ltZGx9J7E8DTer0D-pfQ2Q) - [MongoDB操作类封装](https://mp.weixin.qq.com/s/u-RHOE5XrjOEkelWIxdplw) - [java网格输出的类](https://mp.weixin.qq.com/s/QW8nKM2Bz7C75fdkCzSbpw) - [将json数据格式化输出到控制台](https://mp.weixin.qq.com/s/2IPwvh-33Ov2jBh0_L8shA) - [利用反射根据方法名执行方法的使用示例](https://mp.weixin.qq.com/s/5ntwDo4ZVcTh1PmK4vkNfA) - [解决统计出现次数问题的方法类](https://mp.weixin.qq.com/s/gqz4wuKkMWAOIQwMtiupnA) - [java利用时间戳来获取UTC时间](https://mp.weixin.qq.com/s/wbDIrwDnxb9_XWkkmP3A_g) - [如何遍历执行一个包里面每个类的用例方法](https://mp.weixin.qq.com/s/OJwCOHCJ4TalatsEWbtzIQ) - [阿拉伯数字转成汉字](https://mp.weixin.qq.com/s/jNZXIvwMpdxt7jIAlVBgHg) - [获取JVM转储文件的Java工具类](https://mp.weixin.qq.com/s/f_TlOb3m8MeR3argBmTzzA) - [基于DOM的XML文件解析类](https://mp.weixin.qq.com/s/scRj7OAhvJYL3mx_hCFp4A) - [XML文件解析实践(DOM解析)](https://mp.weixin.qq.com/s/V2DG3osaPNUJzFNDQgqM-w) - [基于DOM4J的XML文件解析类](https://mp.weixin.qq.com/s/K5R7iMXouTn4g0p14T7iAQ) - [将HTTP请求对象转成curl命令行](https://mp.weixin.qq.com/s/861uMAMMWtINjy4Z99WA6w) ## 构建工具 - [java和groovy混编的Maven项目如何用intellij打包执行jar包](https://mp.weixin.qq.com/s/bKexZXlONeo3r6FDhfMltQ) - [window系统权限不足导致gradle构建失败的解决办法](https://mp.weixin.qq.com/s/dqiQvmVG1o6glU-pknLDwQ) - [使用groovy脚本使gradle灵活加载本地jar包的两种方式](https://mp.weixin.qq.com/s/p3K3ZS7iOUeKO7E94gKFVg) - [Java 8,Jenkins,Jacoco和Sonar进行持续集成](https://mp.weixin.qq.com/s/dOoXnKnWtQmmC5itClsl4g) - [Gradle如何在任务失败后继续构建](https://mp.weixin.qq.com/s/GcXDzRN7cM_QQpt9ytqoKg) - [Gradle+Groovy基础篇](https://mp.weixin.qq.com/s/c2j7G-PoNtAB3oYYDUhCGw) - [Gradle+Groovy提高篇](https://mp.weixin.qq.com/s/yXmYj_1fynLkR0-5FV_Arw) - [Maven进行增量构建](https://mp.weixin.qq.com/s/ThQ7j6TS93KJZFqlNx8IQg) - [SonarQube8.3中的Maven项目的测试覆盖率报告](https://mp.weixin.qq.com/s/Xhp26jyE1c7Auielz48Llw) ## plotly可视化 - [MacOS使用pip安装pandas提示Cannot uninstall 'numpy'解决方案](https://mp.weixin.qq.com/s/fIqMAMXRQvf_vBtS5jDsyg) - [Python使用plotly生成本地文件教程](https://mp.weixin.qq.com/s/4dJdIP-g3fF40vX7S31jNg) - [Python2.7使用plotly绘制本地散点图和折线图实例](https://mp.weixin.qq.com/s/9QWrA0c-STmrmjSkBYWvbQ) - [Python可视化工具plotly从数据库读取数据作图示例](https://mp.weixin.qq.com/s/EUtPidiz_r1rpQBH_kudbA) - [利用Python+plotly制作接口请求时间的violin图表](https://mp.weixin.qq.com/s/3GdiLaiVRfkxwM3MOG-U8w) - [Python+plotly生成本地饼状图实例](https://mp.weixin.qq.com/s/61Qz9Kz-4ruzC0OvIuElpA) - [python plotly处理接口性能测试数据方法封装](https://mp.weixin.qq.com/s/NxVdvYlD7PheNCv8AMYqhg) - [利用python+plotly 制作接口响应时间Distplot图表](https://mp.weixin.qq.com/s/yrcUW1fFC18newqHcxhVvw) - [利用 python+plotly 制作Contour Plots模拟双波源干涉现象](https://mp.weixin.qq.com/s/vNW80BDeHsyjNQrnaBGk3Q) - [利用 python+plotly 制作双波源干涉三维图像](https://mp.weixin.qq.com/s/KSeV8VvQXRIg-bnzYoa5qg) - [python plotly制作接口响应耗时的时间序列表(Time Series )](https://mp.weixin.qq.com/s/U8chcVzCjGTdT3T_X5v4kw) - [python使用plotly批量生成图表](https://mp.weixin.qq.com/s/l18WfWz-s6qQ1JKKuh_2AQ) ================================================ FILE: long/1 ================================================ id=324,23,4,234,2,4,32,4,23 size1=9 size2=5 c=5,10,15 a=3,6,9 b=4,8,12 ================================================ FILE: long/30 ================================================ 309 163 240 173 169 114 158 87 106 107 143 131 175 112 236 230 357 83 255 191 314 120 316 81 195 107 141 163 151 123 171 237 172 109 128 206 206 149 83 104 191 198 90 105 157 91 118 148 214 238 148 175 191 276 161 158 74 345 115 134 154 154 139 96 187 174 124 105 160 70 77 134 303 82 127 169 370 174 147 233 128 198 480 140 95 231 189 237 279 170 108 146 240 165 109 236 181 228 150 108 340 207 141 221 161 97 183 134 136 111 155 90 123 192 187 130 134 128 155 330 127 324 281 197 214 193 146 120 76 121 254 157 164 186 120 145 192 132 317 154 195 144 121 185 138 97 120 287 251 221 126 599 130 208 137 261 165 100 179 218 159 102 109 175 176 124 99 225 268 161 105 98 179 136 170 201 100 98 342 231 111 292 180 239 85 145 155 232 159 195 121 307 164 203 102 122 235 203 172 280 336 177 226 318 114 146 137 204 293 138 188 366 77 131 202 174 175 131 120 253 147 183 123 208 140 274 184 118 117 140 335 162 164 132 142 183 220 126 221 216 157 94 177 115 267 174 178 80 113 152 86 177 194 184 91 129 171 356 144 159 143 205 237 156 198 90 198 130 107 137 140 148 93 239 281 162 229 301 460 142 173 271 67 168 434 116 139 227 257 98 158 249 192 201 130 191 92 83 102 82 413 168 223 272 114 302 256 154 127 231 224 100 114 148 106 175 285 127 201 167 229 138 96 170 133 104 104 102 230 237 224 241 129 165 314 182 175 136 270 175 130 132 123 162 354 184 100 207 181 354 120 97 196 171 99 107 301 190 169 191 150 80 157 183 101 171 148 100 180 132 234 235 148 184 100 117 121 163 290 399 130 296 74 164 208 90 307 167 97 123 134 127 249 225 250 208 233 91 83 244 411 162 90 237 161 109 112 154 126 98 179 96 168 195 350 162 271 271 112 379 175 218 153 120 91 118 90 83 87 237 196 246 186 126 106 127 142 162 411 57 94 118 163 121 99 152 194 318 361 135 82 121 175 119 263 251 146 181 131 196 74 299 119 133 102 65 118 87 341 177 112 290 133 120 146 94 241 110 271 190 239 243 160 167 266 223 456 251 173 116 597 176 277 213 159 103 129 122 170 217 466 206 118 257 142 111 155 153 297 127 225 124 236 192 194 223 203 106 302 401 244 112 178 157 186 151 124 159 118 203 153 358 108 209 333 247 154 114 72 143 113 102 144 96 183 114 128 119 230 410 234 187 160 157 266 64 228 162 121 219 250 129 289 141 112 133 128 178 97 126 293 263 245 94 301 271 104 239 223 116 168 156 330 93 165 106 139 107 139 85 173 200 290 146 199 128 193 109 195 224 329 149 153 316 137 111 319 114 144 133 213 145 201 245 134 214 255 178 164 197 112 306 132 240 145 152 110 95 170 89 130 106 199 182 153 131 232 293 178 167 182 145 174 128 152 192 172 106 171 94 286 183 195 568 237 267 152 263 144 119 124 152 159 84 169 149 159 249 440 223 298 161 152 136 269 275 188 124 185 134 379 244 177 115 105 103 207 269 123 130 109 130 269 151 124 125 223 142 95 184 528 201 232 152 141 189 179 261 202 203 93 273 133 155 276 150 101 119 253 396 252 162 141 121 206 119 198 125 161 154 120 116 226 202 426 156 205 191 108 135 93 99 379 152 140 141 226 180 146 141 144 273 68 216 306 95 180 128 155 127 166 87 167 407 128 157 178 103 166 152 221 147 66 113 135 209 289 217 265 136 247 98 436 131 259 246 125 150 189 153 238 101 155 82 105 137 288 179 238 141 418 194 105 122 207 128 170 264 289 174 121 190 203 111 148 266 334 102 214 177 307 118 108 228 168 176 345 92 159 106 292 184 192 168 184 191 226 135 121 198 86 327 327 165 119 117 158 136 270 110 201 192 104 182 275 92 262 195 231 165 102 160 161 241 122 207 135 112 169 98 155 315 118 138 259 178 248 201 293 158 315 201 187 144 143 226 207 117 149 191 153 277 166 112 124 227 195 162 135 164 286 95 188 113 132 117 126 403 122 288 141 106 146 347 267 121 137 334 119 144 345 228 115 171 187 99 92 120 131 173 166 132 351 133 204 208 117 180 262 176 119 248 179 149 160 198 104 200 220 145 206 146 246 126 249 240 196 368 160 196 365 219 205 122 122 224 143 240 171 251 171 149 97 408 251 127 172 154 217 281 118 357 145 126 207 242 216 134 171 109 139 177 131 142 201 123 124 147 80 166 93 120 83 156 111 115 422 132 154 107 249 102 107 174 140 218 147 84 283 79 262 202 314 235 174 83 222 138 143 193 184 314 101 199 185 129 148 154 182 289 82 363 134 103 161 218 327 101 191 111 174 276 294 179 163 167 296 212 142 196 152 103 288 285 105 150 119 86 153 140 178 216 179 164 382 88 467 291 104 308 247 416 97 259 158 158 152 206 163 283 157 191 119 132 121 156 269 152 133 177 330 165 150 194 105 161 199 86 269 127 109 105 117 148 393 176 206 316 130 103 205 106 422 167 185 203 123 136 145 138 259 143 98 143 117 300 239 106 196 101 174 373 158 171 373 106 190 105 221 399 208 199 108 100 144 272 76 226 259 135 71 171 313 110 232 103 162 155 221 112 105 193 146 173 147 351 145 105 223 303 255 239 246 104 82 192 137 116 127 403 336 120 203 115 161 116 313 253 141 209 326 146 163 342 101 134 234 178 290 173 179 155 329 241 172 241 136 226 140 186 245 128 193 191 109 68 214 118 113 167 172 180 118 244 175 411 52 302 97 121 131 147 171 174 188 203 163 180 125 175 89 287 309 119 145 390 140 236 104 132 322 89 113 231 152 364 141 316 114 205 148 232 228 266 233 185 204 183 205 319 251 174 160 152 95 270 98 159 258 290 115 131 114 122 169 155 122 104 123 106 366 202 75 115 122 170 147 286 247 181 122 199 136 217 399 206 119 100 126 116 427 156 178 225 380 126 168 166 153 79 156 195 241 175 174 145 84 260 223 203 198 183 108 154 162 140 130 91 200 397 486 100 157 190 185 128 145 118 339 238 107 112 270 163 315 147 256 217 163 125 159 145 328 134 243 239 136 246 111 285 473 100 87 80 200 245 210 215 91 239 77 171 118 121 111 95 326 86 146 175 356 155 244 111 231 138 275 147 185 156 190 170 129 177 209 227 161 206 120 261 383 220 79 221 189 138 134 135 82 230 367 165 106 179 225 129 74 171 178 137 81 195 122 144 162 224 110 187 155 269 171 250 286 156 166 140 134 106 296 143 102 285 308 94 163 163 97 119 91 160 135 201 135 402 111 162 266 374 113 130 295 303 213 93 512 188 292 161 63 265 88 165 238 138 285 362 153 93 113 204 521 427 166 85 145 92 116 348 246 155 276 122 226 93 328 86 177 261 94 265 216 209 123 133 302 199 136 139 282 157 173 208 395 95 199 166 139 105 310 125 91 174 102 99 128 134 93 137 165 240 170 254 165 138 208 146 175 232 97 191 144 204 76 233 100 154 149 156 191 380 113 225 228 195 391 337 202 178 107 243 219 185 164 218 267 304 240 159 168 155 109 136 84 265 280 81 106 91 116 220 352 224 191 140 132 131 96 418 219 142 228 167 312 132 122 370 101 168 328 312 149 136 360 205 170 114 171 139 238 110 189 347 166 102 74 336 158 214 249 79 131 291 124 105 140 97 190 270 367 210 285 173 159 115 173 233 84 144 151 131 103 286 136 148 301 140 124 203 118 158 181 130 311 299 145 159 156 108 138 156 248 323 254 140 111 195 150 153 200 396 177 143 127 220 228 181 114 93 201 67 347 180 134 174 96 163 127 159 148 147 174 105 157 225 206 348 333 311 113 313 274 181 151 150 107 108 182 170 225 147 151 267 264 135 178 162 154 109 114 95 153 158 107 169 80 84 173 262 214 168 373 200 325 171 179 192 153 193 116 211 254 243 119 148 185 152 171 286 396 129 296 165 396 173 184 112 302 165 157 347 297 233 94 119 120 177 359 132 188 230 209 102 147 250 204 149 158 197 67 86 487 198 252 176 84 253 106 233 119 163 186 97 275 238 245 325 214 185 162 193 206 102 277 89 145 102 190 155 389 135 87 185 80 219 164 242 187 138 118 187 219 107 129 274 155 159 120 115 283 260 147 190 159 222 212 293 114 119 200 173 232 169 230 134 116 352 337 107 207 195 136 109 90 252 206 221 384 307 180 115 119 161 218 100 212 286 336 205 92 220 66 204 183 253 128 166 256 228 75 79 371 109 222 181 214 126 145 82 105 112 214 160 104 150 275 124 169 413 122 224 218 175 117 185 116 115 252 132 280 235 266 170 139 80 73 257 200 308 80 147 129 234 145 95 415 271 128 111 289 371 228 142 90 183 136 285 129 115 329 427 184 171 172 189 248 175 201 283 95 168 165 165 274 186 209 193 157 218 100 514 277 127 144 228 244 373 183 205 131 145 230 89 78 162 153 266 197 68 78 66 337 137 128 473 182 181 224 246 117 157 167 230 249 665 131 267 414 130 113 85 275 152 108 227 392 194 82 206 142 136 118 213 104 152 116 129 188 190 134 122 133 246 130 133 153 168 301 199 365 221 103 240 343 222 223 169 224 80 243 193 144 240 219 175 273 165 174 132 313 188 223 156 128 107 242 140 546 97 112 181 176 132 168 314 137 179 141 129 87 127 171 171 97 146 117 122 86 214 88 83 366 201 105 374 124 181 194 97 69 122 124 150 149 325 137 236 145 203 80 160 299 164 192 93 193 326 148 199 158 177 105 74 255 354 159 215 144 119 80 149 209 272 333 230 287 149 235 121 215 246 86 163 206 206 155 160 146 189 169 191 123 387 283 166 91 169 233 223 229 68 235 371 354 242 398 189 65 224 315 88 179 113 106 199 436 106 189 135 232 255 96 382 101 97 280 90 195 67 87 55 379 117 294 169 164 166 196 169 177 119 151 157 614 362 176 174 381 370 130 285 94 102 170 161 142 280 89 84 111 126 224 119 153 332 183 231 246 112 234 177 204 134 155 240 174 179 196 258 314 161 139 88 134 227 233 178 142 275 194 123 141 94 135 105 144 193 134 468 177 92 135 345 390 360 145 358 67 192 111 144 129 191 191 202 97 185 112 107 205 140 164 369 438 88 100 174 67 67 66 72 304 143 170 179 81 226 91 219 246 295 139 166 171 167 215 203 248 278 174 219 117 240 200 116 285 126 143 203 126 170 218 146 148 139 250 315 67 145 112 137 137 136 157 150 85 126 249 220 94 213 194 257 424 111 177 201 317 212 170 258 165 279 240 161 323 246 263 182 200 125 149 258 255 304 285 124 226 245 219 438 74 287 275 274 144 257 144 176 74 207 141 111 106 237 116 155 86 60 58 73 393 243 338 193 158 193 250 136 185 105 153 231 258 151 317 154 134 94 156 403 284 143 174 166 206 171 177 235 113 80 187 118 231 246 102 84 84 130 152 295 140 168 504 208 209 468 222 263 122 334 153 200 116 191 126 87 152 110 170 195 148 181 116 178 141 155 159 100 352 238 354 204 242 111 579 525 216 102 167 86 253 174 183 191 77 146 93 83 195 113 85 111 222 234 104 139 253 93 58 78 430 73 332 209 128 185 111 199 241 163 221 350 120 246 157 329 316 111 212 114 121 133 247 135 82 211 212 204 260 272 216 312 264 273 105 278 93 150 115 191 184 217 161 92 171 536 201 139 243 103 297 232 237 145 208 190 236 181 125 510 230 134 130 203 285 119 289 141 549 158 138 169 256 212 82 257 147 95 334 140 168 108 181 161 149 140 126 163 248 245 151 83 231 61 56 61 72 67 54 136 316 199 232 128 172 123 121 109 127 214 171 273 209 436 633 477 417 81 260 215 77 152 167 146 82 121 121 136 166 219 155 84 250 142 149 213 241 107 215 102 168 244 89 123 96 259 290 293 245 234 109 300 397 242 155 235 165 154 195 130 137 212 102 184 127 289 271 123 152 448 196 288 246 120 376 191 108 168 133 122 214 170 312 134 177 339 121 371 80 142 255 134 234 65 60 101 64 51 74 112 383 75 222 153 223 167 283 268 136 104 354 218 112 330 128 175 320 188 230 260 205 249 179 98 121 148 96 232 216 190 233 290 306 127 178 168 306 102 199 259 163 237 269 305 305 180 207 129 119 224 91 122 136 103 139 181 349 175 241 175 160 186 283 148 266 291 179 441 177 206 140 105 144 285 314 434 129 104 96 194 179 218 125 221 124 170 156 159 181 291 280 83 197 64 64 84 65 63 67 130 328 303 131 163 136 134 154 165 130 240 128 176 198 149 220 137 123 429 173 199 157 349 145 181 294 288 178 230 104 158 125 314 364 211 153 77 230 286 189 123 283 179 178 118 124 829 269 239 227 106 183 116 123 214 119 161 94 80 88 141 178 121 164 410 147 125 242 182 250 134 239 220 170 196 475 229 174 400 256 356 252 144 72 294 139 128 117 237 236 149 83 112 91 212 101 89 62 64 77 139 507 367 233 116 366 82 276 135 179 110 262 232 208 381 390 107 370 107 226 155 125 126 96 159 202 213 285 208 168 131 377 84 270 144 203 252 242 276 204 150 247 417 74 404 383 221 151 136 320 149 133 130 67 90 264 112 118 246 223 203 145 206 248 269 311 336 112 330 142 152 459 309 101 101 140 169 194 258 181 256 260 74 73 127 139 141 111 174 71 118 228 67 61 60 68 55 52 60 117 51 ================================================ FILE: long/poster.markdown ================================================ # FunTester广告位分享 由于公众号资源有限,所以暂定每周一篇头条软文投放,为了方便特写此文档. 日期周一代表当周. ps:有兴趣可以一起聊一聊文末和次条. 转载事宜直接微信联系 [原创汇总,每周更新](https://gitee.com/fanapi/tester/blob/okay/document/directory.markdown) |日期(周一)|状态|代号|付款| |----|----|----|-----| |12.7|已预订| L+C|已付款| |12.14|已预订|L|已付款| |12.21|已预订|N|已付款| |12.28|已预订|L|已付款| |1.4|已预订|N|被鸽| |1.4|已预订|C|未付款| |1.11|已预订|M|被鸽| |1.11|已预订|M|已付款| |1.18|未预定||| |1.25|未预定||| |2.22|已预定|七七八十一|已付款| |3.1|未预定|虚位以待|| |3.8|已预定|花卷|已付款| |3.15|虚位以待|| |3.22|已预定|花卷|未付款 |3.29|未预定|虚位以待|| |4.5|未预定|虚位以待|| |4.12|已预订|花卷|未付款| |4.20|已预订|花卷|未付款| |4.26|未预定|虚位以待|未付款| ================================================ FILE: long/sql/performance.sql ================================================ /* Navicat Premium Data Transfer Source Server : test Source Server Type : MySQL Source Server Version : 50640 Source Host : 172.18.4.55:3306 Source Schema : okayapi Target Server Type : MySQL Target Server Version : 50640 File Encoding : 65001 Date: 02/01/2020 18:13:16 */ SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for performance -- ---------------------------- DROP TABLE IF EXISTS `performance`; CREATE TABLE `performance` ( `id` int(10) NOT NULL AUTO_INCREMENT, `threads` int(4) DEFAULT NULL COMMENT '线程数', `rt` int(5) DEFAULT NULL COMMENT '平均响应时间,ms', `qps` double(10,4) DEFAULT NULL COMMENT 'QPS处理能力 /s', `error` double(10,4) DEFAULT NULL COMMENT '错误率', `fail` double(10,4) DEFAULT NULL COMMENT '失败率', `des` varchar(1000) DEFAULT NULL COMMENT '任务描述', `total` int(10) DEFAULT NULL COMMENT '总请求次数', `start_time` timestamp NULL DEFAULT NULL, `end_time` timestamp NULL DEFAULT NULL, `create_time` timestamp NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP, PRIMARY KEY (`id`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=91 DEFAULT CHARSET=utf8; SET FOREIGN_KEY_CHECKS = 1; ================================================ FILE: long/sql/request.sql ================================================ /* Navicat Premium Data Transfer Source Server : test Source Server Type : MySQL Source Server Version : 50640 Source Host : 172.18.4.55:3306 Source Schema : okayapi Target Server Type : MySQL Target Server Version : 50640 File Encoding : 65001 Date: 02/01/2020 18:14:01 */ SET NAMES utf8mb4; SET FOREIGN_KEY_CHECKS = 0; -- ---------------------------- -- Table structure for request -- ---------------------------- DROP TABLE IF EXISTS `request`; CREATE TABLE `request` ( `id` int(10) NOT NULL AUTO_INCREMENT, `type` varchar(6) DEFAULT NULL, `method` varchar(6) DEFAULT NULL, `domain` varchar(100) DEFAULT NULL, `api` varchar(100) DEFAULT NULL, `status` int(10) DEFAULT NULL, `code` int(10) DEFAULT NULL, `expend_time` double(10,2) DEFAULT NULL, `data_size` int(6) DEFAULT NULL, `local_ip` varchar(20) DEFAULT NULL, `local_name` varchar(20) DEFAULT NULL, `create_time` varchar(100) DEFAULT NULL, PRIMARY KEY (`id`) USING BTREE ) ENGINE=InnoDB AUTO_INCREMENT=122167 DEFAULT CHARSET=utf8; SET FOREIGN_KEY_CHECKS = 1; ================================================ FILE: readme.markdown ================================================ # 分支简介 该分支为主分支,其他分支停止更新. > **FunTester**,[腾讯云年度作者,优秀讲师 | 腾讯云+社区权威认证](https://mp.weixin.qq.com/s/oeTeJZs6h4jJJMRyUunurw),非著名测试开发,欢迎关注。 ## [GitHub地址](https://github.com/JunManYuanLong/FunTester) 联系地址:FunTester@88.com # [**570+原创文章**](/document/directory.markdown) # [**接口篇**](/document/api.markdown) # [**基础篇**](/document/base.markdown) # [**升级篇**](/document/update.markdown) # [**7788篇**](/document/7788.markdown) * 文章链接可能无法访问,各位看官移步GitHub地址即可. [FunTester测试框架架构图](http://pic.automancloud.com/structure.html) [FunTester测试项目架构图](http://pic.automancloud.com/project.html) ![FunTester测试框架架构图](http://pic.automancloud.com/structure.png) ![FunTester测试项目架构图](http://pic.automancloud.com/project.png) ================================================ FILE: settings.gradle ================================================ rootProject.name = 'funtester' ================================================ FILE: src/main/groovy/com/funtester/base/bean/AbstractBean.groovy ================================================ package com.funtester.base.bean import com.alibaba.fastjson.JSON import com.alibaba.fastjson.JSONObject import com.funtester.config.Constant import com.funtester.frame.Save import com.funtester.frame.SourceCode import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.Logger /** * bean的基类 */ abstract class AbstractBean extends Constant{ static final Logger logger = LogManager.getLogger(AbstractBean.class) /** * 将bean转化为json,为了进行数据处理和打印 * * @return */ JSONObject toJson() { JSONObject.parseObject(JSONObject.toJSONString(this)) } /** * 文本形式保存 */ def save() { Save.saveJson(this.toJson(), this.getClass().toString() + SourceCode.getMark()); } /** * 控制台打印,使用WARN记录,以便查看 */ def print() { logger.warn(this.getClass().toString() + ":" + this.toString()); } def initFrom(String str) { JSONObject.parseObject(str, this.getClass()) } def initFrom(Object str) { initFrom(JSON.toJSONString(str)) } def copyFrom(AbstractBean source) { JSON.parseObject(JSON.toJSONString(source), source.class) } def copyTo(AbstractBean target) { JSON.parseObject(JSON.toJSONString(this, target.class)) } /** * 这里bean的属性必需是可以访问的,不然会返回空json串 * @return */ @Override String toString() { JSONObject.toJSONString(this) } @Override protected Object clone() { initFrom(this) } } ================================================ FILE: src/main/groovy/com/funtester/base/bean/PerformanceResultBean.groovy ================================================ package com.funtester.base.bean import com.funtester.db.mysql.MySqlTest import com.funtester.frame.Output import com.funtester.utils.DecodeEncode /** * 性能测试结果集 */ class PerformanceResultBean extends AbstractBean implements Serializable { private static final long serialVersionUID = -1595942562342357L; /** * 测试用例描述 */ String mark /** * 开始时间 */ String startTime /** * 结束时间 */ String endTime /** * 表格信息 */ String table /** * 线程数 */ int threads /** * 总请求次数 */ int total /** * 平均响应时间 */ int rt /** * 吞吐量,公式为QPS=Thead/avg(time) */ double qps /** * 通过QPS=count(r)/T公式计算得到的QPS,在固定QPS模式中,这个值来源于预设QPS */ double qps2 /** * 理论误差,两种统计模式 */ String deviation /** * 错误率 */ double errorRate /** * 失败率 */ double failRate /** * 执行总数 */ int executeTotal 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) { this.mark = mark this.startTime = startTime this.endTime = endTime this.threads = threads this.total = total this.rt = rt this.qps = qps this.qps2 = qps2 this.errorRate = errorRate this.failRate = failRate this.executeTotal = executeTotal this.table = DecodeEncode.zipBase64(table) this.deviation = com.funtester.frame.SourceCode.getPercent(Math.abs(qps - qps2) * 100 / Math.max(qps, qps2)) Output.output(this.toJson()) Output.output(table) MySqlTest.savePerformanceBean(this) } } ================================================ FILE: src/main/groovy/com/funtester/base/bean/RecordBean.groovy ================================================ package com.funtester.base.bean import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.Logger /** * 测试记录的bean */ class RecordBean extends AbstractBean implements Serializable{ private static final long serialVersionUID = -159594234325649847L; static Logger logger = LogManager.getLogger(RecordBean.class) String domain; String type; String api; long expend_time; int data_size; int status; int code; String method; String local_ip; String local_name; String create_time; static RecordBean get() { new RecordBean() } RecordBean setDomain(String domain) { this.domain = domain this } RecordBean setType(String type) { this.type = type this } RecordBean setApi(String api) { this.api = api this } RecordBean setExpend_time(long expend_time) { this.expend_time = expend_time this } RecordBean setData_size(int data_size) { this.data_size = data_size this } RecordBean setStatus(int status) { this.status = status this } RecordBean setCode(int code) { this.code = code this } RecordBean setMethod(String method) { this.method = method this } RecordBean setLocal_ip(String local_ip) { this.local_ip = local_ip this } RecordBean setLocal_name(String local_name) { this.local_name = local_name this } RecordBean setCreate_time(String create_time) { this.create_time = create_time this } @Override def print() { logger.info "接口:{},响应时间{}", api, expend_time } } ================================================ FILE: src/main/groovy/com/funtester/base/bean/RequestInfo.groovy ================================================ package com.funtester.base.bean import com.alibaba.fastjson.JSONObject import com.funtester.base.interfaces.MarkRequest import com.funtester.config.Constant import com.funtester.config.RequestType import com.funtester.config.SysInit import org.apache.http.Header import org.apache.http.HttpEntity import org.apache.http.client.methods.HttpEntityEnclosingRequestBase import org.apache.http.client.methods.HttpRequestBase import org.apache.http.util.EntityUtils import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.Logger /** * 请求信息封装类 */ class RequestInfo extends AbstractBean implements Serializable { private static final long serialVersionUID = 5942566988949859847L; private static Logger logger = LogManager.getLogger(RequestInfo.class) /** * 请求信息的标记字段,用于日志记录请求 */ private static MarkRequest mark; static void initMark(MarkRequest markRequest) { mark = markRequest; } /** * 接口地址 */ String apiName /** * 请求的url */ String url /** * 请求的uri */ String uri /** * 方法,get/post */ RequestType method /** * 域名 */ String host /** * 协议类型 */ String type /** * 参数 */ String params /** * host是否是黑名单 */ boolean isBlack; /** * 所有的请求header,会去重 */ JSONObject headers /** * 存一下 */ HttpRequestBase request /** * 通过request获取请求的相关信息,并输出部分信息 * * @param request */ RequestInfo(HttpRequestBase request) { this.request = request getRequestInfo() } /** * 封装获取请求的各种信息的方法 * * @param request 传入请求对象 * @return 返回一个map,包含api_name,host_name,type,method,params */ private void getRequestInfo() { method = RequestType.getRequestType request.getMethod() uri = request.getURI().toString()// 获取uri getRequestUrl(uri) String one = url.substring(url.indexOf("//") + 2)// 删除掉http:// apiName = one.substring(one.indexOf("/"))// 获取接口名 host = one.substring(0, one.indexOf("/"))// 获取host地址 isBlack = SysInit.isBlack(host) type = url.substring(0, url.indexOf("//") - 1)// 获取协议类型 if (method == RequestType.GET) { if (!uri.contains(UNKNOW)) return params = uri.substring(uri.indexOf(UNKNOW) + 1) } else if (method == RequestType.POST) { getPostRequestParams(request) } List
list = Arrays.asList(request.getAllHeaders()) headers = new JSONObject() { { list.each { put(it.name, it.value) } } } } /** * 获取请求url,遇到get请求,先截取 * * @param uri */ private void getRequestUrl(String uri) { url = uri.contains(UNKNOW) ? uri.substring(0, uri.indexOf(UNKNOW)) : uri } /** * 获取响应实体,post path,put方法适用 * * @param request */ private void getPostRequestParams(HttpEntityEnclosingRequestBase request) { HttpEntity entity = request.getEntity()// 获取实体 if (entity == null) return try { params = EntityUtils.toString(entity)// 解析实体 EntityUtils.consume(entity)// 确保实体消耗 } catch (Exception e) { logger.warn("获取post请求参数时异常!") params = "entity类型:" + entity.getClass() } } boolean isBlack() { isBlack } String mark() { mark == null ? Constant.EMPTY : mark.mark(request) } @Override String toString() { this.toJson().toString() } } ================================================ FILE: src/main/groovy/com/funtester/base/bean/Result.groovy ================================================ package com.funtester.base.bean import com.alibaba.fastjson.JSONObject import com.funtester.base.interfaces.ReturnCode import com.funtester.config.Constant /** * 通用的返回体 * 配合moco框架使用 * @param < T > */ class Result extends AbstractBean implements Serializable{ private static final long serialVersionUID = -196371159847L; /** * code码 */ int code /** * 返回信息 */ T data Result(int code, T data) { this.code = code this.data = data } /** * 返回简单的响应 * @param c */ Result(ReturnCode errorCode) { this(errorCode.getCode(), errorCode.getDesc()) } def Result() { } /** * 返回成功响应内容 * @param data * @return */ static Result success(T data) { new Result(0, data) } static Result success() { new Result() } static Result build(ReturnCode errorCode) { new Result(errorCode) } static Result build(int code, String msg) { new Result(code, msg) } static Result build(List listData) { success([list: listData] as JSONObject) } /** * 返回通用失败的响应内容 * @param data * @return */ static Result fail(T data) { new Result(Constant.TEST_ERROR_CODE, data) } static Result fail() { new Result(Constant.TEST_ERROR_CODE) } static Result fail(ReturnCode errorCode) { new Result(errorCode) } /** * 是否成功响应 * @return */ boolean isSuccess() { code == 0 } } ================================================ FILE: src/main/groovy/com/funtester/base/bean/VerifyBean.groovy ================================================ package com.funtester.base.bean import com.alibaba.fastjson.JSON import com.funtester.base.exception.ParamException import com.funtester.config.VerifyType import com.funtester.utils.JsonUtil import com.funtester.utils.Regex import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.Logger /** * 验证对象类 */ class VerifyBean extends AbstractBean implements Serializable, Cloneable { private static Logger logger = LogManager.getLogger(VerifyBean.class) private static final long serialVersionUID = -1595942567071153982L; VerifyType type /** * 验证语法 */ String verify /** * 待验证内容 */ String value String des boolean isVerify boolean result; VerifyBean(String verify, String value, String des) { this.value = value this.des = des def split = verify.split(REG_PART, 2) this.verify = split[1] this.type = VerifyType.getRequestType(split[0]) } /** * 用于进行自身验证,会进行isverify和result记录 * @return */ boolean verify() { if (isVerify && result) return result isVerify = true result = verify(value) result } /** * 用于进行对象外String验证,不会修改对象属性 * @param val * @return */ boolean verify(String val) { boolean res try { switch (type) { case VerifyType.CONTAIN: res = val.contains(verify) return res case VerifyType.REGEX: res = Regex.isRegex(val, verify) return res case VerifyType.JSONPATH: def split = verify.split(REG_PART, 2) def path = split[0] def v = split[1] def instance = JsonUtil.getInstance(JSON.parseObject(val)) res = instance.getVerify(path).fit(v) return res case VerifyType.HANDLE: def sp = verify.split(REG_PART, 2) def path = sp[0] def ve = sp[1] def instance = JsonUtil.getInstance(JSON.parseObject(val)) res = instance.getVerify(path).fitFun(ve) return res default: ParamException.fail("验证类型参数错误!") } } catch (Exception e) { logger.warn("验证出现问题: {}", e.getMessage()) res = false } finally { /*这里Groovy可以这么写,但是Java不能这么写,因为需要有返回值*/ logger.info("verify对象 {} ,验证结果: {}", verify, res) } } @Override def print() { logger.info("{} 验证结果: {}", des, result) } @Override VerifyBean clone() { new VerifyBean(this.verify, this.value, this.des) } } ================================================ FILE: src/main/groovy/com/funtester/base/constaint/CaseBase.java ================================================ package com.funtester.base.constaint; import com.alibaba.fastjson.JSONObject; import com.funtester.db.mysql.MySqlTest; import com.funtester.frame.SourceCode; /** * 用例虚拟类 */ public abstract class CaseBase extends SourceCode { /** * 保存测试用例的执行结果 * * @param label 测试用例的标签 * @param result 测试用例结果 */ public void saveResult(String label, JSONObject result) { MySqlTest.saveTestResult(label, result); } /** * 前置处理 * * @return */ public abstract boolean before(); /** * 后置处理 * * @return */ public abstract boolean after(); /** * 初始化 * * @return */ public abstract boolean init(); } ================================================ FILE: src/main/groovy/com/funtester/base/constaint/FixedQpsThread.java ================================================ package com.funtester.base.constaint; import com.funtester.base.interfaces.MarkThread; import com.funtester.config.HttpClientConstant; import com.funtester.frame.execute.FixedQpsConcurrent; import com.funtester.utils.Time; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; public abstract class FixedQpsThread extends ThreadBase { private static Logger logger = LogManager.getLogger(FixedQpsThread.class); public int qps; /** * 根据属性isTimesMode判断,次数或者时间(单位ms) */ public int limit; public boolean isTimesMode; public FixedQpsThread(F f, int limit, int qps, MarkThread markThread, boolean isTimesMode) { this.limit = limit; this.qps = qps; this.mark = markThread; this.f = f; this.isTimesMode = isTimesMode; } protected FixedQpsThread() { super(); } @Override public void run() { try { before(); threadmark = this.mark == null ? EMPTY : this.mark.mark(this); long s = Time.getTimeStamp(); doing(); long e = Time.getTimeStamp(); FixedQpsConcurrent.executeTimes.getAndIncrement(); int diff = (int) (e - s); FixedQpsConcurrent.allTimes.add(diff); if (diff > HttpClientConstant.MAX_ACCEPT_TIME) FixedQpsConcurrent.marks.add(diff + CONNECTOR + threadmark + CONNECTOR + Time.getNow()); } catch (Exception e) { FixedQpsConcurrent.errorTimes.getAndIncrement(); logger.warn("执行任务失败!,标记:{}", threadmark, e); } finally { after(); } } @Override public void before() { } } ================================================ FILE: src/main/groovy/com/funtester/base/constaint/ThreadBase.java ================================================ package com.funtester.base.constaint; import com.funtester.base.interfaces.MarkThread; import com.funtester.frame.SourceCode; import com.funtester.httpclient.FunLibrary; import java.io.Serializable; import java.util.ArrayList; import java.util.List; import java.util.concurrent.CountDownLatch; import java.util.stream.Collectors; /** * 多线程任务基类,可单独使用 * * @param 必需实现Serializable */ public abstract class ThreadBase extends SourceCode implements Runnable, Serializable { private static final long serialVersionUID = -1282879464717720145L; /** * 全局的时间终止开关,true表示终止,false表示不终止. */ private static boolean ABORT = false; /** * 线程的名字 */ public String threadName; /** * 线程标记对象,用户标记请求或者单次执行任务的 */ public String threadmark; /** * 错误数 *

这里注意使用{@link FunLibrary#getHttpResponse(org.apache.http.client.methods.HttpRequestBase)}方法获取响应的功能封装方法,即使报错也不会抛异常.这样会导致errorNum错误数为零

*/ public int errorNum; /** * 执行数,一般与响应时间记录数量相同 */ public int executeNum; /** * 计数锁 *

* 会在concurrent类里面根据线程数自动设定 *

*/ protected CountDownLatch countDownLatch; /** * 标记对象 */ public MarkThread mark; /** * 用于设置访问资源,用于闭包中无法访问包外实例对象的情况,这里还有一个用处就是在标记线程对象的时候,用到了这个t(参数标记模式中) * * @since 2020年10月19日, 统一用来设置HTTPrequestbase对象.同样可以用于执行SQL和redis查询语句或者对象, 暂未使用dubbo尝试 */ public F f; protected ThreadBase() { } /** * 记录所有超时的请求标记 */ public List marks = new ArrayList<>(); /** * 用于存储请求耗时集合 * 2021年03月16日,将统计集合提取为对象属性,用于外部访问,可用于取样器实现 */ public List costs = new ArrayList<>(); /** * 运行待测方法的之前的准备 */ public void before() { ABORT = false; } /** * 待测方法 * * @throws Exception 抛出异常后记录错误次数,一般在性能测试的时候重置重试控制器不再重试 */ protected abstract void doing() throws Exception; /** * 运行待测方法后的处理 */ protected void after() { costs = new ArrayList<>(); marks = new ArrayList<>(); if (countDownLatch != null) countDownLatch.countDown(); } /** * 设置计数器 * * @param countDownLatch */ public void setCountDownLatch(CountDownLatch countDownLatch) { this.countDownLatch = countDownLatch; } /** * 拷贝对象方法,用于统计单一对象多线程调用时候的请求数和成功数,对于的复杂情况,需要将T类型也重写clone方法 * *

* 此处若具体实现类而非虚拟类建议自己写clone方法,子类重写需注意{@link ThreadBase#initBase()}方法调用 *

* * @return */ @Override public abstract ThreadBase clone(); /** * 用于对象拷贝之后,清空存储列表 */ public void initBase() { this.costs = new ArrayList<>(); this.marks = new ArrayList<>(); } /** * 线程任务是否需要提前关闭,默认返回false *

* 一般用于单线程错误率过高的情况 *

* * @return */ public boolean status() { return false; } /** * Groovy乘法调用方法 * * @param num * @return */ public List multiply(int num) { return range(num).mapToObj(x -> this.clone()).collect(Collectors.toList()); } /** * 用于在某些情况下提前终止测试 */ public static void stop() { ABORT = true; } /** * true表示终止,false表示不终止. * * @return */ public static boolean needAbort() { return ABORT; } } ================================================ FILE: src/main/groovy/com/funtester/base/constaint/ThreadLimitTimeCount.java ================================================ package com.funtester.base.constaint; import com.funtester.base.interfaces.MarkThread; import com.funtester.config.HttpClientConstant; import com.funtester.frame.execute.Concurrent; import com.funtester.httpclient.GCThread; import com.funtester.utils.Time; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import java.util.ArrayList; import java.util.List; /** * 请求时间限制的多线程类,限制每个线程执行的时间 *

* 通常在测试某项用例固定时间的场景下使用,可以提前终止测试用例 *

* * @param 闭包参数传递使用,Groovy脚本会有一些兼容问题,部分对象需要tostring获取参数值 */ public abstract class ThreadLimitTimeCount extends ThreadBase { private static final long serialVersionUID = -7017995186493855741L; private static final Logger logger = LogManager.getLogger(ThreadLimitTimeCount.class); public List marks = new ArrayList<>(); /** * 任务请求执行时间,单位是ms秒 */ public int time; public ThreadLimitTimeCount(F f, int time, MarkThread markThread) { this.time = time * 1000; this.f = f; this.mark = markThread; } protected ThreadLimitTimeCount() { super(); } @Override public void run() { try { before(); long ss = Time.getTimeStamp(); while (true) { try { threadmark = mark == null ? EMPTY : this.mark.mark(this); long s = Time.getTimeStamp(); doing(); long et = Time.getTimeStamp(); executeNum++; int diff =(int) (et - s); costs.add(diff); if (diff > HttpClientConstant.MAX_ACCEPT_TIME) marks.add(diff + CONNECTOR + threadmark + CONNECTOR + Time.getNow()); if ((et - ss) > time || status() || ThreadBase.needAbort()) break; } catch (Exception e) { logger.warn("执行任务失败!", e); logger.warn("执行失败对象的标记:{}", threadmark); errorNum++; } } long ee = Time.getTimeStamp(); logger.info("线程:{},执行次数:{}, 失败次数: {},总耗时: {} s", threadName, executeNum, errorNum, (ee - ss) / 1000.0); Concurrent.allTimes.addAll(costs); Concurrent.requestMark.addAll(marks); } catch (Exception e) { logger.warn("执行任务失败!", e); } finally { after(); } } public boolean status() { return errorNum > 10; } /** * 运行待测方法的之前的准备 */ @Override public void before() { super.before(); } @Override protected void after() { super.after(); GCThread.stop(); } } ================================================ FILE: src/main/groovy/com/funtester/base/constaint/ThreadLimitTimesCount.java ================================================ package com.funtester.base.constaint; import com.funtester.base.interfaces.MarkThread; import com.funtester.config.HttpClientConstant; import com.funtester.frame.execute.Concurrent; import com.funtester.httpclient.GCThread; import com.funtester.utils.Time; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; /** * 请求时间限制的多线程类,限制每个线程执行的次数 * *

* 通常在测试某项用例固定时间的场景下使用,可以提前终止测试用例 *

* * @param 闭包参数传递使用,Groovy脚本会有一些兼容问题,部分对象需要tostring获取参数值 */ public abstract class ThreadLimitTimesCount extends ThreadBase { private static final long serialVersionUID = -4617192188292407063L; private static final Logger logger = LogManager.getLogger(ThreadLimitTimesCount.class); /** * 任务请求执行次数 */ public int times; public ThreadLimitTimesCount(F f, int times, MarkThread markThread) { this.times = times; this.f = f; this.mark = markThread; } protected ThreadLimitTimesCount() { super(); } @Override public void run() { try { before(); long ss = Time.getTimeStamp(); for (int i = 0; i < times; i++) { try { threadmark = mark == null ? EMPTY : this.mark.mark(this); long s = Time.getTimeStamp(); doing(); long e = Time.getTimeStamp(); executeNum++; int diff =(int) (e - s); costs.add(diff); if (diff > HttpClientConstant.MAX_ACCEPT_TIME) marks.add(diff + CONNECTOR + threadmark + CONNECTOR + Time.getNow()); if (status() || ThreadBase.needAbort()) break; } catch (Exception e) { logger.warn("执行任务失败!", e); logger.warn("执行失败对象的标记:{}", threadmark); errorNum++; } } long ee = Time.getTimeStamp(); logger.info("线程:{},执行次数:{},错误次数: {},总耗时:{} s", threadName, times, errorNum, (ee - ss) / 1000.0); Concurrent.allTimes.addAll(costs); Concurrent.requestMark.addAll(marks); } catch (Exception e) { logger.warn("执行任务失败!", e); } finally { after(); } } /** * 运行待测方法的之前的准备 */ @Override public void before() { super.before(); } @Override public boolean status() { return errorNum > 10; } @Override protected void after() { super.after(); GCThread.stop(); } } ================================================ FILE: src/main/groovy/com/funtester/base/exception/FailException.java ================================================ package com.funtester.base.exception; import com.funtester.config.Constant; /** * 自定义异常基类 */ public class FailException extends RuntimeException { private static final long serialVersionUID = -7041169491254546905L; public FailException() { super(Constant.DEFAULT_STRING); } protected FailException(String message) { super(message); } public static void fail(String message) { throw new FailException(message); } /** * 默认抛异常,多用于调试 */ public static void fail() { throw new FailException(); } /** * 将检查异常修改为运行异常 * * @param e */ public static void fail(Exception e) { throw new FailException(e.getMessage()); } } ================================================ FILE: src/main/groovy/com/funtester/base/exception/LoginException.java ================================================ package com.funtester.base.exception; import com.alibaba.fastjson.JSONObject; /** * 处理项目中的登录异常 */ public class LoginException extends FailException { private static final long serialVersionUID = 8674617502387938483L; private LoginException() { super(); } private LoginException(String message) { super(message); } public static void fail(String message) { throw new LoginException(message); } /** * 用于处理记录登录响应结果的抛异常方法 * * @param response 登录接口响应结果 */ public static void fail(JSONObject response) { fail(response.toJSONString()); } } ================================================ FILE: src/main/groovy/com/funtester/base/exception/ParamException.java ================================================ package com.funtester.base.exception; import com.alibaba.fastjson.JSONObject; /** * 参数错误运行异常类 */ public class ParamException extends FailException { private static final long serialVersionUID = -5079364420579956243L; private ParamException() { super(); } private ParamException(String message) { super(message); } public static void fail(String message) { throw new ParamException(message); } public static void fail(JSONObject message) { throw new ParamException(message.toJSONString()); } } ================================================ FILE: src/main/groovy/com/funtester/base/exception/RequestException.java ================================================ package com.funtester.base.exception; import org.apache.http.client.methods.HttpRequestBase; /** * 用于处理请求异常 */ public class RequestException extends FailException { private static final long serialVersionUID = 7916010541762451964L; private RequestException() { super(); } private RequestException(HttpRequestBase request) { super(request.toString()); } public RequestException(String message) { super(message); } public static void fail(HttpRequestBase base) { throw new RequestException(base); } public static void fail(String message) { throw new RequestException(message); } } ================================================ FILE: src/main/groovy/com/funtester/base/exception/VerifyException.java ================================================ package com.funtester.base.exception; import com.alibaba.fastjson.JSONObject; import com.funtester.frame.SourceCode; import com.funtester.httpclient.FunRequest; import org.apache.http.client.methods.HttpRequestBase; /** * 用于处理验证过程中的异常 */ public class VerifyException extends FailException { private static final long serialVersionUID = 7916010541762451964L; private VerifyException() { super(); } private VerifyException(HttpRequestBase request) { super(request.toString()); } private VerifyException(String message) { super(message); } public static void fail(String message) { SourceCode.getiMessage().sendBusinessMessage(); throw new VerifyException(message); } public static void fail(JSONObject message) { SourceCode.getiMessage().sendBusinessMessage(); fail(message.toJSONString()); } public static void fail(HttpRequestBase request) { SourceCode.getiMessage().sendBusinessMessage(); fail(FunRequest.initFromRequest(request).toString()); } } ================================================ FILE: src/main/groovy/com/funtester/base/interfaces/IBase.java ================================================ package com.funtester.base.interfaces; import com.alibaba.fastjson.JSONObject; import com.funtester.base.bean.RequestInfo; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpRequestBase; import java.io.File; /** * 每个项目需要重写的方法 */ public interface IBase { /** * 获取get请求对象 * * @param url * @return */ HttpGet getGet(String url); /** * 获取get请求对象 * * @param url * @param arg * @return */ HttpGet getGet(String url, JSONObject arg); /** * 获取post请求对象 * * @param url * @return */ HttpPost getPost(String url); /** * 获取post请求对象 * * @param url * @param params * @return */ HttpPost getPost(String url, JSONObject params); /** * 获取post请求对象 * * @param url * @param params * @param file * @return */ HttpPost getPost(String url, JSONObject params, File file); /** * 获取响应 * * @param request * @return */ JSONObject getResponse(HttpRequestBase request); /** * 获取响应 * * @param url * @return */ JSONObject getGetResponse(String url); /** * 获取响应 * * @param url * @param args * @return */ JSONObject getGetResponse(String url, JSONObject args); /** * 获取响应 * * @param url * @return */ JSONObject getPostResponse(String url); /** * 获取响应 * * @param url * @param params * @return */ JSONObject getPostResponse(String url, JSONObject params); /** * 获取响应 * * @param url * @param params * @param file * @return */ JSONObject getPostResponse(String url, JSONObject params, File file); /** * 校验响应正确性 *

* 用于处理响应结果,一般校验json的必要层级和响应码 *

* * @param response * @return */ boolean isRight(JSONObject response); /** * 检查响应是否符合标准 *

* 会在FunLibrary类使用,如果没有ibase对象,会默认返回test_error_code * requestinfo主要用于校验该请求是否需要校验,黑名单有配置black_host提供 *

* * @param response 响应json * @param requestInfo 请求info * @return */ int checkCode(JSONObject response, RequestInfo requestInfo); /** * 登录 */ void login(); /** * 设置header */ void setHeaders(HttpRequestBase request); /** * 处理响应结果 * * @param response */ void handleResponseHeader(JSONObject response); /** * 获取公共的登录参数 * * @return */ JSONObject getParams(); /** * 初始化对象,从json数据中,一般指cookie *

* 主要用于new了新的对象之后,然后赋值的操作,场景是从另外一个服务的对象拷贝到现在的对象,区别于clone,因为可能还会涉及其他的验证,所以单独写出一个方法,极少用到 *

*/ void init(JSONObject info); /** * 记录请求 */ void recordRequest(HttpRequestBase base); /** * 获取请求,用于并发 * * @return */ HttpRequestBase getRequest(); /** * 输出JSON格式的响应结果,用于统一屏蔽打印或者不打印响应内容 * * @param response */ public void print(JSONObject response); /** * 打印所有的请求header,此处功能与print响应类似,需要用一个开关控制 * * @param request */ public void printHeader(HttpRequestBase request); /** * 打印所有的响应header,此处功能与print响应类似,需要用一个开关控制 * * @param response */ public void printHeader(CloseableHttpResponse response); } ================================================ FILE: src/main/groovy/com/funtester/base/interfaces/IMessage.java ================================================ package com.funtester.base.interfaces; public interface IMessage { /** * 发送系统异常 */ public void sendSystemMessage(); /** * 发送功能异常 */ public void sendFunctionMessage(); /** * 发送业务异常 */ public void sendBusinessMessage(); /** * 发送程序异常 */ public void sendCodeMessage(); /** * 提醒推送 */ public void sendRemindMessage(); } ================================================ FILE: src/main/groovy/com/funtester/base/interfaces/IMySqlBasic.java ================================================ package com.funtester.base.interfaces; import java.sql.ResultSet; /** * 项目数据库执行类接口 */ public interface IMySqlBasic { /** * 执行查询sql * * @param sql * @return */ ResultSet executeQuerySql(String sql); /** * 执行查询sql * * @param database * @param sql * @return */ ResultSet executeQuerySql(String database, String sql); /** * 执行修改sql * * @param sql */ void executeUpdateSql(String sql); /** * 执行查询sql * * @param database * @param sql */ void executeUpdateSql(String database, String sql); /** * 关闭数据库连接 */ void mySqlOver(); /** * 初始化数据库连接 * * @param database */ void getConnection(String database); /** * 初始化数据库连接 */ void getConnection(); } ================================================ FILE: src/main/groovy/com/funtester/base/interfaces/ISocketClient.java ================================================ package com.funtester.base.interfaces; import com.alibaba.fastjson.JSONObject; import com.funtester.socket.ScoketIOFunClient; import com.funtester.socket.WebSocketFunClient; import java.util.List; /** * 对于基类base拓展Socket功能,暂时分成WebSocket和Socket.IO * {@link ScoketIOFunClient} * {@link WebSocketFunClient} */ public interface ISocketClient { /** * 连接 */ void connect(); /** * 初始化 */ void init(); /** * 发送消息 * * @param mgs */ void send(JSONObject mgs); /** * 发送消息 * * @param mgs */ void send(String mgs); /** * 关闭 */ void close(); /** * 克隆对象,性能测试中需要 */ ISocketClient clone(); /** * 是否已连接 * * @return */ boolean isConnect(); /** * 获取记录的消息,用于验证响应,请注意需要返回副本 * * @return */ List getMsgs(); /** * 用于保存收到的信息,不同于Client的saveMsg,此方法需要将对象存储的消息全都存到long_path目录下,是否需要清空Client对象中的msgs信息,需要视情况而定. */ void savaMsg(); } ================================================ FILE: src/main/groovy/com/funtester/base/interfaces/ISocketVerify.java ================================================ package com.funtester.base.interfaces; import com.funtester.base.bean.VerifyBean; import java.util.List; /** * Socket接口通用验证接口,暂时无用 */ public interface ISocketVerify extends Runnable { /** * 初始化消息,某些场景下需要将消息转成固定对象,进行验证,如json * * @param msg */ public void initMsg(List msg); /** * 执行一次现有消息的全部验证,是否有匹配 * * @return */ public boolean verify(); /** * 往验证队列中添加verify对象 * * @param bean */ public void addVerify(VerifyBean bean); /** * 清除verify,验证通过的verify可以从队列中清除 * * @param bean */ public void remoreVerify(VerifyBean bean); /** * 清除所有验证对象,通常是未验证通过,可以区分未通过和已通过 */ public void removeAllVerify(); /** * 保存verify队列的测试结果 */ public void saveResult(); } ================================================ FILE: src/main/groovy/com/funtester/base/interfaces/MarkRequest.java ================================================ package com.funtester.base.interfaces; import org.apache.http.client.methods.HttpRequestBase; /** * 专门用来标记HTTP请求的接口 */ public interface MarkRequest extends MarkThread { /** * 标记请求对象 * * @param requestBase * @return */ public String mark(HttpRequestBase requestBase); } ================================================ FILE: src/main/groovy/com/funtester/base/interfaces/MarkThread.java ================================================ package com.funtester.base.interfaces; import com.funtester.base.constaint.ThreadBase; /** * 用来标记thread,为了记录超时的请求 */ public interface MarkThread { /** * 标记线程任务 * * @param threadBase * @return */ public String mark(ThreadBase threadBase); public MarkThread clone(); } ================================================ FILE: src/main/groovy/com/funtester/base/interfaces/ReturnCode.java ================================================ package com.funtester.base.interfaces; public interface ReturnCode { int getCode(); String getDesc(); } ================================================ FILE: src/main/groovy/com/funtester/config/Constant.java ================================================ package com.funtester.config; import com.funtester.utils.FileUtil; import com.funtester.utils.Time; import org.apache.http.Consts; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import java.io.File; import java.net.InetAddress; import java.net.UnknownHostException; import java.nio.charset.Charset; import java.util.List; import java.util.Properties; /** * 常量类 */ public class Constant { private static Logger logger = LogManager.getLogger(Constant.class); /*常用的常量*/ public static final String LINE = "\r\n"; public static final String TAB = "\t"; public static final String EMPTY = ""; public static final String COMMA = ","; public static final String UNKNOW = "?"; public static final String OR = "/"; public static final String PART = "|"; /** * 正则表达式中用到的{@link Constant#PART} */ public static final String REG_PART = "\\|"; public static final String SPACE_1 = " "; public static final String CONNECTOR = "_"; public static final String QUOTE_DOUBLE = "\""; public static final String QUOTE_SINGLE = "\'"; private static final String[] PERCENT = {SPACE_1, "▁", "▂", "▃", "▄", "▅", "▅", "▇", "█"}; /** * 此处前七处等高,第八个元素不等高,不能正常使用 */ private static final String[] PARTS = {SPACE_1, "▏", "▎", "▍", "▌", "▋", "▊", "▉", "█"}; /** * 统计性能数据的分桶数 */ public static final int BUCKET_SIZE = 32; /** * 读写配置文件过滤的文本 */ public static final String FILTER = "##"; public static final String DEFAULT_STRING = "FunTester"; public static final String RESPONSE_CODE = "code"; public static final String RESPONSE_CONTENT = "content"; public static final int TEST_ERROR_CODE = -2; public static final long DEFAULT_LONG = 0L; public static final Properties SYSTEM_INFO = getSysInfo(); private static Properties getSysInfo() { return System.getProperties(); } /** * 校验IP+port的正确性 */ 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])"; /** * UTF-8字符编码格式 */ public static final Charset UTF_8 = Consts.UTF_8; /** * gb2312编码格式 */ public static final Charset GB2312 = Charset.forName("gb2312"); /** * Unicode编码格式 */ public static final Charset UNICODE = Charset.forName("Unicode"); /** * utf-16字符集 */ public static final Charset UTF_16 = Charset.forName("UTF-16"); /** * ISO-8859-1编码格式 */ public static final Charset ISO_8859_1 = Charset.forName("ISO-8859-1"); /** * GBK编码格式 */ public static final Charset GBK = Charset.forName("GBK"); /** * 默认字符集 */ public static Charset DEFAULT_CHARSET = UTF_8; /** * 当前工作目录 */ public static final String WORK_SPACE = new File(EMPTY).getAbsolutePath() + "/"; /** * 测试数据存储目录 */ public static final String LONG_Path = WORK_SPACE + "long/"; /** * 日志存存储目录 */ public static final String LOG_Path = WORK_SPACE + "log/"; /** * request日志记录目录 */ public static final String REQUEST_Path = LONG_Path + "request/"; /** * 标记请求地址 */ public static final String MARK_Path = LONG_Path + "mark/"; /** * 压测数据存放地址 */ public static final String DATA_Path = LONG_Path + "data/"; /** * 毫秒数 */ public static final long DAY = 86400000; /** * 反射方法执行用例时间间隔 */ public static final int EXECUTE_GAP_TIME = 10; /** * 本机ip,程序初始化会赋值 */ public static final String LOCAL_IP = getLocalIp(); /** * 本机用户名,程序初始化会赋值 */ public static final String COMPUTER_USER_NAME = SYSTEM_INFO.getOrDefault("user.name", DEFAULT_STRING).toString(); public static final String JAVA_VERSION = SYSTEM_INFO.get("java.version").toString(); public static final String SYS_ENCODING = SYSTEM_INFO.get("file.encoding").toString(); public static final String SYS_VERSION = SYSTEM_INFO.get("os.version").toString(); public static final String SYS_NAME = SYSTEM_INFO.get("os.name").toString(); /** * 获取本机IP * * @return */ public static String getLocalIp() { try { return InetAddress.getLocalHost().getHostAddress(); } catch (UnknownHostException e) { logger.warn("获取本机IP失败!", e); return EMPTY; } } /** * 直接获取long目录下的文件 * * @param fileName * @return */ public static String getLongFile(String fileName) { return LONG_Path + fileName; } public static String getPercent(int i) { return PERCENT[i % 9]; } public static String getPart(int i) { return PARTS[i % 9]; } /** * 创建日志文件夹和数据存储文件夹 */ static { new File(LOG_Path).mkdir(); new File(LONG_Path).mkdir(); File file = new File(REQUEST_Path); File mark = new File(MARK_Path); File data = new File(DATA_Path); file.mkdir(); mark.mkdir(); data.mkdir(); List allFile = FileUtil.getAllFile(DATA_Path); allFile.addAll(FileUtil.getAllFile(MARK_Path)); allFile.addAll(FileUtil.getAllFile(REQUEST_Path)); allFile.stream().map(y -> new File(y)).forEach(x -> { if (Time.getTimeStamp() - x.lastModified() > 3 * DAY) x.delete(); }); logger.info("当前用户:{},IP:{},工作目录:{},系统编码格式:{},系统{}版本:{}", COMPUTER_USER_NAME, LOCAL_IP, WORK_SPACE, SYS_ENCODING, SYS_NAME, SYS_VERSION); } } ================================================ FILE: src/main/groovy/com/funtester/config/EmailConstant.java ================================================ package com.funtester.config; public class EmailConstant { /** * QQ邮箱发件服务器 */ public static final String QQ_HOST = "smtp.qq.com"; /** * QQ发件邮箱 */ public static final String QQ_USERNAME = "1009329307@qq.com"; /** * 授权码 */ public static final String QQ_PASSWORD = "nhkmsrcucjpgbbcj"; /** * email加密传输类型 */ public static final String SSL_FACTORY = "javax.net.ssl.SSLSocketFactory"; } ================================================ FILE: src/main/groovy/com/funtester/config/HttpClientConstant.java ================================================ package com.funtester.config; import org.apache.http.Header; import java.util.ArrayList; import java.util.Arrays; import java.util.List; import static com.funtester.config.Constant.DEFAULT_CHARSET; import static com.funtester.httpclient.FunLibrary.getHeader; /** * */ public class HttpClientConstant { static PropertyUtils.Property propertyUtils = PropertyUtils.getProperties("http"); static String getProperty(String name) { return propertyUtils.getProperty(name); } /** * 默认user_agent */ public static Header USER_AGENT = getHeader("User-Agent", getProperty("User-Agent")); /** * 从连接目标url最大超时 单位:毫秒 */ public static int CONNECT_REQUEST_TIMEOUT = propertyUtils.getPropertyInt("TIMEOUT") * 1000; /** * 连接池中获取可用连接最大超时时间 单位:毫秒 */ public static int CONNECT_TIMEOUT = CONNECT_REQUEST_TIMEOUT; /** * 等待响应(读数据)最大超时 单位:毫秒 */ public static int SOCKET_TIMEOUT = CONNECT_REQUEST_TIMEOUT; /** * 记录 */ public static int MAX_ACCEPT_TIME = propertyUtils.getPropertyInt("MAX_ACCEPT_TIME") * 1000; /** * 连接池最大连接数 */ public static int MAX_TOTAL_CONNECTION = 5000; /** * 每个路由最大连接数 */ public static int MAX_PER_ROUTE_CONNECTION = 2000; /** * 最大header数 */ public static int MAX_HEADER_COUNT = 100; /** * 消息最大长度 */ public static int MAX_LINE_LENGTH = 10000; /** * 设置的本机ip */ public static String IP = SysInit.getRandomIP(); /** * 连接header设置 */ public static Header CONNECTION = getHeader("Connection", getProperty("Connection")); public static Header CLIENT_IP = getHeader("Client-Ip", IP); public static Header HTTP_X_FORWARDED_FOR = getHeader("HTTP_X_FORWARDED_FOR", IP); public static Header WL_Proxy_Client_IP = getHeader("WL-Proxy-Client-IP", IP); public static Header Proxy_Client_IP = getHeader("Proxy-Client-IP", IP); public static Header X_FORWARDED_FOR = getHeader("X-FORWARDED-FOR", IP); public static Header ContentType_JSON = getHeader("Content-Type", "application/json; charset=" + DEFAULT_CHARSET.toString()); public static Header ContentType_FORM = getHeader("Content-Type", "application/x-www-form-urlencoded; charset=" + DEFAULT_CHARSET.toString()); public static Header ContentType_TEXT = getHeader("Content-Type", "text/plain; charset=" + DEFAULT_CHARSET.toString()); public static Header X_Requested_KWith = getHeader("X-Requested-With", "XMLHttpRequest"); /** * 重试次数 */ public static int TRY_TIMES = propertyUtils.getPropertyInt("TRY_TIMES"); /** * 关闭超时的链接 */ public static int IDLE_TIMEOUT = 5; /** * 在设置请求contenttype参数,表示请求以io流发送数据 */ public static String CONTENTTYPE_MULTIPART_FORM = "multipart/form-data"; /** * 在设置请求contenttype参数,表示请求以文本发送数据 */ public static String CONTENTTYPE_TEXT = "text/plain"; /** * 请求头,cookie */ public static String COOKIE = "cookie"; /** * SSL版本 */ public static String SSL_VERSION = getProperty("ssl_v"); /** * 域名黑名单 */ public static List BLACK_HOSTS = new ArrayList<>(); /** * 通用循环间隔时间,单位s */ public static final int LOOP_INTERVAL = 5; /** * 线程池,线程最大空闲时间 */ public static final int THREAD_ALIVE_TIME = 3; /** * 线程池核心线程数 */ public static final int THREADPOOL_CORE = 20; /** * 线程池最大线程数 */ public static final int THREADPOOL_MAX = 500; /** * 关闭线程池最大等待时间 */ public static final int WAIT_TERMINATION_TIMEOUT = 10; /** * 添加黑名单 * * @param host */ public static void addBlackHost(String host) { BLACK_HOSTS.add(host); } static { BLACK_HOSTS.addAll(Arrays.asList(getProperty("black_host").split(","))); } } ================================================ FILE: src/main/groovy/com/funtester/config/PropertyUtils.groovy ================================================ package com.funtester.config import com.alibaba.fastjson.JSONObject import com.funtester.utils.RWUtil import com.funtester.frame.SourceCode import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.Logger import java.util.stream.Stream /** * 读取配置工具 */ class PropertyUtils extends SourceCode { private static Logger logger = LogManager.getLogger(PropertyUtils.class) /** * 获取指定.properties配置文件中所以的数据 * @param propertyName * 调用方式: * 1.配置文件放在resource源包下,不用加后缀 * PropertiesUtil.getAllMessage("message") * 2.放在包里面的 * PropertiesUtil.getAllMessage("com.test.message") * @return */ static Property getProperties(String propertyName) { logger.debug("读取配置文件:{}", propertyName) try { new Property(ResourceBundle.getBundle(propertyName.trim())) } catch (MissingResourceException e) { getLocalProperties(WORK_SPACE + propertyName + ".properties") } } /** * 获取指定路径下的文件配置,过滤掉{@link com.funtester.config.Constant#FILTER} * @param filePath * @return */ static Property getLocalProperties(String filePath) { logger.debug("读取配置文件:{}", filePath) try { new Property(RWUtil.readTxtByJson(filePath, FILTER)) } catch (MissingResourceException e) { logger.warn("找不到配置文件", e) new Property() } } /** * 获取当前项目下的文件配置,过滤掉{@link com.funtester.config.Constant#FILTER} * @param propertyName * @return */ static Property getPropertiesByFile(String propertyName) { getLocalProperties(WORK_SPACE + propertyName) } /** * 配置项 */ static class Property { Map properties = new HashMap<>() def Property(ResourceBundle resourceBundle) { def set = resourceBundle.keySet() for (def key in set) { properties.put key, resourceBundle.getString(key) } } def Property(JSONObject json) { properties.putAll(json) } /** * 获取string类型 * @param name * @return */ String getProperty(String name) { PropertyUtils.logger.debug("获取配置项:{}", name) if (contain(name)) properties.get(name) } /** * 获取int值 * @param name * @return */ int getPropertyInt(String name) { changeStringToInt(properties.get(name)) } /** * 获取long值 * @param name * @return */ int getPropertyLong(String name) { Long.valueOf(properties.get(name)) } /** * 获取boolean值 * @param name * @return */ boolean getPropertyBoolean(String name) { changeStringToBoolean(properties.get(name)) } /** * 获取数组 * @param name * @return */ String[] getArrays(String name) { getProperty(name).split(COMMA) } /** * 获取数字类型的数组 * @param name * @return */ Integer[] getIntArray(String name) { def split = getProperty(name).split(COMMA) Stream.of(split).map { x -> x as Integer }.toArray() } /** * 返回配置文件的配置项的大小 * @return */ int size() { properties.size() } /** * 输出所以配置项 * @return */ def printAll() { output properties } /** * 是否有配置项 * @param key * @return */ boolean contain(def key) { boolean var = properties.containsKey key asBoolean() if (!var) PropertyUtils.logger.error("配置{}未发现!", key) var } } } ================================================ FILE: src/main/groovy/com/funtester/config/RequestType.java ================================================ package com.funtester.config; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; /** * 请求枚举类,fun备用,暂时无用,通过其他方式区分了post请求的参数格式 */ public enum RequestType { GET("get"), POST("post"), FUN("fun"); static Logger logger = LogManager.getLogger(RequestType.class); String name; private RequestType(String name) { this.name = name; } public static RequestType getRequestType(String name) { logger.debug("验证请求方式:{}", name); for (RequestType requestType : RequestType.values()) { if (requestType.name.equalsIgnoreCase(name)) { return requestType; } } return FUN; } /** * 获取名字 * * @return */ public String getName() { return name; } } ================================================ FILE: src/main/groovy/com/funtester/config/SocketConstant.java ================================================ package com.funtester.config; /** * socket测试相关的配置 */ public class SocketConstant { /* WebSocket独享配置 */ /** * 最大等待次数,超过次数*时间就是连接失败 */ public static int MAX_WATI_TIMES = 3; /* 共享配置 */ /** * 默认连接间隔 */ public static int WAIT_INTERVAL = 3; /** * 默认最大的保存响应消息的数量 */ public static int MAX_MSG_SIZE = 200; /*Socket.IO独享配置*/ /** * Socket.IO独享配置 */ public static int MAX_RETRY = 3; /** * 重试延迟 */ public static int RETRY_DELAY = 1000; /** * 请求超时 */ public static int TIMEOUT = 10000; public static String[] transports = {"websocket"}; } ================================================ FILE: src/main/groovy/com/funtester/config/SqlConstant.java ================================================ package com.funtester.config; /** * */ public class SqlConstant { static PropertyUtils.Property propertyUtils = PropertyUtils.getProperties("mysql"); static String getProperty(String name) { return propertyUtils.getProperty(name); } /** * 驱动名称 */ public static final String DRIVE = "com.mysql.cj.jdbc.Driver"; /** * 数据库默认连接设置 */ public static final String SQLARGS = "?useUnicode=true&characterEncoding=utf-8&useOldAliasMetadataBehavior=true"; /** * 测试数据库 */ public static final String TEST_SQL_URL = getProperty("test_mysql_url") + SQLARGS; public static final String TEST_USER = getProperty("user"); public static final String TEST_PASS_WORD = getProperty("password"); /** * 数据库账号 */ public static final String FUN_SQL_URL = "jdbc:mysql://ip/database" + SQLARGS; /** * 数据库存储服务接口地址 */ public static final String MYSQL_SERVER_PATH = getProperty("mysql_server_path"); /** * 数据库连接重连间隔 */ public static final int MYSQL_RECONNECTION_GAP = 250; /** * 数据库存储任务每个线程最大等待数量 */ public static final int MYSQL_WORK_PER_THREAD = 30; /** * 最大等待数量,超过上限不再创建新的线程 */ public static final int MYSQL_MAX_WAIT_WORK = MYSQL_WORK_PER_THREAD * 50; /** * 获取数据库存储任务的超时时间,单位毫秒 */ public static final int MYSQLWORK_TIMEOUT = 200; /** * 默认request表名 */ public static String REQUEST_TABLE; /** * 默认result表名 */ public static String RESULT_TABLE; /** * 默认class表名 */ public static String CLASS_TABLE; /** * 默认性能测试表名 */ public static String PERFORMANCE_TABLE; /** * 默认的alertover表格 */ public static String ALERTOVER_TABLE; /** * 是否保存所有的请求到数据库 */ public static boolean flag = propertyUtils.getPropertyBoolean("flag"); } ================================================ FILE: src/main/groovy/com/funtester/config/SysInit.java ================================================ package com.funtester.config; import com.funtester.frame.SourceCode; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; /** * 存放一些系统初始化的方法,可被外部调用 */ public class SysInit extends SourceCode{ private static Logger logger = LogManager.getLogger(SysInit.class); /** * 是否是黑名单的host *

先检验fv1314和本地local还有10.10.的地址,然后校验配置文件中的host name

* * @param name * @return */ public static boolean isBlack(String name) { return name.contains("10.10") || name.contains("local") || HttpClientConstant.BLACK_HOSTS.contains(name); } } ================================================ FILE: src/main/groovy/com/funtester/config/VerifyType.groovy ================================================ package com.funtester.config; import com.funtester.base.exception.ParamException; import org.apache.commons.lang3.StringUtils import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.Logger; /** * 通用验证类型,包含,正则,JsonPath,handle四项 */ enum VerifyType { CONTAIN("contain"), REGEX("regex"), JSONPATH("jsonpath"), HANDLE("handle"); String vname; VerifyType(String vname) { this.vname = vname; } private static Logger logger = LogManager.getLogger(VerifyType.class); /** * 获取验证类型,不区分大小写 * * @param name * @return */ static VerifyType getRequestType(String name) { logger.debug("验证校验方式方式:{}", name); if (StringUtils.isEmpty(name)) ParamException.fail("参数不能为空!"); name = name.toLowerCase(); switch (name) { case CONTAIN.getVname(): return CONTAIN; case REGEX.getVname(): return REGEX; case JSONPATH.getVname(): return JSONPATH; case HANDLE.getVname(): return HANDLE; default: ParamException.fail(name + "参数错误!"); } } } ================================================ FILE: src/main/groovy/com/funtester/db/mongodb/MongoBase.java ================================================ package com.funtester.db.mongodb; import com.funtester.frame.SourceCode; /** * mongo操作类的基础类 */ @SuppressWarnings("all") public class MongoBase extends SourceCode { // // /** // * 获取服务地址list // * // * @param addresses // * @return // */ // public static List getServers(ServerAddress... addresses) { // return Arrays.asList(addresses); // } // // /** // * 获取服务地址 // * // * @param host // * @param port // * @return // */ // public static ServerAddress getServerAdress(String host, int port) { // return new ServerAddress(host, port); // } // // /** // * 获取认证list // * // * @param credentials // * @return // */ // public static List getCredentials(MongoCredential... credentials) { // return Arrays.asList(credentials); // } // // /** // * 获取验证 // * // * @param userName // * @param database // * @param password // * @return // */ // public static MongoCredential getMongoCredential(String userName, String database, String password) { // return MongoCredential.createCredential(userName, database, password.toCharArray()); // } // // /** // * 获取mongo客户端 // * // * @param addresses // * @param credentials // * @return // */ // public static MongoClient getMongoClient(List addresses, List credentials) { // return new MongoClient(addresses, credentials); // } // // /** // * 连接mongo数据库 // * // * @param mongoClient // * @param databaseName // * @return // */ // public static MongoDatabase getMongoDatabase(MongoClient mongoClient, String databaseName) { // return mongoClient.getDatabase(databaseName); // } // // /** // * 连接mongo集 // * // * @param mongoDatabase // * @param collectionName // * @return // */ // public static MongoCollection getMongoCollection(MongoDatabase mongoDatabase, String collectionName) { // return mongoDatabase.getCollection(collectionName); // } // // /** // * 关闭数据库连接 // * // * @param mongoClient // */ // public static void over(MongoClient mongoClient) { // mongoClient.close(); // } // // /** // * 获取mongo客户端对象,通过servers和credentials对象创建 // * // * @param mongoObject // * @return // */ // public static MongoClient getMongoClient(MongoObject mongoObject) { // MongoClient mongoClient = new MongoClient(getServers(getServerAdress(mongoObject.host, mongoObject.port)), getCredentials(getMongoCredential(mongoObject.user, mongoObject.database, mongoObject.password))); // return mongoClient; // } // // /** // * 获取mongo客户端对象,通过uri方式连接 // * // * @param mongoObject // * @return // */ // public static MongoClient getMongoClientOnline(MongoObject mongoObject) { // String format = String.format("mongodb://%s:%s@%s:%d/%s", mongoObject.user, mongoObject.password, mongoObject.host, mongoObject.port, mongoObject.database); // return new MongoClient(new MongoClientURI(format)); // } // // /** // * 获取collection对象 // * // * @param mongoObject // * @return // */ // public static MongoCollection getCollection(MongoObject mongoObject, String collectionName) { // return getMongoClient(mongoObject).getDatabase(mongoObject.database).getCollection(collectionName); // } // // /** // * 获取collection对象 // * // * @param mongoObject // * @return // */ // public static MongoCollection getCollectionOnline(MongoObject mongoObject, String collectionName) { // return getMongoClientOnline(mongoObject).getDatabase(mongoObject.database).getCollection(collectionName); // } } ================================================ FILE: src/main/groovy/com/funtester/db/mongodb/MongoObject.java ================================================ package com.funtester.db.mongodb; /** * mongo数据库配置对象,针对单个数据服务,单个身份验证 */ public class MongoObject extends MongoBase { // // String host; // // int port; // // String user; // // String password; // // String database; // // MongoClient mongoClient; // // /** // * 创建测试数据连接 // * // * @param host // * @param port // * @param user // * @param password // * @param database // */ // public MongoObject(String host, int port, String user, String password, String database) { // this.host = host; // this.port = port; // this.user = user; // this.password = password; // this.database = database; // this.mongoClient = getMongoClient(this); // } // // /** // * 创建线上数据库连接 // * // * @param port // * @param host // * @param user // * @param password // * @param database // */ // public MongoObject(int port, String host, String user, String password, String database) { // this.host = host; // this.port = port; // this.user = user; // this.password = password; // this.database = database; // this.mongoClient = getMongoClientOnline(this); // } // // /** // * 获取colletion对象 // * // * @param collectionName // * @return // */ // public MongoCollection getMongoCollection(String collectionName) { // MongoClient mongoClientOnline = getMongoClientOnline(this); // return mongoClientOnline.getDatabase(database).getCollection(collectionName); // } // // // /** // * 关闭连接 // */ // public void over() { // over(this.mongoClient); // } // // @Override // public MongoObject clone() { // return new MongoObject(this.host, this.port, this.user, this.password, this.database); // } // // public MongoObject clone2() { // return new MongoObject(this.port, this.host, this.user, this.password, this.database); // } } ================================================ FILE: src/main/groovy/com/funtester/db/mysql/AidThread.java ================================================ package com.funtester.db.mysql; import com.funtester.frame.SourceCode; /** * mysql辅助线程,当任务数太满的时候启用 *

已经启用,单独写了基于springboot的sql存储服务

*/ @Deprecated public class AidThread extends SourceCode { //public class AidThread extends SourceCode implements Runnable { // private static Logger logger = LogManager.getLogger(AidThread.class); // // @Override // public void run() { // MySqlObject object = new MySqlObject(); // MySqlObject.threadNum.incrementAndGet(); // while (true) { // if (object.statement == null || MySqlTest.getWaitWorkNum() < SqlConstant.MYSQL_WORK_PER_THREAD) break; // String sql = MySqlTest.getWork(); // if (sql == null) break; // logger.info("辅助线程执行SQL:{}", sql); // object.executeUpdateSql(sql); // } // MySqlObject.threadNum.decrementAndGet(); // object.close(); // } } ================================================ FILE: src/main/groovy/com/funtester/db/mysql/MySqlFun.java ================================================ package com.funtester.db.mysql; /** * mysql操作的基础类 *

用于存储数据,多用于爬虫

*/ @Deprecated public class MySqlFun extends SqlBase { //public class MySqlFun extends SqlBase implements IMySqlBasic { // String url; // // String database; // // String user; // // String password; // // Connection connection; // // Statement statement; // // /** // * 私有构造方法 // */ // public MySqlFun(String url, String database, String user, String password) { // this.url = url; // this.database = database; // this.user = user; // this.password = password; // getConnection(database); // } // // /** // * 初始化连接 // */ // @Override // public void getConnection() { // getConnection(EMPTY); // } // // /** // * 执行sql语句,非query语句,并不关闭连接 // * // * @param sql // */ // @Override // public void executeUpdateSql(String sql) { // executeUpdateSql(EMPTY, sql); // } // // /** // * 执行sql语句,非query语句,并不关闭连接 // * // * @param database // * @param sql // */ // @Override // public void executeUpdateSql(String database, String sql) { // getConnection(database); // SqlBase.executeUpdateSql(connection, statement, sql); // } // // /** // * 查询功能 // * // * @param sql // * @return // */ // @Override // public ResultSet executeQuerySql(String sql) { // return SqlBase.executeQuerySql(connection, statement, sql); // } // // @Override // public ResultSet executeQuerySql(String database, String sql) { // getConnection(database); // return executeQuerySql(sql); // } // // /** // * 关闭query连接 // */ // @Override // public void mySqlOver() { // SqlBase.mySqlOver(connection, statement); // } // // @Override // public void getConnection(String database) { // connection = SqlBase.getConnection(SqlConstant.FUN_SQL_URL.replace("ip", url).replace("database", database), user, password); // statement = SqlBase.getStatement(connection); // } } ================================================ FILE: src/main/groovy/com/funtester/db/mysql/MySqlObject.java ================================================ package com.funtester.db.mysql; /** * 辅助线程,处理sql任务 *

不再使用该方式存储数据库数据

*/ @Deprecated public class MySqlObject { // /** // * 标记多少辅助线程存活数量 // */ // public static AtomicInteger threadNum = new AtomicInteger(0); // /** // * 标记连接使用 // */ // int updateTime; // Connection connection; // Statement statement; // // /** // * 初始化连接方法 // */ // public MySqlObject() { // getConnection(); // } // // /** // * 获取当前辅助线程数 // * // * @return // */ // public static int getThreadNum() { // return threadNum.get(); // } // // /** // * 更新连接使用标记 // */ // void updateLastUpdate() { // updateTime = SourceCode.getMark(); // } // // /** // * 执行sql方法 // * // * @param sql // */ // void executeUpdateSql(String sql) { // getConnection(); // SqlBase.executeUpdateSql(connection, statement, sql); // updateLastUpdate(); // } // // /** // * 获取数据库连接 // */ // void getConnection() { // try { // if (SourceCode.getMark() - updateTime > SqlConstant.MYSQL_RECONNECTION_GAP || connection == null || connection.isClosed()) { // connection = TestConnectionManage.getConnection(SqlConstant.TEST_SQL_URL, SqlConstant.TEST_USER, SqlConstant.TEST_PASS_WORD); // statement = TestConnectionManage.getStatement(connection); // } // } catch (SQLException e) { // Output.output("数据库连接获取失败!", e); // } finally { // updateLastUpdate(); // } // } // // /** // * 关闭对象方法 // */ // void close() { // SqlBase.mySqlOver(connection, statement); // } } ================================================ FILE: src/main/groovy/com/funtester/db/mysql/MySqlTest.java ================================================ package com.funtester.db.mysql; import com.alibaba.fastjson.JSONObject; import com.funtester.base.bean.PerformanceResultBean; import com.funtester.base.bean.RecordBean; import com.funtester.base.bean.RequestInfo; import com.funtester.config.SqlConstant; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import java.sql.Connection; import java.sql.Statement; import java.util.concurrent.LinkedBlockingQueue; import java.util.concurrent.atomic.AtomicInteger; /** * 数据库读写类 *

* 用来存储接口请求信息的mysql数据库类 * 打印请求信息的方法写在这里面,数据库服务的队列也在这里(可不用),暂时才用直接抛出sql语句完成记录功能 *

*/ public class MySqlTest extends SqlBase { private static Logger logger = LogManager.getLogger(MySqlTest.class); /** * 控台statement1和statement均衡 */ static AtomicInteger key = new AtomicInteger(0); /** * 存放数据库存储任务 */ static LinkedBlockingQueue sqls = new LinkedBlockingQueue<>(); public static Connection getConnection0() { return connection0; } public static Statement getStatement0() { return statement0; } /** * 用于查询 */ static Connection connection0; /** * 用于写入 */ static Connection connection1; /** * 用于写入 */ static Connection connection2; static Statement statement0; static Statement statement1; static Statement statement2; /** * 新方法,报错requestinfo对象 * * @param requestInfo 请求信息 * @param data_size * @param expend_time * @param status * @param mark * @param code * @param localIP * @param computerName */ public static void saveApiTestDate(RequestInfo requestInfo, int data_size, long expend_time, int status, int mark, int code, String localIP, String computerName) { logger.info("请求uri:{},耗时:{} ms, {}", requestInfo.getUri(), expend_time, requestInfo.mark()); // if (StringUtils.isEmpty(SqlConstant.REQUEST_TABLE) || SysInit.isBlack(requestInfo.getHost())) return; // 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()); // RecordBean requestBean = new RecordBean(); // requestBean.setApi(requestInfo.getApiName()); // requestBean.setDomain(requestInfo.getHost()); // requestBean.setType(requestInfo.getType()); // requestBean.setExpend_time(expend_time); // requestBean.setData_size(data_size); // requestBean.setStatus(status); // requestBean.setMethod(requestInfo.getMethod().getName()); // requestBean.setCode(code); // requestBean.setLocal_ip(localIP); // requestBean.setLocal_name(computerName); // requestBean.setCreate_time(Time.getDate()); // 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()); // sendWork(sql); } /** * 保存性能测试结果的方法 * * @param bean */ public static void savePerformanceBean(PerformanceResultBean bean) { if (!StringUtils.isNoneEmpty(SqlConstant.PERFORMANCE_TABLE)) return; 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()); sendWork(sql); } /** * 保存测试结果 * * @param label 测试标记 * @param result 测试结果 */ public static void saveTestResult(String label, JSONObject result) { // if (SqlConstant.RESULT_TABLE == null) return; // String data = result.toString(); // Iterator iterator = result.keySet().iterator(); // int abc = 1; // while (iterator.hasNext() && abc == 1) { // String key = iterator.next().toString(); // String value = result.getString(key); // if (value.equals("false")) abc = 2; // } // if (abc != 1) new AlertOver("用例失败!", label + "测试结果:" + abc + LINE + data).sendBusinessMessage(); // logger.info(label + LINE + "测试结果:" + (abc == 1 ? "通过" : "失败") + LINE + data); // 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()); // sendWork(sql); } /** * 记录alertover警告 * * @param requestInfo * @param type * @param title * @param localIP * @param computerName */ public static void saveAlertOverMessage(RequestInfo requestInfo, String type, String title, String localIP, String computerName) { // String host_name = requestInfo.getHost(); // if (SysInit.isBlack(host_name) || SqlConstant.ALERTOVER_TABLE == null) return; // 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()); // sendWork(sql); } /** * 获取所有有效的用例类 * * @return */ // public static List getAllCaseName() { // List list = new ArrayList<>(); // if (SqlConstant.CLASS_TABLE == null) return list; // String sql = "SELECT * FROM " + SqlConstant.CLASS_TABLE + " WHERE flag = 1 ORDER BY create_time DESC;"; // TestConnectionManage.getQueryConnection(); // ResultSet resultSet = executeQuerySql(connection0, statement0, sql); // try { // while (resultSet != null && resultSet.next()) { // String className = resultSet.getString("class"); // list.add(className); // } // } catch (SQLException e) { // logger.warn(sql, e); // } // return list; // } /** * 获取用例状态 * * @param name * @return */ // public static boolean getCaseStatus(String name) { // if (SqlConstant.CLASS_TABLE == null) return false; // String sql = "SELECT flag FROM " + SqlConstant.CLASS_TABLE + " WHERE class = \"" + name + "\";"; // TestConnectionManage.getQueryConnection(); // ResultSet resultSet = executeQuerySql(connection0, statement0, sql); // try { // if (resultSet != null && resultSet.next()) { // int flag = resultSet.getInt(1); // return flag == 1 ? true : false; // } // } catch (SQLException e) { // logger.warn(sql, e); // } // return false; // } /** * 确保所有的储存任务都结束 */ // private static void check() { // while (sqls.size() != 0) { // sleep(100); // } // TestConnectionManage.stopAllThread(); // } /** * 执行sql语句,非query语句,并不关闭连接 * * @param sql * @param key */ // static void executeUpdateSql(String sql, boolean key) { // int size = getWaitWorkNum(); // if (size % 3 == 1 && size > MySqlObject.getThreadNum() * (SqlConstant.MYSQL_WORK_PER_THREAD + 1) && size < SqlConstant.MYSQL_MAX_WAIT_WORK) // new Thread(new AidThread()).start(); // if (key) { // TestConnectionManage.getUpdateConnection1(); // executeUpdateSql(connection1, statement1, sql); // TestConnectionManage.updateLastUpdate1(); // } else { // TestConnectionManage.getUpdateConnection2(); // executeUpdateSql(connection2, statement2, sql); // TestConnectionManage.updateLastUpdate2(); // } // } /** * 发送数据库任务,暂时用请求服务器接口 * * @param sql * @return */ public static void sendWork(String sql) { // if (!SqlConstant.flag) return; // logger.debug("记录SQL:{}", sql); // FunLibrary.noHeader(); // JSONObject argss = new JSONObject(); // argss.put("sql", DecodeEncode.urlEncoderText(sql)); // FunLibrary.getHttpResponse(FunLibrary.getHttpPost(SqlConstant.MYSQL_SERVER_PATH, argss)); } /** * 添加请求记录 * * @param requestBean */ public static void sendWork(RecordBean requestBean) { // FunLibrary.noHeader(); // if (SqlConstant.flag) // FunLibrary.getHttpResponse(FunLibrary.getHttpPost(SqlConstant.MYSQL_SERVER_PATH, requestBean.toJson())); } /** * 添加存储任务,数据库存储服务用 * * @param sql * @return */ public static boolean addWork(String sql) { // try { // sqls.put(sql); // } catch (InterruptedException e) { // logger.warn("添加数据库存储任务失败!", e); // return false; // } return true; } /** * 从任务池里面获取任务 * * @return */ static String getWork() { // String sql = null; // try { // sql = sqls.poll(SqlConstant.MYSQLWORK_TIMEOUT, TimeUnit.MILLISECONDS); // } catch (InterruptedException e) { // logger.warn("获取存储任务失败!", e); // } finally { // return sql; // } return EMPTY; } /** * 获取等待任务数 * * @return */ public static int getWaitWorkNum() { return sqls.size(); } /** * 提供外部查询功能 * * @param sql * @return */ // public static ResultSet executeQuerySql(String sql) { // TestConnectionManage.getQueryConnection(); // return executeQuerySql(connection0, statement0, sql); // } /** * 关闭数据库链接的方法,供外部使用 */ // public static void mySqlOver() { // mySqlQueryOver(); // } /** * 关闭update连接 */ // static void mySqlUpdateOver() { // check(); // mySqlOver(connection1, statement1); // mySqlOver(connection2, statement2); // } /** * 关闭query连接 */ // public static void mySqlQueryOver() { // mySqlOver(connection0, statement0); // } } ================================================ FILE: src/main/groovy/com/funtester/db/mysql/SqlBase.java ================================================ package com.funtester.db.mysql; import com.funtester.frame.SourceCode; /** * 数据库基础类,主要公共的获取连接和操作对象 */ public class SqlBase extends SourceCode { // private static Logger logger = LogManager.getLogger(SqlBase.class); // // /** // * 获取数据库连接 // * // * @param url 地址,包括端口 // * @param user 用户名 // * @param passowrd 密码 // * @return // */ // public static Connection getConnection(String url, String user, String passowrd) { // logger.debug("连接数据库url:{},user:{},password:{}", url, user, passowrd); // try { // Class.forName(SqlConstant.DRIVE); // } catch (ClassNotFoundException e) { // logger.warn("加载驱动程序失败!", e); // } // try { // return DriverManager.getConnection(url, user, passowrd); // } catch (SQLException e) { // logger.warn("数据库连接失败!", e); // } // return null; // } // // /** // * 获取statement对象 // * // * @param connection // * @return // */ // public static Statement getStatement(Connection connection) { // try { // return connection.createStatement(); // } catch (SQLException e) { // logger.warn("获取数据库连接失败!", e); // } catch (ExceptionInInitializerError e) { // logger.warn("初始化失败!", e); // } // return null; // } // // /** // * 执行sql语句,查询语句,返回ResultSet,并不关闭连接 // * // * @param connection // * @param statement // * @param sql // * @return // */ // public static ResultSet executeQuerySql(Connection connection, Statement statement, String sql) { // logger.debug("执行的SQL:{}", sql); // try { // if (connection != null && !connection.isClosed()) { // ResultSet resultSet = statement.executeQuery(sql); // return resultSet; // } // } catch (SQLException e) { // logger.warn(sql, e); // } // return null; // } // // /** // * 执行sql语句,非query语句,不关闭连接 // * // * @param connection // * @param statement // * @param sql // */ // public static void executeUpdateSql(Connection connection, Statement statement, String sql) { // logger.debug("执行的SQL:{}", sql); // try { // if (!connection.isClosed()) statement.executeUpdate(sql); // } catch (SQLException e) { // logger.warn(sql, e); // } // } // // /** // * 关闭数据库资源 // * // * @param connection // * @param statement // */ // public static void mySqlOver(Connection connection, Statement statement) { // try { // if (connection == null || connection.isClosed()) return; // statement.close(); // connection.close(); // } catch (SQLException e) { // logger.warn("关闭数据库链接失败!", e); // } // } } ================================================ FILE: src/main/groovy/com/funtester/db/mysql/TestConnectionManage.java ================================================ package com.funtester.db.mysql; /** * 测试数据存储数据库连接管理类 *

放弃使用该方式存储,换成springboot数据库服务

*/ @Deprecated public class TestConnectionManage extends SqlBase { // static Logger logger = LogManager.getLogger(TestConnectionManage.class); // // public static ExecuteThread executeThread1 = new ExecuteThread(true); // // // public static ExecuteThread executeThread2 = new ExecuteThread(false); // // // /** // * 记录query的最后调用时间时间 // */ // private static int lastQuery; // // /** // * 记录update的最后调用时间 // */ // private static int lastUpdate1; // // /** // * 记录update的最后调用时间 // */ // private static int lastUpdate2; // // public static void start() { // getUpdateConnection1(); // getUpdateConnection2(); // executeThread1.start(); // executeThread2.start(); // } // // static void getQueryConnection() { // try { // if (getMark() - lastQuery > SqlConstant.MYSQL_RECONNECTION_GAP || MySqlTest.connection0 == null || MySqlTest.connection0.isClosed()) // MySqlTestInitQuery(); // } catch (SQLException e) { // logger.warn("数据库连接获取失败!", e); // } // } // // public static void getUpdateConnection1() { // try { // if (getMark() - lastUpdate1 > SqlConstant.MYSQL_RECONNECTION_GAP || MySqlTest.connection1 == null || MySqlTest.connection1.isClosed()) // MySqlTestInitUpdate(true); // } catch (SQLException e) { // logger.warn("数据库连接获取失败!", e); // } // } // // public static void getUpdateConnection2() { // try { // if (getMark() - lastUpdate2 > SqlConstant.MYSQL_RECONNECTION_GAP || MySqlTest.connection2 == null || MySqlTest.connection2.isClosed()) // MySqlTestInitUpdate(false); // } catch (SQLException e) { // logger.warn("数据库连接获取失败!", e); // } // } // // static void updateLastQuery() { // lastQuery = getMark(); // } // // static void updateLastUpdate1() { // lastUpdate1 = getMark(); // } // // static void updateLastUpdate2() { // lastUpdate2 = getMark(); // } // // /** // * 连接初始化,last自动赋值 // */ // private static void MySqlTestInitQuery() { // updateLastQuery(); // MySqlTest.mySqlQueryOver(); // MySqlTest.connection0 = getConnection(SqlConstant.TEST_SQL_URL, SqlConstant.TEST_USER, SqlConstant.TEST_PASS_WORD); // MySqlTest.statement0 = getStatement(MySqlTest.connection0); // } // // // /** // * 连接初始化,last自动赋值 // */ // private static void MySqlTestInitUpdate(boolean key) { // if (key) { // updateLastUpdate1(); // MySqlTest.connection1 = getConnection(SqlConstant.TEST_SQL_URL, SqlConstant.TEST_USER, SqlConstant.TEST_PASS_WORD); // MySqlTest.statement1 = getStatement(MySqlTest.connection1); // } else { // updateLastUpdate2(); // MySqlTest.connection2 = getConnection(SqlConstant.TEST_SQL_URL, SqlConstant.TEST_USER, SqlConstant.TEST_PASS_WORD); // MySqlTest.statement2 = getStatement(MySqlTest.connection2); // } // } // // // /** // * 结束所有sql任务线程 // */ // public static void stopAllThread() { // ExecuteThread.threadKey = true; // } // //} // ///** // * 多线程类,用于消耗mysqltest里sqls中的数据库任务 // */ //@Deprecated //class ExecuteThread extends Thread { // // /** // * 分配连接 // */ // boolean key; // // /** // * 结束标志 // */ // static boolean threadKey = false; // // ExecuteThread(boolean key) { // this.key = key; // } // // @Override // public void run() { // while (true) { // if (threadKey) break; // String sql = MySqlTest.getWork(); // if (sql == null) continue; // TestConnectionManage.logger.info("辅助线程执行SQL:{}", sql); // MySqlTest.executeUpdateSql(sql, key); // } // } } ================================================ FILE: src/main/groovy/com/funtester/db/redis/RedisPool.java ================================================ package com.funtester.db.redis; import com.funtester.frame.SourceCode; /** * redis连接池 */ public class RedisPool extends SourceCode { // static Logger logger = LogManager.getLogger(RedisPool.class); // // static PropertyUtils.Property property = PropertyUtils.getProperties("redis"); // // private static String IP = property.getProperty("ip"); // // private static int PORT = property.getPropertyInt("port"); // // /** // * 最大连接数 // */ // private static int MAX_TOTAL = property.getPropertyInt("max_total"); // // /** // * 在jedispool中最大的idle状态(空闲的)的jedis实例的个数 // */ // private static int MAX_IDLE = property.getPropertyInt("max_idle"); // // /** // * 在jedispool中最小的idle状态(空闲的)的jedis实例的个数 // */ // private static int MIN_IDLE = property.getPropertyInt("min_idle"); // // /** // * 获取实例的最大等待时间 // */ // private static long MAX_WAIT = property.getPropertyLong("max_wait"); // // /** // * redis连接的超时时间 // */ // private static int TIMEOUT = property.getPropertyInt("timeout"); // // /** // * 在borrow一个jedis实例的时候,是否要进行验证操作,如果赋值true。则得到的jedis实例肯定是可以用的 // */ // private static boolean testOnBorrow = true; // // /** // * 在return一个jedis实例的时候,是否要进行验证操作,如果赋值true。则放回jedispool的jedis实例肯定是可以用的。 // */ // private static boolean testOnReturn = true; // // /** // * 连接耗尽的时候,是否阻塞,false会抛出异常,true阻塞直到超时。默认为true // */ // private static boolean blockWhenExhausted = true; // // private static JedisPoolConfig config = getConfig(); // // private static JedisPool pool = initPool(); // // /** // * 初始化连接池 // */ // private static JedisPool initPool() { // logger.debug("redis连接池IP:{},端口:{},超时设置:{}", IP, PORT, TIMEOUT); // return new JedisPool(config, IP, PORT, TIMEOUT); // } // // /** // * 默认连接池配置 // * // * @return // */ // private static JedisPoolConfig getConfig() { // JedisPoolConfig config = new JedisPoolConfig(); // config.setMaxTotal(MAX_TOTAL); // config.setMaxIdle(MAX_IDLE); // config.setMinIdle(MIN_IDLE); // config.setTestOnBorrow(testOnBorrow); // config.setTestOnReturn(testOnReturn); // config.setBlockWhenExhausted(blockWhenExhausted); // config.setMaxWaitMillis(MAX_WAIT); // logger.debug("连接redis配置:{}", JSONObject.toJSONString(config)); // return config; // } // // /** // * 获取连接池 // * // * @return // */ // public static JedisPool getPool() { // return pool; // } // // /** // * 关闭连接池资源 // */ // public static void close() { // pool.close(); // } } ================================================ FILE: src/main/groovy/com/funtester/db/redis/RedisUtil.java ================================================ package com.funtester.db.redis; public class RedisUtil extends RedisPool { // public static int getIndex() { // return index; // } // // public static void setIndex(int index) { // RedisUtil.index = index; // } // // /** // * redis数据库索引,默认0 // */ // private static int index; // // private static Logger logger = LogManager.getLogger(RedisUtil.class); // // /** // * 获取jedis操作对象,回收资源方法close,3.0以后废弃了其他方法,默认连接第一个数据库 // * // * @return // */ // public static Jedis getJedis() { // return RedisPool.getPool().getResource(); // } // // /** // * 获取某一个database的操作连接 // * // * @param index // * @return // */ // public static Jedis getJedis(int index) { // Jedis jedis = getJedis(); // jedis.select(index); // return jedis; // } // // /** // * 设置key的有效期,单位是秒 // * // * @param key // * @param exTime // * @return // */ // public static boolean expire(String key, int exTime) { // try (Jedis jedis = getJedis()) { // return jedis.expire(key, exTime) == 1; // } catch (Exception e) { // logger.error("expire key:{} error", key, e); // return false; // } // } // // /** // * 设置key-value,过期时间 // * // * @param key // * @param value // * @param expiredTime // * @return // */ // public static String set(String key, String value, int expiredTime) { // try (Jedis jedis = getJedis()) { // return jedis.setex(key, expiredTime, value); // } catch (Exception e) { // logger.error("setex key:{} value:{} error", key, value, e); // return EMPTY; // } // } // // /** // * 设置redis内容 // * // * @param key // * @param value // * @return // */ // public static String set(String key, String value) { // try (Jedis jedis = getJedis()) { // return jedis.set(key, value); // } catch (Exception e) { // logger.error("set key:{} value:{} error", key, value, e); // return EMPTY; // } // } // // /** // * 获取value // * // * @param key // * @return // */ // public static String get(String key) { // try (Jedis jedis = getJedis()) { // return jedis.get(key); // } catch (Exception e) { // logger.error("get key:{} error", key, e); // return EMPTY; // } // } // // /** // * 是否存在key // * // * @param key // * @return // */ // public static boolean exists(String key) { // try (Jedis jedis = getJedis()) { // return jedis.exists(key); // } catch (Exception e) { // logger.error("exists key:{} error", key, e); // return false; // } // } // // /** // * 删除key // * jedis返回值1表示成功,0表示失败,可能是不存在的key // * // * @param key // * @return // */ // public static boolean del(String key) { // try (Jedis jedis = getJedis()) { // return jedis.del(key) == 1; // } catch (Exception e) { // logger.error("del key:{} error", key, e); // return false; // } // } // // /** // * 获取key对应value的类型 // * // * @param key // * @return // */ // public static String type(String key) { // try (Jedis jedis = getJedis()) { // return jedis.type(key); // } catch (Exception e) { // logger.error("type key:{} error", key, e); // return EMPTY; // } // } // // /** // * 获取符合条件的key集合 // * // * @param pattern // * @return // */ // public static Set getKeys(String pattern) { // try (Jedis jedis = getJedis()) { // return jedis.keys(pattern); // } catch (Exception e) { // logger.error("type key:{} error", pattern, e); // return new HashSet(); // } // } // // /** // * 获取符合条件的key集合 // * // * @param key // * @param content // * @return // */ // public static boolean append(String key, String content) { // try (Jedis jedis = getJedis()) { // return jedis.append(key, content) > 0; // } catch (Exception e) { // logger.error("append key:{} ,content:{},error", key, content, e); // return false; // } // } } ================================================ FILE: src/main/groovy/com/funtester/dubbo/DubboBase.java ================================================ package com.funtester.dubbo; public class DubboBase { // private ApplicationConfig applicationConfig = new ApplicationConfig(); // // private RegistryConfig registryConfig = new RegistryConfig(); // // private String version; // // private String registryAddress; // // ReferenceConfig referenceConfig; // // ReferenceConfigCache configCache; // // public DubboBase(String propertyName) { // PropertyUtils.Property properties = PropertyUtils.getProperties(propertyName); // this.registryAddress = properties.getProperty("address"); // this.version = properties.getProperty("version"); // RegistryConfig registryConfig = new RegistryConfig(); // registryConfig.setAddress(registryAddress); // applicationConfig.setName(properties.getProperty("name")); // } // // /** // * 不依赖配置文件 // * // * @param adress // * @param version // * @param name // */ // public DubboBase(String adress, String version, String name) { // this.registryAddress = adress; // this.version = version; // RegistryConfig registryConfig = new RegistryConfig(); // registryConfig.setAddress(registryAddress); // applicationConfig.setName(name); // } // // /** // * ReferenceConfig实例很重,封装了与注册中心的连接以及与提供者的连接, // * 需要缓存,否则重复生成ReferenceConfig可能造成性能问题并且会有内存和连接泄漏。 // * API方式编程时,容易忽略此问题。 // * 这里使用dubbo内置的简单缓存工具类进行缓存 // * // * @param interfaceClass // * @return // */ // public GenericService getGenericService(String interfaceClass) { // if (referenceConfig == null) { // referenceConfig = new ReferenceConfig(); // referenceConfig.setApplication(applicationConfig); // referenceConfig.setRegistry(registryConfig); // referenceConfig.setVersion(version); // // 弱类型接口名 // referenceConfig.setInterface(interfaceClass); // // 声明为泛化接口 // referenceConfig.setGeneric(true); // } // configCache = ReferenceConfigCache.getCache(StringUtil.getChinese(5)); // return configCache.get(referenceConfig); // } // // /** // * 释放资源 // */ // public void over() { // if (null != configCache) configCache.destroy(referenceConfig); // } // } ================================================ FILE: src/main/groovy/com/funtester/dubbo/DubboInvokeParams.groovy ================================================ package com.funtester.dubbo class DubboInvokeParams { // // int length // // String[] types = new String[length] // // Object[] values = new Object[length] // // DubboInvokeParams(int length) { // this.length = length; // } } ================================================ FILE: src/main/groovy/com/funtester/dubbo/DubboParamBase.groovy ================================================ package com.funtester.dubbo class DubboParamBase { // // String type // // Object value // // DubboParamBase(Class type, Object value) { // this.type = type.getTypeName() // this.value = value // } } ================================================ FILE: src/main/groovy/com/funtester/dubbo/DubboUtil.java ================================================ package com.funtester.dubbo; import com.funtester.frame.SourceCode; /** * dubbo泛化调用的方法 */ public class DubboUtil extends SourceCode { // // public static Logger logger = LogManager.getLogger(DubboUtil.class); // // public static DubboInvokeParams initDubboInvokeParams(DubboParamBase... params) { // DubboInvokeParams invokeParams = new DubboInvokeParams(params.length); // range(invokeParams.getLength()).forEach(x -> // { // DubboParamBase param = params[x]; // invokeParams.getTypes()[x] = param.getType(); // invokeParams.getValues()[x] = param.getValue(); // } // ); // return invokeParams; // } // // public static Object getResponse(GenericService genericService, String methodName, DubboInvokeParams params) { // return genericService.$invoke(methodName, params.getTypes(), params.getValues()); // } // // public static void main(String[] args) { // Map getDataRequest = new HashMap(); // getDataRequest.put("orgId", "119"); // getDataRequest.put("orgType", "2"); // getDataRequest.put("reqId", "123456789"); // DubboInvokeParams dubboInvokeParams = initDubboInvokeParams(new DubboParamBase(Object.class, getDataRequest)); // DubboBase dubbo = new DubboBase("dubbo"); // GenericService genericService = dubbo.getGenericService("com.noriental.usersvr.service.okuser.SchoolYearService"); // // Object response = getResponse(genericService, "findFutureYear", dubboInvokeParams); // output(response.toString()); // // } } ================================================ FILE: src/main/groovy/com/funtester/frame/JsonVerify.groovy ================================================ package com.funtester.frame import com.alibaba.fastjson.JSON import com.alibaba.fastjson.JSONArray import com.alibaba.fastjson.JSONException import com.alibaba.fastjson.JSONObject import com.funtester.base.exception.ParamException import com.funtester.utils.JsonUtil import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.Logger /** * 操作符重写类,用于匹配JSonpath验证语法,基本重载的方法以及各种比较方法,每个方法重载三次,参数为double,String,verify * 数字统一采用double类型,无法操作的String对象的方法返回empty * 操作符现在支持['>', '<', '=']三种,暂无增加计划 */ class JsonVerify extends SourceCode implements Comparable { private static Logger logger = LogManager.getLogger(JsonVerify.class) /** * 验证文本 */ String extra /** * 验证数字格式 */ double num /** * 构造方法,暂时写着,尽量使用jsonutil创造verify对象 * * @param json * @param path */ private JsonVerify(JSONObject json, String path) { this(JsonUtil.getInstance(json).getString(path)) if (isNumber()) num = changeStringToDouble(extra) } private JsonVerify(String value) { extra = value if (isNumber()) num = changeStringToDouble(extra) } /** * 获取实例方法 * @param json * @param path * @return */ static JsonVerify getInstance(JSONObject json, String path) { new JsonVerify(json, path) } static JsonVerify getInstance(String str) { new JsonVerify(str) } /** * 加法重载 * @param i * @return */ def plus(double i) { isNumber() ? num + i : extra + i.toString() } /** * 加法重载,string类型 * @param s * @return */ def plus(String s) { isNumber() && isNumber(s) ? num + changeStringToDouble(s) : extra + s } /** * 加法重载,verify类型 * @param s * @return */ def plus(JsonVerify v) { isNumber() && v.isNumber() ? this + (v.num) : extra + v.extra } /** * 减法重载 * @param i * @return */ def minus(double i) { isNumber() ? num - i : extra - i.toString() } /** * 减法重载,string类型 * @param s * @return */ def minus(String s) { extra - s } /** * 减法重载 * @param v * @return */ def minus(JsonVerify v) { if (isNumber() && v.isNumber()) this - v.num else extra - v.extra } /** * extra * i 这里会去强转double为int,调用intvalue()方法 * @param i * @return */ def multiply(double i) { if (isNumber()) num * i else extra * i } def multiply(String s) { isNumber() ? isNumber(s) ? num * changeStringToDouble(s) : s * num : isNumber(s) ? extra * changeStringToDouble(s) : EMPTY } def multiply(JsonVerify v) { this * v.extra } /** * 除法重载 * @param i * @return */ def div(int i) { if (isNumber()) num / i } def div(String s) { if (isNumber() && isNumber(s)) num / changeStringToDouble(s) } def div(JsonVerify v) { if (isNumber() && v.isNumber()) num / v.num } def mod(int i) { if (isNumber()) (int) (num % i * 10000) * 1.0 / 10000 } /** * 直接取值,用于数组类型 * @param i * @return */ def getAt(int i) { try { JSONArray.parseArray(extra)[i] } catch (JSONException e) { i >= extra.length() ? EMPTY : extra[i] } } /** * 直接取值,用户json类型 * @param i * @return */ def getAt(String s) { try { JSON.parseObject(extra)[s] } catch (JSONException e) { isNumber(s) ? extra.charAt(changeStringToInt(s)) : null } } /** * if (a implements Comparable) { a.compareTo(b) == 0 } else { a.equals(b) }* @param a * @return */ boolean equals(JsonVerify verify) { extra == verify.extra } boolean equals(Number n) { num.toString() == n.toString() } /** * 此方法存在缺陷,在其他项目引入jar时,调用==会直接调用Java的 * @param s * @return */ boolean equals(String s) { extra == s } @Override boolean equals(Object o) { this == o.toString() } /** * a <=> b a.compareTo(b) * a>b a.compareTo(b) > 0 * a>=b a.compareTo(b) >= 0 * a T asType(Class tClass) { logger.debug("强转类型:{}", tClass.toString()) if (tClass == Integer) num.intValue() else if (tClass == Double) num else if (tClass == Long) num.longValue() else if (tClass == String) extra else if (tClass == JsonVerify) new JsonVerify(extra) else if (tClass == Boolean) changeStringToBoolean(extra) } /** * 用户正则匹配 * @param regex * @return */ def regex(String regex) { extra ==~ regex } /** * 是否是数字 * @return */ def isNumber() { isNumber(extra) } /** * 是否为boolean类型 * @return */ def isBoolean() { extra ==~ ("false|true") } @Override String toString() { extra } /** * 使用与switch-case方法判断 * @param o * @return */ boolean isCase(Object o) { this == o } /** * 判断是否符合期望 * @param str * @return */ boolean fit(String str) { logger.info("verify对象: {},匹配的字符串: {}", extra, str) OPS o = OPS.getInstance(str.charAt(0)) def res = str.substring(1) switch (o) { case OPS.GREATER: return this > res case OPS.LESS: return this < res case OPS.EQUAL: return extra == res case OPS.REGEX: return extra ==~ res default: ParamException.fail("判断字符串参数错误!") } } /** * 判断是否符合操作后期望,通过{@link com.funtester.config.Constant#REG_PART}分隔 * @param str * @return */ boolean fitFun(String str) { def split = str.split(REG_PART, 2) def handle = split[0] def ops = split[1] HPS h = HPS.getInstance(handle.charAt(0)) def hr = handle.substring(1) switch (h) { case HPS.PLUS: def n = getInstance((this + hr) as String) return n.fit(ops) case HPS.MINUS: def n = getInstance((this - hr) as String) return n.fit(ops) case HPS.MUL: def n = getInstance((this * hr) as String) return n.fit(ops) case HPS.DIV: def n = getInstance((this / hr) as String) return n.fit(ops) default: ParamException.fail("运算操作字符串参数错误!") } } /** * 支持的判断类型的操作符枚举类 */ static enum OPS { GREATER((char) '>'), LESS((char) '<'), EQUAL((char) '='), REGEX((char) '~') char name OPS(char name) { this.name = name } static OPS getInstance(char c) { switch (c) { case GREATER.getName(): return GREATER; case LESS.getName(): return LESS; case EQUAL.getName(): return EQUAL; case REGEX.getName(): return REGEX default: ParamException.fail("判断操作符参数错误!") } } } /** * 支持的运算类型的操作符枚举类 */ static enum HPS { PLUS((char) '+'), MINUS((char) '-'), MUL((char) '*'), DIV((char) '/') char name HPS(char name) { this.name = name } static HPS getInstance(char c) { switch (c) { case PLUS.getName(): return PLUS case MINUS.getName(): return MINUS case MUL.getName(): return MUL case DIV.getName(): return DIV default: ParamException.fail("运算操作符参数错误!") } } } } ================================================ FILE: src/main/groovy/com/funtester/frame/Output.java ================================================ package com.funtester.frame; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONException; import com.alibaba.fastjson.JSONObject; import com.funtester.base.bean.AbstractBean; import com.funtester.config.Constant; import com.funtester.utils.StringUtil; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import java.util.*; import java.util.stream.Collectors; import java.util.stream.IntStream; @SuppressWarnings("all") public class Output extends Constant { private static Logger logger = LogManager.getLogger(Output.class); private static final String UP = "~☢~~☢~~☢~~☢~~☢~~☢~~☢~~☢~~☢~~☢~"; private static final String DOWN = "~☢~~☢~~☢~~☢~~☢~~☢~~☢~~☢~~☢~~☢~"; public static void output(AbstractBean bean) { output(bean.toJson()); } /** * 输出,自带log方法,排除root用户使用输出 * * @param text */ public static void output(String text) { logger.info(text); } /** * 输出,针对各种不同情况做兼容 *

* 在处理两个对象,默认情况第一个是说明文字,第二个是list内容 *

* * @param object */ public static void output(Object... object) { if (ArrayUtils.isEmpty(object)) { logger.warn("怎么空了呢!"); } else if (object.length == 1) { if (object[0] instanceof List) { output((List) object[0]); } else { output(object[0].toString()); } } else if (object.length == 2) { output(object[0]); if (object[1] instanceof List) { output((List) object[1]); } else { output(object[1]); } } else if (object.getClass().isArray()) { output(Arrays.asList(object)); } } public static void output(List list) { list.forEach(x -> output("第" + (list.indexOf(x) + 1) + "个:" + x.toString())); } public static void output(Iterator its) { its.forEachRemaining(x -> output(x.toString())); } /** * 输出无序集合 * * @param its */ public static void output(Iterable its) { its.forEach(x -> output(x.toString())); } public static void output(Map map) { if (map == null || map.size() == 0) { logger.warn("怎么空了呢!"); } else { show(map); } } /** * 输出json数组 * * @param jsonArray */ public static void output(JSONArray jsonArray) { if (jsonArray == null || jsonArray.isEmpty()) { output("jsonarray对象为空!"); return; } jsonArray.forEach(x -> { try { output(JSON.parseObject(x.toString())); } catch (JSONException e) { output(x); } }); } /** * 输出数组 * * @param arrays */ public static void output(T[] nums) { if (ArrayUtils.isEmpty(nums)) return; Arrays.asList(nums).forEach(x -> output(x)); } /** * 泛型做输出数字对象 * * @param x * @param */ public static void output(T x) { output(x.toString()); } public static void output(Object o) { if (o == null) logger.warn("怎么空了呢!"); else output(o.toString()); } public static void output(int[] nums) { output(ArrayUtils.toObject(nums)); } public static void output(long[] nums) { output(ArrayUtils.toObject(nums)); } public static void output(double[] nums) { output(ArrayUtils.toObject(nums)); } /** * 输出json * * @param jsonObject json格式响应实体 */ public static JSONObject output(JSONObject jsonObject) { if (jsonObject == null || jsonObject.size() == 0) { output("json 对象是空的!"); return jsonObject; } String jsonStr = jsonObject.toString();// 先将json对象转化为string对象 jsonStr = jsonStr.replaceAll("\\\\/", OR); int level = 0;// 用户标记层级 StringBuffer jsonResultStr = new StringBuffer("> ");// 新建stringbuffer对象,用户接收转化好的string字符串 int length = jsonStr.length(); for (int i = 0; i < length; i++) {// 循环遍历每一个字符 char piece = jsonStr.charAt(i);// 获取当前字符 // 如果上一个字符是断行,则在本行开始按照level数值添加标记符,排除第一行 if ('\n' == jsonResultStr.charAt(jsonResultStr.length() - 1)) { jsonResultStr.append(StringUtil.getSerialEmoji(level) + " . "); IntStream.range(0, level - 1).forEach(x -> jsonResultStr.append(". . "));//没有采用sourcecode的getmanystring } char last = i == 0 ? '{' : jsonStr.charAt(i - 1); char next = i < length - 1 ? jsonStr.charAt(i + 1) : '}'; switch (piece) { case ',': // 如果是“,”,则断行 jsonResultStr.append(piece + (("\"0123456789le]}".contains(last + EMPTY) && "\"[{".contains(next + EMPTY)) ? LINE : EMPTY)); break; case '{': case '[': // 如果字符是{或者[,则断行,level加1 jsonResultStr.append(piece + (":[{,".contains(last + EMPTY) && ",[{}]\"0123456789le".contains(next + EMPTY) ? LINE : EMPTY)); if (last != '[') level++;//解决jsonarray:[{ break; case '}': case ']': // 如果是}或者],则断行,level减1 // jsonResultStr.append(LINE); jsonResultStr.append(("\"0123456789le]}{[,".contains(last + EMPTY) && "}],".contains(next + EMPTY) ? LINE : EMPTY)); if (next != ']') level--;//解决jsonarray:[{ jsonResultStr.append(level == 0 ? "" : StringUtil.getSerialEmoji(level) + " . "); IntStream.range(0, level - 1).forEach(x -> jsonResultStr.append(". . "));//没有采用sourcecode的getmanystring jsonResultStr.append(piece); break; default: jsonResultStr.append(piece); break; } } output(LINE + UP + " JSON " + UP + LINE + jsonResultStr.toString().replaceAll(LINE, LINE + "> ") + LINE + DOWN + " JSON " + DOWN); return jsonObject; } public static void show(Map map) { new ConsoleTable(map); } public static void show(List> rows) { new ConsoleTable(rows); } /** * 打印可能的json数据 * * @param content */ public static void showStr(String content) { try { if (content.contains("&")) output(SourceCode.getJson(content.split("&"))); else output(JSONObject.parseObject(content)); } catch (JSONException e) { output(content); } } static class ConsoleTable extends SourceCode { List rowLength = new ArrayList<>(); public static void show(Map map) { new ConsoleTable(map); } /** * 输出map * * @param map */ private ConsoleTable(Map map) { Set set = map.keySet(); int asInt0 = set.stream().mapToInt(key -> key.toString().length()).max().getAsInt(); rowLength.add(asInt0 + 2); List values = new ArrayList<>(); set.forEach(key -> values.add(map.get(key).toString())); int asInt1 = values.stream().mapToInt(value -> value.length()).max().getAsInt(); rowLength.add(asInt1 + 2); StringBuffer stringBuffer = new StringBuffer(LINE + getHeader()); map.forEach((k, v) -> { stringBuffer.append(getCel(0, k.toString())); stringBuffer.append(getCel(1, v.toString())); }); output(stringBuffer.append(LINE + getHeader()).toString()); } /** * 输出list * * @param rows */ private ConsoleTable(List> rows) { for (int i = 0; i < rows.size(); i++) { List line = rows.get(i); for (int j = 0; j < line.size(); j++) { String s = line.get(j); if (rowLength.size() <= j) rowLength.add(0); if (rowLength.get(j) < s.length()) rowLength.set(j, s.length()); } } rowLength = rowLength.stream().map(n -> n + 2).collect(Collectors.toList()); StringBuffer stringBuffer = new StringBuffer(LINE + getHeader()); for (int i = 0; i < rows.size(); i++) { List line = rows.get(i); for (int j = 0; j < rowLength.size(); j++) { stringBuffer.append(getCel(j, j < line.size() ? line.get(j) : EMPTY)); } } output(stringBuffer.append(LINE + getHeader()).toString()); } /** * 获取每一格的string * * @param colum 列 * @param content 格内容 * @return */ public String getCel(int colum, String content) { return (colum == 0 ? LINE + PART : PART) + StringUtil.center(content, rowLength.get(colum)) + (rowLength.size() - colum == 1 ? PART : EMPTY); } /** * 获取头尾行 * * @return */ private String getHeader() { List collect = rowLength.stream().map(size -> getManyString("-", size)).collect(Collectors.toList()); return "+" + StringUtils.join(collect.toArray(), "+") + "+"; } } } ================================================ FILE: src/main/groovy/com/funtester/frame/ResponseVerify.java ================================================ package com.funtester.frame; import com.alibaba.fastjson.JSONObject; import com.funtester.httpclient.FunLibrary; import com.funtester.utils.Regex; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import java.util.ArrayList; import java.util.Arrays; import java.util.List; /** * 通用验证方法封装 */ public class ResponseVerify extends SourceCode { private static Logger logger = LogManager.getLogger(ResponseVerify.class); /** * 断言的json对象 */ private JSONObject verifyJson; /** * 断言的code码 */ private int code; /** * 获取所有lines * * @return */ public List getLines() { return lines; } /** * 断言的json对象分行解析 */ private List lines = new ArrayList<>(); public ResponseVerify(JSONObject jsonObject) { this.verifyJson = jsonObject; this.lines = parseJsonLines(jsonObject); } /** * 获取 code *

这里的requestinfo主要的目的是为了拦截一些不必要的checkcode验证的,主要有black_host名单提供,在使用时,注意requestinfo的非空校验

* * @return */ public int getCode() { return FunLibrary.getiBase().checkCode(verifyJson, null); } /** * 校验code码是否正确,==0 * * @return */ public boolean isRight() { return 0 == this.getCode(); } /** * 获取节点值 * * @param key 节点 * @return 返回节点值 */ public String getValue(String key) { int size = lines.size(); for (int i = 0; i < size; i++) { String line = lines.get(i); if (line.startsWith(key + ":")) return line.replaceFirst(key + ":", EMPTY); } return EMPTY; } /** * 校验是否包含文本 * * @param text 需要校验的文本 * @return 返回 Boolean 值 */ public boolean isContains(String... text) { boolean result = true; String content = verifyJson.toString(); int length = text.length; for (int i = 0; i < length; i++) { if (!result) break; result = content.contains(text[i]) & result; } return result; } /** * 校验节点值为数字 * * @param value 节点名 * @return 返回 Boolean 值 */ public boolean isNum(String... value) { boolean result = true; int length = value.length; for (int i = 0; i < length; i++) { String key = value[i] + ":"; if (!verifyJson.toString().contains(value[i]) || !result) return false; for (int k = 0; k < lines.size(); k++) { String line = lines.get(k); if (line.startsWith(key)) { String lineValue = line.replaceFirst(key, EMPTY); result = isNumber(lineValue) & result; } } } return result; } /** * 校验节点值不为空 * * @param keys 节点名 * @return 返回 Boolean 值,为空返回false,不为空返回true */ public boolean notNull(String... keys) { boolean result = true; for (int i = 0; i < keys.length; i++) { String key = keys[i] + ":"; if (!verifyJson.toString().contains(keys[i]) || !result) return false; for (int k = 0; k < lines.size(); k++) { String line = lines.get(k); if (line.startsWith(key)) { String lineValue = line.replaceFirst(key, EMPTY); result = lineValue != null & !lineValue.isEmpty() & result; } } } return result; } /** * 验证是否为列表,根据字段后面的符号是否是[ * * @param key 返回体的字段值 * @return */ public boolean isArray(String key) { String json = verifyJson.toString(); int index = json.indexOf(key); char a = json.charAt(index + key.length() + 2); return a == '['; } /** * 验证是否是json,根据后面跟的符号是否是{ * * @param key 返回体的字段值 * @return */ public boolean isJson(String key) { String json = verifyJson.toString(); int index = json.indexOf(key); char a = json.charAt(index + key.length() + 2); if (a == '{') return true; return false; } /** * 是否是Boolean值 * * @return */ public boolean isBoolean(String... value) { boolean result = true; int length = value.length; for (int i = 0; i < length; i++) { String key = value[i] + ":"; if (!verifyJson.toString().contains(value[i]) || !result) return false; for (int k = 0; k < lines.size(); k++) { String line = lines.get(k); if (line.startsWith(key)) { String lineValue = line.replaceFirst(key, EMPTY); result = Regex.isRegex(lineValue, "^(false)|(true)$") & result; } } } return result; } /** * 验证正则匹配结果 * * @param regex * @return */ public boolean isRegex(String regex) { String text = verifyJson.toString(); return Regex.isRegex(text, regex); } /** * 解析json信息 * * @param response json格式的响应实体 * @return json每个字段和值,key:value形式 */ public static List parseJsonLines(JSONObject response) { String jsonStr = response.toString();// 先将json对象转化为string对象 jsonStr = jsonStr.replaceAll(",", LINE); jsonStr = jsonStr.replaceAll("\"", EMPTY); jsonStr = jsonStr.replaceAll("\\\\/", OR); jsonStr = jsonStr.replaceAll("\\{", LINE); jsonStr = jsonStr.replaceAll("\\[", LINE); jsonStr = jsonStr.replaceAll("}", LINE); jsonStr = jsonStr.replaceAll("]", LINE); List jsonLines = Arrays.asList(jsonStr.split(LINE)); return jsonLines; } } ================================================ FILE: src/main/groovy/com/funtester/frame/Save.java ================================================ package com.funtester.frame; import com.funtester.base.exception.FailException; import com.funtester.utils.RWUtil; import com.alibaba.fastjson.JSONObject; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import java.io.File; import java.util.ArrayList; import java.util.Collection; import java.util.List; import static com.funtester.config.Constant.*; /** * 用来保存数据的类,如果文件已经存在会删除原来的文件 */ public class Save { private static Logger logger = LogManager.getLogger(Save.class); /** * 保存信息,每次回删除文件,默认当前工作空间 * * @param content 内容 */ public static void info(String content) { info("long", content); } public static void info(String name, String content) { File dirFile = new File(LONG_Path + name); if (dirFile.exists()) { boolean delete = dirFile.delete(); if (!delete) FailException.fail("删除文件失败!" + name); } RWUtil.writeText(dirFile, content); logger.info("数据保存成功!文件名:{}{}", LONG_Path, name); } /** * 保存list数据到本地文件 */ public static void saveLongList(Collection data, Object name) { List list = new ArrayList<>(); data.forEach(num -> list.add(num.toString())); saveStringList(list, name.toString()); } /** * 保存list数据到本地文件 */ public static void saveIntegerList(Collection data, String name) { List list = new ArrayList<>(); data.forEach(num -> list.add(num.toString())); saveStringList(list, name); } /** * 保存list数据到本地文件 */ public static void saveDoubleList(Collection data, String name) { List list = new ArrayList<>(); data.forEach(num -> list.add(num.toString())); saveStringList(list, name); } /** * 保存list数据,long类型无法覆盖 * * @param data * @param name */ public static void saveList(Collection data, String name) { List list = new ArrayList<>(); data.forEach(num -> list.add(num.toString())); saveStringList(list, name); } /** * 保存list数据到本地文件 */ public static void saveStringList(Collection data, String name) { String join = StringUtils.join(data, LINE); info(name, join); } /** * 保存json数据到本地文件 */ public static void saveJson(JSONObject data, String name) { StringBuffer buffer = new StringBuffer(); data.keySet().forEach(x -> buffer.append(LINE + x.toString() + PART + data.getString(x.toString()))); /*处理\n\t(LINE)*/ if (buffer.length() > 2) info(name, buffer.substring(2)); } /** * 同步save数据,用于匿名类多线程保存测试数据 * * @param data * @param name */ public static void saveStringListSync(Collection data, String name) { synchronized (Save.class) { if (data.isEmpty()) return; saveStringList(data, name); } } } ================================================ FILE: src/main/groovy/com/funtester/frame/SourceCode.java ================================================ package com.funtester.frame; import com.alibaba.fastjson.JSONObject; import com.funtester.base.exception.FailException; import com.funtester.base.exception.ParamException; import com.funtester.base.interfaces.IMessage; import com.funtester.utils.Regex; import com.funtester.utils.Time; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import java.io.*; import java.text.DecimalFormat; import java.util.Arrays; import java.util.List; import java.util.Random; import java.util.Scanner; import java.util.concurrent.TimeUnit; import java.util.stream.Collectors; import java.util.stream.IntStream; public class SourceCode extends Output { private static Logger logger = LogManager.getLogger(SourceCode.class); private static Scanner scanner; private static IMessage iMessage; public static IMessage getiMessage() { return iMessage; } public static void setiMessage(IMessage iMessage) { SourceCode.iMessage = iMessage; } private static ThreadLocal random = new ThreadLocal() { @Override protected Random initialValue() { return new Random(); } }; /** * 获取当前时间戳10位int 类型的数据 * * @return */ public static int getMark() { return (int) (Time.getTimeStamp() / 1000); } /** * 获取纳秒的时间标记 * * @return */ public static long getNanoMark() { return System.nanoTime(); } /** * 等待方法,用sacnner类,控制台输出字符key时会跳出循环 *

如何执行close方法,只能用一次

* * @param key */ public static void waitForKey(Object key) { logger.warn("请输入“{}”继续运行!", key.toString()); long start = Time.getTimeStamp(); scanner = new Scanner(System.in, DEFAULT_CHARSET.name()); while (scanner.hasNext()) { String next = scanner.next(); if (next.equalsIgnoreCase(key.toString())) break; logger.warn("输入:{}错误!", next); } long end = Time.getTimeStamp(); double timeDiffer = Time.getTimeDiffer(start, end); logger.info("本次共等待:" + timeDiffer + "秒!"); } /** * 获取屏幕输入内容 *

如何执行close方法,只能用一次

* * @return */ public static String getInput() { scanner = new Scanner(System.in, DEFAULT_CHARSET.name()); String next = scanner.next(); logger.debug("输、入内容:{}", next); return next; } /** * 关闭scanner,解决无法多次使用wait的BUG */ public static void closeScanner() { scanner.close(); } /** * 将数组变成json对象,使用split方法 *

* split方法默认limit=2 *

* int和double使用数字类型,其他使用字符串类型 * * @param objects * @param regex 分隔的regex表达式 * @return */ public static JSONObject changeArraysToJson(Object[] objects, String regex) { JSONObject args = new JSONObject(); Arrays.stream(objects).forEach(x -> { String[] split = x.toString().split(regex, 2); args.put(split[0], isInteger(split[1]) ? changeStringToInt(split[1]) : isDouble(split[1]) ? changeStringToDouble(split[1]) : split[1]); }); return args; } /** * 获取一个简单的json对象 *

* 使用“=”号作为分隔符,limit=2 *

* * @param content * @return */ public static JSONObject getJson(String... content) { if (StringUtils.isAnyEmpty(content)) ParamException.fail("转换成json格式参数错误!"); return changeArraysToJson(content, "="); } /** * 获取一个简单的JSON对象 * * @param key * @param value * @return */ public static JSONObject getSimpleJson(String key, Object value) { return StringUtils.isBlank(key) ? null : new JSONObject(1) {{ put(key, value); }}; } /** * 获取text复制拼接的string * * @param text * @param time 次数 * @return */ public static String getManyString(String text, int time) { return IntStream.range(0, time).mapToObj(x -> text).collect(Collectors.joining()); } /** * 获取一个百分比,两位小数 * * @param total 总数 * @param piece 成功数 * @return 百分比 */ public static double getPercent(int total, int piece) { if (total == 0) return 0.00; int s = (int) (piece * 1.0 / total * 10000); return s * 1.0 / 100; } /** * 获取百分比,string类型,拼接%符合,两位小数 * * @param percent 这里传的需要计算好的百分比,实际比例*100,而不是比例 * @return */ public static String getPercent(double percent) { return formatDouble(percent) + "%"; } /** * 格式化数字格式,使用千分号 * * @param number * @return */ public static String formatLong(Number number) { return formatNumber(number, "#,###"); } /** * 格式化数字格式,保留两位有效数字,使用去尾法 * * @param number * @return */ public static String formatDouble(Number number) { return formatNumber(number, "#.##"); } /** * 格式化数字格式 * * @param number * @param pattern * @return */ public static String formatNumber(Number number, String pattern) { DecimalFormat format = new DecimalFormat(pattern); return format.format(number); } /** * 获取随机IP地址 * * @return */ public static String getRandomIP() { return getRandomInt(255) + "." + getRandomInt(255) + "." + getRandomInt(255) + "." + getRandomInt(255); } /** * 把string类型转化为int * * @param text 需要转化的文本 * @return */ public static int changeStringToInt(String text) { logger.debug("需要转化成的文本:{}", text); try { return Integer.parseInt(text); } catch (NumberFormatException e) { logger.warn("转化int类型失败!", e); return TEST_ERROR_CODE; } } /** * 把string类型转化为long * * @param text 需要转化的文本 * @return */ public static long changeStringToLong(String text) { logger.debug("需要转化成的文本:{}", text); try { return Long.parseLong(text); } catch (NumberFormatException e) { logger.warn("转化int类型失败!", e); return TEST_ERROR_CODE; } } /** * 将string转换成boolean,失败返回null,待修改 * * @param text * @return */ public static boolean changeStringToBoolean(String text) { logger.debug("需要转化成的文本:{}", text); if (text == null || !Regex.isMatch(text.toLowerCase(), "false|ture")) return false; return text.equalsIgnoreCase("true"); } /** * string转化为double * * @param text * @return */ public static double changeStringToDouble(String text) { logger.debug("需要转化成的文本:{}", text); try { return Double.parseDouble(text); } catch (NumberFormatException e) { logger.warn("转化double类型失败!", e); return TEST_ERROR_CODE * 1.0; } } /** * 是否是数字,000不算,0.0也算,-0和-0.0不算 * * @param text * @return */ public static boolean isNumber(String text) { logger.debug("需要判断的文本:{}", text); if (StringUtils.isEmpty(text) || text.equals("-0")) return false; if (text.equals("0")) return true; return Regex.isMatch(text, "-{0,1}(([1-9][0-9]*)|0)(.\\d+){0,1}"); } public static boolean isInteger(String str) { return isNumber(str) && !str.contains(".") && str.length() < 11; } public static boolean isDouble(String str) { return isNumber(str) && str.contains("."); } /** * 线程休眠,单位是秒 * * @param second 秒,可以是小数 */ public static void sleep(int second) { if (second > 100) FailException.fail("休眠时间过长,请更换其他方式!"); try { Thread.sleep(second * 1000); } catch (InterruptedException e) { logger.warn("sleep发生错误!", e); } } /** * 睡眠,提供更精准的休眠功能 * * @param time 单位s */ public static void sleep(double time) { if (time > 100) FailException.fail("休眠时间过长,请更换其他方式!"); try { Thread.sleep((long) (time * 1000)); } catch (InterruptedException e) { logger.warn("sleep发生错误!", e); } } /** * 线程休眠,以纳秒为单位 * * @param nanosec */ public static void sleep(long nanosec) { if (nanosec < 1_000_000) return; try { TimeUnit.NANOSECONDS.sleep(nanosec); } catch (InterruptedException e) { logger.warn("sleep发生错误!", e); } } /** * 获取随机数,获取1~num 的数字,包含 num * * @param num 随机数上限 * @return 随机数 */ public static int getRandomInt(int num) { return random.get().nextInt(num) + 1; } /** * 随机范围int,取头不取尾 * * @param start * @param end * @return */ public static int getRandomIntRange(int start, int end) { if (end <= start) return TEST_ERROR_CODE; return random.get().nextInt(end - start) + start; } /** * 随机选择某一个值 * * @param fs * @param * @return */ public static F random(F... fs) { return fs[getRandomInt(fs.length) - 1]; } /** * 随机选择某一个字符串 * * @param fs * @return */ public static String random(String... fs) { if (ArrayUtils.isEmpty(fs)) ParamException.fail("数组不能为空!"); return fs[getRandomInt(fs.length) - 1]; } /** * 随机选择某一个对象 * * @param list * @param * @return */ public static F random(List list) { if (list == null || list.isEmpty()) ParamException.fail("数组不能为空!"); return list.get(getRandomInt(list.size()) - 1); } /** * 获取一定范围内的随机值 * * @param start 初始值 * @param range 随机范围 * @return */ public static double getRandomRange(double start, double range) { return start - range + getRandomDouble() * range * 2; } /** * 获取随机数,获取0-1 的数字 * * @return 随机数 */ public static double getRandomDouble() { return random.get().nextDouble(); } /** * 获取一个intsteam * * @param start * @param end * @return */ public static IntStream range(int start, int end) { return IntStream.range(start, end); } /** * 获取一个intsteam,默认从0开始,num为止,不包含num * * @param num * @return */ public static IntStream range(int num) { return IntStream.range(0, num); } /** * 通用的终止运行的方法,用于脚本调试等场景 */ public static void fail() { throw new FailException(); } /** * 通过将对象序列化成数据流实现深层拷贝的方法 *

* 将该对象序列化成流,因为写在流里的是对象的一个拷贝,而原对象仍然存在于JVM里面。所以利用这个特性可以实现对象的深拷贝 *

* * @param t 需要被拷贝的对象,必需实现 Serializable接口,不然会报错 * @param 需要拷贝对象的类型 * @return */ public static T deepClone(T t) { try { ByteArrayOutputStream baos = new ByteArrayOutputStream(); ObjectOutputStream oos = new ObjectOutputStream(baos); oos.writeObject(t); // 将流序列化成对象 ByteArrayInputStream bais = new ByteArrayInputStream(baos.toByteArray()); ObjectInputStream ois = new ObjectInputStream(bais); return (T) ois.readObject(); } catch (IOException e) { logger.error("线程任务拷贝失败!", e); } catch (ClassNotFoundException e) { logger.error("未找到对应类!", e); } return null; } } ================================================ FILE: src/main/groovy/com/funtester/frame/execute/Concurrent.java ================================================ package com.funtester.frame.execute; import com.funtester.base.bean.PerformanceResultBean; import com.funtester.base.constaint.ThreadBase; import com.funtester.config.Constant; import com.funtester.frame.Save; import com.funtester.frame.SourceCode; import com.funtester.utils.Time; import com.funtester.utils.RWUtil; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import java.util.ArrayList; import java.util.List; import java.util.Vector; import java.util.concurrent.CountDownLatch; import java.util.concurrent.ExecutorService; import static java.util.stream.Collectors.toList; /** * 并发类,用于启动压力脚本 */ public class Concurrent extends SourceCode { private static Logger logger = LogManager.getLogger(Concurrent.class); /** * 开始时间 */ private long startTime; /** * 结束时间 */ private long endTime; /** * 任务描述 */ public String desc; /** * 任务集 */ public List threads = new ArrayList<>(); /** * 线程数 */ public int threadNum; /** * 执行失败总数 */ private int errorTotal; /** * 任务执行失败总数 */ private int failTotal; /** * 执行总数 */ private int executeTotal; /** * 用于记录所有请求时间 */ public static Vector allTimes = new Vector<>(); /** * 记录所有markrequest的信息 */ public static Vector requestMark = new Vector<>(); /** * 线程池 */ ExecutorService executorService; /** * 计数器 */ CountDownLatch countDownLatch; /** * @param thread 线程任务 * @param threadNum 线程数 * @param desc 任务描述 */ public Concurrent(ThreadBase thread, int threadNum, String desc) { this(threadNum, desc); range(threadNum).forEach(x -> threads.add(thread.clone())); } /** * @param threads 线程组 * @param desc 任务描述 */ public Concurrent(List threads, String desc) { this(threads.size(), desc); this.threads = threads; } private Concurrent(int threadNum, String desc) { this.threadNum = threadNum; this.desc = StatisticsUtil.getFileName(desc); executorService = ThreadPoolUtil.createFixedPool(threadNum); countDownLatch = new CountDownLatch(threadNum); } private Concurrent() { } /** * 执行多线程任务 * 默认取list中thread对象,丢入线程池,完成多线程执行,如果没有threadname,name默认采用desc+线程数作为threadname,去除末尾的日期 */ public PerformanceResultBean start() { Progress progress = new Progress(threads, StatisticsUtil.getTrueName(desc)); new Thread(progress).start(); startTime = Time.getTimeStamp(); for (int i = 0; i < threadNum; i++) { ThreadBase thread = threads.get(i); if (StringUtils.isBlank(thread.threadName)) thread.threadName = StatisticsUtil.getTrueName(desc) + i; thread.setCountDownLatch(countDownLatch); executorService.execute(thread); } shutdownService(executorService, countDownLatch); endTime = Time.getTimeStamp(); progress.stop(); threads.forEach(x -> { if (x.status()) failTotal++; errorTotal += x.errorNum; executeTotal += x.executeNum; }); logger.info("总计{}个线程,共用时:{} s,执行总数:{},错误数:{},失败数:{}", threadNum, Time.getTimeDiffer(startTime, endTime), executeTotal, errorTotal, failTotal); return over(); } /** * 关闭任务相关资源 * * @param executorService 线程池 * @param countDownLatch 计数器 */ private static void shutdownService(ExecutorService executorService, CountDownLatch countDownLatch) { try { countDownLatch.await(); executorService.shutdown(); } catch (InterruptedException e) { logger.warn("线程池关闭失败!", e); } } private PerformanceResultBean over() { Save.saveIntegerList(allTimes, DATA_Path.replace(LONG_Path, EMPTY) + StatisticsUtil.getFileName(threadNum, desc)); Save.saveStringListSync(Concurrent.requestMark, MARK_Path.replace(LONG_Path, EMPTY) + desc); allTimes = new Vector<>(); requestMark = new Vector<>(); return countQPS(threadNum, desc, Time.getTimeByTimestamp(startTime), Time.getTimeByTimestamp(endTime)); } /** * 计算结果 *

此结果仅供参考

* 此处因为start和end的不准确问题,所以采用改计算方法,与fixQPS有区别 * * @param name 线程数 */ public PerformanceResultBean countQPS(int name, String desc, String start, String end) { List strings = RWUtil.readTxtFileByLine(Constant.DATA_Path + StatisticsUtil.getFileName(name, desc)); int size = strings.size(); List data = strings.stream().map(x -> changeStringToInt(x)).collect(toList()); int sum = data.stream().mapToInt(x -> x).sum(); String statistics = StatisticsUtil.statistics(data, desc, threadNum); int rt = sum / size; double qps = 1000.0 * name / rt; double qps2 = (executeTotal + errorTotal) * 1000.0 / (endTime - startTime); return new PerformanceResultBean(desc, start, end, name, size, rt, qps, qps2, getPercent(executeTotal, errorTotal), getPercent(threadNum, failTotal), executeTotal, statistics); } /** * 用于做后期的计算 * * @param name * @param desc * @return */ public PerformanceResultBean countQPS(int name, String desc) { return countQPS(name, desc, Time.getDate(), Time.getDate()); } } ================================================ FILE: src/main/groovy/com/funtester/frame/execute/ExecuteGroovy.java ================================================ package com.funtester.frame.execute; import com.funtester.frame.SourceCode; import com.funtester.utils.FileUtil; import groovy.lang.GroovyClassLoader; import groovy.lang.GroovyObject; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import java.io.File; import java.io.IOException; import java.lang.reflect.Method; import java.util.ArrayList; import java.util.List; /** * groovy脚本执行类,用户执行上传的groovy脚本,功能简单,使用未做封装,将就用一下 */ public class ExecuteGroovy extends SourceCode { private static Logger logger = LogManager.getLogger(ExecuteSource.class); /** * 路径 */ private String path; /** * 文件名 */ private String name; /** * 所有的脚本文件 */ private List files = new ArrayList<>(); /** * Groovy类加载器 */ private GroovyClassLoader loader = new GroovyClassLoader(getClass().getClassLoader()); /** * Groovy对象 */ private GroovyObject groovyObject; /** * 加载类 */ private Class groovyClass; public ExecuteGroovy(String path, String name) { this.path = path; this.name = name; getGroovyObject(); } /** * 执行一个类的所有方法 */ public void executeAllMethod(String path) { FileUtil.getAllFile(path); if (files == null) return; files.forEach((file) -> new ExecuteGroovy(file, EMPTY).executeMethodByPath()); } /** * 执行某个类的方法,需要做过滤 * * @return */ public void executeMethodByName() { if (new File(path).isDirectory()) { logger.warn("文件类型错误!"); } try { groovyObject.invokeMethod(name, null); } catch (Exception e) { logger.warn("执行" + name + "失败!", e); } } /** * 根据path执行相关方法 */ public void executeMethodByPath() { Method[] methods = groovyClass.getDeclaredMethods();//获取类方法,此处方法比较多,需过滤 for (Method method : methods) { String methodName = method.getName(); if (methodName.contains("test") || methodName.equals("main")) { groovyObject.invokeMethod(methodName, null); } } } /** * 获取groovy对象和执行类 */ public void getGroovyObject() { try { groovyClass = loader.parseClass(new File(path));//创建类 } catch (IOException e) { logger.warn("groovy类加载失败!", e); } try { groovyObject = (GroovyObject) groovyClass.newInstance();//创建类对象 } catch (InstantiationException e) { logger.warn("创建对象失败!", e); } catch (IllegalAccessException e) { logger.warn("非法异常!", e); } } /** * 获取文件下所有的groovy脚本,不支持递归查询 * * @return */ public List getAllGroovyFile(String path) { File file = new File(path); if (file.isFile()) { files.add(path); return files; } File[] files1 = file.listFiles(); int size = files1.length; for (int i = 0; i < size; i++) { String name = files1[i].getAbsolutePath(); if (name.endsWith(".groovy")) files.add(name); } return files; } } ================================================ FILE: src/main/groovy/com/funtester/frame/execute/ExecuteSource.java ================================================ package com.funtester.frame.execute; import com.alibaba.fastjson.JSON; import com.funtester.base.exception.FailException; import com.funtester.config.Constant; import com.funtester.frame.SourceCode; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import java.io.File; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.net.URL; import java.util.ArrayList; import java.util.Arrays; import java.util.List; public class ExecuteSource extends SourceCode { private static Logger logger = LogManager.getLogger(ExecuteSource.class); /** * 执行包内所有类的非 main 方法 * * @param packageName */ public static void executeAllMethodInPackage(String packageName) { List classNames = getClassName(packageName); if (classNames != null) { for (String className : classNames) { String path = packageName + "." + className; executeAllMethod(path);// 执行所有方法 } } } /** * 执行一个类的方法内所有的方法,非 main,执行带参方法的代码过滤 * * @param path 类名 */ public static void executeAllMethod(String path) { Class c = null; Object object = null; try { c = Class.forName(path); object = c.newInstance(); } catch (Exception e) { e.printStackTrace(); } Method[] methods = c.getDeclaredMethods(); for (Method method : methods) { try { method.invoke(object); } catch (IllegalAccessException e) { logger.warn("非法访问导致反射方法执行失败!", e); } catch (InvocationTargetException e) { logger.warn("反射调用目标异常导致方法执行失败!", e); } catch (Exception e) { logger.warn("反射方法执行失败!", e); } finally { sleep(Constant.EXECUTE_GAP_TIME); } } } /** * 提供给命令行main方法使用 *

防止编译报错,用list绕一圈

* * @param params */ public static void executeMethod(List params) { Object[] objects = params.subList(1, params.size()).toArray(); executeMethod(params.get(0), objects); } /** * 提供给命令行main方法使用 *

防止编译报错,用list绕一圈

* * @param params */ public static void executeMethod(String[] params) { executeMethod(Arrays.asList(params)); } /** * 执行具体的某一个方法,提供内部方法调用 * * @param path */ public static void executeMethod(String path, Object... paramsTpey) { int length = paramsTpey.length; if (length % 2 == 1) FailException.fail("参数个数错误,应该是偶数"); String className = path.substring(0, path.lastIndexOf(".")); String methodname = path.substring(className.length() + 1); Class c = null; Object object = null; try { c = Class.forName(className); object = c.newInstance(); } catch (InstantiationException | IllegalAccessException | ClassNotFoundException e) { logger.warn("创建实例对象时错误:{}", className, e); } Method[] methods = c.getDeclaredMethods(); for (Method method : methods) { if (!method.getName().equalsIgnoreCase(methodname)) continue; try { Class[] classs = new Class[length / 2]; for (int i = 0; i < paramsTpey.length; i = +2) { classs[i / 2] = Class.forName(paramsTpey[i].toString());//此处基础数据类型的参数会导致报错,但不影响下面的调用 } method = c.getMethod(method.getName(), classs); } catch (NoSuchMethodException | ClassNotFoundException e) { logger.warn("方法属性处理错误!", e); } try { Object[] ps = new Object[length / 2]; for (int i = 1; i < paramsTpey.length; i = +2) { String name = paramsTpey[i - 1].toString(); String param = paramsTpey[i].toString(); Object p = param; if (name.contains("Integer")) { p = Integer.parseInt(param); } else if (name.contains("JSON")) { p = JSON.parseObject(param); } ps[i / 2] = p; } method.invoke(object, ps); } catch (IllegalAccessException | InvocationTargetException e) { logger.warn("反射执行方法失败:{}", path, e); } break; } } /** * 获取当前类的所有用例方法名 * * @param path * @return */ public static List getAllMethodName(String path) { List methods = new ArrayList<>(); Class c = null; Object object = null; try { c = Class.forName(path); object = c.newInstance(); } catch (Exception e) { FailException.fail("初始化对象失败:" + path); } Method[] all = c.getDeclaredMethods(); for (int i = 0; i < all.length; i++) { String str = all[i].getName(); methods.add(str); } return methods; } /** * 获取某包下所有类 * * @param packageName 包名 * @return 类的完整名称 */ public static List getClassName(String packageName) { List fileNames = new ArrayList<>(); ClassLoader loader = Thread.currentThread().getContextClassLoader();// 获取当前位置 String packagePath = packageName.replace(".", Constant.OR);// 转化路径,Linux 系统 URL url = loader.getResource(packagePath);// 具体路径 if (url == null || !"file".equals(url.getProtocol())) { FailException.fail("获取包路径失败!"); } File file = new File(url.getPath()); File[] childFiles = file.listFiles(); for (File childFile : childFiles) { String path = childFile.getPath(); if (path.endsWith(".class")) { path = path.substring(path.lastIndexOf(OR) + 1, path.lastIndexOf(".")); fileNames.add(path); } } return fileNames; } } ================================================ FILE: src/main/groovy/com/funtester/frame/execute/FixedQpsConcurrent.java ================================================ package com.funtester.frame.execute; import com.funtester.base.bean.PerformanceResultBean; import com.funtester.base.constaint.FixedQpsThread; import com.funtester.config.Constant; import com.funtester.config.HttpClientConstant; import com.funtester.frame.Save; import com.funtester.frame.SourceCode; import com.funtester.httpclient.GCThread; import com.funtester.utils.Time; import com.funtester.utils.RWUtil; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import java.util.ArrayList; import java.util.List; import java.util.Vector; import java.util.concurrent.ExecutorService; import java.util.concurrent.TimeUnit; import java.util.concurrent.atomic.AtomicInteger; import static java.util.stream.Collectors.toList; /** * 并发类,用于启动压力脚本 */ public class FixedQpsConcurrent extends SourceCode { private static Logger logger = LogManager.getLogger(FixedQpsConcurrent.class); public static boolean key = false; public static AtomicInteger executeTimes = new AtomicInteger(0); public static AtomicInteger errorTimes = new AtomicInteger(0); public static Vector marks = new Vector<>(); /** * 基础任务对象 */ public FixedQpsThread baseThread; /** * 用于记录所有请求时间 */ public static Vector allTimes = new Vector<>(); /** * 开始时间 */ public long startTime; /** * 结束时间 */ public long endTime; /** * 任务队列的长度,因为会循环去那队列的任务 */ public int queueLength; /** * 任务描述 */ public String desc; /** * 任务集 */ public List threads = new ArrayList<>(); /** * 线程池 */ ExecutorService executorService; /** * @param thread 线程任务 * @param desc 任务描述 */ public FixedQpsConcurrent(FixedQpsThread thread, String desc) { this(desc); this.queueLength = 1; threads.add(thread); baseThread = thread; } /** * @param threads 线程组 * @param desc 任务描述 */ public FixedQpsConcurrent(List threads, String desc) { this(desc); this.threads = threads; baseThread = threads.get(0); this.queueLength = threads.size(); } /** * 初始化连接池 */ private FixedQpsConcurrent(String desc) { this.desc = StatisticsUtil.getFileName(desc); executorService = ThreadPoolUtil.createPool(HttpClientConstant.THREADPOOL_CORE, HttpClientConstant.THREADPOOL_MAX, HttpClientConstant.THREAD_ALIVE_TIME); } private FixedQpsConcurrent() { } /** * 重置连接池,用以改变并发能力 * * @param core * @param max */ public void initPool(int core, int max) { executorService = ThreadPoolUtil.createPool(core, max, HttpClientConstant.THREAD_ALIVE_TIME); } /** * 执行多线程任务 * 默认取list中thread对象,丢入线程池,完成多线程执行,如果没有threadname,name默认采用desc+线程数作为threadname,去除末尾的日期 */ public PerformanceResultBean start() { key = false; Progress progress = new Progress(threads, StatisticsUtil.getTrueName(desc), executeTimes); new Thread(progress).start(); boolean isTimesMode = baseThread.isTimesMode; int limit = baseThread.limit; int qps = baseThread.qps; long interval = 1_000_000_000 / qps;//此处单位1s=1000ms,1ms=1000000ns startTime = Time.getTimeStamp(); AidThread aidThread = new AidThread(); new Thread(aidThread).start(); while (true) { executorService.execute(threads.get(limit-- % queueLength).clone()); if (key ? true : isTimesMode ? limit < 1 : Time.getTimeStamp() - startTime > limit) break; sleep(interval); } endTime = Time.getTimeStamp(); aidThread.stop(); progress.stop(); GCThread.stop(); try { executorService.shutdown(); executorService.awaitTermination(HttpClientConstant.WAIT_TERMINATION_TIMEOUT, TimeUnit.SECONDS);//此方法需要在shutdown方法执行之后执行 } catch (InterruptedException e) { logger.error("线程池等待任务结束失败!", e); } logger.info("总计执行 {} ,共用时:{} s,执行总数:{},错误数:{}!", baseThread.isTimesMode ? baseThread.limit + "次任务" : "秒", Time.getTimeDiffer(startTime, endTime), executeTimes, errorTimes); return over(); } private PerformanceResultBean over() { key = true; Save.saveIntegerList(allTimes, DATA_Path.replace(LONG_Path, EMPTY) + StatisticsUtil.getFileName(queueLength, desc)); Save.saveStringListSync(marks, MARK_Path.replace(LONG_Path, EMPTY) + desc); allTimes = new Vector<>(); marks = new Vector<>(); int executeNum = executeTimes.getAndSet(0); int errorNum = errorTimes.getAndSet(0); return countQPS(queueLength, desc, startTime, endTime, executeNum, errorNum); } /** * 计算结果 *

此结果仅供参考

* 由于fixQPS模型没有固定线程数,所以智能采取QPS=Q/T的计算,与concurrent有区别 * * @param name 线程数 */ public PerformanceResultBean countQPS(int name, String desc, long start, long end, int executeNum, int errorNum) { List strings = RWUtil.readTxtFileByLine(Constant.DATA_Path + StatisticsUtil.getFileName(name, desc)); int size = strings.size(); List data = strings.stream().map(x -> changeStringToInt(x)).collect(toList()); int sum = data.stream().mapToInt(x -> x).sum(); String statistics = StatisticsUtil.statistics(data, desc, name); double qps = executeNum * 1000.0 / (end - start); int qps2 = baseThread.qps; return new PerformanceResultBean(desc, Time.getTimeByTimestamp(start), Time.getTimeByTimestamp(end), name, size, sum / size, qps, qps2, getPercent(executeNum, errorNum), 0, executeNum, statistics); } /** * 补偿线程,如果超过一半QPS量,才会进行补偿,补偿速率为每秒20个 */ class AidThread implements Runnable { private boolean key = true; int i; public AidThread() { } @Override public void run() { logger.info("补偿线程开始!"); try { while (key) { sleep(HttpClientConstant.LOOP_INTERVAL); int actual = executeTimes.get(); int qps = baseThread.qps; long expect = (Time.getTimeStamp() - FixedQpsConcurrent.this.startTime) / 2000 * qps; if (expect > actual + qps) { logger.info("期望执行数:{},实际执行数:{},设置QPS:{}", expect, actual, qps); range((int) expect - actual).forEach(x -> { sleep(0.05); if (!executorService.isShutdown()) { executorService.execute(threads.get(this.i++ % queueLength).clone()); } }); } } logger.info("补偿线程结束!"); } catch (Exception e) { logger.error("补偿线程发生错误!", e); } } public void stop() { key = false; } } } ================================================ FILE: src/main/groovy/com/funtester/frame/execute/Progress.java ================================================ package com.funtester.frame.execute; import com.funtester.base.constaint.FixedQpsThread; import com.funtester.base.constaint.ThreadBase; import com.funtester.base.constaint.ThreadLimitTimeCount; import com.funtester.base.constaint.ThreadLimitTimesCount; import com.funtester.base.exception.ParamException; import com.funtester.config.HttpClientConstant; import com.funtester.frame.SourceCode; import com.funtester.utils.StringUtil; import com.funtester.utils.Time; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import java.util.ArrayList; import java.util.List; import java.util.concurrent.atomic.AtomicInteger; import java.util.stream.Collectors; /** * 用于异步展示性能测试进度的多线程类 * * @param 多线程任务{@link ThreadBase}对象的实现子类 */ public class Progress extends SourceCode implements Runnable { private static Logger logger = LogManager.getLogger(Progress.class); /** * 会长 */ private static final String SUFFIX = "QPS变化曲线"; /** * 记录每一次获取QPS的值,可能用于结果展示 */ public List qs = new ArrayList<>(); /** * 多线程任务类对象 */ private List threads; /** * 线程数,用于计算实时QPS */ private int threadNum; /** * 进度条的长度 */ private static final int LENGTH = 67; /** * 标志符号 */ private static final String ONE = getPart(3); /** * 总开关,是否运行,默认true */ private boolean st = true; /** * 是否次数模型 */ private boolean isTimesMode; /** * 用于区分固定QPS请求模型,这里不计算固定QPS模型中的实时QPS */ private boolean canCount; /** * 多线程任务基类对象,本类中不处理,只用来获取值,若使用的话请调用clone()方法 */ private F base; /** * 在固定QPS模式中使用 */ private AtomicInteger excuteNum; /** * 限制条件 */ private int limit; /** * 非精确时间,误差可以忽略 */ private long startTime = Time.getTimeStamp(); /** * 描述 */ private String taskDesc; /** * 固定线程模型 * * @param threads * @param desc */ public Progress(final List threads, String desc) { this.threads = threads; this.threadNum = threads.size(); this.taskDesc = desc; this.base = threads.get(0); init(); } /** * 适配固定QPS模型 * * @param threads * @param desc * @param excuteNum */ public Progress(final List threads, String desc, final AtomicInteger excuteNum) { this.threads = threads; this.threadNum = threads.size(); this.taskDesc = desc; this.base = threads.get(0); init(); } /** * 初始化对象,对istimesMode和limit赋值 */ private void init() { if (base instanceof ThreadLimitTimeCount) { this.isTimesMode = false; this.canCount = true; this.limit = ((ThreadLimitTimeCount) base).time; } else if (base instanceof ThreadLimitTimesCount) { this.isTimesMode = true; this.canCount = true; this.limit = ((ThreadLimitTimesCount) base).times; } else if (base instanceof FixedQpsThread) { FixedQpsThread fix = (FixedQpsThread) base; this.canCount = false; this.isTimesMode = fix.isTimesMode; this.limit = fix.limit; } else { ParamException.fail("创建进度条对象失败!"); } } @Override public void run() { double pro = 0; while (st) { sleep(HttpClientConstant.LOOP_INTERVAL); pro = isTimesMode ? base.executeNum == 0 ? FixedQpsConcurrent.executeTimes.get() * 1.0 / limit : base.executeNum * 1.0 / limit : (Time.getTimeStamp() - startTime) * 1.0 / limit; if (pro > 0.95) break; if (st) logger.info("{}进度:{} {} ,当前QPS: {}", taskDesc, getManyString(ONE, (int) (pro * LENGTH)), getPercent(pro * 100), getQPS()); } } /** * 获取某一个时刻的QPS * * @return */ private int getQPS() { int qps = 0; if (canCount) { List times = new ArrayList<>(); for (int i = 0; i < threadNum; i++) { List costs = threads.get(i).costs; int size = costs.size(); if (size < 3) continue; times.add(costs.get(size - 1)); times.add(costs.get(size - 2)); } qps = times.isEmpty() ? 0 : (int) (1000 * threadNum / (times.stream().collect(Collectors.summarizingInt(x -> x)).getAverage())); } else { qps = excuteNum.get() / (int) (Time.getTimeStamp() - startTime); } qs.add(qps); return qps; } /** * 关闭线程,防止死循环 */ public void stop() { st = false; logger.info("{}进度:{} {}", taskDesc, getManyString(ONE, LENGTH), "100%"); printQPS(); } /** * 打印QPS变化曲线 */ private void printQPS() { int size = qs.size(); if (size < 5) return; if (size <= BUCKET_SIZE) { output(StatisticsUtil.draw(qs, StringUtil.center(taskDesc + SUFFIX, size * 3))); } else { double v = size * 1.0 / BUCKET_SIZE; List qpss = range(BUCKET_SIZE).mapToObj(x -> qs.get((int) (x * v))).collect(Collectors.toList()); output(StatisticsUtil.draw(qpss, StringUtil.center(taskDesc + SUFFIX, BUCKET_SIZE * 3))); } } } ================================================ FILE: src/main/groovy/com/funtester/frame/execute/StatisticsUtil.java ================================================ package com.funtester.frame.execute; import com.funtester.config.Constant; import com.funtester.utils.StringUtil; import com.funtester.utils.Time; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; import java.util.ArrayList; import java.util.Arrays; import java.util.Collections; import java.util.List; import java.util.stream.Collectors; import static com.funtester.frame.SourceCode.getManyString; import static com.funtester.frame.SourceCode.range; import static java.util.stream.Collectors.toList; /** * 用于性能测试数据统计工具类,主要是统计常用指标QPS,rt,时间,以及图形化输出测试结果 * *

Demo * * FunTester300线程 * * >>响应时间分布图,横轴排序分成桶的序号,纵轴每个桶的中位数<< * --<中位数数据最小值为:55 ms,最大值:1255 ms>-- * ██ * ██ * ██ * ██ * ██ * ▃▃ ██ * ▃▃ ▅▅ ▇▇ ██ ██ * ▇▇ ██ ██ ██ ██ ██ * ▃▃ ██ ██ ██ ██ ██ ██ * ▁▁ ██ ██ ██ ██ ██ ██ ██ * ▁▁ ██ ██ ██ ██ ██ ██ ██ ██ * ██ ██ ██ ██ ██ ██ ██ ██ ██ * ▇▇ ██ ██ ██ ██ ██ ██ ██ ██ ██ * ▇▇ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ * ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ * ▃▃ ▇▇ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ * ▃▃ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ * ▂▂ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ * ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ * ▅▅ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ * ▃▃ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ * ▁▁ ▂▂ ▃▃ ▄▄ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ * ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ ██ *

*/ public class StatisticsUtil extends Constant { /** * 将性能测试数据图表展示,需要等宽字体显示 * *

* 将数据排序,然后按照循序分桶,选择桶中中位数作代码,通过二维数组转化成柱状图 *

* 生成统计结果数组大小{@link com.funtester.config.Constant#BUCKET_SIZE},小于{@link com.funtester.config.Constant#BUCKET_SIZE}平方的数据量不予以统计 * * @param data 性能测试数据,也可以其他统计数据 * @return */ public static String statistics(List data, String title, int threadNum) { if (data.size() < BUCKET_SIZE * BUCKET_SIZE) return "数据量太少,无法绘图!";//过滤少量数据 List nums = batchNums(data); 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)); } /** * 根据数组画图,无序亦可 *

* 此处注意title处理调用center方法的时候,需要data.size()乘以3才是正确的长度,一个长度包含一个空格和两个特殊字符 *

* * @param data * @param title * @return */ public static String draw(int[] data, String title) { List integers = Arrays.asList(ArrayUtils.toObject(data)); return draw(integers, title); } /** * 根据数组画图,无序亦可 *

* 此处注意title处理调用center方法的时候,需要data.size()乘以3才是正确的长度,一个长度包含一个空格和两个特殊字符 *

* * @param data * @param title * @return */ public static String draw(List data, String title) { int largest = Collections.max(data); int buket = data.size(); String[][] map = data.stream().map(x -> getPercent(x, largest, buket)).collect(toList()).toArray(new String[buket][buket]);//转换成string二维数组 String[][] result = new String[buket][buket]; /*将二维数组反转成竖排*/ for (int i = 0; i < buket; i++) { for (int j = 0; j < buket; j++) { result[i][j] = getManyString(map[j][buket - 1 - i], 2) + SPACE_1; } } StringBuffer table = new StringBuffer(LINE + StringUtil.center(title, buket) + LINE + LINE); range(buket).forEach(x -> table.append(Arrays.asList(result[x]).stream().collect(Collectors.joining()) + LINE)); return table.toString(); } /** * 分割数组,变成可以图形化的数组 * * @param data * @return */ public static List batchNums(List data) { int size = data.size();//获取总数据量大小 Collections.sort(data); return new ArrayList() {{ range(1, BUCKET_SIZE + 1).forEach(x -> add(data.get(size * x / BUCKET_SIZE - size / BUCKET_SIZE / 2))); }}; } /** * 将数据转化成string数组 * * @param part 数据 * @param total 基准数据,默认最大的中位数 * @param length * @return */ private static String[] getPercent(int part, int total, int length) { int i = part * 8 * length / total; int prefix = i / 8; int suffix = i % 8; String s = getManyString(getPercent(8), prefix) + (prefix == length ? EMPTY : getPercent(suffix) + getManyString(SPACE_1, length - prefix - 1)); return s.split(EMPTY); } /** * 统一处理压测原始数据保存文件名 * * @param thread * @param desc * @return */ public static String getFileName(int thread, String desc) { return desc + CONNECTOR + thread; } /** * 用于处理初始化concurrent的desc,主要处理后缀 * * @param desc * @return */ public static String getFileName(String desc) { return desc + Time.markDate(); } /** * 用于处理title,除去标记数字 * * @param desc * @return */ public static String getTrueName(String desc) { if (StringUtils.isEmpty(desc)) return EMPTY; return desc.replaceAll("\\d{6}$", EMPTY); } } ================================================ FILE: src/main/groovy/com/funtester/frame/execute/ThreadPoolUtil.groovy ================================================ package com.funtester.frame.execute; import java.util.concurrent.*; /** * Java线程池Demo */ class ThreadPoolUtil { /** * 重建可变线程池 * corePoolSize:核心池的大小,这个参数跟后面讲述的线程池的实现原理有非常大的关系。在创建了线程池后,默认情况下,线程池中并没有任何线程,而是等待有任务到来才创建线程去执行任务,除非调用了prestartAllCoreThreads()或者prestartCoreThread()方法,从这2个方法的名字就可以看出,是预创建线程的意思,即在没有任务到来之前就创建corePoolSize个线程或者一个线程。默认情况下,在创建了线程池后,线程池中的线程数为0,当有任务来之后,就会创建一个线程去执行任务,当线程池中的线程数目达到corePoolSize后,就会把到达的任务放到缓存队列当中; * maximumPoolSize:线程池最大线程数,这个参数也是一个非常重要的参数,它表示在线程池中最多能创建多少个线程; * keepAliveTime:表示线程没有任务执行时最多保持多久时间会终止。默认情况下,只有当线程池中的线程数大于corePoolSize时,keepAliveTime才会起作用,直到线程池中的线程数不大于corePoolSize,即当线程池中的线程数大于corePoolSize时,如果一个线程空闲的时间达到keepAliveTime,则会终止,直到线程池中的线程数不超过corePoolSize。但是如果调用了allowCoreThreadTimeOut(boolean)方法,在线程池中的线程数不大于corePoolSize时,keepAliveTime参数也会起作用,直到线程池中的线程数为0; * unit:参数keepAliveTime的时间单位,有7种取值,在TimeUnit类中有7种静态属性: * workQueue:一个阻塞队列,用来存储等待执行的任务,这个参数的选择也很重要,会对线程池的运行过程产生重大影响,一般来说,这里的阻塞队列有以下几种选择:ArrayBlockingQueue;LinkedBlockingQueue; SynchronousQueue; *   ArrayBlockingQueue和PriorityBlockingQueue使用较少,一般使用LinkedBlockingQueue和Synchronous。线程池的排队策略与BlockingQueue有关。 * threadFactory:线程工厂,主要用来创建线程; * handler:表示当拒绝处理任务时的策略,有以下四种取值: * ThreadPoolExecutor.AbortPolicy:丢弃任务并抛出RejectedExecutionException异常。 * ThreadPoolExecutor.DiscardPolicy:也是丢弃任务,但是不抛出异常。 * ThreadPoolExecutor.DiscardOldestPolicy:丢弃队列最前面的任务,然后重新尝试执行任务(重复此过程) * ThreadPoolExecutor.CallerRunsPolicy:由调用线程处理该任务 * * @param core 核心线程数 * @param max 最大线程数 * @param liveTime 空闲时间 * @return */ static ThreadPoolExecutor createPool(int core = 5, int max = 20, int liveTime = 5) { return new ThreadPoolExecutor(core, max, liveTime, TimeUnit.SECONDS, new LinkedBlockingDeque(1000)); } /** * 定长的线程池 * * @param size * @return */ static ExecutorService createFixedPool(int size = 10) { return Executors.newFixedThreadPool(size); } /** * 缓存线程池,无限长度 * * @return */ static ExecutorService createCachePool() { return Executors.newCachedThreadPool(); } /*获取线程安全的单例的线程池 static ThreadPoolExecutor getSingleThreadPoolExecutor(AtomicInteger atomicInteger) { if (singleThreadPoolExecutor == null){ synchronized (objectLock){ if (singleThreadPoolExecutor == null){ singleThreadPoolExecutor = new ThreadPoolExecutor(1, 1, 5, TimeUnit.SECONDS, new LinkedBlockingQueue(100), new ThreadFactory() { @Override Thread newThread(Runnable runnable) { Thread thread = new Thread(runnable); thread.setName("UserCenter-business-" + atomicInteger.getAndIncrement()); return thread; } }, new ThreadPoolExecutor.CallerRunsPolicy()); } } } return singleThreadPoolExecutor; } */ } ================================================ FILE: src/main/groovy/com/funtester/frame/thread/FixedQpsHeaderMark.groovy ================================================ package com.funtester.frame.thread import com.funtester.base.constaint.ThreadBase import com.funtester.base.exception.ParamException import com.funtester.base.interfaces.MarkRequest import com.funtester.frame.SourceCode import org.apache.http.client.methods.HttpRequestBase import java.util.concurrent.atomic.AtomicInteger /** * 针对固定QPS模式的多线程对象的标记类 */ class FixedQpsHeaderMark extends SourceCode implements MarkRequest, Cloneable, Serializable { private static final long serialVersionUID = -158942567078477L; private static volatile AtomicInteger num = new AtomicInteger(10000); String headerName; @Override String mark(ThreadBase threadBase) { if (threadBase instanceof RequestTimesFixedQps) { RequestTimesFixedQps req = (RequestTimesFixedQps) threadBase; return mark(req.t); } else if (threadBase instanceof RequestTimeFixedQps) { RequestThreadTimes req = (RequestTimeFixedQps) threadBase; return mark(req.t); } else { ParamException.fail(threadBase.getClass().toString()); } EMPTY; } /** * 标记请求 * * @param base * @return */ @Override String mark(HttpRequestBase base) { base.removeHeaders(headerName); String value = 8 + EMPTY + num.getAndIncrement(); base.addHeader(headerName, value); value; } @Override FixedQpsHeaderMark clone() { new FixedQpsHeaderMark(headerName); } FixedQpsHeaderMark(String headerName) { this.headerName = headerName; } private FixedQpsHeaderMark() { } } ================================================ FILE: src/main/groovy/com/funtester/frame/thread/FixedQpsParamMark.java ================================================ package com.funtester.frame.thread; import com.funtester.base.constaint.ThreadBase; import com.funtester.base.interfaces.MarkThread; import com.funtester.frame.SourceCode; import java.io.Serializable; import java.util.concurrent.atomic.AtomicInteger; /** * 用于非单纯的http请求以及非HTTP请求,没有httprequestbase对象的标记方法,自己实现的虚拟类,可用户标记header固定字段或者随机参数,使用T作为参数载体,目前只能使用在T为string类才行 */ public class FixedQpsParamMark extends SourceCode implements MarkThread, Cloneable, Serializable { private static final long serialVersionUID = 2135701056209833015L; public static AtomicInteger num = new AtomicInteger(10000); /** * 用于标记执行线程 */ String name; @Override public String mark(ThreadBase threadBase) { return name + num.getAndIncrement(); } @Override public FixedQpsParamMark clone() { FixedQpsParamMark paramMark = new FixedQpsParamMark(this.name); return paramMark; } private FixedQpsParamMark() { name = EMPTY; } public FixedQpsParamMark(String name) { this(); this.name = name; } } ================================================ FILE: src/main/groovy/com/funtester/frame/thread/HeaderMark.java ================================================ package com.funtester.frame.thread; import com.funtester.base.constaint.ThreadBase; import com.funtester.base.exception.ParamException; import com.funtester.base.interfaces.MarkRequest; import com.funtester.frame.SourceCode; import com.funtester.utils.StringUtil; import org.apache.http.client.methods.HttpRequestBase; import java.io.Serializable; import java.util.concurrent.atomic.AtomicInteger; public class HeaderMark extends SourceCode implements MarkRequest, Cloneable, Serializable { private static final long serialVersionUID = -1595942567071153477L; public static AtomicInteger threadName = new AtomicInteger(getRandomIntRange(1000, 9000)); String headerName; private String m; int num = getRandomIntRange(100, 999) * 1000; @Override public String mark(ThreadBase threadBase) { if (threadBase instanceof RequestThreadTime) { RequestThreadTime req = (RequestThreadTime) threadBase; return mark(req.f); } else if (threadBase instanceof RequestThreadTimes) { RequestThreadTimes req = (RequestThreadTimes) threadBase; return mark(req.f); } else { ParamException.fail(threadBase.getClass().toString()); } return EMPTY; } /** * 标记请求 * * @param base * @return */ @Override public String mark(HttpRequestBase base) { base.removeHeaders(headerName); String value = m + num++; base.addHeader(headerName, value); return value; } @Override public HeaderMark clone() { return new HeaderMark(headerName); } public HeaderMark(String headerName) { this.headerName = headerName; this.m = DEFAULT_STRING + StringUtil.getStringWithoutNum(4) + threadName.getAndIncrement(); } public HeaderMark() { } } ================================================ FILE: src/main/groovy/com/funtester/frame/thread/ParamMark.java ================================================ package com.funtester.frame.thread; import com.funtester.base.constaint.ThreadBase; import com.funtester.base.interfaces.MarkThread; import com.funtester.frame.SourceCode; import java.io.Serializable; import java.util.concurrent.atomic.AtomicInteger; /** * 用于非单纯的http请求以及非HTTP请求,没有httprequestbase对象的标记方法,自己实现的虚拟类,可用户标记header固定字段或者随机参数,使用T作为参数载体,目前只能使用在T为string类才行 */ public class ParamMark extends SourceCode implements MarkThread, Cloneable, Serializable { private static final long serialVersionUID = -5532592151245141262L; public static AtomicInteger threadName = new AtomicInteger(getRandomIntRange(1000, 9000)); /** * 用于标记执行线程 */ String name; int num = getRandomIntRange(100, 999) * 1000; @Override public String mark(ThreadBase threadBase) { return name + num++; } @Override public ParamMark clone() { ParamMark paramMark = new ParamMark(); return paramMark; } public ParamMark() { this.name = threadName.getAndIncrement() + EMPTY; } public ParamMark(String name) { this(); this.name = name; } } ================================================ FILE: src/main/groovy/com/funtester/frame/thread/QuerySqlThread.java ================================================ package com.funtester.frame.thread; import com.funtester.base.constaint.ThreadBase; import com.funtester.base.constaint.ThreadLimitTimesCount; import com.funtester.base.interfaces.IMySqlBasic; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import java.sql.SQLException; /** * 数据库多线程类,query方法类,区别于updatethread */ public class QuerySqlThread extends ThreadLimitTimesCount { private static final long serialVersionUID = 879371247008746883L; private static Logger logger = LogManager.getLogger(QuerySqlThread.class); String sql; IMySqlBasic base; public QuerySqlThread(IMySqlBasic base, String sql, int times) { this.times = times; this.sql = sql; this.base = base; } @Override public void before() { base.getConnection(); } @Override protected void doing() throws SQLException { base.executeQuerySql(sql); } @Override protected void after() { super.after(); base.mySqlOver(); } @Override public ThreadBase clone() { return null; } } ================================================ FILE: src/main/groovy/com/funtester/frame/thread/RequestThreadTime.java ================================================ package com.funtester.frame.thread; import com.funtester.base.constaint.ThreadLimitTimeCount; import com.funtester.base.interfaces.MarkThread; import com.funtester.httpclient.FunLibrary; import com.funtester.httpclient.FunRequest; import com.funtester.httpclient.GCThread; import org.apache.http.client.methods.HttpRequestBase; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; /** * http请求多线程类 */ public class RequestThreadTime extends ThreadLimitTimeCount { private static final long serialVersionUID = -6554503654885966097L; private static Logger logger = LogManager.getLogger(RequestThreadTime.class); /** * 单请求多线程多次任务构造方法 * * @param request 被执行的请求 * @param time 每个线程运行的次数 */ public RequestThreadTime(HttpRequestBase request, int time) { super(request, time, null); } /** * @param request 被执行的请求 * @param time 执行时间 * @param mark 标记类对象 */ public RequestThreadTime(HttpRequestBase request, int time, MarkThread mark) { super(request, time, mark); } protected RequestThreadTime() { super(); } @Override public void before() { super.before(); GCThread.starts(); } @Override protected void doing() throws Exception { FunLibrary.executeSimlple(f); } @Override public RequestThreadTime clone() { RequestThreadTime threadTime = new RequestThreadTime(); threadTime.time = this.time; threadTime.f = FunRequest.cloneRequest(f); threadTime.mark = mark == null ? null : mark.clone(); return threadTime; } } ================================================ FILE: src/main/groovy/com/funtester/frame/thread/RequestThreadTimes.java ================================================ package com.funtester.frame.thread; import com.funtester.base.constaint.ThreadLimitTimesCount; import com.funtester.base.interfaces.MarkThread; import com.funtester.httpclient.FunLibrary; import com.funtester.httpclient.FunRequest; import com.funtester.httpclient.GCThread; import org.apache.http.client.methods.HttpRequestBase; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; /** * http请求多线程类 */ public class RequestThreadTimes extends ThreadLimitTimesCount { private static final long serialVersionUID = 84690314667174004L; private static Logger logger = LogManager.getLogger(RequestThreadTimes.class); /** * 单请求多线程多次任务构造方法 * * @param request 被执行的请求 * @param times 每个线程运行的次数 */ public RequestThreadTimes(HttpRequestBase request, int times) { super(request, times, null); } /** * 应对对每个请求进行标记的情况 * * @param request * @param times * @param mark */ public RequestThreadTimes(HttpRequestBase request, int times, MarkThread mark) { super(request, times, mark); } protected RequestThreadTimes() { super(); } @Override public void before() { super.before(); GCThread.starts(); } /** * @throws Exception */ @Override protected void doing() throws Exception { FunLibrary.executeSimlple(f); } @Override public RequestThreadTimes clone() { RequestThreadTimes threadTimes = new RequestThreadTimes(); threadTimes.times = this.times; threadTimes.f = FunRequest.cloneRequest(f); threadTimes.mark = mark == null ? null : mark.clone(); return threadTimes; } } ================================================ FILE: src/main/groovy/com/funtester/frame/thread/RequestTimeFixedQps.java ================================================ package com.funtester.frame.thread; import com.funtester.base.constaint.FixedQpsThread; import com.funtester.base.interfaces.MarkRequest; import com.funtester.httpclient.FunLibrary; import com.funtester.httpclient.FunRequest; import com.funtester.httpclient.GCThread; import org.apache.http.client.methods.HttpRequestBase; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; public class RequestTimeFixedQps extends FixedQpsThread { private static final long serialVersionUID = -64206522585960792L; private static Logger logger = LogManager.getLogger(RequestTimeFixedQps.class); private RequestTimeFixedQps() { } public RequestTimeFixedQps(int qps, int time, MarkRequest markRequest, HttpRequestBase request) { super(request, time, qps, markRequest, false); } @Override public void before() { super.before(); GCThread.starts(); } @Override protected void doing() throws Exception { FunLibrary.executeSimlple(f); } @Override public RequestTimeFixedQps clone() { RequestTimeFixedQps newone = new RequestTimeFixedQps(); newone.f = FunRequest.cloneRequest(this.f); newone.mark = this.mark == null ? null : this.mark.clone(); newone.qps = this.qps; newone.isTimesMode = this.isTimesMode; newone.limit = this.limit; return newone; } } ================================================ FILE: src/main/groovy/com/funtester/frame/thread/RequestTimesFixedQps.java ================================================ package com.funtester.frame.thread; import com.funtester.base.constaint.FixedQpsThread; import com.funtester.base.interfaces.MarkRequest; import com.funtester.httpclient.FunLibrary; import com.funtester.httpclient.FunRequest; import com.funtester.httpclient.GCThread; import org.apache.http.client.methods.HttpRequestBase; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; public class RequestTimesFixedQps extends FixedQpsThread { private static final long serialVersionUID = 679065222134424087L; private static Logger logger = LogManager.getLogger(RequestTimesFixedQps.class); private RequestTimesFixedQps() { } public RequestTimesFixedQps(int qps, int times, MarkRequest markRequest, HttpRequestBase request) { super(request, times, qps, markRequest, true); } @Override public void before() { super.before(); GCThread.starts(); } @Override protected void doing() throws Exception { FunLibrary.executeSimlple(f); } @Override public RequestTimesFixedQps clone() { RequestTimesFixedQps newone = new RequestTimesFixedQps(); newone.f = FunRequest.cloneRequest(this.f); newone.mark = this.mark == null ? null : this.mark.clone(); newone.qps = this.qps; newone.isTimesMode = this.isTimesMode; newone.limit = this.limit; return newone; } } ================================================ FILE: src/main/groovy/com/funtester/frame/thread/UpdateSqlThread.java ================================================ package com.funtester.frame.thread; import com.funtester.base.constaint.ThreadBase; import com.funtester.base.constaint.ThreadLimitTimesCount; import com.funtester.base.interfaces.IMySqlBasic; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; /** * 数据库多线程类,update方法类,区别于querythread */ public class UpdateSqlThread extends ThreadLimitTimesCount { private static final long serialVersionUID = 5808571085138930143L; private static Logger logger = LogManager.getLogger(UpdateSqlThread.class); IMySqlBasic base; public UpdateSqlThread(IMySqlBasic base, String sql, int times) { this.times = times; this.f = sql; this.base = base; } @Override protected void doing() { base.executeUpdateSql(f); } @Override protected void after() { super.after(); base.mySqlOver(); } @Override public ThreadBase clone() { return null; } } ================================================ FILE: src/main/groovy/com/funtester/httpclient/ClientManage.java ================================================ package com.funtester.httpclient; import com.funtester.base.exception.FailException; import com.funtester.config.Constant; import com.funtester.config.HttpClientConstant; import com.funtester.utils.Regex; import org.apache.commons.lang3.StringUtils; import org.apache.http.HttpEntityEnclosingRequest; import org.apache.http.HttpHost; import org.apache.http.NoHttpResponseException; import org.apache.http.client.HttpRequestRetryHandler; import org.apache.http.client.config.CookieSpecs; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.methods.HttpUriRequest; import org.apache.http.client.protocol.HttpClientContext; import org.apache.http.config.ConnectionConfig; import org.apache.http.config.MessageConstraints; import org.apache.http.config.Registry; import org.apache.http.config.RegistryBuilder; import org.apache.http.conn.socket.ConnectionSocketFactory; import org.apache.http.conn.socket.PlainConnectionSocketFactory; import org.apache.http.conn.ssl.SSLConnectionSocketFactory; import org.apache.http.impl.client.CloseableHttpClient; import org.apache.http.impl.client.HttpClients; import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; import org.apache.http.impl.nio.client.CloseableHttpAsyncClient; import org.apache.http.impl.nio.client.HttpAsyncClients; import org.apache.http.impl.nio.conn.PoolingNHttpClientConnectionManager; import org.apache.http.impl.nio.reactor.DefaultConnectingIOReactor; import org.apache.http.impl.nio.reactor.IOReactorConfig; import org.apache.http.nio.reactor.ConnectingIOReactor; import org.apache.http.nio.reactor.IOReactorException; import org.apache.http.protocol.HttpContext; import org.apache.http.protocol.HttpCoreContext; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import javax.net.ssl.SSLContext; import javax.net.ssl.SSLException; import javax.net.ssl.TrustManager; import javax.net.ssl.X509TrustManager; import java.io.IOException; import java.io.InterruptedIOException; import java.net.SocketException; import java.net.UnknownHostException; import java.nio.charset.CodingErrorAction; import java.security.KeyManagementException; import java.security.NoSuchAlgorithmException; import java.security.cert.CertificateException; import java.util.concurrent.TimeUnit; /** * 连接池管理类 */ public class ClientManage { private static Logger logger = LogManager.getLogger(ClientManage.class); /** * ssl验证 */ private static SSLContext sslContext = createIgnoreVerifySSL(); /** * 请求超时控制器 */ private static RequestConfig requestConfig = getRequestConfig(); /** * 请求重试管理器 */ private static HttpRequestRetryHandler httpRequestRetryHandler = getHttpRequestRetryHandler(); /** * 连接池 */ private static PoolingHttpClientConnectionManager connManager = getPool(); /** * 异步连接池 */ private static PoolingNHttpClientConnectionManager NconnManager = getNPool(); /** * httpclient对象 */ public static CloseableHttpClient httpsClient = getCloseableHttpsClients(); /** * 异步连接池 */ public static CloseableHttpAsyncClient httpAsyncClient = getCloseableHttpAsyncClient(); /** * 获取连接池 * * @return */ private static PoolingHttpClientConnectionManager getPool() { PoolingHttpClientConnectionManager connManager = null; // 采用绕过验证的方式处理https请求 // 设置协议http和https对应的处理socket链接工厂的对象 Registry socketFactoryRegistry = RegistryBuilder.create().register("http", PlainConnectionSocketFactory.INSTANCE).register("https", new SSLConnectionSocketFactory(sslContext)).build(); connManager = new PoolingHttpClientConnectionManager(socketFactoryRegistry); // 消息约束 MessageConstraints messageConstraints = MessageConstraints.custom().setMaxHeaderCount(HttpClientConstant.MAX_HEADER_COUNT).setMaxLineLength(HttpClientConstant.MAX_LINE_LENGTH).build(); // 连接设置 ConnectionConfig connectionConfig = ConnectionConfig.custom().setMalformedInputAction(CodingErrorAction.IGNORE).setUnmappableInputAction(CodingErrorAction.IGNORE).setCharset(Constant.DEFAULT_CHARSET).setMessageConstraints(messageConstraints).build(); connManager.setDefaultConnectionConfig(connectionConfig); connManager.setMaxTotal(HttpClientConstant.MAX_TOTAL_CONNECTION); connManager.setDefaultMaxPerRoute(HttpClientConstant.MAX_PER_ROUTE_CONNECTION); return connManager; } /** * 获取异步连接池 * * @return */ private static PoolingNHttpClientConnectionManager getNPool() { IOReactorConfig ioReactorConfig = IOReactorConfig.custom().setIoThreadCount(Runtime.getRuntime().availableProcessors()).setConnectTimeout(HttpClientConstant.CONNECT_REQUEST_TIMEOUT).setSoTimeout(HttpClientConstant.SOCKET_TIMEOUT).build(); ConnectingIOReactor ioReactor = null; try { ioReactor = new DefaultConnectingIOReactor(ioReactorConfig); } catch (IOReactorException e) { logger.error("创建连接响应器失败!", e); } MessageConstraints messageConstraints = MessageConstraints.custom().setMaxHeaderCount(HttpClientConstant.MAX_HEADER_COUNT).setMaxLineLength(HttpClientConstant.MAX_LINE_LENGTH).build(); PoolingNHttpClientConnectionManager connManager = new PoolingNHttpClientConnectionManager(ioReactor); ConnectionConfig connectionConfig = ConnectionConfig.custom().setMalformedInputAction(CodingErrorAction.IGNORE).setUnmappableInputAction(CodingErrorAction.IGNORE).setCharset(Constant.DEFAULT_CHARSET).setMessageConstraints(messageConstraints).build(); connManager.setDefaultConnectionConfig(connectionConfig); connManager.setMaxTotal(HttpClientConstant.MAX_TOTAL_CONNECTION); connManager.setDefaultMaxPerRoute(HttpClientConstant.MAX_PER_ROUTE_CONNECTION); return connManager; } /** * 获取SSL套接字对象 重点重点:设置tls协议的版本 * * @return */ private static SSLContext createIgnoreVerifySSL() { SSLContext sslContext = null;// 创建套接字对象 try { sslContext = SSLContext.getInstance(HttpClientConstant.SSL_VERSION);// 指定TLS版本 } catch (NoSuchAlgorithmException e) { FailException.fail("创建套接字失败!" + e.getMessage()); } // 实现X509TrustManager接口,用于绕过验证 X509TrustManager trustManager = new X509TrustManager() { @Override public void checkClientTrusted(java.security.cert.X509Certificate[] paramArrayOfX509Certificate, String paramString) throws CertificateException { } @Override public void checkServerTrusted(java.security.cert.X509Certificate[] paramArrayOfX509Certificate, String paramString) throws CertificateException { } @Override public java.security.cert.X509Certificate[] getAcceptedIssuers() { return null; } }; try { sslContext.init(null, new TrustManager[]{trustManager}, null);// 初始化sslContext对象 } catch (KeyManagementException e) { logger.warn("初始化套接字失败!", e); } return sslContext; } /** * 获取重试控制器 * * @return */ private static HttpRequestRetryHandler getHttpRequestRetryHandler() { return new HttpRequestRetryHandler() { public boolean retryRequest(IOException exception, int executionCount, HttpContext context) { boolean log = log(exception, executionCount, context); if (log) logger.warn("请求发生重试! 次数: {}", executionCount); return log; } /**绕一圈,记录重试信息,避免错误日志影响观感 * @param exception * @param executionCount * @param context * @return */ private boolean log(IOException exception, int executionCount, HttpContext context) { if (executionCount + 1 > HttpClientConstant.TRY_TIMES) return false; logger.warn("请求发生错误:{}", exception.getMessage()); HttpClientContext clientContext = HttpClientContext.adapt(context); final Object request = clientContext.getAttribute(HttpCoreContext.HTTP_REQUEST); if (request instanceof HttpUriRequest) { HttpUriRequest uriRequest = (HttpUriRequest) request; logger.warn("请求失败接口URI:{}", uriRequest.getURI().toString()); } if (exception instanceof NoHttpResponseException) { return false; } else if (exception instanceof InterruptedIOException) { return true; } else if (exception instanceof UnknownHostException) { return false; } else if (exception instanceof SSLException) { return false; } else if (exception instanceof SocketException) { return false; } else { logger.warn("未记录的请求异常:{}", exception.getClass().getName()); } // 如果请求是幂等的,则不重试,HttpEntityEnclosingRequest类以及子类都是非幂等性的 if (!(request instanceof HttpEntityEnclosingRequest)) { return false; } return true; } }; } /** * 通过连接池获取https协议请求对象 *

* 增加默认的请求控制器,和请求配置,连接控制器,取消了cookiestore,单独解析响应set-cookie和发送请求的header,适配多用户同时在线的情况 *

* * @return */ private static CloseableHttpAsyncClient getCloseableHttpAsyncClient() { return HttpAsyncClients.custom().setConnectionManager(NconnManager).setSSLHostnameVerifier(SSLConnectionSocketFactory.ALLOW_ALL_HOSTNAME_VERIFIER).setSSLContext(sslContext).build(); } private static CloseableHttpClient getCloseableHttpsClients() { return HttpClients.custom().setConnectionManager(connManager).setRetryHandler(httpRequestRetryHandler).setDefaultRequestConfig(requestConfig).build(); } /** * 获取请求超时控制器 *

* cookieSpec:即cookie策略。参数为cookiespecs的一些字段。作用: * 1、如果网站header中有set-cookie字段时,采用默认方式可能会被cookie reject,无法写入cookie。将此属性设置成CookieSpecs.STANDARD_STRICT可避免此情况。 * 2、如果要想忽略cookie访问,则将此属性设置成CookieSpecs.IGNORE_COOKIES。 *

* * @return */ private static RequestConfig getRequestConfig() { return RequestConfig.custom().setConnectionRequestTimeout(HttpClientConstant.CONNECT_REQUEST_TIMEOUT).setConnectTimeout(HttpClientConstant.CONNECT_TIMEOUT).setSocketTimeout(HttpClientConstant.SOCKET_TIMEOUT).setCookieSpec(CookieSpecs.IGNORE_COOKIES).setRedirectsEnabled(false).build(); } /** * 获取代理配置项 * * @param ip * @param port * @return */ public static RequestConfig getProxyRequestConfig(String ip, int port) { 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(); } /** * 回收资源方法,关闭过期连接,关闭超时连接,用于另起线程回收连接池连接 */ public static void recyclingConnection() { connManager.closeExpiredConnections(); connManager.closeIdleConnections(HttpClientConstant.IDLE_TIMEOUT, TimeUnit.MILLISECONDS); } /** * 重新初始化连接池,用于临时改变超时和超时标准线的重置 *

* 会重置请求控制器,重置连接池和重试控制器 *

* 时间单位s,默认配置单位ms,自动乘以1000 * * @param timeout * @param accepttime * @param retrytimes * @param ip * @param port */ public static void init(int timeout, int accepttime, int retrytimes, String ip, int port) { HttpClientConstant.CONNECT_REQUEST_TIMEOUT = timeout * 1000; HttpClientConstant.CONNECT_TIMEOUT = timeout * 1000; HttpClientConstant.SOCKET_TIMEOUT = timeout * 1000; HttpClientConstant.MAX_ACCEPT_TIME = accepttime * 1000; HttpClientConstant.TRY_TIMES = retrytimes < 1 ? Constant.TEST_ERROR_CODE : retrytimes; requestConfig = StringUtils.isNoneBlank(ip) && Regex.isMatch(ip + ":" + port, Constant.HOST_REGEX) ? getProxyRequestConfig(ip, port) : getRequestConfig(); httpsClient = getCloseableHttpsClients(); httpRequestRetryHandler = getHttpRequestRetryHandler(); } } ================================================ FILE: src/main/groovy/com/funtester/httpclient/FunLibrary.java ================================================ package com.funtester.httpclient; import com.alibaba.fastjson.JSONException; import com.alibaba.fastjson.JSONObject; import com.funtester.base.bean.RequestInfo; import com.funtester.base.exception.ParamException; import com.funtester.base.exception.RequestException; import com.funtester.base.interfaces.IBase; import com.funtester.config.Constant; import com.funtester.config.HttpClientConstant; import com.funtester.db.mysql.MySqlTest; import com.funtester.frame.SourceCode; import com.funtester.utils.DecodeEncode; import com.funtester.utils.Regex; import com.funtester.utils.Time; import com.funtester.utils.message.AlertOver; import org.apache.commons.lang3.StringUtils; import org.apache.http.*; import org.apache.http.client.config.RequestConfig; import org.apache.http.client.entity.UrlEncodedFormEntity; import org.apache.http.client.methods.CloseableHttpResponse; import org.apache.http.client.methods.HttpGet; import org.apache.http.client.methods.HttpPost; import org.apache.http.client.methods.HttpRequestBase; import org.apache.http.entity.ContentType; import org.apache.http.entity.StringEntity; import org.apache.http.entity.mime.MultipartEntityBuilder; import org.apache.http.entity.mime.content.StringBody; import org.apache.http.message.BasicHeader; import org.apache.http.message.BasicNameValuePair; import org.apache.http.util.EntityUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import java.io.*; import java.util.ArrayList; import java.util.Arrays; import java.util.Iterator; import java.util.List; import java.util.concurrent.ExecutionException; import java.util.concurrent.Future; import java.util.stream.Collectors; /** * 请求相关类,采用统一的静态方法,在登录后台管理页面是自动化设置cookie,其他公参由各自的base类实现header */ public class FunLibrary extends SourceCode { private static Logger logger = LogManager.getLogger(FunLibrary.class); /** * ibase实现类,需要用来校验响应是否正确的响应体,获取响应的code码,code码默认-2,对于不同的项目ibase的isright方法不一样 */ private static IBase iBase; /** * 最近发送的请求 */ private static HttpRequestBase lastRequest; /** * 是否保存请求和响应 */ public static boolean SAVE_KEY = false; /** * 方法已重载,获取get对象 *

方法重载,主要区别参数,会自动进行urlencode操作

* * @param url 表示请求地址 * @param args 表示传入数据 * @return 返回get对象 */ public static HttpGet getHttpGet(String url, JSONObject args) { if (args == null || args.size() == 0) return getHttpGet(url); String uri = url + changeJsonToArguments(args); return getHttpGet(uri); } /** * 方法已重载,获取get对象 *

方法重载,主要区别参数,会自动进行urlencode操作

* * @param url 表示请求地址 * @return 返回get对象 */ public static HttpGet getHttpGet(String url) { return new HttpGet(url.replace(SPACE_1, EMPTY)); } /** * 获取post对象,以form表单提交数据 *

方法重载,文字信息form表单提交,文件信息二进制流提交,具体参照文件上传的方法主食,post请求可以不需要参数,暂时不支持其他参数类型,如果是公参需要在url里面展示,需要传一个json对象,一般默认args为get公参,params为post请求参数

* 请求header参数类型为{@link HttpClientConstant#ContentType_FORM} * * @param url 请求地址 * @param params 请求数据,form表单形式设置请求实体 * @return 返回post对象 */ public static HttpPost getHttpPost(String url, JSONObject params) { HttpPost httpPost = getHttpPost(url); setFormHttpEntity(httpPost, params); httpPost.addHeader(HttpClientConstant.ContentType_FORM); return httpPost; } /** * 获取httppost对象,没有参数设置 *

方法重载,文字信息form表单提交,文件信息二进制流提交,具体参照文件上传的方法主食,post请求可以不需要参数,暂时不支持其他参数类型,如果是公参需要在url里面展示,需要传一个json对象,一般默认args为get公参,params为post请求参数

* * @param url * @return */ public static HttpPost getHttpPost(String url) { return new HttpPost(url.replace(SPACE_1, EMPTY)); } /** * 获取httppost对象,json格式对象,传参时手动tostring *

新重载方法,适应post请求json传参,估计utf-8编码格式

* 请求header参数类型为{@link HttpClientConstant#ContentType_JSON} * * @param url * @param params * @return */ public static HttpPost getHttpPost(String url, String params) { HttpPost httpPost = getHttpPost(url); httpPost.setEntity(new StringEntity(params, DEFAULT_CHARSET.toString())); httpPost.addHeader(HttpClientConstant.ContentType_JSON); return httpPost; } /** * * 获取httppost对象,json格式对象,传参时手动tostring *

新重载方法,适应post请求json传参

* 请求header参数类型为{@link HttpClientConstant#ContentType_JSON} * * @param url * @param args * @return */ public static HttpPost getHttpPost(String url, JSONObject args, String params) { return getHttpPost(url + changeJsonToArguments(args), params); } /** * 获取 httppost 请求对象 *

方法重载,文字信息form表单提交,文件信息二进制流提交,具体参照文件上传的方法主食,post请求可以不需要参数,暂时不支持其他参数类型,如果是公参需要在url里面展示,需要传一个json对象,一般默认args为get公参,params为post请求参数

* * @param url 请求地址 * @param args 请求地址参数 * @param params 请求参数 * @return */ public static HttpPost getHttpPost(String url, JSONObject args, JSONObject params) { return getHttpPost(url + changeJsonToArguments(args), params); } /** * 获取 httpPost 对象 *

方法重载,文字信息form表单提交,文件信息二进制流提交,具体参照文件上传的方法主食,post请求可以不需要参数,暂时不支持其他参数类型,如果是公参需要在url里面展示,需要传一个json对象,一般默认args为get公参,params为post请求参数

* * @param url 请求地址 * @param args 请求通用参数 * @param params 请求参数,其中二进制流必须是 file * @param file 文件 * @return */ public static HttpPost getHttpPost(String url, JSONObject args, JSONObject params, File file) { return getHttpPost(url + changeJsonToArguments(args), params, file); } /** * 获取 httpPost 对象 *

方法重载,文字信息form表单提交,文件信息二进制流提交,具体参照文件上传的方法主食,post请求可以不需要参数,暂时不支持其他参数类型,如果是公参需要在url里面展示,需要传一个json对象,一般默认args为get公参,params为post请求参数

* * @param url 请求地址 * @param params 请求参数,其中二进制流必须是 file * @param file 文件 * @return */ public static HttpPost getHttpPost(String url, JSONObject params, File file) { HttpPost httpPost = getHttpPost(url); setMultipartEntityEntity(httpPost, params, file); return httpPost; } /** * 设置二进制流实体,params 里面参数值为 file * * @param httpPost httpPsot 请求 * @param params 请求参数 * @param file 文件 */ private static void setMultipartEntityEntity(HttpPost httpPost, JSONObject params, File file) { logger.debug("上传文件名:{}", file.getAbsolutePath()); String fileName = file.getName(); InputStream inputStream = null; try { inputStream = new FileInputStream(file); } catch (FileNotFoundException e) { logger.warn("读取文件失败!", e); } Iterator keys = params.keySet().iterator();// 遍历 params 参数和值 MultipartEntityBuilder builder = MultipartEntityBuilder.create();// 新建MultipartEntityBuilder对象 while (keys.hasNext()) { String key = keys.next(); String value = params.getString(key); if (value.equals("file")) { builder.addBinaryBody(key, inputStream, ContentType.create(HttpClientConstant.CONTENTTYPE_MULTIPART_FORM), fileName);// 设置流参数 } else { StringBody body = new StringBody(value, ContentType.create(HttpClientConstant.CONTENTTYPE_TEXT, DEFAULT_CHARSET));// 设置普通参数 builder.addPart(key, body); } } HttpEntity entity = builder.build(); httpPost.setEntity(entity); } /** * 发送请求之前,目前修改为止增加一个connection请求头 * * @param request */ protected static void beforeRequest(HttpRequestBase request) { request.addHeader(HttpClientConstant.CONNECTION); } /** * 响应结束之后,处理响应头信息,如set-cookien内容 * * @param response 响应内容 * @return */ private static JSONObject afterResponse(CloseableHttpResponse response) { JSONObject cookies = new JSONObject(); List
headers = Arrays.asList(response.getHeaders("Set-Cookie")); if (headers.size() == 0) return cookies; headers.forEach(x -> { String[] split = x.getValue().split(";")[0].split("=", 2); cookies.put(split[0], split[1]); }); return cookies; } /** * 根据解析好的content,转化json对象 * * @param content * @return */ private static JSONObject getJsonResponse(String content, JSONObject cookies) { JSONObject jsonObject = new JSONObject(); try { if (StringUtils.isEmpty(content)) ParamException.fail("响应为空!"); jsonObject = JSONObject.parseObject(content); } catch (JSONException e) { jsonObject = new JSONObject() {{ put(RESPONSE_CONTENT, content); put(RESPONSE_CODE, TEST_ERROR_CODE); }}; logger.warn("响应体非json格式,已经自动转换成json格式!"); } finally { if (cookies != null && !cookies.isEmpty()) jsonObject.put(HttpClientConstant.COOKIE, cookies); return jsonObject; } } /** * 根据响应获取响应实体 * * @param response * @return */ public static String getContent(HttpResponse response) { HttpEntity entity = response.getEntity();// 获取响应实体 String content = EMPTY; try { content = EntityUtils.toString(entity, DEFAULT_CHARSET);// 用string接收响应实体 EntityUtils.consume(entity);// 消耗响应实体,并关闭相关资源占用 } catch (Exception e1) { logger.warn("解析响应实体异常!", e1); } return content; } /** * 获取响应状态,处理重定向的url * * @param response * @param res * @return */ public static int getStatus(CloseableHttpResponse response, JSONObject res) { int status = response.getStatusLine().getStatusCode(); if (status != HttpStatus.SC_OK) logger.warn("响应状态码错误:{}", status); if (status == HttpStatus.SC_MOVED_TEMPORARILY) res.put("location", response.getFirstHeader("Location").getValue()); return status; } /** * 获取响应实体 *

会自动设置cookie,但是需要各个项目再自行实现cookie管理

*

该方法只会处理文本信息,对于文件处理可以调用两个过期的方法解决

* * @param request 请求对象 * @return 返回json类型的对象 */ public static JSONObject getHttpResponse(HttpRequestBase request) { if (!isRightRequest(request)) RequestException.fail(request); beforeRequest(request); JSONObject res = new JSONObject(); RequestInfo requestInfo = new RequestInfo(request); long start = Time.getTimeStamp(); try (CloseableHttpResponse response = ClientManage.httpsClient.execute(request)) { long end = Time.getTimeStamp(); long elapsed_time = end - start; int status = getStatus(response, res); JSONObject setCookies = afterResponse(response); String content = getContent(response); int data_size = content.length(); res.putAll(getJsonResponse(content, setCookies)); int code = iBase == null ? -2 : iBase.checkCode(res, requestInfo); // if (iBase != null && !iBase.isRight(res)) // new AlertOver("响应状态码错误:" + status, "状态码错误:" + status, requestInfo.getUrl(), requestInfo).sendSystemMessage(); MySqlTest.saveApiTestDate(requestInfo, data_size, elapsed_time, status, getMark(), code, LOCAL_IP, COMPUTER_USER_NAME); } catch (Exception e) { logger.warn("获取请求相应失败!请求内容:{}", FunRequest.initFromRequest(request).toString(), e); if (!requestInfo.isBlack()) new AlertOver("接口请求失败", requestInfo.toString(), requestInfo.getUrl(), requestInfo).sendSystemMessage(); } finally { if (!requestInfo.isBlack()) { lastRequest = request; } } return res; } /** * 判断请求是否是正确的,目前主要过滤一些不完整的请求和超长的url * * @param request * @return */ private static boolean isRightRequest(HttpRequestBase request) { String url = request.getURI().toString().toLowerCase(); return StringUtils.isNoneEmpty(url) && url.startsWith("http"); } /** * 设置post接口上传表单,默认的编码格式 * 默认编码格式{@link Constant#DEFAULT_CHARSET} * * @param httpPost post请求 * @param params 参数 */ private static void setFormHttpEntity(HttpPost httpPost, JSONObject params) { List formparams = new ArrayList(); params.keySet().forEach(x -> formparams.add(new BasicNameValuePair(x, params.getString(x)))); UrlEncodedFormEntity entity = new UrlEncodedFormEntity(formparams, DEFAULT_CHARSET); httpPost.setEntity(entity); } /** * 解析response,使用char数组,注意编码格式 *

自定义解析响应实体的方法,暂不采用

* * @param response 传入的response,非closedresponse * @return string类型的response */ @Deprecated private static String parseResponeEntityByChar(HttpResponse response) { StringBuffer buffer = new StringBuffer();// 创建并实例化stringbuffer,存放响应信息 try (InputStream input = response.getEntity().getContent(); InputStreamReader reader = new InputStreamReader(input, DEFAULT_CHARSET)) { char[] buff = new char[1024];// 创建并实例化字符数组 int length = 0;// 声明变量length,表示读取长度 while ((length = reader.read(buff)) != -1) {// 循环读取字符输入流 String x = new String(buff, 0, length);// 获取读取到的有效内容 buffer.append(x);// 将读取到的内容添加到stringbuffer中 } } catch (IOException e) { logger.warn("解析响应实体失败!", e); } return buffer.toString(); } /** * 从响应解析到文件 * * @param response * @param file */ @Deprecated private static void parseResponeByFile(HttpResponse response, File file) { int bytesum = 0;// 这个用来统计需要写入byte数组的长度 int byteread = 0;// 这个用来接收read()方法的返回值,表示读取内容的长度 try (InputStream inputStream = response.getEntity().getContent(); FileOutputStream fileOutputStream = new FileOutputStream(file);) { byte[] buffer = new byte[1024];// 新建读取文件所用的数组 // 此处用while循环每次按buffer读取文件直到读取完成 while ((byteread = inputStream.read(buffer)) != -1) {// 如何读取到文件末尾 bytesum += byteread;// 此处计算读取长度,byteread表示每次读取的长度 fileOutputStream.write(buffer, 0, byteread);// 此方法第一个参数是byte数组,第二次参数是开始位置,第三个参数是长度 } } catch (IOException e) { logger.warn("解析响应实体失败!", e); } } /** * 把json数据转化为参数,为get请求和post请求stringentity的时候使用 * * @param argument 请求参数,json数据类型,map类型,可转化 * @return 返回拼接参数后的地址 */ public static String changeJsonToArguments(JSONObject argument) { 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(); } /** * 通过json对象信息,生成cookie的header * * @param cookies * @return */ public static Header getCookies(JSONObject cookies) { return getHeader(HttpClientConstant.COOKIE, cookies.keySet().stream().map(x -> x.toString() + "=" + cookies.get(x).toString()).collect(Collectors.joining(";")).toString()); } /** * 生成header * * @param name * @param value * @return */ public static Header getHeader(String name, String value) { return new BasicHeader(name, value); } public static IBase getiBase() { return iBase; } public static void setiBase(IBase iBase) { FunLibrary.iBase = iBase; } /** * 将header转成json对象 * * @param headers * @return */ public static JSONObject header2Json(List
headers) { return new JSONObject() {{ headers.forEach(x -> put(x.getName(), x.getValue())); }}; } /** * 简单发送请求 * * @param request */ public static String executeSimlple(HttpRequestBase request) throws IOException { try (CloseableHttpResponse response = ClientManage.httpsClient.execute(request);) { if (response.getStatusLine().getStatusCode() != HttpStatus.SC_OK) RequestException.fail("响应状态码错误:" + response.getStatusLine().getStatusCode()); return getContent(response); } } /** * 设置代理请求 * * @param request * @param adress */ public static void setProxy(HttpRequestBase request, String adress) { request.setConfig(getProxyConfig(adress)); } /** * 设置代理请求 * * @param request * @param ip * @param port */ public static void setProxy(HttpRequestBase request, String ip, int port) { setProxy(request, ip + ":" + port); } /** * 通过IP和端口获取代理配置对象 * * @param adress * @return */ public static RequestConfig getProxyConfig(String adress) { if (StringUtils.isBlank(adress) || !Regex.isMatch(adress, Constant.HOST_REGEX)) ParamException.fail("adress格式错误:" + adress); String[] split = adress.split(":", 2); return ClientManage.getProxyRequestConfig(split[0], changeStringToInt(split[1])); } /** * 异步发送请求 * * @param request */ public static void executeSync(HttpRequestBase request) { if (!ClientManage.httpAsyncClient.isRunning()) ClientManage.httpAsyncClient.start(); ClientManage.httpAsyncClient.execute(request, null); } /** * 异步发送请求获取影响Demo *

经过测试没卵用

* * @param request * @throws ExecutionException * @throws InterruptedException */ public static JSONObject executeSyncWithResponse(HttpRequestBase request) { if (!ClientManage.httpAsyncClient.isRunning()) ClientManage.httpAsyncClient.start(); Future execute = ClientManage.httpAsyncClient.execute(request, null); try { HttpResponse httpResponse = execute.get(); String content = getContent(httpResponse); return getJsonResponse(content, null); } catch (Exception e) { logger.error("异步请求获取响应失败!", e); } return new JSONObject(); } /** * 获取最后一个发出的请求,用于进行性能测试用的,也可以由基类对象{@link IBase}实现 * * @return */ public static HttpRequestBase getLastRequest() { return lastRequest; } /** * 结束测试,关闭连接池 */ public static void testOver() { try { ClientManage.httpsClient.close(); ClientManage.httpAsyncClient.close(); } catch (Exception e) { logger.warn("连接池关闭失败!", e); } } /** * 初始化连接池和各类管理器 * * @param timeout * @param accepttime * @param retrytimes */ public synchronized static void init(int timeout, int accepttime, int retrytimes) { ClientManage.init(timeout, accepttime, retrytimes, null, 0); } public synchronized static void init(int timeout, int accepttime, int retrytimes, String ip, int port) { ClientManage.init(timeout, accepttime, retrytimes, ip, port); } } ================================================ FILE: src/main/groovy/com/funtester/httpclient/FunRequest.groovy ================================================ package com.funtester.httpclient import com.alibaba.fastjson.JSONObject import com.funtester.base.bean.RequestInfo import com.funtester.base.exception.RequestException import com.funtester.config.HttpClientConstant import com.funtester.config.RequestType import com.funtester.frame.Save import com.funtester.frame.SourceCode import com.funtester.utils.Time import org.apache.commons.lang3.StringUtils import org.apache.http.Header import org.apache.http.HttpEntity import org.apache.http.client.methods.HttpPost import org.apache.http.client.methods.HttpRequestBase import org.apache.http.client.methods.RequestBuilder import org.apache.http.util.EntityUtils import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.Logger /** * 重写FunLibrary,使用面对对象思想,不用轻易使用set属性方法,可能存在BUG */ class FunRequest extends SourceCode implements Serializable, Cloneable { private static final long serialVersionUID = -4153600036943378727L private static Logger logger = LogManager.getLogger(FunRequest.class) /** * 请求类型,true为get,false为post */ RequestType requestType /** * 请求对象 */ HttpRequestBase request /** * host地址 */ String host = EMPTY /** * 接口地址 */ String apiName = EMPTY /** * 请求地址,如果为空则由host和apiname拼接 */ String uri = EMPTY /** * header集合 */ List
headers = new ArrayList<>() /** * get参数 */ JSONObject args = new JSONObject() /** * post参数,表单 */ JSONObject params = new JSONObject() /** * json参数 */ JSONObject json = new JSONObject() /** * 响应,若没有这个参数,从将funrequest对象转换成json对象时会自动调用getresponse方法 */ JSONObject response = new JSONObject() /** * 构造方法 * * @param requestType */ private FunRequest(RequestType requestType) { this.requestType = requestType } /** * 获取get对象 * * @return */ static FunRequest isGet() { new FunRequest(RequestType.GET) } /** * 获取post对象 * * @return */ static FunRequest isPost() { new FunRequest(RequestType.POST) } /** * 设置host * * @param host * @return */ FunRequest setHost(String host) { this.host = host this } /** * 设置接口地址 * * @param apiName * @return */ FunRequest setApiName(String apiName) { this.apiName = apiName this } /** * 设置uri * * @param uri * @return */ FunRequest setUri(String uri) { this.uri = uri this } /** * 添加get参数 * * @param key * @param value * @return */ FunRequest addArgs(Object key, Object value) { args.put(key, value) this } /** * 添加post参数 * * @param key * @param value * @return */ FunRequest addParam(Object key, Object value) { params.put(key, value) this } /** * 添加json参数 * * @param key * @param value * @return */ FunRequest addJson(Object key, Object value) { json.put(key, value) this } /** * 添加header * * @param key * @param value * @return */ FunRequest addHeader(Object key, Object value) { headers << getHeader(key.toString(), value.toString()) this } /** * 添加header * * @param header * @return */ FunRequest addHeader(Header header) { headers.add(header) this } /** * 批量添加header * * @param header * @return */ FunRequest addHeader(List
header) { header.each {h -> headers << h} this } /** * 增加header中cookies * * @param cookies * @return */ FunRequest addCookies(JSONObject cookies) { headers << getCookies(cookies) this } FunRequest addHeaders(List
headers) { this.headers.addAll(headers) this } FunRequest addHeaders(JSONObject headers) { headers.each {x -> this.headers.add(getHeader(x.getKey().toString(), x.getValue().toString())) } this } FunRequest addArgs(JSONObject args) { this.args.putAll(args) this } FunRequest addParams(JSONObject params) { this.params.putAll(params) this } FunRequest addJson(JSONObject json) { this.json.putAll(json) this } /** * 获取请求响应,兼容相关参数方法,不包括file * * @return */ JSONObject getResponse() { response = response.isEmpty() ? FunLibrary.getHttpResponse(request == null ? getRequest() : request) : response response } /** * 获取请求对象 * * @return */ HttpRequestBase getRequest() { if (request != null) request if (StringUtils.isEmpty(uri)) uri = host + apiName switch (requestType) { case RequestType.GET: request = FunLibrary.getHttpGet(uri, args) break case RequestType.POST: 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)) break } for (Header header in headers) { request.addHeader(header) } logger.debug("请求信息:{}", new RequestInfo(this.request).toString()) request } FunRequest setHeaders(List
headers) { this.headers = headers this } FunRequest setArgs(JSONObject args) { this.args = args this } FunRequest setParams(JSONObject params) { this.params = params this } FunRequest setJson(JSONObject json) { this.json = json this } @Override FunRequest clone() { initFromRequest(this.getRequest()) } @Override String toString() { return "{" + "requestType='" + requestType.getName() + '\'' + ", host='" + host + '\'' + ", apiName='" + apiName + '\'' + ", uri='" + uri + '\'' + ", headers=" + FunLibrary.header2Json(headers).toString() + ", args=" + args.toString() + ", params=" + params.toString() + ", json=" + json.toString() + ", response=" + response.toString() + '}' } /** * 将请求对象转成curl命令行 * @return */ String toCurl() { 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} ") if (requestType == RequestType.GET) curl << " -G" headers.each { curl << " -H '${it.getName()}:${it.getValue().replace(SPACE_1, EMPTY)}'" } switch (requestType) { case RequestType.GET: args.each { curl << " -d '${it.key}=${it.value}'" } break case RequestType.POST: if (!params.isEmpty()) { curl << " -H Content-Type:application/x-www-form-urlencoded" params.each { curl << " -F '${it.key}=${it.value}'" } } if (!json.isEmpty()) { curl << " -H \"Content-Type:application/json\"" //此处多余,防止从外部构建curl命令 json.each { curl << " -d '${it.key}=${it.value}'" } } break default: break } curl << " ${uri}" // curl << " --compressed" //这里防止生成多个curl请求,批量生成有用 curl.toString() } /** * 将请求对象转成curl命令行 * @param requestBase * @return */ static String reqToCurl(HttpRequestBase requestBase) { initFromRequest(requestBase).toCurl() } /** * 从requestbase对象从初始化funrequest * @param base * @return */ static FunRequest initFromRequest(HttpRequestBase base) { FunRequest request = null String method = base.getMethod() RequestType requestType = RequestType.getRequestType(method) String uri = base.getURI().toString() List
headers = Arrays.asList(base.getAllHeaders()) if (requestType == requestType.GET) { request = isGet().setUri(uri).addHeaders(headers) } else if (requestType == RequestType.POST) { HttpPost post = (HttpPost) base HttpEntity entity = post.getEntity() String value = entity.getContentType().getValue() String content = null try { content = EntityUtils.toString(entity) } catch (IOException e) { logger.error("解析响应失败!", e) fail() } if (value.equalsIgnoreCase(HttpClientConstant.ContentType_TEXT.getValue()) || value.equalsIgnoreCase(HttpClientConstant.ContentType_JSON.getValue())) { request = isPost().setUri(uri).addHeaders(headers).addJson(JSONObject.parseObject(content)) } else if (value.equalsIgnoreCase(HttpClientConstant.ContentType_FORM.getValue())) { request = isPost().setUri(uri).addHeaders(headers).addParams(getJson(content.split("&"))) } } else { RequestException.fail("不支持的请求类型!") } return request } static HttpRequestBase doCopy(HttpRequestBase base) { (HttpRequestBase) RequestBuilder.copy(base).build() } /** * 拷贝HttpRequestBase对象 * @param base * @return */ static HttpRequestBase cloneRequest(HttpRequestBase base) { initFromRequest(base).getRequest() } /** * 保存请求和响应 * @param base * @param response */ static void save(HttpRequestBase base, JSONObject response) { FunRequest request = initFromRequest(base) request.setResponse(response) Save.info("/request/" + Time.getDate().substring(8) + SPACE_1 + request.getUri().replace(OR, CONNECTOR).replaceAll("https*:_+", EMPTY), request.toString()) } } ================================================ FILE: src/main/groovy/com/funtester/httpclient/GCThread.java ================================================ package com.funtester.httpclient; import com.funtester.config.HttpClientConstant; import static com.funtester.frame.SourceCode.sleep; /** * 从连接池中回收连接的多线程类 */ public class GCThread implements Runnable { /** * 资源回收线程 */ private static volatile Thread gc = init(); /** * 增加了线程状态的判断,同一进程多次运行HTTP请求的压测功能 */ public synchronized static void starts() { if (gc.getState() == Thread.State.NEW) gc.start(); else if (gc.getState() == Thread.State.TERMINATED) gc = init(); } /** * 初始化方法,获取新的gc线程对象 * * @return */ public static synchronized Thread init() { FLAG = true; return new Thread(new GCThread()); } private GCThread() { } /** * 线程结束标志 */ private static boolean FLAG = true; @Override public void run() { while (FLAG) { sleep(HttpClientConstant.LOOP_INTERVAL); ClientManage.recyclingConnection(); } } /** * 结束线程方法 */ public static void stop() { FLAG = false; } } ================================================ FILE: src/main/groovy/com/funtester/main/ExecuteMethod.java ================================================ package com.funtester.main; import com.funtester.frame.SourceCode; import com.funtester.frame.execute.ExecuteSource; import org.apache.commons.lang3.ArrayUtils; public class ExecuteMethod extends SourceCode { public static void main(String[] args) { if (ArrayUtils.isEmpty(args)) args = new String[]{"com.funtester.ztest.java.T.test", "java.lang.Integer", "1"}; ExecuteSource.executeMethod(args); } } ================================================ FILE: src/main/groovy/com/funtester/main/PerformanceFromFile.groovy ================================================ package com.funtester.main import com.funtester.frame.SourceCode import com.funtester.frame.execute.Concurrent import com.funtester.frame.thread.RequestThreadTimes import com.funtester.httpclient.FunLibrary import com.funtester.utils.request.RequestFile import org.apache.http.client.methods.HttpRequestBase /** * 从文本配置中读取request,进行压测的类 */ class PerformanceFromFile extends SourceCode { static void main(String[] args) { FunLibrary.setSocketTimeOut(30) def size = args.size(); List list = new ArrayList<>() for (int i = 0; i < size - 1; i += 2) { def name = args[i] int thread = changeStringToInt(args[i + 1]) def request = new RequestFile(name).getRequest() for (int j = 0; j < thread; j++) { list.add(request) } } int perTimes = changeStringToInt(args[size - 1]) List thread = new ArrayList<>() for (int i = 0; i < list.size(); i++) { def get = list.get(i) def thread1 = new RequestThreadTimes(get, perTimes) thread.add(thread1) } def concurrent = new Concurrent(thread) concurrent.start() FunLibrary.testOver() } } ================================================ FILE: src/main/groovy/com/funtester/socket/ScoketIOFunClient.java ================================================ package com.funtester.socket; /** * 基于Socket.IO的Client封装对象 */ public class ScoketIOFunClient { //public class ScoketIOFunClient extends SourceCode implements Serializable { // private static final long serialVersionUID = -7229704711068396512L; // // private static Logger logger = LogManager.getLogger(ScoketIOFunClient.class); // // public static ThreadLocal options = new ThreadLocal() { // // /** // * 通用配置,初始化连接选项的方法,默认采取重置 // * // * @return // */ // @Override // public IO.Options initialValue() { // IO.Options options = new IO.Options(); // options.transports = SocketConstant.transports; // //失败重试次数 // options.reconnectionAttempts = SocketConstant.MAX_RETRY; // //失败重连的时间间隔 // options.reconnectionDelay = SocketConstant.RETRY_DELAY; // //连接超时时间(ms) // options.timeout = SocketConstant.TIMEOUT; // return options; // } // // }; // // /** // * 所有的客户端 // */ // public static Vector clients = new Vector<>(); // // /** // * 记录的消息 // */ // public LinkedList msgs = new LinkedList<>(); // // /** // * 客户端名称 // */ // private String cname; // // /** // * 连接的URL // */ // private String url; // // /** // * Socket对象 // */ // public Socket socket; // // /** // * 监听事件记录,此处使用量很小,故而不考虑线程安全 // */ // public Set events = new HashSet<>(); // // // private ScoketIOFunClient(String url, Socket socket) { // this.url = url; // this.socket = socket; // clients.add(this); // } // // /** // * 获取socketClient实例 // * // * @param url // * @param cname // * @return // */ // public static ScoketIOFunClient getInstance(String url, String cname) { // logger.info("Socket 连接: {},客户端名称: {}", url, cname); // ScoketIOFunClient client = null; // try { // client = new ScoketIOFunClient(url, IO.socket(url, options.get())); // client.setCname(cname); // } catch (URISyntaxException e) { // FailException.fail(e); // } // return client; // } // // /** // * 注册通用的事件监听,需要脚本自己注册改监听 // * {@link io.socket.client.Socket} // */ // public void init() { // this.socket.on(Socket.EVENT_CONNECTING, objects -> { // logger.info("{} 正在连接...信息:{}", cname, initMsg(objects)); // }); // events.add(Socket.EVENT_CONNECTING); // this.socket.on(Socket.EVENT_ERROR, objects -> { // logger.info("{} 收到错误信息:{}", cname, initMsg(objects)); // }); // events.add(Socket.EVENT_ERROR); // this.socket.on(Socket.EVENT_CONNECT_TIMEOUT, objects -> { // logger.info("{} 连接超时!,url:{},信息:{}", cname, url, initMsg(objects)); // }); // events.add(Socket.EVENT_CONNECT_TIMEOUT); // this.socket.on(Socket.EVENT_CONNECT_ERROR, objects -> { // logger.info("{} 连接错误,信息:{}", cname, initMsg(objects)); // }); // events.add(Socket.EVENT_CONNECT_ERROR); // this.socket.on(Socket.EVENT_PING, objects -> { // logger.info("{} ping消息:{}", cname, initMsg(objects)); // }); // events.add(Socket.EVENT_PING); // this.socket.on(Socket.EVENT_PONG, objects -> { // logger.info("{} ping消息:{}", cname, initMsg(objects)); // }); // events.add(Socket.EVENT_PONG); // /*此处统一的message做记录*/ // this.socket.on(Socket.EVENT_MESSAGE, objects -> { // String msg = initMsg(objects); // saveMsg(msg); // logger.info("{} 收到 {} 事件,信息:{}", cname, Socket.EVENT_MESSAGE, msg); // }); // } // // /** // * 开始建立socket连接 // */ // public void connect() { // logger.info("{} 开始连接...", cname); // this.socket.connect(); // int a = 0; // while (true) { // this.socket.connect(); // if (this.socket.connected()) break; // if ((a++ > SocketConstant.MAX_RETRY)) FailException.fail(cname + "连接重试失败!"); // SourceCode.sleep(SocketConstant.WAIT_INTERVAL); // } // logger.info("{} 连接成功!", cname); // } // // /** // * 添加监听事件 // * // * @param event // * @param fn // */ // public void addEventListener(String event, Emitter.Listener fn) { // events.add(event); // this.socket.on(event, fn); // } // // /** // * 发送消息,暂不重载 // * // * @param event // * @param objects // */ // public void send(String event, Object... objects) { // events.add(event); // this.socket.emit(event, objects); // } // // /** // * 关闭SocketClient // */ // public void close() { // logger.info("{} socket链接关闭!", cname); // this.socket.close(); // } // // /** // * 初始化收到的信息 // * // * @param objects // * @return // */ // public static String initMsg(Object... objects) { // if (ArrayUtils.isEmpty(objects)) return EMPTY; // return Arrays.toString(objects); // } // // /** // * 该方法用于性能测试中,clone多线程对象 // * // * @return // */ // @Override // public ScoketIOFunClient clone() { // return getInstance(this.url, this.cname + StringUtil.getString(4)); // } // // /** // * 设置cname,多用于性能测试clone()之后 // * // * @param cname // */ // public void setCname(String cname) { // this.cname = cname; // } // // public String getCname() { // return cname; // } // // public String getUrl() { // return url; // } // // public void setUrl(String url) { // this.url = url; // } // // /** // * 保存收到的信息,只保留最近的{@link SocketConstant}条 // * // * @param msg // */ // public void saveMsg(String msg) { // synchronized (msgs) { // if (msgs.size() > SocketConstant.MAX_MSG_SIZE) msgs.remove(); // msgs.add(msg); // } // } // // /** // * 关闭所有socketclient // */ // public static void closeAll() { // clients.forEach(x -> // { // if (x != null && x.socket.connected()) x.close(); // } // ); // clients.clear(); // logger.info("关闭所有Socket客户端!"); // } } ================================================ FILE: src/main/groovy/com/funtester/socket/WebSocketFunClient.java ================================================ package com.funtester.socket; /** * socket客户端代码,限于WebSocket协议的测试 */ public class WebSocketFunClient { //public class WebSocketFunClient extends WebSocketClient { // private static Logger logger = LogManager.getLogger(WebSocketFunClient.class); // // public static Vector clients = new Vector<>(); // // /** // * 存储收到的消息 // */ // public LinkedList msgs = new LinkedList<>(); // // /** // * 连接的url // */ // private String url; // // /** // * 客户端名称 // */ // private String cname; // // private WebSocketFunClient(String url, String cname) throws URISyntaxException { // super(new URI(url)); // this.cname = cname; // this.url = url; // clients.add(this); // } // // /** // * 获取socketclient实例 // * // * @param url // * @return // */ // public static WebSocketFunClient getInstance(String url) { // return getInstance(url, Constant.DEFAULT_STRING + StringUtil.getString(4)); // } // // /** // * 获取socketclient实例 // * // * @param url // * @param cname // * @return // */ // public static WebSocketFunClient getInstance(String url, String cname) { // WebSocketFunClient client = null; // try { // client = new WebSocketFunClient(url, cname); // } catch (URISyntaxException e) { // ParamException.fail(cname + "创建socket client 失败! 原因:" + e.getMessage()); // } // return client; // } // // @Override // public void onOpen(ServerHandshake handshakedata) { // logger.info("{} 正在建立socket连接...", cname); // handshakedata.iterateHttpFields().forEachRemaining(x -> logger.info("握手信息key: {} ,value: {}", x, handshakedata.getFieldValue(x))); // } // // /** // * 收到消息时候调用的方法 // * // * @param message // */ // @Override // public void onMessage(String message) { // saveMsg(message); // logger.info("{}收到: {}", cname, message); // } // // /** // * 关闭 // * // * @param code 关闭code码,详情查看 {@link org.java_websocket.framing.CloseFrame} // * @param reason 关闭原因 // * @param remote // */ // @Override // public void onClose(int code, String reason, boolean remote) { // logger.info("{} socket 连接关闭,URL: {} ,code码:{},原因:{},是否由远程服务关闭:{}", cname, url, code, reason, remote); // } // // /** // * 关闭socketclient // */ // @Override // public void close() { // logger.warn("{}:socket连接关闭!", cname); // super.close(); // } // // /** // * 出错时候调用 // * // * @param e // */ // @Override // public void onError(Exception e) { // logger.error("{} socket异常,URL: {}", cname, url, e); // } // // /** // * 发送消息 // * // * @param text // */ // @Override // public void send(String text) { // logger.debug("{} 发送:{}", cname, text); // super.send(text); // } // // /** // * 简历socket连接 // */ // @Override // public void connect() { // logger.info("{} 开始连接...", cname); // super.connect(); // int a = 0; // while (true) { // if (this.getReadyState() == ReadyState.OPEN) break; // if ((a++ > SocketConstant.MAX_WATI_TIMES)) FailException.fail(cname + "连接重试失败!"); // SourceCode.sleep(SocketConstant.WAIT_INTERVAL); // } // logger.info("{} 连接成功!", cname); // } // // /** // * 发送非默认编码格式的文字 // * // * @param text // * @param charset // */ // public void send(String text, Charset charset) { // send(new String(text.getBytes(), charset)); // } // // /** // * 发送json信息 // * // * @param json // */ // public void send(JSONObject json) { // send(json.toJSONString()); // } // // /** // * 发送bean // * // * @param bean // */ // public void send(AbstractBean bean) { // send(bean.toString()); // } // // /** // * 重置连接 // */ // @Override // public void reconnect() { // logger.info("{}重置连接并尝试重新连接!", cname); // super.reconnect(); // } // // /** // * 设置cname,多用于性能测试clone()之后 // * // * @param cname // */ // public void setCname(String cname) { // this.cname = cname; // } // // public String getCname() { // return cname; // } // // public String getUrl() { // return url; // } // // public void setUrl(String url) { // this.url = url; // } // // /** // * 该方法用于性能测试中,clone多线程对象 // * // * @return // */ // @Override // public WebSocketFunClient clone() { // return getInstance(this.url, this.cname + StringUtil.getString(4)); // } // // /** // * 保存收到的信息,只保留最近的{@link SocketConstant}条 // * // * @param msg // */ // public void saveMsg(String msg) { // synchronized (msgs) { // if (msgs.size() > SocketConstant.MAX_MSG_SIZE) msgs.remove(); // msgs.add(msg); // } // } // // /** // * 关闭所有socketclient // */ // public static void closeAll() { // clients.forEach(x -> // { // if (x != null && !x.isClosed()) x.close(); // } // ); // clients.clear(); // logger.info("关闭所有Socket客户端!"); // } } ================================================ FILE: src/main/groovy/com/funtester/utils/ArgsUtil.java ================================================ package com.funtester.utils; import com.alibaba.fastjson.JSON; import com.alibaba.fastjson.JSONObject; import com.funtester.frame.SourceCode; import java.io.File; public class ArgsUtil extends SourceCode { String[] all; public ArgsUtil(String[] args) { all = (String[]) args.clone(); } /** * 获取int参数 * * @param i 获取的参数索引 * @param k 默认值 * @return */ public int getIntOrdefault(int i, int k) { return i >= all.length ? k : changeStringToInt(all[i]); } /** * 获取boolean参数 * * @param i * @param k * @return */ public boolean getBooleanOrdefault(int i, boolean k) { return i >= all.length ? k : changeStringToBoolean(all[i]); } /** * @param i * @param k * @return */ public String getStringOrdefault(int i, String k) { return i >= all.length ? k : all[i]; } /** * @param i * @param path * @return */ public File getFileOrDefault(int i, String path) { return i >= all.length ? new File(path) : new File(all[i]); } /** * @param i * @param json * @return */ public JSONObject getJsonOrDefault(int i, String json) { return i >= all.length ? JSON.parseObject(json) : JSON.parseObject(all[i]); } } ================================================ FILE: src/main/groovy/com/funtester/utils/CMD.java ================================================ package com.funtester.utils; import com.funtester.config.Constant; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import java.io.*; import java.nio.charset.Charset; /** * 执行命令的类 */ public class CMD extends Constant { private static Logger logger = LogManager.getLogger(CMD.class); /** * 执行cmd命令,控制台信息编码方式 * * @param cmd 需要执行的命令 */ public static int execCmd(String cmd) { return execCmd(cmd, DEFAULT_CHARSET); } /** * 执行cmd命令,注意Mac 系统添加环境路径 * * @param cmd 需要执行的命令 */ public static int execCmd(String cmd, Charset charset) { return execCmd(cmd, charset, false, EMPTY); } public static int execCmd(String cmd, boolean filter, String mark) { return execCmd(cmd, DEFAULT_CHARSET, false, EMPTY); } public static int execCmd(String cmd, Charset charset, boolean filter, String mark) { logger.info("执行命令:{}", cmd); Process p = null;// 通过runtime类执行cmd命令 try { p = Runtime.getRuntime().exec(cmd); } catch (IOException e) { logger.error("cmd:{}命令错误", e); return 1; } try (InputStream input = p.getInputStream(); InputStreamReader inputStreamReader = new InputStreamReader(input, charset); BufferedReader reader = new BufferedReader(inputStreamReader); InputStream errorInput = p.getErrorStream(); InputStreamReader streamReader = new InputStreamReader(errorInput, charset.name()); BufferedReader errorReader = new BufferedReader(streamReader)) { String line = EMPTY; while ((line = reader.readLine()) != null) {// 循环读取 if (!filter || (line.contains(mark))) logger.info(line); } String eline = EMPTY; while ((eline = errorReader.readLine()) != null) {// 循环读取 logger.info(eline);// 输出 } return 0; } catch (IOException e) { logger.warn("执行命令:{}失败!", cmd, e); p.destroy(); return 1; } } /** * 获取文本信息的最后几行,用户查看日志 * * @param path * @param num * @return */ public static String catFile(String path, int num) { logger.info("查询的文件:{}", path); if (StringUtils.isEmpty(path)) return EMPTY; File file = new File(path); if (!file.exists() || file.isDirectory()) return EMPTY; StringBuffer stringBuffer = new StringBuffer(); String command = "tail -n " + num + SPACE_1 + path; logger.debug("执行命令:{}", command); try (InputStream input = Runtime.getRuntime().exec(command).getInputStream(); InputStreamReader inputStreamReader = new InputStreamReader(input, DEFAULT_CHARSET); BufferedReader reader = new BufferedReader(inputStreamReader);) { String line = EMPTY; while ((line = reader.readLine()) != null) {// 循环读取 stringBuffer.append(line + LINE); } } catch (IOException e) { logger.error("获取:{}文件信息失败!", path, e); } finally { return stringBuffer.toString(); } } } ================================================ FILE: src/main/groovy/com/funtester/utils/CountUtil.groovy ================================================ package com.funtester.utils import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.Logger import java.util.stream.Collectors /** * 统计出现次数相关类 */ class CountUtil { private static Logger logger = LogManager.getLogger(CountUtil.class) /** * 统计数据出现的次数 * * @param counts 统计的 jsonobject 对象 * @param object 需要统计的数据 */ static def count(Map counts, Object object) { count(counts, object, 1) } /** * 统计数据出现的次数 * * @param counts 统计的 jsonobject 对象 * @param object 需要统计的数据 * @param num 增加值 */ static def count(Map counts, Object object, int num) { counts.put(object, Integer.valueOf(counts.getOrDefault(object, 0) + num)) } /** * 统计某个list里面某个元素出现的次数 * @param list * @param str * @return */ static def count(List list, def str) { list.count {s -> s.toString().equals(str.toString())} } /** * 统计某个list里面各个元素出现的次数 * collect,是一个map对象 * @param list * @return */ static def count(List list) { list.stream().collect(Collectors.groupingBy {x -> x}).each { it.setValue(it.value.size()) logger.info("元素:${it.key},次数:${it.value}") } } } ================================================ FILE: src/main/groovy/com/funtester/utils/CurlUtil.groovy ================================================ package com.funtester.utils import com.alibaba.fastjson.JSONObject import com.funtester.config.Constant import com.funtester.config.RequestType import com.funtester.frame.SourceCode import com.funtester.httpclient.FunLibrary import com.funtester.httpclient.FunRequest import org.apache.http.Header import org.apache.http.client.methods.HttpRequestBase /** * 通过将浏览器中复制的curl文本信息转化成HTTPrequestbase对象工具类 */ class CurlUtil { private static def filterWords = [".js", ".png", ".gif", ".css", ".ico", "list_unread", ".svg", ".htm", ".jpeg", ".ashx"] /** * 从curl复制结果中获取请求 * @param path * @return */ static List getRequests(String path) { def fileinfo = RWUtil.readTxtFileByLine(path.contains(Constant.OR) ? path : Constant.LONG_Path + path).stream().map {it.trim()} List requests = [] def base = new CurlRequestBase() fileinfo.each { if (it.startsWith("curl")) { def split = it.split(" ", 2) def type = split[0] def value = split[1] base.url = value.substring(value.indexOf('h'), value.lastIndexOf("'")) } else if (it.startsWith("-H")) { def split = it.split(" ", 2)[1].split(": ") base.headers << FunLibrary.getHeader(split[0].substring(1), split[1].substring(0, split[1].lastIndexOf("'"))) } else if (it.startsWith("--data-raw")) { base.params = SourceCode.getJson(it.substring(it.indexOf("'") + 1, it.lastIndexOf("'")).split("&")) base.type = RequestType.POST } else if (it.startsWith("--compressed")) { requests << getRequest(base) base = new CurlRequestBase() } } requests.findAll { it != null && it.getFirstHeader("accept").getValue().contains("application/json") } } /** * 将curlrequestbase对象转换成HTTPrequestbase * @param base * @return */ static HttpRequestBase getRequest(CurlRequestBase base) { if (filterWords.any { base.url.contains(it) }) return 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() } /** * 添加URL过滤词汇 * @param w */ static void addFilterWord(String w) { filterWords << w } /** * 用于存储每一个请求的详情 */ static class CurlRequestBase { String url RequestType type = RequestType.GET List
headers = new ArrayList<>() JSONObject params = new JSONObject() } } ================================================ FILE: src/main/groovy/com/funtester/utils/DecodeEncode.java ================================================ package com.funtester.utils; import com.funtester.base.exception.FailException; import com.funtester.config.Constant; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import java.io.ByteArrayOutputStream; import java.io.IOException; import java.io.OutputStream; import java.io.UnsupportedEncodingException; import java.nio.charset.Charset; import java.security.MessageDigest; import java.security.NoSuchAlgorithmException; import java.util.Base64; import java.util.regex.Matcher; import java.util.regex.Pattern; import java.util.zip.DeflaterOutputStream; import java.util.zip.InflaterOutputStream; /** * 编码格式转码解码类 */ public class DecodeEncode extends Constant{ private static Logger logger = LogManager.getLogger(DecodeEncode.class); /** * url进行转码,常用于网络请求 * * @param text 需要转码的文本 * @return 返回转码后的文本 */ public static String urlEncoderText(String text) { return urlEncoderText(text, UTF_8); } /** * url进行转码,常用于网络请求 * * @param text 需要转码的文本 * @return 返回转码后的文本 */ public static String urlEncoderText(String text, Charset charset) { String result = EMPTY; try { result = java.net.URLEncoder.encode(text, charset.toString()); } catch (UnsupportedEncodingException e) { logger.warn("数据格式错误!", e); } return result; } /** * url进行解码,常用于解析响应,默认是UTF-8字符集 * * @param text 需要解码的文本 * @return 解码后的文本 */ public static String urlDecoderText(String text, Charset charset) { String result = EMPTY; try { result = java.net.URLDecoder.decode(text, charset.toString()); } catch (UnsupportedEncodingException e) { logger.warn("数据格式错误!", e); } return result; } /** * url进行解码,常用于解析响应,默认是UTF-8字符集 * * @param text 需要解码的文本 * @return 解码后的文本 */ public static String urlDecoderText(String text) { return urlDecoderText(text, UTF_8); } /** * 对本文进行base64解码,方法默认UTF_8 * * @param text * @return */ public static String base64Decode(String text) { return base64Decode(text, UTF_8); } /** * 对字符串进行解码,使用编码格式参数 * * @param text * @param charset * @return */ public static String base64Decode(String text, Charset charset) { return new String(base64Byte(text.getBytes(charset))); } /** * 转换 * * @param text * @return */ public static byte[] base64Byte(byte[] text) { return Base64.getDecoder().decode(text); } /** * 获取字符串的字节数组 * * @param text * @return */ public static byte[] base64Byte(String text) { return base64Byte(text.getBytes()); } /** * 压缩字符串,默认梳utf-8 * * @param text * @return */ public static String zipBase64(String text) { try (ByteArrayOutputStream out = new ByteArrayOutputStream()) { try (DeflaterOutputStream deflaterOutputStream = new DeflaterOutputStream(out)) { deflaterOutputStream.write(text.getBytes(Constant.UTF_8)); } return DecodeEncode.base64Encode(out.toByteArray()); } catch (IOException e) { logger.error("压缩文本失败:{}", text, e); } return EMPTY; } /** * 解压字符串,默认utf-8 * * @param text * @return */ public static String unzipBase64(String text) { try (ByteArrayOutputStream os = new ByteArrayOutputStream()) { try (OutputStream outputStream = new InflaterOutputStream(os)) { outputStream.write(DecodeEncode.base64Byte(text)); } return new String(os.toByteArray(), Constant.UTF_8); } catch (IOException e) { logger.error("解压文本失败:{}", text, e); } return EMPTY; } /** * 对本文进行base64转码,方法默认了utf8 * * @param text * @return */ public static String base64Encode(String text) { return base64Encode(text, UTF_8); } /** * 对本文进行base64转码,编码格式自定义 * * @param text * @param charset * @return */ public static String base64Encode(String text, Charset charset) { try { return new String(Base64.getEncoder().encode(text.getBytes(charset))); } catch (Exception e) { logger.warn("base64转码失败!", e); return EMPTY; } } public static String base64Encode(byte[] data) { try { return new String(Base64.getEncoder().encode(data)); } catch (Exception e) { logger.warn("base64转码失败!", e); return EMPTY; } } /** * 使用md5加密数据 * * @param text * @return */ public static String encodeByMd5(String text) { byte[] date = null; try { date = text.getBytes("utf-8"); } catch (UnsupportedEncodingException e) { FailException.fail("utf-8格式错误!" + e.getMessage()); } MessageDigest message = null; try { message = MessageDigest.getInstance("md5"); } catch (NoSuchAlgorithmException e) { FailException.fail("md5加密失败!" + e.getMessage()); } message.update(date); byte[] result = message.digest(); StringBuffer stringBuffer = new StringBuffer(); for (int offset = 0; offset < result.length; offset++) { int i = result[offset]; if (i < 0)// 如果负数 i += 256;// 变成正数,0xff & i 也可以 if (i < 16)// 如果小于16,则加上0小于16,转换之后就是一位缺少一位故要加0 stringBuffer.append("0"); stringBuffer.append(Integer.toHexString(i)); } return stringBuffer.toString(); } /** * MD5加盐加密 * * @param text * @param salt * @return */ public static String encodeByMd5(String text, String salt) { return encodeByMd5(text + salt); } /** * 处理Unicode码转(\u6210\u529f) * * @param str * @return */ public static String unicodeToString(String str) { Pattern pattern = Pattern.compile("(\\\\u(\\p{XDigit}{4}))"); Matcher matcher = pattern.matcher(str); char ch; while (matcher.find()) { String group = matcher.group(2); ch = (char) Integer.parseInt(group, 16); String group1 = matcher.group(1); str = str.replace(group1, ch + EMPTY); } return str; } /** * 处理Unicode码转成(\xe6\x88\x90\xe5\x8a\x9f") * * @param str * @return */ public static String unicodeToStringX(String str) { str = str.replaceAll("\\\\x", "%"); return urlDecoderText(str, DEFAULT_CHARSET); } } ================================================ FILE: src/main/groovy/com/funtester/utils/FileUtil.groovy ================================================ package com.funtester.utils import com.funtester.config.Constant import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.Logger /** * 文件读写类,与{@link RWUtil}有功能上的重合,原因在与Java和Groovy的不兼容问题. */ class FileUtil extends Constant { private static Logger logger = LogManager.getLogger(FileUtil.class) /** * 拷贝文件 * @param source * @param target * @return */ static def copy(String source, String target) { def s = new File(source) def t = new File(target) if (s.exists() && s.isFile()) t.newOutputStream() << s.newInputStream() } /** * 重命名一个文件 * @param oldPath * @param newPath * @return */ static def rename(String oldPath, String newPath) { if (new File(oldPath).renameTo(newPath)) logger.error("rename file error!,old:{},new:{}", oldPath, newPath) } /** * 从url下载文件 * @param url * @param name * @return */ static def down(String url, String name) { new File(name) << new URL(url).openStream() } /** * 下载文件,目前只要针对图片 * @param url * @return */ static def down(String url) { def tuple = handlePicName(url) down(tuple.first, tuple.second); } /** * 获取文件夹下所有文件的绝对路径的方法,递归,排除了Linux系统的隐藏文件 * * @param path * @return */ static List getAllFile(String path) { List list = new ArrayList<>() File file = new File(path) if (!file.exists() || file.isFile()) return list File[] files = file.listFiles() int length = files.length if (length == 0) return list for (int i in 0..length - 1) { File file1 = files[i] if (file1.isDirectory()) { List allFile = getAllFile(file1.getAbsolutePath()) list.addAll(allFile) continue } String path1 = file1.getAbsolutePath() if (path1.contains("/.")) continue list.add(path1) } return list } /** * 处理下载网络图片的时候明文件的问题 * @param name * @return */ static Tuple2 handlePicName(String url) { url -= ".webp" String name = url.substring(url.lastIndexOf("/") + 1); if (name.contains(UNKNOW)) name = name.substring(0, name.indexOf(UNKNOW)) return new Tuple2(url, name) } } ================================================ FILE: src/main/groovy/com/funtester/utils/HeapDumper.java ================================================ package com.funtester.utils; import com.sun.management.HotSpotDiagnosticMXBean; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import javax.management.MBeanServer; import java.lang.management.ManagementFactory; /** * 获取JVM内存转储文件的工具类 */ public class HeapDumper { private static Logger logger = LogManager.getLogger(HeapDumper.class); /** * 这是HotSpot Diagnostic MBean的名称 */ private static final String HOTSPOT_BEAN_NAME = "com.sun.management:type=HotSpotDiagnostic"; /** * 用于存储热点诊断MBean的字段 */ private static volatile HotSpotDiagnosticMXBean hotspotMBean; /** * 下载内存转储文件 * * @param fileName 文件名,例如:heap.bin,不兼容路径,会在当前目录下生成 * @param live */ public static void dumpHeap(String fileName, boolean live) { initHotspotMBean(); try { hotspotMBean.dumpHeap(fileName, live); } catch (Exception e) { logger.error("生成内存转储文件失败!", e); } } /** * 初始化热点诊断MBean */ private static void initHotspotMBean() { if (hotspotMBean == null) { synchronized (HeapDumper.class) { if (hotspotMBean == null) { try { MBeanServer server = ManagementFactory.getPlatformMBeanServer(); hotspotMBean = ManagementFactory.newPlatformMXBeanProxy(server, HOTSPOT_BEAN_NAME, HotSpotDiagnosticMXBean.class); } catch (Exception e) { logger.error("初始化mbean失败!", e); } } } } } } ================================================ FILE: src/main/groovy/com/funtester/utils/Join.java ================================================ package com.funtester.utils; import org.apache.commons.lang3.ArrayUtils; import org.apache.commons.lang3.StringUtils; public class Join { /** * 把字符串每个字符用分隔器连接起来 * * @param text * @param separator * @return */ public static String join(String text, String separator) { return StringUtils.join(ArrayUtils.toObject(text.toCharArray()), separator); } /** * 把字符串每个字符用分隔器连接起来 * * @param text * @param separator * @return */ public static String join(String text, String separator, String prefix, String suffix) { return prefix + join(text, separator) + suffix; } /** * 把Iterable用分隔器连接起来 * * @param iterable * @param separator * @param prefix * @param suffix * @return */ public static String join(Iterable iterable, String separator, String prefix, String suffix) { return prefix + join(iterable, separator) + suffix; } /** * 把Iterable用分隔器连接起来,没有前后缀 * * @param iterable * @param separator * @return */ public static String join(Iterable iterable, String separator) { return StringUtils.join(iterable, separator); } /** * 把数组添加用间隔符连接 * * @param objects * @param separator * @param prefix 前缀 * @param suffix 后缀 * @return */ public static String join(Object[] objects, String separator, String prefix, String suffix) { return prefix + join(objects, separator) + suffix; } /** * 把数组添加用间隔符连接 * * @param objects * @param separator 间隔 * @return */ public static String join(Object[] objects, String separator) { return StringUtils.join(objects, separator); } } ================================================ FILE: src/main/groovy/com/funtester/utils/JsonUtil.groovy ================================================ package com.funtester.utils /**下面是例子,官方文档地址:https://github.com/json-path/JsonPath/blob/master/README.md * $.store.book[*].author The authors of all books * $..author All authors * $.store.* All things, both books and bicycles * $.store..price The price of everything * $..book[2] The third book * $..book[-2] The second to last book * $..book[0,1] The first two books * $..book[:2] All books from index 0 (inclusive) until index 2 (exclusive) * $..book[1:2] All books from index 1 (inclusive) until index 2 (exclusive) * $..book[-2:] Last two books * $..book[2:] Book number two from tail * $..book[?(@.isbn)] All books with an ISBN number * $.store.book[?(@.price < 10)] All books in store cheaper than 10 * $..book[?(@.price <= $['expensive'])] All books in store that are not "expensive" * $..book[?(@.author =~ /.*REES/i)] All books matching regex (ignore case) * $..* Give me every thing * $..book.length() The number of books * * * min() Provides the min value of an array of numbers Double * max() Provides the max value of an array of numbers Double * avg() Provides the average value of an array of numbers Double * stddev() Provides the standard deviation value of an array of numbers Double * length() Provides the length of an array Integer * sum() Provides the sum value of an array of numbers Double * min() 最小值 Double * max() 最大值 Double * avg() 平均值 Double * stddev() 标准差 Double * length() 数组长度 Integer * sum() 数组之和 Double * == left is equal to right (note that 1 is not equal to '1') * != left is not equal to right * < left is less than right * <= left is less or equal to right * > left is greater than right * >= left is greater than or equal to right * =~ left matches regular expression [?(@.name =~ /foo.*?/i)] * in left exists in right [?(@.size in ['S', 'M'])] * nin left does not exists in right * subsetof 子集 [?(@.sizes subsetof ['S', 'M', 'L'])] * anyof left has an intersection with right [?(@.sizes anyof ['M', 'L'])] * noneof left has no intersection with right [?(@.sizes noneof ['M', 'L'])] * size size of left (array or string) should match right * empty left (array or string) should be empty * * groovy需要 \$ Java不需要直接 $ * */ class JsonUtil { // private static Logger logger = LogManager.getLogger(JsonUtil.class) // // /** // * 用户构建对象,获取verify对象 // */ // private JSONObject json // // private JsonUtil(JSONObject json) { // this.json = json // } // // static JsonUtil getInstance(JSONObject json) { // new JsonUtil(json) // } // // JsonVerify getVerify(String path) { // JsonVerify.getInstance(this.json, path) // } // // /** // * 获取string对象 // * @param path // * @return // */ // String getString(String path) { // def object = get(path) // object == null ? Constant.EMPTY : object.toString() // } // // // /** // * 获取int类型 // * @param path // * @return // */ // int getInt(String path) { // SourceCode.changeStringToInt(getString ( path)) // } // // /** // * 获取boolean类型 // * @param path // * @return // */ // int getBoolean(String path) { // SourceCode.changeStringToBoolean(getString(path)) // } // // /** // * 获取long类型 // * @param path // * @return // */ // int getLong(String path) { // SourceCode.changeStringToLong(getString(path)) // } // // /** // * 获取double类型 // * @param path // * @return // */ // double getDouble(String path) { // SourceCode.changeStringToDouble(getString(path)) // } // // /** // * 获取list对象 // * @param path // * @return // */ // List getList(String path) { // get(path) as List // } // // /** // * 获取匹配对象,类型传参 // * 这里不加public IDE会报错 // * @param path // * @param tClass // * @return // */ // public T getT(String path, Class tClass) { // try { // get(path) as T // } catch (ClassCastException e) { // logger.warn("类型转换失败!", e) // } // } // // /** // * 获取匹配对象,这里如果使用模糊路径匹配失败,返回"[]",如果绝对路径报错 // * @param path // * @return // */ // def get(String path) { // logger.debug("匹配对象:{},表达式:{}", json.toString(), path) // if (json == null || json.isEmpty()) ParamException.fail("json为空或者null,参数错误!") // try { // JsonPath.read(this.json, path) // } catch (JsonPathException e) { // logger.warn("json: {} 解析失败,path: {}", json.toString(), path, e) // } // } // } ================================================ FILE: src/main/groovy/com/funtester/utils/RWUtil.java ================================================ package com.funtester.utils; import com.alibaba.fastjson.JSONObject; import com.funtester.base.exception.FailException; import com.funtester.base.exception.ParamException; import com.funtester.config.Constant; import com.funtester.frame.SourceCode; import groovy.lang.Tuple2; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import java.io.*; import java.net.URL; import java.util.ArrayList; import java.util.List; import java.util.stream.Collectors; /** * 文件读写类,与{@link FileUtil}有功能上的重合,原因在与Java和Groovy的不兼容问题. */ public class RWUtil extends Constant { private static Logger logger = LogManager.getLogger(RWUtil.class); /** * 读取文件信息,返回json数据 * * @param filePath * @return */ public static JSONObject readTxtByJson(String filePath) { if (StringUtils.isEmpty(filePath) || !new File(filePath).exists() || new File(filePath).isDirectory()) ParamException.fail("配置文件信息错误!" + filePath); logger.debug("读取文件名:{}", filePath); List lines = readTxtFileByLine(filePath); JSONObject info = new JSONObject(); lines.forEach(line -> { String[] split = line.split("=", 2); info.put(split[0], split[1]); }); return info; } public static JSONObject readTxtByJson(String filePath, String filter) { if (StringUtils.isEmpty(filePath) || !new File(filePath).exists() || new File(filePath).isDirectory()) ParamException.fail("配置文件信息错误!" + filePath); logger.debug("读取文件名:{}", filePath); List lines = readTxtFileByLine(filePath, filter, false); JSONObject info = new JSONObject(); lines.forEach(line -> { String[] split = line.split("=", 2); info.put(split[0], split[1]); }); return info; } /** * 通过文件信息,返回string * * @param filePath * @return */ public static String readTextByString(String filePath) { if (StringUtils.isEmpty(filePath) || !new File(filePath).exists() || new File(filePath).isDirectory()) ParamException.fail("配置文件信息错误!" + filePath); logger.debug("读取文件名:{}", filePath); List list = readTxtFileByLine(filePath); StringBuffer all = new StringBuffer(); list.forEach(line -> all.append(line + Constant.LINE)); return all.toString(); } /** * 分行读取txt文档,默认使用utf-8编码格式 * * @param filePath 文件路径 * @return 返回list数组 */ public static List readTxtFileByLine(String filePath) { return readTxtFileByLine(filePath, Constant.EMPTY, true); } /** * 分行读取txt文档,默认使用utf-8编码格式 *

line.contains(content) == key

* * @param filePath 文件路径 * @param content 过滤文本 * @param key 是否包含 * @return 返回list数组 */ public static List readTxtFileByLine(String filePath, String content, boolean key) { if (StringUtils.isEmpty(filePath) || !new File(filePath).exists() || new File(filePath).isDirectory()) ParamException.fail("文件信息错误!" + filePath); logger.debug("读取文件名:{}", filePath); List lines = new ArrayList<>(); try { String encoding = Constant.UTF_8.toString(); File file = new File(filePath); if (file.isFile() && file.exists()) { // 判断文件是否存在 FileInputStream fileInputStream = new FileInputStream(file); InputStreamReader read = new InputStreamReader(fileInputStream, encoding);// 考虑到编码格式 BufferedReader bufferedReader = new BufferedReader(read); String line = null; while ((line = bufferedReader.readLine()) != null) { if (line.contains(content) == key) lines.add(line); } bufferedReader.close(); read.close(); fileInputStream.close(); } else { logger.warn("找不到指定的文件:{}", filePath); } } catch (Exception e) { logger.warn("读取文件内容出错", e); } return lines; } /** * 从配置文件中读取数字信息 * * @param filePath * @return */ public static List readTxtFileByNumLine(String filePath) { return readTxtFileByLine(filePath, Constant.EMPTY, true).stream().map(x -> SourceCode.changeStringToInt(x)).collect(Collectors.toList()); } /** * 从配置文件中读取数字信息 * * @param filePath * @return */ public static List readTxtFileByDoubleLine(String filePath) { return readTxtFileByLine(filePath, Constant.EMPTY, true).stream().map(x -> SourceCode.changeStringToDouble(x)).collect(Collectors.toList()); } /** * 下载文件,目前只要针对图片 * * @param url */ public static void down(String url) { Tuple2 tuple2 = FileUtil.handlePicName(url); down(tuple2.getFirst().toString(), tuple2.getSecond().toString()); } /** * 通过url下载图片 * * @param url * @param name */ public static void down(String url, String name) { File file = new File(name); logger.info("下载链接:{},存储文件名:{}", url, file.getAbsolutePath()); if (!file.exists()) try { file.createNewFile(); } catch (IOException e) { logger.warn("创建文件失败!", e); } try (InputStream is = new URL(url).openStream(); OutputStream os = new FileOutputStream(file)) { int bytesRead = 0; byte[] buffer = new byte[1024]; while ((bytesRead = is.read(buffer)) != -1) { os.write(buffer, 0, bytesRead); } } catch (IOException e) { logger.warn("下载文件失败!", e); } } /** * 复制文件 * * @param oldPath 旧路径 * @param newPath 新路径 */ public static void copyFile(String oldPath, String newPath) { logger.debug("源文件名:{},目标文件名:{}", oldPath, newPath); int bytesum = 0;// 这个用来统计需要写入byte数组的长度 int byteread = 0;// 这个用来接收read()方法的返回值,表示读取内容的长度 File oldfile = new File(oldPath);// 获取源文件的file对象 if (oldfile.exists()) {// 文件存在时 try (InputStream inputStream = new FileInputStream(oldPath); FileOutputStream fileOutputStream = new FileOutputStream(newPath);) { byte[] buffer = new byte[1024];// 新建读取文件所用的数组 // 此处用while循环每次按buffer读取文件直到读取完成 while ((byteread = inputStream.read(buffer)) != -1) {// 如何读取到文件末尾 bytesum += byteread;// 此处计算读取长度,byteread表示每次读取的长度 fileOutputStream.write(buffer, 0, byteread);// 此方法第一个参数是byte数组,第二次参数是开始位置,第三个参数是长度 } logger.info("文件:{},总大小是:", oldfile, SourceCode.formatLong(bytesum));// 输出读取的总长度 fileOutputStream.flush();// 强制缓存输出,防止数据丢失 } catch (IOException e) { FailException.fail("复制文件出错!" + e.getMessage()); } } else { logger.warn("文件不存在!"); } // File oldfile2 = new File(oldPath); // oldfile2.delete(); } /** * 写入文本信息,会自动新建文件 * * @param file file对象,必须是存在的路径 * @param text 写入的内容,如果file存在,续写 */ public static void writeText(File file, String text) { logger.debug("写入文件名:{}", file); if (!file.exists()) try { file.createNewFile(); } catch (IOException e) { logger.error("文件创建失败!", e); } try { FileWriter fileWriter = new FileWriter(file, true); BufferedWriter bw1 = new BufferedWriter(fileWriter); bw1.write(text);// 将内容写到文件中 bw1.flush();// 强制输出缓冲区内容 bw1.close();// 关闭流 } catch (IOException e) { logger.warn("写入文件失败!", e); } } } ================================================ FILE: src/main/groovy/com/funtester/utils/Regex.java ================================================ package com.funtester.utils; import com.funtester.base.exception.ParamException; import com.funtester.frame.SourceCode; import org.apache.commons.lang3.StringUtils; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import java.util.ArrayList; import java.util.List; import java.util.regex.Matcher; import java.util.regex.Pattern; /** * 正则验证的封装 */ public class Regex extends SourceCode { private static Logger logger = LogManager.getLogger(Regex.class); /** * 正则校验文本是否匹配 * * @param text 需要匹配的文本 * @param regex 正则表达式 * @return */ public static boolean isRegex(String text, String regex) { return matcher(text, regex).find(); } /** * 正则校验文本是否完全匹配,不包含其他杂项,相当于加上了^和$ * * @param text 需要匹配的文本 * @param regex 正则表达式 * @return */ public static boolean isMatch(String text, String regex) { return matcher(text, regex).matches(); } /** * 获取匹配对象 * * @param text * @param regex * @return */ private static Matcher matcher(String text, String regex) { if (StringUtils.isAnyBlank(text, regex)) ParamException.fail("正则参数错误!"); return Pattern.compile(regex).matcher(text); } /** * 返回所有匹配项 * * @param text 需要匹配的文本 * @param regex 正则表达式 * @return */ public static List regexAll(String text, String regex) { Matcher matcher = matcher(text, regex); List result = new ArrayList<>(); while (matcher.find()) { result.add(matcher.group()); } return result; } /** * 获取第一个匹配对象 * * @param text * @param regex * @return */ public static String findFirst(String text, String regex) { Matcher matcher = matcher(text, regex); if (matcher.find()) return matcher.group(); return EMPTY; } /** * 获取匹配项,不包含文字信息,会删除regex的内容 *

不保证完全正确

* * @param text * @param regex * @return */ @Deprecated public static String getRegex(String text, String regex) { if (StringUtils.isAnyBlank(text, regex)) ParamException.fail("正则参数错误!"); String result = EMPTY; try { result = regexAll(text, regex).get(0); String[] split = regex.split("(\\.|\\+|\\*|\\?)"); for (int i = 0; i < split.length; i++) { String s1 = split[i]; if (!s1.isEmpty()) result = result.replaceAll(s1, EMPTY); } } catch (Exception e) { logger.warn("获取匹配对象失败!", e); } finally { return result; } } } ================================================ FILE: src/main/groovy/com/funtester/utils/StringUtil.groovy ================================================ package com.funtester.utils import com.funtester.frame.SourceCode import java.util.stream.Collectors /** * 处理各种字符串的工具类 */ class StringUtil extends SourceCode { 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] /** * emoji表情 */ private static final String[] EMOJIS = ["☢", "💸", "💷", "💶", "💵", "💼", "💻", "💰", "💮", "💴", "💳", "💨", "💧", "💦", "💪", "💡", "📘", "📗", "📖", "📕", "📜", "📚", "📙", "📐", "📏", "📎", "📍", "📔", "📓", "📑", "📈", "📇", "📆", "📅", "📌", "📋", "📊", "📉", "📀", "💿", "💾", "💽", "📄", "📃", "📂", "📁", "📷", "📼", "📻", "📺", "📹", "📰", "📯", "📮", "📭", "📲", "📱", "📨", "📧", "📦", "📥", "📬", "📫", "📪", "📩", "📠", "📟", "📞", "📝", "📤", "📡", "🔗", "🔖", "🔐", "🔏", "🔎", "🔍", "🔓", "🔒", "🔑", "🔌", "🔋", "🔮", "🔭", "🔨", "🔧", "🔦", "🔥", "🔬", "🔫", "🔪", "🔩", "🦋", "😘", "😗", "😖", "😕", "😜", "😛", "😚", "😙", "😐", "😏", "😎", "😍", "😔", "😓", "😒", "😑", "😇", "😆", "😅", "😌", "😋", "😊", "😉", "😀", "😄", "😃", "😂", "😁", "😸", "😷", "😶", "😵", "😼", "😻", "😺", "😹", "😰", "😯", "😮", "😭", "😴", "😳", "😲", "😱", "😨", "😧", "😦", "😥", "😬", "😫", "😪", "😠", "😟", "😞", "😝", "😤", "😣", "😢", "😡", "🙏", "🙎", "🙍", "🙈", "🙇", "🙆", "🙅", "🙌", "🙋", "🙊", "🙉", "🙀", "😿", "😾", "😽", "🚘", "🚗", "🚖", "🚕", "🚜", "🚛", "🚚", "✏✒", "🚐", "🚏", "🚎", "🚍", "🚔", "🚓", "🚒", "🚑", "🚈", "🚌", "🚋", "🚊", "🚉", "☀", "☁", "🚶", "🚵", "🚴", "🚲", "☎", "🚨", "🚥", "🚬", "☔", "☕", "🚪", "🚩", "🚞", "🚝", "🚣", "🛀", "🚿", "🚽", "🛁", "🌗", "🌖", "🌕", "🌔", "🌛", "🌚", "🌙", "🌘", "♈", "🌏", "♉", "🌎", "♊", "🌍", "♋", "♌", "🌓", "♍", "🌒", "♎", "🌑", "♏", "🌐", "♐", "♑", "♒", "♓", "🌊", "🌂", "🌷", "🌵", "🌴", "🌻", "🌺", "🌹", "🌸", "🌳", "🌲", "🌱", "🌰", "🌟", "🌞", "🌝", "🌜", "🌠", "🍗", "🍖", "🍕", "🍔", "🍛", "🍚", "🍙", "🍘", "🍏", "🍎", "🍍", "🍌", "🍓", "🍒", "🍑", "🍐", "🍇", "🍆", "🍅", "🍄", "🍋", "🍊", "🍉", "🍈", "🌿", "🌾", "🌽", "🌼", "🍃", "🍂", "🍁", "🍀", "🍷", "🍶", "⚡", "🍵", "🍴", "🍻", "🍺", "🍹", "🍸", "🍯", "🍮", "🍭", "🍬", "🍳", "🍲", "🍱", "🍰", "🍧", "🍦", "🍥", "🍤", "🍫", "🍪", "🍩", "🍨", "🍟", "🍞", "🍝", "🍜", "🍣", "🍢", "⚽", "🍡", "⚾", "🍠", "⛅", "🎏", "🎎", "🎌", "🎓", "🎒", "⛎", "🎐", "🎅", "🎊", "🎉", "🎈", "🍼", "🎂", "🎁", "🎀", "🎷", "🎻", "🎺", "🎸", "🎯", "🎮", "🎭", "🎬", "🎳", "🎲", "🎱", "🎰", "🎥", "🎫", "🎪", "🎩", "🎨", "🎣", "✂", "✉", "✊", "✋", "✌", "🏇", "🏆", "🏄", "🏊", "🏉", "🏈", "🎿", "🎾", "🎽", "⌚", "⌛", "🏃", "🏂", "🏀", "🏮", "❄", "⭐", "🐘", "🐗", "🐖", "🐕", "🐜", "🐛", "🐚", "🐙", "🐐", "🐏", "🐎", "🐍", "🐔", "🐓", "🐒", "🐑", "🐈", "🐇", "🐆", "🐅", "🐌", "🐋", "🐊", "🐉", "🐀", "🐄", "🐃", "🐂", "🐁", "🐸", "🐷", "🐶", "🐵", "🐼", "🐻", "🐺", "🐹", "🐰", "🐯", "🐮", "🐭", "🐴", "🐳", "🐲", "🐱", "🐨", "🐧", "🐦", "🐥", "🐬", "🐫", "🐪", "🐩", "🐠", "🐟", "🐞", "🐝", "🐤", "🐣", "🐢", "🐡", "👘", "👗", "👖", "👕", "👜", "👛", "👚", "👙", "👐", "👏", "👎", "👍", "👔", "👓", "👒", "👑", "👈", "👇", "👆", "👅", "👌", "👋", "👊", "👉", "👀", "🐾", "🐽", "👄", "👃", "👂", "👸", "👷", "👶", "👵", "👼", "👰", "👯", "👮", "👭", "👴", "👳", "👲", "👱", "👨", "👧", "👦", "👥", "👬", "👫", "👪", "👩", "👠", "👟", "👞", "👝", "👤", "👣", "👢", "👡", "💐", "💏", "💎", "💍", "💑", "⏰", "💈", "💇", "💆", "💅", "⏳", "💌", "💋", "💊", "💉", "💄", "💃", "💂", "💁"] /** * 序号 */ private static final String[] SERIAL = ["⓪", "①", "②", "③", "④", "⑤", "⑥", "⑦", "⑧", "⑨", "⑩", "⑪", "⑫", "⑬", "⑭", "⑮", "⑯", "⑰", "⑱", "⑲", "⑳"] /** * 小写汉字数字 */ private static final String[] chineses = ["〇", "一", "二", "三", "四", "五", "六", "七", "八", "九"] /** * 大写汉字数字 */ private static final String[] capeChineses = ["零", "壹", "贰", "叁", "肆", "伍", "陆", "柒", "捌", "玖"] /** * 获取随机字符串 * @param i * @return */ static String getString(int i) { def re = new StringBuffer() if (i < 1) return re for (int j in 1..i) { re << getChar() } re.toString() } /** * 获取随机字符 * @return */ static char getChar() { chars[getRandomInt(62) - 1] } /** * 获取随机字母,区分大小写 * * @return */ static char getWord() { chars[getRandomInt(52) + 9]; } /** * 获取随机字符串,没有数字 * @param i * @return */ static String getStringWithoutNum(int i) { def re = new StringBuffer() if (i < 1) return re for (int j in 1..i) { re << getWord() } re.toString() } /** * 获取所有小写字母 * @return */ static String getLowWords() { "abcdefghijklmnopqrstuvwxyz" } /** * 获取所有大写字母 * @return */ static String getUpWords() { "ABCDEFGHIJKLMNOPQRSTUVWXYZ" } /** * 获取所有的数字 * @return */ static String getNumbers() { "0123456789" } /** * 将int类型转化为汉子数字,对于3位数的数字自动补零 * @param i * @return */ static String getChinese(int i) { if (i <= 0) return "〇〇〇" String num = (i + EMPTY).collect { x -> chineses[changeStringToInt(x)] }.join() num.length() > 2 ? num : getManyString(chineses[0] + EMPTY, 3 - num.length()) + num } /** * 将int类型转化汉字大写数字表示,对于3位数的数字自动补零 * @param i * @return */ static String getCapeChinese(int i) { if (i <= 0) return "零零零" def num = (i + EMPTY).collect { x -> capeChineses[changeStringToInt(x)] }.join() num.length() > 2 ? num : getManyString(capeChineses[0] + EMPTY, 3 - num.length()) + num } /** * 随机获取emoji表情数 * * @param size * @return */ static String getEmojis(int size) { range(size).map { x -> getEmojis() }.collect(Collectors.toString()); } /** * 获取序号符号 * * @param i * @return */ static String getSerialEmoji(int i) { (i < 0 || i > 20) ? EMOJIS[0] : SERIAL[i]; } /** * 随机获取emoji表情 * * @return */ static String getEmojis() { EMOJIS[getRandomInt(EMOJIS.length - 1)]; } /** * 返回一个居中的字符串 * @param str * @param size * @return */ static String center(String str, int size) { str.center(size) } /** * 返回一个居左的文本 * @param str * @param size * @return */ static String left(String str, int size) { str.padLeft(size) } /** * 返回一个居右的文本 * @param str * @param size * @return */ static String right(String str, int size) { str.padRight(size) } //这个是添加新的的emoji表情的方法 // static void main(String[] args) { // String aa = ""; // String aaa = EMPTY; // for (int i = 0; i < aa.length(); i += 2) { // String abc = aa.substring(i, i + 2); // aaa = aaa + "\"" + aa.substring(i, i + 2) + "\","; // } // output(aaa); // aaa = EMPTY; // int length = EMOJIS.length; // HashSet strings = new HashSet<>(Arrays.asList(EMOJIS)); // for (String string : strings) { // aaa = aaa + "\"" + string + "\","; // } // output(aaa); // output(length, strings.size()); // } } ================================================ FILE: src/main/groovy/com/funtester/utils/Time.java ================================================ package com.funtester.utils; import com.funtester.config.Constant; import com.funtester.frame.SourceCode; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; import java.text.ParseException; import java.text.SimpleDateFormat; import java.util.Calendar; import java.util.Date; /** * 时间相关功能工具类 */ public class Time extends SourceCode { private static Logger logger = LogManager.getLogger(Time.class); /** * 默认的日志显示格式 */ private static ThreadLocal DEFAULT_FORMAT = new ThreadLocal() { @Override protected SimpleDateFormat initialValue() { return new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); } }; /** * 纯数字的日期格式 */ private static ThreadLocal NUM_FORMAT = new ThreadLocal() { @Override protected SimpleDateFormat initialValue() { return new SimpleDateFormat("yyyyMMddHHmmss"); } }; /** * 标记日期格式,选用ddHHmm */ private static ThreadLocal MARK_FORMAT = new ThreadLocal() { @Override protected SimpleDateFormat initialValue() { return new SimpleDateFormat("ddHHmm"); } }; /** * 获取calendar类对象,默认UTC时间 * * @return */ private static ThreadLocal calendar = new ThreadLocal() { @Override protected Calendar initialValue() { Calendar calendar = Calendar.getInstance(); calendar.setTime(new Date()); return calendar; } }; /** * 获取时间戳,13位long类型 * * @return */ public static long getTimeStamp() { return System.currentTimeMillis(); } /** * 获取一天开始,utc * * @return */ public static String getStartOfDay() { return getUtcDate() + " 00:00:00"; } /** * 获取一天结束,utc * * @return */ public static String getEndOfDay() { return getUtcDate() + " 23:55:55"; } /** * 获取当天日期,utc * * @return */ public static String getUtcDate() { int month = getMonth(); int day = getDay(); return getYear() + "-" + (month < 10 ? "0" + month : month) + "-" + (day < 10 ? "0" + day : day); } /** * 获取时间戳 * * @param time 传入时间,纯数字 * @return 返回时间戳,毫秒 */ public static long getUtcTimestamp(String time) { long timestamp = getTimeStamp(time); long utc = timestamp - Calendar.getInstance().getTimeZone().getRawOffset(); return utc; } /** * 获取UTC时间戳 * * @param time 纯数字日期 * @return */ public static long getUtcTimestamp(long time) { return getUtcTimestamp(time + EMPTY); } /** * 获取当前星期数(按年) * * @return */ public static int getWeeksNum() { return calendar.get().get(Calendar.WEEK_OF_YEAR); } /** * 获取月份,获取值+1,索引从0开始的 * * @return */ public static int getMonth() { return calendar.get().get(Calendar.MONTH) + 1; } /** * 获取当前是当月的第几天 * * @return */ public static int getDay() { return calendar.get().get(Calendar.DAY_OF_MONTH); } /** * 获取年份 * * @return */ public static int getYear() { return calendar.get().get(Calendar.YEAR); } public static int getHour() { return calendar.get().get(Calendar.HOUR_OF_DAY); } public static int getMinute() { return calendar.get().get(Calendar.MINUTE); } public static int getSecond() { return calendar.get().get(Calendar.SECOND); } /** * 获取当前时间 * * @return 返回当前时间 */ public static String getNow() { return getNow(NUM_FORMAT.get()); } public static String markDate() { return getNow(MARK_FORMAT.get()); } public static String getNow(String format) { return getNow(new SimpleDateFormat(format)); } public static String getNow(SimpleDateFormat now) { return now.format(new Date()); } /** * 获取时间戳,会替换掉所有非数字的字符 * 默认返回{@link Constant#DEFAULT_LONG} * * @param time 传入时间,纯数字组成的时间 * @return 返回时间戳,毫秒 */ public static long getTimeStamp(String time) { time = time.replaceAll("\\D*", EMPTY); try { return NUM_FORMAT.get().parse(time).getTime(); } catch (ParseException e) { logger.warn("时间格式错误!", e); } return DEFAULT_LONG; } /** * 根据时间戳返回对应的时间,并且输出 * * @param time long 时间戳 * @return 返回时间 */ public static String getTimeByTimestamp(long time) { Date now = new Date(time); String nowTime = DEFAULT_FORMAT.get().format(now); return nowTime; } /** * 获取时间差,以秒为单位 * * @param start 开始时间 * @param end 结束时间 * @return */ @Deprecated public static double getTimeDiffer(Date start, Date end) { return getTimeDiffer(start.getTime(), end.getTime()); } /** * 重载,用long类型取代date * * @param start * @param end * @return */ public static double getTimeDiffer(long start, long end) { return (end - start) / 1000.0; } /** * 获取当前时间,返回date类型 * * @return */ public static String getDate() { return getNow(DEFAULT_FORMAT.get()); } } ================================================ FILE: src/main/groovy/com/funtester/utils/TimeWatch.groovy ================================================ package com.funtester.utils import com.funtester.frame.SourceCode import org.apache.logging.log4j.LogManager import org.apache.logging.log4j.Logger import static com.funtester.config.Constant.LINE import static com.funtester.config.Constant.TAB import static com.funtester.frame.SourceCode.formatLong import static com.funtester.frame.SourceCode.getNanoMark /** * 时间观察者类,用于简单记录执行时间 */ class TimeWatch implements Serializable { private static final long serialVersionUID = -4156600036913348727L; private static Logger logger = LogManager.getLogger(TimeWatch.class) /** * 默认的名称 */ def name = "default" /** * 纳秒 */ def startNano /** * 标记集合 */ def marks = new HashMap() /** * 毫秒 */ def startMillis /** * 无参创建方法,默认名称 * @return */ static TimeWatch create() { final TimeWatch timeWatch = new TimeWatch() timeWatch.start() } /** * 创建方法 * @param name * @return */ static TimeWatch create(def name) { final TimeWatch timeWatch = new TimeWatch() timeWatch.start() } private TimeWatch() { } /** * 开始记录 * @return */ def start() { reset() } /** * 重置 */ def reset() { startNano = SourceCode.getNanoMark() startMillis = Time.getTimeStamp() this } /** * 标记 * @param name * @return */ String mark(String name) { marks.put name, new Mark(name) name } /** * 标记 * @return */ String mark() { mark(name) } /** * 获取标记时间 * @return */ def getMarkTime() { if (marks.containsKey(name)) { def diff = Time.getTimeStamp() - marks.get(name).getStartMillis() logger.info(LINE + "观察者:{}的标记:{}记录时间:{} ms", name, name, formatLong(diff)) } else { logger.warn("没有默认标记!") } } /** * 获取标记时间 * @return */ def getMarkNanoTime() { if (marks.containsKey(name)) { def diff = getNanoMark() - marks.get(name).getStartNano() logger.info(LINE + "观察者:{}的标记:{}记录时间:{} ns", name, name, formatLong(diff)) } else { logger.warn("没有默认标记!") } } /** * 获取某个标记的记录时间 * @param name * @return */ def getMarkTime(String name) { if (marks.containsKey(name)) { def diff = Time.getTimeStamp() - marks.get(name).getStartMillis() logger.info(LINE + "观察者:{}的标记:{}记录时间:{} ms", name, name, formatLong(diff)) } else { logger.warn("没有{}标记!", name) } } /** * 获取某个标记的记录时间 * @param name * @return */ def getMarkNanoTime(String name) { if (marks.containsKey(name)) { def diff = getNanoMark() - marks.get(name).getStartNano() logger.info(LINE + "观察者:{}的标记:{}记录时间:{} ns", name, name, formatLong(diff)) } else { logger.warn("没有{}标记!", name) } } /** * 获取记录时间 * @return */ def getTime() { def diff = Time.getTimeStamp() - startMillis logger.info(LINE + "观察者:{},记录时间:{} ms", getName(), formatLong(diff)) diff } /** * 获取记录时间纳秒 * @return */ def getNanoTime() { long diff = getNanoMark() - startNano logger.info(LINE + "观察者:{},记录时间:{} ns", getName(), formatLong(diff)) diff } /** * 获取标记与观察者的时间差 * @param name * @return */ def getDiffTime(String name) { if (marks.containsKey(name)) { def diff = marks.get(name).getStartMillis() - this.getStartMillis() logger.info(LINE + "观察者:{}和标记:{}记录时间差:{} ms", name, name, formatLong(diff)) } else { logger.warn("没有{}标记!", name) } } /** * 获取标记与观察者的时间差 * @param name * @return */ def getDiffNanoTime(String name) { if (marks.containsKey(name)) { def diff = marks.get(name).getStartNano() - this.getStartNano() logger.info(LINE + "观察者:{}和标记:{}记录时间差:{} ns", name, name, formatLong(diff > 0 ? diff : -diff)) } else { logger.warn("没有{}标记!", name) } } /** * 获取两个标记的时间差 * @param first * @param second * @return */ def getDiffTime(String first, String second) { if (marks.containsKey(first) && marks.containsKey(second)) { def diff = marks.get(second).getStartMillis() - marks.get(first).getStartMillis() logger.info(LINE + "标记:{}和标记:{}记录时间差:{} ms", first, second, diff) } else { logger.warn("没有{}标记!", first + TAB + second) } } /** * 获取两个标记的时间差 * @param first * @param second * @return */ def getDiffNanoTime(String first, String second) { if (marks.containsKey(first) && marks.containsKey(second)) { def diff = marks.get(second).getStartNano() - marks.get(first).getStartNano() logger.info(LINE + "标记:{}和标记:{}记录时间差:{} ns", first, second, formatLong(diff)) } else { logger.warn("没有{}标记!", first + TAB + second) } } @Override String toString() { return "时间观察者:" + this.name } @Override TimeWatch clone() { TimeWatch watch = new TimeWatch() watch.name = getName() + "_c" watch.startMillis = this.getStartMillis() watch.startNano = this.getStartNano() watch } /** * 标记类 */ class Mark implements Serializable { private static final long serialVersionUID = -41564036913335727L; Mark(def name) { this.name = name reset() } def name def startNano def startMillis def lastNano def lastMills def reset() { this.startNano = getNanoMark() this.startMillis = Time.getTimeStamp() } } } ================================================ FILE: src/main/groovy/com/funtester/utils/WriteHtml.java ================================================ package com.funtester.utils; import java.util.List; /** * 生成表格封装类 */ public class WriteHtml { /** * 获取表格头部信息 * * @param list * @return */ private static String getTable(List list) { StringBuffer start = new StringBuffer(""); start.append(""); for (int i = 0; i < list.size(); i++) { start.append(""); } String end = ""; return start + end; } /** * 拼接整个页面 * * @param result * @param title * @return */ public static String createWebReport(List> result, String title) { String starttext = "

" + title + "

" + getTable(result.get(0)); String endtext = "
序号" + list.get(i).toString() + "
"; StringBuffer sheet = new StringBuffer(starttext); for (int i = 1; i < result.size(); i++) { List objects = result.get(i); sheet.append(""); sheet.append("" + i + ""); sheet.append(getTr(objects)); sheet.append(""); } sheet.append(endtext); return sheet.toString(); } /** * 获取行信息 * * @param tr * @return */ private static StringBuffer getTr(List tr) { StringBuffer body = new StringBuffer(); for (int i = 0; i < tr.size(); i++) { body.append("" + tr.get(i).toString() + ""); } return body; } } ================================================ FILE: src/main/groovy/com/funtester/utils/message/AlertOver.java ================================================ package com.funtester.utils.message; import com.funtester.base.bean.RequestInfo; import com.funtester.base.interfaces.IMessage; import com.funtester.httpclient.FunLibrary; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; public class AlertOver extends FunLibrary implements IMessage { private static Logger logger = LogManager.getLogger(AlertOver.class); String title; String content; String murl; RequestInfo requestInfo; private static String system = "s-7e93ec02-1308-480c-bc11-a7260c14";//系统异常 private static String function = "s-7e3b7ea5-b4b0-4479-a0e3-bce6c830";//功能异常 private static String business = "s-466a191a-cbb8-4164-b8be-9779bb88";//业务异常 private static String remind = "s-f49ac5bc-008b-4b11-890e-6715ef89";//提醒推送 private static String code = "s-490d0fc6-35cc-4430-9f87-09cdeb05";//程序异常 private static final String testGroup = "g-4eefc0ad-19af-4b1c-9d0b-ef87be15"; public AlertOver() { this("test title", "test content!"); } public AlertOver(String title, String content) { this.title = title; this.content = content + LINE + "发送源:" + COMPUTER_USER_NAME; } public AlertOver(String title, String content, String url) { this(title, content); this.murl = url; } public AlertOver(String title, String content, String url, RequestInfo requestInfo) { this(title, content); this.murl = url; this.requestInfo = requestInfo; } /** * 发送系统异常 */ public void sendSystemMessage() { // if (SysInit.isBlack(murl)) return; // sendMessage(system); // MySqlTest.saveAlertOverMessage(requestInfo, "system", title, LOCAL_IP, COMPUTER_USER_NAME); // logger.info("发送系统错误提醒,title:{},ip:{},computer:{}", title, LOCAL_IP, COMPUTER_USER_NAME); } /** * 发送功能异常 */ public void sendFunctionMessage() { sendMessage(function); } /** * 发送业务异常 */ public void sendBusinessMessage() { sendMessage(business); } /** * 发送程序异常 */ public void sendCodeMessage() { sendMessage(code); } /** * 提醒推送 */ public void sendRemindMessage() { sendMessage(remind); } /** * 发送消息 * * @return */ public void sendMessage(String source) { // if (SysInit.isBlack(murl)) return; // String url = "https://api.alertover.com/v1/alert"; // String receiver = testGroup;//测试组ID // JSONObject jsonObject = new JSONObject();// 新建json数组 // jsonObject.put("frame", source);// 添加发送源id // jsonObject.put("receiver", receiver);// 添加接收组id // jsonObject.put("content", content);// 发送内容 // jsonObject.put("title", title);// 发送标题 // jsonObject.put("url", murl);// 发送标题 // jsonObject.put("sound", "pianobar");// 发送声音 // logger.debug("消息详情:{}", jsonObject.toString()); // HttpPost httpPost = getHttpPost(url, jsonObject); /*取消发送*/ // getHttpResponse(httpPost); } } ================================================ FILE: src/main/groovy/com/funtester/utils/message/EmailUtil.java ================================================ package com.funtester.utils.message; import com.funtester.frame.SourceCode; /** * 发送邮件的类,暂时使用静态方法,默认使用QQ邮箱发送 */ public class EmailUtil extends SourceCode { // private static Logger logger = LogManager.getLogger(EmailUtil.class); // // private static Session session; // // private static void instance() { // Security.addProvider(new Provider()); // //设置邮件会话参数 // Properties props = new Properties(); // //邮箱的发送服务器地址 // props.setProperty("mail.smtp.host", EmailConstant.QQ_HOST); // props.setProperty("mail.smtp.socketFactory.class", EmailConstant.SSL_FACTORY); // props.setProperty("mail.smtp.socketFactory.fallback", "false"); // //邮箱发送服务器端口,这里设置为465端口 // props.setProperty("mail.smtp.port", "465"); // props.setProperty("mail.smtp.socketFactory.port", "465"); // props.put("mail.smtp.auth", "true"); // //获取到邮箱会话,利用匿名内部类的方式,将发送者邮箱用户名和密码授权给jvm // session = Session.getDefaultInstance(props, new Authenticator() { // protected PasswordAuthentication getPasswordAuthentication() { // return new PasswordAuthentication(EmailConstant.QQ_USERNAME, EmailConstant.QQ_PASSWORD); // } // }); // } // // /** // * 向邮箱发送邮件 // * // * @param email 对方的邮件地址 // * @param title 邮件的标题 // * @param content 邮件的内容 // * @return // */ // public static boolean sendEmail(String email, String title, String content) { // //多线程优化 // if (session == null) { // synchronized (EmailUtil.class) { // if (session == null) // instance(); // } // } // try { // Message msg = new MimeMessage(session); // //设置发件人 // msg.setFrom(new InternetAddress(EmailConstant.QQ_USERNAME)); // //设置收件人,to为收件人,cc为抄送,bcc为密送 // msg.setRecipients(Message.RecipientType.TO, InternetAddress.parse(email, false)); // msg.setRecipients(Message.RecipientType.CC, InternetAddress.parse(email, false)); // msg.setRecipients(Message.RecipientType.BCC, InternetAddress.parse(email, false)); // msg.setSubject(title); // //设置邮件消息 // msg.setText(content); // //设置发送的日期 // msg.setSentDate(new Date()); // //调用Transport的send方法去发送邮件 // Transport.send(msg); // return true; // } catch (MessagingException e) { // logger.error(e.getMessage()); // return false; // } // } } ================================================ FILE: src/main/groovy/com/funtester/utils/request/Request.java ================================================ package com.funtester.utils.request; import com.alibaba.fastjson.JSONObject; import com.funtester.config.RequestType; import com.funtester.frame.SourceCode; import com.funtester.utils.Regex; import java.util.ArrayList; import java.util.List; import java.util.Set; /** * 从swagger文档中读取到的一个请求的所有信息 */ public class Request extends SourceCode { /** * 请求的url */ private String url; public String getUrl() { return url; } public RequestType getType() { return type; } public String getApiName() { return apiName; } public String getDesc() { return desc; } public JSONObject getArgs() { return args; } public JSONObject getParams() { return params; } public StringBuffer getStringBuffer() { return stringBuffer; } /** * 请求类型 */ RequestType type; public void setUrl(String url) { this.url = url; } public void setType(RequestType type) { this.type = type; } public void setApiName(String apiName) { this.apiName = apiName; } public void setDesc(String desc) { this.desc = desc; } public void setArgs(JSONObject args) { this.args = args; } public void setParams(JSONObject params) { this.params = params; } public void setStringBuffer(StringBuffer stringBuffer) { this.stringBuffer = stringBuffer; } public void setCode(StringBuffer code) { this.code = code; } /** * 接口名称 */ private String apiName; /** * 接口描述 */ private String desc; /** * restful参数 */ List restfulArgs = new ArrayList<>(); /** * query参数 */ JSONObject args = new JSONObject(); /** * formdata参数 */ JSONObject params = new JSONObject(); /** * 参数替换字符串,用户想方法里面添加参数 */ StringBuffer stringBuffer = new StringBuffer(); /** * 代码文本 */ StringBuffer code = new StringBuffer(); /** * 如果遇到post请求,fromdata参数为空时,url里面直接拼接请求字符串 */ boolean postNoParams = false; /** * 拼接json参数 * * @param i 0:get请求;1:post请求 */ private void spliceArgs(int i) { String type = i == 1 ? "params" : "args"; this.code.append(LINE + TAB + TAB + "JSONObject " + type + " = new JSONObject();"); Set keySet = i == 0 ? args.keySet() : params.keySet(); keySet.forEach(key -> { collectArgs(key.toString(), params.getString(key.toString())); this.code.append(LINE + TAB + TAB + type + ".put(\"" + key.toString() + "\", " + key.toString() + ");"); }); } /** * 收集参数,拼接往json传参的代码行 * * @param key * @param value */ private void collectArgs(String key, String value) { if (value.equals("string")) this.stringBuffer.append("String " + key.toString() + ","); if (value.equals("integer")) this.stringBuffer.append("int " + key.toString() + ","); } /** * 收集restful参数,处理url */ private void collectRestfulArgs() { if (this.url.contains("{")) {//restful公参处理,并提取到restfulargs里面 List regexAll = Regex.regexAll(this.url, "\\{[^}]+\\}"); regexAll.forEach(regex -> { regex = regex.replace("{", EMPTY).replace("}", EMPTY); this.restfulArgs.add(regex); }); } } /** * 拼接url * * @return */ private String spliceUrl() { collectRestfulArgs(); this.url = this.url.contains("{") ? this.url : this.url + "\""; this.url = "\"" + this.url.replace("}/{", "+OR+").replace("{", "\"+").replace("}", EMPTY); return TAB + TAB + "String url = HOST + " + this.url + ";"; } /** * 拼接get请求 */ private void spliceGet() { if (!this.args.isEmpty()) { spliceArgs(0); this.code.append(LINE + TAB + TAB + "HttpGet httpGet = getHttpGet(url, args);");//拼接获取请求方法 } else { this.code.append(LINE + TAB + TAB + "HttpGet httpGet = getHttpGet(url);");//拼接获取请求方法 } this.code.append(LINE + TAB + TAB + "JSONObject response = getHttpResponseEntityByJson(httpGet);");//拼接发送请求获取响应的方法 } /** * 拼接post请求 */ private void splicePost() { if (!this.args.isEmpty()) spliceArgs(0); if (!this.params.isEmpty()) spliceArgs(1); if (this.args.isEmpty()) {//处理为空的情况 if (!this.params.isEmpty()) { this.code.append(LINE + TAB + TAB + "HttpPost httpPost = getHttpPost(url, params);"); } else { this.code.append(LINE + TAB + TAB + "HttpPost httpPost = getHttpPost(url);"); } } else { if (!this.params.isEmpty()) { this.code.append(LINE + TAB + TAB + "HttpPost httpPost = getHttpPost(url, args, params);"); } else { this.postNoParams = true; } } this.code.append(LINE + TAB + TAB + "JSONObject response = getHttpResponseEntityByJson(httpPost);"); } /** * 拼接响应后代码行 * * @return */ private String spliceEnd() { restfulArgs.forEach(key -> stringBuffer.append("int " + key.toString() + ","));//在方法中添加参数类型的名称 this.code.append(LINE + TAB + TAB + "output(response);");//拼接输出响应 this.code.append(LINE + TAB + TAB + "return response;");//返回响应 this.code.append(LINE + TAB + "}"); return this.code.toString().replace("() {", "(" + stringBuffer.toString() + ") {").replace(",)", ")");//替换参数类型和名称 } /** * 把request对象变成代码的方法 * * @return */ public String magic() { this.code.append(TAB + "/**\n\t * " + desc + "\n\t *\n\t * @return\n\t */" + LINE); this.code.append(TAB + "public JSONObject " + apiName + "() {" + LINE);//新建方法行 String urlLine = spliceUrl(); this.code.append(urlLine); if (restfulArgs.size() > 0) restfulArgs.forEach(arg -> args.remove(arg));//将公参从args里面删除 if (this.type == RequestType.GET) spliceGet(); if (this.type == RequestType.POST) splicePost(); String finalCode = spliceEnd(); if (this.postNoParams) finalCode = finalCode.replace(urlLine, urlLine.replace(";", EMPTY) + " + changeJsonToArguments(args)"); return finalCode; } } ================================================ FILE: src/main/groovy/com/funtester/utils/request/RequestFile.java ================================================ package com.funtester.utils.request; import com.funtester.config.Constant; import com.funtester.config.RequestType; import com.funtester.httpclient.FunLibrary; import com.funtester.utils.RWUtil; import com.alibaba.fastjson.JSONObject; import org.apache.http.client.methods.HttpRequestBase; import org.apache.logging.log4j.LogManager; import org.apache.logging.log4j.Logger; /** * 从文件中读取接口相关参数,用来发送请求,实现接口请求的配置化 *

从当前路径下获取后缀为.log的文件,以文件名为准读取文件内容

*/ public class RequestFile extends FunLibrary { private static Logger logger = LogManager.getLogger(RequestFile.class); String url; /** * get对应get请求,post对应post请求表单参数,其他对应post请求json参数 */ JSONObject headers; RequestType requestType; String name; JSONObject info; JSONObject params; /** * @param name */ public RequestFile(String name) { this.name = name; getInfo(); this.url = this.info.getString("url"); requestType = RequestType.getRequestType(this.info.getString("requestType")); getParams(); headers = JSONObject.parseObject(this.info.getString("headers")); } /** * 获取当前目录下的配置文件,以数字开头,后缀是.log的 * * @param i */ public RequestFile(int i) { this(i + Constant.EMPTY); } /** * 从配置文件中读取信息,组成一个json对象 */ private void getInfo() { String filePath = Constant.WORK_SPACE + this.name; logger.info("配置文件地址:" + filePath); this.info = RWUtil.readTxtByJson(filePath); } /** * 获取请求参数 */ private void getParams() { params = JSONObject.parseObject(info.getString("params")); } /** * 根据info组成请求 * * @return */ public HttpRequestBase getRequest() { HttpRequestBase requestBase; switch (this.requestType) { case GET: requestBase = getHttpGet(this.url, this.params); break; case POST: requestBase = getHttpPost(this.url, this.params); break; default: requestBase = getHttpPost(this.url, this.params.toString()); break; } this.headers.keySet().forEach(x -> requestBase.addHeader(getHeader(x.toString(), headers.getString(x.toString())))); output(getHttpResponse(requestBase)); return requestBase; } } ================================================ FILE: src/main/groovy/com/funtester/utils/request/Swagger.java ================================================ package com.funtester.utils.request; import com.alibaba.fastjson.JSONArray; import com.alibaba.fastjson.JSONObject; import com.funtester.config.RequestType; import com.funtester.httpclient.FunLibrary; import com.funtester.utils.Regex; import java.util.ArrayList; import java.util.List; import java.util.Set; /** * swagger文档解析类 */ public class Swagger extends FunLibrary { /** * 关键字,用于url前 */ String key; /** * swagger文档地址 */ String swaggerPath; /** * 构造方法中接口地址类别 */ String name; /** * swagger地址所有类别 */ List names = new ArrayList<>(); /** * 某类别所有接口地址 */ List urls = new ArrayList<>(); /** * swagger文档转换成的json对象 */ JSONObject swagger = new JSONObject(); /** * 所有接口地址的json对象 */ JSONObject paths = new JSONObject(); /** * 对应构造方法中url的request对象 */ Request request = new Request(); public Request getRequest() { return request; } public List getAllRequests() { return allRequests; } /** * 对应构造方法中name的所有request对象 */ List allRequests = new ArrayList<>(); /** * 获取某一类的接口的request对象 * * @param swaggerPath * @param name */ public Swagger(String swaggerPath, String name) { this.swaggerPath = swaggerPath; this.name = name; build(); } /** * 获取某一类的某一个接口的request对象 * * @param swaggerPath * @param name * @param url */ public Swagger(String swaggerPath, String name, String url) { this.swaggerPath = swaggerPath; this.name = name; build(); request = getRequest(url); } public String getKey() { this.key = Regex.regexAll(this.swaggerPath, "/((?!/).)*/swagger.json").get(0); this.key = this.key.replace(OR, EMPTY).replace("swagger.json", EMPTY); if (this.key.contains(":")) this.key = EMPTY; return this.key; } /** * 获取name下所有接口的request对象 */ private void getRequests() { this.urls.forEach(url -> { Request request = getRequest(url); if (request != null) allRequests.add(request); }); } /** * 初始化处理方法 */ public void build() { swagger = getHttpResponse(getHttpGet(this.swaggerPath)); getKey(); getNames(); getPaths(); getUrls(); getRequests(); } /** * 获取某一个url地址的请求request对象 * * @param url 接口地址 * @return */ private Request getRequest(String url) { Request request = new Request(); request.setUrl((OR + key + url).replace("//", "/")); JSONObject json1 = paths.getJSONObject(url); JSONObject json2 = new JSONObject(); if (json1.containsKey("get")) { request.setType(RequestType.GET); json2 = json1.getJSONObject("get"); } else if (json1.containsKey("post")) { request.setType(RequestType.POST); json2 = json1.getJSONObject("post"); } String tags = json2.get("tags").toString(); if (!tags.contains(name)) return null; String apiName = json2.getString("operationId"); request.setApiName(apiName); String desc = json2.getString("summary"); request.setDesc(desc); JSONArray json3 = json2.getJSONArray("parameters"); JSONObject json5 = new JSONObject(); JSONObject json6 = new JSONObject(); json3.forEach(json -> {//获取参数,区分query和formdata JSONObject json4 = (JSONObject) json; String in = json4.getString("in"); if (in.equals("query")) { boolean required = json4.getBoolean("required"); if (required) { String format = json4.getString("type"); String name = json4.getString("name"); json5.put(name, format); } } else if (in.equals("formData")) { boolean required = json4.getBoolean("required"); if (required) { String format = json4.getString("type"); String name = json4.getString("name"); json6.put(name, format); } } }); request.setArgs(json5); request.setParams(json6); return request; } /** * 获取name下所有接口的地址 */ private void getUrls() { Set keySet = paths.keySet(); keySet.forEach(key -> urls.add(key.toString())); } /** * 获取所有name */ private void getNames() { JSONArray tags = swagger.getJSONArray("tags"); tags.forEach(info -> { JSONObject name = (JSONObject) info; names.add(name.getString("name")); }); } /** * 获取所有的接口地址 */ private void getPaths() { paths = swagger.getJSONObject("paths"); } } ================================================ FILE: src/main/groovy/com/funtester/utils/xml/Attr.groovy ================================================ package com.funtester.utils.xml /** * 节点属性信息 */ class Attr { //class Attr extends AbstractBean implements Serializable{ // private static final long serialVersionUID = -35484487563215649L // // String name // // String value // // Attr(String name, String value) { // this.name = name // this.value = value // } // } ================================================ FILE: src/main/groovy/com/funtester/utils/xml/NodeInfo.groovy ================================================ package com.funtester.utils.xml /** * 节点信息 */ class NodeInfo { //class NodeInfo extends AbstractBean implements Serializable{ // private static final long serialVersionUID = 568896512159847L // // List attrs // // List children } ================================================ FILE: src/main/groovy/com/funtester/utils/xml/XMLUtil.groovy ================================================ package com.funtester.utils.xml /** * 基于dom解析xml文件工具类 */ class XMLUtil { // private static Logger logger = LogManager.getLogger(XMLUtil.class) // // /** // * 解析某个节点(根节点)信息 // * @param path 绝对路径或者URL // * @param root // * @return // */ // static List parseRoot(String path, String root) { // DocumentBuilderFactory dbf = DocumentBuilderFactory.newInstance() // try { // DocumentBuilder db = dbf.newDocumentBuilder() // Document document = db.parse(path.startsWith("http") ? new URI(path) : new File(path)) // NodeList nodeList = document.getElementsByTagName(root) // return range(nodeList.getLength()).mapToObj {x -> parseNode(nodeList.item(x))}.collect() as List // } catch (ParserConfigurationException e) { // logger.error("解析配置错误!", e) // } catch (IOException e) { // logger.error("IO错误!", e) // } catch (SAXException e) { // logger.error("SAX错误!", e) // } // FailException.fail("解析文件:${path}中${root}节点出错!") // } // // /** // * 解析某个节点信息 // * @param node // * @return // */ // static NodeInfo parseNode(Node node) { // if (node.getNodeType() != Node.ELEMENT_NODE) return null // NodeInfo nodeInfo = new NodeInfo() // NamedNodeMap attrs = node.getAttributes() // List nodeAttr = new ArrayList<>() // range(attrs.getLength()).each { // Node attr = attrs.item(it) // nodeAttr << new Attr(attr.getNodeName(), attr.getNodeValue()) // } // nodeInfo.attrs = nodeAttr // NodeList childNodes = node.getChildNodes() // List children = new ArrayList<>() // range(childNodes.getLength()).each {children.add(parseNode(childNodes.item(it)))} // nodeInfo.children = children.findAll {it != null} // return nodeInfo // } } ================================================ FILE: src/main/groovy/com/funtester/utils/xml/XMLUtil2.groovy ================================================ package com.funtester.utils.xml /** * 基于dom4j解析xml工具类 */ class XMLUtil2 { // private static Logger logger = LogManager.getLogger(XMLUtil2.class) // // /** // * 解析xml文件 // * @param path 绝对路径或者URL // * @return // */ // static List parse(String path) { // SAXReader reader = new SAXReader(); // try { // Document document = reader.read(path.startsWith("http") ? new URL(path) : new File(path)); // Element rootElement = document.getRootElement(); // def iterator = rootElement.elementIterator() // List info = new ArrayList<>() // while (iterator.hasNext()) { // info << parseNode(iterator.next() as Element) // } // return info; // } catch (DocumentException e) { // logger.error("解析文件${path}失败!", e) // } // FailException.fail("解析文件${path}失败!") // } // // /** // * 解析节点信息 // * @param e // * @return // */ // static NodeInfo parseNode(Element e) { // if (e.getNodeType() != Node.ELEMENT_NODE) return null; // def info = new NodeInfo() // List attributes = e.attributes(); // List attrs = new ArrayList<>() // attributes.each { // attrs << new Attr(it.name, it.value) // } // info.setAttrs(attrs) // List children = new ArrayList<>() // def iterator = e.elementIterator() // if (iterator.hasNext()) { // children << parseNode(iterator.next() as Element) // } // info.setChildren(children) // return info; // } } ================================================ FILE: src/main/resources/http.properties ================================================ ssl_v=TLSv1.2 User-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 Connection=keep-alive black_host=wblçog.fv1314.xyz:8082,172.18.4.55:8888 TIMEOUT=10 TRY_TIMES=3 MAX_ACCEPT_TIME=5 ================================================ FILE: src/main/resources/log4j2.xml ================================================ ================================================ FILE: src/main/resources/mysql.properties ================================================ test_mysql_url=jdbc:mysql://172.18.4.55:3306/okayapi user=root password=dsjw2016 mysql_server_path=http://172.18.4.55:8888/mysql flag=false ================================================ FILE: src/main/resources/redis.properties ================================================ ip=58.87.70.151 port=6379 max_total=10 max_idle=5 min_idle=2 max_wait=60000 timeout=60000