diff --git a/CHANGELOG.md b/CHANGELOG.md index 11ddb22fb..df97fc9c3 100644 --- a/CHANGELOG.md +++ b/CHANGELOG.md @@ -3,20 +3,33 @@ ------------------------------------------------------------------------------------------------------------- -# 5.7.20 (2022-01-14) +# 5.7.20 (2022-01-20) ### 🐣新特性 * 【core 】 增加对null值友好的groupingBy操作的Collector实现,可指定map类型(pr#498@Gitee) * 【core 】 增加KetamaHash(issue#2084@Github) * 【crypto 】 增加SignUtil * 【json 】 JSONGetter增加getBeanList方法 -* 【core 】 ObjectUtil 添加三个defaultIfXxxx方法,用于节省CPU及内存损耗。(pr#2094@Github) +* 【core 】 ObjectUtil 添加三个defaultIfXxxx方法,用于节省CPU及内存损耗(pr#2094@Github) +* 【db 】 增加单条数据原生upsert语义支持(pr#501@Gitee) +* 【core 】 在CollectorUtil提交Collectors.toMap的对null友好实现,避免NPE(pr#502@Gitee) +* 【http 】 增加HttpGlobalConfig.setIgnoreEOFError(issue#2092@Github) +* 【core 】 RandomUtil.randomStringWithoutStr排除字符串兼容大写字母(pr#503@Gitee) +* 【core 】 LocalDateTime增加isOverlap方法(pr#512@Gitee) +* 【core 】 Ipv4Util.getBeginIpLong、getEndIpLong改为public(pr#508@Gitee) * ### 🐞Bug修复 * 【core 】 修复setter重载导致匹配错误(issue#2082@Github) * 【core 】 修复RegexPool汉字匹配范围小问题(pr#2081@Github) * 【core 】 修复OS中的拼写错误(pr#500@Gitee) * 【core 】 修复CustomKeyMap的merge失效问题(issue#2086@Github) +* 【core 】 修复FileUtil.appendLines换行问题(issue#I4QCEZ@Gitee) +* 【core 】 修复java.time.Month解析问题(issue#2090@Github) +* 【core 】 修复PathUtil.moveContent移动覆盖导致的问题(issue#I4QV0L@Gitee) +* 【core 】 修复Opt.ofTry中并发环境下线程安全问题(pr#504@Gitee) +* 【core 】 修复PatternFinder中end边界判断问题(issue#2099@Github) +* 【core 】 修复格式化为中文日期时,0被处理为空串(pr#507@Gitee) +* 【core 】 修复UrlPath转义冒号问题(issue#I4RA42@Gitee) ------------------------------------------------------------------------------------------------------------- # 5.7.19 (2022-01-07) diff --git a/hutool-all/pom.xml b/hutool-all/pom.xml index 10a7032a4..511746da4 100644 --- a/hutool-all/pom.xml +++ b/hutool-all/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 5.7.20-SNAPSHOT + 5.7.20 hutool-all diff --git a/hutool-aop/pom.xml b/hutool-aop/pom.xml index edc46b7a7..7ea45314f 100644 --- a/hutool-aop/pom.xml +++ b/hutool-aop/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 5.7.20-SNAPSHOT + 5.7.20 hutool-aop diff --git a/hutool-bloomFilter/pom.xml b/hutool-bloomFilter/pom.xml index abeaec1c1..c8cdcd926 100644 --- a/hutool-bloomFilter/pom.xml +++ b/hutool-bloomFilter/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 5.7.20-SNAPSHOT + 5.7.20 hutool-bloomFilter diff --git a/hutool-bom/pom.xml b/hutool-bom/pom.xml index 3b8ad4fd4..c3cee33b3 100644 --- a/hutool-bom/pom.xml +++ b/hutool-bom/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 5.7.20-SNAPSHOT + 5.7.20 hutool-bom diff --git a/hutool-cache/pom.xml b/hutool-cache/pom.xml index d544b5527..062080971 100644 --- a/hutool-cache/pom.xml +++ b/hutool-cache/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 5.7.20-SNAPSHOT + 5.7.20 hutool-cache diff --git a/hutool-captcha/pom.xml b/hutool-captcha/pom.xml index 5dd8f9f15..45754904a 100644 --- a/hutool-captcha/pom.xml +++ b/hutool-captcha/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 5.7.20-SNAPSHOT + 5.7.20 hutool-captcha diff --git a/hutool-core/pom.xml b/hutool-core/pom.xml index 0129bcab8..995d36026 100644 --- a/hutool-core/pom.xml +++ b/hutool-core/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 5.7.20-SNAPSHOT + 5.7.20 hutool-core diff --git a/hutool-core/src/main/java/cn/hutool/core/bean/copier/BeanCopier.java b/hutool-core/src/main/java/cn/hutool/core/bean/copier/BeanCopier.java index 1b9afc7cf..c3f4cb35c 100644 --- a/hutool-core/src/main/java/cn/hutool/core/bean/copier/BeanCopier.java +++ b/hutool-core/src/main/java/cn/hutool/core/bean/copier/BeanCopier.java @@ -35,6 +35,7 @@ public class BeanCopier implements Copier, Serializable { private static final long serialVersionUID = 1L; /** 源对象 */ + @SuppressWarnings("NonSerializableFieldInSerializableClass") private final Object source; /** 目标对象 */ private final T dest; diff --git a/hutool-core/src/main/java/cn/hutool/core/collection/CollStreamUtil.java b/hutool-core/src/main/java/cn/hutool/core/collection/CollStreamUtil.java index ef3893d6d..b70f24a62 100644 --- a/hutool-core/src/main/java/cn/hutool/core/collection/CollStreamUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/collection/CollStreamUtil.java @@ -199,7 +199,7 @@ public class CollStreamUtil { if (CollUtil.isEmpty(collection) || key1 == null || key2 == null) { return Collections.emptyMap(); } - return groupBy(collection, key1, Collectors.toMap(key2, Function.identity(), (l, r) -> l), isParallel); + return groupBy(collection, key1, CollectorUtil.toMap(key2, Function.identity(), (l, r) -> l), isParallel); } /** diff --git a/hutool-core/src/main/java/cn/hutool/core/collection/CollUtil.java b/hutool-core/src/main/java/cn/hutool/core/collection/CollUtil.java index 201308b2e..dd22d5275 100644 --- a/hutool-core/src/main/java/cn/hutool/core/collection/CollUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/collection/CollUtil.java @@ -2343,11 +2343,7 @@ public class CollUtil { */ @SuppressWarnings("unchecked") public static ArrayList valuesOfKeys(Map map, K... keys) { - final ArrayList values = new ArrayList<>(); - for (K k : keys) { - values.add(map.get(k)); - } - return values; + return MapUtil.valuesOfKeys(map, new ArrayIter<>(keys)); } /** @@ -2377,11 +2373,7 @@ public class CollUtil { * @since 3.0.9 */ public static ArrayList valuesOfKeys(Map map, Iterator keys) { - final ArrayList values = new ArrayList<>(); - while (keys.hasNext()) { - values.add(map.get(keys.next())); - } - return values; + return MapUtil.valuesOfKeys(map, keys); } // ------------------------------------------------------------------------------------------------- sort diff --git a/hutool-core/src/main/java/cn/hutool/core/convert/NumberChineseFormatter.java b/hutool-core/src/main/java/cn/hutool/core/convert/NumberChineseFormatter.java index afbb9d472..29c2405e1 100644 --- a/hutool-core/src/main/java/cn/hutool/core/convert/NumberChineseFormatter.java +++ b/hutool-core/src/main/java/cn/hutool/core/convert/NumberChineseFormatter.java @@ -164,6 +164,7 @@ public class NumberChineseFormatter { */ public static String formatThousand(int amount, boolean isUseTraditional){ Assert.checkBetween(amount, -999, 999, "Number support only: (-999 ~ 999)!"); + final String chinese = thousandToChinese(amount, isUseTraditional); if(amount < 20 && amount > 10){ // "十一"而非"一十一" @@ -284,6 +285,11 @@ public class NumberChineseFormatter { * @return 转换后的汉字 */ private static String thousandToChinese(int amountPart, boolean isUseTraditional) { + if (amountPart == 0) { + // issue#I4R92H@Gitee + return String.valueOf(DIGITS[0]); + } + int temp = amountPart; StringBuilder chineseStr = new StringBuilder(); diff --git a/hutool-core/src/main/java/cn/hutool/core/date/CalendarUtil.java b/hutool-core/src/main/java/cn/hutool/core/date/CalendarUtil.java index 752a052c8..a1e2f176b 100644 --- a/hutool-core/src/main/java/cn/hutool/core/date/CalendarUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/date/CalendarUtil.java @@ -555,6 +555,12 @@ public class CalendarUtil { result.append(NumberChineseFormatter.formatThousand(day, false)); result.append('日'); + // 只替换年月日,时分秒中零不需要替换 + String temp = result.toString().replace('零', '〇'); + result.delete(0, result.length()); + result.append(temp); + + if (withTime) { // 时 int hour = calendar.get(Calendar.HOUR_OF_DAY); @@ -570,7 +576,7 @@ public class CalendarUtil { result.append('秒'); } - return result.toString().replace('零', '〇'); + return result.toString(); } /** diff --git a/hutool-core/src/main/java/cn/hutool/core/date/LocalDateTimeUtil.java b/hutool-core/src/main/java/cn/hutool/core/date/LocalDateTimeUtil.java index f41d4e781..102fdff4e 100644 --- a/hutool-core/src/main/java/cn/hutool/core/date/LocalDateTimeUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/date/LocalDateTimeUtil.java @@ -14,6 +14,7 @@ import java.time.LocalTime; import java.time.Period; import java.time.ZoneId; import java.time.ZonedDateTime; +import java.time.chrono.ChronoLocalDateTime; import java.time.format.DateTimeFormatter; import java.time.format.DateTimeFormatterBuilder; import java.time.temporal.ChronoField; @@ -493,7 +494,7 @@ public class LocalDateTimeUtil { * @since 5.7.18 */ public static LocalDateTime endOfDay(LocalDateTime time, boolean truncateMillisecond) { - if(truncateMillisecond){ + if (truncateMillisecond) { return time.with(LocalTime.of(23, 59, 59)); } return time.with(LocalTime.MAX); @@ -544,4 +545,21 @@ public class LocalDateTimeUtil { public static Week dayOfWeek(LocalDate localDate) { return Week.of(localDate.getDayOfWeek()); } + + /** + * 检查两个时间段是否有时间重叠
+ * 重叠指两个时间段是否有交集 + * + * @param realStartTime 第一个时间段的开始时间 + * @param realEndTime 第一个时间段的结束时间 + * @param startTime 第二个时间段的开始时间 + * @param endTime 第二个时间段的结束时间 + * @return true 表示时间有重合 + * @since 5.7.20 + */ + public static boolean isOverlap(ChronoLocalDateTime realStartTime, ChronoLocalDateTime realEndTime, + ChronoLocalDateTime startTime, ChronoLocalDateTime endTime) { + return startTime.isAfter(realEndTime) || endTime.isBefore(realStartTime); + } + } diff --git a/hutool-core/src/main/java/cn/hutool/core/date/TemporalAccessorUtil.java b/hutool-core/src/main/java/cn/hutool/core/date/TemporalAccessorUtil.java index 1d9baef32..034f4868b 100644 --- a/hutool-core/src/main/java/cn/hutool/core/date/TemporalAccessorUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/date/TemporalAccessorUtil.java @@ -7,6 +7,7 @@ import java.time.Instant; import java.time.LocalDate; import java.time.LocalDateTime; import java.time.LocalTime; +import java.time.Month; import java.time.OffsetDateTime; import java.time.OffsetTime; import java.time.ZoneId; @@ -40,7 +41,8 @@ public class TemporalAccessorUtil extends TemporalUtil{ } /** - * 格式化日期时间为指定格式 + * 格式化日期时间为指定格式
+ * 如果为{@link Month},调用{@link Month#toString()} * * @param time {@link TemporalAccessor} * @param formatter 日期格式化器,预定义的格式见:{@link DateTimeFormatter} @@ -52,6 +54,10 @@ public class TemporalAccessorUtil extends TemporalUtil{ return null; } + if(time instanceof Month){ + return time.toString(); + } + if(null == formatter){ formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME; } @@ -74,7 +80,8 @@ public class TemporalAccessorUtil extends TemporalUtil{ } /** - * 格式化日期时间为指定格式 + * 格式化日期时间为指定格式
+ * 如果为{@link Month},调用{@link Month#toString()} * * @param time {@link TemporalAccessor} * @param format 日期格式 @@ -86,6 +93,10 @@ public class TemporalAccessorUtil extends TemporalUtil{ return null; } + if(time instanceof Month){ + return time.toString(); + } + // 检查自定义格式 if(GlobalCustomFormat.isCustomFormat(format)){ return GlobalCustomFormat.format(time, format); @@ -98,13 +109,17 @@ public class TemporalAccessorUtil extends TemporalUtil{ } /** - * {@link TemporalAccessor}转换为 时间戳(从1970-01-01T00:00:00Z开始的毫秒数) + * {@link TemporalAccessor}转换为 时间戳(从1970-01-01T00:00:00Z开始的毫秒数)
+ * 如果为{@link Month},调用{@link Month#getValue()} * * @param temporalAccessor Date对象 * @return {@link Instant}对象 * @since 5.4.1 */ public static long toEpochMilli(TemporalAccessor temporalAccessor) { + if(temporalAccessor instanceof Month){ + return ((Month) temporalAccessor).getValue(); + } return toInstant(temporalAccessor).toEpochMilli(); } diff --git a/hutool-core/src/main/java/cn/hutool/core/io/FileUtil.java b/hutool-core/src/main/java/cn/hutool/core/io/FileUtil.java index c658db8cb..3d4b80858 100644 --- a/hutool-core/src/main/java/cn/hutool/core/io/FileUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/io/FileUtil.java @@ -2876,7 +2876,12 @@ public class FileUtil extends PathUtil { } /** - * 将列表写入文件,追加模式 + * 将列表写入文件,追加模式,策略为: + *
    + *
  • 当文件为空,从开头追加,尾部不加空行
  • + *
  • 当有内容,换行追加,尾部不加空行
  • + *
  • 当有内容,并末尾有空行,依旧换行追加
  • + *
* * @param 集合元素类型 * @param list 列表 diff --git a/hutool-core/src/main/java/cn/hutool/core/io/file/PathUtil.java b/hutool-core/src/main/java/cn/hutool/core/io/file/PathUtil.java index 1c3ed2d92..7c1cd4955 100644 --- a/hutool-core/src/main/java/cn/hutool/core/io/file/PathUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/io/file/PathUtil.java @@ -20,6 +20,7 @@ import java.nio.charset.Charset; import java.nio.file.AccessDeniedException; import java.nio.file.CopyOption; import java.nio.file.DirectoryStream; +import java.nio.file.FileAlreadyExistsException; import java.nio.file.FileVisitOption; import java.nio.file.FileVisitResult; import java.nio.file.FileVisitor; @@ -516,6 +517,11 @@ public class PathUtil { try { return Files.move(src, target, options); } catch (IOException e) { + if(e instanceof FileAlreadyExistsException){ + // 目标文件已存在,直接抛出异常 + // issue#I4QV0L@Gitee + throw new IORuntimeException(e); + } // 移动失败,可能是跨分区移动导致的,采用递归移动方式 try { Files.walkFileTree(src, new MoveVisitor(src, target, options)); diff --git a/hutool-core/src/main/java/cn/hutool/core/lang/Opt.java b/hutool-core/src/main/java/cn/hutool/core/lang/Opt.java index e03ad64cd..dad1b5cd0 100644 --- a/hutool-core/src/main/java/cn/hutool/core/lang/Opt.java +++ b/hutool-core/src/main/java/cn/hutool/core/lang/Opt.java @@ -122,7 +122,7 @@ public class Opt { try { return Opt.ofNullable(supplier.call()); } catch (Exception e) { - final Opt empty = Opt.empty(); + final Opt empty = new Opt<>(null); empty.exception = e; return empty; } diff --git a/hutool-core/src/main/java/cn/hutool/core/map/MapUtil.java b/hutool-core/src/main/java/cn/hutool/core/map/MapUtil.java index 7bbce4c4b..b0fb79250 100644 --- a/hutool-core/src/main/java/cn/hutool/core/map/MapUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/map/MapUtil.java @@ -1354,4 +1354,23 @@ public class MapUtil { } } } + + /** + * 从Map中获取指定键列表对应的值列表
+ * 如果key在map中不存在或key对应值为null,则返回值列表对应位置的值也为null + * + * @param 键类型 + * @param 值类型 + * @param map {@link Map} + * @param keys 键列表 + * @return 值列表 + * @since 5.7.20 + */ + public static ArrayList valuesOfKeys(Map map, Iterator keys) { + final ArrayList values = new ArrayList<>(); + while (keys.hasNext()) { + values.add(map.get(keys.next())); + } + return values; + } } diff --git a/hutool-core/src/main/java/cn/hutool/core/net/Ipv4Util.java b/hutool-core/src/main/java/cn/hutool/core/net/Ipv4Util.java index 4a65ca3df..886f9c47f 100644 --- a/hutool-core/src/main/java/cn/hutool/core/net/Ipv4Util.java +++ b/hutool-core/src/main/java/cn/hutool/core/net/Ipv4Util.java @@ -187,7 +187,7 @@ public class Ipv4Util { * @param maskBit 给定的掩码位,如30 * @return 起始IP的长整型表示 */ - private static Long getBeginIpLong(String ip, int maskBit) { + public static Long getBeginIpLong(String ip, int maskBit) { return ipv4ToLong(ip) & ipv4ToLong(getMaskByMaskBit(maskBit)); } @@ -348,7 +348,7 @@ public class Ipv4Util { * @param maskBit 给定的掩码位,如30 * @return 终止IP的长整型表示 */ - private static Long getEndIpLong(String ip, int maskBit) { + public static Long getEndIpLong(String ip, int maskBit) { return getBeginIpLong(ip, maskBit) + ~ipv4ToLong(getMaskByMaskBit(maskBit)); } diff --git a/hutool-core/src/main/java/cn/hutool/core/net/NetUtil.java b/hutool-core/src/main/java/cn/hutool/core/net/NetUtil.java index aef83a6b8..3fded9388 100644 --- a/hutool-core/src/main/java/cn/hutool/core/net/NetUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/net/NetUtil.java @@ -571,16 +571,6 @@ public class NetUtil { return null; } - /** - * 获得本机物理地址 - * - * @return 本机物理地址 - * @since 5.7.3 - */ - public static byte[] getLocalHardwareAddress() { - return getHardwareAddress(getLocalhost()); - } - /** * 获得指定地址信息中的硬件地址 * @@ -604,6 +594,16 @@ public class NetUtil { return null; } + /** + * 获得本机物理地址 + * + * @return 本机物理地址 + * @since 5.7.3 + */ + public static byte[] getLocalHardwareAddress() { + return getHardwareAddress(getLocalhost()); + } + /** * 获取主机名称,一次获取会缓存名称 * diff --git a/hutool-core/src/main/java/cn/hutool/core/net/url/UrlPath.java b/hutool-core/src/main/java/cn/hutool/core/net/url/UrlPath.java index 0ab1bef1d..8a968f1a7 100644 --- a/hutool-core/src/main/java/cn/hutool/core/net/url/UrlPath.java +++ b/hutool-core/src/main/java/cn/hutool/core/net/url/UrlPath.java @@ -117,7 +117,10 @@ public class UrlPath { } /** - * 构建path,前面带'/' + * 构建path,前面带'/'
+ *
+	 *     path = path-abempty / path-absolute / path-noscheme / path-rootless / path-empty
+	 * 
* * @param charset encode编码,null表示不做encode * @return 如果没有任何内容,则返回空字符串"" @@ -129,10 +132,14 @@ public class UrlPath { final StringBuilder builder = new StringBuilder(); for (String segment : segments) { - // 根据https://www.ietf.org/rfc/rfc3986.html#section-3.3定义 - // path的第一部分允许有":",其余部分不允许 - // 在此处的Path部分特指host之后的部分,即不包含第一部分 - builder.append(CharUtil.SLASH).append(RFC3986.SEGMENT_NZ_NC.encode(segment, charset)); + if(builder.length() == 0){ + // 根据https://www.ietf.org/rfc/rfc3986.html#section-3.3定义 + // path的第一部分不允许有":",其余部分允许 + // 在此处的Path部分特指host之后的部分,即不包含第一部分 + builder.append(CharUtil.SLASH).append(RFC3986.SEGMENT_NZ_NC.encode(segment, charset)); + } else { + builder.append(CharUtil.SLASH).append(RFC3986.SEGMENT.encode(segment, charset)); + } } if (StrUtil.isEmpty(builder)) { // 空白追加是保证以/开头 diff --git a/hutool-core/src/main/java/cn/hutool/core/stream/CollectorUtil.java b/hutool-core/src/main/java/cn/hutool/core/stream/CollectorUtil.java index 55e96877c..82821897d 100644 --- a/hutool-core/src/main/java/cn/hutool/core/stream/CollectorUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/stream/CollectorUtil.java @@ -7,6 +7,7 @@ import java.util.Collections; import java.util.EnumSet; import java.util.HashMap; import java.util.Map; +import java.util.Set; import java.util.StringJoiner; import java.util.function.BiConsumer; import java.util.function.BinaryOperator; @@ -17,11 +18,21 @@ import java.util.stream.Collector; /** * 可变的汇聚操作{@link Collector} 相关工具封装 * - * @author looly + * @author looly, VampireAchao * @since 5.6.7 */ public class CollectorUtil { + /** + * 说明已包含IDENTITY_FINISH特征 为 Characteristics.IDENTITY_FINISH 的缩写 + */ + public static final Set CH_ID + = Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.IDENTITY_FINISH)); + /** + * 说明不包含IDENTITY_FINISH特征 + */ + public static final Set CH_NOID = Collections.emptySet(); + /** * 提供任意对象的Join操作的{@link Collector}实现,对象默认调用toString方法 * @@ -93,17 +104,12 @@ public class CollectorUtil { A container = m.computeIfAbsent(key, k -> downstreamSupplier.get()); downstreamAccumulator.accept(container, t); }; - BinaryOperator> merger = (m1, m2) -> { - for (Map.Entry e : m2.entrySet()) { - m1.merge(e.getKey(), e.getValue(), downstream.combiner()); - } - return m1; - }; + BinaryOperator> merger = mapMerger(downstream.combiner()); @SuppressWarnings("unchecked") Supplier> mangledFactory = (Supplier>) mapFactory; if (downstream.characteristics().contains(Collector.Characteristics.IDENTITY_FINISH)) { - return new SimpleCollector<>(mangledFactory, accumulator, merger, Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.IDENTITY_FINISH))); + return new SimpleCollector<>(mangledFactory, accumulator, merger, CH_ID); } else { @SuppressWarnings("unchecked") Function downstreamFinisher = (Function) downstream.finisher(); @@ -113,7 +119,7 @@ public class CollectorUtil { M castResult = (M) intermediate; return castResult; }; - return new SimpleCollector<>(mangledFactory, accumulator, merger, finisher, Collections.emptySet()); + return new SimpleCollector<>(mangledFactory, accumulator, merger, finisher, CH_NOID); } } @@ -134,4 +140,65 @@ public class CollectorUtil { return groupingBy(classifier, HashMap::new, downstream); } + + /** + * 对null友好的 toMap 操作的 {@link Collector}实现,默认使用HashMap + * + * @param keyMapper 指定map中的key + * @param valueMapper 指定map中的value + * @param mergeFunction 合并前对value进行的操作 + * @param 实体类型 + * @param map中key的类型 + * @param map中value的类型 + * @return 对null友好的 toMap 操作的 {@link Collector}实现 + */ + public static + Collector> toMap(Function keyMapper, + Function valueMapper, + BinaryOperator mergeFunction) { + return toMap(keyMapper, valueMapper, mergeFunction, HashMap::new); + } + + /** + * 对null友好的 toMap 操作的 {@link Collector}实现 + * + * @param keyMapper 指定map中的key + * @param valueMapper 指定map中的value + * @param mergeFunction 合并前对value进行的操作 + * @param mapSupplier 最终需要的map类型 + * @param 实体类型 + * @param map中key的类型 + * @param map中value的类型 + * @param map的类型 + * @return 对null友好的 toMap 操作的 {@link Collector}实现 + */ + public static > + Collector toMap(Function keyMapper, + Function valueMapper, + BinaryOperator mergeFunction, + Supplier mapSupplier) { + BiConsumer accumulator + = (map, element) -> map.put(Opt.ofNullable(element).map(keyMapper).get(), Opt.ofNullable(element).map(valueMapper).get()); + return new SimpleCollector<>(mapSupplier, accumulator, mapMerger(mergeFunction), CH_ID); + } + + /** + * 用户合并map的BinaryOperator,传入合并前需要对value进行的操作 + * + * @param mergeFunction 合并前需要对value进行的操作 + * @param key的类型 + * @param value的类型 + * @param map + * @return 用户合并map的BinaryOperator + */ + public static > BinaryOperator mapMerger(BinaryOperator mergeFunction) { + return (m1, m2) -> { + for (Map.Entry e : m2.entrySet()) { + m1.merge(e.getKey(), e.getValue(), mergeFunction); + } + return m1; + }; + } + + } diff --git a/hutool-core/src/main/java/cn/hutool/core/text/finder/PatternFinder.java b/hutool-core/src/main/java/cn/hutool/core/text/finder/PatternFinder.java index 92afc408d..c258daed0 100644 --- a/hutool-core/src/main/java/cn/hutool/core/text/finder/PatternFinder.java +++ b/hutool-core/src/main/java/cn/hutool/core/text/finder/PatternFinder.java @@ -66,7 +66,7 @@ public class PatternFinder extends TextFinder { }else{ limit = Math.min(endIndex, text.length()); } - return end < limit ? end : INDEX_NOT_FOUND; + return end <= limit ? end : INDEX_NOT_FOUND; } @Override diff --git a/hutool-core/src/main/java/cn/hutool/core/text/split/SplitIter.java b/hutool-core/src/main/java/cn/hutool/core/text/split/SplitIter.java index 6dc104800..b1cb444d9 100644 --- a/hutool-core/src/main/java/cn/hutool/core/text/split/SplitIter.java +++ b/hutool-core/src/main/java/cn/hutool/core/text/split/SplitIter.java @@ -87,9 +87,8 @@ public class SplitIter extends ComputeIter implements Serializable { } // 找到新的分隔符位置 - final int end = finder.end(start); final String result = text.substring(offset, start); - offset = end; + offset = finder.end(start); if (ignoreEmpty && result.isEmpty()) { // 发现空串且需要忽略时,跳过之 diff --git a/hutool-core/src/main/java/cn/hutool/core/thread/ThreadUtil.java b/hutool-core/src/main/java/cn/hutool/core/thread/ThreadUtil.java index 0227ca523..3418bd7cb 100644 --- a/hutool-core/src/main/java/cn/hutool/core/thread/ThreadUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/thread/ThreadUtil.java @@ -27,7 +27,6 @@ public class ThreadUtil { * 1. 初始线程数为corePoolSize指定的大小 * 2. 没有最大线程数限制 * 3. 默认使用LinkedBlockingQueue,默认队列大小为1024 - * 4. 当运行线程大于corePoolSize放入队列,队列满后抛出异常 * * * @param corePoolSize 同时执行的线程数大小 diff --git a/hutool-core/src/main/java/cn/hutool/core/util/EscapeUtil.java b/hutool-core/src/main/java/cn/hutool/core/util/EscapeUtil.java index 05beb38ac..f2b3dee92 100644 --- a/hutool-core/src/main/java/cn/hutool/core/util/EscapeUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/util/EscapeUtil.java @@ -10,6 +10,7 @@ import cn.hutool.core.text.escape.XmlUnescape; * 转义和反转义工具类Escape / Unescape
* escape采用ISO Latin字符集对指定的字符串进行编码。
* 所有的空格符、标点符号、特殊字符以及其他非ASCII字符都将被转化成%xx格式的字符编码(xx等于该字符在字符集表里面的编码的16进制数字)。 + * TODO 6.x迁移到core.text.escape包下 * * @author xiaoleilu */ diff --git a/hutool-core/src/main/java/cn/hutool/core/util/IdUtil.java b/hutool-core/src/main/java/cn/hutool/core/util/IdUtil.java index 9e9167b72..6ac904e3a 100644 --- a/hutool-core/src/main/java/cn/hutool/core/util/IdUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/util/IdUtil.java @@ -1,6 +1,7 @@ package cn.hutool.core.util; import cn.hutool.core.exceptions.UtilException; +import cn.hutool.core.lang.Assert; import cn.hutool.core.lang.ObjectId; import cn.hutool.core.lang.Singleton; import cn.hutool.core.lang.Snowflake; @@ -203,8 +204,17 @@ public class IdUtil { * @since 5.7.3 */ public static long getDataCenterId(long maxDatacenterId) { + Assert.isTrue(maxDatacenterId > 0, "maxDatacenterId must be > 0"); + if(maxDatacenterId == Long.MAX_VALUE){ + maxDatacenterId -= 1; + } long id = 1L; - final byte[] mac = NetUtil.getLocalHardwareAddress(); + byte[] mac = null; + try{ + mac = NetUtil.getLocalHardwareAddress(); + }catch (UtilException ignore){ + // ignore + } if (null != mac) { id = ((0x000000FF & (long) mac[mac.length - 2]) | (0x0000FF00 & (((long) mac[mac.length - 1]) << 8))) >> 6; diff --git a/hutool-core/src/main/java/cn/hutool/core/util/RandomUtil.java b/hutool-core/src/main/java/cn/hutool/core/util/RandomUtil.java index 8d77384e7..2c415b386 100644 --- a/hutool-core/src/main/java/cn/hutool/core/util/RandomUtil.java +++ b/hutool-core/src/main/java/cn/hutool/core/util/RandomUtil.java @@ -516,15 +516,15 @@ public class RandomUtil { } /** - * 获得一个随机的字符串(只包含数字和字符) 并排除指定字符串 + * 获得一个随机的字符串(只包含数字和小写字母) 并排除指定字符串 * * @param length 字符串的长度 - * @param elemData 要排除的字符串,如:去重容易混淆的字符串,oO0、lL1、q9Q、pP + * @param elemData 要排除的字符串,如:去重容易混淆的字符串,oO0、lL1、q9Q、pP,不区分大小写 * @return 随机字符串 */ public static String randomStringWithoutStr(int length, String elemData) { String baseStr = BASE_CHAR_NUMBER; - baseStr = StrUtil.removeAll(baseStr, elemData.toCharArray()); + baseStr = StrUtil.removeAll(baseStr, elemData.toLowerCase().toCharArray()); return randomString(baseStr, length); } diff --git a/hutool-core/src/test/java/cn/hutool/core/collection/CollStreamUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/collection/CollStreamUtilTest.java index 7c95075af..69da448ba 100644 --- a/hutool-core/src/test/java/cn/hutool/core/collection/CollStreamUtilTest.java +++ b/hutool-core/src/test/java/cn/hutool/core/collection/CollStreamUtilTest.java @@ -150,6 +150,13 @@ public class CollStreamUtilTest { compare.put(2L, map2); Assert.assertEquals(compare, map); + // 对null友好 + Map> termIdClassIdStudentMap = CollStreamUtil.group2Map(Arrays.asList(null, new Student(2, 2, 1, "王五")), Student::getTermId, Student::getClassId); + Map> termIdClassIdStudentCompareMap = new HashMap>() {{ + put(null, MapUtil.of(null, null)); + put(2L, MapUtil.of(2L, new Student(2, 2, 1, "王五"))); + }}; + Assert.assertEquals(termIdClassIdStudentCompareMap, termIdClassIdStudentMap); } @Test diff --git a/hutool-core/src/test/java/cn/hutool/core/collection/CollUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/collection/CollUtilTest.java index 4e866979d..ad0582e18 100644 --- a/hutool-core/src/test/java/cn/hutool/core/collection/CollUtilTest.java +++ b/hutool-core/src/test/java/cn/hutool/core/collection/CollUtilTest.java @@ -4,6 +4,7 @@ import cn.hutool.core.comparator.ComparableComparator; import cn.hutool.core.date.DateUtil; import cn.hutool.core.lang.Dict; import cn.hutool.core.map.MapUtil; +import cn.hutool.core.util.StrUtil; import lombok.AllArgsConstructor; import lombok.Data; import org.junit.Assert; @@ -302,6 +303,14 @@ public class CollUtilTest { Assert.assertEquals(CollUtil.newArrayList("b", "c"), filtered); } + @Test + public void filterSetTest() { + Set set = CollUtil.newLinkedHashSet("a", "b", "", " ", "c"); + Set filtered = CollUtil.filter(set, StrUtil::isNotBlank); + + Assert.assertEquals(CollUtil.newLinkedHashSet("a", "b", "c"), filtered); + } + @Test public void filterRemoveTest() { ArrayList list = CollUtil.newArrayList("a", "b", "c"); 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 6d3a2529e..24e7321a8 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 @@ -289,6 +289,9 @@ public class DateUtilTest { public void formatChineseDateTimeTest() { String formatChineseDateTime = DateUtil.formatChineseDate(DateUtil.parse("2018-02-24 12:13:14"), true, true); Assert.assertEquals("二〇一八年二月二十四日十二时十三分十四秒", formatChineseDateTime); + + formatChineseDateTime = DateUtil.formatChineseDate(DateUtil.parse("2022-01-18 12:00:00"), true, true); + Assert.assertEquals("二〇二二年一月十八日十二时零分零秒", formatChineseDateTime); } @Test diff --git a/hutool-core/src/test/java/cn/hutool/core/date/LocalDateTimeUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/date/LocalDateTimeUtilTest.java index f8455b941..8f35c59a5 100644 --- a/hutool-core/src/test/java/cn/hutool/core/date/LocalDateTimeUtilTest.java +++ b/hutool-core/src/test/java/cn/hutool/core/date/LocalDateTimeUtilTest.java @@ -177,4 +177,24 @@ public class LocalDateTimeUtilTest { final Week seven = LocalDateTimeUtil.dayOfWeek(LocalDate.of(2021, 9, 26)); Assert.assertEquals(Week.SUNDAY, seven); } + + @Test + public void isOverlapTest(){ + LocalDateTime oneStartTime = LocalDateTime.of(2022, 1, 1, 10, 10, 10); + LocalDateTime oneEndTime = LocalDateTime.of(2022, 1, 1, 11, 10, 10); + + LocalDateTime oneStartTime2 = LocalDateTime.of(2022, 1, 1, 11, 20, 10); + LocalDateTime oneEndTime2 = LocalDateTime.of(2022, 1, 1, 11, 30, 10); + + LocalDateTime oneStartTime3 = LocalDateTime.of(2022, 1, 1, 11, 40, 10); + LocalDateTime oneEndTime3 = LocalDateTime.of(2022, 1, 1, 11, 50, 10); + + //真实请假数据 + LocalDateTime realStartTime = LocalDateTime.of(2022, 1, 1, 11, 49, 10); + LocalDateTime realEndTime = LocalDateTime.of(2022, 1, 1, 12, 0, 10); + + Assert.assertTrue(LocalDateTimeUtil.isOverlap(oneStartTime,oneEndTime,realStartTime,realEndTime)); + Assert.assertTrue(LocalDateTimeUtil.isOverlap(oneStartTime2,oneEndTime2,realStartTime,realEndTime)); + Assert.assertFalse(LocalDateTimeUtil.isOverlap(oneStartTime3,oneEndTime3,realStartTime,realEndTime)); + } } diff --git a/hutool-core/src/test/java/cn/hutool/core/io/FileUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/io/FileUtilTest.java index 712b19b5a..13bf3382f 100644 --- a/hutool-core/src/test/java/cn/hutool/core/io/FileUtilTest.java +++ b/hutool-core/src/test/java/cn/hutool/core/io/FileUtilTest.java @@ -1,5 +1,6 @@ package cn.hutool.core.io; +import cn.hutool.core.collection.ListUtil; import cn.hutool.core.io.file.LineSeparator; import cn.hutool.core.lang.Console; import cn.hutool.core.util.CharsetUtil; @@ -444,4 +445,11 @@ public class FileUtilTest { File file2 = new File("."); Assert.assertTrue(FileUtil.isSub(file, file2)); } + + @Test + @Ignore + public void appendLinesTest(){ + List list = ListUtil.toList("a", "b", "c"); + FileUtil.appendLines(list, FileUtil.file("d:/test/appendLines.txt"), CharsetUtil.CHARSET_UTF_8); + } } diff --git a/hutool-core/src/test/java/cn/hutool/core/lang/OptTest.java b/hutool-core/src/test/java/cn/hutool/core/lang/OptTest.java index 0bdab907d..41d2d19c3 100644 --- a/hutool-core/src/test/java/cn/hutool/core/lang/OptTest.java +++ b/hutool-core/src/test/java/cn/hutool/core/lang/OptTest.java @@ -187,6 +187,21 @@ public class OptTest { Assert.assertEquals(indexOut, indexOutSituation); Assert.assertEquals("hutool", npe); Assert.assertEquals("hutool", indexOut); + + // 多线程下情况测试 + Stream.iterate(0, i -> ++i).limit(20000).parallel().forEach(i -> { + Opt opt = Opt.ofTry(() -> { + if (i % 2 == 0) { + throw new IllegalStateException(i + ""); + } else { + throw new NullPointerException(i + ""); + } + }); + Assert.assertTrue( + (i % 2 == 0 && opt.getException() instanceof IllegalStateException) || + (i % 2 != 0 && opt.getException() instanceof NullPointerException) + ); + }); } @Data diff --git a/hutool-core/src/test/java/cn/hutool/core/net/UrlBuilderTest.java b/hutool-core/src/test/java/cn/hutool/core/net/UrlBuilderTest.java index 07c6e28ba..764167662 100644 --- a/hutool-core/src/test/java/cn/hutool/core/net/UrlBuilderTest.java +++ b/hutool-core/src/test/java/cn/hutool/core/net/UrlBuilderTest.java @@ -251,13 +251,23 @@ public class UrlBuilderTest { Assert.assertEquals(urlStr, urlBuilder.toString()); } + @Test + public void encodePathTest2(){ + // https://gitee.com/dromara/hutool/issues/I4RA42 + // Path中`:`在第一个segment需要转义,之后的不需要 + final String urlStr = "https://hutool.cn/aa/bb/Pre-K,Kindergarten,First,Second,Third,Fourth,Fifth/Page:3"; + final UrlBuilder urlBuilder = UrlBuilder.ofHttp(urlStr, CharsetUtil.CHARSET_UTF_8); + Assert.assertEquals(urlStr, urlBuilder.toString()); + } + @Test public void gimg2Test(){ String url = "https://gimg2.baidu.com/image_search/src=http%3A%2F%2Fpic.jj20.com%2Fup%2Fallimg%2F1114%2F0H320120Z3%2F200H3120Z3-6-1200.jpg&refer=http%3A%2F%2Fpic.jj20.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1621996490&t=8c384c2823ea453da15a1b9cd5183eea"; final UrlBuilder urlBuilder = UrlBuilder.of(url); - - Assert.assertEquals(url, urlBuilder.toString()); + // PATH除了第一个path外,:是允许的 + String url2 = "https://gimg2.baidu.com/image_search/src=http:%2F%2Fpic.jj20.com%2Fup%2Fallimg%2F1114%2F0H320120Z3%2F200H3120Z3-6-1200.jpg&refer=http:%2F%2Fpic.jj20.com&app=2002&size=f9999,10000&q=a80&n=0&g=0n&fmt=jpeg?sec=1621996490&t=8c384c2823ea453da15a1b9cd5183eea"; + Assert.assertEquals(url2, urlBuilder.toString()); } @Test diff --git a/hutool-core/src/test/java/cn/hutool/core/text/split/StrSpliterTest.java b/hutool-core/src/test/java/cn/hutool/core/text/split/StrSplitterTest.java similarity index 73% rename from hutool-core/src/test/java/cn/hutool/core/text/split/StrSpliterTest.java rename to hutool-core/src/test/java/cn/hutool/core/text/split/StrSplitterTest.java index 8458f70f6..16f812f69 100644 --- a/hutool-core/src/test/java/cn/hutool/core/text/split/StrSpliterTest.java +++ b/hutool-core/src/test/java/cn/hutool/core/text/split/StrSplitterTest.java @@ -11,7 +11,7 @@ import java.util.List; * @author Looly * */ -public class StrSpliterTest { +public class StrSplitterTest { @Test public void splitByCharTest(){ @@ -71,4 +71,22 @@ public class StrSpliterTest { Assert.assertNotNull(strings); Assert.assertEquals(0, strings.length); } + + /** + * https://github.com/dromara/hutool/issues/2099 + */ + @Test + public void splitByRegexTest(){ + String text = "01 821 34567890182345617821"; + List strings = StrSplitter.splitByRegex(text, "21", 0, false, true); + Assert.assertEquals(2, strings.size()); + Assert.assertEquals("01 8", strings.get(0)); + Assert.assertEquals(" 345678901823456178", strings.get(1)); + + strings = StrSplitter.splitByRegex(text, "21", 0, false, false); + Assert.assertEquals(3, strings.size()); + Assert.assertEquals("01 8", strings.get(0)); + Assert.assertEquals(" 345678901823456178", strings.get(1)); + Assert.assertEquals("", strings.get(2)); + } } diff --git a/hutool-core/src/test/java/cn/hutool/core/util/IdUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/util/IdUtilTest.java index b68d2f20b..f0dd9c5ab 100644 --- a/hutool-core/src/test/java/cn/hutool/core/util/IdUtilTest.java +++ b/hutool-core/src/test/java/cn/hutool/core/util/IdUtilTest.java @@ -137,7 +137,8 @@ public class IdUtilTest { @Test public void getDataCenterIdTest(){ + //按照mac地址算法拼接的算法,maxDatacenterId应该是0xffffffffL>>6-1此处暂时按照0x7fffffffffffffffL-1,防止最后取模溢出 final long dataCenterId = IdUtil.getDataCenterId(Long.MAX_VALUE); - Assert.assertTrue(dataCenterId > 1); + Assert.assertTrue(dataCenterId >= 0); } } diff --git a/hutool-core/src/test/java/cn/hutool/core/util/RandomUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/util/RandomUtilTest.java index 978fcd2ed..e61e5bcbf 100644 --- a/hutool-core/src/test/java/cn/hutool/core/util/RandomUtilTest.java +++ b/hutool-core/src/test/java/cn/hutool/core/util/RandomUtilTest.java @@ -8,6 +8,7 @@ import org.junit.Test; import java.math.RoundingMode; import java.util.List; +import java.util.Locale; import java.util.Set; public class RandomUtilTest { @@ -59,4 +60,16 @@ public class RandomUtilTest { char c = RandomUtil.randomChinese(); Assert.assertTrue(c > 0); } + + @Test + @Ignore + public void randomStringWithoutStrTest() { + for (int i = 0; i < 100; i++) { + final String s = RandomUtil.randomStringWithoutStr(8, "0IPOL"); + System.out.println(s); + for (char c : "0IPOL".toCharArray()) { + Assert.assertFalse(s.contains((String.valueOf(c).toLowerCase(Locale.ROOT)))); + } + } + } } diff --git a/hutool-cron/pom.xml b/hutool-cron/pom.xml index d95c455b4..f0929496c 100644 --- a/hutool-cron/pom.xml +++ b/hutool-cron/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 5.7.20-SNAPSHOT + 5.7.20 hutool-cron diff --git a/hutool-crypto/pom.xml b/hutool-crypto/pom.xml index 50f7356ac..7105bd7f5 100644 --- a/hutool-crypto/pom.xml +++ b/hutool-crypto/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 5.7.20-SNAPSHOT + 5.7.20 hutool-crypto diff --git a/hutool-db/pom.xml b/hutool-db/pom.xml index 92fe54fa8..4d6d83222 100644 --- a/hutool-db/pom.xml +++ b/hutool-db/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 5.7.20-SNAPSHOT + 5.7.20 hutool-db diff --git a/hutool-db/src/main/java/cn/hutool/db/AbstractDb.java b/hutool-db/src/main/java/cn/hutool/db/AbstractDb.java index c18399029..700a92027 100644 --- a/hutool-db/src/main/java/cn/hutool/db/AbstractDb.java +++ b/hutool-db/src/main/java/cn/hutool/db/AbstractDb.java @@ -200,9 +200,9 @@ public abstract class AbstractDb implements Serializable { * 执行自定义的{@link PreparedStatement},结果使用{@link RsHandler}处理
* 此方法主要用于自定义场景,如游标查询等 * - * @param 结果集需要处理的对象类型 + * @param 结果集需要处理的对象类型 * @param statementFunc 自定义{@link PreparedStatement}创建函数 - * @param rsh 结果集处理对象 + * @param rsh 结果集处理对象 * @return 结果对象 * @throws SQLException SQL执行异常 * @since 5.7.17 @@ -369,6 +369,26 @@ public abstract class AbstractDb implements Serializable { } } + /** + * 使用upsert语义插入或更新数据
+ * 根据给定的字段名查询数据,如果存在则更新这些数据,否则执行插入 + * 如果方言未实现本方法,内部会自动调用insertOrUpdate来实现功能,由于upsert和insert使用有区别,为了兼容性保留原有insertOrUpdate不做变动 + * @param record 记录 + * @param keys 需要检查唯一性的字段 + * @return 插入行数 + * @throws SQLException SQL执行异常 + * @since 5.7.21 + */ + public int upsert(Entity record, String... keys) throws SQLException { + Connection conn = null; + try { + conn = this.getConnection(); + return runner.upsert(conn, record, keys); + } finally { + this.closeConnection(conn); + } + } + /** * 批量插入数据
* 需要注意的是,批量插入每一条数据结构必须一致。批量插入数据时会获取第一条数据的字段结构,之后的数据会按照这个格式插入。
@@ -864,7 +884,7 @@ public abstract class AbstractDb implements Serializable { /** * 分页查询 * - * @param 处理结果类型,可以将ResultSet转换为给定类型 + * @param 处理结果类型,可以将ResultSet转换为给定类型 * @param sql SQL构建器 * @param page 分页对象 * @param rsh 结果集处理对象 @@ -884,8 +904,8 @@ public abstract class AbstractDb implements Serializable { /** * 分页查询 * - * @param sql SQL语句字符串 - * @param page 分页对象 + * @param sql SQL语句字符串 + * @param page 分页对象 * @param params 参数列表 * @return 结果对象 * @throws SQLException SQL执行异常 diff --git a/hutool-db/src/main/java/cn/hutool/db/DialectRunner.java b/hutool-db/src/main/java/cn/hutool/db/DialectRunner.java index 43d0574e3..24c535c4b 100644 --- a/hutool-db/src/main/java/cn/hutool/db/DialectRunner.java +++ b/hutool-db/src/main/java/cn/hutool/db/DialectRunner.java @@ -86,6 +86,55 @@ public class DialectRunner implements Serializable { } } + /** + * 更新或插入数据
+ * 此方法不会关闭Connection + * 如果方言未实现此方法则内部自动使用insertOrUpdate来替代功能 + * + * @param conn 数据库连接 + * @param record 记录 + * @param keys 需要检查唯一性的字段 + * @return 插入行数 + * @throws SQLException SQL执行异常 + * @since 5.7.20 + */ + public int upsert(Connection conn, Entity record, String... keys) throws SQLException { + PreparedStatement ps = null; + try{ + ps = getDialect().psForUpsert(conn, record, keys); + }catch (SQLException ignore){ + // 方言不支持,使用默认 + } + if (null != ps) { + try { + return ps.executeUpdate(); + } finally { + DbUtil.close(ps); + } + } else { + return insertOrUpdate(conn, record, keys); + } + } + + /** + * 插入或更新数据
+ * 此方法不会关闭Connection + * + * @param conn 数据库连接 + * @param record 记录 + * @param keys 需要检查唯一性的字段 + * @return 插入行数 + * @throws SQLException SQL执行异常 + */ + public int insertOrUpdate(Connection conn, Entity record, String... keys) throws SQLException { + final Entity where = record.filter(keys); + if (MapUtil.isNotEmpty(where) && count(conn, where) > 0) { + return update(conn, record, where); + } else { + return insert(conn, record)[0]; + } + } + /** * 插入数据
* 此方法不会关闭Connection @@ -212,7 +261,7 @@ public class DialectRunner implements Serializable { * 获取查询结果总数,生成类似于 SELECT count(1) from (sql) hutool_alias_count_
* 此方法会重新构建{@link SqlBuilder},并去除末尾的order by子句 * - * @param conn 数据库连接对象 + * @param conn 数据库连接对象 * @param sqlBuilder 查询语句 * @return 复合条件的结果数 * @throws SQLException SQL执行异常 diff --git a/hutool-db/src/main/java/cn/hutool/db/SqlConnRunner.java b/hutool-db/src/main/java/cn/hutool/db/SqlConnRunner.java index e21d8c5af..28f1bfc2b 100644 --- a/hutool-db/src/main/java/cn/hutool/db/SqlConnRunner.java +++ b/hutool-db/src/main/java/cn/hutool/db/SqlConnRunner.java @@ -1,7 +1,6 @@ package cn.hutool.db; import cn.hutool.core.collection.CollUtil; -import cn.hutool.core.map.MapUtil; import cn.hutool.db.dialect.Dialect; import cn.hutool.db.dialect.DialectFactory; import cn.hutool.db.handler.EntityListHandler; @@ -22,7 +21,8 @@ import java.util.List; /** * SQL执行类
* 此执行类只接受方言参数,不需要数据源,只有在执行方法时需要数据库连接对象
- * 此对象存在的意义在于,可以由使用者自定义数据库连接对象,并执行多个方法,方便事务的统一控制或减少连接对象的创建关闭 + * 此对象存在的意义在于,可以由使用者自定义数据库连接对象,并执行多个方法,方便事务的统一控制或减少连接对象的创建关闭
+ * 相比{@link DialectRunner},此类中提供了更多重载方法 * * @author Luxiaolei */ @@ -82,25 +82,6 @@ public class SqlConnRunner extends DialectRunner { //---------------------------------------------------------------------------- CRUD start - /** - * 插入或更新数据
- * 此方法不会关闭Connection - * - * @param conn 数据库连接 - * @param record 记录 - * @param keys 需要检查唯一性的字段 - * @return 插入行数 - * @throws SQLException SQL执行异常 - */ - public int insertOrUpdate(Connection conn, Entity record, String... keys) throws SQLException { - final Entity where = record.filter(keys); - if (MapUtil.isNotEmpty(where) && count(conn, where) > 0) { - return update(conn, record, where); - } else { - return insert(conn, record); - } - } - /** * 批量插入数据
* 需要注意的是,批量插入每一条数据结构必须一致。批量插入数据时会获取第一条数据的字段结构,之后的数据会按照这个格式插入。
diff --git a/hutool-db/src/main/java/cn/hutool/db/StatementUtil.java b/hutool-db/src/main/java/cn/hutool/db/StatementUtil.java index b3a6eed12..b821c819e 100644 --- a/hutool-db/src/main/java/cn/hutool/db/StatementUtil.java +++ b/hutool-db/src/main/java/cn/hutool/db/StatementUtil.java @@ -194,7 +194,7 @@ public class StatementUtil { * @throws SQLException SQL异常 * @since 4.6.7 */ - public static PreparedStatement prepareStatementForBatch(Connection conn, String sql, List fields, Entity... entities) throws SQLException { + public static PreparedStatement prepareStatementForBatch(Connection conn, String sql, Iterable fields, Entity... entities) throws SQLException { Assert.notBlank(sql, "Sql String must be not blank!"); sql = sql.trim(); diff --git a/hutool-db/src/main/java/cn/hutool/db/dialect/Dialect.java b/hutool-db/src/main/java/cn/hutool/db/dialect/Dialect.java index 7eacc8d14..6ad5922bd 100644 --- a/hutool-db/src/main/java/cn/hutool/db/dialect/Dialect.java +++ b/hutool-db/src/main/java/cn/hutool/db/dialect/Dialect.java @@ -37,7 +37,8 @@ public interface Dialect extends Serializable { // -------------------------------------------- Execute /** - * 构建用于插入的PreparedStatement + * 构建用于插入的{@link PreparedStatement}
+ * 用户实现需按照数据库方言格式,将{@link Entity}转换为带有占位符的SQL语句及参数列表 * * @param conn 数据库连接对象 * @param entity 数据实体类(包含表名) @@ -47,7 +48,8 @@ public interface Dialect extends Serializable { PreparedStatement psForInsert(Connection conn, Entity entity) throws SQLException; /** - * 构建用于批量插入的PreparedStatement + * 构建用于批量插入的PreparedStatement
+ * 用户实现需按照数据库方言格式,将{@link Entity}转换为带有占位符的SQL语句及参数列表 * * @param conn 数据库连接对象 * @param entities 数据实体,实体的结构必须全部一致,否则插入结果将不可预知 @@ -57,7 +59,9 @@ public interface Dialect extends Serializable { PreparedStatement psForInsertBatch(Connection conn, Entity... entities) throws SQLException; /** - * 构建用于删除的PreparedStatement + * 构建用于删除的{@link PreparedStatement}
+ * 用户实现需按照数据库方言格式,将{@link Query}转换为带有占位符的SQL语句及参数列表
+ * {@link Query}中包含了删除所需的表名、查询条件等信息,可借助SqlBuilder完成SQL语句生成。 * * @param conn 数据库连接对象 * @param query 查找条件(包含表名) @@ -67,7 +71,9 @@ public interface Dialect extends Serializable { PreparedStatement psForDelete(Connection conn, Query query) throws SQLException; /** - * 构建用于更新的PreparedStatement + * 构建用于更新的{@link PreparedStatement}
+ * 用户实现需按照数据库方言格式,将{@link Entity}配合{@link Query}转换为带有占位符的SQL语句及参数列表
+ * 其中{@link Entity}中包含需要更新的数据信息,{@link Query}包含更新的查找条件信息。 * * @param conn 数据库连接对象 * @param entity 数据实体类(包含表名) @@ -80,7 +86,9 @@ public interface Dialect extends Serializable { // -------------------------------------------- Query /** - * 构建用于获取多条记录的PreparedStatement + * 构建用于获取多条记录的{@link PreparedStatement}
+ * 用户实现需按照数据库方言格式,将{@link Query}转换为带有占位符的SQL语句及参数列表
+ * {@link Query}中包含了查询所需的表名、查询条件等信息,可借助SqlBuilder完成SQL语句生成。 * * @param conn 数据库连接对象 * @param query 查询条件(包含表名) @@ -90,7 +98,9 @@ public interface Dialect extends Serializable { PreparedStatement psForFind(Connection conn, Query query) throws SQLException; /** - * 构建用于分页查询的PreparedStatement + * 构建用于分页查询的{@link PreparedStatement}
+ * 用户实现需按照数据库方言格式,将{@link Query}转换为带有占位符的SQL语句及参数列表
+ * {@link Query}中包含了分页查询所需的表名、查询条件、分页等信息,可借助SqlBuilder完成SQL语句生成。 * * @param conn 数据库连接对象 * @param query 查询条件(包含表名) @@ -100,7 +110,7 @@ public interface Dialect extends Serializable { PreparedStatement psForPage(Connection conn, Query query) throws SQLException; /** - * 构建用于分页查询的PreparedStatement
+ * 构建用于分页查询的{@link PreparedStatement}
* 可以在此方法中使用{@link SqlBuilder#orderBy(Order...)}方法加入排序信息, * 排序信息通过{@link Page#getOrders()}获取 * @@ -114,28 +124,32 @@ public interface Dialect extends Serializable { PreparedStatement psForPage(Connection conn, SqlBuilder sqlBuilder, Page page) throws SQLException; /** - * 构建用于查询行数的PreparedStatement + * 构建用于查询行数的{@link PreparedStatement}
+ * 用户实现需按照数据库方言格式,将{@link Query}转换为带有占位符的SQL语句及参数列表
+ * {@link Query}中包含了表名、查询条件等信息,可借助SqlBuilder完成SQL语句生成。 * * @param conn 数据库连接对象 * @param query 查询条件(包含表名) * @return PreparedStatement * @throws SQLException SQL执行异常 */ - default PreparedStatement psForCount(Connection conn, Query query) throws SQLException{ + default PreparedStatement psForCount(Connection conn, Query query) throws SQLException { query.setFields(ListUtil.toList("count(1)")); return psForFind(conn, query); } /** - * 构建用于查询行数的PreparedStatement + * 构建用于查询行数的{@link PreparedStatement}
+ * 用户实现需按照数据库方言格式,将{@link Query}转换为带有占位符的SQL语句及参数列表
+ * {@link Query}中包含了表名、查询条件等信息,可借助SqlBuilder完成SQL语句生成。 * - * @param conn 数据库连接对象 + * @param conn 数据库连接对象 * @param sqlBuilder 查询语句,应该包含分页等信息 * @return PreparedStatement * @throws SQLException SQL执行异常 * @since 5.7.2 */ - default PreparedStatement psForCount(Connection conn, SqlBuilder sqlBuilder) throws SQLException{ + default PreparedStatement psForCount(Connection conn, SqlBuilder sqlBuilder) throws SQLException { sqlBuilder = sqlBuilder .insertPreFragment("SELECT count(1) from(") // issue#I3IJ8X@Gitee,在子查询时需设置单独别名,此处为了防止和用户的表名冲突,使用自定义的较长别名 @@ -143,6 +157,22 @@ public interface Dialect extends Serializable { return psForPage(conn, sqlBuilder, null); } + /** + * 构建用于upsert的{@link PreparedStatement}
+ * 方言实现需实现此默认方法,如果没有实现,抛出{@link SQLException} + * + * @param conn 数据库连接对象 + * @param entity 数据实体类(包含表名) + * @param keys 查找字段,某些数据库此字段必须,如H2,某些数据库无需此字段,如MySQL(通过主键) + * @return PreparedStatement + * @throws SQLException SQL执行异常,或方言数据不支持此操作 + * @since 5.7.20 + */ + default PreparedStatement psForUpsert(Connection conn, Entity entity, String... keys) throws SQLException { + throw new SQLException("Unsupported upsert operation of " + dialectName()); + } + + /** * 方言名 * diff --git a/hutool-db/src/main/java/cn/hutool/db/dialect/DialectFactory.java b/hutool-db/src/main/java/cn/hutool/db/dialect/DialectFactory.java index 5f6e76194..42d81cc58 100644 --- a/hutool-db/src/main/java/cn/hutool/db/dialect/DialectFactory.java +++ b/hutool-db/src/main/java/cn/hutool/db/dialect/DialectFactory.java @@ -149,6 +149,9 @@ public class DialectFactory implements DriverNamePool{ } else if (nameContainsProductInfo.contains("sybase")) { // 神州数据库 driver = DRIVER_SYBASE; + } else if (nameContainsProductInfo.contains("xugu")) { + // 虚谷数据库 + driver = DRIVER_XUGO; } return driver; diff --git a/hutool-db/src/main/java/cn/hutool/db/dialect/DriverNamePool.java b/hutool-db/src/main/java/cn/hutool/db/dialect/DriverNamePool.java index 6da118859..56392945a 100644 --- a/hutool-db/src/main/java/cn/hutool/db/dialect/DriverNamePool.java +++ b/hutool-db/src/main/java/cn/hutool/db/dialect/DriverNamePool.java @@ -108,5 +108,9 @@ public interface DriverNamePool { * JDBC 驱动 Sybase */ String DRIVER_SYBASE = "com.sybase.jdbc4.jdbc.SybDriver"; + /** + * JDBC 驱动 虚谷 + */ + String DRIVER_XUGO = "com.xugu.cloudjdbc.Driver"; } diff --git a/hutool-db/src/main/java/cn/hutool/db/dialect/impl/AnsiSqlDialect.java b/hutool-db/src/main/java/cn/hutool/db/dialect/impl/AnsiSqlDialect.java index 3793d41f0..a615a45f3 100644 --- a/hutool-db/src/main/java/cn/hutool/db/dialect/impl/AnsiSqlDialect.java +++ b/hutool-db/src/main/java/cn/hutool/db/dialect/impl/AnsiSqlDialect.java @@ -1,5 +1,6 @@ package cn.hutool.db.dialect.impl; +import cn.hutool.core.collection.CollUtil; import cn.hutool.core.lang.Assert; import cn.hutool.core.util.ArrayUtil; import cn.hutool.core.util.StrUtil; @@ -17,16 +18,17 @@ import cn.hutool.db.sql.Wrapper; import java.sql.Connection; import java.sql.PreparedStatement; import java.sql.SQLException; +import java.util.Set; /** * ANSI SQL 方言 - * + * * @author loolly * */ public class AnsiSqlDialect implements Dialect { private static final long serialVersionUID = 2088101129774974580L; - + protected Wrapper wrapper = new Wrapper(); @Override @@ -53,7 +55,8 @@ public class AnsiSqlDialect implements Dialect { } // 批量,根据第一行数据结构生成SQL占位符 final SqlBuilder insert = SqlBuilder.create(wrapper).insert(entities[0], this.dialectName()); - return StatementUtil.prepareStatementForBatch(conn, insert.build(), insert.getFields(), entities); + final Set fields = CollUtil.filter(entities[0].keySet(), StrUtil::isNotBlank); + return StatementUtil.prepareStatementForBatch(conn, insert.build(), fields, entities); } @Override @@ -113,7 +116,7 @@ public class AnsiSqlDialect implements Dialect { /** * 根据不同数据库在查询SQL语句基础上包装其分页的语句
* 各自数据库通过重写此方法实现最小改动情况下修改分页语句 - * + * * @param find 标准查询语句 * @param page 分页对象 * @return 分页语句 diff --git a/hutool-db/src/main/java/cn/hutool/db/dialect/impl/H2Dialect.java b/hutool-db/src/main/java/cn/hutool/db/dialect/impl/H2Dialect.java index 96311bb0f..110aea5f6 100644 --- a/hutool-db/src/main/java/cn/hutool/db/dialect/impl/H2Dialect.java +++ b/hutool-db/src/main/java/cn/hutool/db/dialect/impl/H2Dialect.java @@ -1,9 +1,18 @@ package cn.hutool.db.dialect.impl; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.db.Entity; import cn.hutool.db.Page; +import cn.hutool.db.StatementUtil; import cn.hutool.db.dialect.DialectName; import cn.hutool.db.sql.SqlBuilder; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; + /** * H2数据库方言 * @@ -26,4 +35,43 @@ public class H2Dialect extends AnsiSqlDialect { // limit A , B 表示:A就是查询的起点位置,B就是你需要多少行。 return find.append(" limit ").append(page.getStartPosition()).append(" , ").append(page.getPageSize()); } + + @Override + public PreparedStatement psForUpsert(Connection conn, Entity entity, String... keys) throws SQLException { + Assert.notEmpty(keys, "Keys must be not empty for H2 MERGE SQL."); + SqlBuilder.validateEntity(entity); + final SqlBuilder builder = SqlBuilder.create(wrapper); + + final StringBuilder fieldsPart = new StringBuilder(); + final StringBuilder placeHolder = new StringBuilder(); + + // 构建字段部分和参数占位符部分 + entity.forEach((field, value)->{ + if (StrUtil.isNotBlank(field)) { + if (fieldsPart.length() > 0) { + // 非第一个参数,追加逗号 + fieldsPart.append(", "); + placeHolder.append(", "); + } + + fieldsPart.append((null != wrapper) ? wrapper.wrap(field) : field); + placeHolder.append("?"); + builder.addParams(value); + } + }); + + String tableName = entity.getTableName(); + if (null != this.wrapper) { + tableName = this.wrapper.wrap(tableName); + } + builder.append("MERGE INTO ").append(tableName) + // 字段列表 + .append(" (").append(fieldsPart) + // 更新关键字列表 + .append(") KEY(").append(ArrayUtil.join(keys, ", ")) + // 更新值列表 + .append(") VALUES (").append(placeHolder).append(")"); + + return StatementUtil.prepareStatement(conn, builder); + } } diff --git a/hutool-db/src/main/java/cn/hutool/db/dialect/impl/MysqlDialect.java b/hutool-db/src/main/java/cn/hutool/db/dialect/impl/MysqlDialect.java index d10e811fb..a52cfa759 100644 --- a/hutool-db/src/main/java/cn/hutool/db/dialect/impl/MysqlDialect.java +++ b/hutool-db/src/main/java/cn/hutool/db/dialect/impl/MysqlDialect.java @@ -1,10 +1,17 @@ package cn.hutool.db.dialect.impl; +import cn.hutool.core.util.StrUtil; +import cn.hutool.db.Entity; import cn.hutool.db.Page; +import cn.hutool.db.StatementUtil; import cn.hutool.db.dialect.DialectName; import cn.hutool.db.sql.SqlBuilder; import cn.hutool.db.sql.Wrapper; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; + /** * MySQL方言 * @author loolly @@ -21,9 +28,65 @@ public class MysqlDialect extends AnsiSqlDialect{ protected SqlBuilder wrapPageSql(SqlBuilder find, Page page) { return find.append(" LIMIT ").append(page.getStartPosition()).append(", ").append(page.getPageSize()); } - + @Override public String dialectName() { return DialectName.MYSQL.toString(); } + + /** + * 构建用于upsert的{@link PreparedStatement}
+ * MySQL通过主键方式实现Upsert,故keys无效,生成SQL语法为: + *
+	 *     INSERT INTO demo(a,b,c) values(?, ?, ?) ON DUPLICATE KEY UPDATE a=values(a), b=values(b), c=values(c);
+	 * 
+ * + * @param conn 数据库连接对象 + * @param entity 数据实体类(包含表名) + * @param keys 此参数无效 + * @return PreparedStatement + * @throws SQLException SQL执行异常 + * @since 5.7.20 + */ + @Override + public PreparedStatement psForUpsert(Connection conn, Entity entity, String... keys) throws SQLException { + SqlBuilder.validateEntity(entity); + final SqlBuilder builder = SqlBuilder.create(wrapper); + + final StringBuilder fieldsPart = new StringBuilder(); + final StringBuilder placeHolder = new StringBuilder(); + final StringBuilder updateHolder = new StringBuilder(); + + // 构建字段部分和参数占位符部分 + entity.forEach((field, value)->{ + if (StrUtil.isNotBlank(field)) { + if (fieldsPart.length() > 0) { + // 非第一个参数,追加逗号 + fieldsPart.append(", "); + placeHolder.append(", "); + updateHolder.append(", "); + } + + field = (null != wrapper) ? wrapper.wrap(field) : field; + fieldsPart.append(field); + updateHolder.append(field).append("=values(").append(field).append(")"); + placeHolder.append("?"); + builder.addParams(value); + } + }); + + String tableName = entity.getTableName(); + if (null != this.wrapper) { + tableName = this.wrapper.wrap(tableName); + } + builder.append("INSERT INTO ").append(tableName) + // 字段列表 + .append(" (").append(fieldsPart) + // 更新值列表 + .append(") VALUES (").append(placeHolder) + // 主键冲突后的更新操作 + .append(") ON DUPLICATE KEY UPDATE ").append(updateHolder); + + return StatementUtil.prepareStatement(conn, builder); + } } diff --git a/hutool-db/src/main/java/cn/hutool/db/dialect/impl/OracleDialect.java b/hutool-db/src/main/java/cn/hutool/db/dialect/impl/OracleDialect.java index 925889611..037aaf8a0 100644 --- a/hutool-db/src/main/java/cn/hutool/db/dialect/impl/OracleDialect.java +++ b/hutool-db/src/main/java/cn/hutool/db/dialect/impl/OracleDialect.java @@ -1,32 +1,44 @@ package cn.hutool.db.dialect.impl; +import cn.hutool.core.util.StrUtil; import cn.hutool.db.Page; import cn.hutool.db.dialect.DialectName; import cn.hutool.db.sql.SqlBuilder; /** * Oracle 方言 - * @author loolly * + * @author loolly */ -public class OracleDialect extends AnsiSqlDialect{ +public class OracleDialect extends AnsiSqlDialect { private static final long serialVersionUID = 6122761762247483015L; + /** + * 检查字段值是否为Oracle自增字段,自增字段以`.nextval`结尾 + * + * @param value 检查的字段值 + * @return 是否为Oracle自增字段 + * @since 5.7.20 + */ + public static boolean isNextVal(Object value) { + return (value instanceof CharSequence) && StrUtil.endWithIgnoreCase(value.toString(), ".nextval"); + } + public OracleDialect() { //Oracle所有字段名用双引号包围,防止字段名或表名与系统关键字冲突 //wrapper = new Wrapper('"'); } - + @Override protected SqlBuilder wrapPageSql(SqlBuilder find, Page page) { final int[] startEnd = page.getStartEnd(); return find - .insertPreFragment("SELECT * FROM ( SELECT row_.*, rownum rownum_ from ( ") - .append(" ) row_ where rownum <= ").append(startEnd[1])// - .append(") table_alias")// - .append(" where table_alias.rownum_ > ").append(startEnd[0]);// + .insertPreFragment("SELECT * FROM ( SELECT row_.*, rownum rownum_ from ( ") + .append(" ) row_ where rownum <= ").append(startEnd[1])// + .append(") table_alias")// + .append(" where table_alias.rownum_ > ").append(startEnd[0]);// } - + @Override public String dialectName() { return DialectName.ORACLE.name(); diff --git a/hutool-db/src/main/java/cn/hutool/db/dialect/impl/PhoenixDialect.java b/hutool-db/src/main/java/cn/hutool/db/dialect/impl/PhoenixDialect.java index c2ad5bdd5..8064e8982 100644 --- a/hutool-db/src/main/java/cn/hutool/db/dialect/impl/PhoenixDialect.java +++ b/hutool-db/src/main/java/cn/hutool/db/dialect/impl/PhoenixDialect.java @@ -24,6 +24,7 @@ public class PhoenixDialect extends AnsiSqlDialect { @Override public PreparedStatement psForUpdate(Connection conn, Entity entity, Query query) throws SQLException { // Phoenix的插入、更新语句是统一的,统一使用upsert into关键字 + // Phoenix只支持通过主键更新操作,因此query无效,自动根据entity中的主键更新 return super.psForInsert(conn, entity); } @@ -31,4 +32,10 @@ public class PhoenixDialect extends AnsiSqlDialect { public String dialectName() { return DialectName.PHOENIX.name(); } + + @Override + public PreparedStatement psForUpsert(Connection conn, Entity entity, String... keys) throws SQLException { + // Phoenix只支持通过主键更新操作,因此query无效,自动根据entity中的主键更新 + return psForInsert(conn, entity); + } } diff --git a/hutool-db/src/main/java/cn/hutool/db/dialect/impl/PostgresqlDialect.java b/hutool-db/src/main/java/cn/hutool/db/dialect/impl/PostgresqlDialect.java index d7109e3c2..1f5a90122 100644 --- a/hutool-db/src/main/java/cn/hutool/db/dialect/impl/PostgresqlDialect.java +++ b/hutool-db/src/main/java/cn/hutool/db/dialect/impl/PostgresqlDialect.java @@ -1,8 +1,18 @@ package cn.hutool.db.dialect.impl; +import cn.hutool.core.lang.Assert; +import cn.hutool.core.util.ArrayUtil; +import cn.hutool.core.util.StrUtil; +import cn.hutool.db.Entity; +import cn.hutool.db.StatementUtil; import cn.hutool.db.dialect.DialectName; +import cn.hutool.db.sql.SqlBuilder; import cn.hutool.db.sql.Wrapper; +import java.sql.Connection; +import java.sql.PreparedStatement; +import java.sql.SQLException; + /** * Postgree方言 @@ -20,4 +30,49 @@ public class PostgresqlDialect extends AnsiSqlDialect{ public String dialectName() { return DialectName.POSTGREESQL.name(); } + + @Override + public PreparedStatement psForUpsert(Connection conn, Entity entity, String... keys) throws SQLException { + Assert.notEmpty(keys, "Keys must be not empty for Postgres."); + SqlBuilder.validateEntity(entity); + final SqlBuilder builder = SqlBuilder.create(wrapper); + + final StringBuilder fieldsPart = new StringBuilder(); + final StringBuilder placeHolder = new StringBuilder(); + final StringBuilder updateHolder = new StringBuilder(); + + // 构建字段部分和参数占位符部分 + entity.forEach((field, value)->{ + if (StrUtil.isNotBlank(field)) { + if (fieldsPart.length() > 0) { + // 非第一个参数,追加逗号 + fieldsPart.append(", "); + placeHolder.append(", "); + updateHolder.append(", "); + } + + final String wrapedField = (null != wrapper) ? wrapper.wrap(field) : field; + fieldsPart.append(wrapedField); + updateHolder.append(wrapedField).append("=EXCLUDED.").append(field); + placeHolder.append("?"); + builder.addParams(value); + } + }); + + String tableName = entity.getTableName(); + if (null != this.wrapper) { + tableName = this.wrapper.wrap(tableName); + } + builder.append("INSERT INTO ").append(tableName) + // 字段列表 + .append(" (").append(fieldsPart) + // 更新值列表 + .append(") VALUES (").append(placeHolder) + // 定义检查冲突的主键或字段 + .append(") ON CONFLICT (").append(ArrayUtil.join(keys,", ")) + // 主键冲突后的更新操作 + .append(") DO UPDATE SET ").append(updateHolder); + + return StatementUtil.prepareStatement(conn, builder); + } } diff --git a/hutool-db/src/main/java/cn/hutool/db/sql/SqlBuilder.java b/hutool-db/src/main/java/cn/hutool/db/sql/SqlBuilder.java index 95d15ed8a..a2a52634d 100644 --- a/hutool-db/src/main/java/cn/hutool/db/sql/SqlBuilder.java +++ b/hutool-db/src/main/java/cn/hutool/db/sql/SqlBuilder.java @@ -7,13 +7,13 @@ import cn.hutool.core.util.StrUtil; import cn.hutool.db.DbRuntimeException; import cn.hutool.db.Entity; import cn.hutool.db.dialect.DialectName; +import cn.hutool.db.dialect.impl.OracleDialect; import java.util.ArrayList; import java.util.Arrays; import java.util.Collection; import java.util.Collections; import java.util.List; -import java.util.Map.Entry; /** * SQL构建器
@@ -57,6 +57,24 @@ public class SqlBuilder implements Builder { return create().append(sql); } + /** + * 验证实体类对象的有效性 + * + * @param entity 实体类对象 + * @throws DbRuntimeException SQL异常包装,获取元数据信息失败 + */ + public static void validateEntity(Entity entity) throws DbRuntimeException { + if (null == entity) { + throw new DbRuntimeException("Entity is null !"); + } + if (StrUtil.isBlank(entity.getTableName())) { + throw new DbRuntimeException("Entity`s table name is null !"); + } + if (entity.isEmpty()) { + throw new DbRuntimeException("No filed and value in this entity !"); + } + } + // --------------------------------------------------------------- Static methods end // --------------------------------------------------------------- Enums start @@ -87,10 +105,6 @@ public class SqlBuilder implements Builder { // --------------------------------------------------------------- Enums end private final StringBuilder sql = new StringBuilder(); - /** - * 字段列表(仅用于插入和更新) - */ - private final List fields = new ArrayList<>(); /** * 占位符对应的值列表 */ @@ -146,41 +160,29 @@ public class SqlBuilder implements Builder { // 验证 validateEntity(entity); - if (null != wrapper) { - // 包装表名 entity = wrapper.wrap(entity); - entity.setTableName(wrapper.wrap(entity.getTableName())); - } - final boolean isOracle = DialectName.ORACLE.match(dialectName);// 对Oracle的特殊处理 final StringBuilder fieldsPart = new StringBuilder(); final StringBuilder placeHolder = new StringBuilder(); - boolean isFirst = true; - String field; - Object value; - for (Entry entry : entity.entrySet()) { - field = entry.getKey(); - value = entry.getValue(); - if (StrUtil.isNotBlank(field) /* && null != value */) { - if (isFirst) { - isFirst = false; - } else { + entity.forEach((field, value) -> { + if (StrUtil.isNotBlank(field)) { + if (fieldsPart.length() > 0) { // 非第一个参数,追加逗号 fieldsPart.append(", "); placeHolder.append(", "); } - this.fields.add(field); fieldsPart.append((null != wrapper) ? wrapper.wrap(field) : field); - if (isOracle && value instanceof String && StrUtil.endWithIgnoreCase((String) value, ".nextval")) { + if (isOracle && OracleDialect.isNextVal(value)) { // Oracle的特殊自增键,通过字段名.nextval获得下一个值 placeHolder.append(value); } else { + // 普通字段使用占位符 placeHolder.append("?"); this.paramValues.add(value); } } - } + }); // issue#1656@Github Phoenix兼容 if (DialectName.PHOENIX.match(dialectName)) { @@ -189,7 +191,12 @@ public class SqlBuilder implements Builder { sql.append("INSERT INTO "); } - sql.append(entity.getTableName()) + String tableName = entity.getTableName(); + if (null != this.wrapper) { + // 包装表名 entity = wrapper.wrap(entity); + tableName = this.wrapper.wrap(tableName); + } + sql.append(tableName) .append(" (").append(fieldsPart).append(") VALUES (")// .append(placeHolder).append(")"); @@ -227,26 +234,22 @@ public class SqlBuilder implements Builder { // 验证 validateEntity(entity); + String tableName = entity.getTableName(); if (null != wrapper) { // 包装表名 - // entity = wrapper.wrap(entity); - entity.setTableName(wrapper.wrap(entity.getTableName())); + tableName = wrapper.wrap(tableName); } - sql.append("UPDATE ").append(entity.getTableName()).append(" SET "); - String field; - for (Entry entry : entity.entrySet()) { - field = entry.getKey(); + sql.append("UPDATE ").append(tableName).append(" SET "); + entity.forEach((field, value) -> { if (StrUtil.isNotBlank(field)) { if (paramValues.size() > 0) { sql.append(", "); } - this.fields.add(field); sql.append((null != wrapper) ? wrapper.wrap(field) : field).append(" = ? "); - this.paramValues.add(entry.getValue());// 更新不对空做处理,因为存在清空字段的情况 + this.paramValues.add(value);// 更新不对空做处理,因为存在清空字段的情况 } - } - + }); return this; } @@ -573,24 +576,6 @@ public class SqlBuilder implements Builder { } // --------------------------------------------------------------- Builder end - /** - * 获得插入或更新的数据库字段列表 - * - * @return 插入或更新的数据库字段列表 - */ - public String[] getFieldArray() { - return this.fields.toArray(new String[0]); - } - - /** - * 获得插入或更新的数据库字段列表 - * - * @return 插入或更新的数据库字段列表 - */ - public List getFields() { - return this.fields; - } - /** * 获得占位符对应的值列表
* @@ -645,23 +630,5 @@ public class SqlBuilder implements Builder { return ConditionBuilder.of(conditions).build(this.paramValues); } - - /** - * 验证实体类对象的有效性 - * - * @param entity 实体类对象 - * @throws DbRuntimeException SQL异常包装,获取元数据信息失败 - */ - private static void validateEntity(Entity entity) throws DbRuntimeException { - if (null == entity) { - throw new DbRuntimeException("Entity is null !"); - } - if (StrUtil.isBlank(entity.getTableName())) { - throw new DbRuntimeException("Entity`s table name is null !"); - } - if (entity.isEmpty()) { - throw new DbRuntimeException("No filed and value in this entity !"); - } - } // --------------------------------------------------------------- private method end } diff --git a/hutool-db/src/main/java/cn/hutool/db/sql/Wrapper.java b/hutool-db/src/main/java/cn/hutool/db/sql/Wrapper.java index fd823804e..8851b2eac 100644 --- a/hutool-db/src/main/java/cn/hutool/db/sql/Wrapper.java +++ b/hutool-db/src/main/java/cn/hutool/db/sql/Wrapper.java @@ -7,6 +7,7 @@ import cn.hutool.core.util.CharUtil; import cn.hutool.core.util.StrUtil; import cn.hutool.db.Entity; +import java.io.Serializable; import java.util.Arrays; import java.util.Collection; import java.util.Map.Entry; @@ -14,14 +15,19 @@ import java.util.Map.Entry; /** * 包装器
* 主要用于字段名的包装(在字段名的前后加字符,例如反引号来避免与数据库的关键字冲突) - * @author Looly * + * @author Looly */ -public class Wrapper { +public class Wrapper implements Serializable { + private static final long serialVersionUID = 1L; - /** 前置包装符号 */ + /** + * 前置包装符号 + */ private Character preWrapQuote; - /** 后置包装符号 */ + /** + * 后置包装符号 + */ private Character sufWrapQuote; public Wrapper() { @@ -29,6 +35,7 @@ public class Wrapper { /** * 构造 + * * @param wrapQuote 单包装字符 */ public Wrapper(Character wrapQuote) { @@ -38,6 +45,7 @@ public class Wrapper { /** * 包装符号 + * * @param preWrapQuote 前置包装符号 * @param sufWrapQuote 后置包装符号 */ @@ -47,14 +55,17 @@ public class Wrapper { } //--------------------------------------------------------------- Getters and Setters start + /** * @return 前置包装符号 */ public char getPreWrapQuote() { return preWrapQuote; } + /** * 设置前置包装的符号 + * * @param preWrapQuote 前置包装符号 */ public void setPreWrapQuote(Character preWrapQuote) { @@ -67,8 +78,10 @@ public class Wrapper { public char getSufWrapQuote() { return sufWrapQuote; } + /** * 设置后置包装的符号 + * * @param sufWrapQuote 后置包装符号 */ public void setSufWrapQuote(Character sufWrapQuote) { @@ -79,26 +92,27 @@ public class Wrapper { /** * 包装字段名
* 有时字段与SQL的某些关键字冲突,导致SQL出错,因此需要将字段名用单引号或者反引号包装起来,避免冲突 + * * @param field 字段名 * @return 包装后的字段名 */ - public String wrap(String field){ - if(preWrapQuote == null || sufWrapQuote == null || StrUtil.isBlank(field)) { + public String wrap(String field) { + if (preWrapQuote == null || sufWrapQuote == null || StrUtil.isBlank(field)) { return field; } //如果已经包含包装的引号,返回原字符 - if(StrUtil.isSurround(field, preWrapQuote, sufWrapQuote)){ + if (StrUtil.isSurround(field, preWrapQuote, sufWrapQuote)) { return field; } //如果字段中包含通配符或者括号(字段通配符或者函数),不做包装 - if(StrUtil.containsAnyIgnoreCase(field, "*", "(", " ", " as ")) { + if (StrUtil.containsAnyIgnoreCase(field, "*", "(", " ", " as ")) { return field; } //对于Oracle这类数据库,表名中包含用户名需要单独拆分包装 - if(field.contains(StrUtil.DOT)){ + if (field.contains(StrUtil.DOT)) { final Collection target = CollUtil.edit(StrUtil.split(field, CharUtil.DOT, 2), t -> StrUtil.format("{}{}{}", preWrapQuote, t, sufWrapQuote)); return CollectionUtil.join(target, StrUtil.DOT); } @@ -109,16 +123,17 @@ public class Wrapper { /** * 包装字段名
* 有时字段与SQL的某些关键字冲突,导致SQL出错,因此需要将字段名用单引号或者反引号包装起来,避免冲突 + * * @param fields 字段名 * @return 包装后的字段名 */ - public String[] wrap(String... fields){ - if(ArrayUtil.isEmpty(fields)) { + public String[] wrap(String... fields) { + if (ArrayUtil.isEmpty(fields)) { return fields; } String[] wrappedFields = new String[fields.length]; - for(int i = 0; i < fields.length; i++) { + for (int i = 0; i < fields.length; i++) { wrappedFields[i] = wrap(fields[i]); } @@ -128,11 +143,12 @@ public class Wrapper { /** * 包装字段名
* 有时字段与SQL的某些关键字冲突,导致SQL出错,因此需要将字段名用单引号或者反引号包装起来,避免冲突 + * * @param fields 字段名 * @return 包装后的字段名 */ - public Collection wrap(Collection fields){ - if(CollectionUtil.isEmpty(fields)) { + public Collection wrap(Collection fields) { + if (CollectionUtil.isEmpty(fields)) { return fields; } @@ -140,13 +156,14 @@ public class Wrapper { } /** - * 包装字段名
+ * 包装表名和字段名,此方法返回一个新的Entity实体类
* 有时字段与SQL的某些关键字冲突,导致SQL出错,因此需要将字段名用单引号或者反引号包装起来,避免冲突 + * * @param entity 被包装的实体 - * @return 包装后的字段名 + * @return 新的实体 */ - public Entity wrap(Entity entity){ - if(null == entity) { + public Entity wrap(Entity entity) { + if (null == entity) { return null; } @@ -166,14 +183,15 @@ public class Wrapper { /** * 包装字段名
* 有时字段与SQL的某些关键字冲突,导致SQL出错,因此需要将字段名用单引号或者反引号包装起来,避免冲突 + * * @param conditions 被包装的实体 * @return 包装后的字段名 */ - public Condition[] wrap(Condition... conditions){ + public Condition[] wrap(Condition... conditions) { final Condition[] clonedConditions = new Condition[conditions.length]; - if(ArrayUtil.isNotEmpty(conditions)) { + if (ArrayUtil.isNotEmpty(conditions)) { Condition clonedCondition; - for(int i = 0; i < conditions.length; i++) { + for (int i = 0; i < conditions.length; i++) { clonedCondition = conditions[i].clone(); clonedCondition.setField(wrap(clonedCondition.getField())); clonedConditions[i] = clonedCondition; diff --git a/hutool-db/src/test/java/cn/hutool/db/H2Test.java b/hutool-db/src/test/java/cn/hutool/db/H2Test.java index c61404527..bc7e48dd7 100644 --- a/hutool-db/src/test/java/cn/hutool/db/H2Test.java +++ b/hutool-db/src/test/java/cn/hutool/db/H2Test.java @@ -9,14 +9,14 @@ import java.util.List; /** * H2数据库单元测试 - * + * * @author looly * */ public class H2Test { - + private static final String DS_GROUP_NAME = "h2"; - + @BeforeClass public static void init() throws SQLException { Db db = Db.use(DS_GROUP_NAME); @@ -27,7 +27,7 @@ public class H2Test { db.insert(Entity.create("test").set("a", 3).set("b", 31)); db.insert(Entity.create("test").set("a", 4).set("b", 41)); } - + @Test public void queryTest() throws SQLException { List query = Db.use(DS_GROUP_NAME).query("select * from test"); @@ -39,4 +39,12 @@ public class H2Test { List query = Db.use(DS_GROUP_NAME).find(Entity.create("test")); Assert.assertEquals(4, query.size()); } + + @Test + public void upsertTest() throws SQLException { + Db db=Db.use(DS_GROUP_NAME); + db.upsert(Entity.create("test").set("a",1).set("b",111),"a"); + Entity a1=db.get("test","a",1); + Assert.assertEquals(Long.valueOf(111),a1.getLong("b")); + } } diff --git a/hutool-db/src/test/java/cn/hutool/db/MySQLTest.java b/hutool-db/src/test/java/cn/hutool/db/MySQLTest.java index e3e72fba1..8ecebb74e 100644 --- a/hutool-db/src/test/java/cn/hutool/db/MySQLTest.java +++ b/hutool-db/src/test/java/cn/hutool/db/MySQLTest.java @@ -1,6 +1,9 @@ package cn.hutool.db; import cn.hutool.core.lang.Console; +import cn.hutool.core.util.ArrayUtil; +import org.junit.Assert; +import org.junit.BeforeClass; import org.junit.Ignore; import org.junit.Test; @@ -9,11 +12,16 @@ import java.util.List; /** * MySQL操作单元测试 - * - * @author looly * + * @author looly */ public class MySQLTest { + @BeforeClass + @Ignore + public static void createTable() throws SQLException { + Db db = Db.use("mysql"); + db.executeBatch("drop table if exists testuser", "CREATE TABLE if not exists `testuser` ( `id` int(11) NOT NULL, `account` varchar(255) DEFAULT NULL, `pass` varchar(255) DEFAULT NULL, PRIMARY KEY (`id`) ) ENGINE=InnoDB DEFAULT CHARSET=utf8"); + } @Test @Ignore @@ -34,13 +42,13 @@ public class MySQLTest { * * @throws SQLException SQL异常 */ - @Test(expected=SQLException.class) + @Test(expected = SQLException.class) @Ignore public void txTest() throws SQLException { Db.use("mysql").tx(db -> { int update = db.update(Entity.create("user").set("text", "描述100"), Entity.create().set("id", 100)); db.update(Entity.create("user").set("text", "描述101"), Entity.create().set("id", 101)); - if(1 == update) { + if (1 == update) { // 手动指定异常,然后测试回滚触发 throw new RuntimeException("Error"); } @@ -64,4 +72,14 @@ public class MySQLTest { Console.log(all); } + @Test + @Ignore + public void upsertTest() throws SQLException { + Db db = Db.use("mysql"); + db.insert(Entity.create("testuser").set("id", 1).set("account", "ice").set("pass", "123456")); + db.upsert(Entity.create("testuser").set("id", 1).set("account", "icefairy").set("pass", "a123456")); + Entity user = db.get(Entity.create("testuser").set("id", 1)); + System.out.println("user======="+user.getStr("account")+"___"+user.getStr("pass")); + Assert.assertEquals(user.getStr("account"), new String("icefairy")); + } } diff --git a/hutool-db/src/test/java/cn/hutool/db/PostgreTest.java b/hutool-db/src/test/java/cn/hutool/db/PostgreTest.java index a19559a7e..250930efe 100644 --- a/hutool-db/src/test/java/cn/hutool/db/PostgreTest.java +++ b/hutool-db/src/test/java/cn/hutool/db/PostgreTest.java @@ -2,6 +2,7 @@ package cn.hutool.db; import java.sql.SQLException; +import org.junit.Assert; import org.junit.Ignore; import org.junit.Test; @@ -9,9 +10,8 @@ import cn.hutool.core.lang.Console; /** * PostgreSQL 单元测试 - * - * @author looly * + * @author looly */ public class PostgreTest { @@ -34,4 +34,16 @@ public class PostgreTest { Console.log(entity.get("id")); } } + + @Test + @Ignore + public void upsertTest() throws SQLException { + Db db = Db.use("postgre"); + db.executeBatch("drop table if exists ctest", + "create table if not exists \"ctest\" ( \"id\" serial4, \"t1\" varchar(255) COLLATE \"pg_catalog\".\"default\", \"t2\" varchar(255) COLLATE \"pg_catalog\".\"default\", \"t3\" varchar(255) COLLATE \"pg_catalog\".\"default\", CONSTRAINT \"ctest_pkey\" PRIMARY KEY (\"id\") ) "); + db.insert(Entity.create("ctest").set("id", 1).set("t1", "111").set("t2", "222").set("t3", "333")); + db.upsert(Entity.create("ctest").set("id", 1).set("t1", "new111").set("t2", "new222").set("t3", "bew333"),"id"); + Entity et=db.get(Entity.create("ctest").set("id", 1)); + Assert.assertEquals("new111",et.getStr("t1")); + } } diff --git a/hutool-dfa/pom.xml b/hutool-dfa/pom.xml index 183c0eff9..244b9c3fe 100644 --- a/hutool-dfa/pom.xml +++ b/hutool-dfa/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 5.7.20-SNAPSHOT + 5.7.20 hutool-dfa diff --git a/hutool-extra/pom.xml b/hutool-extra/pom.xml index 7fca763ac..0a6a57284 100644 --- a/hutool-extra/pom.xml +++ b/hutool-extra/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 5.7.20-SNAPSHOT + 5.7.20 hutool-extra diff --git a/hutool-http/pom.xml b/hutool-http/pom.xml index a27a4ac72..b4a57da4c 100644 --- a/hutool-http/pom.xml +++ b/hutool-http/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 5.7.20-SNAPSHOT + 5.7.20 hutool-http diff --git a/hutool-http/src/main/java/cn/hutool/http/HttpGlobalConfig.java b/hutool-http/src/main/java/cn/hutool/http/HttpGlobalConfig.java index 918085592..99f28b1e1 100755 --- a/hutool-http/src/main/java/cn/hutool/http/HttpGlobalConfig.java +++ b/hutool-http/src/main/java/cn/hutool/http/HttpGlobalConfig.java @@ -32,6 +32,7 @@ public class HttpGlobalConfig implements Serializable { private static boolean isAllowPatch = false; private static String boundary = "--------------------Hutool_" + RandomUtil.randomString(16); private static int maxRedirectCount = 0; + private static boolean ignoreEOFError = true; /** * 获取全局默认的超时时长 @@ -99,6 +100,30 @@ public class HttpGlobalConfig implements Serializable { maxRedirectCount = customMaxRedirectCount; } + /** + * 获取是否忽略响应读取时可能的EOF异常。
+ * 在Http协议中,对于Transfer-Encoding: Chunked在正常情况下末尾会写入一个Length为0的的chunk标识完整结束。
+ * 如果服务端未遵循这个规范或响应没有正常结束,会报EOF异常,此选项用于是否忽略这个异常。 + * + * @return 是否忽略响应读取时可能的EOF异常 + * @since 5.7.20 + */ + public static boolean isIgnoreEOFError() { + return ignoreEOFError; + } + + /** + * 设置是否忽略响应读取时可能的EOF异常。
+ * 在Http协议中,对于Transfer-Encoding: Chunked在正常情况下末尾会写入一个Length为0的的chunk标识完整结束。
+ * 如果服务端未遵循这个规范或响应没有正常结束,会报EOF异常,此选项用于是否忽略这个异常。 + * + * @param customIgnoreEOFError 是否忽略响应读取时可能的EOF异常。 + * @since 5.7.20 + */ + synchronized public static void setIgnoreEOFError(boolean customIgnoreEOFError) { + ignoreEOFError = customIgnoreEOFError; + } + /** * 获取Cookie管理器,用于自定义Cookie管理 * diff --git a/hutool-http/src/main/java/cn/hutool/http/HttpResponse.java b/hutool-http/src/main/java/cn/hutool/http/HttpResponse.java index 03ae911da..d43ae1e6c 100644 --- a/hutool-http/src/main/java/cn/hutool/http/HttpResponse.java +++ b/hutool-http/src/main/java/cn/hutool/http/HttpResponse.java @@ -588,7 +588,8 @@ public class HttpResponse extends HttpBase implements Closeable { copyLength = IoUtil.copy(in, out, IoUtil.DEFAULT_BUFFER_SIZE, contentLength, streamProgress); } catch (IORuntimeException e) { //noinspection StatementWithEmptyBody - if (e.getCause() instanceof EOFException || StrUtil.containsIgnoreCase(e.getMessage(), "Premature EOF")) { + if (HttpGlobalConfig.isIgnoreEOFError() + && (e.getCause() instanceof EOFException || StrUtil.containsIgnoreCase(e.getMessage(), "Premature EOF"))) { // 忽略读取HTTP流中的EOF错误 } else { throw e; diff --git a/hutool-json/pom.xml b/hutool-json/pom.xml index 5103dc136..a41748fd4 100644 --- a/hutool-json/pom.xml +++ b/hutool-json/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 5.7.20-SNAPSHOT + 5.7.20 hutool-json diff --git a/hutool-json/src/main/java/cn/hutool/json/JSONObject.java b/hutool-json/src/main/java/cn/hutool/json/JSONObject.java index 403070853..a7582edd4 100644 --- a/hutool-json/src/main/java/cn/hutool/json/JSONObject.java +++ b/hutool-json/src/main/java/cn/hutool/json/JSONObject.java @@ -671,7 +671,6 @@ public class JSONObject implements JSON, JSONGetter, Map // 不支持对象类型转换为JSONObject throw new JSONException("Unsupported type [{}] to JSONObject!", source.getClass()); } - } /** diff --git a/hutool-json/src/test/java/cn/hutool/json/Issue2090Test.java b/hutool-json/src/test/java/cn/hutool/json/Issue2090Test.java new file mode 100755 index 000000000..f1f59b68e --- /dev/null +++ b/hutool-json/src/test/java/cn/hutool/json/Issue2090Test.java @@ -0,0 +1,43 @@ +package cn.hutool.json; + +import lombok.Data; +import org.junit.Assert; +import org.junit.Test; + +import java.time.LocalDate; +import java.time.Month; + +/** + * https://github.com/dromara/hutool/issues/2090 + */ +public class Issue2090Test { + + @Test + public void parseTest(){ + final TestBean test = new TestBean(); + test.setLocalDate(LocalDate.now()); + + final JSONObject json = JSONUtil.parseObj(test); + final TestBean test1 = json.toBean(TestBean.class); + Assert.assertEquals(test, test1); + } + + @Test + public void parseLocalDateTest(){ + LocalDate localDate = LocalDate.now(); + final JSONObject jsonObject = JSONUtil.parseObj(localDate); + Assert.assertNotNull(jsonObject.toString()); + } + + @Test + public void monthTest(){ + final JSONObject jsonObject = new JSONObject(); + jsonObject.set("month", Month.JANUARY); + Assert.assertEquals("{\"month\":1}", jsonObject.toString()); + } + + @Data + public static class TestBean{ + private LocalDate localDate; + } +} diff --git a/hutool-json/src/test/java/cn/hutool/json/JSONObjectTest.java b/hutool-json/src/test/java/cn/hutool/json/JSONObjectTest.java index f3ae62388..3b18c8059 100644 --- a/hutool-json/src/test/java/cn/hutool/json/JSONObjectTest.java +++ b/hutool-json/src/test/java/cn/hutool/json/JSONObjectTest.java @@ -30,6 +30,7 @@ import org.junit.Test; import java.math.BigDecimal; import java.sql.Timestamp; +import java.time.LocalDate; import java.util.Date; import java.util.HashMap; import java.util.List; diff --git a/hutool-jwt/pom.xml b/hutool-jwt/pom.xml index 7520a39c4..549b5b819 100644 --- a/hutool-jwt/pom.xml +++ b/hutool-jwt/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 5.7.20-SNAPSHOT + 5.7.20 hutool-jwt diff --git a/hutool-log/pom.xml b/hutool-log/pom.xml index 075926147..ea661e721 100644 --- a/hutool-log/pom.xml +++ b/hutool-log/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 5.7.20-SNAPSHOT + 5.7.20 hutool-log diff --git a/hutool-poi/pom.xml b/hutool-poi/pom.xml index f80d0c8e9..80d7ce519 100644 --- a/hutool-poi/pom.xml +++ b/hutool-poi/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 5.7.20-SNAPSHOT + 5.7.20 hutool-poi diff --git a/hutool-script/pom.xml b/hutool-script/pom.xml index 21854b669..7fa28a4f5 100644 --- a/hutool-script/pom.xml +++ b/hutool-script/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 5.7.20-SNAPSHOT + 5.7.20 hutool-script diff --git a/hutool-setting/pom.xml b/hutool-setting/pom.xml index 107bd6367..7bd5fdce5 100644 --- a/hutool-setting/pom.xml +++ b/hutool-setting/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 5.7.20-SNAPSHOT + 5.7.20 hutool-setting diff --git a/hutool-socket/pom.xml b/hutool-socket/pom.xml index 26529bfd2..d8d684dfe 100644 --- a/hutool-socket/pom.xml +++ b/hutool-socket/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 5.7.20-SNAPSHOT + 5.7.20 hutool-socket diff --git a/hutool-system/pom.xml b/hutool-system/pom.xml index ca87e5d2d..4105c3a27 100644 --- a/hutool-system/pom.xml +++ b/hutool-system/pom.xml @@ -9,7 +9,7 @@ cn.hutool hutool-parent - 5.7.20-SNAPSHOT + 5.7.20 hutool-system diff --git a/pom.xml b/pom.xml index baad04d0a..ec82d9de5 100644 --- a/pom.xml +++ b/pom.xml @@ -8,7 +8,7 @@ cn.hutool hutool-parent - 5.7.20-SNAPSHOT + 5.7.20 hutool Hutool是一个小而全的Java工具类库,通过静态方法封装,降低相关API的学习成本,提高工作效率,使Java拥有函数式语言般的优雅,让Java语言也可以“甜甜的”。 https://github.com/dromara/hutool