├── .gitignore ├── Berserker_Schema.sql ├── README.md ├── build.gradle ├── build.gradle.prod ├── gradle └── wrapper │ ├── gradle-wrapper.jar │ └── gradle-wrapper.properties ├── gradlew ├── gradlew.bat └── src ├── main ├── java │ └── com │ │ └── berserker │ │ └── architecture │ │ ├── ArchitectureApplication.java │ │ ├── RedisConfig.java │ │ ├── ServletInitializer.java │ │ ├── WebAppConfigurerAdapter.java │ │ ├── WebSocketConfig.java │ │ ├── controller │ │ ├── ScenarioInfoController.java │ │ ├── ScenarioResultController.java │ │ ├── ScenarioRunController.java │ │ └── ScenarioSampleController.java │ │ ├── domain │ │ ├── entity │ │ │ ├── ParamFileInfo.java │ │ │ ├── SampleResultInfo.java │ │ │ ├── ScenarioInfo.java │ │ │ ├── ScenarioResultInfo.java │ │ │ └── ScriptFileInfo.java │ │ └── param │ │ │ ├── ScenarioInfoAddParams.java │ │ │ ├── ScenarioInfoListParams.java │ │ │ ├── ScenarioInfoModParams.java │ │ │ ├── ScenarioResultByIdParams.java │ │ │ └── ScenarioResultListParams.java │ │ ├── engine │ │ ├── cache │ │ │ └── RedisSampleSerializer.java │ │ ├── collect │ │ │ ├── EngineSampleCollector.java │ │ │ └── EngineSampleRealTimeOuter.java │ │ ├── core │ │ │ ├── EngineController.java │ │ │ ├── EngineParamLoader.java │ │ │ ├── EngineResultHandler.java │ │ │ ├── EngineScriptParser.java │ │ │ ├── EngineTestPlanSetter.java │ │ │ └── EngineTestRunner.java │ │ └── reader │ │ │ └── EngineScenarioReader.java │ │ ├── exception │ │ └── GlobalExceptionHandler.java │ │ ├── interceptor │ │ ├── PreventRepeatSubmit.java │ │ └── PreventRepeatSubmitInterceptor.java │ │ ├── mapper │ │ ├── ParamFileInfoMapper.java │ │ ├── SampleResultInfoMapper.java │ │ ├── SampleResultInfoProvider.java │ │ ├── ScenarioInfoMapper.java │ │ ├── ScenarioInfoProvider.java │ │ ├── ScenarioResultInfoMapper.java │ │ └── ScriptFileInfoMapper.java │ │ ├── service │ │ ├── ParamFileService.java │ │ ├── SampleResultService.java │ │ ├── ScenarioInfoService.java │ │ ├── ScenarioResultService.java │ │ ├── ScenarioRunService.java │ │ ├── ScriptInfoService.java │ │ └── impl │ │ │ ├── ParamFileServiceImpl.java │ │ │ ├── SampleResultServiceImpl.java │ │ │ ├── ScenarioInfoServiceImpl.java │ │ │ ├── ScenarioResultServiceImpl.java │ │ │ ├── ScenarioRunServiceImpl.java │ │ │ └── ScriptInfoServiceImpl.java │ │ └── utils │ │ ├── CommonUtil.java │ │ ├── FileStorageUtil.java │ │ └── ScriptUtil.java └── resources │ ├── application.yml │ ├── application.yml.prod │ ├── logback.xml │ ├── static │ ├── css │ │ ├── bootstrap-fileinput.css │ │ ├── bootstrap-select.css │ │ ├── bootstrap-table.css │ │ ├── bootstrap-theme.min.css │ │ ├── bootstrap-treeview.min.css │ │ ├── bootstrap.css │ │ ├── bootstrap.css.map │ │ ├── bootstrap.min.css.map │ │ └── sb-admin.css │ ├── custom │ │ ├── layout.js │ │ ├── scenarioAdd.js │ │ ├── scenarioList.js │ │ ├── scenarioModify.js │ │ ├── scenarioNewResultList.js │ │ ├── scenarioRealTimeChart.js │ │ ├── scenarioRealTimeChartGrid.js │ │ ├── scenarioResultListById.js │ │ └── scenarioSampleResult.js │ ├── font-awesome │ │ ├── css │ │ │ ├── font-awesome.css │ │ │ └── font-awesome.min.css │ │ ├── fonts │ │ │ ├── FontAwesome.otf │ │ │ ├── fontawesome-webfont.eot │ │ │ ├── fontawesome-webfont.svg │ │ │ ├── fontawesome-webfont.ttf │ │ │ ├── fontawesome-webfont.woff │ │ │ ├── fontawesome-webfont.woff2 │ │ │ ├── glyphicons-halflings-regular.eot │ │ │ ├── glyphicons-halflings-regular.svg │ │ │ ├── glyphicons-halflings-regular.ttf │ │ │ ├── glyphicons-halflings-regular.woff │ │ │ └── glyphicons-halflings-regular.woff2 │ │ ├── less │ │ │ ├── animated.less │ │ │ ├── bordered-pulled.less │ │ │ ├── core.less │ │ │ ├── fixed-width.less │ │ │ ├── font-awesome.less │ │ │ ├── icons.less │ │ │ ├── larger.less │ │ │ ├── list.less │ │ │ ├── mixins.less │ │ │ ├── path.less │ │ │ ├── rotated-flipped.less │ │ │ ├── screen-reader.less │ │ │ ├── stacked.less │ │ │ └── variables.less │ │ └── scss │ │ │ ├── _animated.scss │ │ │ ├── _bordered-pulled.scss │ │ │ ├── _core.scss │ │ │ ├── _fixed-width.scss │ │ │ ├── _icons.scss │ │ │ ├── _larger.scss │ │ │ ├── _list.scss │ │ │ ├── _mixins.scss │ │ │ ├── _path.scss │ │ │ ├── _rotated-flipped.scss │ │ │ ├── _screen-reader.scss │ │ │ ├── _stacked.scss │ │ │ ├── _variables.scss │ │ │ └── font-awesome.scss │ ├── img │ │ └── loading.gif │ └── js │ │ ├── boost-canvas.js │ │ ├── boost-canvas.js.map │ │ ├── boost.js │ │ ├── boost.js.map │ │ ├── bootbox.min.js │ │ ├── bootstrap-fileinput-theme.js │ │ ├── bootstrap-fileinput-zh-CN.js │ │ ├── bootstrap-fileinput.js │ │ ├── bootstrap-select.js │ │ ├── bootstrap-table-zh-CN.js │ │ ├── bootstrap-table.js │ │ ├── bootstrap-treeview.min.js │ │ ├── bootstrap.js │ │ ├── bootstrap.min.js │ │ ├── dark-unica.js │ │ ├── dark-unica.js.map │ │ ├── exporting.js │ │ ├── exporting.js.map │ │ ├── highcharts-zh_CN.js │ │ ├── highcharts.js │ │ ├── highcharts.js.map │ │ ├── jquery-1.12.4.js │ │ ├── jquery-1.12.4.min.js │ │ ├── jquery.form.js │ │ ├── sockjs.js │ │ ├── sockjs.min.js │ │ ├── stomp.js │ │ └── stomp.min.js │ └── templates │ ├── layout.html │ ├── scenarioAdd.html │ ├── scenarioException.html │ ├── scenarioList.html │ ├── scenarioModify.html │ ├── scenarioNewResultList.html │ ├── scenarioRealTimeChart.html │ ├── scenarioRealTimeChartGrid.html │ ├── scenarioResultListById.html │ ├── scenarioSampleDetailChart.html │ └── scenarioSampleDetailChartGrid.html └── test └── java └── com └── berserker └── architecture ├── ScenarioControllerTests.java ├── ScenarioServiceTests.java └── ScriptControllerTests.java /.gitignore: -------------------------------------------------------------------------------- 1 | .gradle 2 | !gradle/wrapper/gradle-wrapper.jar 3 | 4 | ### IntelliJ IDEA ### 5 | .idea 6 | *.iws 7 | *.iml 8 | *.ipr 9 | 10 | ### NetBeans ### 11 | nbproject/private/ 12 | build/ 13 | nbbuild/ 14 | dist/ 15 | nbdist/ 16 | .nb-gradle/ 17 | log/ 18 | out/ -------------------------------------------------------------------------------- /Berserker_Schema.sql: -------------------------------------------------------------------------------- 1 | CREATE TABLE `scenarioinfo` ( 2 | `id` int(8) NOT NULL AUTO_INCREMENT, 3 | `scenarioName` varchar(256) DEFAULT NULL, 4 | `createTime` datetime DEFAULT NULL, 5 | `numThreads` int(4) DEFAULT NULL, 6 | `rampUp` int(4) DEFAULT NULL, 7 | `duration` int(4) DEFAULT NULL, 8 | `scenarioDescription` text, 9 | PRIMARY KEY (`id`) 10 | ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; 11 | 12 | CREATE TABLE `scriptfileinfo` ( 13 | `id` int(10) NOT NULL AUTO_INCREMENT, 14 | `scriptFileName` varchar(256) DEFAULT NULL, 15 | `scriptFilePath` varchar(512) DEFAULT NULL, 16 | `uploadTime` datetime DEFAULT NULL, 17 | `scenarioId` int(8) DEFAULT NULL, 18 | PRIMARY KEY (`id`) 19 | ) ENGINE=InnoDB AUTO_INCREMENT=2 DEFAULT CHARSET=utf8; 20 | 21 | CREATE TABLE `paramfileinfo` ( 22 | `id` int(10) NOT NULL AUTO_INCREMENT, 23 | `paramFileName` varchar(256) DEFAULT NULL, 24 | `paramFilePath` varchar(512) DEFAULT NULL, 25 | `uploadTime` datetime DEFAULT NULL, 26 | `scenarioId` int(8) DEFAULT NULL, 27 | PRIMARY KEY (`id`) 28 | ) ENGINE=InnoDB AUTO_INCREMENT=4 DEFAULT CHARSET=utf8; 29 | 30 | CREATE TABLE `scenarioresultinfo` ( 31 | `id` int(8) NOT NULL AUTO_INCREMENT, 32 | `scenarioName` varchar(256) DEFAULT NULL, 33 | `numThreads` int(4) DEFAULT NULL, 34 | `rampUp` int(4) DEFAULT NULL, 35 | `duration` int(4) DEFAULT NULL, 36 | `runTime` datetime DEFAULT NULL, 37 | `scenarioId` int(8) DEFAULT NULL, 38 | PRIMARY KEY (`id`) 39 | ) ENGINE=InnoDB AUTO_INCREMENT=22 DEFAULT CHARSET=utf8; 40 | 41 | CREATE TABLE `sampleresultinfo` ( 42 | `id` int(11) NOT NULL AUTO_INCREMENT, 43 | `timeStamp` timestamp(3) NOT NULL DEFAULT CURRENT_TIMESTAMP(3) ON UPDATE CURRENT_TIMESTAMP(3), 44 | `samplerLabel` varchar(64) DEFAULT NULL, 45 | `meanTime` int(16) DEFAULT NULL, 46 | `minTime` int(16) DEFAULT NULL, 47 | `maxTime` int(16) DEFAULT NULL, 48 | `standardDeviation` double(8,2) DEFAULT NULL, 49 | `errorPercentage` int(3) DEFAULT NULL, 50 | `requestRate` double(8,2) DEFAULT NULL, 51 | `receiveKBPerSecond` double(12,2) DEFAULT NULL, 52 | `sentKBPerSecond` double(12,2) DEFAULT NULL, 53 | `avgPageBytes` double(16,2) DEFAULT NULL, 54 | `threadCount` int(4) DEFAULT NULL, 55 | `resultId` int(8) DEFAULT NULL, 56 | PRIMARY KEY (`id`) 57 | ) ENGINE=InnoDB AUTO_INCREMENT=18582 DEFAULT CHARSET=utf8; 58 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # Berserker 2 | 使用SpringBoot封装的Jmeter.性能测试平台化. 3 | -------------------------------------------------------------------------------- /build.gradle: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext { 3 | springBootVersion = '1.5.11.RELEASE' 4 | } 5 | repositories { 6 | mavenCentral() 7 | } 8 | dependencies { 9 | classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") 10 | } 11 | } 12 | 13 | apply plugin: 'java' 14 | apply plugin: 'eclipse-wtp' 15 | apply plugin: 'org.springframework.boot' 16 | apply plugin: 'war' 17 | 18 | group = 'com.berserker' 19 | version = '0.0.1-SNAPSHOT' 20 | sourceCompatibility = 1.8 21 | 22 | repositories { 23 | mavenCentral() 24 | } 25 | 26 | configurations { 27 | providedRuntime 28 | all*.exclude group: 'ch.qos.logback', module: 'logback-classic' 29 | } 30 | 31 | bootRun { 32 | // addResources = true 33 | } 34 | 35 | dependencies { 36 | compile('org.springframework.boot:spring-boot-starter-web:1.5.11.RELEASE') 37 | compile('org.springframework.boot:spring-boot-starter-aop:1.5.11.RELEASE') 38 | compile('org.springframework.boot:spring-boot-starter-websocket:1.5.11.RELEASE') 39 | compile('org.springframework.boot:spring-boot-starter-data-redis:1.5.11.RELEASE') 40 | compile('org.springframework.boot:spring-boot-gradle-plugin:1.5.11.RELEASE') 41 | compile('org.springframework.boot:spring-boot-devtools:1.5.11.RELEASE') 42 | compile('org.springframework.boot:spring-boot-starter-logging:1.5.11.RELEASE') 43 | 44 | compile('org.thymeleaf:thymeleaf:3.0.9.RELEASE') 45 | compile('org.thymeleaf:thymeleaf-spring4:3.0.9.RELEASE') 46 | compile('nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect:2.2.2') 47 | 48 | runtime('mysql:mysql-connector-java:6.0.6') 49 | compile('com.alibaba:druid:1.1.6') 50 | compile('org.mybatis.spring.boot:mybatis-spring-boot-starter:1.3.1') 51 | compile('com.github.pagehelper:pagehelper-spring-boot-starter:1.2.3') 52 | 53 | compile('org.apache.commons:commons-lang3:3.7') 54 | compile('commons-io:commons-io:2.6') 55 | compile('commons-beanutils:commons-beanutils:1.9.3') 56 | compile('com.google.guava:guava:23.0') 57 | compile('com.alibaba:fastjson:1.2.46') 58 | 59 | // Jmeter Base Component 60 | compile('org.apache.jmeter:ApacheJMeter_core:4.0') 61 | compile('org.apache.jmeter:ApacheJMeter_java:4.0') 62 | compile('org.apache.jmeter:ApacheJMeter_http:4.0') 63 | compile('org.apache.jmeter:ApacheJMeter_components:4.0') 64 | compile('org.apache.jmeter:ApacheJMeter_config:4.0') 65 | compile('org.apache.jmeter:jorphan:4.0') 66 | // Jmeter Plugin 67 | compile('kg.apc:jmeter-plugins-graphs-basic:2.0') // Jmeter图形显示-base 68 | compile('kg.apc:jmeter-plugins-graphs-additional:2.0') // Jmeter图形显示-additional 69 | compile('kg.apc:jmeter-plugins-casutg:2.5') // Custom Thread Groups 70 | compile('kg.apc:jmeter-plugins-httpraw:0.1') // Jmeter自定义HTTP请求 71 | compile('kg.apc:jmeter-plugins-fifo:0.2') // 多个线程组之间交互数据 72 | compile('kg.apc:jmeter-plugins-redis:0.2') // Jmeter Redis支持 73 | compile('kg.apc:jmeter-plugins-tst:2.3') // 基于处理能力Jmeter插件 74 | compile('net.luminis.jmeter:jmeter-websocket-samplers:1.1.1') // Jmeter对WebSocket的支持 75 | 76 | testCompile('org.hamcrest:hamcrest-all:1.3') 77 | testCompile('org.mockito:mockito-all:1.10.19') 78 | testCompile('org.springframework.boot:spring-boot-starter-test:1.5.11.RELEASE') 79 | 80 | // 部署至Tomcat时需要打开此注释 81 | // providedRuntime('org.springframework.boot:spring-boot-starter-tomcat:1.5.11.RELEASE') 82 | } 83 | -------------------------------------------------------------------------------- /build.gradle.prod: -------------------------------------------------------------------------------- 1 | buildscript { 2 | ext { 3 | springBootVersion = '1.5.11.RELEASE' 4 | } 5 | repositories { 6 | mavenCentral() 7 | } 8 | dependencies { 9 | classpath("org.springframework.boot:spring-boot-gradle-plugin:${springBootVersion}") 10 | } 11 | } 12 | 13 | apply plugin: 'java' 14 | apply plugin: 'eclipse-wtp' 15 | apply plugin: 'org.springframework.boot' 16 | apply plugin: 'war' 17 | 18 | group = 'com.bushmaster' 19 | version = '0.0.1-SNAPSHOT' 20 | sourceCompatibility = 1.8 21 | 22 | repositories { 23 | mavenCentral() 24 | } 25 | 26 | configurations { 27 | providedRuntime 28 | all*.exclude group: 'ch.qos.logback', module: 'logback-classic' 29 | } 30 | 31 | bootRun { 32 | // addResources = true 33 | } 34 | 35 | dependencies { 36 | compile('org.springframework.boot:spring-boot-starter-web:1.5.11.RELEASE') 37 | compile('org.springframework.boot:spring-boot-starter-aop:1.5.11.RELEASE') 38 | compile('org.springframework.boot:spring-boot-starter-websocket:1.5.11.RELEASE') 39 | compile('org.springframework.boot:spring-boot-starter-data-redis:1.5.11.RELEASE') 40 | compile('org.springframework.boot:spring-boot-gradle-plugin:1.5.11.RELEASE') 41 | compile('org.springframework.boot:spring-boot-devtools:1.5.11.RELEASE') 42 | compile('org.springframework.boot:spring-boot-starter-logging:1.5.11.RELEASE') 43 | 44 | compile('org.thymeleaf:thymeleaf:3.0.9.RELEASE') 45 | compile('org.thymeleaf:thymeleaf-spring4:3.0.9.RELEASE') 46 | compile('nz.net.ultraq.thymeleaf:thymeleaf-layout-dialect:2.2.2') 47 | 48 | runtime('mysql:mysql-connector-java:6.0.6') 49 | compile('com.alibaba:druid:1.1.6') 50 | compile('org.mybatis.spring.boot:mybatis-spring-boot-starter:1.3.1') 51 | compile('com.github.pagehelper:pagehelper-spring-boot-starter:1.2.3') 52 | 53 | compile('org.apache.commons:commons-lang3:3.7') 54 | compile('commons-io:commons-io:2.6') 55 | compile('commons-beanutils:commons-beanutils:1.9.3') 56 | compile('com.google.guava:guava:23.0') 57 | compile('com.alibaba:fastjson:1.2.46') 58 | 59 | // Jmeter Base Component 60 | compile('org.apache.jmeter:ApacheJMeter_core:4.0') 61 | compile('org.apache.jmeter:ApacheJMeter_java:4.0') 62 | compile('org.apache.jmeter:ApacheJMeter_http:4.0') 63 | compile('org.apache.jmeter:ApacheJMeter_components:4.0') 64 | compile('org.apache.jmeter:ApacheJMeter_config:4.0') 65 | compile('org.apache.jmeter:jorphan:4.0') 66 | // Jmeter Plugin 67 | compile('kg.apc:jmeter-plugins-graphs-basic:2.0') // Jmeter图形显示-base 68 | compile('kg.apc:jmeter-plugins-graphs-additional:2.0') // Jmeter图形显示-additional 69 | compile('kg.apc:jmeter-plugins-casutg:2.5') // Custom Thread Groups 70 | compile('kg.apc:jmeter-plugins-httpraw:0.1') // Jmeter自定义HTTP请求 71 | compile('kg.apc:jmeter-plugins-fifo:0.2') // 多个线程组之间交互数据 72 | compile('kg.apc:jmeter-plugins-redis:0.2') // Jmeter Redis支持 73 | compile('kg.apc:jmeter-plugins-tst:2.3') // 基于处理能力Jmeter插件 74 | compile('net.luminis.jmeter:jmeter-websocket-samplers:1.1.1') // Jmeter对WebSocket的支持 75 | 76 | testCompile('org.hamcrest:hamcrest-all:1.3') 77 | testCompile('org.mockito:mockito-all:1.10.19') 78 | testCompile('org.springframework.boot:spring-boot-starter-test:1.5.11.RELEASE') 79 | 80 | // 部署至Tomcat时需要打开此注释 81 | providedRuntime('org.springframework.boot:spring-boot-starter-tomcat:1.5.11.RELEASE') 82 | } 83 | -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.jar: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Manchester117/Berserker/a71f06b37f231d0ef8d2bb73800131abea1024b7/gradle/wrapper/gradle-wrapper.jar -------------------------------------------------------------------------------- /gradle/wrapper/gradle-wrapper.properties: -------------------------------------------------------------------------------- 1 | #Mon Mar 05 12:27:34 CST 2018 2 | distributionBase=GRADLE_USER_HOME 3 | distributionPath=wrapper/dists 4 | zipStoreBase=GRADLE_USER_HOME 5 | zipStorePath=wrapper/dists 6 | distributionUrl=https\://services.gradle.org/distributions/gradle-4.5-all.zip 7 | -------------------------------------------------------------------------------- /gradlew: -------------------------------------------------------------------------------- 1 | #!/usr/bin/env sh 2 | 3 | ############################################################################## 4 | ## 5 | ## Gradle start up script for UN*X 6 | ## 7 | ############################################################################## 8 | 9 | # Attempt to set APP_HOME 10 | # Resolve links: $0 may be a link 11 | PRG="$0" 12 | # Need this for relative symlinks. 13 | while [ -h "$PRG" ] ; do 14 | ls=`ls -ld "$PRG"` 15 | link=`expr "$ls" : '.*-> \(.*\)$'` 16 | if expr "$link" : '/.*' > /dev/null; then 17 | PRG="$link" 18 | else 19 | PRG=`dirname "$PRG"`"/$link" 20 | fi 21 | done 22 | SAVED="`pwd`" 23 | cd "`dirname \"$PRG\"`/" >/dev/null 24 | APP_HOME="`pwd -P`" 25 | cd "$SAVED" >/dev/null 26 | 27 | APP_NAME="Gradle" 28 | APP_BASE_NAME=`basename "$0"` 29 | 30 | # Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 31 | DEFAULT_JVM_OPTS="" 32 | 33 | # Use the maximum available, or set MAX_FD != -1 to use that value. 34 | MAX_FD="maximum" 35 | 36 | warn ( ) { 37 | echo "$*" 38 | } 39 | 40 | die ( ) { 41 | echo 42 | echo "$*" 43 | echo 44 | exit 1 45 | } 46 | 47 | # OS specific support (must be 'true' or 'false'). 48 | cygwin=false 49 | msys=false 50 | darwin=false 51 | nonstop=false 52 | case "`uname`" in 53 | CYGWIN* ) 54 | cygwin=true 55 | ;; 56 | Darwin* ) 57 | darwin=true 58 | ;; 59 | MINGW* ) 60 | msys=true 61 | ;; 62 | NONSTOP* ) 63 | nonstop=true 64 | ;; 65 | esac 66 | 67 | CLASSPATH=$APP_HOME/gradle/wrapper/gradle-wrapper.jar 68 | 69 | # Determine the Java command to use to start the JVM. 70 | if [ -n "$JAVA_HOME" ] ; then 71 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then 72 | # IBM's JDK on AIX uses strange locations for the executables 73 | JAVACMD="$JAVA_HOME/jre/sh/java" 74 | else 75 | JAVACMD="$JAVA_HOME/bin/java" 76 | fi 77 | if [ ! -x "$JAVACMD" ] ; then 78 | die "ERROR: JAVA_HOME is set to an invalid directory: $JAVA_HOME 79 | 80 | Please set the JAVA_HOME variable in your environment to match the 81 | location of your Java installation." 82 | fi 83 | else 84 | JAVACMD="java" 85 | which java >/dev/null 2>&1 || die "ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 86 | 87 | Please set the JAVA_HOME variable in your environment to match the 88 | location of your Java installation." 89 | fi 90 | 91 | # Increase the maximum file descriptors if we can. 92 | if [ "$cygwin" = "false" -a "$darwin" = "false" -a "$nonstop" = "false" ] ; then 93 | MAX_FD_LIMIT=`ulimit -H -n` 94 | if [ $? -eq 0 ] ; then 95 | if [ "$MAX_FD" = "maximum" -o "$MAX_FD" = "max" ] ; then 96 | MAX_FD="$MAX_FD_LIMIT" 97 | fi 98 | ulimit -n $MAX_FD 99 | if [ $? -ne 0 ] ; then 100 | warn "Could not set maximum file descriptor limit: $MAX_FD" 101 | fi 102 | else 103 | warn "Could not query maximum file descriptor limit: $MAX_FD_LIMIT" 104 | fi 105 | fi 106 | 107 | # For Darwin, add options to specify how the application appears in the dock 108 | if $darwin; then 109 | GRADLE_OPTS="$GRADLE_OPTS \"-Xdock:name=$APP_NAME\" \"-Xdock:icon=$APP_HOME/media/gradle.icns\"" 110 | fi 111 | 112 | # For Cygwin, switch paths to Windows format before running java 113 | if $cygwin ; then 114 | APP_HOME=`cygpath --path --mixed "$APP_HOME"` 115 | CLASSPATH=`cygpath --path --mixed "$CLASSPATH"` 116 | JAVACMD=`cygpath --unix "$JAVACMD"` 117 | 118 | # We build the pattern for arguments to be converted via cygpath 119 | ROOTDIRSRAW=`find -L / -maxdepth 1 -mindepth 1 -type d 2>/dev/null` 120 | SEP="" 121 | for dir in $ROOTDIRSRAW ; do 122 | ROOTDIRS="$ROOTDIRS$SEP$dir" 123 | SEP="|" 124 | done 125 | OURCYGPATTERN="(^($ROOTDIRS))" 126 | # Add a user-defined pattern to the cygpath arguments 127 | if [ "$GRADLE_CYGPATTERN" != "" ] ; then 128 | OURCYGPATTERN="$OURCYGPATTERN|($GRADLE_CYGPATTERN)" 129 | fi 130 | # Now convert the arguments - kludge to limit ourselves to /bin/sh 131 | i=0 132 | for arg in "$@" ; do 133 | CHECK=`echo "$arg"|egrep -c "$OURCYGPATTERN" -` 134 | CHECK2=`echo "$arg"|egrep -c "^-"` ### Determine if an option 135 | 136 | if [ $CHECK -ne 0 ] && [ $CHECK2 -eq 0 ] ; then ### Added a condition 137 | eval `echo args$i`=`cygpath --path --ignore --mixed "$arg"` 138 | else 139 | eval `echo args$i`="\"$arg\"" 140 | fi 141 | i=$((i+1)) 142 | done 143 | case $i in 144 | (0) set -- ;; 145 | (1) set -- "$args0" ;; 146 | (2) set -- "$args0" "$args1" ;; 147 | (3) set -- "$args0" "$args1" "$args2" ;; 148 | (4) set -- "$args0" "$args1" "$args2" "$args3" ;; 149 | (5) set -- "$args0" "$args1" "$args2" "$args3" "$args4" ;; 150 | (6) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" ;; 151 | (7) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" ;; 152 | (8) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" ;; 153 | (9) set -- "$args0" "$args1" "$args2" "$args3" "$args4" "$args5" "$args6" "$args7" "$args8" ;; 154 | esac 155 | fi 156 | 157 | # Escape application args 158 | save ( ) { 159 | for i do printf %s\\n "$i" | sed "s/'/'\\\\''/g;1s/^/'/;\$s/\$/' \\\\/" ; done 160 | echo " " 161 | } 162 | APP_ARGS=$(save "$@") 163 | 164 | # Collect all arguments for the java command, following the shell quoting and substitution rules 165 | eval set -- $DEFAULT_JVM_OPTS $JAVA_OPTS $GRADLE_OPTS "\"-Dorg.gradle.appname=$APP_BASE_NAME\"" -classpath "\"$CLASSPATH\"" org.gradle.wrapper.GradleWrapperMain "$APP_ARGS" 166 | 167 | # by default we should be in the correct project dir, but when run from Finder on Mac, the cwd is wrong 168 | if [ "$(uname)" = "Darwin" ] && [ "$HOME" = "$PWD" ]; then 169 | cd "$(dirname "$0")" 170 | fi 171 | 172 | exec "$JAVACMD" "$@" 173 | -------------------------------------------------------------------------------- /gradlew.bat: -------------------------------------------------------------------------------- 1 | @if "%DEBUG%" == "" @echo off 2 | @rem ########################################################################## 3 | @rem 4 | @rem Gradle startup script for Windows 5 | @rem 6 | @rem ########################################################################## 7 | 8 | @rem Set local scope for the variables with windows NT shell 9 | if "%OS%"=="Windows_NT" setlocal 10 | 11 | set DIRNAME=%~dp0 12 | if "%DIRNAME%" == "" set DIRNAME=. 13 | set APP_BASE_NAME=%~n0 14 | set APP_HOME=%DIRNAME% 15 | 16 | @rem Add default JVM options here. You can also use JAVA_OPTS and GRADLE_OPTS to pass JVM options to this script. 17 | set DEFAULT_JVM_OPTS= 18 | 19 | @rem Find java.exe 20 | if defined JAVA_HOME goto findJavaFromJavaHome 21 | 22 | set JAVA_EXE=java.exe 23 | %JAVA_EXE% -version >NUL 2>&1 24 | if "%ERRORLEVEL%" == "0" goto init 25 | 26 | echo. 27 | echo ERROR: JAVA_HOME is not set and no 'java' command could be found in your PATH. 28 | echo. 29 | echo Please set the JAVA_HOME variable in your environment to match the 30 | echo location of your Java installation. 31 | 32 | goto fail 33 | 34 | :findJavaFromJavaHome 35 | set JAVA_HOME=%JAVA_HOME:"=% 36 | set JAVA_EXE=%JAVA_HOME%/bin/java.exe 37 | 38 | if exist "%JAVA_EXE%" goto init 39 | 40 | echo. 41 | echo ERROR: JAVA_HOME is set to an invalid directory: %JAVA_HOME% 42 | echo. 43 | echo Please set the JAVA_HOME variable in your environment to match the 44 | echo location of your Java installation. 45 | 46 | goto fail 47 | 48 | :init 49 | @rem Get command-line arguments, handling Windows variants 50 | 51 | if not "%OS%" == "Windows_NT" goto win9xME_args 52 | 53 | :win9xME_args 54 | @rem Slurp the command line arguments. 55 | set CMD_LINE_ARGS= 56 | set _SKIP=2 57 | 58 | :win9xME_args_slurp 59 | if "x%~1" == "x" goto execute 60 | 61 | set CMD_LINE_ARGS=%* 62 | 63 | :execute 64 | @rem Setup the command line 65 | 66 | set CLASSPATH=%APP_HOME%\gradle\wrapper\gradle-wrapper.jar 67 | 68 | @rem Execute Gradle 69 | "%JAVA_EXE%" %DEFAULT_JVM_OPTS% %JAVA_OPTS% %GRADLE_OPTS% "-Dorg.gradle.appname=%APP_BASE_NAME%" -classpath "%CLASSPATH%" org.gradle.wrapper.GradleWrapperMain %CMD_LINE_ARGS% 70 | 71 | :end 72 | @rem End local scope for the variables with windows NT shell 73 | if "%ERRORLEVEL%"=="0" goto mainEnd 74 | 75 | :fail 76 | rem Set variable GRADLE_EXIT_CONSOLE if you need the _script_ return code instead of 77 | rem the _cmd.exe /c_ return code! 78 | if not "" == "%GRADLE_EXIT_CONSOLE%" exit 1 79 | exit /b 1 80 | 81 | :mainEnd 82 | if "%OS%"=="Windows_NT" endlocal 83 | 84 | :omega 85 | -------------------------------------------------------------------------------- /src/main/java/com/berserker/architecture/ArchitectureApplication.java: -------------------------------------------------------------------------------- 1 | package com.berserker.architecture; 2 | 3 | import com.github.pagehelper.PageHelper; 4 | import org.mybatis.spring.annotation.MapperScan; 5 | import org.springframework.boot.SpringApplication; 6 | import org.springframework.boot.autoconfigure.SpringBootApplication; 7 | import org.springframework.boot.autoconfigure.mongo.MongoAutoConfiguration; 8 | import org.springframework.context.annotation.Bean; 9 | import org.springframework.scheduling.annotation.EnableAsync; 10 | import org.springframework.transaction.annotation.EnableTransactionManagement; 11 | import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; 12 | 13 | import java.util.Properties; 14 | 15 | @SpringBootApplication(exclude = MongoAutoConfiguration.class) // SpringBoot启动时不要默认加载MongoDriver 16 | @EnableTransactionManagement // 开启事务管理 17 | @EnableWebSocketMessageBroker // 启用WebSocket 18 | @EnableAsync // 启用异步事务 19 | @MapperScan("com.berserker.architecture.mapper") // 扫描Mapper 20 | public class ArchitectureApplication { 21 | @Bean 22 | public PageHelper pageHelper() { 23 | PageHelper pageHelper = new PageHelper(); 24 | Properties properties = new Properties(); 25 | properties.setProperty("offsetAsPageNum", "true"); 26 | properties.setProperty("rowBoundsWithCount", "true"); 27 | properties.setProperty("reasonable", "true"); 28 | properties.setProperty("dialect", "mysql"); // 使用MySQL的方言 29 | pageHelper.setProperties(properties); 30 | 31 | return pageHelper; 32 | } 33 | 34 | public static void main(String[] args) { 35 | SpringApplication.run(ArchitectureApplication.class, args); 36 | } 37 | 38 | // @Override 39 | // public void run(String... args) throws Exception { 40 | // System.out.println(this.mapper.getScenarioInfoList("个", 1).size()); 41 | 42 | // ScenarioInfo info_1 = new ScenarioInfo(); 43 | // info_1.setId(1); 44 | // info_1.setScenarioDescription("第一个场景描述"); 45 | // info_1.setRampUpSeconds(30); 46 | // info_1.setDuration(1200); 47 | // info_1.setStatus(true); 48 | // System.out.println(this.mapper.modifyScenarioInfo(info_1)); 49 | 50 | // ScenarioInfo info_2 = new ScenarioInfo(); 51 | // info_2.setScenarioName("第四个场景"); 52 | // info_2.setScenarioDescription("第四个场景描述"); 53 | // info_2.setCreateTime(new Date()); 54 | // info_2.setConcurrentNum(20); 55 | // info_2.setRampUpSeconds(20); 56 | // info_2.setDuration(1800); 57 | // info_2.setStatus(Boolean.TRUE); 58 | // System.out.println(this.mapper.insertScenarioInfo(info_2)); 59 | 60 | // ScriptFileInfo scriptInfo = new ScriptFileInfo(); 61 | // scriptInfo.setScriptFileName("第一个脚本"); 62 | // scriptInfo.setScriptFilePath("D:\\Script\\第一个脚本"); 63 | // scriptInfo.setUploadTime(new Date()); 64 | // scriptInfo.setScenarioId(1); 65 | // 66 | // scriptMapper.insertScriptFileInfo(scriptInfo); 67 | 68 | // ParamFileInfo paramInfo = new ParamFileInfo(); 69 | // paramInfo.setParamFileName("第一个参数化文件"); 70 | // paramInfo.setParamFilePath("D:\\Param\\第一个参数化文件"); 71 | // paramInfo.setUploadTime(new Date()); 72 | // paramInfo.setScenarioId(1); 73 | // 74 | // paramMapper.insertParamFileInfo(paramInfo); 75 | // } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/com/berserker/architecture/RedisConfig.java: -------------------------------------------------------------------------------- 1 | package com.berserker.architecture; 2 | 3 | import com.berserker.architecture.domain.entity.SampleResultInfo; 4 | import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean; 5 | import org.springframework.cache.CacheManager; 6 | import org.springframework.cache.annotation.EnableCaching; 7 | import org.springframework.context.annotation.Bean; 8 | import org.springframework.context.annotation.Configuration; 9 | import org.springframework.data.redis.cache.RedisCacheManager; 10 | import org.springframework.data.redis.connection.RedisConnectionFactory; 11 | import org.springframework.data.redis.connection.jedis.JedisConnectionFactory; 12 | import org.springframework.data.redis.core.RedisTemplate; 13 | import org.springframework.data.redis.serializer.StringRedisSerializer; 14 | 15 | @Configuration 16 | @EnableCaching 17 | public class RedisConfig { 18 | @Bean 19 | public CacheManager cacheManager(RedisTemplate redisTemplate) { 20 | return new RedisCacheManager(redisTemplate); 21 | } 22 | 23 | @Bean 24 | JedisConnectionFactory jedisConnectionFactory() { 25 | return new JedisConnectionFactory(); 26 | } 27 | 28 | @Bean 29 | @ConditionalOnMissingBean(name = {"redisTemplate"}) 30 | public RedisTemplate redisTemplate(RedisConnectionFactory connectionFactory) { 31 | RedisTemplate template = new RedisTemplate<>(); 32 | template.setConnectionFactory(connectionFactory); 33 | template.setKeySerializer(new StringRedisSerializer()); 34 | // template.setValueSerializer(new RedisSampleSerializer()); 35 | return template; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/berserker/architecture/ServletInitializer.java: -------------------------------------------------------------------------------- 1 | package com.berserker.architecture; 2 | 3 | import org.springframework.boot.builder.SpringApplicationBuilder; 4 | import org.springframework.boot.web.support.SpringBootServletInitializer; 5 | 6 | public class ServletInitializer extends SpringBootServletInitializer { 7 | 8 | @Override 9 | protected SpringApplicationBuilder configure(SpringApplicationBuilder application) { 10 | return application.sources(ArchitectureApplication.class); 11 | } 12 | 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/berserker/architecture/WebAppConfigurerAdapter.java: -------------------------------------------------------------------------------- 1 | package com.berserker.architecture; 2 | 3 | import com.berserker.architecture.interceptor.PreventRepeatSubmitInterceptor; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.web.servlet.config.annotation.InterceptorRegistry; 6 | import org.springframework.web.servlet.config.annotation.WebMvcConfigurerAdapter; 7 | 8 | @Configuration 9 | public class WebAppConfigurerAdapter extends WebMvcConfigurerAdapter { 10 | /** 11 | * @description 添加拦截器(针对于添加场景和修改场景) 12 | * @param registry 13 | */ 14 | @Override 15 | public void addInterceptors(InterceptorRegistry registry) { 16 | registry.addInterceptor(new PreventRepeatSubmitInterceptor()).addPathPatterns("/addScenarioInfo"); 17 | registry.addInterceptor(new PreventRepeatSubmitInterceptor()).addPathPatterns("/modScenarioInfo"); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/berserker/architecture/WebSocketConfig.java: -------------------------------------------------------------------------------- 1 | package com.berserker.architecture; 2 | 3 | import org.springframework.context.annotation.Configuration; 4 | import org.springframework.messaging.simp.config.MessageBrokerRegistry; 5 | import org.springframework.web.socket.config.annotation.AbstractWebSocketMessageBrokerConfigurer; 6 | import org.springframework.web.socket.config.annotation.EnableWebSocketMessageBroker; 7 | import org.springframework.web.socket.config.annotation.StompEndpointRegistry; 8 | 9 | @Configuration 10 | @EnableWebSocketMessageBroker 11 | public class WebSocketConfig extends AbstractWebSocketMessageBrokerConfigurer { 12 | @Override 13 | public void registerStompEndpoints(StompEndpointRegistry registry) { 14 | // 添加一个Socket端点,客户端可以通过这个端点进行连接 15 | registry.addEndpoint("/socket").withSockJS(); 16 | } 17 | 18 | @Override 19 | public void configureMessageBroker(MessageBrokerRegistry registry) { 20 | // client接收server信息的地址前缀 21 | registry.enableSimpleBroker("/sampleResult"); 22 | // client给server发消息的地址前缀 23 | registry.setApplicationDestinationPrefixes("/app"); 24 | } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/java/com/berserker/architecture/controller/ScenarioResultController.java: -------------------------------------------------------------------------------- 1 | package com.berserker.architecture.controller; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.alibaba.fastjson.JSONObject; 5 | import com.berserker.architecture.domain.param.ScenarioResultByIdParams; 6 | import com.berserker.architecture.domain.param.ScenarioResultListParams; 7 | import com.berserker.architecture.utils.CommonUtil; 8 | import com.berserker.architecture.service.ScenarioResultService; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.stereotype.Controller; 11 | import org.springframework.ui.Model; 12 | import org.springframework.validation.BindingResult; 13 | import org.springframework.validation.annotation.Validated; 14 | import org.springframework.web.bind.annotation.*; 15 | 16 | import java.util.List; 17 | import java.util.Map; 18 | import java.util.Objects; 19 | 20 | @Controller 21 | public class ScenarioResultController { 22 | @Autowired 23 | private ScenarioResultService resultService; 24 | 25 | @GetMapping(path = "/scenarioNewResultList") 26 | public String scenarioNewResultList() { 27 | return "scenarioNewResultList"; 28 | } 29 | 30 | @PostMapping(path = "/getScenarioNewResultList") 31 | public @ResponseBody JSONObject getScenarioNewResultList(@Validated @RequestBody ScenarioResultListParams requestParams, BindingResult bindingResult) { 32 | // 参数验证 33 | Map> errorResultInfo = CommonUtil.paramsValidator(bindingResult); 34 | if (Objects.nonNull(errorResultInfo)) 35 | return JSONObject.parseObject(JSON.toJSONString(errorResultInfo)); 36 | 37 | Integer offset = Integer.parseInt(requestParams.getOffset()); 38 | Integer limit = Integer.parseInt(requestParams.getLimit()); 39 | 40 | Map result = resultService.getScenarioResultInfoList(offset, limit); 41 | return JSONObject.parseObject(JSON.toJSONString(result)); 42 | } 43 | 44 | @GetMapping(path = "/scenarioResultListById") 45 | public String scenarioResultListById(Model model, @RequestParam("scenarioId") Integer scenarioId) { 46 | model.addAttribute("scenarioId", scenarioId); 47 | return "scenarioResultListById"; 48 | } 49 | 50 | @PostMapping(path = "/getScenarioResultListByScenarioId") 51 | public @ResponseBody JSONObject getScenarioResultListByScenarioId(@Validated @RequestBody ScenarioResultByIdParams requestParams, BindingResult bindingResult) { 52 | // 参数验证 53 | Map> errorResultInfo = CommonUtil.paramsValidator(bindingResult); 54 | if (Objects.nonNull(errorResultInfo)) 55 | return JSONObject.parseObject(JSON.toJSONString(errorResultInfo)); 56 | 57 | Integer offset = Integer.parseInt(requestParams.getOffset()); 58 | Integer limit = Integer.parseInt(requestParams.getLimit()); 59 | Integer scenarioId = Integer.parseInt(requestParams.getScenarioId()); 60 | 61 | Map result = resultService.getScenarioResultInfoByScenarioId(offset, limit, scenarioId); 62 | return JSONObject.parseObject(JSON.toJSONString(result)); 63 | } 64 | 65 | @PostMapping(path = "/delScenarioResult") 66 | public @ResponseBody JSONObject delScenarioResult(@RequestParam("resultId") Integer resultId) { 67 | Map result = resultService.delScenarioResultInfoByResultId(resultId); 68 | return JSONObject.parseObject(JSON.toJSONString(result)); 69 | } 70 | } -------------------------------------------------------------------------------- /src/main/java/com/berserker/architecture/controller/ScenarioRunController.java: -------------------------------------------------------------------------------- 1 | package com.berserker.architecture.controller; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.alibaba.fastjson.JSONObject; 5 | import com.berserker.architecture.domain.entity.ScenarioInfo; 6 | import com.berserker.architecture.service.ScenarioInfoService; 7 | import com.berserker.architecture.service.ScenarioRunService; 8 | import org.apache.commons.lang3.StringUtils; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.stereotype.Controller; 11 | import org.springframework.ui.Model; 12 | import org.springframework.web.bind.annotation.GetMapping; 13 | import org.springframework.web.bind.annotation.PostMapping; 14 | import org.springframework.web.bind.annotation.RequestParam; 15 | import org.springframework.web.bind.annotation.ResponseBody; 16 | 17 | import java.util.*; 18 | 19 | @Controller 20 | public class ScenarioRunController { 21 | @Autowired 22 | private ScenarioRunService runService; 23 | @Autowired 24 | private ScenarioInfoService infoService; 25 | 26 | @PostMapping(path = "/scenarioStartRunCheck") 27 | public @ResponseBody JSONObject scenarioStartRunCheck(@RequestParam("scenarioId") String scenarioId) { 28 | Map startRunInfo = new HashMap<>(); 29 | ScenarioInfo scenarioInfo = infoService.getScenarioInfo(Integer.parseInt(scenarioId)); 30 | // 如果并发数量|攀升时间|运行时间为空则提示用于先设定这些参数 31 | List message = new ArrayList<>(); 32 | if (Objects.isNull(scenarioInfo.getNumThreads())) { 33 | startRunInfo.put("enableRun", "False"); 34 | message.add("请先设置并发数量"); 35 | } 36 | if (Objects.isNull(scenarioInfo.getRampUp())) { 37 | startRunInfo.put("enableRun", "False"); 38 | message.add("请先设置攀升时间"); 39 | } 40 | if (Objects.isNull(scenarioInfo.getDuration())) { 41 | startRunInfo.put("enableRun", "False"); 42 | message.add("请先设置运行时长"); 43 | } 44 | // 如果有场景正在运行 45 | Boolean engineIsActive = runService.getEngineIsActive(); 46 | if (engineIsActive) { 47 | // 如果场景正在运行,则返回正在运行的场景名称 48 | String runningScenarioName = runService.getRunningScenarioName(); 49 | startRunInfo.put("enableRun", "False"); 50 | message.add(StringUtils.join("场景[", runningScenarioName, "]正在运行,请等待测试完成后再试")); 51 | } 52 | 53 | // 如果填写项正确填写,并且没有场景运行,则返回可以运行测试 54 | if (Objects.equals(startRunInfo.get("enableRun"), "False")) { 55 | startRunInfo.put("message", message); 56 | } else { 57 | // 如果场景没有运行,则返回要运行场景的ID 58 | startRunInfo.put("isActive", "True"); 59 | startRunInfo.put("scenarioId", scenarioId); 60 | } 61 | 62 | return JSONObject.parseObject(JSON.toJSONString(startRunInfo)); 63 | } 64 | 65 | @GetMapping(path = "/scenarioStartRun") 66 | public String scenarioStartRun(Model model, @RequestParam("scenarioId") String scenarioId) { 67 | // 启动场景 68 | runService.scenarioStartRun(Integer.parseInt(scenarioId)); 69 | model.addAttribute("scenarioId", scenarioId); 70 | return "scenarioRealTimeChartGrid"; 71 | } 72 | 73 | @GetMapping(path = "/scenarioStopRun") 74 | public @ResponseBody JSONObject scenarioStopRun() { 75 | Map stopRunMessage = runService.scenarioStopRun(); 76 | return JSONObject.parseObject(JSON.toJSONString(stopRunMessage)); 77 | } 78 | 79 | @GetMapping(path = "/scenarioRunningCheck") 80 | public String scenarioRunningCheck(Model model) { 81 | Boolean engineIsActive = runService.getEngineIsActive(); 82 | if (engineIsActive) { 83 | Integer scenarioId = runService.getRunningScenarioId(); 84 | model.addAttribute("scenarioId", scenarioId); 85 | return "scenarioRealTimeChartGrid"; 86 | } else { 87 | model.addAttribute("isActive", "False"); 88 | return "scenarioList"; 89 | } 90 | } 91 | 92 | @GetMapping(path = "/scenarioRealTimeChartDetail") 93 | public String scenarioRealTimeChartDetail(Model model, @RequestParam("scenarioId") String scenarioId, @RequestParam("dataType") String dataType) { 94 | runService.scenarioSampleResultRealOuter(); 95 | model.addAttribute("scenarioId", scenarioId); 96 | model.addAttribute("dataType", dataType); 97 | 98 | if (Objects.equals(dataType, "meanTime")) { 99 | model.addAttribute("chartTitle", "平均响应时间-运行状态"); 100 | model.addAttribute("unit", "毫秒"); 101 | // return "scenarioResponseTimeChart"; 102 | } 103 | if (Objects.equals(dataType, "requestRate")) { 104 | model.addAttribute("chartTitle", "每秒请求处理能力-运行状态"); 105 | model.addAttribute("unit", "个"); 106 | // return "scenarioRequestRateChart"; 107 | } 108 | if (Objects.equals(dataType, "errorPercentage")) { 109 | model.addAttribute("chartTitle", "错误百分比-运行状态"); 110 | model.addAttribute("unit", "百分比"); 111 | // return "scenarioErrorByPercentChart"; 112 | } 113 | if (Objects.equals(dataType, "threadCount")) { 114 | model.addAttribute("chartTitle", "并发数量趋势-运行状态"); 115 | model.addAttribute("unit", "个"); 116 | // return "scenarioThreadCountChart"; 117 | } 118 | if (Objects.equals(dataType, "sentKBPerSecond")) { 119 | model.addAttribute("chartTitle", "发送数据量趋势-运行状态"); 120 | model.addAttribute("unit", "KB"); 121 | // return "scenarioSendKBPerSecChart"; 122 | } 123 | if (Objects.equals(dataType, "receiveKBPerSecond")) { 124 | model.addAttribute("chartTitle", "接收数据量趋势-运行状态"); 125 | model.addAttribute("unit", "KB"); 126 | // return "scenarioReceiveKBPerSecChart"; 127 | } 128 | return "scenarioRealTimeChart"; 129 | } 130 | } 131 | -------------------------------------------------------------------------------- /src/main/java/com/berserker/architecture/controller/ScenarioSampleController.java: -------------------------------------------------------------------------------- 1 | package com.berserker.architecture.controller; 2 | 3 | import com.berserker.architecture.domain.entity.SampleResultInfo; 4 | import com.berserker.architecture.service.SampleResultService; 5 | import com.google.common.base.Objects; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.stereotype.Controller; 8 | import org.springframework.ui.Model; 9 | import org.springframework.web.bind.annotation.GetMapping; 10 | import org.springframework.web.bind.annotation.PostMapping; 11 | import org.springframework.web.bind.annotation.RequestParam; 12 | import org.springframework.web.bind.annotation.ResponseBody; 13 | 14 | import java.util.List; 15 | import java.util.Map; 16 | 17 | @Controller 18 | public class ScenarioSampleController { 19 | @Autowired 20 | private SampleResultService resultService; 21 | 22 | @GetMapping(path = "/scenarioSampleDetailChartGrid") 23 | public String scenarioSampleDetailChartGrid(Model model, @RequestParam("resultId") Integer resultId) { 24 | List resultDataList = resultService.getSampleResultData(resultId); 25 | model.addAttribute("resultDataList", resultDataList); 26 | model.addAttribute("resultId", resultId); 27 | return "scenarioSampleDetailChartGrid"; 28 | } 29 | 30 | @GetMapping(path = "/scenarioSampleResultChart") 31 | public String scenarioSampleResultChart(Model model, @RequestParam("resultId") Integer resultId, @RequestParam("dataType") String dataType) { 32 | model.addAttribute("resultId", resultId); 33 | model.addAttribute("dataType", dataType); 34 | if (Objects.equal(dataType, "meanTime")) { 35 | model.addAttribute("chartTitle", "平均响应时间趋势"); 36 | model.addAttribute("unit", "毫秒"); 37 | } 38 | if (Objects.equal(dataType, "requestRate")) { 39 | model.addAttribute("chartTitle", "请求处理能力"); 40 | model.addAttribute("unit", "个"); 41 | } 42 | if (Objects.equal(dataType, "errorPercentage")) { 43 | model.addAttribute("chartTitle", "请求错误率"); 44 | model.addAttribute("unit", "百分比"); 45 | } 46 | if (Objects.equal(dataType, "threadCount")) { 47 | model.addAttribute("chartTitle", "并发数量趋势"); 48 | model.addAttribute("unit", "个"); 49 | } 50 | if (Objects.equal(dataType, "sentKBPerSecond")) { 51 | model.addAttribute("chartTitle", "发送数据量趋势"); 52 | model.addAttribute("unit", "KB"); 53 | } 54 | if (Objects.equal(dataType, "receiveKBPerSecond")) { 55 | model.addAttribute("chartTitle", "接收数据量趋势"); 56 | model.addAttribute("unit", "KB"); 57 | } 58 | return "scenarioSampleDetailChart"; 59 | } 60 | 61 | // Springboot 1.5.X 直接返回JSON时解析会报错.避免此问题需要直接按照Java的数据结构返回. 62 | // @GetMapping(path = "/getSampleResultDetailData") 63 | // public @ResponseBody JSONObject getSampleResultDetailData(@RequestParam("resultId") Integer resultId, 64 | // @RequestParam("dataType") String dataType) { 65 | // Map>> sampleResultContainer = resultService.getSampleResultDataListByResultId(resultId, dataType); 66 | // return JSONObject.parseObject(JSON.toJSONString(sampleResultContainer)); 67 | // } 68 | 69 | // 修改后的方法,直接使用Java的数据结构返回. 70 | @PostMapping(path = "/getSampleResultDetailData") 71 | public @ResponseBody Map>> getSampleResultDetailData(@RequestParam("resultId") Integer resultId, 72 | @RequestParam("dataType") String dataType) { 73 | // 如果给HighCharts传时间,需要使用Long类型. 74 | return resultService.getSampleResultDataListByResultId(resultId, dataType); 75 | } 76 | } 77 | -------------------------------------------------------------------------------- /src/main/java/com/berserker/architecture/domain/entity/ParamFileInfo.java: -------------------------------------------------------------------------------- 1 | package com.berserker.architecture.domain.entity; 2 | 3 | import java.util.Date; 4 | 5 | public class ParamFileInfo { 6 | private Integer id; 7 | private String paramFileName; 8 | private String paramFilePath; 9 | private Date uploadTime; 10 | private Integer scenarioId; 11 | 12 | public Integer getId() { 13 | return id; 14 | } 15 | 16 | public void setId(Integer id) { 17 | this.id = id; 18 | } 19 | 20 | public String getParamFileName() { 21 | return paramFileName; 22 | } 23 | 24 | public void setParamFileName(String paramFileName) { 25 | this.paramFileName = paramFileName; 26 | } 27 | 28 | public String getParamFilePath() { 29 | return paramFilePath; 30 | } 31 | 32 | public void setParamFilePath(String paramFilePath) { 33 | this.paramFilePath = paramFilePath; 34 | } 35 | 36 | public Date getUploadTime() { 37 | return uploadTime; 38 | } 39 | 40 | public void setUploadTime(Date uploadTime) { 41 | this.uploadTime = uploadTime; 42 | } 43 | 44 | public Integer getScenarioId() { 45 | return scenarioId; 46 | } 47 | 48 | public void setScenarioId(Integer scenarioId) { 49 | this.scenarioId = scenarioId; 50 | } 51 | 52 | @Override 53 | public String toString() { 54 | return "ParamFileInfo{" + 55 | "id=" + id + 56 | ", paramFileName='" + paramFileName + '\'' + 57 | ", paramFilePath='" + paramFilePath + '\'' + 58 | ", uploadTime=" + uploadTime + 59 | ", scenarioId=" + scenarioId + 60 | '}'; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/com/berserker/architecture/domain/entity/SampleResultInfo.java: -------------------------------------------------------------------------------- 1 | package com.berserker.architecture.domain.entity; 2 | 3 | import java.io.Serializable; 4 | import java.math.BigDecimal; 5 | import java.sql.Timestamp; 6 | 7 | public class SampleResultInfo implements Serializable { 8 | private Timestamp timeStamp; 9 | private String samplerLabel; 10 | private Long samplerCount; 11 | private Double meanTime; 12 | private Long minTime; 13 | private Long maxTime; 14 | private Double standardDeviation; 15 | private Double errorPercentage; 16 | private Double requestRate; 17 | private Double receiveKBPerSecond; 18 | private Double sentKBPerSecond; 19 | private Double avgPageBytes; 20 | private Integer threadCount; 21 | private Integer resultId; 22 | 23 | public Timestamp getTimeStamp() { 24 | return timeStamp; 25 | } 26 | 27 | public void setTimeStamp(Timestamp timeStamp) { 28 | this.timeStamp = timeStamp; 29 | } 30 | 31 | public String getSamplerLabel() { 32 | return samplerLabel; 33 | } 34 | 35 | public void setSamplerLabel(String samplerLabel) { 36 | this.samplerLabel = samplerLabel; 37 | } 38 | 39 | public Long getSamplerCount() { 40 | return samplerCount; 41 | } 42 | 43 | public void setSamplerCount(Long samplerCount) { 44 | this.samplerCount = samplerCount; 45 | } 46 | 47 | public Double getMeanTime() { 48 | return meanTime; 49 | } 50 | 51 | public void setMeanTime(Double meanTime) { 52 | if (Double.isFinite(meanTime)) { 53 | BigDecimal bigDecimal = new BigDecimal(meanTime.toString()); 54 | this.meanTime = bigDecimal.setScale(2, BigDecimal.ROUND_HALF_UP).doubleValue(); 55 | } else { 56 | this.meanTime = 0.0; 57 | } 58 | } 59 | 60 | public Long getMinTime() { 61 | return minTime; 62 | } 63 | 64 | public void setMinTime(Long minTime) { 65 | this.minTime = minTime; 66 | } 67 | 68 | public Long getMaxTime() { 69 | return maxTime; 70 | } 71 | 72 | public void setMaxTime(Long maxTime) { 73 | this.maxTime = maxTime; 74 | } 75 | 76 | public Double getStandardDeviation() { 77 | return standardDeviation; 78 | } 79 | 80 | public void setStandardDeviation(Double standardDeviation) { 81 | if (Double.isFinite(standardDeviation)) { 82 | BigDecimal bigDecimal = new BigDecimal(standardDeviation.toString()); 83 | this.standardDeviation = bigDecimal.setScale(2, BigDecimal.ROUND_HALF_UP).doubleValue(); // 保留2位小数 84 | } else { 85 | this.standardDeviation = 0.0; 86 | } 87 | } 88 | 89 | public Double getErrorPercentage() { 90 | return errorPercentage; 91 | } 92 | 93 | public void setErrorPercentage(Double errorPercentage) { 94 | this.errorPercentage = errorPercentage; 95 | } 96 | 97 | public Double getRequestRate() { 98 | return requestRate; 99 | } 100 | 101 | public void setRequestRate(Double requestRate) { 102 | if (Double.isFinite(requestRate)) { // 这里使用了isFinite方法,判断是有效数据,避免计数器返回无限大的数据,导致数据解析失败. 103 | BigDecimal bigDecimal = new BigDecimal(requestRate.toString()); 104 | this.requestRate = bigDecimal.setScale(2, BigDecimal.ROUND_HALF_UP).doubleValue(); 105 | } else { 106 | this.requestRate = 0.0; 107 | } 108 | } 109 | 110 | public Double getReceiveKBPerSecond() { 111 | return receiveKBPerSecond; 112 | } 113 | 114 | public void setReceiveKBPerSecond(Double receiveKBPerSecond) { 115 | if (Double.isFinite(receiveKBPerSecond)) { 116 | BigDecimal bigDecimal = new BigDecimal(receiveKBPerSecond); 117 | this.receiveKBPerSecond = bigDecimal.setScale(2, BigDecimal.ROUND_HALF_UP).doubleValue(); 118 | } else { 119 | this.receiveKBPerSecond = 0.0; 120 | } 121 | } 122 | 123 | public Double getSentKBPerSecond() { 124 | return sentKBPerSecond; 125 | } 126 | 127 | public void setSentKBPerSecond(Double sentKBPerSecond) { 128 | if (Double.isFinite(sentKBPerSecond)) { 129 | BigDecimal bigDecimal = new BigDecimal(sentKBPerSecond.toString()); 130 | this.sentKBPerSecond = bigDecimal.setScale(2, BigDecimal.ROUND_HALF_UP).doubleValue(); 131 | } else { 132 | this.sentKBPerSecond = 0.0; 133 | } 134 | } 135 | 136 | public Double getAvgPageBytes() { 137 | return avgPageBytes; 138 | } 139 | 140 | public void setAvgPageBytes(Double avgPageBytes) { 141 | if (Double.isFinite(avgPageBytes)) { 142 | BigDecimal bigDecimal = new BigDecimal(avgPageBytes.toString()); 143 | this.avgPageBytes = bigDecimal.setScale(2, BigDecimal.ROUND_HALF_UP).doubleValue(); 144 | } else { 145 | this.avgPageBytes = 0.0; 146 | } 147 | } 148 | 149 | public Integer getThreadCount() { 150 | return threadCount; 151 | } 152 | 153 | public void setThreadCount(Integer threadCount) { 154 | this.threadCount = threadCount; 155 | } 156 | 157 | public Integer getResultId() { 158 | return resultId; 159 | } 160 | 161 | public void setResultId(Integer resultId) { 162 | this.resultId = resultId; 163 | } 164 | 165 | @Override 166 | public String toString() { 167 | return "SampleResultInfo{" + 168 | "timeStamp=" + timeStamp + 169 | ", samplerLabel='" + samplerLabel + '\'' + 170 | ", samplerCount=" + samplerCount + 171 | ", meanTime=" + meanTime + 172 | ", minTime=" + minTime + 173 | ", maxTime=" + maxTime + 174 | ", standardDeviation=" + standardDeviation + 175 | ", errorPercentage=" + errorPercentage + 176 | ", requestRate=" + requestRate + 177 | ", receiveKBPerSecond=" + receiveKBPerSecond + 178 | ", sentKBPerSecond=" + sentKBPerSecond + 179 | ", avgPageBytes=" + avgPageBytes + 180 | ", threadCount=" + threadCount + 181 | ", resultId=" + resultId + 182 | '}'; 183 | } 184 | } 185 | -------------------------------------------------------------------------------- /src/main/java/com/berserker/architecture/domain/entity/ScenarioInfo.java: -------------------------------------------------------------------------------- 1 | package com.berserker.architecture.domain.entity; 2 | 3 | import java.util.Date; 4 | 5 | public class ScenarioInfo { 6 | private Integer id; 7 | private String scenarioName; 8 | private String scenarioDescription; 9 | private Date createTime; 10 | private Integer numThreads; 11 | private Integer rampUp; 12 | private Integer duration; 13 | 14 | public Integer getId() { 15 | return id; 16 | } 17 | 18 | public void setId(Integer id) { 19 | this.id = id; 20 | } 21 | 22 | public String getScenarioName() { 23 | return scenarioName; 24 | } 25 | 26 | public void setScenarioName(String scenarioName) { 27 | this.scenarioName = scenarioName; 28 | } 29 | 30 | public String getScenarioDescription() { 31 | return scenarioDescription; 32 | } 33 | 34 | public void setScenarioDescription(String scenarioDescription) { 35 | this.scenarioDescription = scenarioDescription; 36 | } 37 | 38 | public Date getCreateTime() { 39 | return createTime; 40 | } 41 | 42 | public void setCreateTime(Date createTime) { 43 | this.createTime = createTime; 44 | } 45 | 46 | public Integer getNumThreads() { 47 | return numThreads; 48 | } 49 | 50 | public void setNumThreads(Integer numThreads) { 51 | this.numThreads = numThreads; 52 | } 53 | 54 | public Integer getRampUp() { 55 | return rampUp; 56 | } 57 | 58 | public void setRampUp(Integer rampUp) { 59 | this.rampUp = rampUp; 60 | } 61 | 62 | public Integer getDuration() { 63 | return duration; 64 | } 65 | 66 | public void setDuration(Integer duration) { 67 | this.duration = duration; 68 | } 69 | 70 | @Override 71 | public String toString() { 72 | return "ScenarioInfo{" + 73 | "id=" + id + 74 | ", scenarioName='" + scenarioName + '\'' + 75 | ", scenarioDescription='" + scenarioDescription + '\'' + 76 | ", createTime=" + createTime + 77 | ", numThreads=" + numThreads + 78 | ", rampUp=" + rampUp + 79 | ", duration=" + duration + 80 | '}'; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/com/berserker/architecture/domain/entity/ScenarioResultInfo.java: -------------------------------------------------------------------------------- 1 | package com.berserker.architecture.domain.entity; 2 | 3 | import java.util.Date; 4 | 5 | public class ScenarioResultInfo { 6 | private Integer id; 7 | private String scenarioName; 8 | private Integer numThreads; 9 | private Integer rampUp; 10 | private Integer duration; 11 | private Date runTime; 12 | private Integer scenarioId; 13 | 14 | public Integer getId() { 15 | return id; 16 | } 17 | 18 | public void setId(Integer id) { 19 | this.id = id; 20 | } 21 | 22 | public String getScenarioName() { 23 | return scenarioName; 24 | } 25 | 26 | public void setScenarioName(String scenarioName) { 27 | this.scenarioName = scenarioName; 28 | } 29 | 30 | public Integer getNumThreads() { 31 | return numThreads; 32 | } 33 | 34 | public void setNumThreads(Integer numThreads) { 35 | this.numThreads = numThreads; 36 | } 37 | 38 | public Integer getRampUp() { 39 | return rampUp; 40 | } 41 | 42 | public void setRampUp(Integer rampUp) { 43 | this.rampUp = rampUp; 44 | } 45 | 46 | public Integer getDuration() { 47 | return duration; 48 | } 49 | 50 | public void setDuration(Integer duration) { 51 | this.duration = duration; 52 | } 53 | 54 | public Date getRunTime() { 55 | return runTime; 56 | } 57 | 58 | public void setRunTime(Date runTime) { 59 | this.runTime = runTime; 60 | } 61 | 62 | public Integer getScenarioId() { 63 | return scenarioId; 64 | } 65 | 66 | public void setScenarioId(Integer scenarioId) { 67 | this.scenarioId = scenarioId; 68 | } 69 | 70 | @Override 71 | public String toString() { 72 | return "ScenarioResultInfo{" + 73 | "id=" + id + 74 | ", scenarioName='" + scenarioName + '\'' + 75 | ", numThreads=" + numThreads + 76 | ", rampUp=" + rampUp + 77 | ", duration=" + duration + 78 | ", runTime=" + runTime + 79 | ", scenarioId=" + scenarioId + 80 | '}'; 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/com/berserker/architecture/domain/entity/ScriptFileInfo.java: -------------------------------------------------------------------------------- 1 | package com.berserker.architecture.domain.entity; 2 | 3 | import java.util.Date; 4 | 5 | public class ScriptFileInfo { 6 | private Integer id; 7 | private String scriptFileName; 8 | private String scriptFilePath; 9 | private Date uploadTime; 10 | private Integer scenarioId; 11 | 12 | public Integer getId() { 13 | return id; 14 | } 15 | 16 | public void setId(Integer id) { 17 | this.id = id; 18 | } 19 | 20 | public String getScriptFileName() { 21 | return scriptFileName; 22 | } 23 | 24 | public void setScriptFileName(String scriptFileName) { 25 | this.scriptFileName = scriptFileName; 26 | } 27 | 28 | public String getScriptFilePath() { 29 | return scriptFilePath; 30 | } 31 | 32 | public void setScriptFilePath(String scriptFilePath) { 33 | this.scriptFilePath = scriptFilePath; 34 | } 35 | 36 | public Date getUploadTime() { 37 | return uploadTime; 38 | } 39 | 40 | public void setUploadTime(Date uploadTime) { 41 | this.uploadTime = uploadTime; 42 | } 43 | 44 | public Integer getScenarioId() { 45 | return scenarioId; 46 | } 47 | 48 | public void setScenarioId(Integer scenarioId) { 49 | this.scenarioId = scenarioId; 50 | } 51 | 52 | @Override 53 | public String toString() { 54 | return "ScriptFileInfo{" + 55 | "id=" + id + 56 | ", scriptFileName='" + scriptFileName + '\'' + 57 | ", scriptFilePath='" + scriptFilePath + '\'' + 58 | ", uploadTime=" + uploadTime + 59 | ", scenarioId=" + scenarioId + 60 | '}'; 61 | } 62 | } 63 | -------------------------------------------------------------------------------- /src/main/java/com/berserker/architecture/domain/param/ScenarioInfoAddParams.java: -------------------------------------------------------------------------------- 1 | package com.berserker.architecture.domain.param; 2 | 3 | import org.hibernate.validator.constraints.NotBlank; 4 | 5 | public class ScenarioInfoAddParams { 6 | @NotBlank(message = "测试场景名称不能为空") 7 | private String scenarioName; 8 | private String numThreads; 9 | private String rampUp; 10 | private String duration; 11 | private String scenarioDescription; 12 | 13 | public String getScenarioName() { 14 | return scenarioName; 15 | } 16 | 17 | public void setScenarioName(String scenarioName) { 18 | this.scenarioName = scenarioName; 19 | } 20 | 21 | public String getScenarioDescription() { 22 | return scenarioDescription; 23 | } 24 | 25 | public void setScenarioDescription(String scenarioDescription) { 26 | this.scenarioDescription = scenarioDescription; 27 | } 28 | 29 | public String getNumThreads() { 30 | return numThreads; 31 | } 32 | 33 | public void setNumThreads(String numThreads) { 34 | this.numThreads = numThreads; 35 | } 36 | 37 | public String getRampUp() { 38 | return rampUp; 39 | } 40 | 41 | public void setRampUp(String rampUp) { 42 | this.rampUp = rampUp; 43 | } 44 | 45 | public String getDuration() { 46 | return duration; 47 | } 48 | 49 | public void setDuration(String duration) { 50 | this.duration = duration; 51 | } 52 | } 53 | -------------------------------------------------------------------------------- /src/main/java/com/berserker/architecture/domain/param/ScenarioInfoListParams.java: -------------------------------------------------------------------------------- 1 | package com.berserker.architecture.domain.param; 2 | 3 | import org.hibernate.validator.constraints.NotBlank; 4 | 5 | 6 | public class ScenarioInfoListParams { 7 | @NotBlank(message = "分页页码不能为空") 8 | private String offset; 9 | @NotBlank(message = "记录个数不能为空") 10 | private String limit; 11 | private String scenarioName; 12 | 13 | public String getOffset() { 14 | return offset; 15 | } 16 | 17 | public void setOffset(String offset) { 18 | this.offset = offset; 19 | } 20 | 21 | public String getLimit() { 22 | return limit; 23 | } 24 | 25 | public void setLimit(String limit) { 26 | this.limit = limit; 27 | } 28 | 29 | public String getScenarioName() { 30 | return scenarioName; 31 | } 32 | 33 | public void setScenarioName(String scenarioName) { 34 | this.scenarioName = scenarioName; 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/berserker/architecture/domain/param/ScenarioInfoModParams.java: -------------------------------------------------------------------------------- 1 | package com.berserker.architecture.domain.param; 2 | 3 | import org.hibernate.validator.constraints.NotBlank; 4 | import javax.validation.constraints.Pattern; 5 | 6 | public class ScenarioInfoModParams { 7 | @Pattern(regexp = "^\\+?[1-9][0-9]*$", message = "场景ID必须是非零正整数") 8 | private String scenarioId; 9 | @NotBlank(message = "测试场景名称不能为空") 10 | private String scenarioName; 11 | private String scenarioDescription; 12 | // @Pattern(regexp = "^[0-9]{4}-[0-9]{2}-[0-9]{2}\\s[0-9]{2}:[0-9]{2}:[0-9]{2}$", message="场景创建时间格式不正确") 13 | // private String createTime; 14 | private String numThreads; 15 | private String rampUp; 16 | private String duration; 17 | 18 | public String getScenarioId() { 19 | return scenarioId; 20 | } 21 | 22 | public void setScenarioId(String scenarioId) { 23 | this.scenarioId = scenarioId; 24 | } 25 | 26 | public String getScenarioName() { 27 | return scenarioName; 28 | } 29 | 30 | public void setScenarioName(String scenarioName) { 31 | this.scenarioName = scenarioName; 32 | } 33 | 34 | public String getScenarioDescription() { 35 | return scenarioDescription; 36 | } 37 | 38 | public void setScenarioDescription(String scenarioDescription) { 39 | this.scenarioDescription = scenarioDescription; 40 | } 41 | 42 | // public String getCreateTime() { 43 | // return createTime; 44 | // } 45 | // 46 | // public void setCreateTime(String createTime) { 47 | // this.createTime = createTime; 48 | // } 49 | 50 | public String getNumThreads() { 51 | return numThreads; 52 | } 53 | 54 | public void setNumThreads(String numThreads) { 55 | this.numThreads = numThreads; 56 | } 57 | 58 | public String getRampUp() { 59 | return rampUp; 60 | } 61 | 62 | public void setRampUp(String rampUp) { 63 | this.rampUp = rampUp; 64 | } 65 | 66 | public String getDuration() { 67 | return duration; 68 | } 69 | 70 | public void setDuration(String duration) { 71 | this.duration = duration; 72 | } 73 | } 74 | -------------------------------------------------------------------------------- /src/main/java/com/berserker/architecture/domain/param/ScenarioResultByIdParams.java: -------------------------------------------------------------------------------- 1 | package com.berserker.architecture.domain.param; 2 | 3 | import org.hibernate.validator.constraints.NotEmpty; 4 | import javax.validation.constraints.Pattern; 5 | 6 | public class ScenarioResultByIdParams { 7 | @NotEmpty(message="分页页码不能为空") 8 | private String offset; 9 | @NotEmpty(message="记录个数不能为空") 10 | private String limit; 11 | @Pattern(regexp = "^\\+?[1-9][0-9]*$", message = "场景ID必须是非零正整数") 12 | private String scenarioId; 13 | 14 | public String getOffset() { 15 | return offset; 16 | } 17 | 18 | public void setOffset(String offset) { 19 | this.offset = offset; 20 | } 21 | 22 | public String getLimit() { 23 | return limit; 24 | } 25 | 26 | public void setLimit(String limit) { 27 | this.limit = limit; 28 | } 29 | 30 | public String getScenarioId() { 31 | return scenarioId; 32 | } 33 | 34 | public void setScenarioId(String scenarioId) { 35 | this.scenarioId = scenarioId; 36 | } 37 | } 38 | -------------------------------------------------------------------------------- /src/main/java/com/berserker/architecture/domain/param/ScenarioResultListParams.java: -------------------------------------------------------------------------------- 1 | package com.berserker.architecture.domain.param; 2 | 3 | import org.hibernate.validator.constraints.NotEmpty; 4 | 5 | public class ScenarioResultListParams { 6 | @NotEmpty(message="分页页码不能为空") 7 | private String offset; 8 | @NotEmpty(message="记录个数不能为空") 9 | private String limit; 10 | 11 | public String getOffset() { 12 | return offset; 13 | } 14 | 15 | public void setOffset(String offset) { 16 | this.offset = offset; 17 | } 18 | 19 | public String getLimit() { 20 | return limit; 21 | } 22 | 23 | public void setLimit(String limit) { 24 | this.limit = limit; 25 | } 26 | } 27 | -------------------------------------------------------------------------------- /src/main/java/com/berserker/architecture/engine/cache/RedisSampleSerializer.java: -------------------------------------------------------------------------------- 1 | package com.berserker.architecture.engine.cache; 2 | 3 | import org.springframework.core.convert.converter.Converter; 4 | import org.springframework.core.serializer.support.DeserializingConverter; 5 | import org.springframework.core.serializer.support.SerializingConverter; 6 | import org.springframework.data.redis.serializer.RedisSerializer; 7 | import org.springframework.data.redis.serializer.SerializationException; 8 | 9 | import java.util.Objects; 10 | 11 | public class RedisSampleSerializer implements RedisSerializer{ 12 | private Converter serializer = new SerializingConverter(); 13 | private Converter deserializer = new DeserializingConverter(); 14 | 15 | private static final byte [] EMPTY_ARRAY = new byte[0]; 16 | 17 | private boolean isEmpty(byte [] data) { 18 | return (Objects.isNull(data) || Objects.equals(data.length, 0)); 19 | } 20 | 21 | /** 22 | * @description 对象序列化 23 | * @param object 待序列化的对象 24 | * @return 字节数组 25 | * @throws SerializationException 序列化异常 26 | */ 27 | @Override 28 | public byte [] serialize(Object object) throws SerializationException { 29 | if (Objects.isNull(object)) { 30 | return EMPTY_ARRAY; 31 | } 32 | return serializer.convert(object); 33 | } 34 | 35 | /** 36 | * @description 对象反序列化 37 | * @param bytes 对象字节数组 38 | * @return 对象 39 | * @throws SerializationException 序列化异常 40 | */ 41 | @Override 42 | public Object deserialize(byte[] bytes) throws SerializationException { 43 | if (this.isEmpty(bytes)) 44 | return null; 45 | return deserializer.convert(bytes); 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/berserker/architecture/engine/collect/EngineSampleCollector.java: -------------------------------------------------------------------------------- 1 | package com.berserker.architecture.engine.collect; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.berserker.architecture.domain.entity.SampleResultInfo; 5 | import org.apache.commons.lang3.StringUtils; 6 | import org.apache.jmeter.reporters.ResultCollector; 7 | import org.apache.jmeter.reporters.Summariser; 8 | import org.apache.jmeter.samplers.SampleEvent; 9 | import org.apache.jmeter.samplers.SampleResult; 10 | import org.apache.jmeter.visualizers.SamplingStatCalculator; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | import org.springframework.data.redis.core.BoundListOperations; 14 | 15 | import java.sql.Timestamp; 16 | import java.util.HashMap; 17 | import java.util.Map; 18 | import java.util.Objects; 19 | 20 | 21 | public class EngineSampleCollector extends ResultCollector{ 22 | private static final Logger log = LoggerFactory.getLogger(EngineSampleCollector.class); 23 | 24 | // 这里可以使用使用简单的Calculator类来进行性能计数(SummaryReport) 25 | // 但这里采用的聚合报告使用的SamplingStatCalculator类(StatVisualizer) 26 | // private static Map calculatorContainer = new HashMap<>(); // 针对不同请求的计数器集合 27 | private static Map calculatorContainer = new HashMap<>(); // 针对不同请求的计数器集合 28 | 29 | private BoundListOperations runningSampleResultList; // 将SampleResult存入Redis的模板 30 | private Integer runningResultId; 31 | 32 | public EngineSampleCollector(Summariser summer, BoundListOperations runningSampleResultList) { 33 | super(summer); 34 | this.runningSampleResultList = runningSampleResultList; 35 | this.runningResultId = runningResultId; 36 | } 37 | 38 | /** 39 | * @description 性能数据的采样方法 40 | * @param event 请求样本 41 | */ 42 | @Override 43 | public void sampleOccurred(SampleEvent event) { 44 | super.sampleOccurred(event); 45 | SampleResult result = event.getResult(); 46 | String sampleLabel = result.getSampleLabel(); 47 | 48 | // 输出请求数据和响应,用于调试.后面考虑加入实时日志显示 49 | if (result.getUrlAsString().contains("/Recruiter/Login")) { 50 | log.info("*************************************"); 51 | log.info(StringUtils.join("请求发送: ", result.getSamplerData())); // 打印请求URL和参数 52 | log.info(StringUtils.join("请求响应: " + result.getResponseDataAsString())); // 打印请求返回值 53 | log.info("*************************************"); 54 | } 55 | 56 | // 必须要针对不同的请求分别实例化计数器.如果直接使用计数器,则区分不出来请求的数据. 57 | if (Objects.isNull(calculatorContainer.get(sampleLabel))){ 58 | calculatorContainer.put(sampleLabel, new SamplingStatCalculator()); 59 | calculatorContainer.get(sampleLabel).addSample(result); 60 | } else { 61 | calculatorContainer.get(sampleLabel).addSample(result); 62 | } 63 | 64 | SamplingStatCalculator calculator = calculatorContainer.get(sampleLabel); 65 | 66 | // log.info("Sampler Label: " + result.getSampleLabel() + 67 | // "\t\tSampler Count: " + calculator.getCount() + 68 | // "\t\tResponse Mean Time: " + calculator.getMeanAsNumber() + 69 | // "\t\tResponse Min Time: " + calculator.getMin() + 70 | // "\t\tResponse Max Time: " + calculator.getMax() + 71 | // "\t\tResponse Standard Deviation: " + calculator.getStandardDeviation() + 72 | // "\t\tError Percentage: " + calculator.getErrorPercentage() + 73 | // "\t\tRequest Rate: " + calculator.getRate() + 74 | // "\t\tThroughput bytes Per Second: " + calculator.getKBPerSecond() + 75 | // "\t\tSent Throughput KB Per Second: " + calculator.getSentKBPerSecond() + 76 | // "\t\tAvg Page Bytes: " + calculator.getAvgPageBytes() + 77 | // "\t\tThread Count: " + result.getAllThreads() 78 | // ); 79 | 80 | SampleResultInfo sampleResultInfo = new SampleResultInfo(); 81 | sampleResultInfo.setTimeStamp(new Timestamp(result.getTimeStamp())); // 时间戳(这里将Long转换为TimeStamp类型) 82 | sampleResultInfo.setSamplerLabel(result.getSampleLabel()); // 请求名称 83 | sampleResultInfo.setSamplerCount(calculator.getCount()); // SamplerCount 84 | sampleResultInfo.setMeanTime(calculator.getMean()); // 平均响应时间 85 | sampleResultInfo.setMinTime(calculator.getMin().longValue()); // 最小响应时间 86 | sampleResultInfo.setMaxTime(calculator.getMax().longValue()); // 最大响应时间 87 | sampleResultInfo.setStandardDeviation(calculator.getStandardDeviation()); // 标准方差 88 | sampleResultInfo.setErrorPercentage(calculator.getErrorPercentage()); // 错误率 89 | sampleResultInfo.setRequestRate(calculator.getRate()); // 每秒请求处理能力 90 | sampleResultInfo.setReceiveKBPerSecond(calculator.getKBPerSecond()); // 每秒Receive的数据量 91 | sampleResultInfo.setSentKBPerSecond(calculator.getSentKBPerSecond()); // 每秒Send的数据量 92 | sampleResultInfo.setAvgPageBytes(calculator.getAvgPageBytes()); // 平均页面大小 93 | sampleResultInfo.setThreadCount(result.getAllThreads()); // 线程数量 94 | 95 | // 将信息以列表形式存入Redis,以时间戳为Key 96 | String sampleResultJson = JSON.toJSONString(sampleResultInfo); 97 | // 使用右侧插入,在实时显示的时候可以按正确顺序显示 98 | runningSampleResultList.rightPush(sampleResultJson); 99 | } 100 | 101 | /** 102 | * @description 在测试完成后,清理结果收集器的数据 103 | */ 104 | public void clearCalculator() { 105 | calculatorContainer.clear(); 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/main/java/com/berserker/architecture/engine/collect/EngineSampleRealTimeOuter.java: -------------------------------------------------------------------------------- 1 | package com.berserker.architecture.engine.collect; 2 | 3 | import com.berserker.architecture.engine.core.EngineController; 4 | import org.slf4j.Logger; 5 | import org.slf4j.LoggerFactory; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.data.redis.core.BoundListOperations; 8 | import org.springframework.messaging.simp.SimpMessagingTemplate; 9 | import org.springframework.scheduling.annotation.Async; 10 | import org.springframework.stereotype.Component; 11 | 12 | @Component 13 | public class EngineSampleRealTimeOuter { 14 | private static final Logger log = LoggerFactory.getLogger(EngineSampleRealTimeOuter.class); 15 | @Autowired 16 | private EngineController controller; 17 | @Autowired 18 | private SimpMessagingTemplate template; 19 | 20 | private BoundListOperations runningSampleResultList; 21 | 22 | /** 23 | * @description 获取Redis的队列列表 24 | * @param runningSampleResultList Redis队列列表 25 | */ 26 | public void setRunningSampleResultList(BoundListOperations runningSampleResultList) { 27 | this.runningSampleResultList = runningSampleResultList; 28 | } 29 | 30 | /** 31 | * @description 通过WebSocket向前端发送数据(控制时间间隔发送数据,避免浏览器崩溃) 32 | */ 33 | @Async 34 | public void sampleRealTimeOuter() { 35 | // 初始化游标 36 | long cursor = 0; 37 | // 定时器开关 38 | Boolean isSendSamplerResult = Boolean.TRUE; 39 | // 下一次发送时间 40 | Long nextSendTime = 0L; 41 | Long currentTime = 0L; 42 | // 如果Engine还处于运行状态则保持输出状态 43 | while (controller.getEngineStatus()) { 44 | // 首先获取Redis中SampleResult列表的长度 45 | long sampleResultLength = runningSampleResultList.size(); 46 | // 如果列表还没有数据,则continue直到有数据为止 47 | if (cursor >= sampleResultLength) 48 | continue; 49 | // 如果列表中有数据,则按照索引读取,通过WebSocket发送到前端 50 | while (cursor < sampleResultLength) { 51 | String sampleResult = runningSampleResultList.index(cursor); 52 | // log.info(sampleResult); 53 | // 做一个定时器,每隔1秒向前端发送一个SamplerResult 54 | currentTime = System.currentTimeMillis(); 55 | if (isSendSamplerResult) { 56 | nextSendTime = currentTime + 1000L; 57 | template.convertAndSend("/sampleResult/data", sampleResult); 58 | isSendSamplerResult = Boolean.FALSE; 59 | } 60 | // 如果当前时间大于等于上次发送信息时定义的nextSendTime,则将是否发送的标志位置为TRUE 61 | if (currentTime >= nextSendTime) 62 | isSendSamplerResult = Boolean.TRUE; 63 | // 列表下标自加 64 | cursor++; 65 | } 66 | // 此时cursor的值和sampleResultLength相同,进行下一次循环 67 | } 68 | } 69 | } 70 | -------------------------------------------------------------------------------- /src/main/java/com/berserker/architecture/engine/core/EngineController.java: -------------------------------------------------------------------------------- 1 | package com.berserker.architecture.engine.core; 2 | 3 | import com.berserker.architecture.engine.reader.EngineScenarioReader; 4 | import org.apache.jmeter.engine.StandardJMeterEngine; 5 | import org.apache.jmeter.reporters.ResultCollector; 6 | import org.apache.jorphan.collections.HashTree; 7 | import org.springframework.data.redis.core.BoundListOperations; 8 | import org.springframework.beans.factory.annotation.Autowired; 9 | import org.springframework.stereotype.Component; 10 | 11 | import java.util.HashMap; 12 | import java.util.List; 13 | import java.util.Map; 14 | 15 | @Component 16 | public class EngineController { 17 | @Autowired 18 | private EngineParamLoader paramLoader; 19 | @Autowired 20 | private EngineScenarioReader scenarioReader; 21 | @Autowired 22 | private EngineResultHandler resultHandler; 23 | @Autowired 24 | private EngineTestPlanSetter testPlanSetter; 25 | @Autowired 26 | private EngineTestRunner testRunner; 27 | 28 | private StandardJMeterEngine engine = new StandardJMeterEngine(); 29 | 30 | private Integer runningScenarioId; 31 | 32 | private String runningScenarioName; 33 | 34 | private Integer runningResultId; 35 | 36 | private BoundListOperations runningSampleResultList; 37 | 38 | public StandardJMeterEngine getEngine() { 39 | return engine; 40 | } 41 | 42 | public Boolean getEngineStatus() { 43 | return engine.isActive(); 44 | } 45 | 46 | public Integer getRunningScenarioId() { 47 | return runningScenarioId; 48 | } 49 | 50 | public String getRunningScenarioName(){ 51 | return runningScenarioName; 52 | } 53 | 54 | public Integer getRunningResultId() { 55 | return runningResultId; 56 | } 57 | 58 | public void setRunningResultId(Integer runningResultId) { 59 | this.runningResultId = runningResultId; 60 | } 61 | 62 | public void setRunningSampleResultList(BoundListOperations runningSampleResultList) { 63 | this.runningSampleResultList = runningSampleResultList; 64 | } 65 | 66 | public BoundListOperations getRunningSampleResultList() { 67 | return runningSampleResultList; 68 | } 69 | 70 | /** 71 | * @description 场景的停止方法 72 | * @return 返回停止的标志位 73 | */ 74 | public Map stopEngine() { 75 | Map stopRunResult = new HashMap<>(); 76 | if (engine.isActive()) { 77 | engine.stopTest(Boolean.TRUE); 78 | stopRunResult.put("status", "True"); 79 | stopRunResult.put("message", "场景成功停止运行"); 80 | } else { 81 | stopRunResult.put("status", "False"); 82 | stopRunResult.put("message", "没有场景处于运行状态"); 83 | } 84 | return stopRunResult; 85 | } 86 | 87 | /** 88 | * @description 场景运行器 89 | * @param scenarioId 场景ID 90 | */ 91 | public void engineScenarioRunner(Integer scenarioId) { 92 | // 设置引擎参数 93 | paramLoader.setEngineParam(); 94 | // 添加场景参数,获得新的测试计划树 95 | Map scenarioRunInfo = scenarioReader.testPlanReader(scenarioId); 96 | // 获取运行场景ID,为检查是否有场景运行做准备 97 | runningScenarioId = Integer.parseInt(scenarioRunInfo.get("scenarioId").toString()); 98 | // 获取运行场景名称,为检查是否有场景运行做准备 99 | runningScenarioName = scenarioRunInfo.get("scenarioName").toString(); 100 | // 添加场景的结果收集 101 | List resultCollectorList = resultHandler.resultCollect(scenarioId); 102 | HashTree testPlanTree = testPlanSetter.testPlanSetting(scenarioRunInfo, resultCollectorList); 103 | // 运行测试 104 | testRunner.scenarioRun(engine, testPlanTree); 105 | // 运行完场景之后,重置结果收集器 106 | resultHandler.clearCalculator(); 107 | // 运行完场景之后,将SamplerResultList赋给EngineController中的runningSampleResultList 108 | runningSampleResultList = resultHandler.getRunningSampleResultList(); 109 | } 110 | 111 | public void engineScenarioRealOuter() { 112 | resultHandler.resultRealTimeOuter(); 113 | } 114 | } 115 | -------------------------------------------------------------------------------- /src/main/java/com/berserker/architecture/engine/core/EngineParamLoader.java: -------------------------------------------------------------------------------- 1 | package com.berserker.architecture.engine.core; 2 | 3 | import org.apache.jmeter.save.SaveService; 4 | import org.apache.jmeter.util.JMeterUtils; 5 | import org.springframework.beans.factory.annotation.Value; 6 | import org.springframework.context.annotation.PropertySource; 7 | import org.springframework.stereotype.Component; 8 | 9 | import java.io.IOException; 10 | 11 | @Component 12 | @PropertySource(value = {"classpath:application.yml"}, encoding="UTF-8") 13 | public class EngineParamLoader { 14 | @Value("${jmeterSetting.jmeter-home}") 15 | private String jmeterHomePath; // Jmeter的路径 16 | @Value("${jmeterSetting.jmeter-properties}") 17 | private String jmeterPropertiesPath; // Jmeter配置文件的路径 18 | 19 | /** 20 | * @description 运行前设置JmeterEngine的参数 21 | */ 22 | public void setEngineParam() { 23 | // 初始化参数,日志,地区等信息 24 | JMeterUtils.setJMeterHome(jmeterHomePath); 25 | JMeterUtils.loadJMeterProperties(jmeterPropertiesPath); 26 | JMeterUtils.initLocale(); 27 | // 从Jmeter3.2之后使用log4j2,不需要使用initLogging() 28 | // JMeterUtils.initLogging(); 29 | // 参数和插件的加载 30 | try { 31 | SaveService.loadProperties(); 32 | } catch (IOException e) { 33 | e.printStackTrace(); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/main/java/com/berserker/architecture/engine/core/EngineResultHandler.java: -------------------------------------------------------------------------------- 1 | package com.berserker.architecture.engine.core; 2 | 3 | import com.berserker.architecture.engine.collect.EngineSampleCollector; 4 | import com.berserker.architecture.engine.collect.EngineSampleRealTimeOuter; 5 | import org.apache.commons.lang3.StringUtils; 6 | import org.apache.jmeter.reporters.ResultCollector; 7 | import org.apache.jmeter.reporters.Summariser; 8 | import org.apache.jmeter.util.JMeterUtils; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.data.redis.core.BoundListOperations; 11 | import org.springframework.data.redis.core.StringRedisTemplate; 12 | import org.springframework.stereotype.Component; 13 | 14 | import java.util.ArrayList; 15 | import java.util.List; 16 | import java.util.Objects; 17 | 18 | @Component 19 | public class EngineResultHandler { 20 | @Autowired 21 | private StringRedisTemplate stringRedisTemplate; 22 | @Autowired 23 | private EngineSampleRealTimeOuter engineSampleRealTimeOuter; 24 | 25 | private EngineSampleCollector engineSampleCollector; 26 | 27 | private BoundListOperations runningSampleResultList; 28 | 29 | /** 30 | * @description 添加结果收集器 31 | * @return 返回结果收集器列表 32 | */ 33 | public List resultCollect(Integer scenarioId) { 34 | // 结果收集 35 | Summariser summary = null; 36 | String summariserName = JMeterUtils.getPropDefault("summariser.name", "summary"); 37 | if (summariserName.length() > 0) { 38 | summary = new Summariser(summariserName); 39 | // 设置summary打印间隔,没起作用 40 | summary.setProperty("summariser.interval", "1"); 41 | } 42 | // 先将SampleResult的结果队列定义好,定义存入Redis的结果Key 43 | String runningScenarioKey = StringUtils.join("scenario_", scenarioId); 44 | runningSampleResultList = stringRedisTemplate.boundListOps(runningScenarioKey); 45 | 46 | // 定义结果收集器的List 47 | List resultCollectorList = new ArrayList<>(); 48 | 49 | // 自定义结果收集.继承自ResultCollector.runningSampleResultList,将SampleResult的数据进行缓存 50 | engineSampleCollector = new EngineSampleCollector(summary, runningSampleResultList); 51 | // 这两个方法都是继承自ResultCollector的方法 52 | engineSampleCollector.setName("自定义结果收集"); 53 | engineSampleCollector.setEnabled(Boolean.TRUE); 54 | // 添加刚才定义的结果收集 55 | resultCollectorList.add(engineSampleCollector); 56 | 57 | // 原生的结果收集器 58 | // ResultCollector csvCollector = new ResultCollector(summary); 59 | // csvCollector.setName("自定义结果记录"); 60 | // csvCollector.setFilename("D:\\zbc.csv"); 61 | // resultCollectorList.add(csvCollector); 62 | 63 | return resultCollectorList; 64 | } 65 | 66 | /** 67 | * @description 结果实时输出 68 | */ 69 | public void resultRealTimeOuter() { 70 | engineSampleRealTimeOuter.setRunningSampleResultList(runningSampleResultList); 71 | engineSampleRealTimeOuter.sampleRealTimeOuter(); 72 | } 73 | 74 | /** 75 | * @description 重置结果收集器 76 | */ 77 | public void clearCalculator() { 78 | if (Objects.nonNull(engineSampleCollector)) 79 | engineSampleCollector.clearCalculator(); 80 | } 81 | 82 | /** 83 | * @description 获取当前的SamplerResult列表(为测试完成后,存储测试结果做准备) 84 | * @return 85 | */ 86 | public BoundListOperations getRunningSampleResultList() { 87 | return runningSampleResultList; 88 | } 89 | } 90 | -------------------------------------------------------------------------------- /src/main/java/com/berserker/architecture/engine/core/EngineTestPlanSetter.java: -------------------------------------------------------------------------------- 1 | package com.berserker.architecture.engine.core; 2 | 3 | import com.berserker.architecture.utils.FileStorageUtil; 4 | import org.apache.jmeter.control.LoopController; 5 | import org.apache.jmeter.reporters.ResultCollector; 6 | import org.apache.jmeter.save.SaveService; 7 | import org.apache.jmeter.testelement.TestElement; 8 | import org.apache.jmeter.threads.ThreadGroup; 9 | import org.apache.jorphan.collections.HashTree; 10 | import org.apache.jorphan.collections.SearchByClass; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.stereotype.Component; 13 | 14 | import java.io.File; 15 | import java.io.IOException; 16 | import java.util.Collection; 17 | import java.util.List; 18 | import java.util.Map; 19 | 20 | @Component 21 | public class EngineTestPlanSetter { 22 | @Autowired 23 | private FileStorageUtil fileUtil; 24 | 25 | /** 26 | * @description 根据页面中输入的参数,给测试计划设定并发数量,攀升时间,持续时间.以及修改一些计划中的数据 27 | * @param testPlanInfo 测试计划的实体 28 | * @param collectorList 结果收集器的列表 29 | * @return 30 | */ 31 | public HashTree testPlanSetting(Map testPlanInfo, List collectorList) { 32 | // 脚本读取 33 | File scriptFile = (File) testPlanInfo.get("scriptFile"); 34 | Integer numThreads = (Integer) testPlanInfo.get("numThreads"); 35 | Integer rampUp = (Integer) testPlanInfo.get("rampUp"); 36 | Integer duration = (Integer) testPlanInfo.get("duration"); 37 | 38 | // 定义测试计划树 39 | HashTree testPlanTree = null; 40 | try { 41 | // 读取测试用例 42 | testPlanTree = SaveService.loadTree(scriptFile); 43 | 44 | // 遍历测试用例 45 | SearchByClass testElementVisitor = new SearchByClass<>(TestElement.class); 46 | testPlanTree.traverse(testElementVisitor); 47 | Collection testElementCollection = testElementVisitor.getSearchResults(); 48 | for (TestElement testElement : testElementCollection) { 49 | // 输出元素名称 50 | // System.out.println(testElement.getName()); 51 | if (testElement instanceof ThreadGroup) { 52 | // 定义线程组参数 53 | ThreadGroup threadGroup = (ThreadGroup) testElement; 54 | if (threadGroup.isEnabled()) { 55 | threadGroup.setNumThreads(numThreads); 56 | threadGroup.setRampUp(rampUp); 57 | threadGroup.setScheduler(Boolean.TRUE); 58 | threadGroup.setDuration(duration); 59 | // 获取测试计划运行逻辑(启用调度器,确定运行时间) 60 | LoopController loopController = (LoopController)threadGroup.getProperty("ThreadGroup.main_controller").getObjectValue(); 61 | loopController.setLoops(-1); // 不按照Loop次数来运行测试 62 | loopController.setContinueForever(Boolean.FALSE); 63 | System.out.println(); 64 | } 65 | } 66 | } 67 | 68 | // 在加入自定义结果收集之前对测试计划进行回写,避免由于添加自定义结果收集而导致测试计划结构出错. 69 | fileUtil.scriptBackWrite(scriptFile.getAbsolutePath(), testPlanTree); 70 | 71 | // 加入自定义的结果收集元素 72 | for (ResultCollector collector: collectorList) 73 | testPlanTree.add(testPlanTree.getArray()[0], collector); 74 | } catch (IOException e) { 75 | e.printStackTrace(); 76 | } 77 | 78 | return testPlanTree; 79 | } 80 | } 81 | -------------------------------------------------------------------------------- /src/main/java/com/berserker/architecture/engine/core/EngineTestRunner.java: -------------------------------------------------------------------------------- 1 | package com.berserker.architecture.engine.core; 2 | 3 | import org.apache.jmeter.engine.StandardJMeterEngine; 4 | import org.apache.jorphan.collections.HashTree; 5 | import org.springframework.stereotype.Component; 6 | 7 | import java.util.Objects; 8 | 9 | @Component 10 | public class EngineTestRunner { 11 | /** 12 | * @description 场景运行方法 13 | * @param engine Jmeter引擎 14 | * @param testPlanTree 测试计划的HashTree 15 | */ 16 | public void scenarioRun(StandardJMeterEngine engine, HashTree testPlanTree) { 17 | if (Objects.nonNull(testPlanTree)) { 18 | engine.configure(testPlanTree); // 载入测试计划 19 | engine.run(); // 运行测试 20 | } 21 | } 22 | } 23 | -------------------------------------------------------------------------------- /src/main/java/com/berserker/architecture/engine/reader/EngineScenarioReader.java: -------------------------------------------------------------------------------- 1 | package com.berserker.architecture.engine.reader; 2 | 3 | import com.berserker.architecture.utils.ScriptUtil; 4 | import com.berserker.architecture.domain.entity.ScenarioInfo; 5 | import com.berserker.architecture.mapper.ScenarioInfoMapper; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.stereotype.Component; 8 | 9 | import java.io.File; 10 | import java.util.HashMap; 11 | import java.util.Map; 12 | 13 | @Component 14 | public class EngineScenarioReader { 15 | @Autowired 16 | private ScenarioInfoMapper scenarioInfoMapper; 17 | @Autowired 18 | private ScriptUtil scriptUtil; 19 | 20 | /** 21 | * @description 场景信息的读取(ID,场景名称,测试计划,并发数量,攀升时间,持续时间) 22 | * @param scenarioId 场景ID 23 | * @return 返回场景的Map结构 24 | */ 25 | public Map testPlanReader(Integer scenarioId) { 26 | ScenarioInfo scenarioInfo = scenarioInfoMapper.getScenarioInfo(scenarioId); 27 | File scriptFileObject = scriptUtil.getScriptFileByScenarioId(scenarioId); 28 | 29 | Map scenarioRunInfo = new HashMap<>(); 30 | scenarioRunInfo.put("scenarioId", scenarioInfo.getId()); 31 | scenarioRunInfo.put("scenarioName", scenarioInfo.getScenarioName()); 32 | scenarioRunInfo.put("scriptFile", scriptFileObject); 33 | scenarioRunInfo.put("scriptAbsolutePath", scriptFileObject.getAbsolutePath()); 34 | scenarioRunInfo.put("numThreads", scenarioInfo.getNumThreads()); 35 | scenarioRunInfo.put("rampUp", scenarioInfo.getRampUp()); 36 | scenarioRunInfo.put("duration", scenarioInfo.getDuration()); 37 | 38 | return scenarioRunInfo; 39 | } 40 | } 41 | -------------------------------------------------------------------------------- /src/main/java/com/berserker/architecture/exception/GlobalExceptionHandler.java: -------------------------------------------------------------------------------- 1 | package com.berserker.architecture.exception; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.alibaba.fastjson.JSONObject; 5 | import org.springframework.web.bind.annotation.ControllerAdvice; 6 | import org.springframework.web.bind.annotation.ExceptionHandler; 7 | import org.springframework.web.servlet.ModelAndView; 8 | 9 | import javax.servlet.http.HttpServletRequest; 10 | import java.util.HashMap; 11 | import java.util.Map; 12 | import java.util.Objects; 13 | 14 | @ControllerAdvice 15 | public class GlobalExceptionHandler { 16 | /** 17 | * @description 判断请求是普通请求还是AJAX请求 18 | * @param request 请求 19 | * @return 是否是AJAX请求 20 | */ 21 | private static boolean isAjax(HttpServletRequest request) { 22 | return Objects.nonNull(request.getHeader("X-Requested-With")) && Objects.equals(request.getHeader("X-Requested-With"), "XMLHttpRequest"); 23 | } 24 | 25 | /** 26 | * @description 统一异常处理的方法 27 | * @param request 请求 28 | * @param e 处理请求时发生的异常 29 | * @return 带有异常信息的Object 30 | * @throws Exception 31 | */ 32 | @ExceptionHandler(value = Exception.class) 33 | public Object exceptHandler(HttpServletRequest request, Exception e) throws Exception { 34 | if (isAjax(request)) { 35 | Map exceptInfo = new HashMap<>(); 36 | exceptInfo.put("url", request.getRequestURL().toString()); 37 | exceptInfo.put("exceptMessage", e.getMessage()); 38 | return JSONObject.parseObject(JSON.toJSONString(exceptInfo)); 39 | } else { 40 | ModelAndView view = new ModelAndView(); 41 | view.addObject("url", request.getRequestURL().toString()); 42 | view.addObject("exceptMessage", e.getMessage()); 43 | view.setViewName("scenarioException"); 44 | return view; 45 | } 46 | } 47 | } 48 | -------------------------------------------------------------------------------- /src/main/java/com/berserker/architecture/interceptor/PreventRepeatSubmit.java: -------------------------------------------------------------------------------- 1 | package com.berserker.architecture.interceptor; 2 | 3 | import java.lang.annotation.ElementType; 4 | import java.lang.annotation.Retention; 5 | import java.lang.annotation.RetentionPolicy; 6 | import java.lang.annotation.Target; 7 | 8 | 9 | /** 10 | * description 自定义注解,放置表单重复提交 11 | */ 12 | @Target(ElementType.METHOD) 13 | @Retention(RetentionPolicy.RUNTIME) 14 | public @interface PreventRepeatSubmit {} 15 | -------------------------------------------------------------------------------- /src/main/java/com/berserker/architecture/interceptor/PreventRepeatSubmitInterceptor.java: -------------------------------------------------------------------------------- 1 | package com.berserker.architecture.interceptor; 2 | 3 | import com.alibaba.fastjson.JSON; 4 | import com.alibaba.fastjson.JSONObject; 5 | import org.springframework.util.DigestUtils; 6 | import org.springframework.util.MultiValueMap; 7 | import org.springframework.web.method.HandlerMethod; 8 | import org.springframework.web.multipart.MultipartFile; 9 | import org.springframework.web.multipart.MultipartHttpServletRequest; 10 | import org.springframework.web.servlet.handler.HandlerInterceptorAdapter; 11 | 12 | import javax.servlet.http.HttpServletRequest; 13 | import javax.servlet.http.HttpServletResponse; 14 | import java.io.IOException; 15 | import java.lang.reflect.Method; 16 | import java.math.BigInteger; 17 | import java.util.HashMap; 18 | import java.util.Map; 19 | import java.util.Objects; 20 | 21 | public class PreventRepeatSubmitInterceptor extends HandlerInterceptorAdapter { 22 | /** 23 | * @description 计算文件MD5,用于重复提交的验证 24 | * @param uploadFile 上传的文件 25 | * @return 返回文件的MD5 26 | */ 27 | private String calculateFileMD5(MultipartFile uploadFile) { 28 | byte [] uploadBytes = null; 29 | try { 30 | uploadBytes = uploadFile.getBytes(); 31 | byte [] digest = DigestUtils.md5Digest(uploadBytes); 32 | String hashString = new BigInteger(1, digest).toString(16); 33 | return hashString.toUpperCase(); 34 | } catch (IOException e) { 35 | e.printStackTrace(); 36 | } 37 | return null; 38 | } 39 | 40 | /** 41 | * @description 拦截表单重复提交 42 | * @param request HttpServletRequest request 43 | * @param response HttpServletResponse response 44 | * @param handler Object handler 45 | * @return 返回false代表重复提交,true为非重复提交 46 | * @throws Exception 47 | */ 48 | @Override 49 | public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception { 50 | if (handler instanceof HandlerMethod) { 51 | HandlerMethod handlerMethod = (HandlerMethod) handler; 52 | Method method = handlerMethod.getMethod(); 53 | PreventRepeatSubmit annotation = method.getAnnotation(PreventRepeatSubmit.class); 54 | if (annotation != null) { 55 | if(this.preventRepeatSubmitValidator(request)) // 如果重复相同数据 56 | return false; // 返回false代表重复提交 57 | else 58 | return true; // 返回true代表非重复提交 59 | } 60 | return true; 61 | } else { 62 | return super.preHandle(request, response, handler); 63 | } 64 | } 65 | 66 | /** 67 | * @description 判断是否重复提交的验证方法,通过前后两次请求的URI和参数对比,来判断是否是重复提交 68 | * @param request 当前请求 69 | * @return 如果返回True代表是重复提交,false为非重复提交 70 | */ 71 | private boolean preventRepeatSubmitValidator(HttpServletRequest request) { 72 | // 获取请求URL 73 | String url = request.getRequestURI(); 74 | Map requestParams = request.getParameterMap(); 75 | // 获取请求参数,转成JSON字符串 76 | JSONObject paramsJsonObject = JSONObject.parseObject(JSON.toJSONString(requestParams)); 77 | 78 | MultiValueMap multiValueMap = ((MultipartHttpServletRequest) request).getMultiFileMap(); 79 | // 判断上传文件是否为空 80 | if (multiValueMap.size() > 0) { 81 | MultipartFile uploadFile = ((MultipartHttpServletRequest) request).getMultiFileMap().get("scriptFile").get(0); 82 | // 获取文件的MD5 83 | String uploadFileMD5 = this.calculateFileMD5(uploadFile); 84 | // 将上传文件的HashCode放置到待验证的参数Json中 85 | paramsJsonObject.put("uploadFileMD5", uploadFileMD5); 86 | } 87 | // 将URL和参数组成map,转成字符串 88 | Map currentRequestObject = new HashMap<>(); 89 | currentRequestObject.put(url, paramsJsonObject.toJSONString()); 90 | String currentRequestString = currentRequestObject.toString(); 91 | 92 | Object preRequestObject = request.getSession().getAttribute("repeatSubmit"); 93 | if (Objects.isNull(preRequestObject)) { 94 | // 如果是第一次提交,则不是重复提交 95 | request.getSession().setAttribute("repeatSubmit", currentRequestString); 96 | return false; 97 | } else { 98 | if (Objects.equals(preRequestObject.toString(), currentRequestString)) { 99 | // 如果本次提交的数据和上次相同,则被认为是重复提交 100 | return true; 101 | } else { 102 | // 如果本次提交的数据和上次不同,则不是重复提交 103 | request.getSession().setAttribute("repeatSubmit", currentRequestString); 104 | return false; 105 | } 106 | } 107 | } 108 | } 109 | -------------------------------------------------------------------------------- /src/main/java/com/berserker/architecture/mapper/ParamFileInfoMapper.java: -------------------------------------------------------------------------------- 1 | package com.berserker.architecture.mapper; 2 | 3 | import com.berserker.architecture.domain.entity.ParamFileInfo; 4 | import org.apache.ibatis.annotations.*; 5 | 6 | import java.util.List; 7 | 8 | public interface ParamFileInfoMapper { 9 | @Select("SELECT COUNT(*) FROM paramfileinfo") 10 | Integer getParamFileInfoCount(); 11 | 12 | @Select("SELECT * FROM paramfileinfo WHERE id = #{id}") 13 | ParamFileInfo getParamFileInfo(@Param("id") Integer id); 14 | 15 | @Select("SELECT * FROM paramfileinfo WHERE scenarioId = #{scenarioId}") 16 | List getParamFileInfoListByScenarioId(@Param("scenarioId") Integer scenarioId); 17 | 18 | @Insert("INSERT INTO paramfileinfo(paramFileName, paramFilePath, uploadTime, scenarioId)" + 19 | "VALUES(#{paramFileName}, #{paramFilePath}, #{uploadTime}, #{scenarioId})") 20 | @Options(useGeneratedKeys = true, keyProperty = "id") 21 | int insertParamFileInfo(ParamFileInfo paramFileInfo); 22 | 23 | @Update("UPDATE paramfileinfo " + 24 | "SET paramFileName = #{paramFileName}, paramFilePath = #{paramFilePath}, uploadTime = #{uploadTime}, scenarioId = #{scenarioId}" + 25 | "WHERE id = #{id}") 26 | int modifyParamFileInfo(ParamFileInfo paramFileInfo); 27 | 28 | @Delete("DELETE FROM paramfileinfo WHERE id = #{id}") 29 | int deleteParamFileInfo(@Param("id") Integer id); 30 | 31 | @Delete("DELETE FROM paramfileinfo WHERE scenarioId = #{scenarioId}") 32 | int deleteParamFileInfoByScenarioId(@Param("scenarioId") Integer scenarioId); 33 | } 34 | -------------------------------------------------------------------------------- /src/main/java/com/berserker/architecture/mapper/SampleResultInfoMapper.java: -------------------------------------------------------------------------------- 1 | package com.berserker.architecture.mapper; 2 | 3 | import com.berserker.architecture.domain.entity.SampleResultInfo; 4 | import org.apache.ibatis.annotations.Delete; 5 | import org.apache.ibatis.annotations.InsertProvider; 6 | import org.apache.ibatis.annotations.Param; 7 | import org.apache.ibatis.annotations.Select; 8 | 9 | import java.util.List; 10 | 11 | public interface SampleResultInfoMapper { 12 | @Select("SELECT * FROM sampleresultinfo WHERE id = #{id}") 13 | SampleResultInfo getSampleResultInfo(Integer id); 14 | 15 | @Select("SELECT * FROM sampleresultinfo WHERE resultId = #{resultId} ORDER BY timeStamp DESC") 16 | List getSampleResultInfoListByResultId(Integer resultId); 17 | 18 | @InsertProvider(type = SampleResultInfoProvider.class, method = "insertBatchSampleResult") 19 | void insertSampleResultInfoList(@Param("sampleResultInfoList") List SampleResultInfoList); 20 | 21 | @Delete("DELETE FROM sampleresultinfo WHERE resultId = #{resultId}") 22 | int deleteSampleResultInfoByResultId(Integer resultId); 23 | 24 | @Select("SELECT samplerLabel FROM sampleresultinfo WHERE resultId = #{resultId} GROUP BY samplerLabel") 25 | List getSamplerLabelByResultId(Integer resultId); 26 | 27 | @Select("SELECT * FROM sampleresultinfo WHERE resultId = #{resultId} AND samplerLabel = #{samplerLabel} ORDER BY id DESC LIMIT 1") 28 | SampleResultInfo getSampleResultData(@Param("resultId") Integer resultId, @Param("samplerLabel") String samplerLabel); 29 | } 30 | -------------------------------------------------------------------------------- /src/main/java/com/berserker/architecture/mapper/SampleResultInfoProvider.java: -------------------------------------------------------------------------------- 1 | package com.berserker.architecture.mapper; 2 | 3 | import com.berserker.architecture.domain.entity.SampleResultInfo; 4 | 5 | import java.text.MessageFormat; 6 | import java.util.List; 7 | import java.util.Map; 8 | 9 | public class SampleResultInfoProvider { 10 | public String insertBatchSampleResult(Map> params) { 11 | List sampleResultInfoList = params.get("sampleResultInfoList"); 12 | MessageFormat messageFormat = new MessageFormat( 13 | "(#'{'sampleResultInfoList[{0}].timeStamp'}', " + 14 | "#'{'sampleResultInfoList[{0}].samplerLabel'}', " + 15 | "#'{'sampleResultInfoList[{0}].meanTime'}', " + 16 | "#'{'sampleResultInfoList[{0}].minTime'}', " + 17 | "#'{'sampleResultInfoList[{0}].maxTime'}', " + 18 | "#'{'sampleResultInfoList[{0}].standardDeviation'}', " + 19 | "#'{'sampleResultInfoList[{0}].errorPercentage'}', " + 20 | "#'{'sampleResultInfoList[{0}].requestRate'}', " + 21 | "#'{'sampleResultInfoList[{0}].receiveKBPerSecond'}', " + 22 | "#'{'sampleResultInfoList[{0}].sentKBPerSecond'}', " + 23 | "#'{'sampleResultInfoList[{0}].avgPageBytes'}', " + 24 | "#'{'sampleResultInfoList[{0}].threadCount'}', " + 25 | "#'{'sampleResultInfoList[{0}].resultId'}')" 26 | ); 27 | StringBuilder batchInsertSQL = new StringBuilder(); 28 | batchInsertSQL.append("INSERT INTO sampleresultinfo"); 29 | batchInsertSQL.append("(timeStamp, samplerLabel, meanTime, minTime, maxTime, standardDeviation, errorPercentage, requestRate, receiveKBPerSecond, sentKBPerSecond, avgPageBytes, threadCount, resultId) "); 30 | batchInsertSQL.append("VALUES"); 31 | for (int index = 0; index < sampleResultInfoList.size(); ++index) { 32 | // 这里有一个坑,不能直接使用new Object[]{index},否则MessageFormat会将下标以科学计数法的方式输出,导致DB无法存储 33 | batchInsertSQL.append(messageFormat.format(new Object[]{String.valueOf(index)})); 34 | if (index < sampleResultInfoList.size() - 1) 35 | batchInsertSQL.append(","); 36 | } 37 | return batchInsertSQL.toString(); 38 | } 39 | } 40 | -------------------------------------------------------------------------------- /src/main/java/com/berserker/architecture/mapper/ScenarioInfoMapper.java: -------------------------------------------------------------------------------- 1 | package com.berserker.architecture.mapper; 2 | 3 | import com.berserker.architecture.domain.entity.ScenarioInfo; 4 | import org.apache.ibatis.annotations.*; 5 | 6 | import java.util.List; 7 | 8 | public interface ScenarioInfoMapper { 9 | @Select("SELECT COUNT(*) FROM scenarioInfo") 10 | Integer getScenarioCount(); 11 | 12 | @Select("SELECT * FROM scenarioInfo WHERE id = #{id}") 13 | ScenarioInfo getScenarioInfo(@Param("id") Integer id); 14 | 15 | @SelectProvider(type = ScenarioInfoProvider.class, method = "selectScenarioByCondition") 16 | List getScenarioInfoList(@Param("scenarioName") String scenarioName); 17 | 18 | @InsertProvider(type = ScenarioInfoProvider.class, method = "insertScenario") 19 | @Options(useGeneratedKeys = true, keyProperty = "id") 20 | int insertScenarioInfo(ScenarioInfo scenarioInfo); 21 | 22 | @UpdateProvider(type = ScenarioInfoProvider.class, method = "updateScenario") 23 | @Options(useGeneratedKeys = true, keyProperty = "id") 24 | int updateScenarioInfo(ScenarioInfo scenarioInfo); 25 | 26 | @DeleteProvider(type = ScenarioInfoProvider.class, method = "deleteScenario") 27 | int deleteScenarioInfo(Integer id); 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/berserker/architecture/mapper/ScenarioInfoProvider.java: -------------------------------------------------------------------------------- 1 | package com.berserker.architecture.mapper; 2 | 3 | import com.berserker.architecture.domain.entity.ScenarioInfo; 4 | import com.google.common.base.Strings; 5 | import org.apache.commons.lang3.StringUtils; 6 | import org.apache.ibatis.jdbc.SQL; 7 | 8 | import java.lang.reflect.Field; 9 | import java.text.SimpleDateFormat; 10 | import java.util.Date; 11 | import java.util.Map; 12 | import java.util.Objects; 13 | 14 | public class ScenarioInfoProvider { 15 | public String selectScenarioByCondition(Map params) { 16 | return new SQL() { 17 | StringBuffer conditionScenarioName = new StringBuffer(); 18 | StringBuffer conditionStatus = new StringBuffer(); 19 | String scenarioNameSQL = null; 20 | { 21 | if (Objects.nonNull(params.get("scenarioName"))) { 22 | String scenarioName = params.get("scenarioName").toString(); 23 | if (StringUtils.isNotEmpty(scenarioName)) { 24 | conditionScenarioName.append("scenarioName LIKE '%"); 25 | conditionScenarioName.append(params.get("scenarioName")); 26 | conditionScenarioName.append("%' "); 27 | } 28 | } 29 | scenarioNameSQL = conditionScenarioName.toString(); 30 | 31 | // 拼凑SQL语句 32 | SELECT("id, scenarioName, scenarioDescription, createTime, numThreads, rampUp, duration"); 33 | FROM("scenarioInfo"); 34 | if (!Strings.isNullOrEmpty(scenarioNameSQL)) 35 | WHERE(scenarioNameSQL); 36 | ORDER_BY("createTime"); 37 | } 38 | }.toString(); 39 | } 40 | 41 | private SQL assembleColumnSQL(SQL sqlColumn, ScenarioInfo scenarioInfo, String statementCTRL) { 42 | StringBuilder column = null; 43 | for (Field field : scenarioInfo.getClass().getDeclaredFields()) { 44 | field.setAccessible(true); 45 | try { 46 | if (Objects.nonNull(field.get(scenarioInfo)) && StringUtils.isNotEmpty(field.get(scenarioInfo).toString())) { 47 | column = new StringBuilder(); 48 | Object columnValue = field.get(scenarioInfo); 49 | if (Objects.equals(statementCTRL, "INSERT")) { 50 | if (columnValue instanceof Integer) 51 | column.append(((Integer) columnValue).toString()); 52 | else if (columnValue instanceof Date) 53 | column.append("'").append(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format((Date) columnValue)).append("'"); 54 | else if (columnValue instanceof String) 55 | column.append("'").append(columnValue).append("'"); 56 | sqlColumn.VALUES(field.getName(), column.toString()); 57 | } 58 | if (Objects.equals(statementCTRL, "UPDATE")) { 59 | if (columnValue instanceof Integer) 60 | column.append(field.getName()).append(" = ").append(columnValue); 61 | else if (columnValue instanceof Date) 62 | column.append(field.getName()).append(" = '").append(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss").format((Date) columnValue)).append("'"); 63 | else if (columnValue instanceof String) 64 | column.append(field.getName()).append(" = '").append(columnValue).append("'"); 65 | sqlColumn.SET(column.toString()); 66 | } 67 | } 68 | } catch (IllegalAccessException e) { 69 | e.printStackTrace(); 70 | } 71 | } 72 | return sqlColumn; 73 | } 74 | 75 | public String insertScenario(ScenarioInfo scenarioInfo) { 76 | return new SQL() { 77 | { 78 | INSERT_INTO("scenarioInfo"); 79 | assembleColumnSQL(this, scenarioInfo, "INSERT"); 80 | } 81 | }.toString(); 82 | } 83 | 84 | public String updateScenario(ScenarioInfo scenarioInfo) { 85 | return new SQL() { 86 | { 87 | UPDATE("scenarioInfo"); 88 | assembleColumnSQL(this, scenarioInfo, "UPDATE"); 89 | if (Objects.nonNull(scenarioInfo.getId()) && StringUtils.isNotEmpty(scenarioInfo.getId().toString())) 90 | WHERE("id = " + scenarioInfo.getId().toString()); 91 | } 92 | }.toString(); 93 | } 94 | 95 | public String deleteScenario(Integer id) { 96 | return new SQL() { 97 | { 98 | DELETE_FROM("scenarioInfo"); 99 | WHERE("id = " + id); 100 | } 101 | }.toString(); 102 | } 103 | } 104 | -------------------------------------------------------------------------------- /src/main/java/com/berserker/architecture/mapper/ScenarioResultInfoMapper.java: -------------------------------------------------------------------------------- 1 | package com.berserker.architecture.mapper; 2 | 3 | import com.berserker.architecture.domain.entity.ScenarioResultInfo; 4 | import org.apache.ibatis.annotations.*; 5 | 6 | import java.util.List; 7 | 8 | public interface ScenarioResultInfoMapper { 9 | @Select("SELECT * FROM scenarioresultinfo WHERE id = #{id}") 10 | ScenarioResultInfo getScenarioResultInfo(@Param("id") Integer id); 11 | 12 | @Select("SELECT * FROM scenarioresultinfo WHERE scenarioId = #{scenarioId} ORDER BY runTime DESC") 13 | List getScenarioResultInfoListByScenarioId(@Param("scenarioId") Integer scenarioId); 14 | 15 | @Select("SELECT * FROM scenarioresultinfo ORDER BY runTime DESC") 16 | List getScenarioResultInfoList(); 17 | 18 | @Insert("INSERT INTO scenarioresultinfo(scenarioName, numThreads, rampUp, duration, runTime, scenarioId)" + 19 | "VALUES(#{scenarioName}, #{numThreads}, #{rampUp}, #{duration}, #{runTime}, #{scenarioId})") 20 | @Options(useGeneratedKeys = true, keyProperty = "id") 21 | int insertScenarioResultInfo(ScenarioResultInfo scenarioResultInfo); 22 | 23 | @Delete("DELETE FROM scenarioresultinfo WHERE id = #{id}") 24 | int deleteScenarioResultInfo(@Param("id") Integer id); 25 | 26 | @Delete("DELETE FROM scenarioresultinfo WHERE scenarioId = #{scenarioId}") 27 | int deleteScenarioResultInfoByScenarioId(@Param("scenarioId") Integer scenarioId); 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/berserker/architecture/mapper/ScriptFileInfoMapper.java: -------------------------------------------------------------------------------- 1 | package com.berserker.architecture.mapper; 2 | 3 | import com.berserker.architecture.domain.entity.ScriptFileInfo; 4 | import org.apache.ibatis.annotations.*; 5 | 6 | public interface ScriptFileInfoMapper { 7 | @Select("SELECT COUNT(*) FROM scriptfileinfo") 8 | Integer getScriptFileInfoCount(); 9 | 10 | @Select("SELECT * FROM scriptfileinfo WHERE id = #{id}") 11 | ScriptFileInfo getScriptFileInfo(@Param("id") Integer id); 12 | 13 | @Select("SELECT * FROM scriptfileinfo WHERE scenarioId = #{scenarioId}") 14 | ScriptFileInfo getScriptFileInfoByScenarioId(@Param("scenarioId") Integer scenarioId); 15 | 16 | @Insert("INSERT INTO scriptfileinfo(scriptFileName, scriptFilePath, uploadTime, scenarioId)" + 17 | "VALUES(#{scriptFileName}, #{scriptFilePath}, #{uploadTime}, #{scenarioId})") 18 | @Options(useGeneratedKeys = true, keyProperty = "id") 19 | int insertScriptFileInfo(ScriptFileInfo scriptFileInfo); 20 | 21 | @Update("UPDATE scriptfileinfo " + 22 | "SET scriptFileName = #{scriptFileName}, scriptFilePath = #{scriptFilePath}, uploadTime = #{uploadTime}, scenarioId = #{scenarioId} " + 23 | "WHERE id = #{id}") 24 | int modifyScriptFileInfo(ScriptFileInfo scriptFileInfo); 25 | 26 | @Delete("DELETE FROM scriptfileinfo WHERE id = #{id}") 27 | int deleteScriptFileInfo(@Param("id") Integer id); 28 | 29 | @Delete("DELETE FROM scriptfileinfo WHERE scenarioId = #{scenarioId}") 30 | int deleteScriptFileInfoByScenarioId(@Param("scenarioId") Integer scenarioId); 31 | } 32 | -------------------------------------------------------------------------------- /src/main/java/com/berserker/architecture/service/ParamFileService.java: -------------------------------------------------------------------------------- 1 | package com.berserker.architecture.service; 2 | 3 | import com.berserker.architecture.domain.entity.ParamFileInfo; 4 | 5 | import java.util.List; 6 | import java.util.Map; 7 | 8 | public interface ParamFileService { 9 | ParamFileInfo getParamFileInfo(Integer id); 10 | 11 | List getParamFileInfoListByScenarioId(Integer scenarioId); 12 | 13 | Map addParamFileInfo(Integer scenarioId, List> csvDataParamFileList); 14 | 15 | Map delParamFileInfo(ParamFileInfo paramFileInfo); 16 | 17 | List> delParamFileInfoByScenarioId(Integer scenarioId); 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/berserker/architecture/service/SampleResultService.java: -------------------------------------------------------------------------------- 1 | package com.berserker.architecture.service; 2 | 3 | import com.berserker.architecture.domain.entity.SampleResultInfo; 4 | import org.springframework.data.redis.core.BoundListOperations; 5 | 6 | import java.util.List; 7 | import java.util.Map; 8 | 9 | public interface SampleResultService { 10 | SampleResultInfo getSampleResultInfo(Integer id); 11 | 12 | Map>> getSampleResultDataListByResultId(Integer resultId, String dataType); 13 | 14 | List getSampleResultData(Integer resultId); 15 | 16 | void addSampleResultToDB(Integer runningResultId, BoundListOperations runningSampleResultList); 17 | 18 | Map delSampleResultInfoByResultId(Integer resultId); 19 | } 20 | -------------------------------------------------------------------------------- /src/main/java/com/berserker/architecture/service/ScenarioInfoService.java: -------------------------------------------------------------------------------- 1 | package com.berserker.architecture.service; 2 | 3 | import com.berserker.architecture.domain.entity.ScenarioInfo; 4 | import com.berserker.architecture.domain.entity.ScriptFileInfo; 5 | import org.springframework.web.multipart.MultipartFile; 6 | 7 | import java.util.List; 8 | import java.util.Map; 9 | 10 | public interface ScenarioInfoService { 11 | ScenarioInfo getScenarioInfo(Integer id); 12 | 13 | Map getScenarioInfoByPageList(Integer offset, Integer limit, String scenarioName); 14 | 15 | ScriptFileInfo getScriptFileInfoByScenarioId(Integer scenarioId); 16 | 17 | List> getCsvDataSetSlotList(Integer scenarioId); 18 | 19 | Map getScriptDataStructure(Integer scenarioId); 20 | 21 | Map addScenarioInfo(ScenarioInfo scenarioInfo, MultipartFile scriptFile); 22 | 23 | Map addScenarioParamFiles(Integer scenarioId, List> csvDataParamFileList); 24 | 25 | Map modScenarioInfo(ScenarioInfo scenarioInfo, MultipartFile scriptFile); 26 | 27 | Map delScenarioInfo(Integer id); 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/berserker/architecture/service/ScenarioResultService.java: -------------------------------------------------------------------------------- 1 | package com.berserker.architecture.service; 2 | 3 | import com.berserker.architecture.domain.entity.ScenarioResultInfo; 4 | import org.springframework.stereotype.Service; 5 | 6 | import java.util.Map; 7 | 8 | @Service 9 | public interface ScenarioResultService { 10 | ScenarioResultInfo getScenarioResultInfo(Integer id); 11 | 12 | Map getScenarioResultInfoByScenarioId(Integer offset, Integer limit, Integer scenarioId); 13 | 14 | Map getScenarioResultInfoList(Integer offset, Integer limit); 15 | 16 | Map addScenarioResultInfo(ScenarioResultInfo scenarioResultInfo); 17 | 18 | Map delScenarioResultInfoByResultId(Integer resultId); 19 | 20 | Map delScenarioResultInfoByScenarioId(Integer scenarioId); 21 | } 22 | -------------------------------------------------------------------------------- /src/main/java/com/berserker/architecture/service/ScenarioRunService.java: -------------------------------------------------------------------------------- 1 | package com.berserker.architecture.service; 2 | 3 | import java.util.Map; 4 | 5 | public interface ScenarioRunService { 6 | void scenarioStartRun(Integer scenarioId); 7 | 8 | Map scenarioStopRun(); 9 | 10 | Boolean getEngineIsActive(); 11 | 12 | Integer getRunningScenarioId(); 13 | 14 | String getRunningScenarioName(); 15 | 16 | void scenarioSampleResultRealOuter(); 17 | } 18 | -------------------------------------------------------------------------------- /src/main/java/com/berserker/architecture/service/ScriptInfoService.java: -------------------------------------------------------------------------------- 1 | package com.berserker.architecture.service; 2 | 3 | import com.berserker.architecture.domain.entity.ScriptFileInfo; 4 | import org.apache.jorphan.collections.HashTree; 5 | import org.springframework.web.multipart.MultipartFile; 6 | 7 | import java.util.List; 8 | import java.util.Map; 9 | 10 | public interface ScriptInfoService { 11 | ScriptFileInfo getScriptFileInfo(Integer id); 12 | 13 | HashTree getTestPlanTreeByScenarioId(Integer scenarioId); 14 | 15 | ScriptFileInfo getScriptFileInfoByScenarioId(Integer scenarioId); 16 | 17 | Map getScriptDataStructure(Integer scenarioId); 18 | 19 | List> getCsvDataSetSlotList(Integer scenarioId); 20 | 21 | Map addScriptInfo(MultipartFile scriptFile, Integer addScenarioId); 22 | 23 | Map modScriptInfo(MultipartFile scriptFileUpload, Integer modScenarioId); 24 | 25 | Map delScriptInfo(ScriptFileInfo scriptFileInfo); 26 | 27 | Map delScriptInfoByScenarioId(Integer scenarioId); 28 | } 29 | -------------------------------------------------------------------------------- /src/main/java/com/berserker/architecture/service/impl/SampleResultServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.berserker.architecture.service.impl; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.berserker.architecture.mapper.SampleResultInfoMapper; 5 | import com.berserker.architecture.domain.entity.SampleResultInfo; 6 | import com.berserker.architecture.service.SampleResultService; 7 | import org.slf4j.Logger; 8 | import org.slf4j.LoggerFactory; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.data.redis.core.BoundListOperations; 11 | import org.springframework.stereotype.Service; 12 | 13 | import java.util.*; 14 | 15 | @Service 16 | public class SampleResultServiceImpl implements SampleResultService{ 17 | private static final Logger log = LoggerFactory.getLogger(SampleResultServiceImpl.class); 18 | @Autowired 19 | private SampleResultInfoMapper sampleMapper; 20 | 21 | @Override 22 | public SampleResultInfo getSampleResultInfo(Integer id) { 23 | SampleResultInfo sampleResultInfo = sampleMapper.getSampleResultInfo(id); 24 | if (Objects.nonNull(sampleResultInfo)) 25 | return sampleResultInfo; 26 | else 27 | return null; 28 | } 29 | 30 | @Override 31 | public Map>> getSampleResultDataListByResultId(Integer resultId, String dataType) { 32 | List sampleResultInfoList = sampleMapper.getSampleResultInfoListByResultId(resultId); 33 | 34 | Map>> sampleResultContainer = new HashMap<>(); 35 | if (Objects.nonNull(sampleResultInfoList)) { 36 | for (SampleResultInfo resultInfo : sampleResultInfoList) { 37 | String sampleLabel = resultInfo.getSamplerLabel(); 38 | List> series = null; 39 | List seriesElement = null; 40 | if (Objects.isNull(sampleResultContainer.get(sampleLabel))) { 41 | series = new ArrayList<>(); 42 | seriesElement = this.createSeriesElement(resultInfo, dataType); 43 | series.add(seriesElement); 44 | sampleResultContainer.put(sampleLabel, series); 45 | } else { 46 | seriesElement = this.createSeriesElement(resultInfo, dataType); 47 | sampleResultContainer.get(sampleLabel).add(seriesElement); 48 | } 49 | } 50 | } 51 | return sampleResultContainer; 52 | } 53 | 54 | private List createSeriesElement(SampleResultInfo resultInfo, String dataType) { 55 | List seriesElement = new ArrayList<>(); 56 | Long timestamp = resultInfo.getTimeStamp().getTime(); 57 | seriesElement.add(timestamp); 58 | if (Objects.equals(dataType, "meanTime")) 59 | seriesElement.add(resultInfo.getMeanTime()); 60 | if (Objects.equals(dataType, "requestRate")) 61 | seriesElement.add(resultInfo.getRequestRate()); 62 | if (Objects.equals(dataType, "errorPercentage")) 63 | seriesElement.add(resultInfo.getErrorPercentage()); 64 | if (Objects.equals(dataType, "threadCount")) 65 | seriesElement.add(resultInfo.getThreadCount()); 66 | if (Objects.equals(dataType, "receiveKBPerSecond")) 67 | seriesElement.add(resultInfo.getReceiveKBPerSecond()); 68 | if (Objects.equals(dataType, "sentKBPerSecond")) 69 | seriesElement.add(resultInfo.getSentKBPerSecond()); 70 | return seriesElement; 71 | } 72 | 73 | /** 74 | * @description 根据resultId获取场景的SampleResultList,提供给前端显示趋势图. 75 | * @param resultId 场景结果ID 76 | * @return 返回给前端的列表 77 | */ 78 | @Override 79 | public List getSampleResultData(Integer resultId) { 80 | List samplerLabelList = sampleMapper.getSamplerLabelByResultId(resultId); 81 | List sampleResultData = new ArrayList<>(); 82 | for (String samplerLabel: samplerLabelList) { 83 | SampleResultInfo resultInfo = sampleMapper.getSampleResultData(resultId, samplerLabel); 84 | sampleResultData.add(resultInfo); 85 | } 86 | return sampleResultData; 87 | } 88 | 89 | /** 90 | * @description 将所有SampleResult写入到DB中 91 | * @param runningResultId 场景结果ID 92 | * @param runningSampleResultList 从Redis中取出的SampleResultList 93 | */ 94 | @Override 95 | public void addSampleResultToDB(Integer runningResultId, BoundListOperations runningSampleResultList) { 96 | List sampleResultInfoList = new ArrayList<>(); 97 | // 对Redis中存放的SampleResultList进行迭代 98 | while (runningSampleResultList.size() > 0) { 99 | // 从Redis中获取每个SampleResult的字符串 100 | String sampleResultString = runningSampleResultList.leftPop(); 101 | // 将获取的JSON字符串转换成SampleResult对象 102 | SampleResultInfo sampleResultInfo = JSONObject.parseObject(sampleResultString, SampleResultInfo.class); 103 | sampleResultInfo.setResultId(runningResultId); 104 | sampleResultInfoList.add(sampleResultInfo); 105 | } 106 | // 将采样结果写入到DB 107 | sampleMapper.insertSampleResultInfoList(sampleResultInfoList); 108 | } 109 | 110 | @Override 111 | public Map delSampleResultInfoByResultId(Integer resultId) { 112 | Map result = new HashMap<>(); 113 | Integer deleteFlag = sampleMapper.deleteSampleResultInfoByResultId(resultId); 114 | if (deleteFlag > 0) { 115 | result.put("status", "Success"); 116 | result.put("message", "采样结果删除成功"); 117 | } else { 118 | result.put("status", "Fail"); 119 | result.put("message", "采样结果删除失败"); 120 | } 121 | return result; 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/main/java/com/berserker/architecture/service/impl/ScenarioResultServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.berserker.architecture.service.impl; 2 | 3 | import com.berserker.architecture.mapper.SampleResultInfoMapper; 4 | import com.berserker.architecture.domain.entity.ScenarioResultInfo; 5 | import com.berserker.architecture.mapper.ScenarioResultInfoMapper; 6 | import com.berserker.architecture.service.ScenarioResultService; 7 | import com.github.pagehelper.Page; 8 | import com.github.pagehelper.PageHelper; 9 | import org.springframework.beans.factory.annotation.Autowired; 10 | import org.springframework.stereotype.Service; 11 | 12 | import java.util.*; 13 | 14 | @Service 15 | public class ScenarioResultServiceImpl implements ScenarioResultService{ 16 | @Autowired 17 | private ScenarioResultInfoMapper resultMapper; 18 | @Autowired 19 | private SampleResultInfoMapper sampleMapper; 20 | 21 | @Override 22 | public ScenarioResultInfo getScenarioResultInfo(Integer id) { 23 | ScenarioResultInfo scenarioResultInfo = resultMapper.getScenarioResultInfo(id); 24 | if (Objects.nonNull(scenarioResultInfo)) 25 | return scenarioResultInfo; 26 | else 27 | return null; 28 | } 29 | 30 | @Override 31 | public Map getScenarioResultInfoByScenarioId(Integer offset, Integer limit, Integer scenarioId) { 32 | Map result = new HashMap<>(); 33 | Page page = PageHelper.offsetPage(offset, limit); 34 | List scenarioResultInfoList = resultMapper.getScenarioResultInfoListByScenarioId(scenarioId); 35 | result.put("total", page.getTotal()); 36 | result.put("rows", scenarioResultInfoList); 37 | return result; 38 | } 39 | 40 | @Override 41 | public Map getScenarioResultInfoList(Integer offset, Integer limit) { 42 | Map result = new HashMap<>(); 43 | Page page = PageHelper.offsetPage(offset, limit); 44 | List scenarioResultInfoList = resultMapper.getScenarioResultInfoList(); 45 | result.put("total", page.getTotal()); 46 | result.put("rows", scenarioResultInfoList); 47 | return result; 48 | } 49 | 50 | @Override 51 | public Map addScenarioResultInfo(ScenarioResultInfo scenarioResultInfo) { 52 | Map result = new HashMap<>(); 53 | int insertFlag = resultMapper.insertScenarioResultInfo(scenarioResultInfo); 54 | if (insertFlag > 0) { 55 | result.put("status", "Success"); 56 | result.put("message", "结果集建立成功"); 57 | result.put("resultId", scenarioResultInfo.getId()); 58 | } else { 59 | result.put("status", "Fail"); 60 | result.put("message", "结果集建立失败"); 61 | } 62 | return result; 63 | } 64 | 65 | @Override 66 | public Map delScenarioResultInfoByResultId(Integer resultId) { 67 | Map result = new HashMap<>(); 68 | int deleteSampleResultFlag = sampleMapper.deleteSampleResultInfoByResultId(resultId); 69 | if (deleteSampleResultFlag >= 0) { 70 | int deleteFlag = resultMapper.deleteScenarioResultInfo(resultId); 71 | if (deleteFlag >= 0) { 72 | result.put("status", "Success"); 73 | result.put("message", "结果集删除成功"); 74 | } else { 75 | result.put("status", "Fail"); 76 | result.put("message", "结果集删除失败"); 77 | } 78 | } else { 79 | result.put("status", "Fail"); 80 | result.put("message", "结果详情删除失败"); 81 | } 82 | return result; 83 | } 84 | 85 | @Override 86 | public Map delScenarioResultInfoByScenarioId(Integer scenarioId) { 87 | Integer successCount = 0; 88 | Integer failCount = 0; 89 | Map result = new HashMap<>(); 90 | List ScenarioResultInfoList = resultMapper.getScenarioResultInfoListByScenarioId(scenarioId); 91 | for (ScenarioResultInfo resultInfo: ScenarioResultInfoList) { 92 | Map delScenarioResult = delScenarioResultInfoByResultId(resultInfo.getId()); 93 | String status = delScenarioResult.get("status").toString(); 94 | if (Objects.equals(status, "Success")) { 95 | result.put("successCount", ++successCount); 96 | } else { 97 | result.put("failCount", ++failCount); 98 | } 99 | } 100 | return result; 101 | } 102 | } 103 | -------------------------------------------------------------------------------- /src/main/java/com/berserker/architecture/service/impl/ScenarioRunServiceImpl.java: -------------------------------------------------------------------------------- 1 | package com.berserker.architecture.service.impl; 2 | 3 | import com.berserker.architecture.engine.core.EngineController; 4 | import com.berserker.architecture.engine.core.EngineResultHandler; 5 | import com.berserker.architecture.service.ScenarioInfoService; 6 | import com.berserker.architecture.domain.entity.ScenarioInfo; 7 | import com.berserker.architecture.domain.entity.ScenarioResultInfo; 8 | import com.berserker.architecture.service.SampleResultService; 9 | import com.berserker.architecture.service.ScenarioResultService; 10 | import com.berserker.architecture.service.ScenarioRunService; 11 | import org.springframework.beans.factory.annotation.Autowired; 12 | import org.springframework.data.redis.core.BoundListOperations; 13 | import org.springframework.scheduling.annotation.Async; 14 | import org.springframework.stereotype.Service; 15 | 16 | import java.util.Date; 17 | import java.util.Map; 18 | import java.util.Objects; 19 | 20 | @Service 21 | public class ScenarioRunServiceImpl implements ScenarioRunService{ 22 | @Autowired 23 | private EngineController engineController; 24 | @Autowired 25 | private EngineResultHandler resultHandler; 26 | @Autowired 27 | private ScenarioInfoService scenarioInfoService; 28 | @Autowired 29 | private ScenarioResultService scenarioResultService; 30 | @Autowired 31 | private SampleResultService sampleResultService; 32 | 33 | /** 34 | * @description 启动场景测试的方法,在启动测试后生成一条测试结果记录,存入ScenarioResultInfo表中. 35 | * @param scenarioId 场景ID 36 | */ 37 | @Async 38 | @Override 39 | public void scenarioStartRun(Integer scenarioId) { 40 | // 启动场景前先获取场景信息 41 | ScenarioInfo scenarioInfo = scenarioInfoService.getScenarioInfo(scenarioId); 42 | // 初始化结果信息 43 | ScenarioResultInfo scenarioResultInfo = new ScenarioResultInfo(); 44 | scenarioResultInfo.setRunTime(new Date()); 45 | scenarioResultInfo.setNumThreads(scenarioInfo.getNumThreads()); 46 | scenarioResultInfo.setRampUp(scenarioInfo.getRampUp()); 47 | scenarioResultInfo.setDuration(scenarioInfo.getDuration()); 48 | scenarioResultInfo.setScenarioName(scenarioInfo.getScenarioName()); 49 | scenarioResultInfo.setScenarioId(scenarioInfo.getId()); 50 | // 在库中添加结果信息 51 | Map addResult = scenarioResultService.addScenarioResultInfo(scenarioResultInfo); 52 | // 保留结果ID,将结果ID放置到EngineController中,方便后面的结果写入DB 53 | Integer resultId = Integer.parseInt(addResult.get("resultId").toString()); 54 | // 在EngineController中保存结果ID 55 | engineController.setRunningResultId(resultId); 56 | // 启动场景测试 57 | engineController.engineScenarioRunner(scenarioId); 58 | // 获取运行完成之后SampleResultList 59 | BoundListOperations runningSampleResultList = engineController.getRunningSampleResultList(); 60 | // 将SamplerResultList中的内容写入到DB中 61 | sampleResultService.addSampleResultToDB(resultId, runningSampleResultList); 62 | } 63 | 64 | /** 65 | * @description 手动终止场景 66 | * @return 返回终止信息 67 | */ 68 | @Override 69 | public Map scenarioStopRun() { 70 | // 停止场景运行 71 | Map result = engineController.stopEngine(); 72 | if (Objects.equals(result.get("status"), "True")) { 73 | // 清空当前的计数器 74 | resultHandler.clearCalculator(); 75 | // 获取当前的SampleResultList,交给EngineController中的runningSampleResultList. 76 | // 由于场景是手动中止的,不能直接从EngineController中直接获取,要从EngineResultHandler中获取. 77 | // 从EngineResultHandler中获取的是已经存入数据队列 78 | engineController.setRunningSampleResultList(resultHandler.getRunningSampleResultList()); 79 | // 获取运行完成之后SampleResultList 80 | BoundListOperations runningSampleResultList = engineController.getRunningSampleResultList(); 81 | // 从EngineController中获取结果ID 82 | Integer resultId = engineController.getRunningResultId(); 83 | // 将SamplerResultList中的内容写入到DB中 84 | sampleResultService.addSampleResultToDB(resultId, runningSampleResultList); 85 | } 86 | return result; 87 | } 88 | 89 | /** 90 | * @description 获取当前测试状态 91 | * @return 是否正在运行 92 | */ 93 | @Override 94 | public Boolean getEngineIsActive() { 95 | return engineController.getEngineStatus(); 96 | } 97 | 98 | /** 99 | * @description 获取当前场景ID 100 | * @return 场景ID 101 | */ 102 | @Override 103 | public Integer getRunningScenarioId() { 104 | return engineController.getRunningScenarioId(); 105 | } 106 | 107 | /** 108 | * @description 获取当前场景的名称 109 | * @return 场景名称 110 | */ 111 | @Override 112 | public String getRunningScenarioName() { 113 | return engineController.getRunningScenarioName(); 114 | } 115 | 116 | /** 117 | * @description SampleResult的实时输出 118 | */ 119 | @Override 120 | public void scenarioSampleResultRealOuter() { 121 | engineController.engineScenarioRealOuter(); 122 | } 123 | } 124 | -------------------------------------------------------------------------------- /src/main/java/com/berserker/architecture/utils/CommonUtil.java: -------------------------------------------------------------------------------- 1 | package com.berserker.architecture.utils; 2 | 3 | import org.apache.commons.lang3.StringUtils; 4 | import org.springframework.validation.BindingResult; 5 | import org.springframework.validation.ObjectError; 6 | 7 | import java.text.ParseException; 8 | import java.text.SimpleDateFormat; 9 | import java.util.*; 10 | 11 | public class CommonUtil { 12 | public static Map> paramsValidator(BindingResult bindingResult) { 13 | if (bindingResult.hasErrors()) { 14 | Map> errorResult = new HashMap<>(); 15 | List errorMsgList = new ArrayList<>(); 16 | for (ObjectError objectError: bindingResult.getAllErrors()) 17 | errorMsgList.add(objectError.getDefaultMessage()); 18 | errorResult.put("paramErrors", errorMsgList); 19 | return errorResult; 20 | } else { 21 | return null; 22 | } 23 | } 24 | 25 | public static Date getDateTimeValue(String dateTime) { 26 | Date dateTimeValue = null; 27 | SimpleDateFormat sdf = new SimpleDateFormat("yyyy-MM-dd HH:mm:ss"); 28 | try { 29 | dateTimeValue = sdf.parse(dateTime); 30 | } catch (ParseException e) { 31 | e.printStackTrace(); 32 | } 33 | return dateTimeValue; 34 | } 35 | 36 | public static Boolean getBooleanValue(String status) { 37 | Boolean statusValue = null; 38 | if (Objects.nonNull(status) && StringUtils.isNumeric(status)) { 39 | if (Objects.equals(status, "1")) 40 | statusValue = Boolean.TRUE; 41 | else 42 | statusValue = Boolean.FALSE; 43 | } else { 44 | statusValue = null; 45 | } 46 | return statusValue; 47 | } 48 | } 49 | -------------------------------------------------------------------------------- /src/main/java/com/berserker/architecture/utils/ScriptUtil.java: -------------------------------------------------------------------------------- 1 | package com.berserker.architecture.utils; 2 | 3 | import com.berserker.architecture.domain.entity.ScriptFileInfo; 4 | 5 | import com.berserker.architecture.mapper.ScriptFileInfoMapper; 6 | import org.apache.commons.lang3.StringUtils; 7 | import org.springframework.beans.factory.annotation.Autowired; 8 | import org.springframework.stereotype.Component; 9 | 10 | import java.io.File; 11 | import java.net.URI; 12 | import java.nio.file.Paths; 13 | 14 | @Component 15 | public class ScriptUtil { 16 | @Autowired 17 | private ScriptFileInfoMapper scriptMapper; 18 | 19 | /** 20 | * @description 通过场景ID获取脚本文件的方法 21 | * @param scenarioId 场景ID 22 | * @return 脚本对象 23 | */ 24 | public File getScriptFileByScenarioId(Integer scenarioId) { 25 | ScriptFileInfo scriptInfo = scriptMapper.getScriptFileInfoByScenarioId(scenarioId); 26 | String scriptFileFolder = scriptInfo.getScriptFilePath(); 27 | String scriptFileName = scriptInfo.getScriptFileName(); 28 | 29 | String scriptFullPath = StringUtils.join(scriptFileFolder, scriptFileName); 30 | URI scriptFullURI = URI.create(scriptFullPath); 31 | return Paths.get(scriptFullURI).toFile(); 32 | } 33 | } 34 | -------------------------------------------------------------------------------- /src/main/resources/application.yml: -------------------------------------------------------------------------------- 1 | spring: 2 | datasource: 3 | type: com.alibaba.druid.pool.DruidDataSource 4 | url: jdbc:mysql://localhost:3306/berserker?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=UTF-8&useSSL=false 5 | username: root 6 | password: 123456 7 | driver-class-name: com.mysql.cj.jdbc.Driver 8 | platform: mysql 9 | mvc: 10 | static-path-pattern: /** 11 | resources: 12 | static-locations: classpath:/static/ 13 | http: 14 | multipart: 15 | enabled: true 16 | max-file-size: 16MB 17 | max-request-size: 24MB 18 | encoding: 19 | charset: UTF-8 20 | enabled: true 21 | force: true 22 | redis: 23 | database: 0 24 | host: localhost 25 | port: 6379 26 | pool: 27 | max-active: 8 28 | max-idle: 8 29 | max-wait: -1 30 | min-idle: 0 31 | timeout: 0 32 | thymeleaf: 33 | cache: false 34 | check-template: true 35 | check-template-location: true 36 | enabled: true 37 | encoding: UTF-8 38 | prefix: classpath:/templates/ 39 | suffix: .html 40 | content-type: text/html 41 | mode: HTML 42 | mybatis: 43 | configuration: 44 | log-impl: org.apache.ibatis.logging.stdout.StdOutImpl 45 | pagehelper: 46 | helperDialect: mysql 47 | reasonable: true 48 | supportMethodsArguments: true 49 | params: count=countSql 50 | server: 51 | tomcat: 52 | uri-encoding: UTF-8 53 | jmeterSetting: 54 | jmeter-home: D:\Apache-jmeter-4.0 55 | jmeter-properties: D:\Apache-jmeter-4.0\bin\jmeter.properties 56 | fileStorage: 57 | script-folder-path: D:\ScriptStorage 58 | -------------------------------------------------------------------------------- /src/main/resources/application.yml.prod: -------------------------------------------------------------------------------- 1 | spring: 2 | datasource: 3 | type: com.alibaba.druid.pool.DruidDataSource 4 | url: jdbc:mysql://localhost:3306/bushmaster?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=UTF-8&useSSL=false 5 | username: root 6 | password: 123456 7 | driver-class-name: com.mysql.cj.jdbc.Driver 8 | platform: mysql 9 | mvc: 10 | static-path-pattern: /** 11 | resources: 12 | static-locations: classpath:/static/ 13 | http: 14 | multipart: 15 | enabled: true 16 | max-file-size: 16MB 17 | max-request-size: 24MB 18 | encoding: 19 | charset: UTF-8 20 | enabled: true 21 | force: true 22 | redis: 23 | database: 0 24 | host: localhost 25 | port: 6379 26 | pool: 27 | max-active: 8 28 | max-idle: 8 29 | max-wait: -1 30 | min-idle: 0 31 | timeout: 0 32 | thymeleaf: 33 | cache: false 34 | check-template: true 35 | check-template-location: true 36 | enabled: true 37 | encoding: UTF-8 38 | prefix: classpath:/templates/ 39 | suffix: .html 40 | content-type: text/html 41 | mode: HTML 42 | mybatis: 43 | configuration: 44 | log-impl: org.apache.ibatis.logging.stdout.StdOutImpl 45 | pagehelper: 46 | helperDialect: mysql 47 | reasonable: true 48 | supportMethodsArguments: true 49 | params: count=countSql 50 | server: 51 | tomcat: 52 | uri-encoding: UTF-8 53 | jmeterSetting: 54 | jmeter-home: C:\Apache-jmeter-4.0 55 | jmeter-properties: C:\Apache-jmeter-4.0\bin\jmeter.properties 56 | fileStorage: 57 | script-folder-path: C:\BushmasterScriptStorage 58 | -------------------------------------------------------------------------------- /src/main/resources/logback.xml: -------------------------------------------------------------------------------- 1 | 2 | 3 | 4 | 5 | 6 | 7 | %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | log/%d{yyyy-MM-dd}.log 16 | 17 | 18 | 19 | 30 20 | 21 | 22 | 23 | %d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{50} - %msg%n 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | -------------------------------------------------------------------------------- /src/main/resources/static/css/bootstrap-treeview.min.css: -------------------------------------------------------------------------------- 1 | .treeview .list-group-item { 2 | cursor: pointer 3 | } 4 | 5 | .treeview span.indent { 6 | margin-left: 10px; 7 | margin-right: 10px 8 | } 9 | 10 | .treeview span.icon { 11 | width: 12px; 12 | margin-right: 5px 13 | } 14 | 15 | .treeview .node-disabled { 16 | color: silver; 17 | cursor: not-allowed 18 | } -------------------------------------------------------------------------------- /src/main/resources/static/css/sb-admin.css: -------------------------------------------------------------------------------- 1 | /* 2 | Author: Start Bootstrap - http://startbootstrap.com 3 | 'SB Admin' HTML Template by Start Bootstrap 4 | 5 | All Start Bootstrap themes are licensed under Apache 2.0. 6 | For more info and more free Bootstrap 3 HTML themes, visit http://startbootstrap.com! 7 | */ 8 | 9 | /* ATTN: This is mobile first CSS - to update 786px and up screen width use the media query near the bottom of the document! */ 10 | 11 | /* Global Styles */ 12 | 13 | body { 14 | margin-top: 50px; 15 | } 16 | 17 | #wrapper { 18 | padding-left: 0; 19 | } 20 | 21 | #page-wrapper { 22 | width: 100%; 23 | padding: 5px 15px; 24 | } 25 | 26 | /* Nav Messages */ 27 | 28 | .messages-dropdown .dropdown-menu .message-preview .avatar, 29 | .messages-dropdown .dropdown-menu .message-preview .name, 30 | .messages-dropdown .dropdown-menu .message-preview .message, 31 | .messages-dropdown .dropdown-menu .message-preview .time { 32 | display: block; 33 | } 34 | 35 | .messages-dropdown .dropdown-menu .message-preview .avatar { 36 | float: left; 37 | margin-right: 15px; 38 | } 39 | 40 | .messages-dropdown .dropdown-menu .message-preview .name { 41 | font-weight: bold; 42 | } 43 | 44 | .messages-dropdown .dropdown-menu .message-preview .message { 45 | font-size: 12px; 46 | } 47 | 48 | .messages-dropdown .dropdown-menu .message-preview .time { 49 | font-size: 12px; 50 | } 51 | 52 | 53 | /* Nav Announcements */ 54 | 55 | .announcement-heading { 56 | font-size: 50px; 57 | margin: 0; 58 | } 59 | 60 | .announcement-text { 61 | margin: 0; 62 | } 63 | 64 | /* Table Headers */ 65 | 66 | table.tablesorter thead { 67 | cursor: pointer; 68 | } 69 | 70 | table.tablesorter thead tr th:hover { 71 | background-color: #f5f5f5; 72 | } 73 | 74 | /* Flot Chart Containers */ 75 | 76 | .flot-chart { 77 | display: block; 78 | height: 400px; 79 | } 80 | 81 | .flot-chart-content { 82 | width: 100%; 83 | height: 100%; 84 | } 85 | 86 | /* Edit Below to Customize Widths > 768px */ 87 | @media (min-width:768px) { 88 | 89 | /* Wrappers */ 90 | 91 | #wrapper { 92 | padding-left: 225px; 93 | } 94 | 95 | #page-wrapper { 96 | padding: 15px 25px; 97 | } 98 | 99 | /* Side Nav */ 100 | 101 | .side-nav { 102 | margin-left: -225px; 103 | left: 225px; 104 | width: 225px; 105 | position: fixed; 106 | top: 50px; 107 | height: 100%; 108 | border-radius: 0; 109 | border: none; 110 | background-color: #222222; 111 | overflow-y: auto; 112 | } 113 | 114 | /* Bootstrap Default Overrides - Customized Dropdowns for the Side Nav */ 115 | 116 | .side-nav>li.dropdown>ul.dropdown-menu { 117 | position: relative; 118 | min-width: 225px; 119 | margin: 0; 120 | padding: 0; 121 | border: none; 122 | border-radius: 0; 123 | background-color: transparent; 124 | box-shadow: none; 125 | -webkit-box-shadow: none; 126 | } 127 | 128 | .side-nav>li.dropdown>ul.dropdown-menu>li>a { 129 | color: #999999; 130 | padding: 15px 15px 15px 25px; 131 | } 132 | 133 | .side-nav>li.dropdown>ul.dropdown-menu>li>a:hover, 134 | .side-nav>li.dropdown>ul.dropdown-menu>li>a.active, 135 | .side-nav>li.dropdown>ul.dropdown-menu>li>a:focus { 136 | color: #fff; 137 | background-color: #080808; 138 | } 139 | 140 | .side-nav>li>a { 141 | width: 225px; 142 | } 143 | 144 | .navbar-inverse .navbar-nav>li>a:hover, 145 | .navbar-inverse .navbar-nav>li>a:focus { 146 | background-color: #080808; 147 | } 148 | 149 | /* Nav Messages */ 150 | 151 | .messages-dropdown .dropdown-menu { 152 | min-width: 300px; 153 | } 154 | 155 | .messages-dropdown .dropdown-menu li a { 156 | white-space: normal; 157 | } 158 | 159 | } 160 | -------------------------------------------------------------------------------- /src/main/resources/static/custom/layout.js: -------------------------------------------------------------------------------- 1 | // 脚本列表bootstrap-table 2 | var $script_list = $('#scenario_list'); 3 | $script_list.bootstrapTable({ 4 | url: '/scenarioInfoList', 5 | method: 'post', 6 | // 如果不指定contentType,则BootstrapTable默认以application/json的形式发送请求 7 | // contentType: "application/x-www-form-urlencoded", 8 | dataType: 'json', 9 | striped: true, 10 | pagination: true, 11 | sidePagination: 'server', 12 | pageSize: 15, 13 | pageList: [15, 30, 60], 14 | paginationPreText: '‹', 15 | paginationNextText: '›', 16 | locale: 'zh-CN', 17 | queryParams: function (params) { 18 | return { 19 | offset: params.offset, 20 | limit: params.limit, 21 | scenarioName: $('#search_name').val(), 22 | status: $('#select_status').val() 23 | } 24 | }, 25 | columns: [ 26 | { 27 | field: 'id', 28 | title: '编号', 29 | align: 'center', 30 | formatter: function(value, row, index) { // 以列表的序列来标号 31 | return index + 1; 32 | } 33 | }, { 34 | field: 'scenarioName', 35 | title: '场景名称', 36 | align: 'center' 37 | }, { 38 | field: 'createTime', 39 | title: '创建时间', 40 | align: 'center', 41 | formatter: function (value, row, index) { 42 | return convertDateFormat(value); 43 | } 44 | }, { 45 | field: 'operate', 46 | title: '操作', 47 | align: 'center', 48 | formatter: function (value, row, index) { 49 | return [ 50 | '' + 51 | '修改' + 52 | '', 53 | '    ' + 54 | '' + 55 | '删除' + 56 | '', 57 | '    ' + 58 | '' + 59 | '运行' + 60 | '' + 61 | '    ' + 62 | '' + 63 | '结果' + 64 | '' 65 | ].join(''); 66 | } 67 | } 68 | ] 69 | }); 70 | 71 | // 转换日期格式(时间戳转换为datetime格式) 72 | function convertDateFormat(date_value) { 73 | var date_val = date_value + ""; 74 | if (date_value != null) { 75 | var date = new Date(parseInt(date_val.replace("/Date(", "").replace(")/", ""), 10)); 76 | var month = date.getMonth() + 1 < 10 ? "0" + (date.getMonth() + 1) : date.getMonth() + 1; 77 | var currentDate = date.getDate() < 10 ? "0" + date.getDate() : date.getDate(); 78 | 79 | var hours = date.getHours() < 10 ? "0" + date.getHours() : date.getHours(); 80 | var minutes = date.getMinutes() < 10 ? "0" + date.getMinutes() : date.getMinutes(); 81 | var seconds = date.getSeconds() < 10 ? "0" + date.getSeconds() : date.getSeconds(); 82 | 83 | return date.getFullYear() + "-" + month + "-" + currentDate + " " + hours + ":" + minutes + ":" + seconds; 84 | } 85 | } -------------------------------------------------------------------------------- /src/main/resources/static/custom/scenarioNewResultList.js: -------------------------------------------------------------------------------- 1 | // 脚本列表bootstrap-table 2 | var $result_list = $('#result_list'); 3 | $result_list.bootstrapTable({ 4 | url: '/getScenarioNewResultList', 5 | method: 'post', 6 | dataType: 'json', 7 | striped: true, 8 | pagination: true, 9 | sidePagination: 'server', 10 | pageSize: 15, 11 | pageList: [15, 30, 60], 12 | paginationPreText: '‹', 13 | paginationNextText: '›', 14 | locale: 'zh-CN', 15 | queryParams: function (params) { 16 | return { 17 | offset: params.offset, 18 | limit: params.limit 19 | } 20 | }, 21 | columns: [ 22 | { 23 | field: 'id', 24 | title: '编号', 25 | align: 'center', 26 | formatter: function(value, row, index) { // 以列表的序列来标号 27 | return index + 1; 28 | } 29 | }, { 30 | field: 'scenarioName', 31 | title: '场景名称', 32 | align: 'center' 33 | }, { 34 | field: 'numThreads', 35 | title: '并发数量', 36 | align: 'center' 37 | }, { 38 | field: 'rampUp', 39 | title: '攀升时间', 40 | align: 'center' 41 | }, { 42 | field: 'duration', 43 | title: '持续时间', 44 | align: 'center' 45 | },{ 46 | field: 'runTime', 47 | title: '运行时间', 48 | align: 'center', 49 | formatter: function (value, row, index) { 50 | return convertDateFormat(value); 51 | } 52 | }, 53 | { 54 | field: 'operate', 55 | title: '操作', 56 | align: 'center', 57 | formatter: function (value, row, index) { 58 | return [ 59 | '' + 60 | '查看' + 61 | '' + 62 | '  ' + 63 | '' + 64 | '删除' + 65 | '' 66 | ].join(''); 67 | } 68 | } 69 | ] 70 | }); 71 | 72 | // 查看历史结果 73 | function resSampler(resultId) { 74 | $(window).attr('location', "/scenarioSampleDetailChartGrid?resultId=" + resultId); 75 | } 76 | 77 | // 删除历史结果 78 | function delResult(resultId) { 79 | $.ajax({ 80 | url: '/delScenarioResult', 81 | type: 'post', 82 | data: { 83 | resultId: resultId 84 | }, 85 | success: function(result) { 86 | bootbox.alert({ 87 | title: '提示', 88 | message: result['message'], 89 | callback: function () { 90 | $result_list.bootstrapTable('selectPage', 1); 91 | } 92 | }); 93 | } 94 | }); 95 | } -------------------------------------------------------------------------------- /src/main/resources/static/custom/scenarioRealTimeChartGrid.js: -------------------------------------------------------------------------------- 1 | var $stopScenarioRun = $("#stopScenarioRun"); 2 | $stopScenarioRun.click( 3 | function() { 4 | $.ajax({ 5 | url: "/scenarioStopRun", 6 | type: "get", 7 | success: function(result) { 8 | bootbox.alert({ 9 | title: '提示', 10 | message: result['message'], 11 | callback: function() { 12 | $(window).attr('location', "/"); 13 | } 14 | }); 15 | }, 16 | error: function(result) {} 17 | }); 18 | } 19 | ); 20 | -------------------------------------------------------------------------------- /src/main/resources/static/custom/scenarioResultListById.js: -------------------------------------------------------------------------------- 1 | // 脚本列表bootstrap-table 2 | var $result_list = $('#result_list'); 3 | $result_list.bootstrapTable({ 4 | url: '/getScenarioResultListByScenarioId', 5 | method: 'post', 6 | dataType: 'json', 7 | striped: true, 8 | pagination: true, 9 | sidePagination: 'server', 10 | pageSize: 15, 11 | pageList: [15, 30, 60], 12 | paginationPreText: '‹', 13 | paginationNextText: '›', 14 | locale: 'zh-CN', 15 | queryParams: function (params) { 16 | return { 17 | offset: params.offset, 18 | limit: params.limit, 19 | scenarioId: $('#scenario_id').val() 20 | } 21 | }, 22 | columns: [ 23 | { 24 | field: 'id', 25 | title: '编号', 26 | align: 'center', 27 | formatter: function(value, row, index) { // 以列表的序列来标号 28 | return index + 1; 29 | } 30 | }, { 31 | field: 'scenarioName', 32 | title: '场景名称', 33 | align: 'center' 34 | }, { 35 | field: 'numThreads', 36 | title: '并发数量', 37 | align: 'center' 38 | }, { 39 | field: 'rampUp', 40 | title: '攀升时间', 41 | align: 'center' 42 | }, { 43 | field: 'duration', 44 | title: '持续时间', 45 | align: 'center' 46 | },{ 47 | field: 'runTime', 48 | title: '运行时间', 49 | align: 'center', 50 | formatter: function (value, row, index) { 51 | return convertDateFormat(value); 52 | } 53 | }, 54 | { 55 | field: 'operate', 56 | title: '操作', 57 | align: 'center', 58 | formatter: function (value, row, index) { 59 | return [ 60 | '' + 61 | '查看' + 62 | '' + 63 | '  ' + 64 | '' + 65 | '删除' + 66 | '' 67 | ].join(''); 68 | } 69 | } 70 | ] 71 | }); 72 | 73 | // 转换日期格式(时间戳转换为datetime格式) 74 | function convertDateFormat(date_value) { 75 | var date_val = date_value + ""; 76 | if (date_value != null) { 77 | var date = new Date(parseInt(date_val.replace("/Date(", "").replace(")/", ""), 10)); 78 | var month = date.getMonth() + 1 < 10 ? "0" + (date.getMonth() + 1) : date.getMonth() + 1; 79 | var currentDate = date.getDate() < 10 ? "0" + date.getDate() : date.getDate(); 80 | 81 | var hours = date.getHours() < 10 ? "0" + date.getHours() : date.getHours(); 82 | var minutes = date.getMinutes() < 10 ? "0" + date.getMinutes() : date.getMinutes(); 83 | var seconds = date.getSeconds() < 10 ? "0" + date.getSeconds() : date.getSeconds(); 84 | 85 | return date.getFullYear() + "-" + month + "-" + currentDate + " " + hours + ":" + minutes + ":" + seconds; 86 | } 87 | } 88 | 89 | // 查看历史结果 90 | function resSampler(resultId) { 91 | $(window).attr('location', "/scenarioSampleDetailChartGrid?resultId=" + resultId); 92 | } 93 | 94 | // 删除历史结果 95 | function delResult(resultId) { 96 | $.ajax({ 97 | url: '/delScenarioResult', 98 | type: 'post', 99 | data: { 100 | resultId: resultId 101 | }, 102 | success: function(result) { 103 | bootbox.alert({ 104 | title: '提示', 105 | message: result['message'], 106 | callback: function () { 107 | $result_list.bootstrapTable('selectPage', 1); 108 | } 109 | }); 110 | } 111 | }); 112 | } -------------------------------------------------------------------------------- /src/main/resources/static/custom/scenarioSampleResult.js: -------------------------------------------------------------------------------- 1 | var chartData = null; 2 | 3 | function getSampleResultDetailData(chart) { 4 | $.ajax({ 5 | url: "/getSampleResultDetailData", 6 | type: "post", 7 | data: { 8 | resultId: $("#result_id").val(), 9 | dataType: $("#data_type").val() 10 | }, 11 | success: function (resultData) { 12 | chartData = resultData; 13 | // 遍历对象,找出每个Series 14 | Object.keys(chartData).forEach(function (key) { 15 | console.log(key, chartData[key]); 16 | chart.addSeries({ 17 | name: key, 18 | data: chartData[key] 19 | }); 20 | }); 21 | } 22 | }); 23 | } 24 | 25 | Highcharts.setOptions({ 26 | global: { 27 | useUTC: false 28 | } 29 | }); 30 | 31 | 32 | $("#container").highcharts({ 33 | series: [], 34 | chart: { 35 | type: 'spline', 36 | animation: false, // 去掉动画 37 | marginRight: 10, 38 | events: { 39 | load: function () { 40 | getSampleResultDetailData(this); 41 | } 42 | } 43 | }, 44 | boost: { 45 | useGPUTranslations: true 46 | }, 47 | title: { 48 | text: $("#chart_title").val() 49 | }, 50 | xAxis: { 51 | type: 'datetime', 52 | tickPixelInterval: 50, 53 | rotation: 50 54 | }, 55 | yAxis: { 56 | title: { 57 | text: $("#unit").val(), 58 | style: { 59 | fontWeight: 'bold' 60 | } 61 | }, 62 | plotLines: [{ 63 | value: 0, 64 | width: 1, 65 | color: '#808080' 66 | }] 67 | }, 68 | plotOptions: { 69 | series: { 70 | marker: { 71 | enabled: false // 显示数据点 72 | }, 73 | lineWidth: 2 // 线宽 74 | } 75 | }, 76 | legend: { 77 | enabled: true 78 | }, 79 | credits: { 80 | enabled: false // 不显示LOGO 81 | } 82 | }); 83 | -------------------------------------------------------------------------------- /src/main/resources/static/font-awesome/fonts/FontAwesome.otf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Manchester117/Berserker/a71f06b37f231d0ef8d2bb73800131abea1024b7/src/main/resources/static/font-awesome/fonts/FontAwesome.otf -------------------------------------------------------------------------------- /src/main/resources/static/font-awesome/fonts/fontawesome-webfont.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Manchester117/Berserker/a71f06b37f231d0ef8d2bb73800131abea1024b7/src/main/resources/static/font-awesome/fonts/fontawesome-webfont.eot -------------------------------------------------------------------------------- /src/main/resources/static/font-awesome/fonts/fontawesome-webfont.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Manchester117/Berserker/a71f06b37f231d0ef8d2bb73800131abea1024b7/src/main/resources/static/font-awesome/fonts/fontawesome-webfont.ttf -------------------------------------------------------------------------------- /src/main/resources/static/font-awesome/fonts/fontawesome-webfont.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Manchester117/Berserker/a71f06b37f231d0ef8d2bb73800131abea1024b7/src/main/resources/static/font-awesome/fonts/fontawesome-webfont.woff -------------------------------------------------------------------------------- /src/main/resources/static/font-awesome/fonts/fontawesome-webfont.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Manchester117/Berserker/a71f06b37f231d0ef8d2bb73800131abea1024b7/src/main/resources/static/font-awesome/fonts/fontawesome-webfont.woff2 -------------------------------------------------------------------------------- /src/main/resources/static/font-awesome/fonts/glyphicons-halflings-regular.eot: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Manchester117/Berserker/a71f06b37f231d0ef8d2bb73800131abea1024b7/src/main/resources/static/font-awesome/fonts/glyphicons-halflings-regular.eot -------------------------------------------------------------------------------- /src/main/resources/static/font-awesome/fonts/glyphicons-halflings-regular.ttf: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Manchester117/Berserker/a71f06b37f231d0ef8d2bb73800131abea1024b7/src/main/resources/static/font-awesome/fonts/glyphicons-halflings-regular.ttf -------------------------------------------------------------------------------- /src/main/resources/static/font-awesome/fonts/glyphicons-halflings-regular.woff: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Manchester117/Berserker/a71f06b37f231d0ef8d2bb73800131abea1024b7/src/main/resources/static/font-awesome/fonts/glyphicons-halflings-regular.woff -------------------------------------------------------------------------------- /src/main/resources/static/font-awesome/fonts/glyphicons-halflings-regular.woff2: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Manchester117/Berserker/a71f06b37f231d0ef8d2bb73800131abea1024b7/src/main/resources/static/font-awesome/fonts/glyphicons-halflings-regular.woff2 -------------------------------------------------------------------------------- /src/main/resources/static/font-awesome/less/animated.less: -------------------------------------------------------------------------------- 1 | // Animated Icons 2 | // -------------------------- 3 | 4 | .@{fa-css-prefix}-spin { 5 | -webkit-animation: fa-spin 2s infinite linear; 6 | animation: fa-spin 2s infinite linear; 7 | } 8 | 9 | .@{fa-css-prefix}-pulse { 10 | -webkit-animation: fa-spin 1s infinite steps(8); 11 | animation: fa-spin 1s infinite steps(8); 12 | } 13 | 14 | @-webkit-keyframes fa-spin { 15 | 0% { 16 | -webkit-transform: rotate(0deg); 17 | transform: rotate(0deg); 18 | } 19 | 100% { 20 | -webkit-transform: rotate(359deg); 21 | transform: rotate(359deg); 22 | } 23 | } 24 | 25 | @keyframes fa-spin { 26 | 0% { 27 | -webkit-transform: rotate(0deg); 28 | transform: rotate(0deg); 29 | } 30 | 100% { 31 | -webkit-transform: rotate(359deg); 32 | transform: rotate(359deg); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/resources/static/font-awesome/less/bordered-pulled.less: -------------------------------------------------------------------------------- 1 | // Bordered & Pulled 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix}-border { 5 | padding: .2em .25em .15em; 6 | border: solid .08em @fa-border-color; 7 | border-radius: .1em; 8 | } 9 | 10 | .@{fa-css-prefix}-pull-left { float: left; } 11 | .@{fa-css-prefix}-pull-right { float: right; } 12 | 13 | .@{fa-css-prefix} { 14 | &.@{fa-css-prefix}-pull-left { margin-right: .3em; } 15 | &.@{fa-css-prefix}-pull-right { margin-left: .3em; } 16 | } 17 | 18 | /* Deprecated as of 4.4.0 */ 19 | .pull-right { float: right; } 20 | .pull-left { float: left; } 21 | 22 | .@{fa-css-prefix} { 23 | &.pull-left { margin-right: .3em; } 24 | &.pull-right { margin-left: .3em; } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/resources/static/font-awesome/less/core.less: -------------------------------------------------------------------------------- 1 | // Base Class Definition 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix} { 5 | display: inline-block; 6 | font: normal normal normal @fa-font-size-base/@fa-line-height-base FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/main/resources/static/font-awesome/less/fixed-width.less: -------------------------------------------------------------------------------- 1 | // Fixed Width Icons 2 | // ------------------------- 3 | .@{fa-css-prefix}-fw { 4 | width: (18em / 14); 5 | text-align: center; 6 | } 7 | -------------------------------------------------------------------------------- /src/main/resources/static/font-awesome/less/font-awesome.less: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome 3 | * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) 4 | */ 5 | 6 | @import "variables.less"; 7 | @import "mixins.less"; 8 | @import "path.less"; 9 | @import "core.less"; 10 | @import "larger.less"; 11 | @import "fixed-width.less"; 12 | @import "list.less"; 13 | @import "bordered-pulled.less"; 14 | @import "animated.less"; 15 | @import "rotated-flipped.less"; 16 | @import "stacked.less"; 17 | @import "icons.less"; 18 | @import "screen-reader.less"; 19 | -------------------------------------------------------------------------------- /src/main/resources/static/font-awesome/less/larger.less: -------------------------------------------------------------------------------- 1 | // Icon Sizes 2 | // ------------------------- 3 | 4 | /* makes the font 33% larger relative to the icon container */ 5 | .@{fa-css-prefix}-lg { 6 | font-size: (4em / 3); 7 | line-height: (3em / 4); 8 | vertical-align: -15%; 9 | } 10 | .@{fa-css-prefix}-2x { font-size: 2em; } 11 | .@{fa-css-prefix}-3x { font-size: 3em; } 12 | .@{fa-css-prefix}-4x { font-size: 4em; } 13 | .@{fa-css-prefix}-5x { font-size: 5em; } 14 | -------------------------------------------------------------------------------- /src/main/resources/static/font-awesome/less/list.less: -------------------------------------------------------------------------------- 1 | // List Icons 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix}-ul { 5 | padding-left: 0; 6 | margin-left: @fa-li-width; 7 | list-style-type: none; 8 | > li { position: relative; } 9 | } 10 | .@{fa-css-prefix}-li { 11 | position: absolute; 12 | left: -@fa-li-width; 13 | width: @fa-li-width; 14 | top: (2em / 14); 15 | text-align: center; 16 | &.@{fa-css-prefix}-lg { 17 | left: (-@fa-li-width + (4em / 14)); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/resources/static/font-awesome/less/mixins.less: -------------------------------------------------------------------------------- 1 | // Mixins 2 | // -------------------------- 3 | 4 | .fa-icon() { 5 | display: inline-block; 6 | font: normal normal normal @fa-font-size-base/@fa-line-height-base FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | 12 | } 13 | 14 | .fa-icon-rotate(@degrees, @rotation) { 15 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=@{rotation})"; 16 | -webkit-transform: rotate(@degrees); 17 | -ms-transform: rotate(@degrees); 18 | transform: rotate(@degrees); 19 | } 20 | 21 | .fa-icon-flip(@horiz, @vert, @rotation) { 22 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=@{rotation}, mirror=1)"; 23 | -webkit-transform: scale(@horiz, @vert); 24 | -ms-transform: scale(@horiz, @vert); 25 | transform: scale(@horiz, @vert); 26 | } 27 | 28 | 29 | // Only display content to screen readers. A la Bootstrap 4. 30 | // 31 | // See: http://a11yproject.com/posts/how-to-hide-content/ 32 | 33 | .sr-only() { 34 | position: absolute; 35 | width: 1px; 36 | height: 1px; 37 | padding: 0; 38 | margin: -1px; 39 | overflow: hidden; 40 | clip: rect(0,0,0,0); 41 | border: 0; 42 | } 43 | 44 | // Use in conjunction with .sr-only to only display content when it's focused. 45 | // 46 | // Useful for "Skip to main content" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1 47 | // 48 | // Credit: HTML5 Boilerplate 49 | 50 | .sr-only-focusable() { 51 | &:active, 52 | &:focus { 53 | position: static; 54 | width: auto; 55 | height: auto; 56 | margin: 0; 57 | overflow: visible; 58 | clip: auto; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/resources/static/font-awesome/less/path.less: -------------------------------------------------------------------------------- 1 | /* FONT PATH 2 | * -------------------------- */ 3 | 4 | @font-face { 5 | font-family: 'FontAwesome'; 6 | src: url('@{fa-font-path}/fontawesome-webfont.eot?v=@{fa-version}'); 7 | src: url('@{fa-font-path}/fontawesome-webfont.eot?#iefix&v=@{fa-version}') format('embedded-opentype'), 8 | url('@{fa-font-path}/fontawesome-webfont.woff2?v=@{fa-version}') format('woff2'), 9 | url('@{fa-font-path}/fontawesome-webfont.woff?v=@{fa-version}') format('woff'), 10 | url('@{fa-font-path}/fontawesome-webfont.ttf?v=@{fa-version}') format('truetype'), 11 | url('@{fa-font-path}/fontawesome-webfont.svg?v=@{fa-version}#fontawesomeregular') format('svg'); 12 | // src: url('@{fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts 13 | font-weight: normal; 14 | font-style: normal; 15 | } 16 | -------------------------------------------------------------------------------- /src/main/resources/static/font-awesome/less/rotated-flipped.less: -------------------------------------------------------------------------------- 1 | // Rotated & Flipped Icons 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix}-rotate-90 { .fa-icon-rotate(90deg, 1); } 5 | .@{fa-css-prefix}-rotate-180 { .fa-icon-rotate(180deg, 2); } 6 | .@{fa-css-prefix}-rotate-270 { .fa-icon-rotate(270deg, 3); } 7 | 8 | .@{fa-css-prefix}-flip-horizontal { .fa-icon-flip(-1, 1, 0); } 9 | .@{fa-css-prefix}-flip-vertical { .fa-icon-flip(1, -1, 2); } 10 | 11 | // Hook for IE8-9 12 | // ------------------------- 13 | 14 | :root .@{fa-css-prefix}-rotate-90, 15 | :root .@{fa-css-prefix}-rotate-180, 16 | :root .@{fa-css-prefix}-rotate-270, 17 | :root .@{fa-css-prefix}-flip-horizontal, 18 | :root .@{fa-css-prefix}-flip-vertical { 19 | filter: none; 20 | } 21 | -------------------------------------------------------------------------------- /src/main/resources/static/font-awesome/less/screen-reader.less: -------------------------------------------------------------------------------- 1 | // Screen Readers 2 | // ------------------------- 3 | 4 | .sr-only { .sr-only(); } 5 | .sr-only-focusable { .sr-only-focusable(); } 6 | -------------------------------------------------------------------------------- /src/main/resources/static/font-awesome/less/stacked.less: -------------------------------------------------------------------------------- 1 | // Stacked Icons 2 | // ------------------------- 3 | 4 | .@{fa-css-prefix}-stack { 5 | position: relative; 6 | display: inline-block; 7 | width: 2em; 8 | height: 2em; 9 | line-height: 2em; 10 | vertical-align: middle; 11 | } 12 | .@{fa-css-prefix}-stack-1x, .@{fa-css-prefix}-stack-2x { 13 | position: absolute; 14 | left: 0; 15 | width: 100%; 16 | text-align: center; 17 | } 18 | .@{fa-css-prefix}-stack-1x { line-height: inherit; } 19 | .@{fa-css-prefix}-stack-2x { font-size: 2em; } 20 | .@{fa-css-prefix}-inverse { color: @fa-inverse; } 21 | -------------------------------------------------------------------------------- /src/main/resources/static/font-awesome/scss/_animated.scss: -------------------------------------------------------------------------------- 1 | // Spinning Icons 2 | // -------------------------- 3 | 4 | .#{$fa-css-prefix}-spin { 5 | -webkit-animation: fa-spin 2s infinite linear; 6 | animation: fa-spin 2s infinite linear; 7 | } 8 | 9 | .#{$fa-css-prefix}-pulse { 10 | -webkit-animation: fa-spin 1s infinite steps(8); 11 | animation: fa-spin 1s infinite steps(8); 12 | } 13 | 14 | @-webkit-keyframes fa-spin { 15 | 0% { 16 | -webkit-transform: rotate(0deg); 17 | transform: rotate(0deg); 18 | } 19 | 100% { 20 | -webkit-transform: rotate(359deg); 21 | transform: rotate(359deg); 22 | } 23 | } 24 | 25 | @keyframes fa-spin { 26 | 0% { 27 | -webkit-transform: rotate(0deg); 28 | transform: rotate(0deg); 29 | } 30 | 100% { 31 | -webkit-transform: rotate(359deg); 32 | transform: rotate(359deg); 33 | } 34 | } 35 | -------------------------------------------------------------------------------- /src/main/resources/static/font-awesome/scss/_bordered-pulled.scss: -------------------------------------------------------------------------------- 1 | // Bordered & Pulled 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-border { 5 | padding: .2em .25em .15em; 6 | border: solid .08em $fa-border-color; 7 | border-radius: .1em; 8 | } 9 | 10 | .#{$fa-css-prefix}-pull-left { float: left; } 11 | .#{$fa-css-prefix}-pull-right { float: right; } 12 | 13 | .#{$fa-css-prefix} { 14 | &.#{$fa-css-prefix}-pull-left { margin-right: .3em; } 15 | &.#{$fa-css-prefix}-pull-right { margin-left: .3em; } 16 | } 17 | 18 | /* Deprecated as of 4.4.0 */ 19 | .pull-right { float: right; } 20 | .pull-left { float: left; } 21 | 22 | .#{$fa-css-prefix} { 23 | &.pull-left { margin-right: .3em; } 24 | &.pull-right { margin-left: .3em; } 25 | } 26 | -------------------------------------------------------------------------------- /src/main/resources/static/font-awesome/scss/_core.scss: -------------------------------------------------------------------------------- 1 | // Base Class Definition 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix} { 5 | display: inline-block; 6 | font: normal normal normal #{$fa-font-size-base}/#{$fa-line-height-base} FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | 12 | } 13 | -------------------------------------------------------------------------------- /src/main/resources/static/font-awesome/scss/_fixed-width.scss: -------------------------------------------------------------------------------- 1 | // Fixed Width Icons 2 | // ------------------------- 3 | .#{$fa-css-prefix}-fw { 4 | width: (18em / 14); 5 | text-align: center; 6 | } 7 | -------------------------------------------------------------------------------- /src/main/resources/static/font-awesome/scss/_larger.scss: -------------------------------------------------------------------------------- 1 | // Icon Sizes 2 | // ------------------------- 3 | 4 | /* makes the font 33% larger relative to the icon container */ 5 | .#{$fa-css-prefix}-lg { 6 | font-size: (4em / 3); 7 | line-height: (3em / 4); 8 | vertical-align: -15%; 9 | } 10 | .#{$fa-css-prefix}-2x { font-size: 2em; } 11 | .#{$fa-css-prefix}-3x { font-size: 3em; } 12 | .#{$fa-css-prefix}-4x { font-size: 4em; } 13 | .#{$fa-css-prefix}-5x { font-size: 5em; } 14 | -------------------------------------------------------------------------------- /src/main/resources/static/font-awesome/scss/_list.scss: -------------------------------------------------------------------------------- 1 | // List Icons 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-ul { 5 | padding-left: 0; 6 | margin-left: $fa-li-width; 7 | list-style-type: none; 8 | > li { position: relative; } 9 | } 10 | .#{$fa-css-prefix}-li { 11 | position: absolute; 12 | left: -$fa-li-width; 13 | width: $fa-li-width; 14 | top: (2em / 14); 15 | text-align: center; 16 | &.#{$fa-css-prefix}-lg { 17 | left: -$fa-li-width + (4em / 14); 18 | } 19 | } 20 | -------------------------------------------------------------------------------- /src/main/resources/static/font-awesome/scss/_mixins.scss: -------------------------------------------------------------------------------- 1 | // Mixins 2 | // -------------------------- 3 | 4 | @mixin fa-icon() { 5 | display: inline-block; 6 | font: normal normal normal #{$fa-font-size-base}/#{$fa-line-height-base} FontAwesome; // shortening font declaration 7 | font-size: inherit; // can't have font-size inherit on line above, so need to override 8 | text-rendering: auto; // optimizelegibility throws things off #1094 9 | -webkit-font-smoothing: antialiased; 10 | -moz-osx-font-smoothing: grayscale; 11 | 12 | } 13 | 14 | @mixin fa-icon-rotate($degrees, $rotation) { 15 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation})"; 16 | -webkit-transform: rotate($degrees); 17 | -ms-transform: rotate($degrees); 18 | transform: rotate($degrees); 19 | } 20 | 21 | @mixin fa-icon-flip($horiz, $vert, $rotation) { 22 | -ms-filter: "progid:DXImageTransform.Microsoft.BasicImage(rotation=#{$rotation}, mirror=1)"; 23 | -webkit-transform: scale($horiz, $vert); 24 | -ms-transform: scale($horiz, $vert); 25 | transform: scale($horiz, $vert); 26 | } 27 | 28 | 29 | // Only display content to screen readers. A la Bootstrap 4. 30 | // 31 | // See: http://a11yproject.com/posts/how-to-hide-content/ 32 | 33 | @mixin sr-only { 34 | position: absolute; 35 | width: 1px; 36 | height: 1px; 37 | padding: 0; 38 | margin: -1px; 39 | overflow: hidden; 40 | clip: rect(0,0,0,0); 41 | border: 0; 42 | } 43 | 44 | // Use in conjunction with .sr-only to only display content when it's focused. 45 | // 46 | // Useful for "Skip to main content" links; see http://www.w3.org/TR/2013/NOTE-WCAG20-TECHS-20130905/G1 47 | // 48 | // Credit: HTML5 Boilerplate 49 | 50 | @mixin sr-only-focusable { 51 | &:active, 52 | &:focus { 53 | position: static; 54 | width: auto; 55 | height: auto; 56 | margin: 0; 57 | overflow: visible; 58 | clip: auto; 59 | } 60 | } 61 | -------------------------------------------------------------------------------- /src/main/resources/static/font-awesome/scss/_path.scss: -------------------------------------------------------------------------------- 1 | /* FONT PATH 2 | * -------------------------- */ 3 | 4 | @font-face { 5 | font-family: 'FontAwesome'; 6 | src: url('#{$fa-font-path}/fontawesome-webfont.eot?v=#{$fa-version}'); 7 | src: url('#{$fa-font-path}/fontawesome-webfont.eot?#iefix&v=#{$fa-version}') format('embedded-opentype'), 8 | url('#{$fa-font-path}/fontawesome-webfont.woff2?v=#{$fa-version}') format('woff2'), 9 | url('#{$fa-font-path}/fontawesome-webfont.woff?v=#{$fa-version}') format('woff'), 10 | url('#{$fa-font-path}/fontawesome-webfont.ttf?v=#{$fa-version}') format('truetype'), 11 | url('#{$fa-font-path}/fontawesome-webfont.svg?v=#{$fa-version}#fontawesomeregular') format('svg'); 12 | // src: url('#{$fa-font-path}/FontAwesome.otf') format('opentype'); // used when developing fonts 13 | font-weight: normal; 14 | font-style: normal; 15 | } 16 | -------------------------------------------------------------------------------- /src/main/resources/static/font-awesome/scss/_rotated-flipped.scss: -------------------------------------------------------------------------------- 1 | // Rotated & Flipped Icons 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-rotate-90 { @include fa-icon-rotate(90deg, 1); } 5 | .#{$fa-css-prefix}-rotate-180 { @include fa-icon-rotate(180deg, 2); } 6 | .#{$fa-css-prefix}-rotate-270 { @include fa-icon-rotate(270deg, 3); } 7 | 8 | .#{$fa-css-prefix}-flip-horizontal { @include fa-icon-flip(-1, 1, 0); } 9 | .#{$fa-css-prefix}-flip-vertical { @include fa-icon-flip(1, -1, 2); } 10 | 11 | // Hook for IE8-9 12 | // ------------------------- 13 | 14 | :root .#{$fa-css-prefix}-rotate-90, 15 | :root .#{$fa-css-prefix}-rotate-180, 16 | :root .#{$fa-css-prefix}-rotate-270, 17 | :root .#{$fa-css-prefix}-flip-horizontal, 18 | :root .#{$fa-css-prefix}-flip-vertical { 19 | filter: none; 20 | } 21 | -------------------------------------------------------------------------------- /src/main/resources/static/font-awesome/scss/_screen-reader.scss: -------------------------------------------------------------------------------- 1 | // Screen Readers 2 | // ------------------------- 3 | 4 | .sr-only { @include sr-only(); } 5 | .sr-only-focusable { @include sr-only-focusable(); } 6 | -------------------------------------------------------------------------------- /src/main/resources/static/font-awesome/scss/_stacked.scss: -------------------------------------------------------------------------------- 1 | // Stacked Icons 2 | // ------------------------- 3 | 4 | .#{$fa-css-prefix}-stack { 5 | position: relative; 6 | display: inline-block; 7 | width: 2em; 8 | height: 2em; 9 | line-height: 2em; 10 | vertical-align: middle; 11 | } 12 | .#{$fa-css-prefix}-stack-1x, .#{$fa-css-prefix}-stack-2x { 13 | position: absolute; 14 | left: 0; 15 | width: 100%; 16 | text-align: center; 17 | } 18 | .#{$fa-css-prefix}-stack-1x { line-height: inherit; } 19 | .#{$fa-css-prefix}-stack-2x { font-size: 2em; } 20 | .#{$fa-css-prefix}-inverse { color: $fa-inverse; } 21 | -------------------------------------------------------------------------------- /src/main/resources/static/font-awesome/scss/font-awesome.scss: -------------------------------------------------------------------------------- 1 | /*! 2 | * Font Awesome 4.7.0 by @davegandy - http://fontawesome.io - @fontawesome 3 | * License - http://fontawesome.io/license (Font: SIL OFL 1.1, CSS: MIT License) 4 | */ 5 | 6 | @import "variables"; 7 | @import "mixins"; 8 | @import "path"; 9 | @import "core"; 10 | @import "larger"; 11 | @import "fixed-width"; 12 | @import "list"; 13 | @import "bordered-pulled"; 14 | @import "animated"; 15 | @import "rotated-flipped"; 16 | @import "stacked"; 17 | @import "icons"; 18 | @import "screen-reader"; 19 | -------------------------------------------------------------------------------- /src/main/resources/static/img/loading.gif: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/Manchester117/Berserker/a71f06b37f231d0ef8d2bb73800131abea1024b7/src/main/resources/static/img/loading.gif -------------------------------------------------------------------------------- /src/main/resources/static/js/boost-canvas.js: -------------------------------------------------------------------------------- 1 | /* 2 | Highcharts JS v6.0.7 (2018-02-16) 3 | Boost module 4 | 5 | (c) 2010-2017 Highsoft AS 6 | Author: Torstein Honsi 7 | 8 | License: www.highcharts.com/license 9 | */ 10 | (function(k){"object"===typeof module&&module.exports?module.exports=k:k(Highcharts)})(function(k){(function(c){var k=c.win.document,Y=function(){},Z=c.Color,w=c.Series,e=c.seriesTypes,m=c.each,q=c.extend,x=c.addEvent,aa=c.fireEvent,ba=c.isNumber,ca=c.merge,da=c.pick,y=c.wrap,I;c.initCanvasBoost=function(){c.seriesTypes.heatmap&&c.wrap(c.seriesTypes.heatmap.prototype,"drawPoints",function(){var a=this.getContext();a?(m(this.points,function(b){var f=b.plotY;void 0===f||isNaN(f)||null===b.y||(f=b.shapeArgs, 11 | b=b.series.pointAttribs(b),a.fillStyle=b.fill,a.fillRect(f.x,f.y,f.width,f.height))}),this.canvasToSVG()):this.chart.showLoading("Your browser doesn't support HTML5 canvas, \x3cbr\x3eplease use a modern browser")});c.extend(w.prototype,{getContext:function(){var a=this.chart,b=a.chartWidth,f=a.chartHeight,c=a.seriesGroup||this.group,d=this,e,h=function(a,f,d,b,c,t,e){a.call(this,d,f,b,c,t,e)};a.isChartSeriesBoosting()&&(d=a,c=a.seriesGroup);e=d.ctx;d.canvas||(d.canvas=k.createElement("canvas"),d.renderTarget= 12 | a.renderer.image("",0,0,b,f).addClass("highcharts-boost-canvas").add(c),d.ctx=e=d.canvas.getContext("2d"),a.inverted&&m(["moveTo","lineTo","rect","arc"],function(a){y(e,a,h)}),d.boostCopy=function(){d.renderTarget.attr({href:d.canvas.toDataURL("image/png")})},d.boostClear=function(){e.clearRect(0,0,d.canvas.width,d.canvas.height);d===this&&d.renderTarget.attr({href:""})},d.boostClipRect=a.renderer.clipRect(),d.renderTarget.clip(d.boostClipRect));d.canvas.width!==b&&(d.canvas.width=b);d.canvas.height!== 13 | f&&(d.canvas.height=f);d.renderTarget.attr({x:0,y:0,width:b,height:f,style:"pointer-events: none",href:""});d.boostClipRect.attr(a.getBoostClipRect(d));return e},canvasToSVG:function(){this.chart.isChartSeriesBoosting()?this.boostClear&&this.boostClear():(this.boostCopy||this.chart.boostCopy)&&(this.boostCopy||this.chart.boostCopy)()},cvsLineTo:function(a,b,f){a.lineTo(b,f)},renderCanvas:function(){var a=this,b=a.options,f=a.chart,t=this.xAxis,d=this.yAxis,e=(f.options.boost||{}).timeRendering||!1, 14 | h,k=0,m=a.processedXData,w=a.processedYData,J=b.data,l=t.getExtremes(),z=l.min,A=l.max,l=d.getExtremes(),y=l.min,ea=l.max,K={},B,fa=!!a.sampling,L,C=b.marker&&b.marker.radius,M=this.cvsDrawPoint,D=b.lineWidth?this.cvsLineTo:!1,N=C&&1>=C?this.cvsMarkerSquare:this.cvsMarkerCircle,ga=this.cvsStrokeBatch||1E3,ha=!1!==b.enableMouseTracking,O,l=b.threshold,p=d.getThreshold(l),P=ba(l),Q=p,ia=this.fill,R=a.pointArrayMap&&"low,high"===a.pointArrayMap.join(","),S=!!b.stacking,ja=a.cropStart||0,l=f.options.loading, 15 | ka=a.requireSorting,T,la=b.connectNulls,U=!m,E,F,r,v,G,n=S?a.data:m||J,ma=a.fillOpacity?(new Z(a.color)).setOpacity(da(b.fillOpacity,.75)).get():a.color,V=function(){ia?(h.fillStyle=ma,h.fill()):(h.strokeStyle=a.color,h.lineWidth=b.lineWidth,h.stroke())},X=function(d,b,c,g){0===k&&(h.beginPath(),D&&(h.lineJoin="round"));f.scroller&&"highcharts-navigator-series"===a.options.className?(b+=f.scroller.top,c&&(c+=f.scroller.top)):b+=f.plotTop;d+=f.plotLeft;T?h.moveTo(d,b):M?M(h,d,b,c,O):D?D(h,d,b):N&& 16 | N.call(a,h,d,b,C,g);k+=1;k===ga&&(V(),k=0);O={clientX:d,plotY:b,yBottom:c}},H=function(a,b,c){G=a+","+b;ha&&!K[G]&&(K[G]=!0,f.inverted&&(a=t.len-a,b=d.len-b),L.push({clientX:a,plotX:a,plotY:b,i:ja+c}))};this.renderTarget&&this.renderTarget.attr({href:""});(this.points||this.graph)&&this.destroyGraphics();a.plotGroup("group","series",a.visible?"visible":"hidden",b.zIndex,f.seriesGroup);a.markerGroup=a.group;x(a,"destroy",function(){a.markerGroup=null});L=this.points=[];h=this.getContext();a.buildKDTree= 17 | Y;this.boostClear&&this.boostClear();this.visible&&(99999=z&&m<=A&&(k=!0);u&&u>=z&&u<=A&&(l=!0);R?(U&&(g=b.slice(1,3)),h=g[0],g=g[1]):S&&(e=b.x,g=b.stackY,h=g-b.y);b=null===g;ka||(q=g>=y&&g<=ea);if(!b&&(e>=z&&e<=A&&q||k||l))if(e=Math.round(t.toPixels(e,!0)),fa){if(void 0===r||e===B){R||(h=g);if(void 0===v||g>F)F=g,v=c;if(void 0===r||h', 19 | uploadIcon: '', 20 | uploadRetryIcon: '', 21 | zoomIcon: '', 22 | dragIcon: '', 23 | indicatorNew: '', 24 | indicatorSuccess: '', 25 | indicatorError: '', 26 | indicatorLoading: '' 27 | }, 28 | layoutTemplates: { 29 | fileIcon: ' ' 30 | }, 31 | previewZoomButtonIcons: { 32 | prev: '', 33 | next: '', 34 | toggleheader: '', 35 | fullscreen: '', 36 | borderless: '', 37 | close: '' 38 | }, 39 | previewFileIcon: '', 40 | browseIcon: '', 41 | removeIcon: '', 42 | cancelIcon: '', 43 | uploadIcon: '', 44 | msgValidationErrorIcon: ' ' 45 | }; 46 | })(window.jQuery); 47 | -------------------------------------------------------------------------------- /src/main/resources/static/js/bootstrap-fileinput-zh-CN.js: -------------------------------------------------------------------------------- 1 | /*! 2 | * FileInput Chinese Translations 3 | * 4 | * This file must be loaded after 'fileinput.js'. Patterns in braces '{}', or 5 | * any HTML markup tags in the messages must not be converted or translated. 6 | * 7 | * @see http://github.com/kartik-v/bootstrap-fileinput 8 | * @author kangqf 9 | * 10 | * NOTE: this file must be saved in UTF-8 encoding. 11 | */ 12 | (function ($) { 13 | "use strict"; 14 | 15 | $.fn.fileinputLocales['zh'] = { 16 | fileSingle: '文件', 17 | filePlural: '个文件', 18 | browseLabel: '选择 …', 19 | removeLabel: '移除', 20 | removeTitle: '清除选中文件', 21 | cancelLabel: '取消', 22 | cancelTitle: '取消进行中的上传', 23 | uploadLabel: '上传', 24 | uploadTitle: '上传选中文件', 25 | msgNo: '没有', 26 | msgNoFilesSelected: '', 27 | msgCancelled: '取消', 28 | msgPlaceholder: 'Select {files}...', 29 | msgZoomModalHeading: '详细预览', 30 | msgFileRequired: '必须选择一个文件上传.', 31 | msgSizeTooSmall: '文件 "{name}" ({size} KB) 必须大于限定大小 {minSize} KB.', 32 | msgSizeTooLarge: '文件 "{name}" ({size} KB) 超过了允许大小 {maxSize} KB.', 33 | msgFilesTooLess: '你必须选择最少 {n} {files} 来上传. ', 34 | msgFilesTooMany: '选择的上传文件个数 ({n}) 超出最大文件的限制个数 {m}.', 35 | msgFileNotFound: '文件 "{name}" 未找到!', 36 | msgFileSecured: '安全限制,为了防止读取文件 "{name}".', 37 | msgFileNotReadable: '文件 "{name}" 不可读.', 38 | msgFilePreviewAborted: '取消 "{name}" 的预览.', 39 | msgFilePreviewError: '读取 "{name}" 时出现了一个错误.', 40 | msgInvalidFileName: '文件名 "{name}" 包含非法字符.', 41 | msgInvalidFileType: '不正确的类型 "{name}". 只支持 "{types}" 类型的文件.', 42 | msgInvalidFileExtension: '不正确的文件扩展名 "{name}". 只支持 "{extensions}" 的文件扩展名.', 43 | msgFileTypes: { 44 | 'image': 'image', 45 | 'html': 'HTML', 46 | 'text': 'text', 47 | 'video': 'video', 48 | 'audio': 'audio', 49 | 'flash': 'flash', 50 | 'pdf': 'PDF', 51 | 'object': 'object' 52 | }, 53 | msgUploadAborted: '该文件上传被中止', 54 | msgUploadThreshold: '处理中...', 55 | msgUploadBegin: '正在初始化...', 56 | msgUploadEnd: '完成', 57 | msgUploadEmpty: '无效的文件上传.', 58 | msgUploadError: 'Error', 59 | msgValidationError: '验证错误', 60 | msgLoading: '加载第 {index} 文件 共 {files} …', 61 | msgProgress: '加载第 {index} 文件 共 {files} - {name} - {percent}% 完成.', 62 | msgSelected: '{n} {files} 选中', 63 | msgFoldersNotAllowed: '只支持拖拽文件! 跳过 {n} 拖拽的文件夹.', 64 | msgImageWidthSmall: '图像文件的"{name}"的宽度必须是至少{size}像素.', 65 | msgImageHeightSmall: '图像文件的"{name}"的高度必须至少为{size}像素.', 66 | msgImageWidthLarge: '图像文件"{name}"的宽度不能超过{size}像素.', 67 | msgImageHeightLarge: '图像文件"{name}"的高度不能超过{size}像素.', 68 | msgImageResizeError: '无法获取的图像尺寸调整。', 69 | msgImageResizeException: '调整图像大小时发生错误。
{errors}
', 70 | msgAjaxError: '{operation} 发生错误. 请重试!', 71 | msgAjaxProgressError: '{operation} 失败', 72 | ajaxOperations: { 73 | deleteThumb: '删除文件', 74 | uploadThumb: '上传文件', 75 | uploadBatch: '批量上传', 76 | uploadExtra: '表单数据上传' 77 | }, 78 | dropZoneTitle: '拖拽文件到这里 …
支持多文件同时上传', 79 | dropZoneClickTitle: '
(或点击{files}按钮选择文件)', 80 | fileActionSettings: { 81 | removeTitle: '删除文件', 82 | uploadTitle: '上传文件', 83 | uploadRetryTitle: 'Retry upload', 84 | zoomTitle: '查看详情', 85 | dragTitle: '移动 / 重置', 86 | indicatorNewTitle: '没有上传', 87 | indicatorSuccessTitle: '上传', 88 | indicatorErrorTitle: '上传错误', 89 | indicatorLoadingTitle: '上传 ...' 90 | }, 91 | previewZoomButtonTitles: { 92 | prev: '预览上一个文件', 93 | next: '预览下一个文件', 94 | toggleheader: '缩放', 95 | fullscreen: '全屏', 96 | borderless: '无边界模式', 97 | close: '关闭当前预览' 98 | } 99 | }; 100 | })(window.jQuery); 101 | -------------------------------------------------------------------------------- /src/main/resources/static/js/bootstrap-table-zh-CN.js: -------------------------------------------------------------------------------- 1 | /** 2 | * Bootstrap Table Chinese translation 3 | * Author: Zhixin Wen 4 | */ 5 | (function ($) { 6 | 'use strict'; 7 | 8 | $.fn.bootstrapTable.locales['zh-CN'] = { 9 | formatLoadingMessage: function () { 10 | return '正在努力地加载数据中,请稍候……'; 11 | }, 12 | formatRecordsPerPage: function (pageNumber) { 13 | return '每页显示 ' + pageNumber + ' 条记录'; 14 | }, 15 | formatShowingRows: function (pageFrom, pageTo, totalRows) { 16 | return '显示第 ' + pageFrom + ' 到第 ' + pageTo + ' 条记录,总共 ' + totalRows + ' 条记录'; 17 | }, 18 | formatSearch: function () { 19 | return '搜索'; 20 | }, 21 | formatNoMatches: function () { 22 | return '没有找到匹配的记录'; 23 | }, 24 | formatPaginationSwitch: function () { 25 | return '隐藏/显示分页'; 26 | }, 27 | formatRefresh: function () { 28 | return '刷新'; 29 | }, 30 | formatToggle: function () { 31 | return '切换'; 32 | }, 33 | formatColumns: function () { 34 | return '列'; 35 | }, 36 | formatExport: function () { 37 | return '导出数据'; 38 | }, 39 | formatClearFilters: function () { 40 | return '清空过滤'; 41 | } 42 | }; 43 | 44 | $.extend($.fn.bootstrapTable.defaults, $.fn.bootstrapTable.locales['zh-CN']); 45 | 46 | })(jQuery); 47 | -------------------------------------------------------------------------------- /src/main/resources/static/js/dark-unica.js: -------------------------------------------------------------------------------- 1 | /* 2 | Highcharts JS v6.0.7 (2018-02-16) 3 | 4 | (c) 2009-2017 Torstein Honsi 5 | 6 | License: www.highcharts.com/license 7 | */ 8 | (function(a){"object"===typeof module&&module.exports?module.exports=a:a(Highcharts)})(function(a){a.createElement("link",{href:"https://fonts.googleapis.com/css?family\x3dUnica+One",rel:"stylesheet",type:"text/css"},null,document.getElementsByTagName("head")[0]);a.theme={colors:"#2b908f #90ee7e #f45b5b #7798BF #aaeeee #ff0066 #eeaaee #55BF3B #DF5353 #7798BF #aaeeee".split(" "),chart:{backgroundColor:{linearGradient:{x1:0,y1:0,x2:1,y2:1},stops:[[0,"#2a2a2b"],[1,"#3e3e40"]]},style:{fontFamily:"'Unica One', sans-serif"}, 9 | plotBorderColor:"#606063"},title:{style:{color:"#E0E0E3",textTransform:"uppercase",fontSize:"20px"}},subtitle:{style:{color:"#E0E0E3",textTransform:"uppercase"}},xAxis:{gridLineColor:"#707073",labels:{style:{color:"#E0E0E3"}},lineColor:"#707073",minorGridLineColor:"#505053",tickColor:"#707073",title:{style:{color:"#A0A0A3"}}},yAxis:{gridLineColor:"#707073",labels:{style:{color:"#E0E0E3"}},lineColor:"#707073",minorGridLineColor:"#505053",tickColor:"#707073",tickWidth:1,title:{style:{color:"#A0A0A3"}}}, 10 | tooltip:{backgroundColor:"rgba(0, 0, 0, 0.85)",style:{color:"#F0F0F0"}},plotOptions:{series:{dataLabels:{color:"#B0B0B3"},marker:{lineColor:"#333"}},boxplot:{fillColor:"#505053"},candlestick:{lineColor:"white"},errorbar:{color:"white"}},legend:{itemStyle:{color:"#E0E0E3"},itemHoverStyle:{color:"#FFF"},itemHiddenStyle:{color:"#606063"}},credits:{style:{color:"#666"}},labels:{style:{color:"#707073"}},drilldown:{activeAxisLabelStyle:{color:"#F0F0F3"},activeDataLabelStyle:{color:"#F0F0F3"}},navigation:{buttonOptions:{symbolStroke:"#DDDDDD", 11 | theme:{fill:"#505053"}}},rangeSelector:{buttonTheme:{fill:"#505053",stroke:"#000000",style:{color:"#CCC"},states:{hover:{fill:"#707073",stroke:"#000000",style:{color:"white"}},select:{fill:"#000003",stroke:"#000000",style:{color:"white"}}}},inputBoxBorderColor:"#505053",inputStyle:{backgroundColor:"#333",color:"silver"},labelStyle:{color:"silver"}},navigator:{handles:{backgroundColor:"#666",borderColor:"#AAA"},outlineColor:"#CCC",maskFill:"rgba(255,255,255,0.1)",series:{color:"#7798BF",lineColor:"#A6C7ED"}, 12 | xAxis:{gridLineColor:"#505053"}},scrollbar:{barBackgroundColor:"#808083",barBorderColor:"#808083",buttonArrowColor:"#CCC",buttonBackgroundColor:"#606063",buttonBorderColor:"#606063",rifleColor:"#FFF",trackBackgroundColor:"#404043",trackBorderColor:"#404043"},legendBackgroundColor:"rgba(0, 0, 0, 0.5)",background2:"#505053",dataLabelsColor:"#B0B0B3",textColor:"#C0C0C0",contrastTextColor:"#F0F0F3",maskColor:"rgba(255,255,255,0.3)"};a.setOptions(a.theme)}); 13 | -------------------------------------------------------------------------------- /src/main/resources/static/js/dark-unica.js.map: -------------------------------------------------------------------------------- 1 | { 2 | "version":3, 3 | "file":"", 4 | "lineCount":12, 5 | "mappings":"A;;;;;;;AAQC,SAAQ,CAACA,CAAD,CAAU,CACO,QAAtB,GAAI,MAAOC,OAAX,EAAkCA,MAAAC,QAAlC,CACID,MAAAC,QADJ,CACqBF,CADrB,CAGIA,CAAA,CAAQG,UAAR,CAJW,CAAlB,CAAA,CAMC,QAAQ,CAACA,CAAD,CAAa,CAmOjBA,CAtNEC,cAAA,CAAyB,MAAzB,CAAiC,CAC7BC,KAAM,sDADuB,CAE7BC,IAAK,YAFwB,CAG7BC,KAAM,UAHuB,CAAjC,CAIG,IAJH,CAISC,QAAAC,qBAAA,CAA8B,MAA9B,CAAA,CAAsC,CAAtC,CAJT,CAsNFN,EAhNEO,MAAA,CAAmB,CACfC,OAAQ,yFAAA,MAAA,CAAA,GAAA,CADO,CAIfC,MAAO,CACHC,gBAAiB,CACbC,eAAgB,CACZC,GAAI,CADQ,CAEZC,GAAI,CAFQ,CAGZC,GAAI,CAHQ,CAIZC,GAAI,CAJQ,CADH,CAObC,MAAO,CACH,CAAC,CAAD,CAAI,SAAJ,CADG,CAEH,CAAC,CAAD,CAAI,SAAJ,CAFG,CAPM,CADd,CAaHC,MAAO,CACHC,WAAY,yBADT,CAbJ;AAgBHC,gBAAiB,SAhBd,CAJQ,CAsBfC,MAAO,CACHH,MAAO,CACHI,MAAO,SADJ,CAEHC,cAAe,WAFZ,CAGHC,SAAU,MAHP,CADJ,CAtBQ,CA6BfC,SAAU,CACNP,MAAO,CACHI,MAAO,SADJ,CAEHC,cAAe,WAFZ,CADD,CA7BK,CAmCfG,MAAO,CACHC,cAAe,SADZ,CAEHC,OAAQ,CACJV,MAAO,CACHI,MAAO,SADJ,CADH,CAFL,CAOHO,UAAW,SAPR,CAQHC,mBAAoB,SARjB,CASHC,UAAW,SATR,CAUHV,MAAO,CACHH,MAAO,CACHI,MAAO,SADJ,CADJ,CAVJ,CAnCQ,CAoDfU,MAAO,CACHL,cAAe,SADZ,CAEHC,OAAQ,CACJV,MAAO,CACHI,MAAO,SADJ,CADH,CAFL,CAOHO,UAAW,SAPR,CAQHC,mBAAoB,SARjB,CASHC,UAAW,SATR,CAUHE,UAAW,CAVR,CAWHZ,MAAO,CACHH,MAAO,CACHI,MAAO,SADJ,CADJ,CAXJ,CApDQ;AAqEfY,QAAS,CACLvB,gBAAiB,qBADZ,CAELO,MAAO,CACHI,MAAO,SADJ,CAFF,CArEM,CA2Efa,YAAa,CACTC,OAAQ,CACJC,WAAY,CACRf,MAAO,SADC,CADR,CAIJgB,OAAQ,CACJT,UAAW,MADP,CAJJ,CADC,CASTU,QAAS,CACLC,UAAW,SADN,CATA,CAYTC,YAAa,CACTZ,UAAW,OADF,CAZJ,CAeTa,SAAU,CACNpB,MAAO,OADD,CAfD,CA3EE,CA8FfqB,OAAQ,CACJC,UAAW,CACPtB,MAAO,SADA,CADP,CAIJuB,eAAgB,CACZvB,MAAO,MADK,CAJZ,CAOJwB,gBAAiB,CACbxB,MAAO,SADM,CAPb,CA9FO,CAyGfyB,QAAS,CACL7B,MAAO,CACHI,MAAO,MADJ,CADF,CAzGM,CA8GfM,OAAQ,CACJV,MAAO,CACHI,MAAO,SADJ,CADH,CA9GO,CAoHf0B,UAAW,CACPC,qBAAsB,CAClB3B,MAAO,SADW,CADf,CAIP4B,qBAAsB,CAClB5B,MAAO,SADW,CAJf,CApHI,CA6Hf6B,WAAY,CACRC,cAAe,CACXC,aAAc,SADH;AAEX7C,MAAO,CACH8C,KAAM,SADH,CAFI,CADP,CA7HG,CAuIfC,cAAe,CACXC,YAAa,CACTF,KAAM,SADG,CAETG,OAAQ,SAFC,CAGTvC,MAAO,CACHI,MAAO,MADJ,CAHE,CAMToC,OAAQ,CACJC,MAAO,CACHL,KAAM,SADH,CAEHG,OAAQ,SAFL,CAGHvC,MAAO,CACHI,MAAO,OADJ,CAHJ,CADH,CAQJsC,OAAQ,CACJN,KAAM,SADF,CAEJG,OAAQ,SAFJ,CAGJvC,MAAO,CACHI,MAAO,OADJ,CAHH,CARJ,CANC,CADF,CAwBXuC,oBAAqB,SAxBV,CAyBXC,WAAY,CACRnD,gBAAiB,MADT,CAERW,MAAO,QAFC,CAzBD,CA6BXyC,WAAY,CACRzC,MAAO,QADC,CA7BD,CAvIA,CAyKf0C,UAAW,CACPC,QAAS,CACLtD,gBAAiB,MADZ,CAELuD,YAAa,MAFR,CADF,CAKPC,aAAc,MALP,CAMPC,SAAU,uBANH,CAOPhC,OAAQ,CACJd,MAAO,SADH,CAEJO,UAAW,SAFP,CAPD;AAWPH,MAAO,CACHC,cAAe,SADZ,CAXA,CAzKI,CAyLf0C,UAAW,CACPC,mBAAoB,SADb,CAEPC,eAAgB,SAFT,CAGPC,iBAAkB,MAHX,CAIPC,sBAAuB,SAJhB,CAKPC,kBAAmB,SALZ,CAMPC,WAAY,MANL,CAOPC,qBAAsB,SAPf,CAQPC,iBAAkB,SARX,CAzLI,CAqMfC,sBAAuB,oBArMR,CAsMfC,YAAa,SAtME,CAuMfC,gBAAiB,SAvMF,CAwMfC,UAAW,SAxMI,CAyMfC,kBAAmB,SAzMJ,CA0MfC,UAAW,uBA1MI,CAgNrBlF,EAFEmF,WAAA,CAEFnF,CAFwBO,MAAtB,CAjOe,CANtB;", 6 | "sources":["Input_0"], 7 | "names":["factory","module","exports","Highcharts","createElement","href","rel","type","document","getElementsByTagName","theme","colors","chart","backgroundColor","linearGradient","x1","y1","x2","y2","stops","style","fontFamily","plotBorderColor","title","color","textTransform","fontSize","subtitle","xAxis","gridLineColor","labels","lineColor","minorGridLineColor","tickColor","yAxis","tickWidth","tooltip","plotOptions","series","dataLabels","marker","boxplot","fillColor","candlestick","errorbar","legend","itemStyle","itemHoverStyle","itemHiddenStyle","credits","drilldown","activeAxisLabelStyle","activeDataLabelStyle","navigation","buttonOptions","symbolStroke","fill","rangeSelector","buttonTheme","stroke","states","hover","select","inputBoxBorderColor","inputStyle","labelStyle","navigator","handles","borderColor","outlineColor","maskFill","scrollbar","barBackgroundColor","barBorderColor","buttonArrowColor","buttonBackgroundColor","buttonBorderColor","rifleColor","trackBackgroundColor","trackBorderColor","legendBackgroundColor","background2","dataLabelsColor","textColor","contrastTextColor","maskColor","setOptions"] 8 | } 9 | -------------------------------------------------------------------------------- /src/main/resources/static/js/highcharts-zh_CN.js: -------------------------------------------------------------------------------- 1 | (function(H){var protocol=window.location.protocol;var defaultOptionsZhCn={lang:{contextButtonTitle:"图表导出菜单",decimalPoint:".",downloadJPEG:"下载JPEG图片",downloadPDF:"下载PDF文件",downloadPNG:"下载PNG文件",downloadSVG:"下载SVG文件",drillUpText:"返回 {series.name}",invalidDate:"无效的时间",loading:"加载中...",months:["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"],noData:"没有数据",numericSymbols:null,printChart:"打印图表",resetZoom:"重置缩放比例",resetZoomTitle:"重置为原始大小",shortMonths:["一月","二月","三月","四月","五月","六月","七月","八月","九月","十月","十一月","十二月"],thousandsSep:",",weekdays:["星期天","星期一","星期二","星期三","星期四","星期五","星期六"],rangeSelectorFrom:"开始时间",rangeSelectorTo:"结束时间",rangeSelectorZoom:"范围",zoomIn:"缩小",zoomOut:"放大"},global:{canvasToolsURL:protocol+"//cdn.hcharts.cn/highcharts/modules/canvas-tools.js",VMLRadialGradientURL:protocol+ +"//cdn.hcharts.cn/highcharts/gfx/vml-radial-gradient.png"},title:{text:"图表标题"},tooltip:{dateTimeLabelFormats:{millisecond:"%H:%M:%S.%L",second:"%H:%M:%S",minute:"%H:%M",hour:"%H:%M",day:"%Y-%m-%d",week:"%Y-%m-%d",month:"%Y-%m",year:"%Y"},split:false},exporting:{url:protocol+"//export.highcharts.com.cn"},credits:{text:"Highcharts.com.cn",href:"https://www.highcharts.com.cn"},xAxis:{dateTimeLabelFormats:{millisecond:"%H:%M:%S.%L",second:"%H:%M:%S",minute:"%H:%M",hour:"%H:%M",day:"%Y-%m-%d",week:"%Y-%m",month:"%Y-%m",year:"%Y"}},rangeSelector:{inputDateFormat:"%Y-%m-%d",buttonTheme:{width:"auto",style:{fontSize:"12px",padding:"4px"}},buttons:[{type:"month",count:1,text:"月"},{type:"month",count:3,text:"季度"},{type:"month",count:6,text:"半年"},{type:"ytd",text:"YTD"},{type:"year",count:1,text:"年"},{type:"all",text:"所有"}]},plotOptions:{series:{dataGrouping:{dateTimeLabelFormats:{millisecond:["%Y-%m-%d %H:%M:%S.%L","%Y-%m-%d %H:%M:%S.%L"," ~ %H:%M:%S.%L"],second:["%Y-%m-%d %H:%M:%S","%Y-%m-%d %H:%M:%S"," ~ %H:%M:%S"],minute:["%Y-%m-%d %H:%M","%Y-%m-%d %H:%M"," ~ %H:%M"],hour:["%Y-%m-%d %H:%M","%Y-%m-%d %H:%M"," ~ %H:%M"],day:["%Y-%m-%d","%Y-%m-%d"," ~ %Y-%m-%d"],week:["%Y-%m-%d","%Y-%m-%d"," ~ %Y-%m-%d"],month:["%Y-%m","%Y-%m"," ~ %Y-%m"],year:["%Y","%Y"," ~ %Y"]}}},ohlc:{tooltip:{split:false,pointFormat:' {series.name}
'+"开盘:{point.open}
"+"最高:{point.high}
"+"最低:{point.low}
"+"收盘:{point.close}
"}},candlestick:{tooltip:{split:false,pointFormat:' {series.name}
'+"开盘:{point.open}
"+"最高:{point.high}
"+"最低:{point.low}
"+"收盘:{point.close}
"}}}};H.setOptions(defaultOptionsZhCn)})(Highcharts); -------------------------------------------------------------------------------- /src/main/resources/templates/layout.html: -------------------------------------------------------------------------------- 1 | 2 | 5 | 6 | 7 | Bushmaster 8 | 9 | 10 | 11 | 12 | 13 | 14 | 15 | 16 | 17 | 18 | 19 | 20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 | 37 | 38 | 39 | 40 | 41 | 42 | 43 | 56 | 57 | 58 |
59 | 60 | 81 | 82 |
83 |
84 |
85 |
86 | 87 | 88 | 89 | 90 | 91 | 92 | -------------------------------------------------------------------------------- /src/main/resources/templates/scenarioException.html: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | Bushmaster 9 | 10 | 11 |
12 |
13 |
14 |
15 | 16 | -------------------------------------------------------------------------------- /src/main/resources/templates/scenarioList.html: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | Bushmaster 9 | 10 | 11 |
12 | 13 |
14 |
15 |
16 |
17 |

