├── src ├── main │ ├── java │ │ └── com │ │ │ ├── util │ │ │ ├── SignType.java │ │ │ ├── SignUtil.java │ │ │ ├── MapUtil.java │ │ │ └── SimpleHttpClient.java │ │ │ └── pressure │ │ │ ├── core │ │ │ ├── LoadConsumer.java │ │ │ ├── Cache.java │ │ │ ├── httputil │ │ │ │ ├── RequestInterface.java │ │ │ │ ├── ResultInfoMonitor.java │ │ │ │ ├── impl │ │ │ │ │ ├── LogResultInfoMonitor.java │ │ │ │ │ ├── RequestImpl.java │ │ │ │ │ └── SummaryMonitor.java │ │ │ │ └── AbstractRequest.java │ │ │ └── bean │ │ │ │ ├── HttpLog.java │ │ │ │ ├── ResultInfo.java │ │ │ │ └── RequestInfo.java │ │ │ └── Main.java │ └── resources │ │ └── diagram.png └── test │ └── java │ └── com │ └── tlong │ ├── TestThread.java │ ├── TestBoolean.java │ ├── TestString.java │ ├── ObjectTest.java │ ├── TestAbstracString.java │ ├── TestObject.java │ ├── TestLong.java │ └── TestChar.java ├── pom.xml └── README.md /src/main/java/com/util/SignType.java: -------------------------------------------------------------------------------- 1 | package com.util; 2 | 3 | public enum SignType { 4 | MD5,SHA1 5 | } 6 | -------------------------------------------------------------------------------- /src/main/resources/diagram.png: -------------------------------------------------------------------------------- https://raw.githubusercontent.com/NotInWine/pressure/HEAD/src/main/resources/diagram.png -------------------------------------------------------------------------------- /src/test/java/com/tlong/TestThread.java: -------------------------------------------------------------------------------- 1 | package com.tlong; 2 | 3 | 4 | 5 | public class TestThread { 6 | 7 | public static void main(String[] args) { 8 | } 9 | } 10 | -------------------------------------------------------------------------------- /src/main/java/com/pressure/core/LoadConsumer.java: -------------------------------------------------------------------------------- 1 | package com.pressure.core; 2 | 3 | /** 4 | * 函数接口 5 | * @param 输入 6 | * @param 输出 7 | * @author YL 8 | */ 9 | @FunctionalInterface 10 | public interface LoadConsumer { 11 | 12 | /** 13 | * 接口方法 14 | * @param i 15 | * @return 16 | */ 17 | R implement(I i); 18 | } 19 | -------------------------------------------------------------------------------- /src/main/java/com/pressure/core/Cache.java: -------------------------------------------------------------------------------- 1 | package com.pressure.core; 2 | 3 | import com.fasterxml.jackson.databind.ObjectMapper; 4 | 5 | import java.text.SimpleDateFormat; 6 | 7 | public class Cache { 8 | 9 | public static final ObjectMapper JSON_UTIL = new ObjectMapper().setDateFormat(new SimpleDateFormat("yyyy-MM-dd HH:mm:ss")); 10 | 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/pressure/core/httputil/RequestInterface.java: -------------------------------------------------------------------------------- 1 | package com.pressure.core.httputil; 2 | 3 | import com.pressure.core.bean.RequestInfo; 4 | 5 | 6 | /** 7 | * 发送请求的接口 8 | * 处理器 9 | * @author YL 10 | */ 11 | public interface RequestInterface { 12 | 13 | /** 14 | * 发送测试脚本 15 | * @param infos 指令集 按顺序执行 16 | */ 17 | void send(RequestInfo... infos); 18 | } 19 | -------------------------------------------------------------------------------- /src/test/java/com/tlong/TestBoolean.java: -------------------------------------------------------------------------------- 1 | package com.tlong; 2 | 3 | 4 | 5 | public class TestBoolean { 6 | 7 | public static void main(String[] args) { 8 | System.out.println(Boolean.logicalXor(false, false)); 9 | System.out.println(Boolean.logicalXor(true, true)); 10 | System.out.println(Boolean.logicalXor(false, true)); 11 | System.out.println(Boolean.logicalXor(true, false)); 12 | } 13 | } 14 | -------------------------------------------------------------------------------- /src/main/java/com/pressure/core/httputil/ResultInfoMonitor.java: -------------------------------------------------------------------------------- 1 | package com.pressure.core.httputil; 2 | 3 | import com.pressure.core.bean.ResultInfo; 4 | 5 | /** 6 | * 压测结果监听 7 | * @param 输出 8 | * @author YL 9 | */ 10 | public interface ResultInfoMonitor { 11 | 12 | /** 13 | * 加入记录进行统计 14 | * @param resultInfo 15 | */ 16 | void add(ResultInfo resultInfo); 17 | 18 | /** 19 | * 获得日志结果 20 | * @return 21 | */ 22 | OUT out(); 23 | 24 | /** 25 | * 用于打印 26 | * 系统会开启线程多次调用此方法,此方法何时调用out方法,以及以什么样的方式输出,由开发者自行定义 27 | * @param endNotice 最后一次通知了为 true 28 | * 默认的打印方法 29 | */ 30 | default void printOut(boolean endNotice) { 31 | OUT out = this.out(); 32 | if (out != null) { 33 | System.out.println(out); 34 | } 35 | } 36 | } 37 | -------------------------------------------------------------------------------- /src/test/java/com/tlong/TestString.java: -------------------------------------------------------------------------------- 1 | package com.tlong; 2 | 3 | 4 | 5 | public class TestString { 6 | 7 | public static void main(String[] args) { 8 | String s = "\uD83C\uDF3A"; 9 | System.out.println(s.codePointAt(0)); 10 | s.chars().forEach(i -> System.out.print((char) i + " ")); 11 | s.chars().forEach(i -> System.out.print("\\u" + Integer.toHexString(i))); 12 | System.out.println(); 13 | s.chars().forEach(i -> System.out.print(i + " ")); 14 | System.out.println(); 15 | s.codePoints().forEach(i -> System.out.print((char) i + " ")); 16 | s.codePoints().forEach(i -> System.out.print("\\u" + Integer.toHexString(i))); 17 | System.out.println(); 18 | s.codePoints().forEach(i -> System.out.print(i + " ")); 19 | 20 | System.out.println(s); 21 | System.out.println((char) 1111); 22 | } 23 | } 24 | -------------------------------------------------------------------------------- /src/test/java/com/tlong/ObjectTest.java: -------------------------------------------------------------------------------- 1 | package com.tlong; 2 | 3 | import java.util.concurrent.atomic.AtomicInteger; 4 | 5 | /** 6 | * 模拟DC 7 | */ 8 | public class ObjectTest { 9 | 10 | private static final Object BAR_COUNTER = new Object(); // 11 | private static final AtomicInteger COUNT = new AtomicInteger(0); // 12 | 13 | private static class Guest implements Runnable { 14 | 15 | private final long HOLD_TIME; 16 | 17 | public Guest() { 18 | HOLD_TIME = 0; 19 | } 20 | 21 | public Guest(long HOLD_TIME) { 22 | this.HOLD_TIME = HOLD_TIME; 23 | } 24 | 25 | @Override 26 | public void run() { 27 | String name = Thread.currentThread().getName(); 28 | System.out.println("【" + name + "】 QBTPD"); 29 | synchronized (BAR_COUNTER) { 30 | System.out.println("【" + name + "】 PDLWZ,BDLYFC,ZZDDQC"); 31 | try { 32 | BAR_COUNTER.wait(HOLD_TIME); // 进入等待 33 | } catch (InterruptedException e) { 34 | // 线程被调用 interrupt() 中断方法, 这里不测试这个情况 35 | e.printStackTrace(); 36 | } 37 | if (COUNT.getAndAdd(-1) <= 0) { 38 | COUNT.getAndAdd(+1); 39 | System.out.println("【" + name + "】 HAZH,WBYL"); 40 | return; 41 | } 42 | } 43 | } 44 | } 45 | 46 | public static void main(String[] args) { 47 | new Thread(new Guest(2000), "0").start(); 48 | new Thread(new Guest(5000), "1").start(); 49 | new Thread(new Guest(5000), "2").start(); 50 | new Thread(new Guest(5000), "3").start(); 51 | 52 | try { 53 | Thread.sleep(3000); 54 | } catch (InterruptedException e) { 55 | e.printStackTrace(); 56 | } 57 | for (int i = 0; i < 3; i++) { 58 | synchronized (BAR_COUNTER) { 59 | BAR_COUNTER.notify(); 60 | } 61 | } 62 | } 63 | } 64 | -------------------------------------------------------------------------------- /src/test/java/com/tlong/TestAbstracString.java: -------------------------------------------------------------------------------- 1 | package com.tlong; 2 | 3 | 4 | public class TestAbstracString { 5 | 6 | public static void main(String[] args) { 7 | int i = Integer.MAX_VALUE; 8 | System.out.println("int最大长度 二进制:" + Integer.toBinaryString(i)); 9 | System.out.println("int最大长度 二进制位数:" + Integer.toBinaryString(i).length()); 10 | while (true) { 11 | try { 12 | System.out.println(new char[i].length); 13 | } catch (OutOfMemoryError e) { 14 | i = i >> 1; 15 | // e.printStackTrace(); 16 | // 异常继续 17 | continue; 18 | } 19 | break; 20 | } 21 | 22 | int scale = 100000000; 23 | System.out.println("精度提升:" + i + " " + scale); 24 | while (true) { 25 | try { 26 | if ((Integer.MAX_VALUE - scale) < i) { 27 | scale = scale / 10; 28 | System.out.println("调整刻度:" + i + " " + scale); 29 | continue; 30 | } else { 31 | i += scale; 32 | } 33 | char[] c = new char[i]; 34 | c = null; 35 | } catch (OutOfMemoryError e) { 36 | if (scale == 1) { 37 | System.out.println("数组最大长度 十进制:" + i); 38 | System.out.println("数组最大长度 二进制:" + Integer.toBinaryString(i)); 39 | System.out.println("数组最大长度 二进制位数:" + Integer.toBinaryString(i).length()); 40 | break; 41 | } 42 | i -= scale; 43 | scale = scale / 10; 44 | System.out.println("调整刻度:" + i + " " + scale); 45 | } 46 | } 47 | 48 | try { 49 | char[] c = new char[i + 1]; 50 | } catch (OutOfMemoryError e) { 51 | e.printStackTrace(); 52 | System.out.println("再次校验通过"); 53 | System.out.println("比 Integer.MAX_VALUE 小:" + (Integer.MAX_VALUE - i)); 54 | return; 55 | } 56 | System.out.println("再次校未通过:" + i); 57 | } 58 | } 59 | -------------------------------------------------------------------------------- /src/main/java/com/pressure/core/bean/HttpLog.java: -------------------------------------------------------------------------------- 1 | package com.pressure.core.bean; 2 | 3 | import com.util.SimpleHttpClient; 4 | import org.apache.http.Header; 5 | 6 | import java.util.Arrays; 7 | import java.util.Map; 8 | 9 | /** 10 | * 请求日志 11 | * @author YL 12 | **/ 13 | public class HttpLog { 14 | 15 | private final String url; 16 | 17 | private final int httpState; 18 | 19 | private final String responseBody; 20 | 21 | private final Map params; 22 | 23 | private final Header[] requestHeaders; 24 | 25 | private final Header[] responseHeaders; 26 | 27 | public HttpLog(String url, Map params, Header[] requestHeaders, SimpleHttpClient.Info info) { 28 | this.url = url; 29 | this.params = params; 30 | this.requestHeaders = requestHeaders; 31 | 32 | if (info != null) { 33 | this.httpState = info.getCode(); 34 | this.responseHeaders = info.getHeads(); 35 | this.responseBody = info.getBody(); 36 | } else { 37 | this.httpState = 0; 38 | this.responseHeaders = null; 39 | this.responseBody = null; 40 | } 41 | } 42 | 43 | @Override 44 | public String toString() { 45 | return "HttpLog{" + 46 | "url='" + url + '\'' + 47 | ", httpState=" + httpState + 48 | ", params=" + params + 49 | "\n, requestHeaders=" + Arrays.toString(requestHeaders) + 50 | "\n, responseHeaders=" + Arrays.toString(responseHeaders) + 51 | "\n, responseBody='" + responseBody + '\'' + 52 | "}\n"; 53 | } 54 | 55 | public String getUrl() { 56 | return url; 57 | } 58 | 59 | public int getHttpState() { 60 | return httpState; 61 | } 62 | 63 | public String getResponseBody() { 64 | return responseBody; 65 | } 66 | 67 | public Map getParams() { 68 | return params; 69 | } 70 | 71 | public Header[] getRequestHeaders() { 72 | return requestHeaders; 73 | } 74 | 75 | public Header[] getResponseHeaders() { 76 | return responseHeaders; 77 | } 78 | } 79 | -------------------------------------------------------------------------------- /src/test/java/com/tlong/TestObject.java: -------------------------------------------------------------------------------- 1 | package com.tlong; 2 | 3 | import java.util.concurrent.atomic.AtomicInteger; 4 | 5 | /** 6 | * wait 方法, notify 方法测试 7 | * 限量供应无序排队的小面馆 8 | * @see Object#wait() 9 | * @see Object#notify() 10 | */ 11 | public class TestObject { 12 | 13 | private static final Object LOCK = new Object(); // 锁 14 | private static final AtomicInteger FOOD_NUMBER = new AtomicInteger(0); // 小面的数量 15 | 16 | private static class Customer implements Runnable { 17 | 18 | private final int TIME_OUT; // 超时时间 毫秒。 0 不会超时 19 | 20 | public Customer(int timeOut) { 21 | TIME_OUT = timeOut; 22 | } 23 | 24 | @Override 25 | public void run() { 26 | synchronized (LOCK) { 27 | System.out.println("【" + Thread.currentThread().getName() + "】: 来到前台排队取餐"); 28 | long b = System.currentTimeMillis(); 29 | try { 30 | LOCK.wait(TIME_OUT); 31 | } catch (InterruptedException e) { 32 | e.printStackTrace(); // 线程中断(Thread.interrupted()) 会抛出这个异常。 本次不测试此特性 33 | } 34 | if (FOOD_NUMBER.getAndAdd(-1) <= 0) { 35 | FOOD_NUMBER.addAndGet(1); 36 | System.out.println("【" + Thread.currentThread().getName() + "】: 还没有我的餐么?太遗憾了 我已经等了 " + (System.currentTimeMillis() - b) / 1000 + "秒 我不要了再见!"); 37 | return; 38 | } 39 | System.out.println("【" + Thread.currentThread().getName() + "】: 虽然等了 " + (System.currentTimeMillis() - b) / 1000 + "秒, 但是 好香的饭菜谢谢! 我要去吃饭了!"); 40 | } 41 | } 42 | } 43 | 44 | public static void main(String[] args) throws InterruptedException { 45 | System.out.println(3*0.1 == 0.3); 46 | System.out.println(3*0.2 == 0.6); 47 | System.out.println(3*0.1); 48 | System.out.println(3*0.2); 49 | System.out.println(2*0.4); 50 | // 四个客人来到 小店取餐 51 | new Thread(new Customer(1000), "浩克").start(); 52 | new Thread(new Customer(5000), "雷神").start(); 53 | new Thread(new Customer(5000), "神奇女侠").start(); 54 | new Thread(new Customer(5000), "钢铁侠").start(); 55 | 56 | // 后厨开始做饭 57 | // 但是要1.5秒才能做好 58 | // 急躁的客人,运气不好的客人 吃不到饭咯 59 | Thread.sleep(1500); 60 | 61 | int number = 2; 62 | synchronized (LOCK) { 63 | for (int i = 0; i < number; i++) { 64 | FOOD_NUMBER.addAndGet(1); 65 | // 验证特性 66 | // notify 方法,只能负责唤醒线程。但不能指定线程唤醒顺序,与线程进入等待的时序无关 67 | LOCK.notify(); 68 | } 69 | } 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/com/pressure/core/bean/ResultInfo.java: -------------------------------------------------------------------------------- 1 | package com.pressure.core.bean; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | 5 | /** 6 | * 响应信息包装 7 | * @author YL 8 | */ 9 | public class ResultInfo { 10 | 11 | private final RequestInfo requestInfo; 12 | 13 | /** 14 | * 返回结果 15 | */ 16 | private JsonNode result; 17 | 18 | private final HttpLog httpLog; 19 | 20 | /** 21 | * 耗时(毫秒) 22 | */ 23 | private final long time; 24 | 25 | /** 26 | * 循环编号 27 | */ 28 | private final int batchId; 29 | 30 | /** 31 | * 开始事件毫秒 32 | */ 33 | private final long beginTime; 34 | 35 | private final State state; 36 | 37 | private final Throwable throwable; 38 | 39 | /** 40 | * 响应状态 41 | */ 42 | public enum State { 43 | 44 | /** 45 | * 成功 46 | */ 47 | SUCCESS, 48 | 49 | /** 50 | * 异常 51 | */ 52 | ERROR; 53 | } 54 | 55 | public ResultInfo(RequestInfo requestInfo, HttpLog httpLog, long time, long beginTime, int batchId, State state) { 56 | this.requestInfo = requestInfo; 57 | this.httpLog = httpLog; 58 | this.time = time; 59 | this.beginTime = beginTime; 60 | this.batchId = batchId; 61 | this.state = state; 62 | this.throwable = null; 63 | } 64 | 65 | public ResultInfo(RequestInfo requestInfo, HttpLog httpLog, long time, long beginTime, int batchId, State state, Throwable throwable) { 66 | this.requestInfo = requestInfo; 67 | this.httpLog = httpLog; 68 | this.time = time; 69 | this.beginTime = beginTime; 70 | this.batchId = batchId; 71 | this.state = state; 72 | this.throwable = throwable; 73 | } 74 | 75 | public RequestInfo getRequestInfo() { 76 | return requestInfo; 77 | } 78 | 79 | public JsonNode getResult() { 80 | return result; 81 | } 82 | 83 | public void setResult(JsonNode result) { 84 | this.result = result; 85 | } 86 | 87 | public HttpLog getHttpLog() { 88 | return httpLog; 89 | } 90 | 91 | public long getTime() { 92 | return time; 93 | } 94 | 95 | public long getBeginTime() { 96 | return beginTime; 97 | } 98 | 99 | public State getState() { 100 | return state; 101 | } 102 | 103 | @Override 104 | public String toString() { 105 | String s = "" + 106 | "batchId=" + batchId + 107 | ", name=" + requestInfo.getName() + 108 | ", state=" + state + 109 | ", httpLog=" + httpLog + 110 | ", time=" + time; 111 | if (throwable != null) { 112 | s += ", throwable=" + throwable.getMessage(); 113 | } 114 | return s; 115 | } 116 | } 117 | -------------------------------------------------------------------------------- /src/main/java/com/pressure/core/httputil/impl/LogResultInfoMonitor.java: -------------------------------------------------------------------------------- 1 | package com.pressure.core.httputil.impl; 2 | 3 | import com.pressure.core.bean.ResultInfo; 4 | import com.pressure.core.httputil.ResultInfoMonitor; 5 | import org.slf4j.Logger; 6 | import org.slf4j.LoggerFactory; 7 | 8 | import java.io.BufferedWriter; 9 | import java.io.File; 10 | import java.io.FileWriter; 11 | import java.io.IOException; 12 | import java.util.ArrayList; 13 | import java.util.Comparator; 14 | import java.util.LinkedList; 15 | import java.util.List; 16 | 17 | /** 18 | * 日志监听器,将请求结果记录到 19 | * @see LogResultInfoMonitor#bufferedWriter 对应的文件中 20 | * @author YL 21 | **/ 22 | public class LogResultInfoMonitor implements ResultInfoMonitor { 23 | 24 | private final Logger logger = LoggerFactory.getLogger(this.getClass()); 25 | private final List logs = new LinkedList<>(); 26 | private final BufferedWriter bufferedWriter; 27 | 28 | public LogResultInfoMonitor(File logFile) { 29 | if (!logFile.exists()) { 30 | try { 31 | boolean newFile = logFile.createNewFile(); 32 | if (!newFile) { 33 | logger.warn("文件创建失败 {}", logFile.toString()); 34 | } 35 | } catch (IOException e) { 36 | logger.error("", e); 37 | } 38 | } 39 | try { 40 | this.bufferedWriter = new BufferedWriter(new FileWriter(logFile, true)); 41 | } catch (IOException e) { 42 | logger.error("文件流获取失败", e); 43 | throw new RuntimeException("文件流获取失败", e); 44 | } 45 | } 46 | 47 | @Override 48 | public void add(ResultInfo resultInfo) { 49 | synchronized (logs) { 50 | logs.add(resultInfo); 51 | } 52 | } 53 | 54 | @Override 55 | public String out() { 56 | if (logs.size() == 0) { 57 | return null; 58 | } 59 | List copy; 60 | synchronized (logs) { 61 | copy = new ArrayList<>(logs); 62 | logs.clear(); 63 | } 64 | copy.sort(Comparator.comparing(ResultInfo::getBeginTime)); 65 | copy.forEach(i -> { 66 | String str = i.toString(); 67 | try { 68 | bufferedWriter.write(str, 0, str.length()); 69 | bufferedWriter.newLine(); 70 | } catch (IOException e) { 71 | logger.error("", e); 72 | } 73 | }); 74 | 75 | return null; 76 | } 77 | 78 | @Override 79 | public void printOut(boolean endNotice) { 80 | this.out(); 81 | if (endNotice) { 82 | try { 83 | bufferedWriter.flush(); 84 | } catch (IOException e) { 85 | logger.error("", e); 86 | } 87 | try { 88 | bufferedWriter.close(); 89 | } catch (IOException e) { 90 | logger.error("", e); 91 | } 92 | } 93 | } 94 | } 95 | -------------------------------------------------------------------------------- /src/test/java/com/tlong/TestLong.java: -------------------------------------------------------------------------------- 1 | package com.tlong; 2 | 3 | 4 | import java.util.concurrent.atomic.AtomicInteger; 5 | 6 | public class TestLong { 7 | 8 | private static final Object COUNTER = new Object(); 9 | private static final AtomicInteger ATOMICINTEGER = new AtomicInteger(0); // 数量 10 | 11 | private static class Foodie implements Runnable { 12 | 13 | private final int TIME_OUT; // 0 为不会超时 14 | 15 | public Foodie(int timeOut) { 16 | TIME_OUT = timeOut; 17 | } 18 | 19 | public Foodie() { 20 | TIME_OUT = 0; 21 | } 22 | 23 | @Override 24 | public void run() { 25 | synchronized (COUNTER) { 26 | System.out.println("e1" + Thread.currentThread().getName()); 27 | try { 28 | COUNTER.wait(TIME_OUT); 29 | int c = ATOMICINTEGER.getAndAdd(-1); 30 | if (c > 0) { 31 | System.out.println("e1 " + Thread.currentThread().getName() + " 2 "); 32 | } else { 33 | ATOMICINTEGER.getAndAdd(1); 34 | System.out.println("e1 " + Thread.currentThread().getName() + " 3 "); 35 | } 36 | } catch (InterruptedException e) { 37 | e.printStackTrace(); 38 | } 39 | } 40 | } 41 | } 42 | 43 | private static class Boss implements Runnable { 44 | 45 | private final int COUNT; // 0 为不会超时 46 | 47 | public Boss(int count) { 48 | COUNT = count; 49 | } 50 | 51 | public Boss() { 52 | COUNT = 1; 53 | } 54 | 55 | @Override 56 | public void run() { 57 | System.out.println("b1" + Thread.currentThread().getName()); 58 | synchronized (COUNTER) { 59 | if (COUNT == Integer.MAX_VALUE) { 60 | COUNTER.notifyAll(); 61 | System.out.println("b1" + Thread.currentThread().getName() + " all"); 62 | } else { 63 | for (int i = 0; i < COUNT; i++) { 64 | ATOMICINTEGER.addAndGet(1); 65 | COUNTER.notify(); 66 | } 67 | System.out.println("b1" + Thread.currentThread().getName() + " " + COUNT); 68 | } 69 | } 70 | System.out.println("b1" + Thread.currentThread().getName() + " 11"); 71 | } 72 | } 73 | 74 | public static void main(String[] args) throws InterruptedException { 75 | new Thread(new Foodie()).start(); 76 | new Thread(new Foodie(200)).start(); 77 | new Thread(new Foodie()).start(); 78 | new Thread(new Foodie()).start(); 79 | new Thread(new Foodie(1000)).start(); 80 | 81 | new Thread(new Boss(1)).start(); 82 | 83 | Thread.sleep(500); 84 | new Thread(new Boss(1)).start(); 85 | 86 | System.out.println(System.getProperty("file.encoding")); 87 | } 88 | } 89 | -------------------------------------------------------------------------------- /src/test/java/com/tlong/TestChar.java: -------------------------------------------------------------------------------- 1 | package com.tlong; 2 | 3 | 4 | import java.util.LinkedHashSet; 5 | import java.util.Set; 6 | 7 | /** 8 | * UTF-16是16bit最多编码65536,那大于65536如何编码?Unicode 标准制定组想出的办法是,从这65536个编码里,拿出2048个,规定他们是「Surrogates」,让他们两个为一组,来代表编号大于65536的那些字符。 9 | * 编号为 U+D800 至 U+DBFF 的规定为「High Surrogates」,共1024个。 10 | * 编号为 U+DC00 至 U+DFFF 的规定为「Low Surrogates」,也是1024个。 11 | */ 12 | public class TestChar { 13 | 14 | /** 15 | * 高位最小值 U+D800 16 | */ 17 | public static final char MIN_HIGH_SURROGATE = Character.MIN_HIGH_SURROGATE; 18 | 19 | /** 20 | * 低位最小值 U+DC00 21 | */ 22 | public static final char MIN_LOW_SURROGATE = Character.MIN_LOW_SURROGATE; 23 | 24 | /** 25 | * 65536 26 | * 最小代码补充位置 选取为两个字节 即长度为16的二进制的最大值。 超过这个值即为补充代码点详情请强查看: 27 | * 28 | * @see java.lang.Character#toCodePoint(char, char) 源码 29 | */ 30 | private final static int MIN_SUPPLEMENTARY_CODE_POINT = Character.MIN_SUPPLEMENTARY_CODE_POINT; 31 | 32 | public static void main(String[] args) { 33 | // 🌺好看 34 | String s = "\uD83C\uDF3A好看"; 35 | System.out.println("原始字符:\n" + s); 36 | System.out.println(); 37 | // 处理char 38 | Set chars = new LinkedHashSet<>(); 39 | s.codePoints().forEach(i -> { 40 | if (MIN_SUPPLEMENTARY_CODE_POINT < i) { 41 | System.out.println("高位合并后的 二进制:\n" + Integer.toBinaryString(i)); 42 | System.out.println(); 43 | // 还原:去除补位 44 | i -= MIN_SUPPLEMENTARY_CODE_POINT; 45 | System.out.println("还原后的代码点:\n" + i); 46 | System.out.println(); 47 | System.out.println("还原后的代码点的二进制:\n" + Integer.toBinaryString(i)); 48 | System.out.println(); 49 | 50 | /** 51 | * 取出二进制,高十位 和低十位 52 | * 为什么是十位不是十六位 因为utf-16 使用Surrogates标识字符时 占用了每个编码的二进制的高六位,用于区分Surrogates 和 常规编码 53 | * 「High Surrogates」「Low Surrogates」的最小值分别是 54 | * @see MIN_HIGH_SURROGATE 55 | * @see MIN_LOW_SURROGATE 56 | */ 57 | int low = i & Integer.parseInt("1111111111", 2); // 取出低十位 58 | int high = i >> 10; // 取出高十位 59 | System.out.println("「High Surrogates」+「Low Surrogates」 的二进制:\n" + Integer.toBinaryString(high) + " + " + Integer.toBinaryString(low)); 60 | System.out.println(); 61 | 62 | // 补位打印 63 | low = MIN_LOW_SURROGATE + low; 64 | high = MIN_HIGH_SURROGATE + high; 65 | 66 | chars.add(high); 67 | chars.add(low); 68 | } else { 69 | chars.add(i); 70 | } 71 | }); 72 | 73 | 74 | System.out.println("对应的 utf-16 字符 \\u + 十六进制代码点:"); 75 | chars.forEach(i -> System.out.print("\\u" + Integer.toHexString(i))); 76 | System.out.println(); 77 | 78 | System.out.println("解码后字符:"); 79 | chars.forEach(i -> System.out.print((char) i.intValue())); 80 | System.out.println(); 81 | } 82 | } 83 | -------------------------------------------------------------------------------- /src/main/java/com/util/SignUtil.java: -------------------------------------------------------------------------------- 1 | package com.util; 2 | 3 | 4 | import java.io.*; 5 | import java.nio.ByteBuffer; 6 | import java.nio.channels.FileChannel; 7 | import java.util.*; 8 | import java.util.function.Function; 9 | 10 | /** 11 | * 12 | * http 请求 签名工具 13 | * @author jryc 14 | * 15 | */ 16 | public class SignUtil { 17 | 18 | /** 19 | * 20 | * @param file 21 | * @param newFile 22 | * @param limit 批次处理字节长度 23 | * @param function bytes 处理 24 | */ 25 | public static void doFinalFile(File file, File newFile, int limit, Function function) { 26 | try { 27 | InputStream is = new FileInputStream(file); 28 | OutputStream os = new FileOutputStream(newFile); 29 | byte[] bytes = new byte[limit]; 30 | while (is.read(bytes) > 0) { 31 | byte[] e = function.apply(bytes); 32 | bytes = new byte[limit]; 33 | os.write(e, 0, e.length); 34 | } 35 | os.close(); 36 | is.close(); 37 | System.out.println("write success"); 38 | } catch (Exception e) { 39 | e.printStackTrace(); 40 | } 41 | } 42 | 43 | /** 44 | * 45 | * @param file 46 | * @param newFile 47 | * @param limit 批次处理字节长度 48 | * @param function bytes 处理 49 | */ 50 | public static void doFinalFileNIO(File file, File newFile, int limit, Function function) throws Exception { 51 | FileInputStream fin = null; 52 | FileOutputStream fos = null; 53 | try { 54 | fin = new FileInputStream(file); 55 | FileChannel channel = fin.getChannel(); 56 | 57 | int capacity = limit;// 字节 58 | ByteBuffer bf = ByteBuffer.allocate(capacity); 59 | System.out.println("限制是:" + bf.limit() + "容量是:" + bf.capacity() 60 | + "位置是:" + bf.position()); 61 | 62 | fos = new FileOutputStream(newFile); 63 | FileChannel channelout = fos.getChannel(); 64 | while (channel.read(bf) != -1) { 65 | /* 66 | * 注意,读取后,将位置置为0,将limit置为容量, 以备下次读入到字节缓冲中,从0开始存储 67 | */ 68 | bf.clear(); 69 | byte[] bytes = bf.array(); 70 | // 写入数据 71 | channelout.write(ByteBuffer.wrap(function.apply(bytes))); 72 | } 73 | channel.close(); 74 | System.out.println("write success"); 75 | } finally { 76 | if (fin != null) { 77 | try { 78 | fin.close(); 79 | } catch (IOException e) { 80 | e.printStackTrace(); 81 | } 82 | } 83 | if (fos != null) { 84 | try { 85 | fos.close(); 86 | } catch (IOException e) { 87 | e.printStackTrace(); 88 | } 89 | } 90 | } 91 | } 92 | 93 | 94 | 95 | /** 96 | * 把数组所有元素排序,并按照“参数=参数值”的模式用“&”字符拼接成字符串 97 | * @param params 需要排序并参与字符拼接的参数组 98 | * @return 拼接后字符串 99 | */ 100 | public static String createLinkString(Map params) { 101 | 102 | List keys = new ArrayList(params.keySet()); 103 | Collections.sort(keys); 104 | 105 | String prestr = ""; 106 | 107 | for (int i = 0; i < keys.size(); i++) { 108 | String key = keys.get(i); 109 | String value = params.get(key); 110 | 111 | if (i == keys.size() - 1) {//拼接时,不包括最后一个&字符 112 | prestr = prestr + key + "=" + value; 113 | } else { 114 | prestr = prestr + key + "=" + value + "&"; 115 | } 116 | } 117 | 118 | return prestr; 119 | } 120 | 121 | public static void main(String[] args) throws Exception { 122 | //System.out.println(SignUtil.sign("73da7bb9d2a475bbc2ab79da7d4e94940cb9f9d5", SignType.SHA1)); 123 | Map params = new HashMap<>(); 124 | params.put("noncestr", "Wm3WZYTPz0wzccnW"); 125 | params.put("jsapi_ticket", "sM4AOVdWfPE4DxkXGEs8VMCPGGVi4C3VM0P37wVUCFvkVAy_90u5h9nbSlYy3-Sl-HhTdfl2fzFy1AOcHKP7qg"); 126 | params.put("timestamp", "1414587457"); 127 | params.put("url", "http://mp.weixin.qq.com?params=value"); 128 | } 129 | } 130 | -------------------------------------------------------------------------------- /src/main/java/com/pressure/Main.java: -------------------------------------------------------------------------------- 1 | package com.pressure; 2 | 3 | import com.pressure.core.bean.RequestInfo; 4 | import com.pressure.core.httputil.RequestInterface; 5 | import com.pressure.core.httputil.impl.LogResultInfoMonitor; 6 | import com.pressure.core.httputil.impl.RequestImpl; 7 | import com.pressure.core.httputil.impl.SummaryMonitor; 8 | 9 | import java.io.File; 10 | 11 | /** 12 | * 测试 13 | * 14 | * @author YL 15 | */ 16 | public class Main { 17 | 18 | public static void main(String[] args) { 19 | 20 | /** 21 | * 获取请求处理器 22 | * 总请求次数等于循环次数 * RequestInfo 脚本长度 23 | * @param poolSize 线程池并发线程数量 24 | * @param requestSize 循环请求次数 25 | */ 26 | RequestInterface requestInterface = RequestImpl.build( 27 | 500, 28 | 5000) 29 | .setContentTimeOut(3000) // 链接超时时间 毫秒 30 | .setSocketTimeOut(5000) // 响应超时时间 毫秒 31 | .setMonitors( 32 | /** 33 | * 设置监听(收集统计压测记录) 34 | * 目前支持两个监听器 35 | * @see com.pressure.core.httputil.impl.LogResultInfoMonitor 日志记录 36 | * @see com.pressure.core.httputil.impl.SummaryMonitor 汇总打印 37 | * 38 | * 想要实现自定义的监听器可实现: 39 | * @see com.pressure.core.httputil.ResultInfoMonitor 40 | * 在此处配置给处理器 41 | */ 42 | new LogResultInfoMonitor(new File("C:\\Users\\73232\\Desktop\\log.txt")), 43 | new SummaryMonitor() 44 | ); 45 | 46 | // 配置请求脚本 47 | requestInterface.send( 48 | new RequestInfo( 49 | "列表", 50 | /** 51 | * 此接口返回数据如下 52 | * { 53 | * data: { 54 | * data: [ 55 | * { 56 | * id: 521, 57 | * ... 58 | * }, 59 | * { 60 | * id: 522, 61 | * ... 62 | * }, 63 | * { 64 | * id: 520, 65 | * ... 66 | * }, 67 | * ] 68 | * { 69 | */ 70 | "https://xkx.xxx.com/api/video/pay/start?start=1&count=15&keyWord=", 71 | RequestInfo.RequestMethod.GET 72 | ), 73 | new RequestInfo( 74 | "详情(包含动态参数)", 75 | /** 76 | * 由上一个接口获取到 ${data.data.$2.id} = 520 77 | * 接口返回数据如下 78 | * { 79 | * msg: "请求成功", 80 | * data: { 81 | * videoId: "520" 82 | * } 83 | * } 84 | */ 85 | "https://xkx.xxx.com/api/video/playDetail?videoId=${data.data.$2.id}&token=", 86 | RequestInfo.RequestMethod.GET 87 | ), 88 | new RequestInfo( 89 | "子列表(包含动态参数)", 90 | "https://xkx.xxx.com/api/video/getVideList?recordId=${data.videoId}&type=video", 91 | RequestInfo.RequestMethod.GET 92 | ), 93 | new RequestInfo( 94 | "子列表详情(包含动态参数)", 95 | "https://xkx.xxx.com/api/video/playDetail?videoId=${data.$1.id}&token=", 96 | RequestInfo.RequestMethod.GET 97 | ) 98 | ); 99 | } 100 | } 101 | -------------------------------------------------------------------------------- /src/main/java/com/pressure/core/httputil/AbstractRequest.java: -------------------------------------------------------------------------------- 1 | package com.pressure.core.httputil; 2 | 3 | import com.fasterxml.jackson.databind.JsonNode; 4 | import com.pressure.core.bean.RequestInfo; 5 | import com.pressure.core.bean.ResultInfo; 6 | 7 | import java.util.HashMap; 8 | import java.util.Iterator; 9 | import java.util.Map; 10 | import java.util.concurrent.locks.Condition; 11 | import java.util.concurrent.locks.ReentrantLock; 12 | 13 | /** 14 | * 发送请求的接口 抽象类提供一些基本方法 15 | * 16 | * @author YL 17 | * time: 2018-12-24 18 | */ 19 | public abstract class AbstractRequest implements RequestInterface { 20 | 21 | 22 | private Condition condition = new ReentrantLock().newCondition(); 23 | 24 | private ResultInfoMonitor[] monitors = null; 25 | 26 | /** 27 | * false 压测执行结束 28 | */ 29 | protected volatile boolean loop = true; 30 | 31 | public AbstractRequest setMonitors(ResultInfoMonitor... monitors) { 32 | this.monitors = monitors; 33 | return this; 34 | } 35 | 36 | protected void putLog(ResultInfo resultInfo) { 37 | for (ResultInfoMonitor monitor : monitors) { 38 | monitor.add(resultInfo); 39 | } 40 | } 41 | 42 | protected void asyncPrint(boolean endNotice) { 43 | Thread thread = new Thread(() -> { 44 | System.out.println("启动打印线程"); 45 | while (loop) { 46 | print(endNotice); 47 | try { 48 | Thread.sleep(1000); 49 | } catch (InterruptedException e) { 50 | e.printStackTrace(); 51 | } 52 | } 53 | }); 54 | thread.start(); 55 | } 56 | 57 | /** 58 | * 打印监听结果 59 | * @param endNotice 60 | */ 61 | protected void print(boolean endNotice) { 62 | for (ResultInfoMonitor monitor : monitors) { 63 | monitor.printOut(endNotice); 64 | } 65 | } 66 | 67 | protected String getValue(String key, JsonNode jsonNode) { 68 | String[] keys = key.split("\\."); 69 | JsonNode reJson = jsonNode; 70 | for (int i2 = 0; i2 < keys.length; i2++) { 71 | String sk = keys[i2]; 72 | if (sk.contains("$")) { 73 | Iterator iterators = reJson.iterator(); 74 | int i = 0; 75 | int l = Integer.parseInt(sk.replace("$", "")); 76 | while (iterators.hasNext() && i <= l) { 77 | i++; 78 | reJson = iterators.next(); 79 | } 80 | } else { 81 | reJson = reJson.get(sk); 82 | } 83 | if (reJson == null) { 84 | return null; 85 | } 86 | } 87 | return reJson.asText(); 88 | } 89 | 90 | protected Map getPostParams(RequestInfo ri, ResultInfo prResult) { 91 | Map postParams = ri.getPostParams(); 92 | if (postParams == null && ri.getLoadParams() != null) { 93 | postParams = ri.getLoadParams().implement(prResult); 94 | } else if (postParams == null) { 95 | postParams = new HashMap<>(); 96 | } 97 | if (prResult != null) { 98 | Map finalParams = postParams; 99 | postParams.forEach((k, v) -> { 100 | if (v.contains(".")) { 101 | // 本次请求依赖上一次请求的返回结果 102 | finalParams.put(k, getValue(v, prResult.getResult())); 103 | } 104 | }); 105 | } 106 | return postParams; 107 | } 108 | 109 | /** 110 | * 获取最终的get 链接 111 | * 112 | * @param url 113 | * @param prReInfo 114 | * @return 115 | */ 116 | protected String getGetUrl(String url, JsonNode prReInfo) { 117 | int b; 118 | if ((b = url.indexOf("${")) == -1) { 119 | return url; 120 | } 121 | 122 | String key = url.substring(b + 2, url.indexOf("}")); 123 | String val = getValue(key, prReInfo); 124 | url = url.replace("${" + key + "}", val); 125 | 126 | if (url.contains("${")) { 127 | return getGetUrl(url, prReInfo); 128 | } 129 | 130 | return url; 131 | } 132 | } 133 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | pressure 8 | pressure 9 | 1.0-SNAPSHOT 10 | 11 | 12 | 13 | UTF-8 14 | 2.11.0 15 | true 16 | 17 | 18 | 19 | 20 | 21 | org.apache.maven.plugins 22 | maven-compiler-plugin 23 | 24 | 1.8 25 | 1.8 26 | 27 | 28 | 29 | org.apache.maven.plugins 30 | maven-shade-plugin 31 | 1.2.1 32 | 33 | 34 | package 35 | 36 | shade 37 | 38 | 39 | 40 | 41 | com.pressure.Main 42 | 43 | 44 | 45 | 46 | 47 | 48 | 49 | 50 | 51 | 52 | 53 | 54 | 55 | 56 | 57 | 58 | org.apache.httpcomponents 59 | httpclient 60 | 4.5.6 61 | 62 | 63 | 64 | org.apache.httpcomponents 65 | httpmime 66 | 4.5.6 67 | 68 | 69 | 70 | org.apache.httpcomponents 71 | httpcore 72 | 4.4.10 73 | 74 | 75 | 76 | 77 | 78 | org.slf4j 79 | slf4j-api 80 | 1.7.25 81 | 82 | 83 | 84 | org.slf4j 85 | slf4j-nop 86 | 1.7.25 87 | 88 | 89 | 90 | 91 | com.fasterxml.jackson.core 92 | jackson-core 93 | ${jackson.version} 94 | 95 | 96 | 97 | com.fasterxml.jackson.core 98 | jackson-databind 99 | ${jackson.version} 100 | 101 | 102 | 103 | com.fasterxml.jackson.dataformat 104 | jackson-dataformat-xml 105 | ${jackson.version} 106 | 107 | 108 | com.fasterxml.jackson.core 109 | jackson-annotations 110 | ${jackson.version} 111 | 112 | 113 | 114 | -------------------------------------------------------------------------------- /src/main/java/com/pressure/core/bean/RequestInfo.java: -------------------------------------------------------------------------------- 1 | package com.pressure.core.bean; 2 | 3 | import com.pressure.core.LoadConsumer; 4 | import com.fasterxml.jackson.databind.JsonNode; 5 | import org.apache.http.Header; 6 | 7 | import java.util.Arrays; 8 | import java.util.Map; 9 | 10 | /** 11 | * 请求指令包 12 | * @author YL 13 | */ 14 | public class RequestInfo { 15 | 16 | /** 17 | * 名字用于除统计,不要重复 18 | */ 19 | private final String name; 20 | 21 | /** 22 | * 请求地址 23 | * 动态参数使用此形式获取 ${data.data.$2.id} 上一个请求返回的json(或者toJson处理后的json)结构 如: 24 | * (上一个请求返回): 25 | * { 26 | * code: 1, 27 | * msg: "success", 28 | * time: "1587270284", 29 | * data: { 30 | * data: [{ 31 | * id: 520 获取此值(${data.data.$2.id}), 32 | * .... 33 | * }] 34 | * } 35 | * } 36 | * 完整uri: https://www.xxxxxx.com/api/video/playDetail?videoId=${data.data.$2.id} 37 | */ 38 | private final String uri; 39 | 40 | private final String body; 41 | 42 | private final String contentType; 43 | 44 | private final RequestMethod method; 45 | 46 | /** 47 | * 请求头 48 | */ 49 | private final Header[] headers; 50 | 51 | /** 52 | * 请求参数 仅post有效 53 | * 动态参数对value生效 54 | * {videoId:${data.data.$2.id}} 55 | */ 56 | private final Map postParams; 57 | 58 | /** 59 | * 处理返回结果时会调用此方法。 60 | * 未实现的话默认直接将返回结果转json,用于帮助下一个请求完成初始化参数 61 | */ 62 | private final LoadConsumer toJson; 63 | 64 | /** 65 | * 根据上下文获取请求参数 66 | * postParams 有值时loadParams 不生效 67 | */ 68 | private final LoadConsumer> loadParams; 69 | 70 | public RequestInfo(String name, String uri, RequestMethod method, Map postParams, LoadConsumer toJson, LoadConsumer> loadParams, Header[] headers) { 71 | this.name = name; 72 | this.uri = uri; 73 | this.body = null; 74 | this.contentType = null; 75 | this.method = method; 76 | this.postParams = postParams; 77 | this.toJson = toJson; 78 | this.loadParams = loadParams; 79 | this.headers = headers; 80 | } 81 | 82 | public RequestInfo(String name, String uri, RequestMethod method) { 83 | this.name = name; 84 | this.uri = uri; 85 | this.method = method; 86 | this.postParams = null; 87 | this.toJson = null; 88 | this.headers = null; 89 | this.loadParams = null; 90 | this.body = null; 91 | this.contentType = null; 92 | } 93 | 94 | public RequestInfo(String name, String uri, RequestMethod method, Header[] headers, String body, String contentType) { 95 | this.name = name; 96 | this.uri = uri; 97 | this.body = body; 98 | this.method = method; 99 | this.postParams = null; 100 | this.toJson = null; 101 | this.headers = headers; 102 | this.loadParams = null; 103 | this.contentType = contentType; 104 | } 105 | 106 | /** 107 | * 请求方法 108 | * @author YL 109 | */ 110 | public enum RequestMethod { 111 | /** 112 | * get 请求 113 | */ 114 | GET, 115 | 116 | /** 117 | * post 请求 118 | */ 119 | POST; 120 | } 121 | 122 | public Header[] getHeaders() { 123 | return headers; 124 | } 125 | 126 | public String getName() { 127 | return name; 128 | } 129 | 130 | public String getUri() { 131 | return uri; 132 | } 133 | 134 | public RequestMethod getMethod() { 135 | return method; 136 | } 137 | 138 | public Map getPostParams() { 139 | return postParams; 140 | } 141 | 142 | public LoadConsumer getToJson() { 143 | return toJson; 144 | } 145 | 146 | public LoadConsumer> getLoadParams() { 147 | return loadParams; 148 | } 149 | 150 | public String getBody() { 151 | return body; 152 | } 153 | 154 | public String getContentType() { 155 | return contentType; 156 | } 157 | 158 | @Override 159 | public String toString() { 160 | return "RequestInfo{" + 161 | "name='" + name + '\'' + 162 | ", uri='" + uri + '\'' + 163 | ", method=" + method + 164 | ", body=" + body + 165 | ", headers=" + Arrays.toString(headers) + 166 | ", postParams=" + postParams + 167 | ", toJson=" + toJson + 168 | ", loadParams=" + loadParams + 169 | '}'; 170 | } 171 | } 172 | -------------------------------------------------------------------------------- /README.md: -------------------------------------------------------------------------------- 1 | # pressure 2 | 一款简单的,基于java的,http 服务器**压力测试**工具 3 | ### 起因 4 | 5 | - jmeter动态脚本编写比较复杂 6 | - 希望压测程序可以在服务端执行,从内网环境发起请求 7 | - 有足够的扩展空间,方便对复杂流程进行压测 8 | 9 | ### 依赖 10 | - JDK 8 11 | - Maven 12 | 13 | ## 快速开始 14 | ### 构建 15 | 1. 执行git命令 16 | ```git 17 | git clone https://github.com/NotInWine/pressure.git 18 | ``` 19 | 2. 导入你的idea 20 | 3. 使用 pom.xml 构建项目 21 | ``` 22 | 步骤略过 23 | ``` 24 | 25 | ### [示例](./src/main/java/com/pressure/Main.java) 26 | ```java 27 | package com.pressure; 28 | 29 | /** 30 | * 测试 31 | * 32 | * @author YL 33 | */ 34 | public class Main { 35 | 36 | public static void main(String[] args) { 37 | 38 | /** 39 | * 获取请求处理器 40 | * 总请求次数等于循环次数 * RequestInfo 脚本长度 41 | * @param poolSize 线程池并发线程数量 42 | * @param requestSize 循环请求次数 43 | */ 44 | RequestInterface requestInterface = RequestImpl.build( 45 | 500, 46 | 5000) 47 | .setContentTimeOut(3000) // 链接超时时间 毫秒 48 | .setSocketTimeOut(5000) // 响应超时时间 毫秒 49 | .setMonitors( 50 | /** 51 | * 设置监听(收集统计压测记录) 52 | * 目前自带两个款监听器 53 | * @see com.pressure.core.httputil.impl.LogResultInfoMonitor 日志记录 54 | * @see com.pressure.core.httputil.impl.SummaryMonitor 汇总打印 55 | * 56 | * 想要实现自定义的监听器可实现: 57 | * @see com.pressure.core.httputil.ResultInfoMonitor 58 | * 在此处配置给处理器 59 | */ 60 | new LogResultInfoMonitor(new File("D:\\big_folder\\log.txt")), 61 | new SummaryMonitor() 62 | ); 63 | 64 | // 配置请求脚本 65 | requestInterface.send( 66 | new RequestInfo( 67 | "列表", 68 | "https://www.xxxxxx.com/api/video/pay/start?start=1&count=15&keyWord=", 69 | RequestInfo.RequestMethod.GET 70 | ), 71 | new RequestInfo( 72 | "详情(包含动态参数)", 73 | "https://www.xxxxxx.com/api/video/playDetail?videoId=${data.data.$2.id}&token=", 74 | RequestInfo.RequestMethod.GET 75 | ), 76 | new RequestInfo( 77 | "子列表(包含动态参数)", 78 | "https://www.xxxxxx.com/api/video/getVideList?recordId=${data.videoId}&type=video", 79 | RequestInfo.RequestMethod.GET 80 | ), 81 | new RequestInfo( 82 | "子列表详情(包含动态参数)", 83 | "https://www.xxxxxx.com/api/video/playDetail?videoId=${data.$1.id}&token=", 84 | RequestInfo.RequestMethod.GET 85 | ) 86 | ); 87 | } 88 | } 89 | ``` 90 | 91 | ### 输出示例 92 | - LogResultInfoMonitor 日志 93 | ```text 94 | batchId=4752, name=子列表(包含动态参数), state=SUCCESS, httpLog=HttpLog{url='https://xkx.xxx.com/api/video/getVideList?recordId=245&type=video', httpState=200, params=null 95 | , requestHeaders=null 96 | , responseHeaders=[Server: nginx, Date: Sun, 19 Apr 2020 04:59:12 GMT, Content-Type: application/json; charset=utf-8, Transfer-Encoding: chunked, Connection: keep-alive, X-Powered-By: PHP/5.5.38, Access-Control-Allow-Origin: *] 97 | , responseBody='{"code":1,"msg":"success","time":"1587272352","data":[{]}'} 98 | , time=4260 99 | ``` 100 | - SummaryMonitor 汇总 101 | ```text 102 | 【子列表】 103 | 成功吞吐量(qps)=35.18,总吞吐量(qps)=38.55, 90%响应=12134, 95%响应=14489, 99%响应=18518, 最慢响应=24910, 最快响应=739, 平均响应=6653, 总请求次数=1272, 请求成功次数=1161, 请求异常次数=111, 异常率=0.0873 104 | 【列表】 105 | 成功吞吐量(qps)=73.64,总吞吐量(qps)=80.24, 90%响应=20970, 95%响应=24402, 99%响应=30721, 最慢响应=36134, 最快响应=752, 平均响应=12610, 总请求次数=2648, 请求成功次数=2430, 请求异常次数=218, 异常率=0.0823 106 | 【详情】 107 | 成功吞吐量(qps)=52.79,总吞吐量(qps)=59.24, 90%响应=12673, 95%响应=15239, 99%响应=22150, 最慢响应=27739, 最快响应=88, 平均响应=6699, 总请求次数=1955, 请求成功次数=1742, 请求异常次数=213, 异常率=0.1090 108 | 【子列表详情(包含动态参数)】 109 | 成功吞吐量(qps)=19.55,总吞吐量(qps)=21.03, 90%响应=10958, 95%响应=13584, 99%响应=17857, 最慢响应=20536, 最快响应=758, 平均响应=6349, 总请求次数=694, 请求成功次数=645, 请求异常次数=49, 异常率=0.0706 110 | 【汇总】 111 | 成功吞吐量(qps)=181.15,总吞吐量(qps)=199.06, 90%响应=17277, 95%响应=20627, 99%响应=27582, 最慢响应=36134, 最快响应=88, 平均响应=9036, 总请求次数=6569, 请求成功次数=5978, 请求异常次数=591, 异常率=0.0900 112 | ``` 113 | ### 打包执行 114 | 需要打包到服务器执行可编辑[示例](./src/main/java/com/pressure/Main.java),再使用mvn install, 打成可执行jar,到(服务端|PC)java -jar 执行 115 | 116 | 117 | # 核心概念 118 | - [控制器 RequestInterface](./src/main/java/com/pressure/core/httputil/RequestInterface.java) 119 | 负责压测请求核心逻辑,负责http连接配置,请求发送,触发统计(调用监听器) 120 | - [请求命令包 RequestInfo](./src/main/java/com/pressure/core/bean/RequestInfo.java) 121 | 封装请求指令,作为参数传递给控制器执行。 122 | - [监听器 ResultInfoMonitor](./src/main/java/com/pressure/core/httputil/ResultInfoMonitor.java) 123 | 处理压测结果目前支持两款监听器,接收[响应日志](./src/main/java/com/pressure/core/bean/ResultInfo.java) 124 | - 日志 125 | - 汇总监听 126 | 作为扩展性最强的部分,可利用监听器自定义各种形式的统计输出 127 | 128 | ## 执行流程 129 | [时序图](https://www.processon.com/view/link/5e9c0eb2f346fb4bdd771fd0) -------------------------------------------------------------------------------- /src/main/java/com/util/MapUtil.java: -------------------------------------------------------------------------------- 1 | package com.util; 2 | 3 | import java.beans.BeanInfo; 4 | import java.beans.Introspector; 5 | import java.beans.PropertyDescriptor; 6 | import java.lang.reflect.Method; 7 | import java.util.*; 8 | import java.util.Map.Entry; 9 | 10 | 11 | public class MapUtil { 12 | 13 | /** 14 | * 将map 转为 实体 15 | * 16 | * @param map 17 | * @param t 18 | * @param 19 | * @return 20 | */ 21 | public static T mapToBean(Map map, T t) { 22 | try { 23 | BeanInfo beanInfo = Introspector.getBeanInfo(t.getClass()); 24 | PropertyDescriptor[] propertyDescriptors = beanInfo.getPropertyDescriptors(); 25 | for (PropertyDescriptor property : propertyDescriptors) { 26 | String key = property.getName(); 27 | if (map.containsKey(key)) { 28 | Object value = map.get(key); 29 | // 得到property对应的setter方法 30 | Method setter = property.getWriteMethod(); 31 | setter.invoke(t, value); 32 | } 33 | } 34 | } catch (Exception e) { 35 | e.printStackTrace(); 36 | } 37 | return t; 38 | } 39 | 40 | /** 41 | * list map 转为 list bean 42 | * 43 | * @param data 44 | * @param t 45 | * @param 46 | * @return 47 | * @throws IllegalAccessException 48 | * @throws InstantiationException 49 | */ 50 | public static List listToListBean(List data, Class t) throws IllegalAccessException, InstantiationException { 51 | List l = new ArrayList(); 52 | for (Map m : data) { 53 | T t1 = t.newInstance(); 54 | l.add(MapUtil.mapToBean(m, t1)); 55 | } 56 | return l; 57 | } 58 | 59 | /** 60 | * @param m 61 | * @return 将map中的value组成list返回 62 | */ 63 | @SuppressWarnings({"rawtypes", "unchecked"}) 64 | public static List valueList(Map m) { 65 | List result = new ArrayList(); 66 | try { 67 | Set s = m.entrySet(); 68 | Iterator it = s.iterator(); 69 | while (it != null && it.hasNext()) { 70 | Map.Entry entry = (Entry) it.next(); 71 | result.add(m.get(entry.getKey())); 72 | } 73 | } catch (Exception e) { 74 | e.printStackTrace(); 75 | } 76 | return result; 77 | } 78 | 79 | @SuppressWarnings({"rawtypes", "unchecked"}) 80 | public static List keyList(Map m) { 81 | List result = new ArrayList(); 82 | try { 83 | Set s = m.entrySet(); 84 | Iterator it = s.iterator(); 85 | while (it != null && it.hasNext()) { 86 | Map.Entry entry = (Entry) it.next(); 87 | result.add(entry.getKey()); 88 | } 89 | } catch (Exception e) { 90 | e.printStackTrace(); 91 | } 92 | return result; 93 | } 94 | 95 | @SuppressWarnings({"unchecked", "rawtypes"}) 96 | public static Map listToMap(List list, String keyValue) { 97 | Map data = new HashMap(); 98 | Iterator it = list.iterator(); 99 | while (it != null && it.hasNext()) { 100 | Map map = (Map) it.next(); 101 | // 获取key 102 | Object key = map.get(keyValue); 103 | // 获取子集合 104 | List l = (List) data.get(key); 105 | 106 | if (l == null) { 107 | // 子集合的第一次初始化 108 | l = new ArrayList<>(); 109 | data.put(key, l); 110 | } 111 | // 元素加入子集合 112 | l.add(map); 113 | } 114 | return data; 115 | } 116 | 117 | private static final String ITEMS_KEY = "items"; 118 | 119 | /** 120 | * 给list 分组, 把被融合的记录坐位子元素 放置于 ITEM_KEY 对应的 list 中 121 | * 122 | * @param list 123 | * @param groupKey 124 | * @param s 125 | * @return [{...s,items:[{},{}]},{...s,items:[{},{}]},...] 126 | */ 127 | @SuppressWarnings({"unchecked", "rawtypes"}) 128 | public static List listGroup(List list, String groupKey, String... s) { 129 | Map data = new LinkedHashMap<>(); 130 | Iterator it = list.iterator(); 131 | while (it != null && it.hasNext()) { 132 | Map map = (Map) it.next(); 133 | // 获取key 134 | Object key = map.get(groupKey); 135 | Map sMap = getlistGroupSon(data, map, key, s); 136 | // 获取子集合 137 | List l = (List) sMap.get(ITEMS_KEY); 138 | getSonMap(map, s); 139 | // 元素加入子集合 140 | l.add(map); 141 | } 142 | return new ArrayList(data.values()); 143 | } 144 | 145 | 146 | 147 | private static void getSonMap(Map map, String... s) { 148 | for (String s1 : 149 | s) { 150 | map.remove(s1); 151 | } 152 | } 153 | 154 | private static Map getlistGroupSon(Map data, Map map, Object key, String... s) { 155 | // 获取key 对应的 map 156 | Map sMap = (Map) data.get(key); 157 | if (sMap == null) { 158 | // 子集合的第一次初始化 159 | sMap = new HashMap(); 160 | for (String s1 : 161 | s) { 162 | sMap.put(s1, map.remove(s1)); 163 | } 164 | sMap.put(ITEMS_KEY, new ArrayList()); 165 | data.put(key, sMap); 166 | } 167 | return sMap; 168 | } 169 | 170 | public static void main(String[] args) { 171 | List> list = new ArrayList<>(); 172 | Map map = new HashMap<>(); 173 | map.put("type", "鞋子"); 174 | map.put("fee", "22"); 175 | Map map1 = new HashMap<>(); 176 | map1.put("type", "鞋子"); 177 | map1.put("fee", "23"); 178 | Map map2 = new HashMap<>(); 179 | map2.put("type", "袜子"); 180 | map2.put("fee", "23"); 181 | Map map3 = new HashMap<>(); 182 | map3.put("type", "裤子"); 183 | map3.put("fee", "33"); 184 | Map map4 = new HashMap<>(); 185 | map4.put("type", "裤子"); 186 | map4.put("fee", "35"); 187 | list.add(map); 188 | list.add(map1); 189 | list.add(map2); 190 | list.add(map4); 191 | list.add(map3); 192 | 193 | } 194 | } 195 | -------------------------------------------------------------------------------- /src/main/java/com/pressure/core/httputil/impl/RequestImpl.java: -------------------------------------------------------------------------------- 1 | package com.pressure.core.httputil.impl; 2 | 3 | import com.pressure.core.Cache; 4 | import com.pressure.core.bean.HttpLog; 5 | import com.pressure.core.bean.RequestInfo; 6 | import com.pressure.core.bean.ResultInfo; 7 | import com.pressure.core.httputil.AbstractRequest; 8 | import com.pressure.core.httputil.RequestInterface; 9 | import com.fasterxml.jackson.databind.JsonNode; 10 | import com.util.SimpleHttpClient; 11 | import org.slf4j.Logger; 12 | import org.slf4j.LoggerFactory; 13 | 14 | import java.io.IOException; 15 | import java.net.SocketTimeoutException; 16 | import java.util.Map; 17 | import java.util.concurrent.*; 18 | 19 | /** 20 | * 发送请求的接口 21 | * 22 | * @author YL 23 | */ 24 | public class RequestImpl extends AbstractRequest implements RequestInterface { 25 | 26 | 27 | private final static Logger log = LoggerFactory.getLogger(RequestImpl.class); 28 | 29 | /** 30 | * 线程池并行数 31 | */ 32 | private final int poolSize; 33 | 34 | /** 35 | * 请求循环数 36 | */ 37 | private final int requestSize; 38 | 39 | /** 40 | * 链接超时时间 41 | */ 42 | private int contentTimeOut = 1000; 43 | 44 | /** 45 | * 响应超时时间 46 | */ 47 | private int socketTimeOut = 3000; 48 | 49 | /** 50 | * 总请求次数等于循环次数 * RequestInfo 脚本长度 51 | * @param poolSize 线程池并发线程数量 52 | * @param requestSize 循环请求次数 53 | * @return 54 | */ 55 | public static RequestImpl build(int poolSize, int requestSize) { 56 | return new RequestImpl(poolSize, requestSize); 57 | } 58 | 59 | public RequestImpl setContentTimeOut(int contentTimeOut) { 60 | this.contentTimeOut = contentTimeOut; 61 | return this; 62 | } 63 | 64 | public RequestImpl setSocketTimeOut(int socketTimeOut) { 65 | this.socketTimeOut = socketTimeOut; 66 | return this; 67 | } 68 | 69 | private RequestImpl(int poolSize, int requestSize) { 70 | this.poolSize = poolSize; 71 | this.requestSize = requestSize; 72 | } 73 | 74 | private boolean close = false; 75 | 76 | /** 77 | * 发送测试脚本 78 | * 79 | * @param infos 指令集 80 | */ 81 | @Override 82 | public synchronized void send(RequestInfo... infos) { 83 | if (close) { 84 | throw new RuntimeException("本方法不可重复调用, 新的测试请重新初始化"); 85 | } 86 | close = true; 87 | 88 | log.info("BEGIN"); 89 | 90 | BlockingQueue blockingQueue = new LinkedBlockingQueue<>(); 91 | ThreadPoolExecutor steadyThreadIdiotPool = createTask(infos, blockingQueue); 92 | 93 | 94 | // 这个监听会阻塞直到运行结束 95 | monitorAndBlock(steadyThreadIdiotPool, blockingQueue); 96 | 97 | // 没有打印干净的打印出来 98 | super.print(true); 99 | } 100 | 101 | /** 102 | * 监听 103 | * 这个监听会阻塞直到运行结束 104 | * @param steadyThreadIdiotPool 105 | * @param blockingQueue 106 | */ 107 | protected void monitorAndBlock(ThreadPoolExecutor steadyThreadIdiotPool, BlockingQueue blockingQueue) { 108 | steadyThreadIdiotPool.shutdown(); 109 | long b = System.currentTimeMillis(); 110 | do { 111 | //等待所有任务完成 112 | try { 113 | ResultInfo take; 114 | while ((take = blockingQueue.poll()) != null){ 115 | putLog(take); 116 | } 117 | loop = !(steadyThreadIdiotPool.awaitTermination(5, TimeUnit.SECONDS) && blockingQueue.size() == 0); //阻塞,直到线程池里所有任务结束 118 | System.out.println("线程池状态:" + steadyThreadIdiotPool + " queue " + blockingQueue.size() + ", loop " + loop); 119 | super.print(false); 120 | } catch (InterruptedException e) { 121 | e.printStackTrace(); 122 | } 123 | } while (loop); 124 | System.out.println("TIME " + (System.currentTimeMillis() - b)); 125 | } 126 | 127 | private ThreadPoolExecutor createTask(RequestInfo[] infos, BlockingQueue blockingQueue) { 128 | ThreadPoolExecutor steadyThreadIdiotPool = new ThreadPoolExecutor(poolSize, poolSize, 2000, TimeUnit.MILLISECONDS, 129 | new LinkedTransferQueue<>(), new ThreadPoolExecutor.CallerRunsPolicy()); 130 | 131 | for (int i = 0; i < requestSize; i++) { 132 | int finalI = i; 133 | steadyThreadIdiotPool.execute(() -> { 134 | SimpleHttpClient sc = new SimpleHttpClient(contentTimeOut, socketTimeOut); 135 | ResultInfo resultInfo = null; 136 | for (RequestInfo ri : infos) { 137 | // 循环处理请求 138 | long begin = System.currentTimeMillis(); 139 | // 发送请求 140 | HttpLog log = null; 141 | try { 142 | log = send(ri, sc, resultInfo); 143 | resultInfo = new ResultInfo( 144 | ri, 145 | log, 146 | System.currentTimeMillis() - begin, 147 | begin, 148 | finalI, 149 | ResultInfo.State.SUCCESS); 150 | } catch (Exception e) { 151 | if (!(e instanceof SocketTimeoutException)) { 152 | this.log.error("请求发送异常", e); 153 | } 154 | resultInfo = new ResultInfo( 155 | ri, 156 | log, 157 | System.currentTimeMillis() - begin, 158 | begin, 159 | finalI, 160 | ResultInfo.State.ERROR, 161 | e); 162 | } 163 | 164 | if (log != null) { 165 | resultInfo.setResult(resultToJsonNode(ri, resultInfo.getHttpLog().getResponseBody())); 166 | } 167 | 168 | // 请求结果加入队列用于统计 169 | blockingQueue.add(resultInfo); 170 | if (resultInfo.getState() == ResultInfo.State.ERROR) { 171 | break; 172 | } 173 | } 174 | }); 175 | } 176 | 177 | return steadyThreadIdiotPool; 178 | } 179 | 180 | private JsonNode resultToJsonNode(RequestInfo ri, String reStr) { 181 | if (reStr == null) { 182 | return null; 183 | } 184 | JsonNode jsonNode = null; 185 | try { 186 | if (ri.getToJson() != null) { 187 | jsonNode = ri.getToJson().implement(reStr); 188 | } else { 189 | jsonNode = Cache.JSON_UTIL.readTree(reStr); 190 | } 191 | } catch (IOException e) { 192 | e.printStackTrace(); 193 | } 194 | return jsonNode; 195 | } 196 | 197 | /** 198 | * 发送请求 199 | * 200 | * @param ri 201 | * @param sc 202 | * @param prResult 203 | * @return 204 | * @throws IOException 205 | */ 206 | private HttpLog send(RequestInfo ri, SimpleHttpClient sc, ResultInfo prResult) throws IOException { 207 | String url = ri.getUri(); 208 | if (prResult != null) { 209 | url = getGetUrl(ri.getUri(), prResult.getResult()); 210 | } 211 | switch (ri.getMethod()) { 212 | case GET: 213 | sc.get(url, ri.getHeaders()); 214 | return new HttpLog(url, null, ri.getHeaders(), sc.getResponseInfo()); 215 | case POST: 216 | Map postParams = getPostParams(ri, prResult); 217 | if (ri.getBody() != null) { 218 | sc.post(url, ri.getBody(), ri.getContentType(), ri.getHeaders()); 219 | } else { 220 | sc.post(url, postParams, ri.getHeaders()); 221 | } 222 | return new HttpLog(ri.getUri(), postParams, ri.getHeaders(), sc.getResponseInfo()); 223 | default: 224 | throw new RuntimeException("没有实现的请求方式 " + ri.getMethod()); 225 | } 226 | } 227 | 228 | public static void main(String[] args) { 229 | } 230 | } 231 | -------------------------------------------------------------------------------- /src/main/java/com/pressure/core/httputil/impl/SummaryMonitor.java: -------------------------------------------------------------------------------- 1 | package com.pressure.core.httputil.impl; 2 | 3 | import com.pressure.core.bean.ResultInfo; 4 | import com.pressure.core.httputil.ResultInfoMonitor; 5 | 6 | import java.math.BigDecimal; 7 | import java.math.RoundingMode; 8 | import java.util.Collection; 9 | import java.util.HashMap; 10 | import java.util.Map; 11 | import java.util.Set; 12 | import java.util.concurrent.ConcurrentSkipListMap; 13 | 14 | /** 15 | * 汇总监控 16 | * 汇总结果将打印出来,汇总结果见 17 | * @see RequestLog#toString(int) 18 | * @author YL 19 | **/ 20 | public class SummaryMonitor implements ResultInfoMonitor { 21 | 22 | /** 23 | * 按name统计 24 | */ 25 | private Map nameSummary = new HashMap<>(); 26 | 27 | /** 28 | * 按秒统计 29 | */ 30 | private Map secondSummary = new HashMap<>(); 31 | 32 | @Override 33 | public void add(ResultInfo resultInfo) { 34 | RequestLog nameLog = getLogOrInit(resultInfo, resultInfo.getRequestInfo().getName(), nameSummary); 35 | RequestLog secondLog = getLogOrInit(resultInfo, (resultInfo.getBeginTime() + resultInfo.getTime()) / 1000, secondSummary); 36 | 37 | long time = resultInfo.getTime(); 38 | boolean success = resultInfo.getState() == ResultInfo.State.SUCCESS; 39 | 40 | nameLog.add((int) time, success); 41 | secondLog.add((int) time, success); 42 | } 43 | 44 | /** 45 | * @return 46 | */ 47 | @Override 48 | public String out() { 49 | StringBuilder str = new StringBuilder(); 50 | 51 | int secondSize = secondSummary.size(); 52 | if (nameSummary.size() > 0) { 53 | this.nameSummary.forEach((k, v) -> { 54 | str.append("【").append(k).append("】\n").append(v.toString(secondSize)).append("\n"); 55 | }); 56 | } 57 | 58 | if (secondSize > 0 && nameSummary.size() > 1) { 59 | Collection values = secondSummary.values(); 60 | RequestLog log = RequestLog.merge(values.toArray(new RequestLog[0])); 61 | str.append("【汇总】\n").append(log.toString(values.size())); 62 | } 63 | 64 | if (str.length() == 0) { 65 | return null; 66 | } 67 | 68 | return str.append("\n").toString(); 69 | } 70 | 71 | 72 | /** 73 | * 获取或者初始化log 74 | * 75 | * @param resultInfo 76 | * @param key 77 | * @param map 78 | * @param 79 | * @return 80 | */ 81 | private RequestLog getLogOrInit(ResultInfo resultInfo, T key, Map map) { 82 | RequestLog requestLog = map.get(key); 83 | if (requestLog == null) { 84 | // 以响应时间点的统计作为性能依据 85 | requestLog = initResultLog(resultInfo); 86 | map.put(key, requestLog); 87 | } 88 | return requestLog; 89 | } 90 | 91 | /** 92 | * 初始化log 93 | * 94 | * @param resultInfo 95 | * @return 96 | */ 97 | private RequestLog initResultLog(ResultInfo resultInfo) { 98 | return new RequestLog((resultInfo.getBeginTime() + resultInfo.getTime()) / 1000); 99 | } 100 | 101 | 102 | /** 103 | * 请求信息记录 104 | * 用于统计不支持并发访问的 105 | * 106 | * @author YL 107 | **/ 108 | protected static class RequestLog { 109 | 110 | /** 111 | * 秒数的时间戳,可以用于分隔统计 112 | */ 113 | private final long second; 114 | 115 | /** 116 | * 最大响应时间 117 | */ 118 | private int maxTime; 119 | 120 | private int minTime = Integer.MAX_VALUE; 121 | 122 | /** 123 | * 总响应时间 124 | */ 125 | private int timeTotal; 126 | 127 | /** 128 | * 请求次数 129 | */ 130 | private int requestCount; 131 | 132 | /** 133 | * 请求次数 134 | */ 135 | private int errorCount; 136 | 137 | /** 138 | * 记录响应时长,以及对应的次数 139 | */ 140 | private ConcurrentSkipListMap timeData = new ConcurrentSkipListMap<>(); 141 | 142 | public RequestLog(long second) { 143 | this.second = second; 144 | } 145 | 146 | /** 147 | * 多个log合并成一个 148 | * 149 | * @param logs 150 | * @return 151 | */ 152 | public static RequestLog merge(RequestLog... logs) { 153 | if (logs.length == 1) { 154 | return logs[0]; 155 | } 156 | RequestLog log = new RequestLog(logs[0].getSecond()); 157 | for (RequestLog rl : logs) { 158 | if (log.maxTime < rl.maxTime) { 159 | log.maxTime = rl.maxTime; 160 | } 161 | if (log.minTime > rl.minTime) { 162 | log.minTime = rl.minTime; 163 | } 164 | 165 | log.timeTotal += rl.timeTotal; 166 | log.requestCount += rl.requestCount; 167 | log.errorCount += rl.errorCount; 168 | rl.timeData.forEach((k, v) -> { 169 | // 合并 170 | Integer integer = log.timeData.get(k); 171 | if (integer == null) { 172 | integer = 0; 173 | } 174 | integer += v; 175 | 176 | log.timeData.put(k, integer); 177 | }); 178 | } 179 | return log; 180 | } 181 | 182 | /** 183 | * 记一次请求 184 | * 185 | * @param time 186 | */ 187 | public void add(int time, boolean success) { 188 | if (time > maxTime) { 189 | maxTime = time; 190 | } else if (time < minTime) { 191 | // TODO 极端情况下,会出现最小请求时间为初始值 192 | minTime = time; 193 | } 194 | 195 | if (!success) { 196 | errorCount++; 197 | } else { 198 | Integer integer = timeData.get(time); 199 | if (integer == null) { 200 | integer = 0; 201 | } 202 | timeData.put(time, integer + 1); 203 | } 204 | timeTotal += time; 205 | requestCount++; 206 | } 207 | 208 | public int getMaxTime() { 209 | return maxTime; 210 | } 211 | 212 | public int getMinTime() { 213 | return minTime; 214 | } 215 | 216 | public int getRequestCount() { 217 | return requestCount; 218 | } 219 | 220 | public long getSecond() { 221 | return second; 222 | } 223 | 224 | public int getTimeTotal() { 225 | return timeTotal; 226 | } 227 | 228 | public int getErrorCount() { 229 | return errorCount; 230 | } 231 | 232 | /** 233 | * @param mergeCount 合并数量(秒数) 234 | * @return 235 | */ 236 | public String toString(int mergeCount) { 237 | int successNum = this.requestCount - this.errorCount; 238 | String s = ""; 239 | if (mergeCount > 1) { 240 | s += "成功吞吐量(qps)=" + new BigDecimal(successNum).divide(new BigDecimal(mergeCount),2, RoundingMode.HALF_DOWN).toString(); 241 | s += ",总吞吐量(qps)=" + new BigDecimal(requestCount).divide(new BigDecimal(mergeCount),2, RoundingMode.HALF_DOWN).toString(); 242 | } 243 | 244 | Temp[] temps = new Temp[]{ 245 | new Temp("90%响应", successNum * 90 / 100), 246 | new Temp("95%响应", successNum * 95/ 100), 247 | new Temp("99%响应", successNum * 99 / 100), 248 | }; 249 | 250 | int number = 0; 251 | Set> entries = this.timeData.entrySet(); 252 | for (Map.Entry entry : entries) { 253 | number += entry.getValue(); 254 | for (Temp temp : temps) { 255 | if (temp.value == 0 && number >= temp.number) { 256 | temp.value = entry.getKey(); 257 | } 258 | } 259 | } 260 | 261 | StringBuilder str = new StringBuilder(); 262 | for (Temp temp : temps) { 263 | str.append(", ").append(temp.title).append("=").append(temp.value); 264 | } 265 | 266 | s += str.toString() + 267 | ", 最慢响应=" + maxTime + 268 | ", 最快响应=" + minTime + 269 | ", 平均响应=" + timeTotal / this.requestCount + 270 | ", 总请求次数=" + this.requestCount + 271 | ", 请求成功次数=" + successNum + 272 | ", 请求异常次数=" + errorCount + 273 | ", 异常率=" + new BigDecimal(errorCount).divide(new BigDecimal(this.requestCount),4, BigDecimal.ROUND_HALF_UP).toString() + 274 | ""; 275 | return s; 276 | } 277 | } 278 | 279 | /** 280 | * 用于计算 n% 响应时间的临时存储结构 281 | */ 282 | private static class Temp { 283 | 284 | final String title; 285 | final int number; 286 | int value; 287 | 288 | Temp(String title, int number) { 289 | this.title = title; 290 | this.number = number; 291 | } 292 | } 293 | } 294 | -------------------------------------------------------------------------------- /src/main/java/com/util/SimpleHttpClient.java: -------------------------------------------------------------------------------- 1 | package com.util; 2 | 3 | import org.apache.http.*; 4 | import org.apache.http.client.ClientProtocolException; 5 | import org.apache.http.client.HttpRequestRetryHandler; 6 | import org.apache.http.client.config.RequestConfig; 7 | import org.apache.http.client.config.RequestConfig.Builder; 8 | import org.apache.http.client.entity.UrlEncodedFormEntity; 9 | import org.apache.http.client.methods.*; 10 | import org.apache.http.client.protocol.HttpClientContext; 11 | import org.apache.http.config.Registry; 12 | import org.apache.http.config.RegistryBuilder; 13 | import org.apache.http.conn.HttpClientConnectionManager; 14 | import org.apache.http.conn.socket.ConnectionSocketFactory; 15 | import org.apache.http.conn.socket.LayeredConnectionSocketFactory; 16 | import org.apache.http.conn.socket.PlainConnectionSocketFactory; 17 | import org.apache.http.conn.ssl.SSLConnectionSocketFactory; 18 | import org.apache.http.entity.ContentType; 19 | import org.apache.http.entity.StringEntity; 20 | import org.apache.http.entity.mime.MultipartEntityBuilder; 21 | import org.apache.http.entity.mime.content.FileBody; 22 | import org.apache.http.entity.mime.content.StringBody; 23 | import org.apache.http.impl.client.CloseableHttpClient; 24 | import org.apache.http.impl.client.HttpClients; 25 | import org.apache.http.impl.conn.PoolingHttpClientConnectionManager; 26 | import org.apache.http.message.BasicHeader; 27 | import org.apache.http.message.BasicNameValuePair; 28 | import org.apache.http.protocol.BasicHttpContext; 29 | import org.apache.http.protocol.HTTP; 30 | import org.apache.http.protocol.HttpContext; 31 | import org.apache.http.util.EntityUtils; 32 | import org.slf4j.Logger; 33 | import org.slf4j.LoggerFactory; 34 | 35 | import javax.net.ssl.SSLException; 36 | import javax.net.ssl.SSLHandshakeException; 37 | import java.io.*; 38 | import java.net.UnknownHostException; 39 | import java.nio.charset.Charset; 40 | import java.util.ArrayList; 41 | import java.util.List; 42 | import java.util.Map; 43 | import java.util.concurrent.TimeUnit; 44 | 45 | /** 46 | * @author YL 47 | */ 48 | public class SimpleHttpClient { 49 | 50 | private final static Logger log = LoggerFactory.getLogger(SimpleHttpClient.class); 51 | private HttpContext context; 52 | private volatile Header[] responseHeaders; 53 | private volatile Info info; 54 | /** 设置连接超时时间,单位毫秒。*/ 55 | private final int CONNECT_TIMEOUT; 56 | /** 请求获取数据的超时时间,单位毫秒 */ 57 | private final int SOCKET_TIMEOUT; 58 | private final static int REQUEST_RETRY_CNT = 1; 59 | private final static int MAX_TOTAL = 1000; 60 | private final static int MAX_PER_ROUTE = 1000; 61 | private final static PoolingHttpClientConnectionManager POOL; 62 | 63 | /** 请求重试处理 */ 64 | private final static HttpRequestRetryHandler HTTP_REQUEST_RETRY_HANDLER = (exception, executionCount, context) -> { 65 | if (executionCount >= REQUEST_RETRY_CNT) {// 如果已经重试了5次,就放弃 66 | return false; 67 | } 68 | if (exception instanceof NoHttpResponseException) {// 如果服务器丢掉了连接,那么就重试 69 | return true; 70 | } 71 | if (exception instanceof SSLHandshakeException) {// 不要重试SSL握手异常 72 | return false; 73 | } 74 | if (exception instanceof InterruptedIOException) {// 超时 75 | return false; 76 | } 77 | if (exception instanceof UnknownHostException) {// 目标服务器不可达 78 | return false; 79 | } 80 | if (exception instanceof SSLException) {// SSL握手异常 81 | return false; 82 | } 83 | 84 | HttpClientContext clientContext = HttpClientContext 85 | .adapt(context); 86 | HttpRequest request = clientContext.getRequest(); 87 | // 如果请求是幂等的,就再次尝试 88 | return !(request instanceof HttpEntityEnclosingRequest); 89 | }; 90 | 91 | static { 92 | ConnectionSocketFactory plainsf = PlainConnectionSocketFactory 93 | .getSocketFactory(); 94 | LayeredConnectionSocketFactory sslsf = SSLConnectionSocketFactory 95 | .getSocketFactory(); 96 | Registry registry = RegistryBuilder 97 | . create().register("http", plainsf) 98 | .register("https", sslsf).build(); 99 | POOL = new PoolingHttpClientConnectionManager(registry); 100 | // 将最大连接数增加 101 | POOL.setMaxTotal(MAX_TOTAL); 102 | // 将每个路由基础的连接增加 103 | POOL.setDefaultMaxPerRoute(MAX_PER_ROUTE); 104 | // 创建线程异步维护链接状态(清理超时链接) 105 | new HttpClientConnectionMonitorThread(POOL); 106 | } 107 | 108 | public SimpleHttpClient(int connectTimeout, int socketTimeout) { 109 | CONNECT_TIMEOUT = connectTimeout; 110 | SOCKET_TIMEOUT = socketTimeout; 111 | context = new BasicHttpContext(); 112 | } 113 | 114 | public SimpleHttpClient() { 115 | CONNECT_TIMEOUT = 1000; 116 | SOCKET_TIMEOUT = 3 * 1000; 117 | context = new BasicHttpContext(); 118 | } 119 | 120 | public static SimpleHttpClient get() { 121 | return new SimpleHttpClient(); 122 | } 123 | 124 | private List toNameValuePairList(Map m) { 125 | List nameValuePairList = new ArrayList<>(); 126 | if (m == null) { 127 | return nameValuePairList; 128 | } 129 | List keylist = MapUtil.keyList(m); 130 | for (String key : keylist) { 131 | nameValuePairList.add(new BasicNameValuePair(key, m.get(key))); 132 | } 133 | return nameValuePairList; 134 | } 135 | 136 | public String get(String url, Header... headers) throws IOException { 137 | HttpGet httpget = new HttpGet(url); 138 | setHeadGet(httpget, headers); 139 | return getString(httpget); 140 | } 141 | 142 | private void setHeadGet(HttpGet httpget, Header[] headers) { 143 | if (headers != null) { 144 | for (Header header : headers) { 145 | httpget.setHeader(header); 146 | } 147 | } 148 | } 149 | 150 | public String get(String url, Map params) throws IOException { 151 | url = !url.contains("?") ? 152 | url + "?" + SignUtil.createLinkString(params) 153 | : url + "&" + SignUtil.createLinkString(params); 154 | return get(url); 155 | } 156 | 157 | public String post(String url) throws IOException { 158 | HttpPost httppost = new HttpPost(url); 159 | return getString(httppost, null); 160 | } 161 | 162 | public String post(String url, Header[] headers) throws IOException { 163 | HttpPost httppost = new HttpPost(url); 164 | setHead(httppost, headers); 165 | return getString(httppost, null); 166 | } 167 | 168 | public String post(String url, Map params) throws IOException { 169 | HttpPost httppost = new HttpPost(url); 170 | List nameValuePairList = toNameValuePairList(params); 171 | return getString(httppost, new UrlEncodedFormEntity(nameValuePairList, "UTF-8")); 172 | } 173 | 174 | /** 175 | * 提交 json数据 176 | * 177 | * @param url 178 | * @param json 179 | * @return 180 | * @throws ClientProtocolException 181 | * @throws IOException 182 | */ 183 | public String postJson(String url, String json, Header... headers) throws ClientProtocolException, IOException { 184 | return post(url, json, "application/json", headers); 185 | } 186 | 187 | /** 188 | * 提交 xml 数据 189 | * 190 | * @param url 191 | * @param xml 192 | * @return 193 | * @throws ClientProtocolException 194 | * @throws IOException 195 | */ 196 | public String postXml(String url, String xml, Header... headers) throws ClientProtocolException, IOException { 197 | return post(url, xml, "application/xml", headers); 198 | } 199 | 200 | public String post(String url, String str, String type, Header... headers) throws IOException { 201 | HttpPost httppost = new HttpPost(url); 202 | setHead(httppost, headers); 203 | StringEntity se = new StringEntity(str, Charset.forName("UTF-8")); 204 | se.setContentType(type); 205 | se.setContentEncoding(new BasicHeader(HTTP.CONTENT_TYPE, type)); 206 | return getString(httppost, se); 207 | } 208 | 209 | public String post(String url, Map params, Header... headers) 210 | throws IOException { 211 | if (params == null) { 212 | return post(url, headers); 213 | } 214 | HttpPost httppost = new HttpPost(url); 215 | setHead(httppost, headers); 216 | List nameValuePairList = toNameValuePairList(params); 217 | return getString(httppost, new UrlEncodedFormEntity(nameValuePairList, "UTF-8")); 218 | } 219 | 220 | private void setHead(HttpPost httppost, Header[] headers) { 221 | if (headers != null) { 222 | for (Header header : headers) { 223 | httppost.setHeader(header); 224 | } 225 | } 226 | } 227 | 228 | /** 229 | * Post 上传文件 230 | * 231 | * @param url 232 | * @param params 233 | * @param fileList 234 | * @return 235 | * @throws ClientProtocolException 236 | * @throws IOException 237 | */ 238 | public String post(String url, Map params, Map fileList) 239 | throws ClientProtocolException, IOException { 240 | HttpPost httppost = new HttpPost(url); 241 | 242 | MultipartEntityBuilder reqEntity = MultipartEntityBuilder.create(); 243 | List fileKey = MapUtil.keyList(fileList); 244 | for (String fk : fileKey) { 245 | FileBody file1 = new FileBody(fileList.get(fk)); 246 | reqEntity.addPart(fk, file1); 247 | } 248 | 249 | List paramsKey = MapUtil.keyList(params); 250 | for (String pk : paramsKey) { 251 | StringBody name = new StringBody(params.get(pk), ContentType.MULTIPART_FORM_DATA); 252 | reqEntity.addPart(pk, name); 253 | } 254 | 255 | return getString(httppost, reqEntity.build()); 256 | } 257 | 258 | private String getString(HttpPost httppost, HttpEntity httpEntity) throws IOException { 259 | if (httpEntity != null) { 260 | httppost.setEntity(httpEntity); 261 | } 262 | return getString(httppost); 263 | } 264 | 265 | private String getString(HttpRequestBase http) throws IOException { 266 | CloseableHttpResponse response = getHttpClient().execute(http, context); 267 | responseHeaders = response.getAllHeaders(); 268 | int statusCode = response.getStatusLine().getStatusCode(); 269 | if (statusCode != 200) { 270 | info = new Info(response.getStatusLine().getStatusCode(), responseHeaders); 271 | } 272 | String result = EntityUtils.toString(response.getEntity()); 273 | 274 | info = new Info(result, response.getStatusLine().getStatusCode(), responseHeaders); 275 | 276 | response.close(); 277 | return result; 278 | } 279 | 280 | private Info get(HttpRequestBase http) throws IOException { 281 | CloseableHttpResponse response = getHttpClient().execute(http, context); 282 | responseHeaders = response.getAllHeaders(); 283 | int statusCode = response.getStatusLine().getStatusCode(); 284 | if (statusCode != 200) { 285 | return new Info(statusCode, responseHeaders); 286 | } 287 | String result = EntityUtils.toString(response.getEntity()); 288 | response.close(); 289 | return new Info(result, statusCode, responseHeaders); 290 | } 291 | 292 | /** 293 | * 禁止重复调用 294 | * @return 295 | */ 296 | public Info getResponseInfo() { 297 | return info; 298 | } 299 | 300 | /** 301 | * @author xiangqi 302 | * @date 2018-01-29 下午 2:10 303 | */ 304 | private static class HttpClientConnectionMonitorThread extends Thread { 305 | 306 | private final HttpClientConnectionManager connManager; 307 | private volatile boolean shutdown = false; 308 | 309 | HttpClientConnectionMonitorThread(HttpClientConnectionManager connManager) { 310 | super(); 311 | this.setName("http-connection-monitor"); 312 | this.setDaemon(true); 313 | this.connManager = connManager; 314 | this.start(); 315 | } 316 | 317 | @Override 318 | public void run() { 319 | System.out.println("清理过期http链接 open:" + !shutdown); 320 | while (!shutdown) { 321 | synchronized (this) { 322 | try { 323 | // 等待 324 | wait(10000); 325 | // 关闭过期的链接 326 | connManager.closeExpiredConnections(); 327 | // 选择关闭 空闲30秒的链接 328 | connManager.closeIdleConnections(30, TimeUnit.SECONDS); 329 | } catch (Throwable e) { 330 | log.error("http 连接池清理任务抛出异常", e); 331 | } 332 | } 333 | } 334 | } 335 | 336 | public boolean isShutdown() { 337 | return shutdown; 338 | } 339 | } 340 | 341 | 342 | /** 343 | * 获取HttpClient对象 344 | * @author YL 345 | */ 346 | private CloseableHttpClient getHttpClient() { 347 | Builder bu = RequestConfig.custom(); 348 | bu.setConnectTimeout(CONNECT_TIMEOUT); 349 | bu.setSocketTimeout(SOCKET_TIMEOUT); 350 | return HttpClients.custom() 351 | .setConnectionManager(POOL) 352 | .setDefaultRequestConfig(bu.build()) 353 | .setRetryHandler(HTTP_REQUEST_RETRY_HANDLER).build(); 354 | } 355 | 356 | public static void main(String[] args) throws IOException { 357 | String url = "https://www.xxxxxx.com/api/video/pay/start?start=1&count=15&keyWord="; 358 | 359 | SimpleHttpClient sc = new SimpleHttpClient(); 360 | System.out.println(sc.get(url)); 361 | System.out.println(sc.get("https://www.xxxxxx.com/api/video/playDetail?videoId=520&token=")); 362 | System.out.println(sc.get("https://www.xxxxxx.com/api/video/getVideList?recordId=520&type=video")); 363 | System.out.println(sc.get("https://www.xxxxxx.com/api/video/playDetail?videoId=462&token=")); 364 | } 365 | 366 | public static class Info { 367 | 368 | private final String body; 369 | private final int code; 370 | private final Header[] heads; 371 | 372 | public Info(int code, Header[] headers) { 373 | this.body = null; 374 | this.heads = initHeads(headers); 375 | this.code = code; 376 | } 377 | 378 | private Header[] initHeads(Header[] headers) { 379 | return headers; 380 | } 381 | 382 | public Info(String body, int code, Header[] headers) { 383 | this.body = body; 384 | this.code = code; 385 | this.heads = initHeads(headers); 386 | } 387 | 388 | public int getCode() { 389 | return code; 390 | } 391 | 392 | public String getBody() { 393 | return body; 394 | } 395 | 396 | public Header[] getHeads() { 397 | return heads; 398 | } 399 | 400 | @Override 401 | public String toString() { 402 | return "Info{" + 403 | " code=" + code + 404 | ", \n heads=" + heads + 405 | '}'; 406 | } 407 | } 408 | 409 | } 410 | --------------------------------------------------------------------------------