diff --git a/develop-toolkit-base/pom.xml b/develop-toolkit-base/pom.xml index 6e5c7cc..5128436 100644 --- a/develop-toolkit-base/pom.xml +++ b/develop-toolkit-base/pom.xml @@ -3,7 +3,7 @@ develop-toolkit com.github.developframework - 1.0.6-SNAPSHOT + 1.0.7-SNAPSHOT 4.0.0 @@ -32,5 +32,10 @@ com.fasterxml.jackson.datatype jackson-datatype-jsr310 + + com.fasterxml.jackson.dataformat + jackson-dataformat-xml + true + \ No newline at end of file diff --git a/develop-toolkit-base/src/main/java/develop/toolkit/base/components/ConcurrentTesting.java b/develop-toolkit-base/src/main/java/develop/toolkit/base/components/ConcurrentTesting.java new file mode 100644 index 0000000..7edfd1b --- /dev/null +++ b/develop-toolkit-base/src/main/java/develop/toolkit/base/components/ConcurrentTesting.java @@ -0,0 +1,67 @@ +package develop.toolkit.base.components; + +import develop.toolkit.base.struct.http.HttpClientReceiver; + +import java.util.concurrent.ExecutorService; +import java.util.concurrent.Executors; +import java.util.function.Consumer; +import java.util.function.Function; + +/** + * 并发测试工具 + * + * @author qiushui on 2021-12-04. + */ +public final class ConcurrentTesting { + + private final ExecutorService service; + + private final int cycleCount; + + private final int triggerCount; + + private final int interval; + + private final HttpClientHelper helper; + + public ConcurrentTesting(int threadCount, int triggerCount, int cycleCount, int interval) { + this.helper = HttpClientHelper.buildDefault(); + this.service = Executors.newFixedThreadPool(threadCount); + this.triggerCount = triggerCount; + this.cycleCount = cycleCount; + this.interval = interval; + } + + public ConcurrentTesting(HttpClientHelper helper, int threadCount, int triggerCount, int cycleCount, int interval) { + this.helper = helper; + this.service = Executors.newFixedThreadPool(threadCount); + this.triggerCount = triggerCount; + this.cycleCount = cycleCount; + this.interval = interval; + } + + public void start(Function> function) { + start( + function, + receiver -> System.out.printf("【%s】%s\t%s%n", Thread.currentThread().getName(), receiver.getHttpStatus(), receiver.getBody()) + ); + } + + public void start(Function> function, Consumer> consumer) { + for (int i = 0; i < triggerCount; i++) { + service.execute(() -> { + for (int j = 0; j < cycleCount; j++) { + if (interval > 0) { + try { + Thread.sleep(interval); + } catch (InterruptedException e) { + e.printStackTrace(); + } + } + consumer.accept(function.apply(helper)); + } + }); + } + service.shutdown(); + } +} diff --git a/develop-toolkit-base/src/main/java/develop/toolkit/base/components/HttpClientHelper.java b/develop-toolkit-base/src/main/java/develop/toolkit/base/components/HttpClientHelper.java index 1d5905e..e3f2943 100644 --- a/develop-toolkit-base/src/main/java/develop/toolkit/base/components/HttpClientHelper.java +++ b/develop-toolkit-base/src/main/java/develop/toolkit/base/components/HttpClientHelper.java @@ -1,57 +1,145 @@ package develop.toolkit.base.components; -import develop.toolkit.base.struct.http.HttpClientSender; +import develop.toolkit.base.struct.http.HttpClientGlobalOptions; +import develop.toolkit.base.struct.http.HttpPostProcessor; +import lombok.AccessLevel; import lombok.RequiredArgsConstructor; -import lombok.Setter; -import lombok.extern.slf4j.Slf4j; +import javax.net.ssl.KeyManagerFactory; +import javax.net.ssl.SSLContext; +import java.io.InputStream; +import java.net.InetSocketAddress; +import java.net.ProxySelector; import java.net.http.HttpClient; +import java.security.KeyStore; +import java.security.SecureRandom; import java.time.Duration; +import java.util.Map; +import java.util.concurrent.Executor; /** * Http发送助手 * * @author qiushui on 2020-09-10. */ -@Slf4j -@RequiredArgsConstructor -public final class HttpClientHelper { +@SuppressWarnings("unused") +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public class HttpClientHelper { private final HttpClient httpClient; - @Setter - private boolean onlyPrintFailed = true; + private final HttpClientGlobalOptions options; - public HttpClientHelper(Duration connectTimeout) { - this.httpClient = HttpClient - .newBuilder() - .version(HttpClient.Version.HTTP_1_1) - .connectTimeout(connectTimeout) - .followRedirects(HttpClient.Redirect.NEVER) - .build(); + public static Builder builder() { + return new Builder(); } - public HttpClientHelper() { - this(Duration.ofSeconds(5L)); + public static HttpClientHelper buildDefault() { + return builder().build(); + } + + public HttpClientSender request(String method, String url) { + return new HttpClientSender(httpClient, method, url, options); } public HttpClientSender get(String url) { - return new HttpClientSender(httpClient, "GET", url).onlyPrintFailed(onlyPrintFailed); + return request("GET", url); } public HttpClientSender post(String url) { - return new HttpClientSender(httpClient, "POST", url).onlyPrintFailed(onlyPrintFailed); + return request("POST", url); } public HttpClientSender put(String url) { - return new HttpClientSender(httpClient, "PUT", url).onlyPrintFailed(onlyPrintFailed); + return request("PUT", url); } public HttpClientSender delete(String url) { - return new HttpClientSender(httpClient, "DELETE", url).onlyPrintFailed(onlyPrintFailed); + return request("DELETE", url); } - public HttpClientSender request(String method, String url) { - return new HttpClientSender(httpClient, method, url).onlyPrintFailed(onlyPrintFailed); + public static class Builder { + + private SSLContext sslContext; + + private Duration connectTimeout = Duration.ofSeconds(10L); + + private InetSocketAddress proxyAddress; + + private Executor executor; + + private final HttpClientGlobalOptions globalOptions = new HttpClientGlobalOptions(); + + public Builder onlyPrintFailed(boolean onlyPrintFailed) { + globalOptions.onlyPrintFailed = onlyPrintFailed; + return this; + } + + public Builder connectTimeout(Duration connectTimeout) { + this.connectTimeout = connectTimeout; + return this; + } + + public Builder proxyAddress(InetSocketAddress proxyAddress) { + this.proxyAddress = proxyAddress; + return this; + } + + public Builder executor(Executor executor) { + this.executor = executor; + return this; + } + + public Builder readTimeout(Duration readTimeout) { + globalOptions.readTimeout = readTimeout; + return this; + } + + public Builder addGlobalPostProcessor(HttpPostProcessor globalPostProcessor) { + globalOptions.postProcessors.add(globalPostProcessor); + return this; + } + + public Builder constant(String key, String value) { + globalOptions.constants.putConstant(key, value); + return this; + } + + public Builder constantMap(Map constantMap) { + globalOptions.constants.putConstantMap(constantMap); + return this; + } + + public Builder ssl(InputStream pkcs12, String password) { + try { + KeyStore ks = KeyStore.getInstance("PKCS12"); + char[] passwordChars = password.toCharArray(); + ks.load(pkcs12, passwordChars); + KeyManagerFactory kmf = KeyManagerFactory.getInstance(KeyManagerFactory.getDefaultAlgorithm()); + kmf.init(ks, passwordChars); + sslContext = SSLContext.getInstance("SSL"); + sslContext.init(kmf.getKeyManagers(), null, new SecureRandom()); + } catch (Exception e) { + throw new IllegalStateException(e.getMessage()); + } + return this; + } + + public HttpClientHelper build() { + final HttpClient.Builder builder = HttpClient.newBuilder() + .version(HttpClient.Version.HTTP_1_1) + .followRedirects(HttpClient.Redirect.NEVER) + .connectTimeout(connectTimeout); + if (sslContext != null) { + builder.sslContext(sslContext); + } + if (proxyAddress != null) { + builder.proxy(ProxySelector.of(proxyAddress)); + } + if (executor != null) { + builder.executor(executor); + } + return new HttpClientHelper(builder.build(), globalOptions); + } } } diff --git a/develop-toolkit-base/src/main/java/develop/toolkit/base/components/HttpClientSender.java b/develop-toolkit-base/src/main/java/develop/toolkit/base/components/HttpClientSender.java new file mode 100644 index 0000000..b2e4b9a --- /dev/null +++ b/develop-toolkit-base/src/main/java/develop/toolkit/base/components/HttpClientSender.java @@ -0,0 +1,260 @@ +package develop.toolkit.base.components; + +import develop.toolkit.base.struct.http.*; +import develop.toolkit.base.utils.K; +import develop.toolkit.base.utils.StringAdvice; +import lombok.Getter; + +import java.io.IOException; +import java.net.URI; +import java.net.http.*; +import java.nio.file.OpenOption; +import java.nio.file.Path; +import java.time.Duration; +import java.time.Instant; +import java.time.temporal.ChronoUnit; +import java.util.LinkedHashMap; +import java.util.LinkedList; +import java.util.List; +import java.util.Map; +import java.util.concurrent.CompletableFuture; + +/** + * Http发送器 + * + * @author qiushui on 2020-09-10. + */ +@Getter +public final class HttpClientSender { + + private final HttpClient httpClient; + + private final String method; + + private final String url; + + private final Map headers = new LinkedHashMap<>(); + + private final Map parameters = new LinkedHashMap<>(); + + private final List postProcessors; + + private Duration readTimeout; + + private String debugLabel; + + private HttpRequestBody requestBody; + + private String requestStringBody; + + private boolean onlyPrintFailed; + + private URI uri; + + private final HttpClientConstants constants; + + protected HttpClientSender(HttpClient httpClient, String method, String url, HttpClientGlobalOptions options) { + this.httpClient = httpClient; + this.method = method; + this.readTimeout = options.readTimeout; + this.postProcessors = new LinkedList<>(options.postProcessors); + this.constants = options.constants; + this.onlyPrintFailed = options.onlyPrintFailed; + this.url = constants.replace(url); + } + + public HttpClientSender header(String header, String value) { + this.headers.put(header, constants.replace(value)); + return this; + } + + public HttpClientSender headers(Map customHeaders) { + if (customHeaders != null) { + customHeaders.forEach((k, v) -> this.headers.put(k, constants.replace(v))); + } + return this; + } + + public HttpClientSender headerAuthorization(String value) { + this.headers.put("Authorization", constants.replace(value)); + return this; + } + + public HttpClientSender headerContentType(String contentType) { + this.headers.put("Content-Type", contentType); + return this; + } + + public HttpClientSender parameter(String parameter, Object value) { + this.parameters.put(parameter, value); + return this; + } + + public HttpClientSender parameters(Map parameterMap) { + if (parameterMap != null) { + parameterMap.forEach((k, v) -> { + if (v instanceof String) { + this.parameters.put(k, constants.replace((String) v)); + } else { + this.parameters.put(k, v); + } + }); + } + return this; + } + + public HttpClientSender readTimeout(Duration readTimeout) { + this.readTimeout = readTimeout; + return this; + } + + public HttpClientSender debugLabel(String debugLabel) { + this.debugLabel = debugLabel; + return this; + } + + public HttpClientSender onlyPrintFailed(boolean onlyPrintFailed) { + this.onlyPrintFailed = onlyPrintFailed; + return this; + } + + public HttpClientSender addPostProcessor(HttpPostProcessor postProcessor) { + postProcessors.add(postProcessor); + return this; + } + + public HttpClientSender bodyJson(String json) { + headers.put("Content-Type", "application/json;charset=utf-8"); + this.requestBody = K.map(json, RawRequestBody::new); + return this; + } + + public HttpClientSender bodyXml(String xml) { + headers.put("Content-Type", "application/xml;charset=utf-8"); + this.requestBody = K.map(xml, RawRequestBody::new); + return this; + } + + public HttpClientSender bodyText(String text) { + headers.put("Content-Type", "text/plain;charset=utf-8"); + this.requestBody = K.map(text, RawRequestBody::new); + return this; + } + + public HttpClientSender bodyBytes(byte[] bytes) { + this.requestBody = K.map(bytes, ByteRequestBody::new); + return this; + } + + public HttpClientSender bodyMultiPartFormData(MultiPartFormDataBody multiPartFormDataBody) { + headers.put("Content-Type", "multipart/form-data; boundary=" + multiPartFormDataBody.getBoundary()); + this.requestBody = multiPartFormDataBody; + return this; + } + + public HttpClientSender bodyFormUrlencoded(FormUrlencodedBody formUrlencodedBody) { + headers.put("Content-Type", "application/x-www-form-urlencoded"); + this.requestBody = formUrlencodedBody; + return this; + } + + public void download(Path path, OpenOption... openOptions) { + send(HttpResponse.BodyHandlers::ofByteArray).ifSuccess(r -> r.save(path, openOptions)); + } + + public HttpClientReceiver send() { + return send(new StringBodySenderHandler()); + } + + public CompletableFuture> sendAsync() { + return sendAsync(new StringBodySenderHandler()); + } + + /** + * 核心发送逻辑 + * + * @param senderHandler 发送器扩展逻辑 + * @param 响应内容 + * @return receiver + */ + public HttpClientReceiver send(SenderHandler senderHandler) { + this.uri = URI.create(url + StringAdvice.urlParametersFormat(parameters, true)); + final HttpRequest.Builder builder = HttpRequest + .newBuilder() + .version(httpClient.version()) + .uri(uri); + headers.forEach(builder::header); + final HttpRequest request = builder + .method(method, senderHandler.bodyPublisher(requestBody)) + .timeout(readTimeout) + .build(); + requestStringBody = HttpRequestBody.bodyToString(requestBody); + final HttpClientReceiver receiver = new HttpClientReceiver<>(); + Instant start = Instant.now(); + try { + HttpResponse response = httpClient.send(request, senderHandler.bodyHandler()); + receiver.setHttpStatus(response.statusCode()); + receiver.setHeaders(response.headers().map()); + receiver.setBody(response.body()); + } catch (HttpConnectTimeoutException e) { + receiver.setConnectTimeout(true); + } catch (HttpTimeoutException e) { + receiver.setReadTimeout(true); + } catch (InterruptedException | IOException e) { + e.printStackTrace(); + receiver.setErrorMessage(e.getMessage()); + } finally { + receiver.setCostTime(start.until(Instant.now(), ChronoUnit.MILLIS)); + doPostProcessors(receiver); + } + return receiver; + } + + /** + * 核心发送逻辑(异步) + * + * @param senderHandler 发送器扩展逻辑 + * @param 响应内容 + * @return completableFuture + */ + public CompletableFuture> sendAsync(SenderHandler senderHandler) { + this.uri = URI.create(url + StringAdvice.urlParametersFormat(parameters, true)); + final HttpRequest.Builder builder = HttpRequest + .newBuilder() + .version(httpClient.version()) + .uri(uri); + headers.forEach(builder::header); + final HttpRequest request = builder + .method(method, senderHandler.bodyPublisher(requestBody)) + .timeout(readTimeout) + .build(); + requestStringBody = HttpRequestBody.bodyToString(requestBody); + final Instant start = Instant.now(); + return httpClient + .sendAsync(request, senderHandler.bodyHandler()) + .handle((response, e) -> { + final HttpClientReceiver receiver = new HttpClientReceiver<>(); + if (e == null) { + receiver.setHttpStatus(response.statusCode()); + receiver.setHeaders(response.headers().map()); + receiver.setBody(response.body()); + } else if (e instanceof HttpConnectTimeoutException) { + receiver.setConnectTimeout(true); + } else if (e instanceof HttpTimeoutException) { + receiver.setReadTimeout(true); + } else if (e instanceof InterruptedException || e instanceof IOException) { + e.printStackTrace(); + receiver.setErrorMessage(e.getMessage()); + } + receiver.setCostTime(start.until(Instant.now(), ChronoUnit.MILLIS)); + doPostProcessors(receiver); + return receiver; + }); + } + + private void doPostProcessors(HttpClientReceiver receiver) { + for (HttpPostProcessor postProcessor : postProcessors) { + postProcessor.process(this, receiver); + } + } +} diff --git a/develop-toolkit-base/src/main/java/develop/toolkit/base/components/IWantData.java b/develop-toolkit-base/src/main/java/develop/toolkit/base/components/IWantData.java new file mode 100644 index 0000000..5519583 --- /dev/null +++ b/develop-toolkit-base/src/main/java/develop/toolkit/base/components/IWantData.java @@ -0,0 +1,122 @@ +package develop.toolkit.base.components; + +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; + +import java.util.function.Consumer; +import java.util.function.Function; +import java.util.function.Supplier; + +/** + * 我想要的数据 + * + * @author qiushui on 2021-10-30. + */ +@Getter +@RequiredArgsConstructor(access = AccessLevel.PRIVATE) +public class IWantData { + + // 是否成功 + private final boolean success; + + // 失败信息 + private final String message; + + // 想要的数据 + private final T data; + + /** + * 成功获取 + * + * @param data 数据 + */ + public static IWantData ok(T data) { + return new IWantData<>(true, null, data); + } + + /** + * 成功获取 + */ + public static IWantData ok() { + return ok(null); + } + + /** + * 获取失败 + * + * @param message 失败信息 + */ + public static IWantData fail(String message) { + return new IWantData<>(false, message, null); + } + + /** + * 转换 + * + * @param function 转换函数 + * @param 目标类型 + * @return 转换值 + */ + public IWantData map(Function function) { + return success ? IWantData.ok(function.apply(data)) : IWantData.fail(message); + } + + /** + * 扁平化转换 + * + * @param function 转换函数 + * @param 目标类型 + * @return 转换值 + */ + public IWantData flatMap(Function> function) { + return success ? function.apply(data) : IWantData.fail(message); + } + + /** + * 提供默认值的数据获取 + * + * @param defaultValue 默认值 + * @param messageConsumer 失败信息处理 + * @return 数据值 + */ + public T returnData(T defaultValue, Consumer messageConsumer) { + if (success) { + return data; + } + if (messageConsumer != null) { + messageConsumer.accept(message); + } + return defaultValue; + } + + /** + * 提供默认值的数据获取 + * + * @param defaultSupplier 默认值提供器 + * @param messageConsumer 失败信息处理 + * @return 数据值 + */ + public T returnData(Supplier defaultSupplier, Consumer messageConsumer) { + if (success) { + return data; + } + if (messageConsumer != null) { + messageConsumer.accept(message); + } + return defaultSupplier.get(); + } + + /** + * 会抛异常的数据获取 + * + * @param throwableFunction 异常函数 + * @return 数据值 + */ + public T returnDataThrows(Function throwableFunction) { + if (success) { + return data; + } + throw throwableFunction.apply(message); + } +} diff --git a/develop-toolkit-base/src/main/java/develop/toolkit/base/components/SnowflakeIdWorker.java b/develop-toolkit-base/src/main/java/develop/toolkit/base/components/SnowflakeIdWorker.java new file mode 100644 index 0000000..70b72b7 --- /dev/null +++ b/develop-toolkit-base/src/main/java/develop/toolkit/base/components/SnowflakeIdWorker.java @@ -0,0 +1,128 @@ +package develop.toolkit.base.components; + +import java.time.Instant; + +/** + * 雪花算法ID生成器 + * + * @author qiushui on 2021-08-05. + */ +public class SnowflakeIdWorker { + + /** + * 机器id所占的位数 + */ + private final long workerIdBits = 5L; + + /** + * 数据标识id所占的位数 + */ + private final long datacenterIdBits = 5L; + + /** + * 工作机器ID(0~31) + */ + private final long workerId; + + /** + * 数据中心ID(0~31) + */ + private final long datacenterId; + + /** + * 毫秒内序列(0~4095) + */ + private long sequence = 0L; + + /** + * 上次生成ID的时间截 + */ + private long lastTimestamp = -1L; + + /** + * 构造函数 + * + * @param workerId 工作ID (0~31) + * @param datacenterId 数据中心ID (0~31) + */ + public SnowflakeIdWorker(long workerId, long datacenterId) { + // 支持的最大机器id,结果是31 (这个移位算法可以很快的计算出几位二进制数所能表示的最大十进制数) + long maxWorkerId = ~(-1L << workerIdBits); + if (workerId > maxWorkerId || workerId < 0) { + throw new IllegalArgumentException(String.format("worker Id can't be greater than %d or less than 0", maxWorkerId)); + } + // 支持的最大数据标识id,结果是31 + long maxDatacenterId = ~(-1L << datacenterIdBits); + if (datacenterId > maxDatacenterId || datacenterId < 0) { + throw new IllegalArgumentException(String.format("datacenter Id can't be greater than %d or less than 0", maxDatacenterId)); + } + this.workerId = workerId; + this.datacenterId = datacenterId; + } + + /** + * 获得下一个ID (该方法是线程安全的) + * + * @return SnowflakeId + */ + public synchronized long nextId() { + long timestamp = timeGen(); + // 如果当前时间小于上一次ID生成的时间戳,说明系统时钟回退过这个时候应当抛出异常 + if (timestamp < lastTimestamp) { + throw new RuntimeException( + String.format("Clock moved backwards. Refusing to generate id for %d milliseconds", lastTimestamp - timestamp)); + } + // 如果是同一时间生成的,则进行毫秒内序列 + // 序列在id中占的位数 + long sequenceBits = 12L; + if (lastTimestamp == timestamp) { + // 生成序列的掩码,这里为4095 (0b111111111111=0xfff=4095) + long sequenceMask = ~(-1L << sequenceBits); + sequence = (sequence + 1) & sequenceMask; + // 毫秒内序列溢出 + if (sequence == 0) { + // 阻塞到下一个毫秒,获得新的时间戳 + timestamp = tilNextMillis(lastTimestamp); + } + } + // 时间戳改变,毫秒内序列重置 + else { + sequence = 0L; + } + + // 上次生成ID的时间截 + lastTimestamp = timestamp; + long datacenterIdShift = sequenceBits + workerIdBits; + // 时间截向左移22位(5+5+12) + long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits; + // 开始时间截 (2020-01-01) + long twepoch = 1577808000000L; + return ((timestamp - twepoch) << timestampLeftShift) + | (datacenterId << datacenterIdShift) + | (workerId << sequenceBits) + | sequence; + } + + /** + * 阻塞到下一个毫秒,直到获得新的时间戳 + * + * @param lastTimestamp 上次生成ID的时间截 + * @return 当前时间戳 + */ + private long tilNextMillis(long lastTimestamp) { + long timestamp = timeGen(); + while (timestamp <= lastTimestamp) { + timestamp = timeGen(); + } + return timestamp; + } + + /** + * 高并发环境下,返回以毫秒为单位的当前时间 + * + * @return 当前时间(毫秒) + */ + private long timeGen() { + return Instant.now().toEpochMilli(); + } +} diff --git a/develop-toolkit-base/src/main/java/develop/toolkit/base/components/StopWatch.java b/develop-toolkit-base/src/main/java/develop/toolkit/base/components/StopWatch.java index 0dd8725..ec9934b 100644 --- a/develop-toolkit-base/src/main/java/develop/toolkit/base/components/StopWatch.java +++ b/develop-toolkit-base/src/main/java/develop/toolkit/base/components/StopWatch.java @@ -3,6 +3,7 @@ import develop.toolkit.base.utils.DateTimeAdvice; import java.time.Instant; +import java.time.temporal.ChronoUnit; import java.util.Map; import java.util.concurrent.ConcurrentHashMap; @@ -31,8 +32,11 @@ public long end() { } public long end(String name) { - final Instant end = Instant.now(); - return end.toEpochMilli() - startInstantMap.get(name).toEpochMilli(); + return startInstantMap.get(name).until(Instant.now(), ChronoUnit.MILLIS); + } + + public long interval(String startName, String endName) { + return startInstantMap.get(startName).until(startInstantMap.get(endName), ChronoUnit.MILLIS); } public String formatEnd() { diff --git a/develop-toolkit-base/src/main/java/develop/toolkit/base/exception/CryptException.java b/develop-toolkit-base/src/main/java/develop/toolkit/base/exception/CryptException.java new file mode 100644 index 0000000..209635a --- /dev/null +++ b/develop-toolkit-base/src/main/java/develop/toolkit/base/exception/CryptException.java @@ -0,0 +1,13 @@ +package develop.toolkit.base.exception; + +/** + * 加密解密异常 + * + * @author qiushui on 2021-10-08. + */ +public class CryptException extends RuntimeException { + + public CryptException(Throwable cause) { + super(cause); + } +} diff --git a/develop-toolkit-base/src/main/java/develop/toolkit/base/struct/HttpAdviceResponse.java b/develop-toolkit-base/src/main/java/develop/toolkit/base/struct/HttpAdviceResponse.java deleted file mode 100644 index 1471cf2..0000000 --- a/develop-toolkit-base/src/main/java/develop/toolkit/base/struct/HttpAdviceResponse.java +++ /dev/null @@ -1,47 +0,0 @@ -package develop.toolkit.base.struct; - -import lombok.AllArgsConstructor; -import lombok.Getter; -import lombok.NoArgsConstructor; -import lombok.Setter; -import org.apache.commons.lang3.StringUtils; - -import java.nio.charset.StandardCharsets; -import java.util.List; -import java.util.Map; - -@Getter -@Setter -@NoArgsConstructor -@AllArgsConstructor -@SuppressWarnings("unused") -public class HttpAdviceResponse { - - private int httpStatus; - - private Map> headers; - - private byte[] body; - - private long costTime; - - public String bodyOfString() { - return new String(body, StandardCharsets.UTF_8); - } - - public String getHeader(String header) { - return StringUtils.join(headers.getOrDefault(header, List.of()), ";"); - } - - @Override - public String toString() { - StringBuilder sb = new StringBuilder(); - sb - .append("\nhttp response:\n status: ").append(httpStatus).append("\n headers:\n"); - for (Map.Entry> entry : headers.entrySet()) { - sb.append(" ").append(entry.getKey()).append(": ").append(StringUtils.join(entry.getValue(), ";")).append("\n"); - } - sb.append(" body: ").append(bodyOfString()); - return sb.toString(); - } -} diff --git a/develop-toolkit-base/src/main/java/develop/toolkit/base/struct/ThreeValues.java b/develop-toolkit-base/src/main/java/develop/toolkit/base/struct/ThreeValues.java index 66250b3..aba46fa 100644 --- a/develop-toolkit-base/src/main/java/develop/toolkit/base/struct/ThreeValues.java +++ b/develop-toolkit-base/src/main/java/develop/toolkit/base/struct/ThreeValues.java @@ -31,6 +31,9 @@ public static ThreeValues of(T firstValue, S secondValue, U t } public static ThreeValues of(T[] objs) { + if (objs.length != 3) { + throw new IllegalArgumentException("TwoValues of array length is " + objs.length); + } return new ThreeValues<>(objs[0], objs[1], objs[2]); } } diff --git a/develop-toolkit-base/src/main/java/develop/toolkit/base/struct/TwoValues.java b/develop-toolkit-base/src/main/java/develop/toolkit/base/struct/TwoValues.java index b9cf56c..4517ef9 100644 --- a/develop-toolkit-base/src/main/java/develop/toolkit/base/struct/TwoValues.java +++ b/develop-toolkit-base/src/main/java/develop/toolkit/base/struct/TwoValues.java @@ -29,6 +29,9 @@ public static TwoValues of(T firstValue, S secondValue) { } public static TwoValues of(T[] objs) { + if (objs.length != 2) { + throw new IllegalArgumentException("TwoValues of array length is " + objs.length); + } return new TwoValues<>(objs[0], objs[1]); } } diff --git a/develop-toolkit-base/src/main/java/develop/toolkit/base/struct/ZipWrapper.java b/develop-toolkit-base/src/main/java/develop/toolkit/base/struct/ZipWrapper.java new file mode 100644 index 0000000..8efc582 --- /dev/null +++ b/develop-toolkit-base/src/main/java/develop/toolkit/base/struct/ZipWrapper.java @@ -0,0 +1,63 @@ +package develop.toolkit.base.struct; + +import lombok.Getter; +import lombok.Setter; + +import java.io.InputStream; +import java.nio.file.attribute.FileTime; +import java.time.LocalDateTime; +import java.util.List; +import java.util.function.Supplier; +import java.util.zip.ZipEntry; + +/** + * @author qiushui on 2022-04-19. + */ +@Getter +@Setter +public class ZipWrapper { + + private String filename; + + private Supplier inputStreamSupplier; + + private boolean file; + + private List children; + + private boolean stored; + + private long compressedSize; + + private long size; + + private long crc; + + private FileTime creationTime; + + private FileTime lastAccessTime; + + private FileTime lastModifyTime; + + private long time; + + private String comment; + + private LocalDateTime timeLocal; + + private byte[] extra; + + public void configureZipEntry(ZipEntry zipEntry) { + zipEntry.setMethod(stored ? ZipEntry.STORED : ZipEntry.DEFLATED); + zipEntry.setCompressedSize(compressedSize); + zipEntry.setSize(size); + zipEntry.setCrc(crc); + zipEntry.setCreationTime(creationTime); + zipEntry.setLastAccessTime(lastAccessTime); + zipEntry.setLastModifiedTime(lastModifyTime); + zipEntry.setTime(time); + zipEntry.setTimeLocal(timeLocal); + zipEntry.setExtra(extra); + zipEntry.setComment(comment); + } +} diff --git a/develop-toolkit-base/src/main/java/develop/toolkit/base/struct/http/ByteRequestBody.java b/develop-toolkit-base/src/main/java/develop/toolkit/base/struct/http/ByteRequestBody.java new file mode 100644 index 0000000..6052fff --- /dev/null +++ b/develop-toolkit-base/src/main/java/develop/toolkit/base/struct/http/ByteRequestBody.java @@ -0,0 +1,22 @@ +package develop.toolkit.base.struct.http; + +import lombok.RequiredArgsConstructor; + +/** + * @author qiushui on 2021-09-16. + */ +@RequiredArgsConstructor +public class ByteRequestBody implements HttpRequestBody { + + private final byte[] bytes; + + @Override + public String toString() { + return "(Binary byte data)"; + } + + @Override + public byte[] getBody() { + return bytes; + } +} diff --git a/develop-toolkit-base/src/main/java/develop/toolkit/base/struct/http/FormUrlencodedBody.java b/develop-toolkit-base/src/main/java/develop/toolkit/base/struct/http/FormUrlencodedBody.java index ed74afb..2571ba6 100644 --- a/develop-toolkit-base/src/main/java/develop/toolkit/base/struct/http/FormUrlencodedBody.java +++ b/develop-toolkit-base/src/main/java/develop/toolkit/base/struct/http/FormUrlencodedBody.java @@ -10,7 +10,7 @@ * @author qiushui on 2020-09-15. */ @RequiredArgsConstructor -public class FormUrlencodedBody { +public final class FormUrlencodedBody implements HttpRequestBody { private final Map pairs; @@ -18,10 +18,16 @@ public FormUrlencodedBody() { pairs = new HashMap<>(); } - public String buildBody() { + @Override + public String getBody() { return StringAdvice.urlParametersFormat(pairs, false); } + @Override + public String toString() { + return getBody(); + } + public FormUrlencodedBody addPair(String name, Object value) { pairs.put(name, value); return this; diff --git a/develop-toolkit-base/src/main/java/develop/toolkit/base/struct/http/HttpClientConstants.java b/develop-toolkit-base/src/main/java/develop/toolkit/base/struct/http/HttpClientConstants.java new file mode 100644 index 0000000..faadd1e --- /dev/null +++ b/develop-toolkit-base/src/main/java/develop/toolkit/base/struct/http/HttpClientConstants.java @@ -0,0 +1,36 @@ +package develop.toolkit.base.struct.http; + +import develop.toolkit.base.utils.StringAdvice; + +import java.util.HashMap; +import java.util.Map; + +/** + * 常量可以被{{}}占位符取代 + * + * @author qiushui on 2021-09-17. + */ +public final class HttpClientConstants { + + private final Map constants = new HashMap<>(); + + public void putConstant(String key, String value) { + constants.put(key, value); + } + + public void putConstantMap(Map constantMap) { + constants.putAll(constantMap); + } + + public String replace(String string) { + if (!constants.isEmpty()) { + for (String key : StringAdvice.regexMatchStartEnd(string, "\\{\\{", "\\}\\}")) { + final String value = constants.get(key); + if (value != null) { + string = string.replace("{{" + key + "}}", value); + } + } + } + return string; + } +} diff --git a/develop-toolkit-base/src/main/java/develop/toolkit/base/struct/http/HttpClientGlobalOptions.java b/develop-toolkit-base/src/main/java/develop/toolkit/base/struct/http/HttpClientGlobalOptions.java new file mode 100644 index 0000000..5b09daa --- /dev/null +++ b/develop-toolkit-base/src/main/java/develop/toolkit/base/struct/http/HttpClientGlobalOptions.java @@ -0,0 +1,19 @@ +package develop.toolkit.base.struct.http; + +import java.time.Duration; +import java.util.LinkedList; +import java.util.List; + +/** + * @author qiushui on 2021-09-17. + */ +public class HttpClientGlobalOptions { + + public boolean onlyPrintFailed = true; + + public Duration readTimeout = Duration.ofSeconds(30L); + + public List postProcessors = new LinkedList<>(List.of(new PrintLogHttpPostProcessor())); + + public HttpClientConstants constants = new HttpClientConstants(); +} diff --git a/develop-toolkit-base/src/main/java/develop/toolkit/base/struct/http/HttpClientSender.java b/develop-toolkit-base/src/main/java/develop/toolkit/base/struct/http/HttpClientSender.java deleted file mode 100644 index 7cca72a..0000000 --- a/develop-toolkit-base/src/main/java/develop/toolkit/base/struct/http/HttpClientSender.java +++ /dev/null @@ -1,226 +0,0 @@ -package develop.toolkit.base.struct.http; - -import develop.toolkit.base.utils.DateTimeAdvice; -import develop.toolkit.base.utils.StringAdvice; -import lombok.Getter; -import lombok.extern.slf4j.Slf4j; -import org.apache.commons.lang3.StringUtils; - -import java.io.IOException; -import java.net.URI; -import java.net.http.*; -import java.nio.file.OpenOption; -import java.nio.file.Path; -import java.time.Duration; -import java.time.Instant; -import java.time.temporal.ChronoUnit; -import java.util.HashMap; -import java.util.List; -import java.util.Map; - -/** - * Http发送器 - * - * @author qiushui on 2020-09-10. - */ -@Slf4j -@Getter -public final class HttpClientSender { - - private final HttpClient httpClient; - - private final String method; - - private final String url; - - private final Map headers = new HashMap<>(); - - private final Map parameters = new HashMap<>(); - - private Duration readTimeout = Duration.ofSeconds(10L); - - private String debugLabel; - - private Object requestBody; - - private boolean onlyPrintFailed; - - public HttpClientSender(HttpClient httpClient, String method, String url) { - this.httpClient = httpClient; - this.method = method; - this.url = url; - } - - public HttpClientSender header(String header, String value) { - this.headers.put(header, value); - return this; - } - - public HttpClientSender headers(Map headers) { - if (headers != null) { - this.headers.putAll(headers); - } - return this; - } - - public HttpClientSender headerAuthorization(String value) { - this.headers.put("Authorization", value); - return this; - } - - public HttpClientSender headerContentType(String value) { - this.headers.put("Content-Type", value); - return this; - } - - public HttpClientSender parameter(String parameter, Object value) { - this.parameters.put(parameter, value); - return this; - } - - public HttpClientSender parameters(Map parameters) { - if (parameters != null) { - this.parameters.putAll(parameters); - } - return this; - } - - public HttpClientSender readTimeout(Duration readTimeout) { - this.readTimeout = readTimeout; - return this; - } - - public HttpClientSender debugLabel(String debugLabel) { - this.debugLabel = debugLabel; - return this; - } - - public HttpClientSender onlyPrintFailed(boolean onlyPrintFailed) { - this.onlyPrintFailed = onlyPrintFailed; - return this; - } - - public HttpClientSender bodyJson(String json) { - headers.put("Content-Type", "application/json;charset=utf-8"); - this.requestBody = json; - return this; - } - - public HttpClientSender bodyXml(String xml) { - headers.put("Content-Type", "application/xml;charset=utf-8"); - this.requestBody = xml; - return this; - } - - public HttpClientSender bodyText(String text) { - this.requestBody = text; - return this; - } - - public HttpClientSender bodyMultiPartFormData(MultiPartFormDataBody multiPartFormDataBody) { - headers.put("Content-Type", "multipart/form-data; boundary=" + multiPartFormDataBody.getBoundary()); - this.requestBody = multiPartFormDataBody.buildBodyPublisher(); - return this; - } - - public HttpClientSender bodyFormUrlencoded(FormUrlencodedBody formUrlencodedBody) { - headers.put("Content-Type", "application/x-www-form-urlencoded"); - this.requestBody = formUrlencodedBody.buildBody(); - return this; - } - - public HttpClientSender bodyBytes(byte[] bytes) { - this.requestBody = bytes; - return this; - } - - public void download(Path path, OpenOption... openOptions) { - send(HttpResponse.BodyHandlers::ofByteArray).ifSuccess(r -> r.save(path, openOptions)); - } - - public HttpClientReceiver send() { - return send(new StringBodySenderHandler()); - } - - /** - * 核心发送逻辑 - * - * @param senderHandler 发送器扩展逻辑 - * @param 响应内容 - * @return receiver - */ - public HttpClientReceiver send(SenderHandler senderHandler) { - final HttpRequest.Builder builder = HttpRequest - .newBuilder() - .version(httpClient.version()) - .uri(URI.create(url + StringAdvice.urlParametersFormat(parameters, true))); - headers.forEach(builder::header); - final HttpRequest request = builder - .method(method, requestBody == null ? HttpRequest.BodyPublishers.noBody() : senderHandler.bodyPublisher(requestBody)) - .timeout(readTimeout) - .build(); - final HttpClientReceiver receiver = new HttpClientReceiver<>(); - try { - Instant start = Instant.now(); - HttpResponse response = httpClient.send(request, senderHandler.bodyHandler()); - receiver.setCostTime(start.until(Instant.now(), ChronoUnit.MILLIS)); - receiver.setHttpStatus(response.statusCode()); - receiver.setHeaders(response.headers().map()); - receiver.setBody(response.body()); - } catch (InterruptedException e) { - throw new RuntimeException(e); - } catch (HttpConnectTimeoutException e) { - receiver.setConnectTimeout(true); - } catch (HttpTimeoutException e) { - receiver.setReadTimeout(true); - } catch (IOException e) { - receiver.setErrorMessage(e.getMessage()); - } finally { - if (log.isDebugEnabled() && (!onlyPrintFailed || !receiver.isSuccess())) { - printDebug(request, receiver); - } - } - return receiver; - } - - private String printBody(Object body) { - if (body == null) { - return "(No content)"; - } else if (body instanceof String) { - return (String) body; - } else if (body.getClass().isArray()) { - return "(Bytes data)"; - } else { - return "(Unknown data)"; - } - } - - private void printDebug(HttpRequest request, HttpClientReceiver receiver) { - StringBuilder sb = new StringBuilder("\n=========================================================================================================\n"); - sb - .append("\nlabel: ").append(debugLabel == null ? "(Undefined)" : debugLabel) - .append("\nhttp request:\n method: ").append(method).append("\n url: ") - .append(request.uri().toString()).append("\n headers:\n"); - request - .headers() - .map() - .forEach((k, v) -> sb.append(" ").append(k).append(": ").append(StringUtils.join(v, ";")).append("\n")); - sb.append(" body: ").append(printBody(requestBody)).append("\n").append("\nhttp response:\n"); - if (receiver.isConnectTimeout()) { - sb.append(" (connect timeout ").append(httpClient.connectTimeout().map(Duration::getSeconds).orElse(0L)).append("s)"); - } else if (receiver.isReadTimeout()) { - sb.append(" (read timeout ").append(readTimeout.getSeconds()).append("s)"); - } else if (receiver.getErrorMessage() != null) { - sb.append(" (ioerror ").append(receiver.getErrorMessage()).append(")"); - } else { - sb.append(" status: ").append(receiver.getHttpStatus()).append("\n headers:\n"); - for (Map.Entry> entry : receiver.getHeaders().entrySet()) { - sb.append(" ").append(entry.getKey()).append(": ").append(StringUtils.join(entry.getValue(), ";")).append("\n"); - } - sb.append(" cost: ").append(DateTimeAdvice.millisecondPretty(receiver.getCostTime())).append("\n"); - sb.append(" body: ").append(printBody(receiver.getBody())); - } - sb.append("\n\n=========================================================================================================\n"); - log.debug(sb.toString()); - } -} diff --git a/develop-toolkit-base/src/main/java/develop/toolkit/base/struct/http/HttpContent.java b/develop-toolkit-base/src/main/java/develop/toolkit/base/struct/http/HttpContent.java deleted file mode 100644 index 534bc84..0000000 --- a/develop-toolkit-base/src/main/java/develop/toolkit/base/struct/http/HttpContent.java +++ /dev/null @@ -1,13 +0,0 @@ -package develop.toolkit.base.struct.http; - -/** - * @author qiushui on 2020-09-14. - */ -public enum HttpContent { - - URL, - - HEADERS, - - BODY -} diff --git a/develop-toolkit-base/src/main/java/develop/toolkit/base/struct/http/HttpPostProcessor.java b/develop-toolkit-base/src/main/java/develop/toolkit/base/struct/http/HttpPostProcessor.java new file mode 100644 index 0000000..d6d9bd0 --- /dev/null +++ b/develop-toolkit-base/src/main/java/develop/toolkit/base/struct/http/HttpPostProcessor.java @@ -0,0 +1,14 @@ +package develop.toolkit.base.struct.http; + +import develop.toolkit.base.components.HttpClientSender; + +/** + * Http发送器后置处理 + * + * @author qiushui on 2021-09-16. + */ +@FunctionalInterface +public interface HttpPostProcessor { + + void process(HttpClientSender sender, HttpClientReceiver receiver); +} diff --git a/develop-toolkit-base/src/main/java/develop/toolkit/base/struct/http/HttpRequestBody.java b/develop-toolkit-base/src/main/java/develop/toolkit/base/struct/http/HttpRequestBody.java new file mode 100644 index 0000000..9cb31b2 --- /dev/null +++ b/develop-toolkit-base/src/main/java/develop/toolkit/base/struct/http/HttpRequestBody.java @@ -0,0 +1,13 @@ +package develop.toolkit.base.struct.http; + +/** + * @author qiushui on 2021-09-16. + */ +public interface HttpRequestBody { + + BODY getBody(); + + static String bodyToString(HttpRequestBody requestBody) { + return requestBody == null ? "(No content)" : requestBody.toString(); + } +} diff --git a/develop-toolkit-base/src/main/java/develop/toolkit/base/struct/http/MultiPartFormDataBody.java b/develop-toolkit-base/src/main/java/develop/toolkit/base/struct/http/MultiPartFormDataBody.java index bbd0bd3..e812da9 100644 --- a/develop-toolkit-base/src/main/java/develop/toolkit/base/struct/http/MultiPartFormDataBody.java +++ b/develop-toolkit-base/src/main/java/develop/toolkit/base/struct/http/MultiPartFormDataBody.java @@ -3,35 +3,46 @@ import lombok.Getter; import org.apache.commons.lang3.RandomStringUtils; +import java.io.ByteArrayInputStream; import java.io.IOException; import java.io.InputStream; import java.io.UncheckedIOException; -import java.net.http.HttpRequest; import java.nio.charset.StandardCharsets; import java.nio.file.Files; import java.nio.file.Path; import java.util.ArrayList; import java.util.Iterator; import java.util.List; -import java.util.NoSuchElementException; import java.util.function.Supplier; /** * @author qiushui on 2020-09-14. */ -public class MultiPartFormDataBody { +public final class MultiPartFormDataBody implements HttpRequestBody { private final List partsSpecificationList = new ArrayList<>(); @Getter private final String boundary = RandomStringUtils.randomAlphabetic(10); - public HttpRequest.BodyPublisher buildBodyPublisher() { + @Override + public byte[] getBody() { if (partsSpecificationList.isEmpty()) { - throw new IllegalStateException("Must have at least one part to build multipart message."); + return new byte[0]; } addFinalBoundaryPart(); - return HttpRequest.BodyPublishers.ofByteArrays(PartsIterator::new); + + /* + * 直接使用迭代器获取字节数据会报错 Too few bytes returned by the publisher + * JDK的bug 参考 https://bugs.openjdk.java.net/browse/JDK-8222968 + */ + // return HttpRequest.BodyPublishers.ofByteArrays(PartsIterator::new); + return assemble(); + } + + @Override + public String toString() { + return "(Binary byte data)"; } public MultiPartFormDataBody addPart(String name, String value) { @@ -52,11 +63,13 @@ public MultiPartFormDataBody addPart(String name, Path path) { return this; } - public MultiPartFormDataBody addPart(String name, byte[] bytes) { + public MultiPartFormDataBody addPart(String name, String filename, String contentType, byte[] bytes) { PartsSpecification newPart = new PartsSpecification(); - newPart.type = PartsSpecification.Type.FILE; + newPart.type = PartsSpecification.Type.BYTES; newPart.name = name; newPart.bytes = bytes; + newPart.filename = filename; + newPart.contentType = contentType; partsSpecificationList.add(newPart); return this; } @@ -79,7 +92,7 @@ private void addFinalBoundaryPart() { partsSpecificationList.add(newPart); } - static class PartsSpecification { + private static class PartsSpecification { public enum Type { STRING, FILE, BYTES, STREAM, FINAL_BOUNDARY @@ -96,87 +109,91 @@ public enum Type { } - class PartsIterator implements Iterator { + private class PartsIterator implements Iterator { private final Iterator iterator = partsSpecificationList.iterator(); - private InputStream currentFileInput; - private boolean done; - private byte[] next; + private InputStream currentInputStream; + + private byte[] nextBytes; private static final String NEW_LINE = "\r\n"; @Override public boolean hasNext() { - if (done) return false; - if (next != null) return true; try { - next = computeNext(); + nextBytes = currentInputStream == null ? determineNextPart() : readCurrentInputStream(); + return nextBytes != null; } catch (IOException e) { throw new UncheckedIOException(e); } - if (next == null) { - done = true; - return false; - } - return true; } @Override public byte[] next() { - if (!hasNext()) throw new NoSuchElementException(); - byte[] res = next; - next = null; - return res; + byte[] result = nextBytes; + nextBytes = null; + return result; } - private byte[] computeNext() throws IOException { - if (currentFileInput == null) { - if (!iterator.hasNext()) return null; - PartsSpecification nextPart = iterator.next(); - String filename = null; - String contentType = "application/octet-stream"; - switch (nextPart.type) { - case STRING: { - return headerBytes(nextPart.name, nextPart.value, null, "text/plain; charset=UTF-8"); - } - case FINAL_BOUNDARY: { - return nextPart.value.getBytes(StandardCharsets.UTF_8); - } - case BYTES: { - return nextPart.bytes; - } - case FILE: { - Path path = nextPart.path; - filename = path.getFileName().toString(); - contentType = Files.probeContentType(path); - currentFileInput = Files.newInputStream(path); - } - break; - case STREAM: { - filename = nextPart.filename; - contentType = nextPart.contentType; - currentFileInput = nextPart.stream.get(); - } - break; + /** + * 决定下一个Part + */ + private byte[] determineNextPart() throws IOException { + if (!iterator.hasNext()) return null; + final PartsSpecification nextPart = iterator.next(); + switch (nextPart.type) { + case FINAL_BOUNDARY: { + return nextPart.value.getBytes(StandardCharsets.UTF_8); } - return headerBytes(nextPart.name, null, filename, contentType); - } else { - byte[] buf = new byte[8192]; - int r = currentFileInput.read(buf); - if (r > 0) { - byte[] actualBytes = new byte[r]; - System.arraycopy(buf, 0, actualBytes, 0, r); - return actualBytes; - } else { - currentFileInput.close(); - currentFileInput = null; - return NEW_LINE.getBytes(); + case STRING: { + currentInputStream = new ByteArrayInputStream((nextPart.value).getBytes(StandardCharsets.UTF_8)); + return headerBytes(nextPart.name, null, "text/plain; charset=UTF-8"); + } + case BYTES: { + currentInputStream = new ByteArrayInputStream(nextPart.bytes); + return headerBytes( + nextPart.name, + nextPart.filename, + nextPart.contentType + ); + } + case FILE: { + currentInputStream = Files.newInputStream(nextPart.path); + return headerBytes( + nextPart.name, + nextPart.path.getFileName().toString(), + Files.probeContentType(nextPart.path) + ); } + case STREAM: { + currentInputStream = nextPart.stream.get(); + return headerBytes( + nextPart.name, + nextPart.filename, + nextPart.contentType + ); + } + default: + throw new AssertionError(); + } + } + + private byte[] readCurrentInputStream() throws IOException { + byte[] buffer = new byte[8192]; + int r = currentInputStream.read(buffer); + if (r > 0) { + byte[] actualBytes = new byte[r]; + System.arraycopy(buffer, 0, actualBytes, 0, r); + return actualBytes; + } else { + currentInputStream.close(); + currentInputStream = null; + return NEW_LINE.getBytes(); } } - private byte[] headerBytes(String name, String value, String filename, String contentType) { + private byte[] headerBytes(String name, String filename, String contentType) { StringBuilder sb = new StringBuilder("--") .append(boundary).append(NEW_LINE) .append("Content-Disposition: form-data; name=").append(name); @@ -184,10 +201,24 @@ private byte[] headerBytes(String name, String value, String filename, String co sb.append("; filename=").append(filename); } sb.append(NEW_LINE).append("Content-Type: ").append(contentType).append(NEW_LINE).append(NEW_LINE); - if (value != null) { - sb.append(value).append(NEW_LINE); - } return sb.toString().getBytes(StandardCharsets.UTF_8); } } + + private byte[] assemble() { + // 使用以下方法 自己拼装byte[] + int length = 0, pos = 0; + PartsIterator iteratorForCount = new PartsIterator(); + while (iteratorForCount.hasNext()) { + length += iteratorForCount.next().length; + } + byte[] data = new byte[length]; + PartsIterator iteratorForBytes = new PartsIterator(); + while (iteratorForBytes.hasNext()) { + final byte[] nextBytes = iteratorForBytes.next(); + System.arraycopy(nextBytes, 0, data, pos, nextBytes.length); + pos += nextBytes.length; + } + return data; + } } diff --git a/develop-toolkit-base/src/main/java/develop/toolkit/base/struct/http/PrintLogHttpPostProcessor.java b/develop-toolkit-base/src/main/java/develop/toolkit/base/struct/http/PrintLogHttpPostProcessor.java new file mode 100644 index 0000000..5f05c3f --- /dev/null +++ b/develop-toolkit-base/src/main/java/develop/toolkit/base/struct/http/PrintLogHttpPostProcessor.java @@ -0,0 +1,63 @@ +package develop.toolkit.base.struct.http; + +import develop.toolkit.base.components.HttpClientSender; +import develop.toolkit.base.utils.DateTimeAdvice; +import develop.toolkit.base.utils.K; +import lombok.extern.slf4j.Slf4j; +import org.apache.commons.lang3.StringUtils; + +import java.time.Duration; +import java.util.List; +import java.util.Map; + +/** + * @author qiushui on 2021-09-16. + */ +@Slf4j +public final class PrintLogHttpPostProcessor implements HttpPostProcessor { + + @Override + public void process(HttpClientSender sender, HttpClientReceiver receiver) { + if (log.isDebugEnabled() && (!sender.isOnlyPrintFailed() || !receiver.isSuccess())) { + debugPrintLog(sender, receiver); + } + } + + private void debugPrintLog(HttpClientSender sender, HttpClientReceiver receiver) { + StringBuilder sb = new StringBuilder("\n=========================================================================================================\n"); + sb + .append("\nlabel: ").append(K.def(sender.getDebugLabel(), "(Undefined)")) + .append("\nhttp request:\n method: ").append(sender.getMethod()).append("\n url: ") + .append(sender.getUri().toString()).append("\n headers:\n"); + sender + .getHeaders() + .forEach((k, v) -> sb.append(" ").append(k).append(": ").append(StringUtils.join(v, ";")).append("\n")); + sb.append(" body: ").append(sender.getRequestStringBody()).append("\n").append("\nhttp response:\n"); + if (receiver.isConnectTimeout()) { + sb.append(" (connect timeout ").append(sender.getHttpClient().connectTimeout().map(Duration::getSeconds).orElse(0L)).append("s)"); + } else if (receiver.isReadTimeout()) { + sb.append(" (read timeout ").append(sender.getReadTimeout().getSeconds()).append("s)"); + } else if (receiver.getErrorMessage() != null) { + sb.append(" (ioerror ").append(receiver.getErrorMessage()).append(")"); + } else if (receiver.getHeaders() != null) { + sb.append(" status: ").append(receiver.getHttpStatus()).append("\n headers:\n"); + for (Map.Entry> entry : receiver.getHeaders().entrySet()) { + sb.append(" ").append(entry.getKey()).append(": ").append(StringUtils.join(entry.getValue(), ";")).append("\n"); + } + sb.append(" cost: ").append(DateTimeAdvice.millisecondPretty(receiver.getCostTime())).append("\n"); + sb.append(" body: ").append(bodyToString(receiver.getBody())); + } + sb.append("\n\n=========================================================================================================\n"); + log.debug(sb.toString()); + } + + private String bodyToString(Object body) { + if (body == null) { + return "(No content)"; + } else if (body instanceof String) { + return (String) body; + } else { + return "(Binary byte data)"; + } + } +} diff --git a/develop-toolkit-base/src/main/java/develop/toolkit/base/struct/http/RawRequestBody.java b/develop-toolkit-base/src/main/java/develop/toolkit/base/struct/http/RawRequestBody.java new file mode 100644 index 0000000..8718fd4 --- /dev/null +++ b/develop-toolkit-base/src/main/java/develop/toolkit/base/struct/http/RawRequestBody.java @@ -0,0 +1,22 @@ +package develop.toolkit.base.struct.http; + +import lombok.RequiredArgsConstructor; + +/** + * @author qiushui on 2021-09-16. + */ +@RequiredArgsConstructor +public class RawRequestBody implements HttpRequestBody { + + private final String raw; + + @Override + public String toString() { + return raw; + } + + @Override + public String getBody() { + return raw; + } +} diff --git a/develop-toolkit-base/src/main/java/develop/toolkit/base/struct/http/SenderHandler.java b/develop-toolkit-base/src/main/java/develop/toolkit/base/struct/http/SenderHandler.java index 8a03516..848d41c 100644 --- a/develop-toolkit-base/src/main/java/develop/toolkit/base/struct/http/SenderHandler.java +++ b/develop-toolkit-base/src/main/java/develop/toolkit/base/struct/http/SenderHandler.java @@ -11,13 +11,17 @@ @FunctionalInterface public interface SenderHandler { - default HttpRequest.BodyPublisher bodyPublisher(Object requestBody) { - if (requestBody instanceof HttpRequest.BodyPublisher) { - return (HttpRequest.BodyPublisher) requestBody; - } else if (requestBody instanceof String) { - return HttpRequest.BodyPublishers.ofString((String) requestBody); - } else if (requestBody.getClass().isArray()) { - return HttpRequest.BodyPublishers.ofByteArray((byte[]) requestBody); + default HttpRequest.BodyPublisher bodyPublisher(HttpRequestBody requestBody) { + if (requestBody == null) { + return HttpRequest.BodyPublishers.noBody(); + } else if (requestBody instanceof RawRequestBody) { + return HttpRequest.BodyPublishers.ofString(((RawRequestBody) requestBody).getBody()); + } else if (requestBody instanceof FormUrlencodedBody) { + return HttpRequest.BodyPublishers.ofString(((FormUrlencodedBody) requestBody).getBody()); + } else if (requestBody instanceof ByteRequestBody) { + return HttpRequest.BodyPublishers.ofByteArray(((ByteRequestBody) requestBody).getBody()); + } else if (requestBody instanceof MultiPartFormDataBody) { + return HttpRequest.BodyPublishers.ofByteArray(((MultiPartFormDataBody) requestBody).getBody()); } else { throw new AssertionError(); } diff --git a/develop-toolkit-base/src/main/java/develop/toolkit/base/struct/http/StringBodySenderHandler.java b/develop-toolkit-base/src/main/java/develop/toolkit/base/struct/http/StringBodySenderHandler.java index f4f74c0..9e6a916 100644 --- a/develop-toolkit-base/src/main/java/develop/toolkit/base/struct/http/StringBodySenderHandler.java +++ b/develop-toolkit-base/src/main/java/develop/toolkit/base/struct/http/StringBodySenderHandler.java @@ -5,7 +5,7 @@ /** * @author qiushui on 2020-09-11. */ -public class StringBodySenderHandler implements SenderHandler { +public final class StringBodySenderHandler implements SenderHandler { @Override public HttpResponse.BodyHandler bodyHandler() { diff --git a/develop-toolkit-base/src/main/java/develop/toolkit/base/utils/ArrayAdvice.java b/develop-toolkit-base/src/main/java/develop/toolkit/base/utils/ArrayAdvice.java index 7cdbbc3..e549526 100644 --- a/develop-toolkit-base/src/main/java/develop/toolkit/base/utils/ArrayAdvice.java +++ b/develop-toolkit-base/src/main/java/develop/toolkit/base/utils/ArrayAdvice.java @@ -8,8 +8,8 @@ import java.lang.reflect.Array; import java.util.*; +import java.util.function.BiFunction; import java.util.function.BiPredicate; -import java.util.function.Consumer; import java.util.function.Function; import java.util.function.Predicate; import java.util.stream.Collectors; @@ -210,6 +210,14 @@ public static Map groupingUniqueKey(V[] array, Function keySu return map; } + public static Map groupingUniqueKey(E[] array, Function keySupplier, Function valueSupplier) { + Map map = new HashMap<>(); + for (E item : array) { + map.put(keySupplier.apply(item), valueSupplier.apply(item)); + } + return map; + } + /** * 分组求数量 */ @@ -326,7 +334,10 @@ public static TwoValues, List> partition(E[] collection, Predicat notMatch.add(e); } } - return TwoValues.of(match, notMatch); + return TwoValues.of( + Collections.unmodifiableList(match), + Collections.unmodifiableList(notMatch) + ); } /** @@ -334,11 +345,12 @@ public static TwoValues, List> partition(E[] collection, Predicat * 将两个集合的元素按索引捆绑到一起 */ public static List> zip(T[] master, S[] other) { - if (master.length != other.length) { + final int length = master.length; + if (length != other.length) { throw new IllegalArgumentException("list size must be same"); } - List> list = new LinkedList<>(); - for (int i = 0; i < master.length; i++) { + List> list = new ArrayList<>(length); + for (int i = 0; i < length; i++) { list.add(TwoValues.of(master[i], other[i])); } return list; @@ -347,15 +359,37 @@ public static List> zip(T[] master, S[] other) { /** * 分页处理 */ - public static void pagingProcess(E[] array, int size, Consumer consumer) { + public static void pagingProcess(T[] array, int size, PagingProcessor consumer) { + final int total = array.length; + final int page = total % size == 0 ? (total / size) : (total / size + 1); + for (int i = 0; i < page; i++) { + int fromIndex = i * size; + int toIndex = fromIndex + Math.min(total - fromIndex, size); + T[] subArray = ArrayUtils.subarray(array, fromIndex, toIndex); + consumer.process(i, page, subArray); + } + } + + /** + * 分页处理 + */ + public static R pagingProcess( + T[] array, + int size, + R initialValue, + BiFunction reduceFunction, + PagingReduceProcessor consumer + ) { final int total = array.length; final int page = total % size == 0 ? (total / size) : (total / size + 1); for (int i = 0; i < page; i++) { int fromIndex = i * size; int toIndex = fromIndex + Math.min(total - fromIndex, size); - E[] subArray = ArrayUtils.subarray(array, fromIndex, toIndex); - consumer.accept(subArray); + T[] subArray = ArrayUtils.subarray(array, fromIndex, toIndex); + final R r = consumer.process(i, page, subArray); + initialValue = reduceFunction.apply(initialValue, r); } + return initialValue; } /** @@ -375,4 +409,16 @@ public static List sort(T[] master, Collection sortTarget, BiPredic .map(s -> ArrayAdvice.getFirstTrue(master, c -> predicate.test(c, s)).orElse(null)) .collect(Collectors.toList()); } + + @FunctionalInterface + public interface PagingProcessor { + + void process(int page, int total, T[] subArray); + } + + @FunctionalInterface + public interface PagingReduceProcessor { + + R process(int page, int total, T[] subArray); + } } diff --git a/develop-toolkit-base/src/main/java/develop/toolkit/base/utils/CollectionAdvice.java b/develop-toolkit-base/src/main/java/develop/toolkit/base/utils/CollectionAdvice.java index 254521f..b465a3d 100644 --- a/develop-toolkit-base/src/main/java/develop/toolkit/base/utils/CollectionAdvice.java +++ b/develop-toolkit-base/src/main/java/develop/toolkit/base/utils/CollectionAdvice.java @@ -16,7 +16,17 @@ * @author qiushui on 2018-12-20. */ @SuppressWarnings("unused") -public final class CollectionAdvice { +public abstract class CollectionAdvice { + + /** + * 获得元素 + */ + public static Optional get(List list, int index) { + return Optional + .ofNullable(list) + .filter(Predicate.not(List::isEmpty)) + .map(c -> c.get(index)); + } /** * 检查元素存在 @@ -200,6 +210,12 @@ public static Map groupingUniqueKey(Collection collection, Funct return map; } + public static Map groupingUniqueKey(Collection collection, Function keySupplier, Function valueSupplier) { + Map map = new HashMap<>(); + collection.forEach(item -> map.put(keySupplier.apply(item), valueSupplier.apply(item))); + return map; + } + /** * 分组求数量 */ @@ -250,8 +266,8 @@ public static Set difference(Collection master, Collection other) { * 合并多集合 */ @SafeVarargs - public static Collection merge(Supplier> supplier, Collection... collections) { - Collection collection = supplier.get(); + public static , E> T merge(Supplier supplier, Collection... collections) { + T collection = supplier.get(); for (Collection coll : collections) { if (coll != null) { collection.addAll(coll); @@ -303,7 +319,10 @@ public static TwoValues, List> partition(Collection collection notMatch.add(e); } } - return TwoValues.of(match, notMatch); + return TwoValues.of( + Collections.unmodifiableList(match), + Collections.unmodifiableList(notMatch) + ); } /** @@ -324,17 +343,61 @@ public static List> zip(List master, List other) { /** * 分页处理 */ - public static void pagingProcess(List list, int size, Consumer> consumer) { + public static void pagingProcess(List list, int size, PagingProcessor processor) { final int total = list.size(); final int page = total % size == 0 ? (total / size) : (total / size + 1); for (int i = 0; i < page; i++) { int fromIndex = i * size; int toIndex = fromIndex + Math.min(total - fromIndex, size); List subList = list.subList(fromIndex, toIndex); - consumer.accept(subList); + processor.process(i, page, subList); + } + } + + /** + * 分页处理 (通过总数) + */ + public static void pagingProcess(int total, int size, BiConsumer consumer) { + final int page = total % size == 0 ? (total / size) : (total / size + 1); + for (int i = 0; i < page; i++) { + consumer.accept(i, page); } } + /** + * 分页处理 (含返回值) + */ + public static R pagingProcess( + List list, + int size, + R initialValue, + BiFunction reduceFunction, + PagingReduceProcessor processor + ) { + final int total = list.size(); + final int page = total % size == 0 ? (total / size) : (total / size + 1); + for (int i = 0; i < page; i++) { + int fromIndex = i * size; + int toIndex = fromIndex + Math.min(total - fromIndex, size); + List subList = list.subList(fromIndex, toIndex); + final R r = processor.process(i, page, subList); + initialValue = reduceFunction.apply(initialValue, r); + } + return initialValue; + } + + /** + * 分页处理 (通过总数 含返回值) + */ + public static R pagingProcess(int total, int size, R initialValue, BiFunction reduceFunction, BiFunction function) { + final int page = total % size == 0 ? (total / size) : (total / size + 1); + for (int i = 0; i < page; i++) { + final R r = function.apply(i, page); + initialValue = reduceFunction.apply(initialValue, r); + } + return initialValue; + } + /** * 指定排序 * 把master的元素值按sortTarget的元素值排序,条件按predicate @@ -352,4 +415,16 @@ public static List sort(Collection master, S[] sortTarget, BiPredic .map(s -> CollectionAdvice.getFirstTrue(master, c -> predicate.test(c, s)).orElse(null)) .collect(Collectors.toList()); } + + @FunctionalInterface + public interface PagingProcessor { + + void process(int page, int total, List subList); + } + + @FunctionalInterface + public interface PagingReduceProcessor { + + R process(int page, int total, List subList); + } } diff --git a/develop-toolkit-base/src/main/java/develop/toolkit/base/utils/CompareAdvice.java b/develop-toolkit-base/src/main/java/develop/toolkit/base/utils/CompareAdvice.java index 4770692..f08a2cd 100644 --- a/develop-toolkit-base/src/main/java/develop/toolkit/base/utils/CompareAdvice.java +++ b/develop-toolkit-base/src/main/java/develop/toolkit/base/utils/CompareAdvice.java @@ -86,4 +86,19 @@ public static > T max(@NonNull T a, @NonNull T b) { public static > T min(@NonNull T a, @NonNull T b) { return lte(a, b) ? a : b; } + + /** + * 调整边界值 + */ + public static > T adjustRange(T x, T start, T end) { + if (gt(start, end)) { + throw new IllegalArgumentException("start great than end"); + } else if (lt(x, start)) { + return start; + } else if (gt(x, end)) { + return end; + } else { + return x; + } + } } diff --git a/develop-toolkit-base/src/main/java/develop/toolkit/base/utils/CompressAdvice.java b/develop-toolkit-base/src/main/java/develop/toolkit/base/utils/CompressAdvice.java new file mode 100644 index 0000000..5e097ee --- /dev/null +++ b/develop-toolkit-base/src/main/java/develop/toolkit/base/utils/CompressAdvice.java @@ -0,0 +1,159 @@ +package develop.toolkit.base.utils; + +import develop.toolkit.base.struct.ZipWrapper; + +import java.io.*; +import java.nio.file.FileVisitResult; +import java.nio.file.FileVisitor; +import java.nio.file.Files; +import java.nio.file.Path; +import java.nio.file.attribute.BasicFileAttributes; +import java.nio.file.attribute.FileTime; +import java.util.List; +import java.util.zip.GZIPInputStream; +import java.util.zip.GZIPOutputStream; +import java.util.zip.ZipEntry; +import java.util.zip.ZipOutputStream; + +/** + * 压缩增强 + * + * @author qiushui on 2022-04-17. + */ +public abstract class CompressAdvice { + + public static class GZip { + + public static void compress(InputStream inputStream, OutputStream outputStream) throws IOException { + try (GZIPOutputStream gzipOutputStream = new GZIPOutputStream(outputStream)) { + inputStream.transferTo(gzipOutputStream); + } + } + + public static void compress(byte[] data, OutputStream outputStream) throws IOException { + try (ByteArrayInputStream bais = new ByteArrayInputStream(data)) { + compress(bais, outputStream); + } + } + + public static byte[] compress(InputStream inputStream) throws IOException { + try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + compress(inputStream, baos); + return baos.toByteArray(); + } + } + + public static byte[] compress(byte[] data) throws IOException { + try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + compress(data, baos); + return baos.toByteArray(); + } + } + + public static void uncompress(InputStream inputStream, OutputStream outputStream) throws IOException { + try (GZIPInputStream gzipInputStream = new GZIPInputStream(inputStream)) { + gzipInputStream.transferTo(outputStream); + } + } + + public static void uncompress(byte[] data, OutputStream outputStream) throws IOException { + try (ByteArrayInputStream bais = new ByteArrayInputStream(data)) { + uncompress(bais, outputStream); + } + } + + public static byte[] uncompress(InputStream inputStream) throws IOException { + try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + uncompress(inputStream, baos); + return baos.toByteArray(); + } + } + + public static byte[] uncompress(byte[] data) throws IOException { + try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + uncompress(data, baos); + return baos.toByteArray(); + } + } + } + + public static class Zip { + + public static void compress(Path path, OutputStream outputStream) throws IOException { + try (ZipOutputStream zos = new ZipOutputStream(outputStream)) { + Files.walkFileTree(path, new FileVisitor<>() { + + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFile(Path filePath, BasicFileAttributes attrs) throws IOException { + String currentName = filePath.subpath(path.getNameCount(), filePath.getNameCount()).toString(); + File file = filePath.toFile(); + final ZipEntry zipEntry = new ZipEntry(currentName); + zipEntry.setMethod(currentName.endsWith(".zip") ? ZipEntry.STORED : ZipEntry.DEFLATED); + zipEntry.setLastModifiedTime(FileTime.fromMillis(file.lastModified())); + zos.putNextEntry(zipEntry); + try (InputStream is = new FileInputStream(file)) { + is.transferTo(zos); + } + zos.closeEntry(); + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException { + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { + return FileVisitResult.CONTINUE; + } + }); + } + } + + public static byte[] compress(Path path) throws IOException { + try (ByteArrayOutputStream baos = new ByteArrayOutputStream()) { + compress(path, baos); + return baos.toByteArray(); + } + } + + public static void compress(Path path, Path outPath) throws IOException { + try (OutputStream os = new FileOutputStream(outPath.toFile())) { + compress(path, os); + } + } + + + public static void compress(ZipWrapper zipWrapper, OutputStream outputStream) throws IOException { + try (ZipOutputStream zos = new ZipOutputStream(outputStream)) { + recursiveCompress(zipWrapper, "", zos); + } + } + + private static void recursiveCompress(ZipWrapper zipWrapper, String parentPath, ZipOutputStream zos) throws IOException { + final String currentName = parentPath + zipWrapper.getFilename(); + if (zipWrapper.isFile()) { + final ZipEntry zipEntry = new ZipEntry(currentName); + zipWrapper.configureZipEntry(zipEntry); + zos.putNextEntry(zipEntry); + try (InputStream is = zipWrapper.getInputStreamSupplier().get()) { + is.transferTo(zos); + } + zos.closeEntry(); + } else { + List children = zipWrapper.getChildren(); + if (children != null) { + for (ZipWrapper childZipWrapper : children) { + recursiveCompress(childZipWrapper, currentName + File.separator, zos); + } + } + } + } + } +} diff --git a/develop-toolkit-base/src/main/java/develop/toolkit/base/utils/CryptAdvice.java b/develop-toolkit-base/src/main/java/develop/toolkit/base/utils/CryptAdvice.java new file mode 100644 index 0000000..072251e --- /dev/null +++ b/develop-toolkit-base/src/main/java/develop/toolkit/base/utils/CryptAdvice.java @@ -0,0 +1,323 @@ +package develop.toolkit.base.utils; + +import develop.toolkit.base.exception.CryptException; +import develop.toolkit.base.struct.TwoValues; +import lombok.AccessLevel; +import lombok.Getter; +import lombok.RequiredArgsConstructor; +import lombok.SneakyThrows; +import org.apache.commons.lang3.ArrayUtils; + +import javax.crypto.Cipher; +import javax.crypto.KeyGenerator; +import javax.crypto.SecretKey; +import javax.crypto.SecretKeyFactory; +import javax.crypto.spec.DESKeySpec; +import javax.crypto.spec.IvParameterSpec; +import javax.crypto.spec.PBEKeySpec; +import javax.crypto.spec.SecretKeySpec; +import java.nio.charset.StandardCharsets; +import java.security.*; +import java.security.interfaces.RSAPublicKey; +import java.security.spec.InvalidKeySpecException; +import java.security.spec.KeySpec; +import java.security.spec.PKCS8EncodedKeySpec; +import java.security.spec.X509EncodedKeySpec; +import java.util.Base64; + +/** + * 加密解密增强 + * + * @author qiushui on 2021-04-23. + */ +public abstract class CryptAdvice { + + /** + * DES算法 + */ + public static class DES { + + /** + * 加密 + * + * @param original 原文 + * @param secretKey 密钥 + * @return 密文 + */ + public static String encrypt(String original, String secretKey) { + try { + final Cipher cipher = initCipher(secretKey, Cipher.ENCRYPT_MODE); + final byte[] data = cipher.doFinal(original.getBytes(StandardCharsets.UTF_8)); + return Base64.getEncoder().encodeToString(data); + } catch (Exception e) { + throw new CryptException(e); + } + } + + /** + * 解密 + * + * @param ciphertext 密文 + * @param secretKey 密钥 + * @return 原文 + */ + public static String decrypt(String ciphertext, String secretKey) throws CryptException { + try { + final byte[] data = Base64.getDecoder().decode(ciphertext); + final Cipher cipher = initCipher(secretKey, Cipher.DECRYPT_MODE); + return new String(cipher.doFinal(data), StandardCharsets.UTF_8); + } catch (Exception e) { + throw new CryptException(e); + } + } + + private static Cipher initCipher(String secretKey, int mode) throws Exception { + final DESKeySpec dks = new DESKeySpec(secretKey.getBytes(StandardCharsets.UTF_8)); + final SecretKey secureKey = SecretKeyFactory.getInstance("DES").generateSecret(dks); + final Cipher cipher = Cipher.getInstance("DES/ECB/PKCS5Padding"); + cipher.init(mode, secureKey, new SecureRandom()); + return cipher; + } + } + + public static class AES { + + private static final String ALGORITHM = "AES"; + private static final String SECRET_KEY_ALGORITHM = "PBKDF2WithHmacSHA256"; + + /** + * 密钥长度枚举 + */ + @RequiredArgsConstructor(access = AccessLevel.PRIVATE) + public enum KeyLength { + + KEY_LENGTH_128(128), + KEY_LENGTH_192(192), + KEY_LENGTH_256(256); + + @Getter + private final int length; + } + + /** + * 创建密钥和iv + * + * @param keyLength 密钥长度 + * @return 密钥 + */ + @SneakyThrows(NoSuchAlgorithmException.class) + public static TwoValues createSecretKeyAndIv(KeyLength keyLength) { + final KeyGenerator keyGenerator = KeyGenerator.getInstance(ALGORITHM); + keyGenerator.init(keyLength.getLength()); + final byte[] iv = new byte[16]; + new SecureRandom().nextBytes(iv); + final Base64.Encoder encoder = Base64.getEncoder(); + return TwoValues.of( + encoder.encodeToString(keyGenerator.generateKey().getEncoded()), + encoder.encodeToString(iv) + ); + } + + /** + * 根据密码创建密钥和iv + * + * @param keyLength 密钥长度 + * @param password 密码 + * @param salt 盐 + * @return 密钥 + */ + @SneakyThrows({NoSuchAlgorithmException.class, InvalidKeySpecException.class}) + public static TwoValues createSecretKeyAndIvByPassword(KeyLength keyLength, String password, String salt) { + final SecretKeyFactory factory = SecretKeyFactory.getInstance(SECRET_KEY_ALGORITHM); + final KeySpec spec = new PBEKeySpec(password.toCharArray(), salt.getBytes(), 65536, keyLength.getLength()); + final SecretKeySpec secretKeySpec = new SecretKeySpec(factory.generateSecret(spec).getEncoded(), ALGORITHM); + final byte[] iv = new byte[16]; + new SecureRandom().nextBytes(iv); + final Base64.Encoder encoder = Base64.getEncoder(); + return TwoValues.of( + encoder.encodeToString(secretKeySpec.getEncoded()), + encoder.encodeToString(iv) + ); + } + + /** + * 加密 + * + * @param original 原文 + * @param secretKeyBase64 base64密钥 + * @param ivBase64 base64 iv + * @return 密文 + * @throws CryptException + */ + public static String encrypt(String original, String secretKeyBase64, String ivBase64) throws CryptException { + try { + final Base64.Decoder decoder = Base64.getDecoder(); + final byte[] secretKey = decoder.decode(secretKeyBase64); + final byte[] iv = decoder.decode(ivBase64); + final SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey, SECRET_KEY_ALGORITHM); + final Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + cipher.init(Cipher.ENCRYPT_MODE, secretKeySpec, new IvParameterSpec(iv)); + final byte[] cipherText = cipher.doFinal(original.getBytes(StandardCharsets.UTF_8)); + return Base64.getEncoder().encodeToString(cipherText); + } catch (Exception e) { + throw new CryptException(e); + } + } + + /** + * 解密 + * + * @param cipherText 密文 + * @param secretKeyBase64 base64密钥 + * @param ivBase64 base64 iv + * @return 原文 + * @throws CryptException + */ + public static String decrypt(String cipherText, String secretKeyBase64, String ivBase64) throws CryptException { + try { + final Base64.Decoder decoder = Base64.getDecoder(); + final byte[] secretKey = decoder.decode(secretKeyBase64); + final byte[] iv = decoder.decode(ivBase64); + final SecretKeySpec secretKeySpec = new SecretKeySpec(secretKey, SECRET_KEY_ALGORITHM); + final Cipher cipher = Cipher.getInstance("AES/CBC/PKCS5Padding"); + cipher.init(Cipher.DECRYPT_MODE, secretKeySpec, new IvParameterSpec(iv)); + final byte[] plainText = cipher.doFinal(Base64.getDecoder().decode(cipherText)); + return new String(plainText, StandardCharsets.UTF_8); + } catch (Exception e) { + throw new CryptException(e); + } + } + } + + /** + * RSA算法 + */ + public static class RSA { + + private static final String KEY_ALGORITHM = "RSA"; + private static final int KEY_SIZE = 1024; + private static final String SIGNATURE_ALGORITHM = "Sha1WithRSA"; + + /** + * 生成公钥和私钥对 + * + * @return 公钥 私钥 + */ + @SneakyThrows(NoSuchAlgorithmException.class) + public static TwoValues createRSAKeys() { + final KeyPairGenerator keyPairGenerator = KeyPairGenerator.getInstance(KEY_ALGORITHM); + keyPairGenerator.initialize(KEY_SIZE, new SecureRandom()); + final KeyPair keyPair = keyPairGenerator.generateKeyPair(); + final PublicKey publicKey = keyPair.getPublic(); + final PrivateKey privateKey = keyPair.getPrivate(); + final Base64.Encoder encoder = Base64.getEncoder(); + return TwoValues.of( + encoder.encodeToString(publicKey.getEncoded()), + encoder.encodeToString(privateKey.getEncoded()) + ); + } + + /** + * RSA公钥加密 + * + * @param original 原文 + * @param publicKeyBase64 base64公钥 + * @return 密文 + */ + public static String encrypt(String original, String publicKeyBase64) throws CryptException { + try { + final byte[] publicKeyBytes = Base64.getDecoder().decode(publicKeyBase64); + RSAPublicKey pubKey = (RSAPublicKey) KeyFactory + .getInstance(KEY_ALGORITHM) + .generatePublic(new X509EncodedKeySpec(publicKeyBytes)); + Cipher cipher = Cipher.getInstance(KEY_ALGORITHM); + cipher.init(Cipher.ENCRYPT_MODE, pubKey); + final byte[] bytes = original.getBytes(StandardCharsets.UTF_8); + final int offset = 64; + byte[] enBytes = null; + for (int i = 0; i < bytes.length; i += offset) { + byte[] doFinal = cipher.doFinal(ArrayUtils.subarray(bytes, i, i + 64)); + enBytes = ArrayUtils.addAll(enBytes, doFinal); + } + return Base64.getEncoder().encodeToString(enBytes); + } catch (Exception e) { + throw new CryptException(e); + } + } + + /** + * RSA私钥解密 + * + * @param ciphertext 密文 + * @param privateKeyBase64 base64私钥 + * @return 明文 + */ + public static String decrypt(String ciphertext, String privateKeyBase64) throws CryptException { + try { + final Base64.Decoder decoder = Base64.getDecoder(); + final byte[] privateKeyBytes = decoder.decode(privateKeyBase64); + PrivateKey privateKey = KeyFactory + .getInstance(KEY_ALGORITHM) + .generatePrivate(new PKCS8EncodedKeySpec(privateKeyBytes)); + Cipher cipher = Cipher.getInstance(KEY_ALGORITHM); + cipher.init(Cipher.DECRYPT_MODE, privateKey); + byte[] inputBytes = decoder.decode(ciphertext.getBytes(StandardCharsets.UTF_8)); + StringBuilder sb = new StringBuilder(); + final int offset = 128; + for (int i = 0; i < inputBytes.length; i += offset) { + byte[] doFinal = cipher.doFinal(ArrayUtils.subarray(inputBytes, i, i + offset)); + sb.append(new String(doFinal)); + } + return sb.toString(); + } catch (Exception e) { + throw new CryptException(e); + } + } + + /** + * 生成base64签名结果 + * + * @param data 数据 + * @param privateKeyBase64 base64私钥 + * @return base64签名 + */ + public static String signature(byte[] data, String privateKeyBase64) { + try { + final byte[] privateKeyBytes = Base64.getDecoder().decode(privateKeyBase64); + Signature sign = Signature.getInstance(SIGNATURE_ALGORITHM); + PrivateKey privateKey = KeyFactory + .getInstance(KEY_ALGORITHM) + .generatePrivate(new PKCS8EncodedKeySpec(privateKeyBytes)); + sign.initSign(privateKey); + sign.update(data); + return Base64.getEncoder().encodeToString(sign.sign()); + } catch (Exception e) { + throw new CryptException(e); + } + } + + /** + * 用公钥验证签名 + * + * @param data 数据 + * @param signatureBase64 base64签名字符串 + * @param publicKeyBase64 base64公钥 + */ + public static boolean verifySignature(byte[] data, String signatureBase64, String publicKeyBase64) { + try { + final Base64.Decoder decoder = Base64.getDecoder(); + final byte[] publicKeyBytes = decoder.decode(publicKeyBase64); + final byte[] signatureBytes = decoder.decode(signatureBase64); + RSAPublicKey publicKey = (RSAPublicKey) KeyFactory + .getInstance(KEY_ALGORITHM) + .generatePublic(new X509EncodedKeySpec(publicKeyBytes)); + Signature sign = Signature.getInstance(SIGNATURE_ALGORITHM); + sign.initVerify(publicKey); + sign.update(data); + return sign.verify(signatureBytes); + } catch (Exception e) { + return false; + } + } + } +} diff --git a/develop-toolkit-base/src/main/java/develop/toolkit/base/utils/FileAdvice.java b/develop-toolkit-base/src/main/java/develop/toolkit/base/utils/FileAdvice.java index 6cf2f9f..6938d20 100644 --- a/develop-toolkit-base/src/main/java/develop/toolkit/base/utils/FileAdvice.java +++ b/develop-toolkit-base/src/main/java/develop/toolkit/base/utils/FileAdvice.java @@ -1,13 +1,12 @@ package develop.toolkit.base.utils; -import lombok.SneakyThrows; - import java.io.*; -import java.nio.charset.StandardCharsets; +import java.nio.charset.Charset; +import java.nio.file.*; +import java.nio.file.attribute.BasicFileAttributes; import java.util.Collections; import java.util.LinkedList; import java.util.List; -import java.util.Stack; import java.util.function.Predicate; /** @@ -15,65 +14,103 @@ */ public abstract class FileAdvice { - /** - * 打开缓冲写 - */ - public static BufferedWriter open(File file, boolean append) throws IOException { - return new BufferedWriter( - new OutputStreamWriter( - new FileOutputStream(file, append), - StandardCharsets.UTF_8 - ) - ); + public static void write(Path filePath, CharSequence text, Charset charset, boolean append) { + try { + touch(filePath); + Files.writeString( + filePath, + text, + charset, + StandardOpenOption.WRITE, + append ? StandardOpenOption.APPEND : StandardOpenOption.TRUNCATE_EXISTING + ); + } catch (IOException e) { + throw new UncheckedIOException(e); + } } - @SneakyThrows(IOException.class) - public static void write(File file, String text, boolean append) { - if (file.exists() || file.getParentFile().mkdirs()) { - try (OutputStreamWriter writer = new OutputStreamWriter(new FileOutputStream(file, append), StandardCharsets.UTF_8)) { - writer.write(text); - } + public static void write(Path filePath, Iterable lines, Charset charset, boolean append) { + try { + touch(filePath); + Files.write( + filePath, + lines, + charset, + StandardOpenOption.WRITE, + append ? StandardOpenOption.APPEND : StandardOpenOption.TRUNCATE_EXISTING + ); + } catch (IOException e) { + throw new UncheckedIOException(e); } } - @SneakyThrows(IOException.class) - public static void write(File file, List lines, boolean append) { - if (file.exists() || file.getParentFile().mkdirs()) { - try (BufferedWriter writer = open(file, append)) { - for (String line : lines) { - writer.write(line); - writer.newLine(); - } - } + public static void touch(Path path) throws IOException { + final Path parent = path.getParent(); + if (Files.notExists(parent)) { + Files.createDirectories(parent); + } + if (Files.notExists(path)) { + Files.createFile(path); } } /** * 遍历目录 找到所有满足条件的文件 */ - public static List files(File dir, Predicate predicate) { - if (dir.isFile()) { - return Collections.emptyList(); + public static List files(Path path, Predicate predicate) { + List paths = new LinkedList<>(); + try { + Files.walkFileTree(path, new FileVisitor<>() { + @Override + public FileVisitResult preVisitDirectory(Path dir, BasicFileAttributes attrs) throws IOException { + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFile(Path file, BasicFileAttributes attrs) throws IOException { + if (predicate.test(file)) { + paths.add(path); + } + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult visitFileFailed(Path file, IOException exc) throws IOException { + return FileVisitResult.CONTINUE; + } + + @Override + public FileVisitResult postVisitDirectory(Path dir, IOException exc) throws IOException { + return FileVisitResult.CONTINUE; + } + }); + return Collections.unmodifiableList(paths); + } catch (IOException e) { + throw new UncheckedIOException(e); } - List files = new LinkedList<>(); - Stack stack = new Stack<>(); - stack.push(dir); - do { - eachFiles(stack.pop(), stack, files, predicate); - } while (!stack.empty()); - return List.copyOf(files); } - private static void eachFiles(File dir, Stack stack, List files, Predicate predicate) { - final File[] listFiles = dir.listFiles(); - if (listFiles != null) { - for (File file : listFiles) { - if (file.isDirectory()) { - stack.push(file); - } else if (predicate.test(file)) { - files.add(file); - } + /** + * 截取文件中某一段的字节数据 + * + * @param bufferSize 缓冲区大小 + * @param offset 偏移量 + * @param chunkSize 截取块大小 + * @param file 文件 内部会采用随机读取文件RandomAccessFile + * @param out 输出流 + * @throws IOException IO异常 + */ + public static long sliceBytes(int bufferSize, long offset, long chunkSize, File file, OutputStream out) throws IOException { + try (RandomAccessFile randomAccessFile = new RandomAccessFile(file, "r")) { + randomAccessFile.seek(offset); + long transferred = 0L; + int read; + final byte[] buffer = new byte[bufferSize]; + while (transferred < chunkSize && (read = randomAccessFile.read(buffer, 0, (int) Math.min(buffer.length, chunkSize - transferred))) != -1) { + transferred += read; + out.write(buffer, 0, read); } + return transferred; } } } diff --git a/develop-toolkit-base/src/main/java/develop/toolkit/base/utils/IOAdvice.java b/develop-toolkit-base/src/main/java/develop/toolkit/base/utils/IOAdvice.java index 4493411..37d84b7 100644 --- a/develop-toolkit-base/src/main/java/develop/toolkit/base/utils/IOAdvice.java +++ b/develop-toolkit-base/src/main/java/develop/toolkit/base/utils/IOAdvice.java @@ -353,4 +353,26 @@ public static void printInputStream(InputStream inputStream, Charset charset) { public static void printInputStream(InputStream inputStream) { readLines(inputStream, StandardCharsets.UTF_8).forEach(System.out::println); } + + /** + * 截取输入流中某一段的字节数据 + * + * @param bufferSize 缓冲区大小 + * @param offset 偏移量 + * @param chunkSize 截取块大小 + * @param in 输入流 + * @param out 输出流 + * @throws IOException IO异常 + */ + public static long sliceBytes(int bufferSize, long offset, long chunkSize, InputStream in, OutputStream out) throws IOException { + in.skip(offset); + long transferred = 0L; + int read; + final byte[] buffer = new byte[bufferSize]; + while (transferred < chunkSize && (read = in.read(buffer, 0, (int) Math.min(buffer.length, chunkSize - transferred))) != -1) { + transferred += read; + out.write(buffer, 0, read); + } + return transferred; + } } diff --git a/develop-toolkit-base/src/main/java/develop/toolkit/base/utils/JacksonAdvice.java b/develop-toolkit-base/src/main/java/develop/toolkit/base/utils/JacksonAdvice.java index eacb5b4..e72e9fd 100644 --- a/develop-toolkit-base/src/main/java/develop/toolkit/base/utils/JacksonAdvice.java +++ b/develop-toolkit-base/src/main/java/develop/toolkit/base/utils/JacksonAdvice.java @@ -2,18 +2,28 @@ import com.fasterxml.jackson.core.JsonGenerator; import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.core.TreeNode; import com.fasterxml.jackson.databind.*; +import com.fasterxml.jackson.databind.node.ArrayNode; +import com.fasterxml.jackson.dataformat.xml.XmlMapper; import com.fasterxml.jackson.datatype.jsr310.JavaTimeModule; import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer; import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer; -import com.github.developframework.expression.*; +import com.github.developframework.expression.ArrayExpression; +import com.github.developframework.expression.EmptyExpression; +import com.github.developframework.expression.Expression; import develop.toolkit.base.constants.DateFormatConstants; import develop.toolkit.base.struct.KeyValuePair; import lombok.SneakyThrows; +import java.lang.reflect.Array; import java.text.SimpleDateFormat; import java.time.LocalDateTime; +import java.util.ArrayList; +import java.util.Collection; +import java.util.Collections; import java.util.List; +import java.util.function.Function; /** * @author qiushui on 2020-09-15. @@ -27,8 +37,7 @@ public static ObjectMapper defaultObjectMapper() { ObjectMapper objectMapper = new ObjectMapper(); objectMapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE); objectMapper.setDateFormat(new SimpleDateFormat(DateFormatConstants.STANDARD)); - objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS); - objectMapper.disable(SerializationFeature.FAIL_ON_EMPTY_BEANS); + objectMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, SerializationFeature.FAIL_ON_EMPTY_BEANS); objectMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); objectMapper.enable(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN); JavaTimeModule javaTimeModule = new JavaTimeModule(); @@ -38,6 +47,23 @@ public static ObjectMapper defaultObjectMapper() { return objectMapper; } + /** + * 常用默认的XmlMapper配置 + */ + public static XmlMapper defaultXmlMapper() { + XmlMapper xmlMapper = new XmlMapper(); + xmlMapper.setPropertyNamingStrategy(PropertyNamingStrategy.SNAKE_CASE); + xmlMapper.setDateFormat(new SimpleDateFormat(DateFormatConstants.STANDARD)); + xmlMapper.disable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, SerializationFeature.FAIL_ON_EMPTY_BEANS); + xmlMapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES); + xmlMapper.enable(JsonGenerator.Feature.WRITE_BIGDECIMAL_AS_PLAIN); + JavaTimeModule javaTimeModule = new JavaTimeModule(); + javaTimeModule.addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateFormatConstants.STANDARD_FORMATTER)); + javaTimeModule.addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateFormatConstants.STANDARD_FORMATTER)); + xmlMapper.registerModule(javaTimeModule); + return xmlMapper; + } + /** * 安静地序列化 */ @@ -50,6 +76,31 @@ public static String serializeQuietly(ObjectMapper objectMapper, Object object, } } + @SneakyThrows(JsonProcessingException.class) + public static JsonNode deserializeTreeQuietly(ObjectMapper objectMapper, String json) { + return objectMapper.readTree(json); + } + + @SneakyThrows(JsonProcessingException.class) + public static T treeToValueQuietly(ObjectMapper objectMapper, TreeNode node, Class clazz) { + return objectMapper.treeToValue(node, clazz); + } + + @SneakyThrows(JsonProcessingException.class) + public static T deserializeQuietly(ObjectMapper objectMapper, String json, Class clazz) { + return objectMapper.readValue(json, clazz); + } + + @SneakyThrows(JsonProcessingException.class) + public static T[] deserializeArrayQuietly(ObjectMapper objectMapper, String json, Class clazz) { + return objectMapper.readValue(json, objectMapper.getTypeFactory().constructArrayType(clazz)); + } + + @SneakyThrows(JsonProcessingException.class) + public static , E> T deserializeCollectionQuietly(ObjectMapper objectMapper, String json, Class collectionClass, Class itemClass) { + return objectMapper.readValue(json, objectMapper.getTypeFactory().constructCollectionType(collectionClass, itemClass)); + } + /** * 用表达式从json中取值 */ @@ -104,18 +155,46 @@ public static Object[] deserializeValues(ObjectMapper objectMapper, JsonNode roo return values; } + /** + * ArrayNode转到List + */ + public static List arrayNodeToList(ArrayNode arrayNode, Function function) { + List list = new ArrayList<>(arrayNode.size()); + for (JsonNode node : arrayNode) { + list.add(function.apply(node)); + } + return Collections.unmodifiableList(list); + } + + /** + * ArrayNode转到数组 + */ + @SuppressWarnings("unchecked") + public static T[] arrayNodeToArray(ArrayNode arrayNode, Class clazz, Function function) { + final T[] array = (T[]) Array.newInstance(clazz, arrayNode.size()); + int i = 0; + for (JsonNode node : arrayNode) { + array[i++] = function.apply(node); + } + return array; + } + private static JsonNode parseExpressionToJsonNode(JsonNode jsonNode, Expression expression) { if (expression != EmptyExpression.INSTANCE) { for (Expression singleExpression : expression.expressionTree()) { - if (singleExpression instanceof ObjectExpression) { - jsonNode = existsJsonNode(jsonNode, ((ObjectExpression) singleExpression).getPropertyName()); - } else if (singleExpression instanceof ArrayExpression) { + if (singleExpression.isObject()) { + jsonNode = existsJsonNode(jsonNode, singleExpression.getName()); + } else if (singleExpression.isArray()) { ArrayExpression ae = (ArrayExpression) singleExpression; - jsonNode = existsJsonNode(jsonNode, ae.getPropertyName()); - if (jsonNode.isArray()) { - jsonNode = jsonNode.get(ae.getIndex()); + jsonNode = existsJsonNode(jsonNode, ae.getName()); + for (int i : ae.getIndexArray()) { + if (jsonNode.isArray()) { + jsonNode = jsonNode.get(i); + } else { + throw new IllegalArgumentException("jsonNode is not Array,for expression:" + singleExpression); + } } - } else if (singleExpression instanceof MethodExpression) { + } else if (singleExpression.isMethod()) { throw new IllegalArgumentException("not support method expression."); } } diff --git a/develop-toolkit-base/src/main/java/develop/toolkit/base/utils/StatisticsAdvice.java b/develop-toolkit-base/src/main/java/develop/toolkit/base/utils/MathAdvice.java similarity index 93% rename from develop-toolkit-base/src/main/java/develop/toolkit/base/utils/StatisticsAdvice.java rename to develop-toolkit-base/src/main/java/develop/toolkit/base/utils/MathAdvice.java index 0eff75b..46ed11d 100644 --- a/develop-toolkit-base/src/main/java/develop/toolkit/base/utils/StatisticsAdvice.java +++ b/develop-toolkit-base/src/main/java/develop/toolkit/base/utils/MathAdvice.java @@ -1,79 +1,79 @@ -package develop.toolkit.base.utils; - -import java.util.Collection; -import java.util.List; -import java.util.stream.Collectors; - -/** - * 统计学指标 - * - * @author qiushui on 2020-01-02. - */ -@SuppressWarnings("unused") -public final class StatisticsAdvice { - - /** - * 最大值 - */ - public static double max(Collection numbers) { - return numbers - .stream() - .mapToDouble(Number::doubleValue) - .max().orElseThrow(); - } - - /** - * 最小值 - */ - public static double min(Collection numbers) { - return numbers - .stream() - .mapToDouble(Number::doubleValue) - .min().orElseThrow(); - } - - /** - * 平均值 - */ - public static double average(Collection numbers) { - return numbers - .stream() - .mapToDouble(Number::doubleValue) - .average().orElseThrow(); - } - - /** - * 方差 - */ - public static double variance(Collection numbers) { - final double average = average(numbers); - return numbers - .stream() - .mapToDouble(number -> Math.pow(number.doubleValue() - average, 2)) - .average().orElseThrow(); - } - - /** - * 标准差 - */ - public static double standardDeviation(Collection numbers) { - return Math.sqrt(variance(numbers)); - } - - /** - * 中位数 - */ - public static double median(Collection numbers) { - final List list = numbers - .stream() - .sorted() - .map(Number::doubleValue) - .collect(Collectors.toList()); - if (list.size() % 2 == 0) { - int half = list.size() / 2; - return (list.get(half) + list.get(half + 1)) / 2; - } else { - return list.get(list.size() / 2 + 1); - } - } -} +package develop.toolkit.base.utils; + +import java.util.Collection; +import java.util.List; +import java.util.stream.Collectors; + +/** + * 数学增强 + * + * @author qiushui on 2020-01-02. + */ +@SuppressWarnings("unused") +public final class MathAdvice { + + /** + * 最大值 + */ + public static double max(Collection numbers) { + return numbers + .stream() + .mapToDouble(Number::doubleValue) + .max().orElseThrow(); + } + + /** + * 最小值 + */ + public static double min(Collection numbers) { + return numbers + .stream() + .mapToDouble(Number::doubleValue) + .min().orElseThrow(); + } + + /** + * 平均值 + */ + public static double average(Collection numbers) { + return numbers + .stream() + .mapToDouble(Number::doubleValue) + .average().orElseThrow(); + } + + /** + * 方差 + */ + public static double variance(Collection numbers) { + final double average = average(numbers); + return numbers + .stream() + .mapToDouble(number -> Math.pow(number.doubleValue() - average, 2)) + .average().orElseThrow(); + } + + /** + * 标准差 + */ + public static double standardDeviation(Collection numbers) { + return Math.sqrt(variance(numbers)); + } + + /** + * 中位数 + */ + public static double median(Collection numbers) { + final List list = numbers + .stream() + .sorted() + .map(Number::doubleValue) + .collect(Collectors.toList()); + if (list.size() % 2 == 0) { + int half = list.size() / 2; + return (list.get(half) + list.get(half + 1)) / 2; + } else { + return list.get(list.size() / 2 + 1); + } + } +} diff --git a/develop-toolkit-base/src/main/java/develop/toolkit/base/utils/ObjectAdvice.java b/develop-toolkit-base/src/main/java/develop/toolkit/base/utils/ObjectAdvice.java index 363d9d4..de614eb 100644 --- a/develop-toolkit-base/src/main/java/develop/toolkit/base/utils/ObjectAdvice.java +++ b/develop-toolkit-base/src/main/java/develop/toolkit/base/utils/ObjectAdvice.java @@ -48,19 +48,20 @@ public static boolean valueNotIn(@NonNull T obj, T... targets) { * * @param instance 实例 * @param field 字段 + * @param value 值 * @param firstUseSetterMethod 优先使用setter方法 */ @SneakyThrows - public static void set(Object instance, Field field, boolean firstUseSetterMethod) { + public static void set(Object instance, Field field, Object value, boolean firstUseSetterMethod) { if (firstUseSetterMethod) { try { final String setterMethodName = JavaBeanUtils.getSetterMethodName(field.getName()); MethodUtils.invokeMethod(instance, true, setterMethodName); } catch (NoSuchMethodException e) { - FieldUtils.writeField(field, instance, true); + FieldUtils.writeField(field, instance, value, true); } } else { - FieldUtils.writeField(field, instance, true); + FieldUtils.writeField(field, instance, value, true); } } @@ -69,27 +70,28 @@ public static void set(Object instance, Field field, boolean firstUseSetterMetho * * @param instance 实例 * @param fieldName 字段 + * @param value 值 * @param firstUseSetterMethod 优先使用setter方法 */ @SneakyThrows - public static void set(Object instance, String fieldName, boolean firstUseSetterMethod) { + public static void set(Object instance, String fieldName, Object value, boolean firstUseSetterMethod) { if (firstUseSetterMethod) { try { final String setterMethodName = JavaBeanUtils.getSetterMethodName(fieldName); MethodUtils.invokeMethod(instance, true, setterMethodName); } catch (NoSuchMethodException e) { - FieldUtils.writeField(instance, fieldName, true); + FieldUtils.writeField(instance, fieldName, value, true); } } else { - FieldUtils.writeField(instance, fieldName, true); + FieldUtils.writeField(instance, fieldName, value, true); } } /** * 反射获取值 * - * @param instance 实例 - * @param field 字段 + * @param instance 实例 + * @param field 字段 * @param firstUseGetterMethod 优先使用getter方法 * @return 反射值 */ @@ -110,8 +112,8 @@ public static Object get(Object instance, Field field, boolean firstUseGetterMet /** * 反射获取值 * - * @param instance 实例 - * @param fieldName 字段 + * @param instance 实例 + * @param fieldName 字段 * @param firstUseGetterMethod 优先使用getter方法 * @return 反射值 */ diff --git a/develop-toolkit-base/src/main/java/develop/toolkit/base/utils/StringAdvice.java b/develop-toolkit-base/src/main/java/develop/toolkit/base/utils/StringAdvice.java index 5eadad5..08a4f5a 100644 --- a/develop-toolkit-base/src/main/java/develop/toolkit/base/utils/StringAdvice.java +++ b/develop-toolkit-base/src/main/java/develop/toolkit/base/utils/StringAdvice.java @@ -3,6 +3,7 @@ import develop.toolkit.base.struct.TwoValues; import java.net.URLEncoder; +import java.nio.charset.Charset; import java.nio.charset.StandardCharsets; import java.util.ArrayList; import java.util.List; @@ -113,7 +114,7 @@ public static String intervalFormat(String separator, Object... objs) { * 处理成url参数格式 */ public static String urlParametersFormat(Map parameters, boolean needQuestionMark) { - if (parameters.isEmpty()) { + if (parameters == null || parameters.isEmpty()) { return ""; } return (needQuestionMark ? "?" : "") + parameters @@ -139,4 +140,11 @@ public static String trim(String str, char ch) { } return str.substring(leftSkip, len - rightSkip); } + + /** + * 修改编码 + */ + public static String changeCharset(String str, String charset) { + return new String(str.getBytes(StandardCharsets.ISO_8859_1), Charset.forName("GBK")); + } } diff --git a/develop-toolkit-base/src/main/java/module-info.java b/develop-toolkit-base/src/main/java/module-info.java index 1b54da5..71ae732 100644 --- a/develop-toolkit-base/src/main/java/module-info.java +++ b/develop-toolkit-base/src/main/java/module-info.java @@ -8,6 +8,7 @@ requires java.net.http; requires com.fasterxml.jackson.databind; requires com.fasterxml.jackson.datatype.jsr310; + requires com.fasterxml.jackson.dataformat.xml; requires expression; exports develop.toolkit.base.components; diff --git a/develop-toolkit-db/pom.xml b/develop-toolkit-db/pom.xml index 181e4c8..eb52012 100644 --- a/develop-toolkit-db/pom.xml +++ b/develop-toolkit-db/pom.xml @@ -3,7 +3,7 @@ com.github.developframework develop-toolkit - 1.0.6-SNAPSHOT + 1.0.7-SNAPSHOT 4.0.0 diff --git a/develop-toolkit-multimedia/pom.xml b/develop-toolkit-multimedia/pom.xml new file mode 100644 index 0000000..42432cc --- /dev/null +++ b/develop-toolkit-multimedia/pom.xml @@ -0,0 +1,30 @@ + + + + develop-toolkit + com.github.developframework + 1.0.7-SNAPSHOT + + 4.0.0 + + develop-toolkit-multimedia + + 开发工具箱 - 多媒体处理 + + + + com.github.developframework + develop-toolkit-base + + + commons-io + commons-io + 2.11.0 + + + com.drewnoakes + metadata-extractor + + + + \ No newline at end of file diff --git a/develop-toolkit-multimedia/src/main/java/develop/toolkit/multimedia/image/ImageAdvice.java b/develop-toolkit-multimedia/src/main/java/develop/toolkit/multimedia/image/ImageAdvice.java new file mode 100644 index 0000000..75f6db7 --- /dev/null +++ b/develop-toolkit-multimedia/src/main/java/develop/toolkit/multimedia/image/ImageAdvice.java @@ -0,0 +1,196 @@ +package develop.toolkit.multimedia.image; + +import com.drew.imaging.FileType; +import com.drew.imaging.ImageMetadataReader; +import com.drew.imaging.ImageProcessingException; +import com.drew.metadata.Metadata; +import com.drew.metadata.MetadataException; +import com.drew.metadata.exif.ExifIFD0Directory; +import develop.toolkit.base.utils.CompareAdvice; +import org.apache.commons.io.IOUtils; + +import javax.imageio.ImageIO; +import java.awt.*; +import java.awt.image.BufferedImage; +import java.io.ByteArrayInputStream; +import java.io.IOException; +import java.io.InputStream; +import java.io.OutputStream; + +/** + * @author qiushui on 2021-06-20. + */ +public abstract class ImageAdvice { + + /** + * 修正图片角度后裁切图片 + * + * @param inputStream 图片输入流 + * @param outputStream 输出流 + * @param rectangle 裁切区域 + * @param outFileType 输出图片类型 + * @throws IOException + */ + public static void fixOrientationAndCut(InputStream inputStream, OutputStream outputStream, Rectangle rectangle, FileType outFileType) throws IOException { + final byte[] data = IOUtils.toByteArray(inputStream); + ByteArrayInputStream bais = new ByteArrayInputStream(data); + final int angle = readOrientationAngle(bais); + bais.close(); + bais = new ByteArrayInputStream(data); + final BufferedImage image = cut( + rotate(ImageIO.read(bais), angle), + rectangle + ); + bais.close(); + ImageIO.write(image, outFileType.getAllExtensions()[0], outputStream); + } + + /** + * 修正图片角度后定宽缩放 + * + * @param inputStream 图片输入流 + * @param outputStream 输出流 + * @param width 定宽 + * @param outFileType 输出图片类型 + * @throws IOException + */ + public static void fixOrientationAndZoom(InputStream inputStream, OutputStream outputStream, int width, FileType outFileType) throws IOException { + final byte[] data = IOUtils.toByteArray(inputStream); + ByteArrayInputStream bais = new ByteArrayInputStream(data); + final int angle = readOrientationAngle(bais); + bais.close(); + bais = new ByteArrayInputStream(data); + final BufferedImage image = zoom( + rotate(ImageIO.read(bais), angle), + width + ); + bais.close(); + ImageIO.write(image, outFileType.getAllExtensions()[0], outputStream); + } + + /** + * 裁切图片 + * + * @param originalImage 原图 + * @param rectangle 裁切区域 + * @return 新图片 + */ + public static BufferedImage cut(BufferedImage originalImage, Rectangle rectangle) { + final int MAX_WIDTH = originalImage.getWidth(); + final int MAX_HEIGHT = originalImage.getHeight(); + final int x = CompareAdvice.adjustRange((int) rectangle.getX(), 0, MAX_WIDTH - 1); + final int y = CompareAdvice.adjustRange((int) rectangle.getY(), 0, MAX_HEIGHT - 1); + final int width = CompareAdvice.adjustRange((int) rectangle.getWidth(), 1, MAX_WIDTH - x); + final int height = CompareAdvice.adjustRange((int) rectangle.getHeight(), 1, MAX_HEIGHT - y); + return originalImage.getSubimage(x, y, width, height); + } + + /** + * 旋转角度 + * + * @param originalImage 原图 + * @param angle 旋转角度 + * @return 新图片 + */ + public static BufferedImage rotate(BufferedImage originalImage, int angle) { + // 修正角度 + while (angle < 0) { + angle += 360; + } + angle %= 360; + if (angle == 0) { + return originalImage; + } + final int MAX_WIDTH = originalImage.getWidth(); + final int MAX_HEIGHT = originalImage.getHeight(); + Rectangle rectangle = computeRotatedSize(new Rectangle(new Dimension(MAX_WIDTH, MAX_HEIGHT)), angle); + BufferedImage newImage = new BufferedImage(rectangle.width, rectangle.height, BufferedImage.TYPE_INT_RGB); + Graphics2D graphics = newImage.createGraphics(); + graphics.translate((rectangle.width - MAX_WIDTH) / 2, (rectangle.height - MAX_HEIGHT) / 2); + graphics.rotate(Math.toRadians(angle), (double) MAX_WIDTH / 2, (double) MAX_HEIGHT / 2); + graphics.drawImage(originalImage, null, null); + return newImage; + } + + /** + * 定宽缩放 + * + * @param originalImage 原图 + * @param width 定宽 + * @return 新图片 + */ + public static BufferedImage zoom(BufferedImage originalImage, int width) { + final int MAX_WIDTH = originalImage.getWidth(); + final int MAX_HEIGHT = originalImage.getHeight(); + if (MAX_WIDTH == width) { + return originalImage; + } + final int newHeight = (int) ((double) width / (double) MAX_WIDTH * (double) MAX_HEIGHT); + final BufferedImage newImage = new BufferedImage(width, newHeight, BufferedImage.TYPE_INT_RGB); + final Image image = originalImage.getScaledInstance(width, newHeight, BufferedImage.SCALE_FAST); + newImage.getGraphics().drawImage(image, 0, 0, null); + return newImage; + } + + /** + * 计算旋转后的画布大小 + */ + private static Rectangle computeRotatedSize(Rectangle rectangle, int angle) { + if (angle >= 90) { + if (angle / 90 % 2 == 1) { + int temp = rectangle.height; + rectangle.height = rectangle.width; + rectangle.width = temp; + } + angle %= 90; + } + double r = Math.sqrt(rectangle.height * rectangle.height + rectangle.width * rectangle.width) / 2; + double len = 2 * Math.sin(Math.toRadians(angle) / 2) * r; + double angelAlpha = (Math.PI - Math.toRadians(angle)) / 2; + double angelDeltaWidth = Math.atan((double) rectangle.height / rectangle.width); + double angelDeltaHeight = Math.atan((double) rectangle.width / rectangle.height); + int lenDeltaWidth = (int) (len * Math.cos(Math.PI - angelAlpha - angelDeltaWidth)); + int lenDeltaHeight = (int) (len * Math.cos(Math.PI - angelAlpha - angelDeltaHeight)); + int desWidth = rectangle.width + lenDeltaWidth * 2; + int des_height = rectangle.height + lenDeltaHeight * 2; + return new Rectangle(new Dimension(desWidth, des_height)); + } + + /** + * 读取图片拍摄角度 + * + * @param inputStream 文件输入流 + * @return 角度 + * @throws IOException + */ + private static int readOrientationAngle(InputStream inputStream) throws IOException { + final int orientation; + try { + final Metadata metadata = ImageMetadataReader.readMetadata(inputStream); + final ExifIFD0Directory directory = metadata.getFirstDirectoryOfType(ExifIFD0Directory.class); + if (directory != null && directory.containsTag(ExifIFD0Directory.TAG_ORIENTATION)) { + orientation = directory.getInt(ExifIFD0Directory.TAG_ORIENTATION); + } else { + orientation = 0; + } + } catch (ImageProcessingException | MetadataException e) { + throw new RuntimeException("read image metadata fail: " + e.getMessage()); + } + int angle; + switch (orientation) { + case 3: + angle = 180; + break; + case 6: + angle = 90; + break; + case 8: + angle = 270; + break; + default: + angle = 0; + break; + } + return angle; + } +} diff --git a/develop-toolkit-multimedia/src/main/java/develop/toolkit/multimedia/image/MediaMetadataAdvice.java b/develop-toolkit-multimedia/src/main/java/develop/toolkit/multimedia/image/MediaMetadataAdvice.java new file mode 100644 index 0000000..e072242 --- /dev/null +++ b/develop-toolkit-multimedia/src/main/java/develop/toolkit/multimedia/image/MediaMetadataAdvice.java @@ -0,0 +1,99 @@ +package develop.toolkit.multimedia.image; + +import com.drew.imaging.FileType; +import com.drew.imaging.ImageMetadataReader; +import com.drew.imaging.ImageProcessingException; +import com.drew.metadata.Directory; +import com.drew.metadata.Metadata; +import com.drew.metadata.Tag; +import com.drew.metadata.exif.ExifSubIFDDirectory; +import com.drew.metadata.mov.QuickTimeDirectory; +import com.drew.metadata.mp4.Mp4Directory; +import develop.toolkit.base.utils.DateTimeAdvice; +import lombok.SneakyThrows; + +import java.io.File; +import java.io.IOException; +import java.io.InputStream; +import java.time.LocalDateTime; +import java.util.Optional; + +/** + * @author qiushui on 2021-12-17. + */ +public abstract class MediaMetadataAdvice { + + /** + * 根据扩展名获取文件类型 + * + * @param extensionName 扩展名 + */ + public static FileType getFileTypeByExtensionName(String extensionName) { + for (FileType fileType : FileType.values()) { + for (String extension : fileType.getAllExtensions()) { + if (extension.equals(extensionName)) { + return fileType; + } + } + } + return FileType.Unknown; + } + + /** + * 根据媒体类型获取文件类型 + * + * @param mediaType 媒体类型 + */ + public static FileType getFileTypeByMediaType(String mediaType) { + for (FileType fileType : FileType.values()) { + if (fileType.getMimeType().equals(mediaType)) { + return fileType; + } + } + return FileType.Unknown; + } + + /** + * 打印所有值 + * + * @param inputStream 文件流 + */ + @SneakyThrows({ImageProcessingException.class, IOException.class}) + public static void printMetadataTags(InputStream inputStream) { + final Metadata metadata = ImageMetadataReader.readMetadata(inputStream); + final Iterable directories = metadata.getDirectories(); + for (Directory directory : directories) { + for (Tag tag : directory.getTags()) { + System.out.println(tag); + } + } + } + + /** + * 提取图片文件中的创建时间 + */ + @SneakyThrows({ImageProcessingException.class, IOException.class}) + public static LocalDateTime extractCreateTimeForImage(File file) { + Metadata metadata = ImageMetadataReader.readMetadata(file); + return getMetadataDate(metadata, ExifSubIFDDirectory.class, ExifSubIFDDirectory.TAG_DATETIME_ORIGINAL).orElse(null); + } + + /** + * 提取视频文件中的创建时间 + */ + @SneakyThrows({ImageProcessingException.class, IOException.class}) + public static LocalDateTime extractCreateTimeForVideo(File file) { + Metadata metadata = ImageMetadataReader.readMetadata(file); + return getMetadataDate(metadata, Mp4Directory.class, Mp4Directory.TAG_CREATION_TIME) + .orElseGet(() -> + getMetadataDate(metadata, QuickTimeDirectory.class, QuickTimeDirectory.TAG_CREATION_TIME).orElse(null) + ); + } + + private static Optional getMetadataDate(Metadata metadata, Class directoryClass, int tag) { + return Optional + .ofNullable(metadata.getFirstDirectoryOfType(directoryClass)) + .map(directory -> directory.getDate(tag)) + .map(DateTimeAdvice::toLocalDateTime); + } +} diff --git a/develop-toolkit-multimedia/src/main/java/module-info.java b/develop-toolkit-multimedia/src/main/java/module-info.java new file mode 100644 index 0000000..86811a3 --- /dev/null +++ b/develop-toolkit-multimedia/src/main/java/module-info.java @@ -0,0 +1,12 @@ +/** + * @author qiushui on 2021-06-20. + */ +module develop.toolkit.multimedia { + requires metadata.extractor; + requires develop.toolkit.base; + requires java.desktop; + requires lombok; + requires org.apache.commons.io; + + exports develop.toolkit.multimedia.image; +} \ No newline at end of file diff --git a/develop-toolkit-mybatis/pom.xml b/develop-toolkit-mybatis/pom.xml new file mode 100644 index 0000000..08a65c5 --- /dev/null +++ b/develop-toolkit-mybatis/pom.xml @@ -0,0 +1,41 @@ + + + + develop-toolkit + com.github.developframework + 1.0.7-SNAPSHOT + + 4.0.0 + + develop-toolkit-mybatis + + + 3.5.5 + 8.0.20 + 4.0.3 + + + + + com.github.developframework + develop-toolkit-base + + + org.mybatis + mybatis + ${mybatis.version} + + + mysql + mysql-connector-java + ${mysql-connector-java.version} + runtime + + + com.zaxxer + HikariCP + ${HikariCP.version} + + + + \ No newline at end of file diff --git a/develop-toolkit-mybatis/src/main/java/develop/toolkit/mybatis/BaseMapper.java b/develop-toolkit-mybatis/src/main/java/develop/toolkit/mybatis/BaseMapper.java new file mode 100644 index 0000000..01fcd13 --- /dev/null +++ b/develop-toolkit-mybatis/src/main/java/develop/toolkit/mybatis/BaseMapper.java @@ -0,0 +1,14 @@ +package develop.toolkit.mybatis; + +import org.apache.ibatis.annotations.SelectProvider; + +import java.util.List; + +/** + * @author qiushui on 2022-02-10. + */ +public interface BaseMapper { + + @SelectProvider(type = BaseMapperMysqlProvider.class, method = "select") + List select(Object search); +} diff --git a/develop-toolkit-mybatis/src/main/java/develop/toolkit/mybatis/BaseMapperMysqlProvider.java b/develop-toolkit-mybatis/src/main/java/develop/toolkit/mybatis/BaseMapperMysqlProvider.java new file mode 100644 index 0000000..c53b5e1 --- /dev/null +++ b/develop-toolkit-mybatis/src/main/java/develop/toolkit/mybatis/BaseMapperMysqlProvider.java @@ -0,0 +1,11 @@ +package develop.toolkit.mybatis; + +/** + * @author qiushui on 2022-02-10. + */ +public class BaseMapperMysqlProvider { + + public String select(Object search) { + return null; + } +} diff --git a/develop-toolkit-mybatis/src/main/java/develop/toolkit/mybatis/ConfigurationHandler.java b/develop-toolkit-mybatis/src/main/java/develop/toolkit/mybatis/ConfigurationHandler.java new file mode 100644 index 0000000..3c8ae82 --- /dev/null +++ b/develop-toolkit-mybatis/src/main/java/develop/toolkit/mybatis/ConfigurationHandler.java @@ -0,0 +1,52 @@ +package develop.toolkit.mybatis; + +import com.zaxxer.hikari.HikariConfig; +import org.apache.ibatis.binding.MapperRegistry; +import org.apache.ibatis.plugin.Interceptor; +import org.apache.ibatis.session.Configuration; +import org.apache.ibatis.type.TypeAliasRegistry; + +import java.util.List; + +/** + * @author qiushui on 2021-06-22. + */ +@FunctionalInterface +public interface ConfigurationHandler { + + void configHikari(HikariConfig config); + + /** + * 配置Mapper + * + * @param mapperRegistry mapper注册器 + */ + default void configMapperRegistry(MapperRegistry mapperRegistry) { + + } + + /** + * 配置别名 + * + * @param typeAliasRegistry 别名注册器 + */ + default void configTypeAliasRegistry(TypeAliasRegistry typeAliasRegistry) { + + } + + /** + * 配置拦截器 + * + * @param interceptors 拦截器链 + */ + default void configInterceptors(List interceptors) { + + } + + /** + * 其它配置 + */ + default void configurationSettings(Configuration configuration) { + + } +} diff --git a/develop-toolkit-mybatis/src/main/java/develop/toolkit/mybatis/MybatisAdvice.java b/develop-toolkit-mybatis/src/main/java/develop/toolkit/mybatis/MybatisAdvice.java new file mode 100644 index 0000000..fa1ff9d --- /dev/null +++ b/develop-toolkit-mybatis/src/main/java/develop/toolkit/mybatis/MybatisAdvice.java @@ -0,0 +1,48 @@ +package develop.toolkit.mybatis; + +import com.zaxxer.hikari.HikariConfig; +import com.zaxxer.hikari.HikariDataSource; +import org.apache.ibatis.mapping.Environment; +import org.apache.ibatis.mapping.MappedStatement; +import org.apache.ibatis.session.Configuration; +import org.apache.ibatis.session.SqlSessionFactory; +import org.apache.ibatis.session.SqlSessionFactoryBuilder; +import org.apache.ibatis.transaction.TransactionFactory; +import org.apache.ibatis.transaction.jdbc.JdbcTransactionFactory; + +import javax.sql.DataSource; + +/** + * @author qiushui on 2021-06-22. + */ +public abstract class MybatisAdvice { + + /** + * 构建SqlSessionFactory + * + * @param configurationHandler 配置处理接口 + */ + public static SqlSessionFactory buildSqlSessionFactory(ConfigurationHandler configurationHandler) { + final Configuration configuration = new Configuration(); + configuration.setMapUnderscoreToCamelCase(true); + configuration.setUseGeneratedKeys(true); + configuration.setLogPrefix("mybatis."); + final HikariConfig hikariConfig = new HikariConfig(); + configurationHandler.configHikari(hikariConfig); + final DataSource dataSource = new HikariDataSource(hikariConfig); + final TransactionFactory transactionFactory = new JdbcTransactionFactory(); + configuration.setEnvironment(new Environment("default", transactionFactory, dataSource)); + configurationHandler.configMapperRegistry(configuration.getMapperRegistry()); + configurationHandler.configTypeAliasRegistry(configuration.getTypeAliasRegistry()); + configurationHandler.configInterceptors(configuration.getInterceptors()); + + for (MappedStatement mappedStatement : configuration.getMappedStatements()) { + try { + SimpleMapperHelper.changeMs(mappedStatement); + } catch (Exception e) { + e.printStackTrace(); + } + } + return new SqlSessionFactoryBuilder().build(configuration); + } +} diff --git a/develop-toolkit-mybatis/src/main/java/develop/toolkit/mybatis/MybatisPager.java b/develop-toolkit-mybatis/src/main/java/develop/toolkit/mybatis/MybatisPager.java new file mode 100644 index 0000000..a755a92 --- /dev/null +++ b/develop-toolkit-mybatis/src/main/java/develop/toolkit/mybatis/MybatisPager.java @@ -0,0 +1,26 @@ +package develop.toolkit.mybatis; + +import develop.toolkit.base.struct.Pager; + +/** + * @author qiushui on 2021-06-22. + */ +public final class MybatisPager extends Pager { + + public MybatisPager() { + super(); + } + + public MybatisPager(int index, int size) { + super(index, size); + } + + /** + * 生成 LIMIT 语句 + * + * @return LIMIT 语句 + */ + public String limitSQL() { + return String.format("LIMIT %d, %d", page * size, size); + } +} diff --git a/develop-toolkit-mybatis/src/main/java/develop/toolkit/mybatis/PagingInterceptor.java b/develop-toolkit-mybatis/src/main/java/develop/toolkit/mybatis/PagingInterceptor.java new file mode 100644 index 0000000..7c65182 --- /dev/null +++ b/develop-toolkit-mybatis/src/main/java/develop/toolkit/mybatis/PagingInterceptor.java @@ -0,0 +1,129 @@ +package develop.toolkit.mybatis; + +import org.apache.commons.lang3.StringUtils; +import org.apache.ibatis.cache.CacheKey; +import org.apache.ibatis.executor.Executor; +import org.apache.ibatis.mapping.BoundSql; +import org.apache.ibatis.mapping.MappedStatement; +import org.apache.ibatis.mapping.ResultMap; +import org.apache.ibatis.plugin.Interceptor; +import org.apache.ibatis.plugin.Intercepts; +import org.apache.ibatis.plugin.Invocation; +import org.apache.ibatis.plugin.Signature; +import org.apache.ibatis.session.ResultHandler; +import org.apache.ibatis.session.RowBounds; + +import java.lang.reflect.Field; +import java.sql.SQLException; +import java.util.*; + +/** + * @author qiushui on 2021-06-18. + */ +@Intercepts(@Signature( + type = Executor.class, + method = "query", + args = {MappedStatement.class, Object.class, RowBounds.class, ResultHandler.class} +)) +@SuppressWarnings("unchecked") +public class PagingInterceptor implements Interceptor { + + @Override + public Object intercept(Invocation invocation) throws Throwable { + final Object[] args = invocation.getArgs(); + final MappedStatement ms = (MappedStatement) args[0]; + final Object parameterObject = args[1]; +// final RowBounds rowBounds = (RowBounds) args[2]; + + final Optional mybatisPagerOptional = extractPager(parameterObject); + if (mybatisPagerOptional.isEmpty()) { + //不需要分页,直接返回结果 + return invocation.proceed(); + } + final ResultHandler resultHandler = (ResultHandler) args[3]; + final MybatisPager mybatisPager = mybatisPagerOptional.get(); + final Executor executor = (Executor) invocation.getTarget(); + final BoundSql boundSql = ms.getBoundSql(parameterObject); + final Map additionalParameters = getAdditionalParameters(boundSql); + final long count = queryCount(executor, ms, boundSql, parameterObject, additionalParameters, resultHandler); + if (count == 0) { + return new ArrayList<>(); + } + return queryList(executor, ms, boundSql, parameterObject, additionalParameters, resultHandler, mybatisPager); + } + + /** + * 查询总条数 + */ + private long queryCount(Executor executor, MappedStatement ms, BoundSql boundSql, Object parameterObject, Map additionalParameters, ResultHandler resultHandler) throws SQLException { + MappedStatement countMs = newMappedStatement(ms); + CacheKey countKey = executor.createCacheKey(countMs, parameterObject, RowBounds.DEFAULT, boundSql); + String countSql = String.format("SELECT COUNT(*) FROM (%s) total", boundSql.getSql()); + BoundSql countBoundSql = new BoundSql(ms.getConfiguration(), countSql, boundSql.getParameterMappings(), parameterObject); + additionalParameters.forEach(countBoundSql::setAdditionalParameter); + List countQueryResult = executor.query(countMs, parameterObject, RowBounds.DEFAULT, resultHandler, countKey, countBoundSql); + return (Long) countQueryResult.get(0); + } + + /** + * 查询列表 + */ + private List queryList(Executor executor, MappedStatement ms, BoundSql boundSql, Object parameterObject, Map additionalParameters, ResultHandler resultHandler, MybatisPager pager) throws SQLException { + CacheKey pageKey = executor.createCacheKey(ms, parameterObject, RowBounds.DEFAULT, boundSql); + String pageSql = boundSql.getSql() + " " + pager.limitSQL(); + BoundSql pageBoundSql = new BoundSql(ms.getConfiguration(), pageSql, boundSql.getParameterMappings(), parameterObject); + additionalParameters.forEach(pageBoundSql::setAdditionalParameter); + return executor.query(ms, parameterObject, RowBounds.DEFAULT, resultHandler, pageKey, pageBoundSql); + } + + private MappedStatement newMappedStatement(MappedStatement ms) { + return new MappedStatement.Builder( + ms.getConfiguration(), + ms.getId() + "_count", + ms.getSqlSource(), + ms.getSqlCommandType() + ) + .resource(ms.getResource()) + .fetchSize(ms.getFetchSize()) + .statementType(ms.getStatementType()) + .timeout(ms.getTimeout()) + .parameterMap(ms.getParameterMap()) + .resultSetType(ms.getResultSetType()) + .cache(ms.getCache()) + .flushCacheRequired(ms.isFlushCacheRequired()) + .useCache(ms.isUseCache()) + .resultMaps( + Collections.singletonList( + new ResultMap.Builder( + ms.getConfiguration(), + ms.getId(), + Long.class, + Collections.emptyList() + ).build() + ) + ) + .keyProperty(StringUtils.join(ms.getKeyProperties(), ",")) + .build(); + } + + private Optional extractPager(Object parameterObject) { + if (parameterObject instanceof MybatisPager) { + return Optional.of((MybatisPager) parameterObject); + } else if (parameterObject instanceof Map) { + return ((Map) parameterObject) + .values() + .stream() + .filter(v -> v instanceof MybatisPager) + .map(v -> (MybatisPager) v) + .findFirst(); + } else { + return Optional.empty(); + } + } + + private Map getAdditionalParameters(BoundSql boundSql) throws NoSuchFieldException, IllegalAccessException { + Field additionalParametersField = BoundSql.class.getDeclaredField("additionalParameters"); + additionalParametersField.setAccessible(true); + return (Map) additionalParametersField.get(boundSql); + } +} diff --git a/develop-toolkit-mybatis/src/main/java/develop/toolkit/mybatis/SimpleMapperHelper.java b/develop-toolkit-mybatis/src/main/java/develop/toolkit/mybatis/SimpleMapperHelper.java new file mode 100644 index 0000000..a5d85ff --- /dev/null +++ b/develop-toolkit-mybatis/src/main/java/develop/toolkit/mybatis/SimpleMapperHelper.java @@ -0,0 +1,79 @@ +package develop.toolkit.mybatis; + +import org.apache.ibatis.mapping.MappedStatement; +import org.apache.ibatis.mapping.SqlSource; +import org.apache.ibatis.reflection.MetaObject; +import org.apache.ibatis.reflection.SystemMetaObject; +import org.apache.ibatis.scripting.xmltags.XMLLanguageDriver; + +import java.lang.reflect.Field; +import java.lang.reflect.ParameterizedType; +import java.lang.reflect.Type; + +/** + * @author qiushui on 2022-02-10. + */ +public class SimpleMapperHelper { + + public static final XMLLanguageDriver XML_LANGUAGE_DRIVER = new XMLLanguageDriver(); + + /** + * 获取泛型类型 + */ + public static Class getEntityClass(Class mapperClass) { + Type[] types = mapperClass.getGenericInterfaces(); + Class entityClass = null; + for (Type type : types) { + if (type instanceof ParameterizedType) { + ParameterizedType t = (ParameterizedType) type; + //判断父接口是否为 BaseMapper.class + if (t.getRawType() == BaseMapper.class) { + //得到泛型类型 + entityClass = (Class) t.getActualTypeArguments()[0]; + break; + } + } + } + return entityClass; + } + + /** + * 替换 SqlSource + */ + public static void changeMs(MappedStatement ms) throws Exception { + String msId = ms.getId(); + //标准msId为 包名.接口名.方法名 + int lastIndex = msId.lastIndexOf("."); + String methodName = msId.substring(lastIndex + 1); + String interfaceName = msId.substring(0, lastIndex); + Class mapperClass = Class.forName(interfaceName); + //判断是否继承了通用接口 + if (BaseMapper.class.isAssignableFrom(mapperClass)) { + //判断当前方法是否为通用 select 方法 + if (methodName.equals("select")) { + Class entityClass = getEntityClass(mapperClass); + //必须使用"); + //解析 sqlSource + SqlSource sqlSource = XML_LANGUAGE_DRIVER.createSqlSource(ms.getConfiguration(), sqlBuilder.toString(), entityClass); + //替换 + MetaObject msObject = SystemMetaObject.forObject(ms); + msObject.setValue("sqlSource", sqlSource); + } + } + } +} diff --git a/develop-toolkit-world/pom.xml b/develop-toolkit-world/pom.xml index 79ec887..c984d84 100644 --- a/develop-toolkit-world/pom.xml +++ b/develop-toolkit-world/pom.xml @@ -3,7 +3,7 @@ develop-toolkit com.github.developframework - 1.0.6-SNAPSHOT + 1.0.7-SNAPSHOT 4.0.0 diff --git a/pom.xml b/pom.xml index 455d0ed..5656813 100644 --- a/pom.xml +++ b/pom.xml @@ -1,207 +1,222 @@ - - - 4.0.0 - - com.github.developframework - develop-toolkit - pom - 1.0.6-SNAPSHOT - 开发工具箱 - 2018 - Develop Toolkit - https://github.com/developframework/develop-toolkit - - - develop-toolkit-base - develop-toolkit-db - develop-toolkit-world - - - - 1.18.16 - 1.7.30 - 1.5.0 - 2.11.3 - - - - - - com.github.developframework - develop-toolkit-base - 1.0.6-SNAPSHOT - - - com.github.developframework - develop-toolkit-db - 1.0.6-SNAPSHOT - - - com.github.developframework - develop-toolkit-world - 1.0.6-SNAPSHOT - - - org.projectlombok - lombok - ${version.lombok} - - - org.slf4j - slf4j-api - ${version.slf4j-api} - - - com.github.developframework - expression - ${version.expression} - - - com.fasterxml.jackson.core - jackson-databind - ${version.jackson} - - - com.fasterxml.jackson.datatype - jackson-datatype-jsr310 - ${version.jackson} - - - - - - - qiuzhenhao - 408000511@qq.com - developframework - http://blog.qiushuicloud.xyz - - - - - GitHub Issues - https://github.com/developframework/develop-toolkit - - - - - The Apache License, Version 2.0 - http://www.apache.org/licenses/LICENSE-2.0.txt - - - - - scm:git:git@github.com:developframework/develop-toolkit.git - scm:git:git@github.com:developframework/develop-toolkit.git - https://github.com/developframework/develop-toolkit - HEAD - - - - - - - maven-source-plugin - 3.1.0 - - - attach-sources - verify - - jar-no-fork - - - - - - maven-release-plugin - - v@{project.version} - true - - - - maven-gpg-plugin - 1.5 - - - sign-artifacts - verify - - sign - - - - - - - - - maven-source-plugin - - - maven-compiler-plugin - 3.8.1 - - - - org.projectlombok - lombok - ${version.lombok} - - - - - - - - - - - hclc nexus - http://nexus.hclc-tech.com:8081/repository/maven-snapshots/ - - - hclc nexus - http://nexus.hclc-tech.com:8081/repository/maven-releases/ - - - - - - release - - - - performRelease - true - - - - - - - maven-gpg-plugin - - - maven-javadoc-plugin - - none - - - - - - - - ossrh - https://oss.sonatype.org/content/repositories/snapshots - - - ossrh - https://oss.sonatype.org/service/local/staging/deploy/maven2/ - - - - + + + 4.0.0 + + com.github.developframework + develop-toolkit + pom + 1.0.7-SNAPSHOT + 开发工具箱 + 2018 + Develop Toolkit + https://github.com/developframework/develop-toolkit + + + develop-toolkit-base + develop-toolkit-db + develop-toolkit-world + develop-toolkit-multimedia + develop-toolkit-mybatis + + + + 11 + 11 + UTF-8 + + 1.18.26 + 1.7.30 + 1.6.0 + 2.11.3 + 2.16.0 + + + + + + com.github.developframework + develop-toolkit-base + 1.0.7-SNAPSHOT + + + com.github.developframework + develop-toolkit-db + 1.0.7-SNAPSHOT + + + com.github.developframework + develop-toolkit-world + 1.0.7-SNAPSHOT + + + com.github.developframework + develop-toolkit-multimedia + 1.0.7-SNAPSHOT + + + com.github.developframework + develop-toolkit-mybatis + 1.0.7-SNAPSHOT + + + org.projectlombok + lombok + ${version.lombok} + + + org.slf4j + slf4j-api + ${version.slf4j-api} + + + com.github.developframework + expression + ${version.expression} + + + com.fasterxml.jackson.core + jackson-databind + ${version.jackson} + + + com.fasterxml.jackson.datatype + jackson-datatype-jsr310 + ${version.jackson} + + + com.fasterxml.jackson.dataformat + jackson-dataformat-xml + ${version.jackson} + + + com.drewnoakes + metadata-extractor + ${version.metadata-extractor} + + + + + + + qiuzhenhao + 408000511@qq.com + developframework + http://blog.qiushuicloud.xyz + + + + + GitHub Issues + https://github.com/developframework/develop-toolkit + + + + + The Apache License, Version 2.0 + http://www.apache.org/licenses/LICENSE-2.0.txt + + + + + scm:git:git@github.com:developframework/develop-toolkit.git + scm:git:git@github.com:developframework/develop-toolkit.git + https://github.com/developframework/develop-toolkit + HEAD + + + + + + + maven-source-plugin + 3.1.0 + + + attach-sources + verify + + jar-no-fork + + + + + + maven-release-plugin + + v@{project.version} + true + + + + maven-gpg-plugin + 1.5 + + + sign-artifacts + verify + + sign + + + + + + + + + maven-source-plugin + + + maven-compiler-plugin + 3.8.1 + + + + org.projectlombok + lombok + ${version.lombok} + + + + + + + + + + release + + + + performRelease + true + + + + + + + maven-gpg-plugin + + + maven-javadoc-plugin + + none + + + + + + + + ossrh + https://oss.sonatype.org/content/repositories/snapshots + + + ossrh + https://oss.sonatype.org/service/local/staging/deploy/maven2/ + + + + \ No newline at end of file