diff --git a/CHANGELOG.md b/CHANGELOG.md
index 08cc1b2a5d..6417f00d3a 100644
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -3,6 +3,26 @@
-------------------------------------------------------------------------------------------------------------
+## 4.5.0
+
+### 新特性
+* 【socket】 增加Socket模块
+* 【core】 Validator增加isIpV4方法(issue#IRQ6W@Gitee)
+* 【crypto】 增加SM2Engine,支持C1C2C3和C1C3C2两种模式
+* 【core】 StrUtil.splitTrim支持其它空白符(issue#IRVPC@Gitee)
+* 【http】 请求支持DELETE附带参数模式(issue#IRW9E@Gitee)
+* 【bloomFilter】调整BitMap注释
+
+### Bug修复
+* 【crypto】 修复KeyUtil中使用BC库导致的其它密钥生成异常
+* 【core】 修正DateUtil.formatHttpDate方法
+* 【extra】 修复FTP.ls无法遍历文件问题(issue#IRTA3@Gitee)
+* 【extra】 修复QrCodeUtil中ratio参数失效问题,调整默认纠错为M(感谢@【上海】皮皮今)
+* 【core】 修复FileTypeUtil对jpg文件识别问题(issue#275@Github)
+* 【cache】 修复cache使用读锁导致的删除节点并发问题(issue#IRZTL@Gitee)
+
+-------------------------------------------------------------------------------------------------------------
+
## 4.4.5
### 新特性
diff --git a/README.md b/README.md
index f2b8592e90..e6a206f6f6 100644
--- a/README.md
+++ b/README.md
@@ -64,6 +64,7 @@ Hutool是Hu + tool的自造词,谐音“糊涂”,寓意追求“万事都
- hutool-json JSON实现
- hutool-captcha 图片验证码实现
- hutool-poi 针对POI中Excel的封装
+- hutool-socket 基于Java的NIO和AIO的Socket封装
可以根据需求对每个模块单独引入,也可以通过引入`hutool-all`方式引入所有模块。
@@ -86,21 +87,21 @@ Hutool是Hu + tool的自造词,谐音“糊涂”,寓意追求“万事都
Hutool是Hu + tool的自造词,前者致敬我的“前任公司”,后者为工具之意,谐音“糊涂”,寓意追求“万事都作糊涂观,无所谓失,无所谓得”的境界。
-Hutool是一个Java工具包,也只是一个工具包,它帮助我们简化每一行代码,减少每一个方法,让Java语言也可以“甜甜的”。Hutool最初是我项目中“util”包的一个整理,后来慢慢积累并加入更多非业务相关功能,并广泛学习其它开源项目精髓,经过自己整理修改,最终形成丰富的开源工具集。
-Hutool的设计思想是尽量减少重复的定义,让项目中的util这个package尽量少,总的来说有如下的几个思想:
-
- <dependency>
- <groupId>cn.hutool</groupId>
- <artifactId>hutool-all</artifactId>
- <version>4.4.5</version>
- </dependency>
-
-
- compile 'cn.hutool:hutool-all:4.4.5'
-
- - 从Maven安装 -
-Hutool是一个Java工具包类库,对文件、流、加密解密、转码、正则、线程、XML等JDK方法进行封装,组成各种Util工具类
-通过DateUtil类,提供高度便捷的日期访问、处理和转换方式。
-通过HttpUtil对HTTP客户端的封装,实现便捷的HTTP请求,并简化文件上传操作。
-通过Convert类中的相应静态方法,提供一整套的类型转换解决方案,并通过ConverterRegistry工厂类自定义转换。
-通过Setting对象,提供兼容Properties文件的更加强大的配置文件工具,用于解决中文、分组等JDK配置文件存在的诸多问题。
-Hutool的日志功能,通过抽象Log接口,提供对Slf4j、LogBack、Log4j、JDK-Logging的全面兼容支持。
-通过db模块,提供对MySQL、Oracle等关系型数据库的JDBC封装,借助ActiveRecord思想,大大简化数据库操作。
-我们不是一个人在战斗
-一个非职业的码农,混迹于非IT圈子,利用8小时之外做自己喜欢的事情,爱前端,爱数码,爱美女。
- -深山耕耘互金行业多年,熟悉互金系统架构和设计,喜欢研究新技术,善于发现和解决问题
- -一个奔波于IT圈子的程序猿,拥有自己的梦想,喜欢美女、喜欢音乐、爱打篮球儿...
- -Java程序员一枚,喜欢从生活中领悟技术,喜欢关注技术细节,ennio morricone 音乐的死忠粉。
- -java码农,爱技术、爱旅游、一直活跃在互联网技术圈。
通过以下方式加入讨论,或为Hutool添砖加瓦
-为Hutool提供赞助,也许他们也会为你提供更优惠的服务
-prune
策略prune
策略0
表示没有设置,单位毫秒 */
protected long timeout;
-
+
/** 每个对象是否有单独的失效时长,用于决定清理过期对象是否有必要。 */
protected boolean existCustomTimeout;
-
+
/** 命中数 */
protected int hitCount;
/** 丢失数 */
protected int missCount;
-
+
// ---------------------------------------------------------------- put start
@Override
public void put(K key, V object) {
@@ -74,27 +74,25 @@ public boolean containsKey(K key) {
readLock.lock();
try {
- //不存在或已移除
+ // 不存在或已移除
final CacheObj* 1. \r\n * 2. \n @@ -149,23 +174,24 @@ public static int lineEnd(ByteBuffer buffer, int maxLength) { // 只有\r无法确认换行 canEnd = false; } - + if (charIndex - primitivePosition > maxLength) { - //查找到尽头,未找到,还原位置 + // 查找到尽头,未找到,还原位置 buffer.position(primitivePosition); throw new IndexOutOfBoundsException(StrUtil.format("Position is out of maxLength: {}", maxLength)); } } - - //查找到buffer尽头,未找到,还原位置 + + // 查找到buffer尽头,未找到,还原位置 buffer.position(primitivePosition); - //读到结束位置 + // 读到结束位置 return -1; } /** * 读取一行,如果buffer中最后一部分并非完整一行,则返回null
* 支持的换行符如下: + * ** 1. \r\n * 2. \n @@ -185,7 +211,41 @@ public static String readLine(ByteBuffer buffer, Charset charset) { } else if (endPosition == startPosition) { return StrUtil.EMPTY; } - + return null; } + + /** + * 创建新Buffer + * + * @param data 数据 + * @return {@link ByteBuffer} + * @since 4.5.0 + */ + public static ByteBuffer create(byte[] data) { + return ByteBuffer.wrap(data); + } + + /** + * 从字符串创建新Buffer + * + * @param data 数据 + * @param charset 编码 + * @return {@link ByteBuffer} + * @since 4.5.0 + */ + public static ByteBuffer create(CharSequence data, Charset charset) { + return create(StrUtil.bytes(data, charset)); + } + + /** + * 从字符串创建新Buffer,使用UTF-8编码 + * + * @param data 数据 + * @return {@link ByteBuffer} + * @since 4.5.0 + */ + public static ByteBuffer createUtf8(CharSequence data) { + return create(StrUtil.utf8Bytes(data)); + } } diff --git a/hutool-core/src/main/java/cn/hutool/core/io/FileTypeUtil.java b/hutool-core/src/main/java/cn/hutool/core/io/FileTypeUtil.java index 3311de58f9..2f572580a5 100644 --- a/hutool-core/src/main/java/cn/hutool/core/io/FileTypeUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/io/FileTypeUtil.java @@ -26,10 +26,10 @@ private FileTypeUtil() { static { fileTypeMap = new ConcurrentHashMap<>(); -// fileTypeMap.put("ffd8ffe000104a464946", "jpg"); // JPEG (jpg) - fileTypeMap.put("ffd8ffe", "jpg"); // JPEG (jpg) - fileTypeMap.put("89504e470d0a1a0a", "png"); // PNG (png) - fileTypeMap.put("47494638396126026f01", "gif"); // GIF (gif) + fileTypeMap.put("ffd8ff", "jpg"); // JPEG (jpg) + fileTypeMap.put("89504e47", "png"); // PNG (png) + fileTypeMap.put("4749463837", "gif"); // GIF (gif) + fileTypeMap.put("4749463839", "gif"); // GIF (gif) fileTypeMap.put("49492a00227105008037", "tif"); // TIFF (tif) fileTypeMap.put("424d228c010000000000", "bmp"); // 16色位图(bmp) fileTypeMap.put("424d8240090000000000", "bmp"); // 24位位图(bmp) diff --git a/hutool-core/src/main/java/cn/hutool/core/io/IoUtil.java b/hutool-core/src/main/java/cn/hutool/core/io/IoUtil.java index 3d5018f35c..552e4125d5 100644 --- a/hutool-core/src/main/java/cn/hutool/core/io/IoUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/io/IoUtil.java @@ -52,7 +52,7 @@ public class IoUtil { /** 默认缓存大小 */ - public static final int DEFAULT_BUFFER_SIZE = 1024; + public static final int DEFAULT_BUFFER_SIZE = 2048; /** 默认中等缓存大小 */ public static final int DEFAULT_MIDDLE_BUFFER_SIZE = 4096; /** 默认大缓存大小 */ @@ -224,6 +224,33 @@ public static long copy(FileInputStream in, FileOutputStream out) throws IORunti } } + /** + * 拷贝流,使用NIO,不会关闭流 + * + * @param in {@link ReadableByteChannel} + * @param out {@link WritableByteChannel} + * @return 拷贝的字节数 + * @throws IORuntimeException IO异常 + * @since 4.5.0 + */ + public static long copy(ReadableByteChannel in, WritableByteChannel out) throws IORuntimeException { + return copy(in, out, DEFAULT_BUFFER_SIZE); + } + + /** + * 拷贝流,使用NIO,不会关闭流 + * + * @param in {@link ReadableByteChannel} + * @param out {@link WritableByteChannel} + * @param bufferSize 缓冲大小,如果小于等于0,使用默认 + * @return 拷贝的字节数 + * @throws IORuntimeException IO异常 + * @since 4.5.0 + */ + public static long copy(ReadableByteChannel in, WritableByteChannel out, int bufferSize) throws IORuntimeException { + return copy(in, out, bufferSize, null); + } + /** * 拷贝流,使用NIO,不会关闭流 * @@ -384,6 +411,20 @@ public static String read(InputStream in, Charset charset) throws IORuntimeExcep return null == charset ? out.toString() : out.toString(charset); } + /** + * 从流中读取内容,读取完毕后并不关闭流 + * + * @param channel 可读通道,读取完毕后并不关闭通道 + * @param charset 字符集 + * @return 内容 + * @throws IORuntimeException IO异常 + * @since 4.5.0 + */ + public static String read(ReadableByteChannel channel, Charset charset) throws IORuntimeException { + FastByteArrayOutputStream out = read(channel); + return null == charset ? out.toString() : out.toString(charset); + } + /** * 从流中读取内容,读到输出流中 * @@ -397,6 +438,19 @@ public static FastByteArrayOutputStream read(InputStream in) throws IORuntimeExc return out; } + /** + * 从流中读取内容,读到输出流中 + * + * @param channel 可读通道,读取完毕后并不关闭通道 + * @return 输出流 + * @throws IORuntimeException IO异常 + */ + public static FastByteArrayOutputStream read(ReadableByteChannel channel) throws IORuntimeException { + final FastByteArrayOutputStream out = new FastByteArrayOutputStream(); + copy(channel, Channels.newChannel(out)); + return out; + } + /** * 从Reader中读取String,读取完毕后并不关闭Reader * diff --git a/hutool-core/src/main/java/cn/hutool/core/lang/PatternPool.java b/hutool-core/src/main/java/cn/hutool/core/lang/PatternPool.java index 379eb19dbd..11c725823b 100644 --- a/hutool-core/src/main/java/cn/hutool/core/lang/PatternPool.java +++ b/hutool-core/src/main/java/cn/hutool/core/lang/PatternPool.java @@ -26,6 +26,8 @@ public class PatternPool { public final static Pattern GROUP_VAR = Pattern.compile("\\$(\\d+)"); /** IP v4 */ public final static Pattern IPV4 = Pattern.compile("\\b((?!\\d\\d\\d)\\d+|1\\d\\d|2[0-4]\\d|25[0-5])\\.((?!\\d\\d\\d)\\d+|1\\d\\d|2[0-4]\\d|25[0-5])\\.((?!\\d\\d\\d)\\d+|1\\d\\d|2[0-4]\\d|25[0-5])\\.((?!\\d\\d\\d)\\d+|1\\d\\d|2[0-4]\\d|25[0-5])\\b"); + /** IP v4 */ + public final static Pattern IPV6 = Pattern.compile("(([0-9a-fA-F]{1,4}:){7,7}[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,7}:|([0-9a-fA-F]{1,4}:){1,6}:[0-9a-fA-F]{1,4}|([0-9a-fA-F]{1,4}:){1,5}(:[0-9a-fA-F]{1,4}){1,2}|([0-9a-fA-F]{1,4}:){1,4}(:[0-9a-fA-F]{1,4}){1,3}|([0-9a-fA-F]{1,4}:){1,3}(:[0-9a-fA-F]{1,4}){1,4}|([0-9a-fA-F]{1,4}:){1,2}(:[0-9a-fA-F]{1,4}){1,5}|[0-9a-fA-F]{1,4}:((:[0-9a-fA-F]{1,4}){1,6})|:((:[0-9a-fA-F]{1,4}){1,7}|:)|fe80:(:[0-9a-fA-F]{0,4}){0,4}%[0-9a-zA-Z]{1,}|::(ffff(:0{1,4}){0,1}:){0,1}((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])|([0-9a-fA-F]{1,4}:){1,4}:((25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9])\\.){3,3}(25[0-5]|(2[0-4]|1{0,1}[0-9]){0,1}[0-9]))"); /** 货币 */ public final static Pattern MONEY = Pattern.compile("^(\\d+(?:\\.\\d+)?)$"); /** 邮件,符合RFC 5322规范,正则来自:http://emailregex.com/ */ diff --git a/hutool-core/src/main/java/cn/hutool/core/lang/Validator.java b/hutool-core/src/main/java/cn/hutool/core/lang/Validator.java index 968c21d09a..0c3015581b 100644 --- a/hutool-core/src/main/java/cn/hutool/core/lang/Validator.java +++ b/hutool-core/src/main/java/cn/hutool/core/lang/Validator.java @@ -30,6 +30,8 @@ private Validator() { public final static Pattern GROUP_VAR = PatternPool.GROUP_VAR; /** IP v4 */ public final static Pattern IPV4 = PatternPool.IPV4; + /** IP v6 */ + public final static Pattern IPV6 = PatternPool.IPV6; /** 货币 */ public final static Pattern MONEY = PatternPool.MONEY; /** 邮件 */ @@ -810,6 +812,32 @@ public staticT validateIpv4(T value, String errorMsg) } return value; } + + /** + * 验证是否为IPV6地址 + * + * @param value 值 + * @return 是否为IPV6地址 + */ + public static boolean isIpv6(CharSequence value) { + return isMactchRegex(IPV6, value); + } + + /** + * 验证是否为IPV6地址 + * + * @param 字符串类型 + * @param value 值 + * @param errorMsg 验证错误的信息 + * @return 验证后的值 + * @throws ValidateException 验证异常 + */ + public static T validateIpv6(T value, String errorMsg) throws ValidateException { + if (false == isIpv6(value)) { + throw new ValidateException(errorMsg); + } + return value; + } /** * 验证是否为MAC地址 diff --git a/hutool-core/src/main/java/cn/hutool/core/text/StrSpliter.java b/hutool-core/src/main/java/cn/hutool/core/text/StrSpliter.java index b93ae04fe1..e0ea2e4193 100644 --- a/hutool-core/src/main/java/cn/hutool/core/text/StrSpliter.java +++ b/hutool-core/src/main/java/cn/hutool/core/text/StrSpliter.java @@ -494,7 +494,7 @@ public static String[] splitByLength(String str, int len) { private static List addToList(List list, String part, boolean isTrim, boolean ignoreEmpty){ part = part.toString(); if(isTrim){ - part = part.trim(); + part = StrUtil.trim(part); } if(false == ignoreEmpty || false == part.isEmpty()){ list.add(part); diff --git a/hutool-core/src/main/java/cn/hutool/core/thread/ExecutorBuilder.java b/hutool-core/src/main/java/cn/hutool/core/thread/ExecutorBuilder.java index b69397ab6f..d772a2de00 100644 --- a/hutool-core/src/main/java/cn/hutool/core/thread/ExecutorBuilder.java +++ b/hutool-core/src/main/java/cn/hutool/core/thread/ExecutorBuilder.java @@ -10,6 +10,7 @@ import java.util.concurrent.TimeUnit; import cn.hutool.core.builder.Builder; +import cn.hutool.core.util.ObjectUtil; /** * {@link ThreadPoolExecutor} 建造者 @@ -19,12 +20,20 @@ */ public class ExecutorBuilder implements Builder { + /** 初始池大小 */ private int corePoolSize; + /** 最大池大小(允许同时执行的最大线程数) */ private int maxPoolSize = Integer.MAX_VALUE; + /** 线程存活时间,既当池中线程多于初始大小时,多出的线程保留的时长 */ private long keepAliveTime = TimeUnit.SECONDS.toNanos(60); + /** 队列,用于存在未执行的线程 */ private BlockingQueue workQueue; + /** 线程工厂,用于自定义线程创建 */ private ThreadFactory threadFactory; + /** 当线程阻塞(block)时的异常处理器,所谓线程阻塞既线程池和等待队列已满,无法处理线程时采取的策略 */ private RejectedExecutionHandler handler; + /** 线程执行超时后是否回收线程 */ + private Boolean allowCoreThreadTimeOut; /** * 设置初始池大小,默认0 @@ -73,8 +82,9 @@ public ExecutorBuilder setKeepAliveTime(long keepAliveTime) { /** * 设置队列,用于存在未执行的线程
* 可选队列有: + * *- * 1. SynchronousQueue 它将任务直接提交给线程而不保持它们。当运行线程小于maxPoolSize时会创建新线程 + * 1. SynchronousQueue 它将任务直接提交给线程而不保持它们。当运行线程小于maxPoolSize时会创建新线程,否则触发异常策略 * 2. LinkedBlockingQueue 无界队列,当运行线程大于corePoolSize时始终放入此队列,此时maximumPoolSize无效 * 3. ArrayBlockingQueue 有界队列,相对无界队列有利于控制队列大小,队列满时,运行线程小于maxPoolSize时会创建新线程,否则触发异常策略 *@@ -86,15 +96,28 @@ public ExecutorBuilder setWorkQueue(BlockingQueueworkQueue) { this.workQueue = workQueue; return this; } - + /** - * 使用{@link SynchronousQueue} 做为等待队列 + * 使用{@link SynchronousQueue} 做为等待队列(非公平策略)
+ * 它将任务直接提交给线程而不保持它们。当运行线程小于maxPoolSize时会创建新线程,否则触发异常策略 * * @return this * @since 4.1.11 */ public ExecutorBuilder useSynchronousQueue() { - return setWorkQueue(new SynchronousQueue()); + return useSynchronousQueue(false); + } + + /** + * 使用{@link SynchronousQueue} 做为等待队列
+ * 它将任务直接提交给线程而不保持它们。当运行线程小于maxPoolSize时会创建新线程,否则触发异常策略 + * + * @param fair 是否使用公平访问策略 + * @return this + * @since 4.5.0 + */ + public ExecutorBuilder useSynchronousQueue(boolean fair) { + return setWorkQueue(new SynchronousQueue(fair)); } /** @@ -123,6 +146,17 @@ public ExecutorBuilder setHandler(RejectedExecutionHandler handler) { return this; } + /** + * 设置线程执行超时后是否回收线程 + * + * @param allowCoreThreadTimeOut 线程执行超时后是否回收线程 + * @return this + */ + public ExecutorBuilder setAllowCoreThreadTimeOut(boolean allowCoreThreadTimeOut) { + this.allowCoreThreadTimeOut = allowCoreThreadTimeOut; + return this; + } + /** * 创建ExecutorBuilder,开始构建 * @@ -151,19 +185,26 @@ private static ThreadPoolExecutor build(ExecutorBuilder builder) { final int maxPoolSize = builder.maxPoolSize; final long keepAliveTime = builder.keepAliveTime; final BlockingQueue workQueue; - if(null != builder.workQueue) { + if (null != builder.workQueue) { workQueue = builder.workQueue; } else { - //corePoolSize为0则要使用SynchronousQueue避免无限阻塞 + // corePoolSize为0则要使用SynchronousQueue避免无限阻塞 workQueue = (corePoolSize <= 0) ? new SynchronousQueue () : new LinkedBlockingQueue (); } final ThreadFactory threadFactory = (null != builder.threadFactory) ? builder.threadFactory : Executors.defaultThreadFactory(); - final RejectedExecutionHandler handler = builder.handler; + RejectedExecutionHandler handler = ObjectUtil.defaultIfNull(builder.handler, new ThreadPoolExecutor.AbortPolicy()); - if (null == handler) { - return new ThreadPoolExecutor(corePoolSize, maxPoolSize, keepAliveTime, TimeUnit.NANOSECONDS, workQueue, threadFactory); - } else { - return new ThreadPoolExecutor(corePoolSize, maxPoolSize, keepAliveTime, TimeUnit.NANOSECONDS, workQueue, threadFactory, handler); + final ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(// + corePoolSize, // + maxPoolSize, // + keepAliveTime, TimeUnit.NANOSECONDS, // + workQueue, // + threadFactory, // + handler// + ); + if (null != builder.allowCoreThreadTimeOut) { + threadPoolExecutor.allowCoreThreadTimeOut(builder.allowCoreThreadTimeOut); } + return threadPoolExecutor; } } diff --git a/hutool-core/src/main/java/cn/hutool/core/util/IdcardUtil.java b/hutool-core/src/main/java/cn/hutool/core/util/IdcardUtil.java index e230b9175a..1a0bb7bd4b 100644 --- a/hutool-core/src/main/java/cn/hutool/core/util/IdcardUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/util/IdcardUtil.java @@ -208,6 +208,11 @@ public static boolean isvalidCard18(String idCard) { if (CHINA_ID_MAX_LENGTH != idCard.length()) { return false; } + + //校验生日 + if(false == Validator.isBirthday(idCard.substring(6, 14))) { + return false; + } // 前17位 String code17 = idCard.substring(0, 17); diff --git a/hutool-core/src/main/java/cn/hutool/core/util/NumberUtil.java b/hutool-core/src/main/java/cn/hutool/core/util/NumberUtil.java index 45e11867e2..206c33ab0a 100644 --- a/hutool-core/src/main/java/cn/hutool/core/util/NumberUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/util/NumberUtil.java @@ -2225,6 +2225,80 @@ public static int toInt(byte[] bytes) { | (bytes[3] & 0xff); } + /** + * 以无符号字节数组的形式返回传入值。 + * + * @param value 需要转换的值 + * @return 无符号bytes + * @since 4.5.0 + */ + public static byte[] toUnsignedByteArray(BigInteger value) { + byte[] bytes = value.toByteArray(); + + if (bytes[0] == 0) { + byte[] tmp = new byte[bytes.length - 1]; + System.arraycopy(bytes, 1, tmp, 0, tmp.length); + + return tmp; + } + + return bytes; + } + + /** + * 以无符号字节数组的形式返回传入值。 + * + * @param length bytes长度 + * @param value 需要转换的值 + * @return 无符号bytes + * @since 4.5.0 + */ + public static byte[] toUnsignedByteArray(int length, BigInteger value) { + byte[] bytes = value.toByteArray(); + if (bytes.length == length) { + return bytes; + } + + int start = bytes[0] == 0 ? 1 : 0; + int count = bytes.length - start; + + if (count > length) { + throw new IllegalArgumentException("standard length exceeded for value"); + } + + byte[] tmp = new byte[length]; + System.arraycopy(bytes, start, tmp, tmp.length - count, count); + return tmp; + } + + /** + * 无符号bytes转{@link BigInteger} + * + * @param buf buf 无符号bytes + * @return {@link BigInteger} + * @since 4.5.0 + */ + public static BigInteger fromUnsignedByteArray(byte[] buf) { + return new BigInteger(1, buf); + } + + /** + * 无符号bytes转{@link BigInteger} + * + * @param buf 无符号bytes + * @param off 起始位置 + * @param length 长度 + * @return {@link BigInteger} + */ + public static BigInteger fromUnsignedByteArray(byte[] buf, int off, int length) { + byte[] mag = buf; + if (off != 0 || length != buf.length) { + mag = new byte[length]; + System.arraycopy(buf, off, mag, 0, length); + } + return new BigInteger(1, mag); + } + // ------------------------------------------------------------------------------------------- Private method start private static int mathSubnode(int selectNum, int minNum) { if (selectNum == minNum) { diff --git a/hutool-core/src/test/java/cn/hutool/core/date/DateUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/date/DateUtilTest.java index 1e5a4d62bc..9166ca3865 100644 --- a/hutool-core/src/test/java/cn/hutool/core/date/DateUtilTest.java +++ b/hutool-core/src/test/java/cn/hutool/core/date/DateUtilTest.java @@ -453,4 +453,10 @@ public void yearAndQTest() { Assert.assertEquals(1, list2.size()); Assert.assertEquals("20184", list2.get(0)); } + + @Test + public void formatHttpDateTest() { + String formatHttpDate = DateUtil.formatHttpDate(DateUtil.parse("2019-01-02 22:32:01")); + Assert.assertEquals("Wed, 02 Jan 2019 14:32:01 GMT", formatHttpDate); + } } diff --git a/hutool-core/src/test/java/cn/hutool/core/date/ZodiacTest.java b/hutool-core/src/test/java/cn/hutool/core/date/ZodiacTest.java index c5f3803e37..986e490d54 100644 --- a/hutool-core/src/test/java/cn/hutool/core/date/ZodiacTest.java +++ b/hutool-core/src/test/java/cn/hutool/core/date/ZodiacTest.java @@ -7,7 +7,9 @@ public class ZodiacTest { @Test public void getZodiacTest() { - Assert.assertEquals("处女座", Zodiac.getZodiac(7, 28)); + Assert.assertEquals("摩羯座", Zodiac.getZodiac(Month.JANUARY, 19)); + Assert.assertEquals("水瓶座", Zodiac.getZodiac(Month.JANUARY, 20)); + Assert.assertEquals("巨蟹座", Zodiac.getZodiac(6, 17)); } @Test diff --git a/hutool-core/src/test/java/cn/hutool/core/util/ImageUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/util/ImageUtilTest.java index 7aebfe7f44..84dc855b3e 100644 --- a/hutool-core/src/test/java/cn/hutool/core/util/ImageUtilTest.java +++ b/hutool-core/src/test/java/cn/hutool/core/util/ImageUtilTest.java @@ -34,10 +34,10 @@ public void cutTest() { } @Test - @Ignore +// @Ignore public void rotateTest() throws IOException { - BufferedImage image = ImageUtil.rotate(ImageIO.read(FileUtil.file("e:/line.png")), 45); - ImageUtil.write(image, FileUtil.file("e:/result.png")); + BufferedImage image = ImageUtil.rotate(ImageIO.read(FileUtil.file("e:/pic/366466.jpg")), 180); + ImageUtil.write(image, FileUtil.file("e:/pic/result.png")); } @Test diff --git a/hutool-cron/pom.xml b/hutool-cron/pom.xml index 68c2cbcfe2..a8a44de3ff 100644 --- a/hutool-cron/pom.xml +++ b/hutool-cron/pom.xml @@ -7,7 +7,7 @@ cn.hutool hutool-parent -4.4.5 +4.5.0 hutool-cron diff --git a/hutool-cron/src/main/java/cn/hutool/cron/Scheduler.java b/hutool-cron/src/main/java/cn/hutool/cron/Scheduler.java index c7531e62b9..05e221e89d 100644 --- a/hutool-cron/src/main/java/cn/hutool/cron/Scheduler.java +++ b/hutool-cron/src/main/java/cn/hutool/cron/Scheduler.java @@ -71,7 +71,7 @@ public class Scheduler { protected TaskExecutorManager taskExecutorManager; /** 监听管理器列表 */ protected TaskListenerManager listenerManager = new TaskListenerManager(); - /** 线程池 */ + /** 线程池,用于执行TaskLauncher和TaskExecutor */ protected ExecutorService threadExecutor; // --------------------------------------------------------- Getters and Setters start @@ -363,6 +363,7 @@ public Scheduler start() { throw new CronException("Schedule is started!"); } + // 无界线程池,确保每一个需要执行的线程都可以及时运行,同时复用已有现成避免线程重复创建 this.threadExecutor = ExecutorBuilder.create().useSynchronousQueue().setThreadFactory(// ThreadFactoryBuilder.create().setNamePrefix("hutool-cron-").setDaemon(this.daemon).build()// ).build(); diff --git a/hutool-crypto/pom.xml b/hutool-crypto/pom.xml index 49bc38018d..583b3bea4a 100644 --- a/hutool-crypto/pom.xml +++ b/hutool-crypto/pom.xml @@ -9,7 +9,7 @@cn.hutool hutool-parent -4.4.5 +4.5.0 hutool-crypto diff --git a/hutool-crypto/src/main/java/cn/hutool/crypto/BCUtil.java b/hutool-crypto/src/main/java/cn/hutool/crypto/BCUtil.java new file mode 100644 index 0000000000..cd1d03d525 --- /dev/null +++ b/hutool-crypto/src/main/java/cn/hutool/crypto/BCUtil.java @@ -0,0 +1,78 @@ +package cn.hutool.crypto; + +import java.security.GeneralSecurityException; +import java.security.KeyFactory; +import java.security.PublicKey; +import java.security.spec.ECFieldFp; +import java.security.spec.ECParameterSpec; +import java.security.spec.ECPoint; +import java.security.spec.ECPublicKeySpec; +import java.security.spec.EllipticCurve; + +import org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey; +import org.bouncycastle.jce.ECNamedCurveTable; +import org.bouncycastle.jce.ECPointUtil; +import org.bouncycastle.jce.spec.ECNamedCurveParameterSpec; +import org.bouncycastle.jce.spec.ECNamedCurveSpec; +import org.bouncycastle.math.ec.ECCurve; + +/** + * Bouncy Castle相关工具类封装 + * + * @author looly + *@since 4.5.0 + */ +public class BCUtil { + /** + * 编码压缩EC公钥(基于BouncyCastle)
+ * 见:https://www.cnblogs.com/xinzhao/p/8963724.html + * + * @param publicKey {@link PublicKey},必须为org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey + * @return 压缩得到的X + * @since 4.4.4 + */ + public static byte[] encodeECPublicKey(PublicKey publicKey) { + return ((BCECPublicKey) publicKey).getQ().getEncoded(true); + } + + /** + * 解码恢复EC压缩公钥,支持Base64和Hex编码,(基于BouncyCastle)
+ * 见:https://www.cnblogs.com/xinzhao/p/8963724.html + * + * @param encode 压缩公钥 + * @param curveName EC曲线名 + * @since 4.4.4 + */ + public static PublicKey decodeECPoint(String encode, String curveName) { + return decodeECPoint(SecureUtil.decodeKey(encode), curveName); + } + + /** + * 解码恢复EC压缩公钥,支持Base64和Hex编码,(基于BouncyCastle)
+ * 见:https://www.cnblogs.com/xinzhao/p/8963724.html + * + * @param encodeByte 压缩公钥 + * @param curveName EC曲线名,例如{@link KeyUtil#SM2_DEFAULT_CURVE} + * @since 4.4.4 + */ + public static PublicKey decodeECPoint(byte[] encodeByte, String curveName) { + final ECNamedCurveParameterSpec namedSpec = ECNamedCurveTable.getParameterSpec(curveName); + final ECCurve curve = namedSpec.getCurve(); + final EllipticCurve ecCurve = new EllipticCurve(// + new ECFieldFp(curve.getField().getCharacteristic()), // + curve.getA().toBigInteger(), // + curve.getB().toBigInteger()); + // 根据X恢复点Y + final ECPoint point = ECPointUtil.decodePoint(ecCurve, encodeByte); + + // 根据曲线恢复公钥格式 + ECParameterSpec ecSpec = new ECNamedCurveSpec(curveName, curve, namedSpec.getG(), namedSpec.getN()); + + final KeyFactory PubKeyGen = KeyUtil.getKeyFactory("EC"); + try { + return PubKeyGen.generatePublic(new ECPublicKeySpec(point, ecSpec)); + } catch (GeneralSecurityException e) { + throw new CryptoException(e); + } + } +} diff --git a/hutool-crypto/src/main/java/cn/hutool/crypto/KeyUtil.java b/hutool-crypto/src/main/java/cn/hutool/crypto/KeyUtil.java index c7a9f89a0e..0753e97428 100644 --- a/hutool-crypto/src/main/java/cn/hutool/crypto/KeyUtil.java +++ b/hutool-crypto/src/main/java/cn/hutool/crypto/KeyUtil.java @@ -1,7 +1,6 @@ package cn.hutool.crypto; import java.io.InputStream; -import java.security.GeneralSecurityException; import java.security.InvalidAlgorithmParameterException; import java.security.InvalidKeyException; import java.security.KeyFactory; @@ -15,13 +14,10 @@ import java.security.PublicKey; import java.security.SecureRandom; import java.security.cert.Certificate; +import java.security.cert.CertificateException; import java.security.cert.CertificateFactory; import java.security.spec.AlgorithmParameterSpec; -import java.security.spec.ECFieldFp; import java.security.spec.ECGenParameterSpec; -import java.security.spec.ECPoint; -import java.security.spec.ECPublicKeySpec; -import java.security.spec.EllipticCurve; import java.security.spec.KeySpec; import java.security.spec.PKCS8EncodedKeySpec; import java.security.spec.X509EncodedKeySpec; @@ -34,8 +30,6 @@ import javax.crypto.spec.PBEKeySpec; import javax.crypto.spec.SecretKeySpec; -import org.bouncycastle.math.ec.ECCurve; - import cn.hutool.core.io.FileUtil; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.ArrayUtil; @@ -604,13 +598,11 @@ public static Certificate readCertificate(String type, InputStream in, char[] pa * @return {@link Certificate} */ public static Certificate readCertificate(String type, InputStream in) { - Certificate certificate; try { - certificate = CertificateFactory.getInstance(type).generateCertificate(in); - } catch (Exception e) { + return getCertificateFactory(type).generateCertificate(in); + } catch (CertificateException e) { throw new CryptoException(e); } - return certificate; } /** @@ -627,6 +619,30 @@ public static Certificate getCertificate(KeyStore keyStore, String alias) { throw new CryptoException(e); } } + + /** + * 获取{@link CertificateFactory} + * + * @param type 类型,例如X.509 + * @return {@link KeyPairGenerator} + * @since 4.5.0 + */ + public static CertificateFactory getCertificateFactory(String type) { + Provider provider = null; + try { + provider = ProviderFactory.createBouncyCastleProvider(); + } catch (NoClassDefFoundError e) { + // ignore + } + + CertificateFactory factory; + try { + factory = (null == provider) ? CertificateFactory.getInstance(type) : CertificateFactory.getInstance(type, provider); + } catch (CertificateException e) { + throw new CryptoException(e); + } + return factory; + } /** * 编码压缩EC公钥(基于BouncyCastle)
@@ -637,7 +653,7 @@ public static Certificate getCertificate(KeyStore keyStore, String alias) { * @since 4.4.4 */ public static byte[] encodeECPublicKey(PublicKey publicKey) { - return ((org.bouncycastle.jcajce.provider.asymmetric.ec.BCECPublicKey) publicKey).getQ().getEncoded(true); + return BCUtil.encodeECPublicKey(publicKey); } /** @@ -649,7 +665,7 @@ public static byte[] encodeECPublicKey(PublicKey publicKey) { * @since 4.4.4 */ public static PublicKey decodeECPoint(String encode, String curveName) { - return decodeECPoint(SecureUtil.decodeKey(encode), curveName); + return BCUtil.decodeECPoint(encode, curveName); } /** @@ -661,23 +677,6 @@ public static PublicKey decodeECPoint(String encode, String curveName) { * @since 4.4.4 */ public static PublicKey decodeECPoint(byte[] encodeByte, String curveName) { - final org.bouncycastle.jce.spec.ECNamedCurveParameterSpec namedSpec = org.bouncycastle.jce.ECNamedCurveTable.getParameterSpec(curveName); - final ECCurve curve = namedSpec.getCurve(); - final EllipticCurve ecCurve = new EllipticCurve(// - new ECFieldFp(curve.getField().getCharacteristic()), // - curve.getA().toBigInteger(), // - curve.getB().toBigInteger()); - // 根据X恢复点Y - final ECPoint point = org.bouncycastle.jce.ECPointUtil.decodePoint(ecCurve, encodeByte); - - // 根据曲线恢复公钥格式 - java.security.spec.ECParameterSpec ecSpec = new org.bouncycastle.jce.spec.ECNamedCurveSpec(curveName, curve, namedSpec.getG(), namedSpec.getN()); - - final KeyFactory PubKeyGen = getKeyFactory("EC"); - try { - return PubKeyGen.generatePublic(new ECPublicKeySpec(point, ecSpec)); - } catch (GeneralSecurityException e) { - throw new CryptoException(e); - } + return BCUtil.decodeECPoint(encodeByte, curveName); } } diff --git a/hutool-crypto/src/main/java/cn/hutool/crypto/SmUtil.java b/hutool-crypto/src/main/java/cn/hutool/crypto/SmUtil.java index 3ae46a3288..76643c4352 100644 --- a/hutool-crypto/src/main/java/cn/hutool/crypto/SmUtil.java +++ b/hutool-crypto/src/main/java/cn/hutool/crypto/SmUtil.java @@ -1,8 +1,19 @@ package cn.hutool.crypto; import java.io.File; +import java.io.IOException; import java.io.InputStream; +import java.math.BigInteger; +import org.bouncycastle.asn1.ASN1EncodableVector; +import org.bouncycastle.asn1.ASN1Integer; +import org.bouncycastle.asn1.ASN1Sequence; +import org.bouncycastle.asn1.DERSequence; +import org.bouncycastle.crypto.params.ECDomainParameters; +import org.bouncycastle.util.Arrays; +import org.bouncycastle.util.encoders.Hex; + +import cn.hutool.core.io.IORuntimeException; import cn.hutool.crypto.asymmetric.SM2; import cn.hutool.crypto.digest.Digester; import cn.hutool.crypto.symmetric.SymmetricCrypto; @@ -16,9 +27,11 @@ */ public class SmUtil { + private final static int RS_LEN = 32; + private static String SM3 = "SM3"; private static String SM4 = "SM4"; - + /** * 创建SM2算法对象
* 生成新的私钥公钥对 @@ -96,7 +109,7 @@ public static String sm3(InputStream data) { public static String sm3(File dataFile) { return new Digester(SM3).digestHex(dataFile); } - + /** * SM4加密,生成随机KEY。注意解密时必须使用相同 {@link SymmetricCrypto}对象或者使用相同KEY
* 例: @@ -127,6 +140,109 @@ public static SymmetricCrypto sm4() { public static SymmetricCrypto sm4(byte[] key) { return new SymmetricCrypto(SM4, key); } - - + + /** + * bc加解密使用旧标c1||c2||c3,此方法在加密后调用,将结果转化为c1||c3||c2 + * + * @param c1c2c3 加密后的bytes,顺序为C1C2C3 + * @param ecDomainParameters {@link ECDomainParameters} + * @return 加密后的bytes,顺序为C1C3C2 + */ + public static byte[] changeC1C2C3ToC1C3C2(byte[] c1c2c3, ECDomainParameters ecDomainParameters) { + // sm2p256v1的这个固定65。可看GMNamedCurves、ECCurve代码。 + final int c1Len = (ecDomainParameters.getCurve().getFieldSize() + 7) / 8 * 2 + 1; + final int c3Len = 32; // new SM3Digest().getDigestSize(); + byte[] result = new byte[c1c2c3.length]; + System.arraycopy(c1c2c3, 0, result, 0, c1Len); // c1 + System.arraycopy(c1c2c3, c1c2c3.length - c3Len, result, c1Len, c3Len); // c3 + System.arraycopy(c1c2c3, c1Len, result, c1Len + c3Len, c1c2c3.length - c1Len - c3Len); // c2 + return result; + } + + /** + * bc加解密使用旧标c1||c3||c2,此方法在解密前调用,将密文转化为c1||c2||c3再去解密 + * + * @param c1c3c2 加密后的bytes,顺序为C1C3C2 + * @param ecDomainParameters {@link ECDomainParameters} + * @return c1c2c3 加密后的bytes,顺序为C1C2C3 + */ + public static byte[] changeC1C3C2ToC1C2C3(byte[] c1c3c2, ECDomainParameters ecDomainParameters) { + // sm2p256v1的这个固定65。可看GMNamedCurves、ECCurve代码。 + final int c1Len = (ecDomainParameters.getCurve().getFieldSize() + 7) / 8 * 2 + 1; + final int c3Len = 32; // new SM3Digest().getDigestSize(); + byte[] result = new byte[c1c3c2.length]; + System.arraycopy(c1c3c2, 0, result, 0, c1Len); // c1: 0->65 + System.arraycopy(c1c3c2, c1Len + c3Len, result, c1Len, c1c3c2.length - c1Len - c3Len); // c2 + System.arraycopy(c1c3c2, c1Len, result, c1c3c2.length - c3Len, c3Len); // c3 + return result; + } + + /** + * BC的SM3withSM2签名得到的结果的rs是asn1格式的,这个方法转化成直接拼接r||s
+ * 来自:https://blog.csdn.net/pridas/article/details/86118774 + * + * @param rsDer rs in asn1 format + * @return sign result in plain byte array + * @since 4.5.0 + */ + public static byte[] rsAsn1ToPlain(byte[] rsDer) { + ASN1Sequence seq = ASN1Sequence.getInstance(rsDer); + byte[] r = bigIntToFixexLengthBytes(ASN1Integer.getInstance(seq.getObjectAt(0)).getValue()); + byte[] s = bigIntToFixexLengthBytes(ASN1Integer.getInstance(seq.getObjectAt(1)).getValue()); + byte[] result = new byte[RS_LEN * 2]; + System.arraycopy(r, 0, result, 0, r.length); + System.arraycopy(s, 0, result, RS_LEN, s.length); + return result; + } + + /** + * BC的SM3withSM2验签需要的rs是asn1格式的,这个方法将直接拼接r||s的字节数组转化成asn1格式
+ * 来自:https://blog.csdn.net/pridas/article/details/86118774 + * + * @param sign in plain byte array + * @return rs result in asn1 format + * @since 4.5.0 + */ + public static byte[] rsPlainToAsn1(byte[] sign) { + if (sign.length != RS_LEN * 2) { + throw new CryptoException("err rs. "); + } + BigInteger r = new BigInteger(1, Arrays.copyOfRange(sign, 0, RS_LEN)); + BigInteger s = new BigInteger(1, Arrays.copyOfRange(sign, RS_LEN, RS_LEN * 2)); + ASN1EncodableVector v = new ASN1EncodableVector(); + v.add(new ASN1Integer(r)); + v.add(new ASN1Integer(s)); + try { + return new DERSequence(v).getEncoded("DER"); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + // -------------------------------------------------------------------------------------------------------- Private method start + /** + * BigInteger转固定长度bytes + * + * @param rOrS {@link BigInteger} + * @return 固定长度bytes + * @since 4.5.0 + */ + private static byte[] bigIntToFixexLengthBytes(BigInteger rOrS) { + // for sm2p256v1, n is 00fffffffeffffffffffffffffffffffff7203df6b21c6052b53bbf40939d54123, + // r and s are the result of mod n, so they should be less than n and have length<=32 + byte[] rs = rOrS.toByteArray(); + if (rs.length == RS_LEN) { + return rs; + } else if (rs.length == RS_LEN + 1 && rs[0] == 0) { + return Arrays.copyOfRange(rs, 1, RS_LEN + 1); + } else if (rs.length < RS_LEN) { + byte[] result = new byte[RS_LEN]; + Arrays.fill(result, (byte) 0); + System.arraycopy(rs, 0, result, RS_LEN - rs.length, rs.length); + return result; + } else { + throw new CryptoException("Error rs: {}", Hex.toHexString(rs)); + } + } + // -------------------------------------------------------------------------------------------------------- Private method end } diff --git a/hutool-crypto/src/main/java/cn/hutool/crypto/asymmetric/SM2.java b/hutool-crypto/src/main/java/cn/hutool/crypto/asymmetric/SM2.java index f151613669..071f40304d 100644 --- a/hutool-crypto/src/main/java/cn/hutool/crypto/asymmetric/SM2.java +++ b/hutool-crypto/src/main/java/cn/hutool/crypto/asymmetric/SM2.java @@ -5,7 +5,8 @@ import java.security.PublicKey; import org.bouncycastle.crypto.CipherParameters; -import org.bouncycastle.crypto.engines.SM2Engine; +import org.bouncycastle.crypto.params.ECPrivateKeyParameters; +import org.bouncycastle.crypto.params.ECPublicKeyParameters; import org.bouncycastle.crypto.params.ParametersWithID; import org.bouncycastle.crypto.params.ParametersWithRandom; import org.bouncycastle.crypto.signers.SM2Signer; @@ -13,10 +14,12 @@ import cn.hutool.crypto.CryptoException; import cn.hutool.crypto.SecureUtil; +import cn.hutool.crypto.asymmetric.SM2Engine.SM2Mode; /** * 国密SM2算法实现,基于BC库
- * SM2算法只支持公钥加密,私钥解密 + * SM2算法只支持公钥加密,私钥解密
+ * 参考:https://blog.csdn.net/pridas/article/details/86118774 * * @author looly * @since 4.3.2 @@ -29,6 +32,10 @@ public class SM2 extends AbstractAsymmetricCrypto{ protected SM2Engine engine; protected SM2Signer signer; + private SM2Mode mode; + private ECPublicKeyParameters publicKeyParams; + private ECPrivateKeyParameters privateKeyParams; + // ------------------------------------------------------------------ Constructor start /** * 构造,生成新的私钥公钥对 @@ -90,9 +97,21 @@ public SM2 init(PrivateKey privateKey, PublicKey publicKey) { return this.init(ALGORITHM_SM2, privateKey, publicKey); } + @Override + protected SM2 init(String algorithm, PrivateKey privateKey, PublicKey publicKey) { + super.init(algorithm, privateKey, publicKey); + return initCipherParams(); + } + // --------------------------------------------------------------------------------- Encrypt /** - * 加密 + * 加密,SM2非对称加密的结果由C1,C2,C3三部分组成,其中: + * + * + * C1 生成随机数的计算出的椭圆曲线点 + * C2 密文数据 + * C3 SM3的摘要值 + ** * @param data 被加密的bytes * @param keyType 私钥或公钥 {@link KeyType} @@ -101,16 +120,16 @@ public SM2 init(PrivateKey privateKey, PublicKey publicKey) { */ @Override public byte[] encrypt(byte[] data, KeyType keyType) throws CryptoException { - lock.lock(); - if (null == this.engine) { - this.engine = new SM2Engine(); + if (KeyType.PublicKey != keyType) { + throw new IllegalArgumentException("Encrypt is only support by public key"); } - final SM2Engine engine = this.engine; + ckeckKey(keyType); + + lock.lock(); + final SM2Engine engine = getEngine(); try { - engine.init(true, new ParametersWithRandom(generateCipherParameters(keyType))); + engine.init(true, new ParametersWithRandom(getCipherParameters(keyType))); return engine.processBlock(data, 0, data.length); - } catch (Exception e) { - throw new CryptoException(e); } finally { lock.unlock(); } @@ -127,16 +146,16 @@ public byte[] encrypt(byte[] data, KeyType keyType) throws CryptoException { */ @Override public byte[] decrypt(byte[] data, KeyType keyType) throws CryptoException { - lock.lock(); - if (null == this.engine) { - this.engine = new SM2Engine(); + if (KeyType.PrivateKey != keyType) { + throw new IllegalArgumentException("Decrypt is only support by private key"); } - final SM2Engine engine = this.engine; + ckeckKey(keyType); + + lock.lock(); + final SM2Engine engine = getEngine(); try { - engine.init(false, generateCipherParameters(keyType)); + engine.init(false, getCipherParameters(keyType)); return engine.processBlock(data, 0, data.length); - } catch (Exception e) { - throw new CryptoException(e); } finally { lock.unlock(); } @@ -152,7 +171,7 @@ public byte[] decrypt(byte[] data, KeyType keyType) throws CryptoException { public byte[] sign(byte[] data) { return sign(data, null); } - + /** * 用私钥对信息生成数字签名 * @@ -162,12 +181,9 @@ public byte[] sign(byte[] data) { */ public byte[] sign(byte[] data, byte[] id) { lock.lock(); - if (null == this.signer) { - this.signer = new SM2Signer(); - } - final SM2Signer signer = this.signer; + final SM2Signer signer = getSigner(); try { - CipherParameters param = new ParametersWithRandom(generateCipherParameters(KeyType.PrivateKey)); + CipherParameters param = new ParametersWithRandom(getCipherParameters(KeyType.PrivateKey)); if (id != null) { param = new ParametersWithID(param, id); } @@ -180,7 +196,7 @@ public byte[] sign(byte[] data, byte[] id) { lock.unlock(); } } - + /** * 用公钥检验数字签名的合法性 * @@ -202,12 +218,9 @@ public boolean verify(byte[] data, byte[] sign) { */ public boolean verify(byte[] data, byte[] sign, byte[] id) { lock.lock(); - if (null == this.signer) { - this.signer = new SM2Signer(); - } - final SM2Signer signer = this.signer; + final SM2Signer signer = getSigner(); try { - CipherParameters param = generateCipherParameters(KeyType.PublicKey); + CipherParameters param = getCipherParameters(KeyType.PublicKey); if (id != null) { param = new ParametersWithID(param, id); } @@ -221,25 +234,101 @@ public boolean verify(byte[] data, byte[] sign, byte[] id) { } } + /** + * 设置加密类型 + * + * @param mode {@link SM2Mode} + * @return this + */ + public SM2 setMode(SM2Mode mode) { + this.mode = mode; + if (null != this.engine) { + this.engine.setMode(mode); + } + return this; + } + // ------------------------------------------------------------------------------------------------------------------------- Private method start /** - * 生成{@link CipherParameters} + * 初始化加密解密参数 * - * @param keyType Key类型枚举,包括私钥或公钥 - * @return {@link CipherParameters} + * @return this */ - private CipherParameters generateCipherParameters(KeyType keyType) { + private SM2 initCipherParams() { try { - switch (keyType) { - case PublicKey: - return ECUtil.generatePublicKeyParameter(this.publicKey); - case PrivateKey: - return ECUtil.generatePrivateKeyParameter(this.privateKey); + if (null != this.publicKey) { + this.publicKeyParams = (ECPublicKeyParameters) ECUtil.generatePublicKeyParameter(this.publicKey); + } + if (null != privateKey) { + this.privateKeyParams = (ECPrivateKeyParameters) ECUtil.generatePrivateKeyParameter(this.privateKey); } } catch (InvalidKeyException e) { throw new CryptoException(e); } + + return this; + } + + /** + * 获取密钥类型对应的加密参数对象{@link CipherParameters} + * + * @param keyType Key类型枚举,包括私钥或公钥 + * @return {@link CipherParameters} + */ + private CipherParameters getCipherParameters(KeyType keyType) { + switch (keyType) { + case PublicKey: + return this.publicKeyParams; + case PrivateKey: + return this.privateKeyParams; + } + return null; } + + /** + * 检查对应类型的Key是否存在 + * + * @param keyType key类型 + */ + private void ckeckKey(KeyType keyType) { + switch (keyType) { + case PublicKey: + if (null == this.publicKey) { + throw new NullPointerException("No public key provided"); + } + break; + case PrivateKey: + if (null == this.privateKey) { + throw new NullPointerException("No private key provided"); + } + break; + } + } + + /** + * 获取{@link SM2Engine} + * + * @return {@link SM2Engine} + */ + private SM2Engine getEngine() { + if (null == this.engine) { + this.engine = new SM2Engine(this.mode); + } + return this.engine; + } + + /** + * 获取{@link SM2Signer} + * + * @return {@link SM2Signer} + */ + private SM2Signer getSigner() { + if (null == this.signer) { + this.signer = new SM2Signer(); + } + return this.signer; + } + // ------------------------------------------------------------------------------------------------------------------------- Private method end } diff --git a/hutool-crypto/src/main/java/cn/hutool/crypto/asymmetric/SM2Engine.java b/hutool-crypto/src/main/java/cn/hutool/crypto/asymmetric/SM2Engine.java new file mode 100644 index 0000000000..91a6e1722c --- /dev/null +++ b/hutool-crypto/src/main/java/cn/hutool/crypto/asymmetric/SM2Engine.java @@ -0,0 +1,359 @@ +package cn.hutool.crypto.asymmetric; + +import java.math.BigInteger; +import java.util.Random; + +import org.bouncycastle.crypto.CipherParameters; +import org.bouncycastle.crypto.CryptoServicesRegistrar; +import org.bouncycastle.crypto.Digest; +import org.bouncycastle.crypto.digests.SM3Digest; +import org.bouncycastle.crypto.params.ECDomainParameters; +import org.bouncycastle.crypto.params.ECKeyParameters; +import org.bouncycastle.crypto.params.ECPrivateKeyParameters; +import org.bouncycastle.crypto.params.ECPublicKeyParameters; +import org.bouncycastle.crypto.params.ParametersWithRandom; +import org.bouncycastle.math.ec.ECConstants; +import org.bouncycastle.math.ec.ECFieldElement; +import org.bouncycastle.math.ec.ECMultiplier; +import org.bouncycastle.math.ec.ECPoint; +import org.bouncycastle.math.ec.FixedPointCombMultiplier; +import org.bouncycastle.util.Arrays; +import org.bouncycastle.util.BigIntegers; +import org.bouncycastle.util.Memoable; +import org.bouncycastle.util.Pack; + +import cn.hutool.core.util.ObjectUtil; +import cn.hutool.crypto.CryptoException; + +/** + * SM2加密解密引擎,来自Bouncy Castle库的SM2Engine类改造
+ * SM2加密后的数据格式为(两种模式): + * + *+ * curve(C1) | data(C2) | digest(C3) + * curve(C1) | digest(C3) | data(C2) + *+ * + * @author looly, bouncycastle + * @since 4.5.0 + */ +public class SM2Engine { + + private final Digest digest; + + private boolean forEncryption; + private ECKeyParameters ecKey; + private ECDomainParameters ecParams; + private int curveLength; + private Random random; + /** 加密解密模式 */ + private SM2Mode mode; + + /** + * 构造 + */ + public SM2Engine() { + this(new SM3Digest()); + } + + /** + * 构造 + * + * @param mode SM2密钥生成模式,可选C1C2C3和C1C3C2 + */ + public SM2Engine(SM2Mode mode) { + this(new SM3Digest(), mode); + } + + /** + * 构造 + * + * @param digest 摘要算法啊 + */ + public SM2Engine(Digest digest) { + this(digest, null); + } + + /** + * 构造 + * + * @param digest 摘要算法啊 + * @param mode SM2密钥生成模式,可选C1C2C3和C1C3C2 + */ + public SM2Engine(Digest digest, SM2Mode mode) { + this.digest = digest; + this.mode = ObjectUtil.defaultIfNull(mode, SM2Mode.C1C2C3); + } + + /** + * 初始化引擎 + * + * @param forEncryption 是否为加密模式 + * @param param {@link CipherParameters},此处应为{@link ParametersWithRandom}(加密时)或{@link ECKeyParameters}(解密时) + */ + public void init(boolean forEncryption, CipherParameters param) { + this.forEncryption = forEncryption; + + if (param instanceof ParametersWithRandom) { + final ParametersWithRandom rParam = (ParametersWithRandom) param; + this.ecKey = (ECKeyParameters) rParam.getParameters(); + this.random = rParam.getRandom(); + } else { + this.ecKey = (ECKeyParameters) param; + } + this.ecParams = this.ecKey.getParameters(); + + if (forEncryption) { + // 检查曲线点 + final ECPoint ecPoint = ((ECPublicKeyParameters) ecKey).getQ().multiply(ecParams.getH()); + if (ecPoint.isInfinity()) { + throw new IllegalArgumentException("invalid key: [h]Q at infinity"); + } + + // 检查随机参数 + if (null == this.random) { + this.random = CryptoServicesRegistrar.getSecureRandom(); + } + } + + // 曲线位长度 + this.curveLength = (this.ecParams.getCurve().getFieldSize() + 7) / 8; + } + + /** + * 处理块,包括加密和解密 + * + * @param in 数据 + * @param inOff 数据开始位置 + * @param inLen 数据长度 + * @return 结果 + */ + public byte[] processBlock(byte[] in, int inOff, int inLen) { + if (forEncryption) { + return encrypt(in, inOff, inLen); + } else { + return decrypt(in, inOff, inLen); + } + } + + /** + * 设置加密类型 + * + * @param mode {@link SM2Mode} + * @return this + */ + public SM2Engine setMode(SM2Mode mode) { + this.mode = mode; + return this; + } + + /** + * SM2算法模式
+ * 在SM2算法中,C1C2C3为旧标准模式,C1C3C2为新标准模式 + * + * @author looly + * + */ + public static enum SM2Mode { + C1C2C3, C1C3C2; + } + + protected ECMultiplier createBasePointMultiplier() { + return new FixedPointCombMultiplier(); + } + + // --------------------------------------------------------------------------------------------------- Private method start + /** + * 加密 + * + * @param in 数据 + * @param inOff 位置 + * @param inLen 长度 + * @return 密文 + */ + private byte[] encrypt(byte[] in, int inOff, int inLen) { + // 加密数据 + byte[] c2 = new byte[inLen]; + System.arraycopy(in, inOff, c2, 0, c2.length); + + final ECMultiplier multiplier = createBasePointMultiplier(); + + byte[] c1; + ECPoint kPB; + BigInteger k; + do { + k = nextK(); + // 产生随机数计算出曲线点C1 + c1 = multiplier.multiply(ecParams.getG(), k).normalize().getEncoded(false); + kPB = ((ECPublicKeyParameters) ecKey).getQ().multiply(k).normalize(); + kdf(kPB, c2); + } while (notEncrypted(c2, in, inOff)); + + // 杂凑值,效验数据 + byte[] c3 = new byte[digest.getDigestSize()]; + + addFieldElement(kPB.getAffineXCoord()); + this.digest.update(in, inOff, inLen); + addFieldElement(kPB.getAffineYCoord()); + + this.digest.doFinal(c3, 0); + + // 按照莫属输出结果 + switch (mode) { + case C1C3C2: + return Arrays.concatenate(c1, c3, c2); + default: + return Arrays.concatenate(c1, c2, c3); + } + } + + /** + * 解密,只支持私钥解密 + * + * @param in 密文 + * @param inOff 位置 + * @param inLen 长度 + * @return 解密后的内容 + */ + private byte[] decrypt(byte[] in, int inOff, int inLen) { + // 获取曲线点 + final byte[] c1 = new byte[this.curveLength * 2 + 1]; + System.arraycopy(in, inOff, c1, 0, c1.length); + + ECPoint c1P = this.ecParams.getCurve().decodePoint(c1); + if (c1P.multiply(this.ecParams.getH()).isInfinity()) { + throw new CryptoException("[h]C1 at infinity"); + } + c1P = c1P.multiply(((ECPrivateKeyParameters) ecKey).getD()).normalize(); + + final int digestSize = this.digest.getDigestSize(); + + // 解密C2数据 + final byte[] c2 = new byte[inLen - c1.length - digestSize]; + + if (SM2Mode.C1C3C2 == this.mode) { + // C2位于第三部分 + System.arraycopy(in, inOff + c1.length + digestSize, c2, 0, c2.length); + } else { + // C2位于第二部分 + System.arraycopy(in, inOff + c1.length, c2, 0, c2.length); + } + kdf(c1P, c2); + + // 使用摘要验证C2数据 + final byte[] c3 = new byte[digestSize]; + + addFieldElement(c1P.getAffineXCoord()); + this.digest.update(c2, 0, c2.length); + addFieldElement(c1P.getAffineYCoord()); + this.digest.doFinal(c3, 0); + + int check = 0; + for (int i = 0; i != c3.length; i++) { + check |= c3[i] ^ in[inOff + c1.length + ((SM2Mode.C1C3C2 == this.mode) ? 0 : c2.length) + i]; + } + + Arrays.fill(c1, (byte) 0); + Arrays.fill(c3, (byte) 0); + + if (check != 0) { + Arrays.fill(c2, (byte) 0); + throw new CryptoException("invalid cipher text"); + } + + return c2; + } + + private boolean notEncrypted(byte[] encData, byte[] in, int inOff) { + for (int i = 0; i != encData.length; i++) { + if (encData[i] != in[inOff]) { + return false; + } + } + return true; + } + + /** + * 解密数据 + * + * @param c1 c1点 + * @param encData 密文 + */ + private void kdf(ECPoint c1, byte[] encData) { + final Digest digest = this.digest; + int digestSize = digest.getDigestSize(); + byte[] buf = new byte[Math.max(4, digestSize)]; + int off = 0; + + Memoable memo = null; + Memoable copy = null; + + if (digest instanceof Memoable) { + addFieldElement(c1.getAffineXCoord()); + addFieldElement(c1.getAffineYCoord()); + memo = (Memoable) digest; + copy = memo.copy(); + } + + int ct = 0; + + while (off < encData.length) { + if (memo != null) { + memo.reset(copy); + } else { + addFieldElement(c1.getAffineXCoord()); + addFieldElement(c1.getAffineYCoord()); + } + + Pack.intToBigEndian(++ct, buf, 0); + digest.update(buf, 0, 4); + digest.doFinal(buf, 0); + + int xorLen = Math.min(digestSize, encData.length - off); + xor(encData, buf, off, xorLen); + off += xorLen; + } + } + + /** + * 异或 + * + * @param data 数据 + * @param kdfOut kdf输出值 + * @param dOff d偏移 + * @param dRemaining d剩余 + */ + private void xor(byte[] data, byte[] kdfOut, int dOff, int dRemaining) { + for (int i = 0; i != dRemaining; i++) { + data[dOff + i] ^= kdfOut[i]; + } + } + + /** + * 下一个K值 + * + * @return K值 + */ + private BigInteger nextK() { + final int qBitLength = this.ecParams.getN().bitLength(); + + BigInteger k; + do { + k = new BigInteger(qBitLength, this.random); + } while (k.equals(ECConstants.ZERO) || k.compareTo(this.ecParams.getN()) >= 0); + + return k; + } + + /** + * 增加字段节点 + * + * @param digest + * @param v + */ + private void addFieldElement(ECFieldElement v) { + final byte[] p = BigIntegers.asUnsignedByteArray(this.curveLength, v.toBigInteger()); + this.digest.update(p, 0, p.length); + } + // --------------------------------------------------------------------------------------------------- Private method start +} diff --git a/hutool-crypto/src/main/java/cn/hutool/crypto/digest/Digester.java b/hutool-crypto/src/main/java/cn/hutool/crypto/digest/Digester.java index 6be4111b6e..45f24c91f6 100644 --- a/hutool-crypto/src/main/java/cn/hutool/crypto/digest/Digester.java +++ b/hutool-crypto/src/main/java/cn/hutool/crypto/digest/Digester.java @@ -168,17 +168,17 @@ public String digestHex(File file) { * @return 摘要bytes */ public byte[] digest(byte[] data) { - if(null != this.salt) { + if (null != this.salt) { digest.update(this.salt); } int digestCount = Math.max(1, this.digestCount); byte[] result = data; - for(int i = 0; i < digestCount; i++) { + for (int i = 0; i < digestCount; i++) { result = doDigest(result); } return result; } - + /** * 生成摘要 * @@ -278,4 +278,14 @@ public String digestHex(InputStream data, int bufferLength) { public MessageDigest getDigest() { return digest; } + + /** + * 获取散列长度,0表示不支持此方法 + * + * @return 散列长度,0表示不支持此方法 + * @since 4.5.0 + */ + public int getDigestLength() { + return this.digest.getDigestLength(); + } } diff --git a/hutool-crypto/src/test/java/cn/hutool/crypto/test/SM2Test.java b/hutool-crypto/src/test/java/cn/hutool/crypto/test/SM2Test.java index e213bb3194..3345b6b79e 100644 --- a/hutool-crypto/src/test/java/cn/hutool/crypto/test/SM2Test.java +++ b/hutool-crypto/src/test/java/cn/hutool/crypto/test/SM2Test.java @@ -15,6 +15,7 @@ import cn.hutool.crypto.SmUtil; import cn.hutool.crypto.asymmetric.KeyType; import cn.hutool.crypto.asymmetric.SM2; +import cn.hutool.crypto.asymmetric.SM2Engine.SM2Mode; /** * SM2算法单元测试 @@ -30,11 +31,9 @@ public void generateKeyPairTest() { Assert.assertNotNull(pair.getPrivate()); Assert.assertNotNull(pair.getPublic()); } - + @Test public void KeyPairOIDTest() { - new SM2();//保证BC被加载 - // OBJECT IDENTIFIER 1.2.156.10197.1.301 String OID = "06082A811CCF5501822D"; KeyPair pair = SecureUtil.generateKeyPair("SM2"); @@ -49,6 +48,7 @@ public void sm2CustomKeyTest() { byte[] publicKey = pair.getPublic().getEncoded(); SM2 sm2 = SmUtil.sm2(privateKey, publicKey); + sm2.setMode(SM2Mode.C1C3C2); // 公钥加密,私钥解密 byte[] encrypt = sm2.encrypt(StrUtil.bytes("我是一段测试aaaa", CharsetUtil.CHARSET_UTF_8), KeyType.PublicKey); @@ -126,7 +126,7 @@ public void sm2SignAndVerifyUseKeyTest() { boolean verify = sm2.verify(content.getBytes(), sign); Assert.assertTrue(verify); } - + @Test public void sm2PublicKeyEncodeDecodeTest() { KeyPair pair = SecureUtil.generateKeyPair("SM2"); @@ -139,5 +139,4 @@ public void sm2PublicKeyEncodeDecodeTest() { Assert.assertEquals(HexUtil.encodeHexStr(publicKey.getEncoded()), HexUtil.encodeHexStr(Hexdecode.getEncoded())); Assert.assertEquals(HexUtil.encodeHexStr(publicKey.getEncoded()), HexUtil.encodeHexStr(B64decode.getEncoded())); } - } diff --git a/hutool-db/pom.xml b/hutool-db/pom.xml index eb5ce7ef9c..53d44551a7 100644 --- a/hutool-db/pom.xml +++ b/hutool-db/pom.xml @@ -9,7 +9,7 @@cn.hutool hutool-parent -4.4.5 +4.5.0 hutool-db diff --git a/hutool-dfa/pom.xml b/hutool-dfa/pom.xml index 53fe3ee544..59b9f2158f 100644 --- a/hutool-dfa/pom.xml +++ b/hutool-dfa/pom.xml @@ -7,7 +7,7 @@cn.hutool hutool-parent -4.4.5 +4.5.0 hutool-dfa diff --git a/hutool-extra/pom.xml b/hutool-extra/pom.xml index 6e6e3c9312..146ae0ab4a 100644 --- a/hutool-extra/pom.xml +++ b/hutool-extra/pom.xml @@ -9,7 +9,7 @@cn.hutool hutool-parent -4.4.5 +4.5.0 hutool-extra diff --git a/hutool-extra/src/main/java/cn/hutool/extra/ftp/Ftp.java b/hutool-extra/src/main/java/cn/hutool/extra/ftp/Ftp.java index c0f47ed00d..354b62628f 100644 --- a/hutool-extra/src/main/java/cn/hutool/extra/ftp/Ftp.java +++ b/hutool-extra/src/main/java/cn/hutool/extra/ftp/Ftp.java @@ -5,13 +5,13 @@ import java.io.InputStream; import java.io.OutputStream; import java.nio.charset.Charset; +import java.util.ArrayList; import java.util.List; import org.apache.commons.net.ftp.FTPClient; import org.apache.commons.net.ftp.FTPFile; import org.apache.commons.net.ftp.FTPReply; -import cn.hutool.core.collection.CollUtil; import cn.hutool.core.io.FileUtil; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.ArrayUtil; @@ -164,11 +164,18 @@ public String pwd() { @Override public Listls(String path) { + FTPFile[] ftpFiles; try { - return CollUtil.toList(this.client.listNames(path)); + ftpFiles = this.client.listFiles(); } catch (IOException e) { throw new FtpException(e); } + + final List fileNames = new ArrayList<>(); + for (FTPFile ftpFile : ftpFiles) { + fileNames.add(ftpFile.getName()); + } + return fileNames; } @Override diff --git a/hutool-extra/src/main/java/cn/hutool/extra/qrcode/QrCodeUtil.java b/hutool-extra/src/main/java/cn/hutool/extra/qrcode/QrCodeUtil.java index 1db552e9a0..aab6b15092 100644 --- a/hutool-extra/src/main/java/cn/hutool/extra/qrcode/QrCodeUtil.java +++ b/hutool-extra/src/main/java/cn/hutool/extra/qrcode/QrCodeUtil.java @@ -180,10 +180,10 @@ public static BufferedImage generate(String content, BarcodeFormat format, QrCon int height; // 按照最短的边做比例缩放 if (qrWidth < qrHeight) { - width = qrWidth / 6; + width = qrWidth / config.ratio; height = logoImg.getHeight(null) * width / logoImg.getWidth(null); } else { - height = qrHeight / 6; + height = qrHeight / config.ratio; width = logoImg.getWidth(null) * height / logoImg.getHeight(null); } ImageUtil.pressImage(image, logoImg, new Rectangle(width, height), 1); diff --git a/hutool-extra/src/main/java/cn/hutool/extra/qrcode/QrConfig.java b/hutool-extra/src/main/java/cn/hutool/extra/qrcode/QrConfig.java index 8652553c16..3cf990bf2c 100644 --- a/hutool-extra/src/main/java/cn/hutool/extra/qrcode/QrConfig.java +++ b/hutool-extra/src/main/java/cn/hutool/extra/qrcode/QrConfig.java @@ -34,13 +34,13 @@ public class QrConfig { /** 边距1~4 */ protected Integer margin = 2; /** 纠错级别 */ - protected ErrorCorrectionLevel errorCorrection = ErrorCorrectionLevel.L; + protected ErrorCorrectionLevel errorCorrection = ErrorCorrectionLevel.M; /** 编码 */ protected Charset charset = CharsetUtil.CHARSET_UTF_8; /** 二维码中的Logo */ protected Image img; /** 二维码中的Logo缩放的比例系数,如5表示长宽最小值的1/5 */ - protected int ratio; + protected int ratio = 6; /** * 创建QrConfig diff --git a/hutool-extra/src/test/java/cn/hutool/extra/qrcode/QrCodeUtilTest.java b/hutool-extra/src/test/java/cn/hutool/extra/qrcode/QrCodeUtilTest.java index eb5637778f..e5af44cdd9 100644 --- a/hutool-extra/src/test/java/cn/hutool/extra/qrcode/QrCodeUtilTest.java +++ b/hutool-extra/src/test/java/cn/hutool/extra/qrcode/QrCodeUtilTest.java @@ -45,9 +45,9 @@ public void generateWithLogoTest() { } @Test - @Ignore +// @Ignore public void decodeTest() { - String decode = QrCodeUtil.decode(FileUtil.file("e:/1.png")); + String decode = QrCodeUtil.decode(FileUtil.file("e:/pic/qr.png")); Console.log(decode); } } diff --git a/hutool-http/pom.xml b/hutool-http/pom.xml index cadf2948ff..d479e3e79f 100644 --- a/hutool-http/pom.xml +++ b/hutool-http/pom.xml @@ -7,7 +7,7 @@ cn.hutool hutool-parent -4.4.5 +4.5.0 hutool-http diff --git a/hutool-http/src/main/java/cn/hutool/http/HttpRequest.java b/hutool-http/src/main/java/cn/hutool/http/HttpRequest.java index 5904b86fdc..78a0e07d69 100644 --- a/hutool-http/src/main/java/cn/hutool/http/HttpRequest.java +++ b/hutool-http/src/main/java/cn/hutool/http/HttpRequest.java @@ -790,6 +790,18 @@ public HttpRequest setSSLProtocol(String protocol) { return this; } + /** + * 设置是否rest模式 + * + * @param isRest 是否rest模式 + * @return this + * @since 4.5.0 + */ + public HttpRequest setRest(boolean isRest) { + this.isRest = isRest; + return this; + } + /** * 执行Reuqest请求 * @@ -928,7 +940,7 @@ private HttpResponse sendRedirectIfPosible() { */ private void send() throws HttpException { try { - if (Method.POST.equals(this.method) || Method.PUT.equals(this.method) || this.isRest) { + if (Method.POST.equals(this.method) || Method.PUT.equals(this.method) || Method.DELETE.equals(this.method) || this.isRest) { if (CollectionUtil.isEmpty(this.fileForm)) { sendFormUrlEncoded();// 普通表单 } else { diff --git a/hutool-json/pom.xml b/hutool-json/pom.xml index 74aaa17b28..e7a7f74ad0 100644 --- a/hutool-json/pom.xml +++ b/hutool-json/pom.xml @@ -8,7 +8,7 @@cn.hutool hutool-parent -4.4.5 +4.5.0 hutool-json diff --git a/hutool-log/pom.xml b/hutool-log/pom.xml index 278fcf1c1e..b86c8aa52a 100644 --- a/hutool-log/pom.xml +++ b/hutool-log/pom.xml @@ -9,7 +9,7 @@cn.hutool hutool-parent -4.4.5 +4.5.0 hutool-log diff --git a/hutool-poi/pom.xml b/hutool-poi/pom.xml index 6f05ba2bef..508cf0f173 100644 --- a/hutool-poi/pom.xml +++ b/hutool-poi/pom.xml @@ -8,7 +8,7 @@cn.hutool hutool-parent -4.4.5 +4.5.0 hutool-poi diff --git a/hutool-script/pom.xml b/hutool-script/pom.xml index bf50b4bf32..c16b9086ae 100644 --- a/hutool-script/pom.xml +++ b/hutool-script/pom.xml @@ -7,7 +7,7 @@cn.hutool hutool-parent -4.4.5 +4.5.0 hutool-script diff --git a/hutool-setting/pom.xml b/hutool-setting/pom.xml index e6b4d71c43..4afce6ab45 100644 --- a/hutool-setting/pom.xml +++ b/hutool-setting/pom.xml @@ -7,7 +7,7 @@cn.hutool hutool-parent -4.4.5 +4.5.0 hutool-setting diff --git a/hutool-socket/pom.xml b/hutool-socket/pom.xml new file mode 100644 index 0000000000..a651326c8d --- /dev/null +++ b/hutool-socket/pom.xml @@ -0,0 +1,31 @@ + ++ diff --git a/hutool-socket/src/main/java/cn/hutool/socket/SocketConfig.java b/hutool-socket/src/main/java/cn/hutool/socket/SocketConfig.java new file mode 100644 index 0000000000..1b12d32aef --- /dev/null +++ b/hutool-socket/src/main/java/cn/hutool/socket/SocketConfig.java @@ -0,0 +1,114 @@ +package cn.hutool.socket; + +import cn.hutool.core.io.IoUtil; + +/** + * Socket通讯配置 + * + * @author looly + * + */ +public class SocketConfig { + + /** CPU核心数 */ + private static int CPU_COUNT = Runtime.getRuntime().availableProcessors(); + + /** 共享线程池大小,此线程池用于接收和处理用户连接 */ + private int threadPoolSize = CPU_COUNT; + + /** 读取超时时长,小于等于0表示默认 */ + private long readTimeout; + /** 写出超时时长,小于等于0表示默认 */ + private long writeTimeout; + + /** 读取缓存大小 */ + private int readBufferSize = IoUtil.DEFAULT_BUFFER_SIZE; + /** 写出缓存大小 */ + private int writeBufferSize = IoUtil.DEFAULT_BUFFER_SIZE; + + /** + * 获取共享线程池大小,此线程池用于接收和处理用户连接 + * + * @return 共享线程池大小,此线程池用于接收和处理用户连接 + */ + public int getThreadPoolSize() { + return threadPoolSize; + } + + /** + * 设置共享线程池大小,此线程池用于接收和处理用户连接 + * + * @param threadPoolSize 共享线程池大小,此线程池用于接收和处理用户连接 + */ + public void setThreadPoolSize(int threadPoolSize) { + this.threadPoolSize = threadPoolSize; + } + + /** + * 获取读取超时时长,小于等于0表示默认 + * + * @return 读取超时时长,小于等于0表示默认 + */ + public long getReadTimeout() { + return readTimeout; + } + + /** + * 设置读取超时时长,小于等于0表示默认 + * + * @param readTimeout 读取超时时长,小于等于0表示默认 + */ + public void setReadTimeout(long readTimeout) { + this.readTimeout = readTimeout; + } + + /** + * 获取写出超时时长,小于等于0表示默认 + * + * @return 写出超时时长,小于等于0表示默认 + */ + public long getWriteTimeout() { + return writeTimeout; + } + + /** + * 设置写出超时时长,小于等于0表示默认 + * + * @param writeTimeout 写出超时时长,小于等于0表示默认 + */ + public void setWriteTimeout(long writeTimeout) { + this.writeTimeout = writeTimeout; + } + + /** + * 获取读取缓存大小 + * @return 读取缓存大小 + */ + public int getReadBufferSize() { + return readBufferSize; + } + + /** + * 设置读取缓存大小 + * @param readBufferSize 读取缓存大小 + */ + public void setReadBufferSize(int readBufferSize) { + this.readBufferSize = readBufferSize; + } + + /** + * 获取写出缓存大小 + * @return 写出缓存大小 + */ + public int getWriteBufferSize() { + return writeBufferSize; + } + + /** + * 设置写出缓存大小 + * @param writeBufferSize 写出缓存大小 + */ + public void setWriteBufferSize(int writeBufferSize) { + this.writeBufferSize = writeBufferSize; + } +} diff --git a/hutool-socket/src/main/java/cn/hutool/socket/SocketRuntimeException.java b/hutool-socket/src/main/java/cn/hutool/socket/SocketRuntimeException.java new file mode 100644 index 0000000000..0c0baf63ba --- /dev/null +++ b/hutool-socket/src/main/java/cn/hutool/socket/SocketRuntimeException.java @@ -0,0 +1,33 @@ +package cn.hutool.socket; + +import cn.hutool.core.exceptions.ExceptionUtil; +import cn.hutool.core.util.StrUtil; + +/** + * Socket异常 + * + * @author xiaoleilu + */ +public class SocketRuntimeException extends RuntimeException { + private static final long serialVersionUID = 8247610319171014183L; + + public SocketRuntimeException(Throwable e) { + super(ExceptionUtil.getMessage(e), e); + } + + public SocketRuntimeException(String message) { + super(message); + } + + public SocketRuntimeException(String messageTemplate, Object... params) { + super(StrUtil.format(messageTemplate, params)); + } + + public SocketRuntimeException(String message, Throwable throwable) { + super(message, throwable); + } + + public SocketRuntimeException(Throwable throwable, String messageTemplate, Object... params) { + super(StrUtil.format(messageTemplate, params), throwable); + } +} diff --git a/hutool-socket/src/main/java/cn/hutool/socket/SocketUtil.java b/hutool-socket/src/main/java/cn/hutool/socket/SocketUtil.java new file mode 100644 index 0000000000..0cd856ebb6 --- /dev/null +++ b/hutool-socket/src/main/java/cn/hutool/socket/SocketUtil.java @@ -0,0 +1,46 @@ +package cn.hutool.socket; + +import java.io.IOException; +import java.net.SocketAddress; +import java.nio.channels.AsynchronousSocketChannel; +import java.nio.channels.ClosedChannelException; + +import cn.hutool.core.io.IORuntimeException; + +/** + * Socket相关工具类 + * + * @author looly + * @since 4.5.0 + */ +public class SocketUtil { + + /** + * 获取远程端的地址信息,包括host和端口4.0.0 + +jar + ++ + +cn.hutool +hutool-parent +4.5.0 +hutool-socket +${project.artifactId} +Hutool套接字,包括BIO、NIO、AIO封装 + ++ ++ +cn.hutool +hutool-core +${project.parent.version} ++ +cn.hutool +hutool-log +${project.parent.version} +
+ * null表示channel为null或者远程主机未连接 + * + * @param channel {@link AsynchronousSocketChannel} + * @return 远程端的地址信息,包括host和端口,null表示channel为null或者远程主机未连接 + */ + public static SocketAddress getRemoteAddress(AsynchronousSocketChannel channel) { + try { + return (null == channel) ? null : channel.getRemoteAddress(); + } catch (ClosedChannelException e) { + // Channel未打开或已关闭,返回null表示未连接 + return null; + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 远程主机是否处于连接状态
+ * 通过判断远程地址获取成功与否判断 + * + * @param channel {@link AsynchronousSocketChannel} + * @return 远程主机是否处于连接状态 + */ + public static boolean isConnected(AsynchronousSocketChannel channel) { + return null != getRemoteAddress(channel); + } +} diff --git a/hutool-socket/src/main/java/cn/hutool/socket/aio/AcceptHandler.java b/hutool-socket/src/main/java/cn/hutool/socket/aio/AcceptHandler.java new file mode 100644 index 0000000000..c2f53b761c --- /dev/null +++ b/hutool-socket/src/main/java/cn/hutool/socket/aio/AcceptHandler.java @@ -0,0 +1,37 @@ +package cn.hutool.socket.aio; + +import java.nio.ByteBuffer; +import java.nio.channels.AsynchronousSocketChannel; +import java.nio.channels.CompletionHandler; + +import cn.hutool.log.StaticLog; + +/** + * 接入完成回调,单例使用 + * + * @author looly + * + */ +public class AcceptHandler implements CompletionHandler{ + + @Override + public void completed(AsynchronousSocketChannel socketChannel, AioServer aioServer) { + // 继续等待接入(异步) + aioServer.accept(); + + final IoAction ioAction = aioServer.ioAction; + // 创建Session会话 + final AioSession session = new AioSession(socketChannel, ioAction, aioServer.config); + // 处理请求接入(同步) + ioAction.accept(session); + + // 处理读(异步) + session.read(); + } + + @Override + public void failed(Throwable exc, AioServer aioServer) { + StaticLog.error(exc); + } + +} diff --git a/hutool-socket/src/main/java/cn/hutool/socket/aio/AioClient.java b/hutool-socket/src/main/java/cn/hutool/socket/aio/AioClient.java new file mode 100644 index 0000000000..7dd8ca2689 --- /dev/null +++ b/hutool-socket/src/main/java/cn/hutool/socket/aio/AioClient.java @@ -0,0 +1,138 @@ +package cn.hutool.socket.aio; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.SocketOption; +import java.nio.ByteBuffer; +import java.nio.channels.AsynchronousChannelGroup; +import java.nio.channels.AsynchronousSocketChannel; +import java.util.concurrent.ExecutionException; + +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.thread.ThreadFactoryBuilder; +import cn.hutool.socket.SocketConfig; +import cn.hutool.socket.SocketRuntimeException; + +/** + * Aio Socket客户端 + * + * @author looly + * @since 4.5.0 + */ +public class AioClient { + + private AioSession session; + + /** + * 构造 + * + * @param address 地址 + * @param ioAction IO处理类 + */ + public AioClient(InetSocketAddress address, IoAction ioAction) { + this(address, ioAction, new SocketConfig()); + } + + /** + * 构造 + * + * @param address 地址 + * @param ioAction IO处理类 + * @param config 配置项 + */ + public AioClient(InetSocketAddress address, IoAction ioAction, SocketConfig config) { + this(createChannel(address, config.getThreadPoolSize()), ioAction, config); + } + + /** + * 构造 + * + * @param channel {@link AsynchronousSocketChannel} + * @param ioAction IO处理类 + * @param config 配置项 + */ + public AioClient(AsynchronousSocketChannel channel, IoAction ioAction, SocketConfig config) { + this.session = new AioSession(channel, ioAction, config); + ioAction.accept(this.session); + } + + /** + * 设置 Socket 的 Option 选项
+ * 选项见:{@link java.net.StandardSocketOptions} + * + * @param选项泛型 + * @param name {@link SocketOption} 枚举 + * @param value SocketOption参数 + * @throws IOException IO异常 + */ + public AioClient setOption(SocketOption name, T value) throws IOException { + this.session.getChannel().setOption(name, value); + return this; + } + + /** + * 获取IO处理器 + * + * @return {@link IoAction} + */ + public IoAction getIoAction() { + return this.session.getIoAction(); + } + + /** + * 从服务端读取数据 + * + * @return this + */ + public AioClient read() { + this.session.read(); + return this; + } + + /** + * 写数据到服务端 + * + * @return this + */ + public AioClient write(ByteBuffer data) { + this.session.write(data); + return this; + } + + /** + * 关闭客户端 + */ + public void close() { + this.session.close(); + } + + // ------------------------------------------------------------------------------------- Private method start + /** + * 初始化 + * + * @param address 地址和端口 + * @param poolSize 线程池大小 + * @return this + */ + private static AsynchronousSocketChannel createChannel(InetSocketAddress address, int poolSize) { + + AsynchronousSocketChannel channel; + try { + AsynchronousChannelGroup group = AsynchronousChannelGroup.withFixedThreadPool(// + poolSize, // 默认线程池大小 + ThreadFactoryBuilder.create().setNamePrefix("Huool-socket-").build()// + ); + channel = AsynchronousSocketChannel.open(group); + } catch (IOException e) { + throw new IORuntimeException(e); + } + + try { + channel.connect(address).get(); + } catch (InterruptedException | ExecutionException e) { + throw new SocketRuntimeException(e); + } + return channel; + } + // ------------------------------------------------------------------------------------- Private method end +} diff --git a/hutool-socket/src/main/java/cn/hutool/socket/aio/AioServer.java b/hutool-socket/src/main/java/cn/hutool/socket/aio/AioServer.java new file mode 100644 index 0000000000..8b51f8ef7d --- /dev/null +++ b/hutool-socket/src/main/java/cn/hutool/socket/aio/AioServer.java @@ -0,0 +1,192 @@ +package cn.hutool.socket.aio; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.SocketOption; +import java.nio.ByteBuffer; +import java.nio.channels.AsynchronousChannelGroup; +import java.nio.channels.AsynchronousServerSocketChannel; + +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.io.IoUtil; +import cn.hutool.core.thread.ThreadFactoryBuilder; +import cn.hutool.log.Log; +import cn.hutool.log.LogFactory; +import cn.hutool.socket.SocketConfig; + +/** + * 基于AIO的Socket服务端实现 + * + * @author looly + * + */ +public class AioServer { + private static final Log log = LogFactory.get(); + private static AcceptHandler ACCEPT_HANDLER = new AcceptHandler(); + + private AsynchronousChannelGroup group; + private AsynchronousServerSocketChannel channel; + protected IoAction ioAction; + protected SocketConfig config; + + + /** + * 构造 + * + * @param port 端口 + */ + public AioServer(int port) { + this(new InetSocketAddress(port), new SocketConfig()); + } + + /** + * 构造 + * + * @param address 地址 + * @param config {@link SocketConfig} 配置项 + */ + public AioServer(InetSocketAddress address, SocketConfig config) { + this.config = config; + init(address); + } + + /** + * 初始化 + * + * @param address 地址和端口 + * @return this + */ + public AioServer init(InetSocketAddress address) { + try { + this.group = AsynchronousChannelGroup.withFixedThreadPool(// + config.getThreadPoolSize(), // 默认线程池大小 + ThreadFactoryBuilder.create().setNamePrefix("Huool-socket-").build()// + ); + this.channel = AsynchronousServerSocketChannel.open(group).bind(address); + } catch (IOException e) { + throw new IORuntimeException(e); + } + return this; + } + + /** + * 开始监听 + * + * @param sync 是否阻塞 + */ + public void start(boolean sync) { + try { + doStart(sync); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 设置 Socket 的 Option 选项
+ * 选项见:{@link java.net.StandardSocketOptions} + * + * @param选项泛型 + * @param name {@link SocketOption} 枚举 + * @param value SocketOption参数 + * @throws IOException IO异常 + */ + public AioServer setOption(SocketOption name, T value) throws IOException { + this.channel.setOption(name, value); + return this; + } + + /** + * 获取IO处理器 + * + * @return {@link IoAction} + */ + public IoAction getIoAction() { + return this.ioAction; + } + + /** + * 设置IO处理器,单例存在 + * + * @param ioAction {@link IoAction} + * @return this; + */ + public AioServer setIoAction(IoAction ioAction) { + this.ioAction = ioAction; + return this; + } + + /** + * 获取{@link AsynchronousServerSocketChannel} + * + * @return {@link AsynchronousServerSocketChannel} + */ + public AsynchronousServerSocketChannel getChannel() { + return this.channel; + } + + /** + * 处理接入的客户端 + * + * @return this + */ + public AioServer accept() { + this.channel.accept(this, ACCEPT_HANDLER); + return this; + } + + /** + * 服务是否开启状态 + * + * @return 服务是否开启状态 + */ + public boolean isOpen() { + return (null == this.channel) ? false : this.channel.isOpen(); + } + + /** + * 关闭服务 + */ + public void close() { + IoUtil.close(this.channel); + + if (null != this.group && false == this.group.isShutdown()) { + try { + this.group.shutdownNow(); + } catch (IOException e) { + // ignore + } + } + + // 结束阻塞 + synchronized (this) { + this.notify(); + } + } + + // ------------------------------------------------------------------------------------- Private method start + /** + * 开始监听 + * + * @param sync 是否阻塞 + * @throws IOException IO异常 + */ + private void doStart(boolean sync) throws IOException { + log.debug("Aio Server started, waiting for accept."); + + // 接收客户端连接 + accept(); + + if (sync) { + // 阻塞当前线程,保证在main方法中执行不被退出 + synchronized (this) { + try { + this.wait(); + } catch (InterruptedException e) { + // ignore + } + } + } + } + // ------------------------------------------------------------------------------------- Private method end +} diff --git a/hutool-socket/src/main/java/cn/hutool/socket/aio/AioSession.java b/hutool-socket/src/main/java/cn/hutool/socket/aio/AioSession.java new file mode 100644 index 0000000000..b16a7b3f2b --- /dev/null +++ b/hutool-socket/src/main/java/cn/hutool/socket/aio/AioSession.java @@ -0,0 +1,206 @@ +package cn.hutool.socket.aio; + +import java.io.IOException; +import java.net.SocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.AsynchronousSocketChannel; +import java.nio.channels.CompletionHandler; +import java.util.concurrent.Future; +import java.util.concurrent.TimeUnit; + +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.io.IoUtil; +import cn.hutool.socket.SocketConfig; +import cn.hutool.socket.SocketUtil; + +/** + * AIO会话
+ * 每个客户端对应一个会话对象 + * + * @author looly + * + */ +public class AioSession { + + private static final ReadHandler READ_HANDLER = new ReadHandler(); + + private AsynchronousSocketChannel channel; + private IoActionioAction; + private ByteBuffer readBuffer; + private ByteBuffer writeBuffer; + /** 读取超时时长,小于等于0表示默认 */ + private long readTimeout; + /** 写出超时时长,小于等于0表示默认 */ + private long writeTimeout; + + /** + * 构造 + * + * @param channel {@link AsynchronousSocketChannel} + * @param ioAction IO消息处理类 + * @param config 配置项 + */ + public AioSession(AsynchronousSocketChannel channel, IoAction ioAction, SocketConfig config) { + this.channel = channel; + this.readBuffer = ByteBuffer.allocate(config.getReadBufferSize()); + this.writeBuffer = ByteBuffer.allocate(config.getWriteBufferSize()); + this.ioAction = ioAction; + } + + /** + * 获取{@link AsynchronousSocketChannel} + * + * @return {@link AsynchronousSocketChannel} + */ + public AsynchronousSocketChannel getChannel() { + return this.channel; + } + + /** + * 获取读取Buffer + * + * @return 读取Buffer + */ + public ByteBuffer getReadBuffer() { + return this.readBuffer; + } + + /** + * 获取写Buffer + * + * @return 写Buffer + */ + public ByteBuffer getWriteBuffer() { + return this.writeBuffer; + } + + /** + * 获取消息处理器 + * + * @return {@link IoAction} + */ + public IoAction getIoAction() { + return this.ioAction; + } + + /** + * 获取远程主机(客户端)地址和端口 + * + * @return 远程主机(客户端)地址和端口 + */ + public SocketAddress getRemoteAddress() { + return SocketUtil.getRemoteAddress(this.channel); + } + + /** + * 读取数据到Buffer + * + * @return this + */ + public AioSession read() { + return read(READ_HANDLER); + } + + /** + * 读取数据到Buffer + * + * @param handler {@link CompletionHandler} + * @return this + */ + public AioSession read(CompletionHandler handler) { + if (isOpen()) { + this.readBuffer.clear(); + this.channel.read(this.readBuffer, Math.max(this.readTimeout, 0L), TimeUnit.MILLISECONDS, this, handler); + } + return this; + } + + /** + * 写数据到目标端,并关闭输出 + * + * @return this + */ + public AioSession writeAndClose(ByteBuffer data) { + write(data); + return closeOut(); + } + + /** + * 写数据到目标端 + * + * @return {@link Future} + */ + public Future write(ByteBuffer data) { + return this.channel.write(data); + } + + /** + * 写数据到目标端 + * + * @param handler {@link CompletionHandler} + * @return this + */ + public AioSession write(ByteBuffer data, CompletionHandler handler) { + this.channel.write(data, Math.max(this.writeTimeout, 0L), TimeUnit.MILLISECONDS, this, handler); + return this; + } + + /** + * 会话是否打开状态
+ * 当Socket保持连接时会话始终打开 + * + * @return 会话是否打开状态 + */ + public boolean isOpen() { + return (null == this.channel) ? false : this.channel.isOpen(); + } + + /** + * 关闭输出 + * + * @return this + */ + public AioSession closeIn() { + if (null != this.channel) { + try { + this.channel.shutdownInput(); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + return this; + } + + /** + * 关闭输出 + * + * @return this + */ + public AioSession closeOut() { + if (null != this.channel) { + try { + this.channel.shutdownOutput(); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + return this; + } + + /** + * 关闭会话 + */ + public void close() { + IoUtil.close(this.channel); + this.readBuffer = null; + this.writeBuffer = null; + } + + /** + * 执行读,用于读取事件结束的回调 + */ + protected void callbackRead() { + readBuffer.flip();// 读模式 + ioAction.doAction(this, readBuffer); + } +} diff --git a/hutool-socket/src/main/java/cn/hutool/socket/aio/IoAction.java b/hutool-socket/src/main/java/cn/hutool/socket/aio/IoAction.java new file mode 100644 index 0000000000..b871d271c7 --- /dev/null +++ b/hutool-socket/src/main/java/cn/hutool/socket/aio/IoAction.java @@ -0,0 +1,35 @@ +package cn.hutool.socket.aio; + +/** + * Socket流处理接口
+ * 实现此接口用于处理接收到的消息,发送指定消息 + * + * @author looly + * + * @param经过解码器解码后的数据类型 + */ +public interface IoAction { + + /** + * 接收客户端连接(会话建立)事件处理 + * + * @param session 会话 + */ + void accept(AioSession session); + + /** + * 执行数据处理(消息读取) + * + * @param session Socket Session会话 + * @param data 解码后的数据 + */ + void doAction(AioSession session, T data); + + /** + * 数据读取失败的回调事件处理(消息读取失败) + * + * @param exc 异常 + * @param session Session + */ + void failed(Throwable exc, AioSession session); +} diff --git a/hutool-socket/src/main/java/cn/hutool/socket/aio/ReadHandler.java b/hutool-socket/src/main/java/cn/hutool/socket/aio/ReadHandler.java new file mode 100644 index 0000000000..73d7e4338a --- /dev/null +++ b/hutool-socket/src/main/java/cn/hutool/socket/aio/ReadHandler.java @@ -0,0 +1,25 @@ +package cn.hutool.socket.aio; + +import java.nio.channels.CompletionHandler; + +import cn.hutool.socket.SocketRuntimeException; + +/** + * 数据读取完成回调,调用Session中相应方法处理消息,单例使用 + * + * @author looly + * + */ +public class ReadHandler implements CompletionHandler { + + @Override + public void completed(Integer result, AioSession session) { + session.callbackRead(); + } + + @Override + public void failed(Throwable exc, AioSession session) { + throw new SocketRuntimeException(exc); + } + +} diff --git a/hutool-socket/src/main/java/cn/hutool/socket/aio/SimpleIoAction.java b/hutool-socket/src/main/java/cn/hutool/socket/aio/SimpleIoAction.java new file mode 100644 index 0000000000..8e891e42ba --- /dev/null +++ b/hutool-socket/src/main/java/cn/hutool/socket/aio/SimpleIoAction.java @@ -0,0 +1,24 @@ +package cn.hutool.socket.aio; + +import java.nio.ByteBuffer; + +import cn.hutool.log.StaticLog; + +/** + * 简易IO信息处理类
+ * 简单实现了accept和failed事件 + * + * @author looly + * + */ +public abstract class SimpleIoAction implements IoAction{ + + @Override + public void accept(AioSession session) { + } + + @Override + public void failed(Throwable exc, AioSession session) { + StaticLog.error(exc); + } +} diff --git a/hutool-socket/src/main/java/cn/hutool/socket/aio/package-info.java b/hutool-socket/src/main/java/cn/hutool/socket/aio/package-info.java new file mode 100644 index 0000000000..57339acf3b --- /dev/null +++ b/hutool-socket/src/main/java/cn/hutool/socket/aio/package-info.java @@ -0,0 +1,7 @@ +/** + * AIO相关封装 + * + * @author looly + * + */ +package cn.hutool.socket.aio; \ No newline at end of file diff --git a/hutool-socket/src/main/java/cn/hutool/socket/nio/NioClient.java b/hutool-socket/src/main/java/cn/hutool/socket/nio/NioClient.java new file mode 100644 index 0000000000..ebe9470a0f --- /dev/null +++ b/hutool-socket/src/main/java/cn/hutool/socket/nio/NioClient.java @@ -0,0 +1,83 @@ +package cn.hutool.socket.nio; + +import java.io.IOException; +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; +import java.nio.channels.SocketChannel; + +import cn.hutool.core.io.IORuntimeException; + +/** + * NIO客户端 + * + * @author looly + * @since 4.4.5 + */ +public class NioClient { + + private SocketChannel channel; + + /** + * 构造 + * + * @param host 服务器地址 + * @param port 端口 + */ + public NioClient(String host, int port) { + init(new InetSocketAddress(host, port)); + } + + /** + * 构造 + * + * @param address 服务器地址 + */ + public NioClient(InetSocketAddress address) { + init(address); + } + + /** + * 初始化 + * + * @param address 地址和端口 + * @return this + */ + public NioClient init(InetSocketAddress address) { + try { + this.channel = SocketChannel.open(address); + } catch (IOException e) { + throw new IORuntimeException(e); + } + return this; + } + + /** + * 处理读事件
+ * 当收到读取准备就绪的信号后,回调此方法,用户可读取从客户端传世来的消息 + * + * @param buffer 服务端数据存储缓存 + */ + public NioClient read(ByteBuffer buffer) { + try { + this.channel.read(buffer); + } catch (IOException e) { + throw new IORuntimeException(e); + } + return this; + } + + /** + * 实现写逻辑
+ * 当收到写出准备就绪的信号后,回调此方法,用户可向客户端发送消息 + * + * @param datas 发送的数据 + */ + public NioClient write(ByteBuffer... datas) { + try { + this.channel.write(datas); + } catch (IOException e) { + throw new IORuntimeException(e); + } + return this; + } +} diff --git a/hutool-socket/src/main/java/cn/hutool/socket/nio/NioServer.java b/hutool-socket/src/main/java/cn/hutool/socket/nio/NioServer.java new file mode 100644 index 0000000000..c5c002e3c2 --- /dev/null +++ b/hutool-socket/src/main/java/cn/hutool/socket/nio/NioServer.java @@ -0,0 +1,174 @@ +package cn.hutool.socket.nio; + +import java.io.Closeable; +import java.io.IOException; +import java.net.InetSocketAddress; +import java.net.ServerSocket; +import java.nio.channels.SelectableChannel; +import java.nio.channels.SelectionKey; +import java.nio.channels.Selector; +import java.nio.channels.ServerSocketChannel; +import java.nio.channels.SocketChannel; +import java.util.Iterator; + +import cn.hutool.core.io.IORuntimeException; +import cn.hutool.core.io.IoUtil; + +/** + * 基于NIO的Socket服务端实现 + * + * @author looly + * + */ +public abstract class NioServer implements Closeable { + + private Selector selector; + private ServerSocketChannel serverSocketChannel; + + /** + * 构造 + * + * @param port 端口 + */ + public NioServer(int port) { + init(new InetSocketAddress(port)); + } + + /** + * 初始化 + * + * @param address 地址和端口 + * @return this + */ + public NioServer init(InetSocketAddress address) { + try { + // 打开服务器套接字通道 + this.serverSocketChannel = ServerSocketChannel.open(); + // 设置为非阻塞状态 + serverSocketChannel.configureBlocking(false); + // 获取通道相关联的套接字 + final ServerSocket serverSocket = serverSocketChannel.socket(); + // 绑定端口号 + serverSocket.bind(address); + + // 打开一个选择器 + selector = Selector.open(); + // 服务器套接字注册到Selector中 并指定Selector监控连接事件 + serverSocketChannel.register(selector, SelectionKey.OP_ACCEPT); + } catch (IOException e) { + throw new IORuntimeException(e); + } + + return this; + } + + /** + * 开始监听 + */ + public void listen() { + try { + doListen(); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } + + /** + * 开始监听 + * + * @throws IOException IO异常 + */ + private void doListen() throws IOException { + while (0 != this.selector.select()) { + // 返回已选择键的集合 + final IteratorkeyIter = selector.selectedKeys().iterator(); + while (keyIter.hasNext()) { + handle(keyIter.next()); + keyIter.remove(); + } + } + } + + /** + * 处理SelectionKey + * + * @param key SelectionKey + */ + private void handle(SelectionKey key) { + // 有客户端接入此服务端 + if (key.isAcceptable()) { + // 获取通道 转化为要处理的类型 + final ServerSocketChannel server = (ServerSocketChannel) key.channel(); + SocketChannel socketChannel; + try { + // 获取连接到此服务器的客户端通道 + socketChannel = server.accept(); + } catch (IOException e) { + throw new IORuntimeException(e); + } + + // SocketChannel通道的可读事件注册到Selector中 + registerChannel(selector, socketChannel, Operation.READ); + } + + // 读事件就绪 + if (key.isReadable()) { + final SocketChannel socketChannel = (SocketChannel) key.channel(); + read(socketChannel); + + // SocketChannel通道的可写事件注册到Selector中 + registerChannel(selector, socketChannel, Operation.WRITE); + } + + // 写事件就绪 + if (key.isWritable()) { + final SocketChannel socketChannel = (SocketChannel) key.channel(); + write(socketChannel); + // SocketChannel通道的可读事件注册到Selector中 + registerChannel(selector, socketChannel, Operation.READ); + } + } + + @Override + public void close() throws IOException { + IoUtil.close(this.selector); + IoUtil.close(this.serverSocketChannel); + } + + /** + * 处理读事件
+ * 当收到读取准备就绪的信号后,回调此方法,用户可读取从客户端传世来的消息 + * + * @param socketChannel SocketChannel + */ + protected abstract void read(SocketChannel socketChannel); + + /** + * 实现写逻辑
+ * 当收到写出准备就绪的信号后,回调此方法,用户可向客户端发送消息 + * + * @param socketChannel SocketChannel + */ + protected abstract void write(SocketChannel socketChannel); + + /** + * 注册通道到指定Selector上 + * + * @param selector Selector + * @param channel 通道 + * @param ops 注册的通道监听类型 + */ + private void registerChannel(Selector selector, SelectableChannel channel, Operation ops) { + if (channel == null) { + return; + } + + try { + channel.configureBlocking(false); + // 注册通道 + channel.register(selector, ops.getValue()); + } catch (IOException e) { + throw new IORuntimeException(e); + } + } +} diff --git a/hutool-core/src/main/java/cn/hutool/core/net/Operation.java b/hutool-socket/src/main/java/cn/hutool/socket/nio/Operation.java similarity index 91% rename from hutool-core/src/main/java/cn/hutool/core/net/Operation.java rename to hutool-socket/src/main/java/cn/hutool/socket/nio/Operation.java index 21140cd11f..e231d54be8 100644 --- a/hutool-core/src/main/java/cn/hutool/core/net/Operation.java +++ b/hutool-socket/src/main/java/cn/hutool/socket/nio/Operation.java @@ -1,4 +1,4 @@ -package cn.hutool.core.net; +package cn.hutool.socket.nio; import java.nio.channels.SelectionKey; diff --git a/hutool-socket/src/main/java/cn/hutool/socket/nio/package-info.java b/hutool-socket/src/main/java/cn/hutool/socket/nio/package-info.java new file mode 100644 index 0000000000..fee9128e1b --- /dev/null +++ b/hutool-socket/src/main/java/cn/hutool/socket/nio/package-info.java @@ -0,0 +1,7 @@ +/** + * NIO相关封装 + * + * @author looly + * + */ +package cn.hutool.socket.nio; \ No newline at end of file diff --git a/hutool-socket/src/main/java/cn/hutool/socket/package-info.java b/hutool-socket/src/main/java/cn/hutool/socket/package-info.java new file mode 100644 index 0000000000..c6e6affbb3 --- /dev/null +++ b/hutool-socket/src/main/java/cn/hutool/socket/package-info.java @@ -0,0 +1,7 @@ +/** + * Socket套接字相关工具类封装 + * + * @author looly + * + */ +package cn.hutool.socket; \ No newline at end of file diff --git a/hutool-socket/src/main/java/cn/hutool/socket/protocol/MsgDecoder.java b/hutool-socket/src/main/java/cn/hutool/socket/protocol/MsgDecoder.java new file mode 100644 index 0000000000..243380d29f --- /dev/null +++ b/hutool-socket/src/main/java/cn/hutool/socket/protocol/MsgDecoder.java @@ -0,0 +1,24 @@ +package cn.hutool.socket.protocol; + +import java.nio.ByteBuffer; + +import cn.hutool.socket.aio.AioSession; + +/** + * 消息解码器 + * + * @author looly + * + * @param解码后的目标类型 + */ +public interface MsgDecoder { + /** + * 对于从Socket流中获取到的数据采用当前MsgDecoder的实现类协议进行解析。 + * + * + * @param session 本次需要解码的session + * @param readBuffer 待处理的读buffer + * @return 本次解码成功后封装的业务消息对象, 返回null则表示解码未完成 + */ + T decode(AioSession session, ByteBuffer readBuffer); +} diff --git a/hutool-socket/src/main/java/cn/hutool/socket/protocol/MsgEncoder.java b/hutool-socket/src/main/java/cn/hutool/socket/protocol/MsgEncoder.java new file mode 100644 index 0000000000..8e563a4adf --- /dev/null +++ b/hutool-socket/src/main/java/cn/hutool/socket/protocol/MsgEncoder.java @@ -0,0 +1,23 @@ +package cn.hutool.socket.protocol; + +import java.nio.ByteBuffer; + +import cn.hutool.socket.aio.AioSession; + +/** + * 消息编码器 + * + * @author looly + * + * @param 编码前后的数据类型 + */ +public interface MsgEncoder { + /** + * 编码数据用于写出 + * + * @param session 本次需要解码的session + * @param writeBuffer 待处理的读buffer + * @param data 写出的数据 + */ + void encode(AioSession session, ByteBuffer writeBuffer, T data); +} diff --git a/hutool-socket/src/main/java/cn/hutool/socket/protocol/Protocol.java b/hutool-socket/src/main/java/cn/hutool/socket/protocol/Protocol.java new file mode 100644 index 0000000000..7e9fa53984 --- /dev/null +++ b/hutool-socket/src/main/java/cn/hutool/socket/protocol/Protocol.java @@ -0,0 +1,23 @@ +/* + * Copyright (c) 2017, org.smartboot. All rights reserved. + * project name: smart-socket + * file name: Protocol.java + * Date: 2017-11-25 + * Author: sandao + */ + +package cn.hutool.socket.protocol; + +/** + * 协议接口
+ * 通过实现此接口完成消息的编码和解码 + * + *+ * 所有Socket使用相同协议对象,类成员变量和对象成员变量易造成并发读写问题。 + *
+ * + * @author Looly + */ +public interface Protocolextends MsgEncoder , MsgDecoder { + +} diff --git a/hutool-socket/src/main/java/cn/hutool/socket/protocol/package-info.java b/hutool-socket/src/main/java/cn/hutool/socket/protocol/package-info.java new file mode 100644 index 0000000000..edb069568a --- /dev/null +++ b/hutool-socket/src/main/java/cn/hutool/socket/protocol/package-info.java @@ -0,0 +1,7 @@ +/** + * 消息协议接口及实现 + * + * @author looly + * + */ +package cn.hutool.socket.protocol; \ No newline at end of file diff --git a/hutool-socket/src/test/java/cn/hutool/socket/AioClientTest.java b/hutool-socket/src/test/java/cn/hutool/socket/AioClientTest.java new file mode 100644 index 0000000000..872c3cee7f --- /dev/null +++ b/hutool-socket/src/test/java/cn/hutool/socket/AioClientTest.java @@ -0,0 +1,31 @@ +package cn.hutool.socket; + +import java.net.InetSocketAddress; +import java.nio.ByteBuffer; + +import cn.hutool.core.lang.Console; +import cn.hutool.core.util.StrUtil; +import cn.hutool.socket.aio.AioClient; +import cn.hutool.socket.aio.AioSession; +import cn.hutool.socket.aio.SimpleIoAction; + +public class AioClientTest { + public static void main(String[] args) { + AioClient client = new AioClient(new InetSocketAddress("localhost", 8899), new SimpleIoAction() { + + @Override + public void doAction(AioSession session, ByteBuffer data) { + if(data.hasRemaining()) { + Console.log(StrUtil.utf8Str(data)); + session.read(); + } + Console.log("OK"); + } + }); + + client.write(ByteBuffer.wrap("Hello".getBytes())); + client.read(); + + client.close(); + } +} diff --git a/hutool-socket/src/test/java/cn/hutool/socket/AioServerTest.java b/hutool-socket/src/test/java/cn/hutool/socket/AioServerTest.java new file mode 100644 index 0000000000..6fecb90f02 --- /dev/null +++ b/hutool-socket/src/test/java/cn/hutool/socket/AioServerTest.java @@ -0,0 +1,45 @@ +package cn.hutool.socket; + +import java.nio.ByteBuffer; + +import cn.hutool.core.date.DateUtil; +import cn.hutool.core.io.BufferUtil; +import cn.hutool.core.lang.Console; +import cn.hutool.core.util.StrUtil; +import cn.hutool.log.StaticLog; +import cn.hutool.socket.aio.AioServer; +import cn.hutool.socket.aio.AioSession; +import cn.hutool.socket.aio.SimpleIoAction; + +public class AioServerTest { + + public static void main(String[] args) { + + AioServer aioServer = new AioServer(8899); + aioServer.setIoAction(new SimpleIoAction() { + + @Override + public void accept(AioSession session) { + StaticLog.debug("【客户端】:{} 连接。", session.getRemoteAddress()); + session.write(BufferUtil.createUtf8("=== Welcome to Hutool socket server. ===")); + } + + @Override + public void doAction(AioSession session, ByteBuffer data) { + Console.log(data); + + if(false == data.hasRemaining()) { + StringBuilder response = StrUtil.builder()// + .append("HTTP/1.1 200 OK\r\n")// + .append("Date: ").append(DateUtil.formatHttpDate(DateUtil.date())).append("\r\n")// + .append("Content-Type: text/html; charset=UTF-8\r\n")// + .append("\r\n") + .append("Hello Hutool socket");// + session.writeAndClose(BufferUtil.createUtf8(response)); + }else { + session.read(); + } + } + }).start(true); + } +} diff --git a/hutool-system/pom.xml b/hutool-system/pom.xml index b1556768fe..13fd26b2d4 100644 --- a/hutool-system/pom.xml +++ b/hutool-system/pom.xml @@ -7,7 +7,7 @@ cn.hutool hutool-parent -4.4.5 +4.5.0 hutool-system diff --git a/pom.xml b/pom.xml index 2a7b1e6998..6e0f4ca7e6 100644 --- a/pom.xml +++ b/pom.xml @@ -8,7 +8,7 @@cn.hutool hutool-parent -4.4.5 +4.5.0 hutool 提供丰富的Java工具方法 https://github.com/looly/hutool @@ -32,6 +32,7 @@hutool-json hutool-poi hutool-captcha +hutool-socket