场景列表

18 |
19 |
20 | 29 |
30 |
31 |
32 |
33 |
34 |
35 |
36 |
37 | 38 | -------------------------------------------------------------------------------- /src/main/resources/templates/scenarioNewResultList.html: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | Bushmaster 9 | 10 | 11 |
12 |
13 |
14 |
15 |

最新结果

16 |
17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 | 25 |
26 | 27 | -------------------------------------------------------------------------------- /src/main/resources/templates/scenarioRealTimeChart.html: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | Bushmaster 9 | 10 | 11 |
12 |
13 | 14 | 15 | 16 | 17 |
18 |
19 |
20 | 21 | 22 | 23 | 24 | 25 | 26 | 27 | 28 | 29 | 30 | 31 | 32 | 33 | 34 | 35 | 36 |
请求名称平均响应时间(ms)最小响应时间(ms)最大响应时间(ms)标准方差错误百分比(%)每秒请求处理能力每秒接收数据量(KB/s)每秒发送数据量(KB/s)平均页面大小(Bytes)
37 |
38 |
39 | 40 | 41 | 42 | 43 |
44 | 45 | -------------------------------------------------------------------------------- /src/main/resources/templates/scenarioResultListById.html: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | Bushmaster 9 | 10 | 11 |
12 | 13 |
14 |
15 |
16 |

