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的自造词,谐音“糊涂”,寓意追求“万事都 cn.hutool hutool-all - 4.4.5 + 4.5.0 ``` ### Gradle ``` -compile 'cn.hutool:hutool-all:4.4.5' +compile 'cn.hutool:hutool-all:4.5.0' ``` ### 非Maven项目 点击以下任一链接,下载`hutool-all-X.X.X.jar`即可: -- [Maven中央库1](https://repo1.maven.org/maven2/cn/hutool/hutool-all/4.4.5/) -- [Maven中央库2](http://repo2.maven.org/maven2/cn/hutool/hutool-all/4.4.5/) +- [Maven中央库1](https://repo1.maven.org/maven2/cn/hutool/hutool-all/4.5.0/) +- [Maven中央库2](http://repo2.maven.org/maven2/cn/hutool/hutool-all/4.5.0/) > 注意 > Hutool只支持JDK7+,对应Android平台没有测试,部分方法并不支持。 diff --git a/bin/replaceVersion.sh b/bin/replaceVersion.sh new file mode 100644 index 0000000000..3c79fdd2fc --- /dev/null +++ b/bin/replaceVersion.sh @@ -0,0 +1,34 @@ +#!/bin/bash + +#----------------------------------------------------------- +# 此脚本用于每次升级Hutool时替换相应位置的版本号 +#----------------------------------------------------------- + +set -o errexit + +pwd=`pwd` + +echo "当前路径:${pwd}" + +if [ -n "$1" ];then + new_version="$1" + old_version=`cat ${pwd}/bin/version.txt` + echo "$old_version 替换为新版本 $new_version" +else + # 参数错误,退出 + echo "ERROR: 请指定新版本!" + exit +fi + +if [ ! -n "$old_version" ]; then + echo "ERROR: 旧版本不存在,请确认bin/version.txt中信息正确" + exit +fi + +# 替换README.md中的版本 +sed -i "s/${old_version}/${new_version}/g" $pwd/README.md +# 替换docs/index.html中的版本 +sed -i "s/${old_version}/${new_version}/g" $pwd/docs/index.html + +# 保留新版本号 +echo "$new_version" > $pwd/bin/version.txt diff --git a/bin/update_version.sh b/bin/update_version.sh index b2975c62d8..1bd49d8650 100644 --- a/bin/update_version.sh +++ b/bin/update_version.sh @@ -1,3 +1,21 @@ #!/bin/bash +#------------------------------------------------ +# 升级Hutool版本,包括: +# 1. 升级pom.xml中的版本号 +# 2. 替换README.md和docs中的版本号 +#------------------------------------------------ + +if [ ! -n "$1" ]; then + echo "ERROR: 新版本不存在,请指定参数1" + exit +fi + +# 替换所有模块pom.xml中的版本 mvn versions:set -DnewVersion=$1 + +# 不带-SNAPSHOT的版本号,用于替换其它地方 +version=${1%-SNAPSHOT} + +# 替换其它地方的版本 +`pwd`/bin/replaceVersion.sh "$version" diff --git a/bin/version.txt b/bin/version.txt new file mode 100644 index 0000000000..a84947d6ff --- /dev/null +++ b/bin/version.txt @@ -0,0 +1 @@ +4.5.0 diff --git a/docs/index.html b/docs/index.html index a33cfb7b4e..e69de29bb2 100644 --- a/docs/index.html +++ b/docs/index.html @@ -1,454 +0,0 @@ - - - - - - - - - - - - - - - - - - Hutool — A set of tools that keep Java sweet. - - - - - - - - - - - - - - - - - - - - - - - -
-
- -
-
-
-
-
-
-

- Hutool - v4.4.5 -

-

A set of tools that keep Java sweet.

- -
-
-
-
-
- - -
- -
- -
-
- -
- -
- -
-

Hutool是Hu + tool的自造词,前者致敬我的“前任公司”,后者为工具之意,谐音“糊涂”,寓意追求“万事都作糊涂观,无所谓失,无所谓得”的境界。

-

Hutool是一个Java工具包,也只是一个工具包,它帮助我们简化每一行代码,减少每一个方法,让Java语言也可以“甜甜的”。Hutool最初是我项目中“util”包的一个整理,后来慢慢积累并加入更多非业务相关功能,并广泛学习其它开源项目精髓,经过自己整理修改,最终形成丰富的开源工具集。

-
    -
  • Web开发
  • -
  • 与其它框架无耦合
  • -
  • 高度可替换
  • -
-
- -
-

Hutool的设计思想是尽量减少重复的定义,让项目中的util这个package尽量少,总的来说有如下的几个思想:

-
    -
  • 方法优先于对象
  • -
  • 自动识别优于用户定义
  • -
  • 便捷性与灵活性并存
  • -
  • 适配与兼容
  • -
  • 可选依赖原则
  • -
  • 无侵入原则
  • -
-
- -
-
Maven:在项目的pom.xml的dependencies中加入以下内容:
-
-									<dependency>
-									      <groupId>cn.hutool</groupId>
-									      <artifactId>hutool-all</artifactId>
-									      <version>4.4.5</version>
-									  </dependency>
-								
-
Gradle:
-
-									compile 'cn.hutool:hutool-all:4.4.5'
-								
-

- 从Maven安装 -

-
- -
- -
- -
-
- -
-
- -
-
- - - - Watch Video -
-
-
-

Hutool 是什么

-

Hutool是一个Java工具包类库,对文件、流、加密解密、转码、正则、线程、XML等JDK方法进行封装,组成各种Util工具类

-
-
-
-
-

日期工具

-

通过DateUtil类,提供高度便捷的日期访问、处理和转换方式。

-
-
-
-
-
-

HTTP客户端

-

通过HttpUtil对HTTP客户端的封装,实现便捷的HTTP请求,并简化文件上传操作。

-
-
-
-
-
-

转换工具

-

通过Convert类中的相应静态方法,提供一整套的类型转换解决方案,并通过ConverterRegistry工厂类自定义转换。

-
-
- -
-
-
-

配置文件工具(Setting)

-

通过Setting对象,提供兼容Properties文件的更加强大的配置文件工具,用于解决中文、分组等JDK配置文件存在的诸多问题。

-
-
-
-
-
-

日志工具

-

Hutool的日志功能,通过抽象Log接口,提供对Slf4j、LogBack、Log4j、JDK-Logging的全面兼容支持。

-
-
-
-
-
-

JDBC工具类(DB模块)

-

通过db模块,提供对MySQL、Oracle等关系型数据库的JDBC封装,借助ActiveRecord思想,大大简化数据库操作。

-
-
-
-

Hutool的更多功能,期待你的探索:

-

- 参考教程 - API 文档 -

-
- -
- -
- - -
-
-
-

开发团队

-

我们不是一个人在战斗

-
-
-
- -

路小磊

-

二手Java码农,Python和前端爱好者

-

一个非职业的码农,混迹于非IT圈子,利用8小时之外做自己喜欢的事情,爱前端,爱数码,爱美女。

- -
- -
-
-
- -

深山码农

-

崇拜自由的生活和善良的人性

-

深山耕耘互金行业多年,熟悉互金系统架构和设计,喜欢研究新技术,善于发现和解决问题

- -
-
-
-
- -

Chinaboy

-

相信自己,明天会更好

-

一个奔波于IT圈子的程序猿,拥有自己的梦想,喜欢美女、喜欢音乐、爱打篮球儿...

- -
-
-
-
- -

汪汪90

-

悲观的乐观主义者

-

Java程序员一枚,喜欢从生活中领悟技术,喜欢关注技术细节,ennio morricone 音乐的死忠粉。

- -
-
-
-
- -

普辉辉

-

java码农,爱技术、爱旅游

-

java码农,爱技术、爱旅游、一直活跃在互联网技术圈。
 

- -
-
-
-
- - -
-
-
-

加入讨论

-

通过以下方式加入讨论,或为Hutool添砖加瓦

-
-
-
- - 871141901 -
- -
- - Gitee Issues -
- -
- -
-
- - -
-
-
-

赞助商

-

为Hutool提供赞助,也许他们也会为你提供更优惠的服务

-
-
-
-
-
- - -
-
-
-

友情链接

-

为Hutool提供各种帮助和支持的朋友们,我们一起共奋进

-
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
- -
-
-
- - -
-
-
-
-

- © 2017 Hutool Project. All Rights Reserved.
- Designed by Looly, Hosted by Coding Pages. -

-
-
-
    -
  • -
  • -
  • -
-
-
-
-
-
- -
- - -
- -
- - - - - - - - - - - - - - - - - - - - \ No newline at end of file diff --git a/hutool-all/pom.xml b/hutool-all/pom.xml index a86f9cf4d3..bdb8a80454 100644 --- a/hutool-all/pom.xml +++ b/hutool-all/pom.xml @@ -7,7 +7,7 @@ cn.hutool hutool-parent - 4.4.5 + 4.5.0 hutool-all diff --git a/hutool-aop/pom.xml b/hutool-aop/pom.xml index f9b73e4c7e..d13625baeb 100644 --- a/hutool-aop/pom.xml +++ b/hutool-aop/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 4.4.5 + 4.5.0 hutool-aop diff --git a/hutool-bloomFilter/pom.xml b/hutool-bloomFilter/pom.xml index 4e2f9fa1b9..095b4c95c4 100644 --- a/hutool-bloomFilter/pom.xml +++ b/hutool-bloomFilter/pom.xml @@ -7,7 +7,7 @@ cn.hutool hutool-parent - 4.4.5 + 4.5.0 hutool-bloomFilter diff --git a/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/bitMap/BitMap.java b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/bitMap/BitMap.java index 9d6c1e6bb7..48af8a37c7 100644 --- a/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/bitMap/BitMap.java +++ b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/bitMap/BitMap.java @@ -1,13 +1,34 @@ package cn.hutool.bloomfilter.bitMap; +/** + * BitMap接口,用于将某个int或long值映射到一个数组中,从而判定某个值是否存在 + * + * @author looly + * + */ public interface BitMap { public final int MACHINE32 = 32; public final int MACHINE64 = 64; + /** + * 加入值 + * + * @param i 值 + */ public void add(long i); + /** + * 检查是否包含值 + * + * @param i 值 + */ public boolean contains(long i); + /** + * 移除值 + * + * @param i 值 + */ public void remove(long i); } \ No newline at end of file diff --git a/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/bitMap/IntMap.java b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/bitMap/IntMap.java index 32050213aa..85c449d1ce 100644 --- a/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/bitMap/IntMap.java +++ b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/bitMap/IntMap.java @@ -2,29 +2,39 @@ /** * 过滤器BitMap在32位机器上.这个类能发生更好的效果.一般情况下建议使用此类 + * * @author loolly * */ public class IntMap implements BitMap { - private static final int MAX = Integer.MAX_VALUE; + private int[] ints = null; + + /** + * 构造 + */ public IntMap() { ints = new int[93750000]; } + /** + * 构造 + * + * @param size 容量 + */ public IntMap(int size) { ints = new int[size]; } - private int[] ints = null; - + @Override public void add(long i) { int r = (int) (i / BitMap.MACHINE32); int c = (int) (i % BitMap.MACHINE32); ints[r] = (int) (ints[r] | (1 << c)); } + @Override public boolean contains(long i) { int r = (int) (i / BitMap.MACHINE32); int c = (int) (i % BitMap.MACHINE32); @@ -34,6 +44,7 @@ public boolean contains(long i) { return false; } + @Override public void remove(long i) { int r = (int) (i / BitMap.MACHINE32); int c = (int) (i % BitMap.MACHINE32); diff --git a/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/bitMap/LongMap.java b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/bitMap/LongMap.java index f04288c506..e2726914c1 100644 --- a/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/bitMap/LongMap.java +++ b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/bitMap/LongMap.java @@ -2,6 +2,7 @@ /** * 过滤器BitMap在64位机器上.这个类能发生更好的效果.一般机器不建议使用 + * * @author loolly * */ @@ -9,22 +10,32 @@ public class LongMap implements BitMap { private static final long MAX = Long.MAX_VALUE; + private long[] longs = null; + + /** + * 构造 + */ public LongMap() { longs = new long[93750000]; } + /** + * 构造 + * + * @param size 容量 + */ public LongMap(int size) { longs = new long[size]; } - private long[] longs = null; - + @Override public void add(long i) { int r = (int) (i / BitMap.MACHINE64); long c = i % BitMap.MACHINE64; longs[r] = longs[r] | (1 << c); } + @Override public boolean contains(long i) { int r = (int) (i / BitMap.MACHINE64); long c = i % BitMap.MACHINE64; @@ -34,9 +45,10 @@ public boolean contains(long i) { return false; } + @Override public void remove(long i) { int r = (int) (i / BitMap.MACHINE64); - long c =i % BitMap.MACHINE64; + long c = i % BitMap.MACHINE64; longs[r] = longs[r] & (((1 << (c + 1)) - 1) ^ MAX); } diff --git a/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/AbstractFilter.java b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/AbstractFilter.java index d7da02e6de..630879c1cc 100644 --- a/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/AbstractFilter.java +++ b/hutool-bloomFilter/src/main/java/cn/hutool/bloomfilter/filter/AbstractFilter.java @@ -17,25 +17,42 @@ public abstract class AbstractFilter implements BloomFilter { protected long size = 0; + /** + * 构造 + * + * @param maxValue 最大值 + * @param machineNum 机器位数 + */ public AbstractFilter(long maxValue, int machineNum) { init(maxValue, machineNum); } + /** + * 构造32位 + * + * @param maxValue 最大值 + */ public AbstractFilter(long maxValue) { this(maxValue, BitMap.MACHINE32); } + /** + * 初始化 + * + * @param maxValue 最大值 + * @param machineNum 机器位数 + */ public void init(long maxValue, int machineNum) { this.size = maxValue; switch (machineNum) { - case BitMap.MACHINE32: - bm = new IntMap((int) (size / machineNum)); - break; - case BitMap.MACHINE64: - bm = new LongMap((int) (size / machineNum)); - break; - default: - throw new RuntimeException("Error Machine number!"); + case BitMap.MACHINE32: + bm = new IntMap((int) (size / machineNum)); + break; + case BitMap.MACHINE64: + bm = new LongMap((int) (size / machineNum)); + break; + default: + throw new RuntimeException("Error Machine number!"); } } @@ -57,8 +74,9 @@ public boolean add(String str) { /** * 自定义Hash方法 + * * @param str 字符串 * @return HashCode */ - public abstract long hash(String str) ; + public abstract long hash(String str); } \ No newline at end of file diff --git a/hutool-cache/pom.xml b/hutool-cache/pom.xml index c2386b2c19..709ce4c021 100644 --- a/hutool-cache/pom.xml +++ b/hutool-cache/pom.xml @@ -7,7 +7,7 @@ cn.hutool hutool-parent - 4.4.5 + 4.5.0 hutool-cache diff --git a/hutool-cache/src/main/java/cn/hutool/cache/impl/AbstractCache.java b/hutool-cache/src/main/java/cn/hutool/cache/impl/AbstractCache.java index e040632b73..1632500015 100644 --- a/hutool-cache/src/main/java/cn/hutool/cache/impl/AbstractCache.java +++ b/hutool-cache/src/main/java/cn/hutool/cache/impl/AbstractCache.java @@ -13,8 +13,8 @@ * 超时和限制大小的缓存的默认实现
* 继承此抽象缓存需要:
* * * @author Looly,jodd @@ -22,7 +22,7 @@ * @param 键类型 * @param 值类型 */ -public abstract class AbstractCache implements Cache{ +public abstract class AbstractCache implements Cache { protected Map> cacheMap; @@ -34,15 +34,15 @@ public abstract class AbstractCache implements Cache{ protected int capacity; /** 缓存失效时长, 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 co = cacheMap.get(key); if (co == null) { return false; } - - //过期 - if (co.isExpired() == true) { - // remove(key); // 此方法无法获得锁 - removeWithoutLock(key); - missCount++; - return false; - } - //命中 - return true; + if (false == co.isExpired()) { + // 命中 + return true; + } } finally { readLock.unlock(); } + + // 过期 + remove(key, true); + return false; } - + /** * @return 命中数 */ @@ -108,7 +106,7 @@ public int getHitCount() { public int getMissCount() { return missCount; } - + @Override public V get(K key) { return get(key, true); @@ -119,29 +117,27 @@ public V get(K key, boolean isUpdateLastAccess) { readLock.lock(); try { - //不存在或已移除 + // 不存在或已移除 final CacheObj co = cacheMap.get(key); if (co == null) { missCount++; return null; } - - //过期 - if (co.isExpired() == true) { - // remove(key); // 此方法无法获得锁 - removeWithoutLock(key); - missCount++; - return null; - } - //命中 - hitCount++; - return co.get(isUpdateLastAccess); + if (false == co.isExpired()) { + // 命中 + hitCount++; + return co.get(isUpdateLastAccess); + } } finally { readLock.unlock(); } + + // 过期 + remove(key, true); + return null; } - + // ---------------------------------------------------------------- get end @Override @@ -150,7 +146,7 @@ public Iterator iterator() { CacheObjIterator copiedIterator = (CacheObjIterator) this.cacheObjIterator(); return new CacheValuesIterator(copiedIterator); } - + @Override public Iterator> cacheObjIterator() { CopiedIter> copiedIterator; @@ -166,11 +162,12 @@ public Iterator> cacheObjIterator() { // ---------------------------------------------------------------- prune start /** * 清理实现 + * * @return 清理数 */ protected abstract int pruneCache(); - @Override + @Override public final int prune() { writeLock.lock(); try { @@ -189,21 +186,22 @@ public int capacity() { /** * @return 默认缓存失效时长。
- * 每个对象可以单独设置失效时长 + * 每个对象可以单独设置失效时长 */ @Override public long timeout() { return timeout; } - + /** * 只有设置公共缓存失效时长或每个对象单独的失效时长时清理可用 + * * @return 过期对象清理是否可用,内部使用 */ protected boolean isPruneExpiredActive() { return (timeout != 0) || existCustomTimeout; } - + @Override public boolean isFull() { return (capacity > 0) && (cacheMap.size() >= capacity); @@ -211,16 +209,7 @@ public boolean isFull() { @Override public void remove(K key) { - writeLock.lock(); - CacheObj co; - try { - co = cacheMap.remove(key); - } finally { - writeLock.unlock(); - } - if(null != co){ - onRemove(co.key, co.obj); - } + remove(key, false); } @Override @@ -242,28 +231,40 @@ public int size() { public boolean isEmpty() { return cacheMap.isEmpty(); } - + @Override public String toString() { return this.cacheMap.toString(); } // ---------------------------------------------------------------- common end - + /** * 对象移除回调。默认无动作 + * * @param key 键 * @param cachedObject 被缓存的对象 */ protected void onRemove(K key, V cachedObject) { } - + /** - * 移除元素,无锁 + * 移除key对应的对象 + * * @param key 键 + * @param withMissCount 是否计数丢失数 */ - private void removeWithoutLock(K key) { - CacheObj co = cacheMap.remove(key); - if(null != co){ + private void remove(K key, boolean withMissCount) { + writeLock.lock(); + CacheObj co; + try { + co = cacheMap.remove(key); + if (withMissCount) { + this.missCount--; + } + } finally { + writeLock.unlock(); + } + if (null != co) { onRemove(co.key, co.obj); } } diff --git a/hutool-captcha/pom.xml b/hutool-captcha/pom.xml index 6a70a383ca..dfb02e855b 100644 --- a/hutool-captcha/pom.xml +++ b/hutool-captcha/pom.xml @@ -7,7 +7,7 @@ cn.hutool hutool-parent - 4.4.5 + 4.5.0 hutool-captcha diff --git a/hutool-core/pom.xml b/hutool-core/pom.xml index 9c414db63d..f179314198 100644 --- a/hutool-core/pom.xml +++ b/hutool-core/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 4.4.5 + 4.5.0 hutool-core diff --git a/hutool-core/src/main/java/cn/hutool/core/date/DatePattern.java b/hutool-core/src/main/java/cn/hutool/core/date/DatePattern.java index 280ee6d1a9..06e515abc6 100644 --- a/hutool-core/src/main/java/cn/hutool/core/date/DatePattern.java +++ b/hutool-core/src/main/java/cn/hutool/core/date/DatePattern.java @@ -1,5 +1,6 @@ package cn.hutool.core.date; +import java.util.Locale; import java.util.TimeZone; import cn.hutool.core.date.format.FastDateFormat; @@ -68,7 +69,7 @@ public class DatePattern { /** HTTP头中日期时间格式:EEE, dd MMM yyyy HH:mm:ss z */ public final static String HTTP_DATETIME_PATTERN = "EEE, dd MMM yyyy HH:mm:ss z"; /** HTTP头中日期时间格式 {@link FastDateFormat}:EEE, dd MMM yyyy HH:mm:ss z */ - public final static FastDateFormat HTTP_DATETIME_FORMAT = FastDateFormat.getInstance(HTTP_DATETIME_PATTERN); + public final static FastDateFormat HTTP_DATETIME_FORMAT = FastDateFormat.getInstance(HTTP_DATETIME_PATTERN, TimeZone.getTimeZone("GMT"), Locale.US); /** JDK中日期时间格式:EEE MMM dd HH:mm:ss zzz yyyy */ public final static String JDK_DATETIME_PATTERN = "EEE MMM dd HH:mm:ss zzz yyyy"; diff --git a/hutool-core/src/main/java/cn/hutool/core/date/DateUtil.java b/hutool-core/src/main/java/cn/hutool/core/date/DateUtil.java index 17bf464ee5..218b57ea31 100644 --- a/hutool-core/src/main/java/cn/hutool/core/date/DateUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/date/DateUtil.java @@ -562,7 +562,8 @@ public static String formatTime(Date date) { } /** - * 格式化为Http的标准日期格式 + * 格式化为Http的标准日期格式
+ * 标准日期格式遵循RFC 1123规范,格式类似于:Fri, 31 Dec 1999 23:59:59 GMT * * @param date 被格式化的日期 * @return HTTP标准形式日期字符串 diff --git a/hutool-core/src/main/java/cn/hutool/core/date/Zodiac.java b/hutool-core/src/main/java/cn/hutool/core/date/Zodiac.java index 31ffad142a..27cdfb30d0 100644 --- a/hutool-core/src/main/java/cn/hutool/core/date/Zodiac.java +++ b/hutool-core/src/main/java/cn/hutool/core/date/Zodiac.java @@ -39,13 +39,25 @@ public static String getZodiac(Calendar calendar) { } return getZodiac(calendar.get(Calendar.MONTH), calendar.get(Calendar.DAY_OF_MONTH)); } - + /** * 通过生日计算星座 * * @param month 月,从0开始计数 * @param day 天 * @return 星座名 + * @since 4.5.0 + */ + public static String getZodiac(Month month, int day) { + return getZodiac(month.getValue(), day); + } + + /** + * 通过生日计算星座 + * + * @param month 月,从0开始计数,见{@link Month#getValue()} + * @param day 天 + * @return 星座名 */ public static String getZodiac(int month, int day) { // 在分隔日前为前一个星座,否则为后一个星座 diff --git a/hutool-core/src/main/java/cn/hutool/core/io/BufferUtil.java b/hutool-core/src/main/java/cn/hutool/core/io/BufferUtil.java index 4d132e7601..fb15159da8 100644 --- a/hutool-core/src/main/java/cn/hutool/core/io/BufferUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/io/BufferUtil.java @@ -3,6 +3,7 @@ import java.nio.ByteBuffer; import java.nio.charset.Charset; +import cn.hutool.core.util.CharsetUtil; import cn.hutool.core.util.StrUtil; /** @@ -65,7 +66,30 @@ public static ByteBuffer copy(ByteBuffer src, int srcStart, ByteBuffer dest, int System.arraycopy(src.array(), srcStart, dest.array(), destStart, length); return dest; } - + + /** + * 读取剩余部分并转为UTF-8编码字符串 + * + * @param buffer ByteBuffer + * @return 字符串 + * @since 4.5.0 + */ + public static String readUtf8Str(ByteBuffer buffer) { + return readStr(buffer, CharsetUtil.CHARSET_UTF_8); + } + + /** + * 读取剩余部分并转为字符串 + * + * @param buffer ByteBuffer + * @param charset 编码 + * @return 字符串 + * @since 4.5.0 + */ + public static String readStr(ByteBuffer buffer, Charset charset) { + return StrUtil.str(readBytes(buffer), charset); + } + /** * 读取剩余部分bytes
* @@ -89,14 +113,14 @@ public static byte[] readBytes(ByteBuffer buffer) { */ public static byte[] readBytes(ByteBuffer buffer, int maxLength) { final int remaining = buffer.remaining(); - if(maxLength > remaining) { + if (maxLength > remaining) { maxLength = remaining; } byte[] ab = new byte[maxLength]; buffer.get(ab); return ab; } - + /** * 读取指定区间的数据 * @@ -110,7 +134,7 @@ public static byte[] readBytes(ByteBuffer buffer, int start, int end) { System.arraycopy(buffer.array(), start, bs, 0, bs.length); return bs; } - + /** * 一行的末尾位置,查找位置时位移ByteBuffer到结束位置 * @@ -124,6 +148,7 @@ public static int lineEnd(ByteBuffer buffer) { /** * 一行的末尾位置,查找位置时位移ByteBuffer到结束位置
* 支持的换行符如下: + * *
 	 * 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 static  T 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(BlockingQueue workQueue) { 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 List ls(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 @@ + + + 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} + + + 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和端口
+ * 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 IoAction ioAction; + 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 Iterator keyIter = 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 Protocol extends 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