结果列表

17 |
18 |
19 |
20 |
21 |
22 |
23 |
24 |
25 | 26 |
27 | 28 | -------------------------------------------------------------------------------- /src/main/resources/templates/scenarioSampleDetailChart.html: -------------------------------------------------------------------------------- 1 | 2 | 6 | 7 | 8 | Bushmaster 9 | 10 | 11 |
12 |
13 | 14 | 15 | 16 | 17 |
18 |
19 | 20 | 21 |
22 | 23 | -------------------------------------------------------------------------------- /src/test/java/com/berserker/architecture/ScenarioServiceTests.java: -------------------------------------------------------------------------------- 1 | package com.berserker.architecture; 2 | 3 | import com.alibaba.fastjson.JSONObject; 4 | import com.berserker.architecture.service.ScenarioInfoService; 5 | import com.fasterxml.jackson.databind.ObjectMapper; 6 | import org.junit.After; 7 | import org.junit.Before; 8 | import org.junit.Test; 9 | import org.junit.runner.RunWith; 10 | import org.springframework.beans.factory.annotation.Autowired; 11 | import org.springframework.boot.test.context.SpringBootTest; 12 | import org.springframework.test.context.junit4.SpringRunner; 13 | import org.springframework.test.context.web.WebAppConfiguration; 14 | 15 | import java.util.Map; 16 | 17 | 18 | @RunWith(SpringRunner.class) 19 | @SpringBootTest(classes = ArchitectureApplication.class) 20 | @WebAppConfiguration 21 | public class ScenarioServiceTests { 22 | @Autowired 23 | private ScenarioInfoService scenarioInfoService; 24 | 25 | @Before 26 | public void before() { 27 | } 28 | 29 | @Test 30 | public void contextLoads() { 31 | } 32 | 33 | @Test 34 | public void testGetScenarioInfoByPageList() { 35 | Map result = scenarioInfoService.getScenarioInfoByPageList(0, 15, ""); 36 | String jsonResult = JSONObject.toJSONString(result); 37 | System.out.println(jsonResult); 38 | } 39 | 40 | // @Test 41 | // public void testAddScenarioInfo() throws Exception { 42 | // for (int i = 0; i < 50; ++i) { 43 | // ScenarioInfo scenarioInfo = new ScenarioInfo(); 44 | // scenarioInfo.setScenarioName("第" + i + "个测试场景"); 45 | // scenarioInfo.setScenarioDescription("第" + i + "个测试场景描述"); 46 | // scenarioInfo.setCreateTime(new Date()); 47 | // scenarioInfo.setNumThreads(200); 48 | // scenarioInfo.setRampUp(180); 49 | // scenarioInfo.setDuration(5600); 50 | // scenarioInfo.setStatus(true); 51 | // 52 | // Map result = scenarioInfoService.addScenarioInfo(scenarioInfo); 53 | // // 格式化输出结果,这里使用Jackson 54 | // ObjectMapper mapper = new ObjectMapper(); 55 | // System.out.println(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(result)); 56 | // } 57 | // } 58 | 59 | // @Test 60 | // public void testModScenarioInfo() throws Exception { 61 | // ScenarioInfo scenarioInfo = new ScenarioInfo(); 62 | // scenarioInfo.setId(5); 63 | // scenarioInfo.setScenarioName("第5个测试场景-修改"); 64 | // scenarioInfo.setScenarioDescription("第5个测试场景描述-修改"); 65 | // scenarioInfo.setCreateTime(new Date()); 66 | // scenarioInfo.setNumThreads(200); 67 | // scenarioInfo.setRampUp(600); 68 | // scenarioInfo.setDuration(4800); 69 | // scenarioInfo.setStatus(true); 70 | // 71 | // Map result = scenarioInfoService.modScenarioInfo(scenarioInfo); 72 | // // 格式化输出结果,这里使用Jackson 73 | // ObjectMapper mapper = new ObjectMapper(); 74 | // System.out.println(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(result)); 75 | // } 76 | 77 | @Test 78 | public void testDelScenarioInfo() throws Exception { 79 | Map result = scenarioInfoService.delScenarioInfo(16); 80 | // 格式化输出结果,这里使用Jackson 81 | ObjectMapper mapper = new ObjectMapper(); 82 | System.out.println(mapper.writerWithDefaultPrettyPrinter().writeValueAsString(result)); 83 | } 84 | 85 | @After 86 | public void after() { 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/test/java/com/berserker/architecture/ScriptControllerTests.java: -------------------------------------------------------------------------------- 1 | package com.berserker.architecture; 2 | 3 | import org.junit.After; 4 | import org.junit.Before; 5 | import org.junit.runner.RunWith; 6 | import org.springframework.beans.factory.annotation.Autowired; 7 | import org.springframework.boot.test.context.SpringBootTest; 8 | import org.springframework.test.context.junit4.SpringRunner; 9 | import org.springframework.test.context.web.WebAppConfiguration; 10 | import org.springframework.test.web.servlet.MockMvc; 11 | import org.springframework.test.web.servlet.setup.MockMvcBuilders; 12 | import org.springframework.web.context.WebApplicationContext; 13 | 14 | @RunWith(SpringRunner.class) 15 | @SpringBootTest(classes = ArchitectureApplication.class) 16 | @WebAppConfiguration 17 | public class ScriptControllerTests { 18 | @Autowired 19 | private WebApplicationContext context; 20 | private MockMvc mock; 21 | 22 | @Before 23 | public void before() { 24 | mock = MockMvcBuilders.webAppContextSetup(context).build(); 25 | } 26 | 27 | @After 28 | public void after() {} 29 | } 30 | --------------------------------------------------------------------------------