mirror of
https://gitee.com/chinabugotech/hutool.git
synced 2025-05-09 23:51:34 +08:00
Prepare release
This commit is contained in:
commit
4fa050c56f
17
CHANGELOG.md
17
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)
|
||||
|
@ -9,7 +9,7 @@
|
||||
<parent>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>5.7.20-SNAPSHOT</version>
|
||||
<version>5.7.20</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hutool-all</artifactId>
|
||||
|
@ -9,7 +9,7 @@
|
||||
<parent>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>5.7.20-SNAPSHOT</version>
|
||||
<version>5.7.20</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hutool-aop</artifactId>
|
||||
|
@ -9,7 +9,7 @@
|
||||
<parent>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>5.7.20-SNAPSHOT</version>
|
||||
<version>5.7.20</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hutool-bloomFilter</artifactId>
|
||||
|
@ -9,7 +9,7 @@
|
||||
<parent>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>5.7.20-SNAPSHOT</version>
|
||||
<version>5.7.20</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hutool-bom</artifactId>
|
||||
|
@ -9,7 +9,7 @@
|
||||
<parent>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>5.7.20-SNAPSHOT</version>
|
||||
<version>5.7.20</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hutool-cache</artifactId>
|
||||
|
@ -9,7 +9,7 @@
|
||||
<parent>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>5.7.20-SNAPSHOT</version>
|
||||
<version>5.7.20</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hutool-captcha</artifactId>
|
||||
|
@ -9,7 +9,7 @@
|
||||
<parent>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>5.7.20-SNAPSHOT</version>
|
||||
<version>5.7.20</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hutool-core</artifactId>
|
||||
|
@ -35,6 +35,7 @@ public class BeanCopier<T> implements Copier<T>, Serializable {
|
||||
private static final long serialVersionUID = 1L;
|
||||
|
||||
/** 源对象 */
|
||||
@SuppressWarnings("NonSerializableFieldInSerializableClass")
|
||||
private final Object source;
|
||||
/** 目标对象 */
|
||||
private final T dest;
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -2343,11 +2343,7 @@ public class CollUtil {
|
||||
*/
|
||||
@SuppressWarnings("unchecked")
|
||||
public static <K, V> ArrayList<V> valuesOfKeys(Map<K, V> map, K... keys) {
|
||||
final ArrayList<V> 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 <K, V> ArrayList<V> valuesOfKeys(Map<K, V> map, Iterator<K> keys) {
|
||||
final ArrayList<V> values = new ArrayList<>();
|
||||
while (keys.hasNext()) {
|
||||
values.add(map.get(keys.next()));
|
||||
}
|
||||
return values;
|
||||
return MapUtil.valuesOfKeys(map, keys);
|
||||
}
|
||||
|
||||
// ------------------------------------------------------------------------------------------------- sort
|
||||
|
@ -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();
|
||||
|
@ -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();
|
||||
}
|
||||
|
||||
/**
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
* 检查两个时间段是否有时间重叠<br>
|
||||
* 重叠指两个时间段是否有交集
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
|
||||
}
|
||||
|
@ -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{
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化日期时间为指定格式
|
||||
* 格式化日期时间为指定格式<br>
|
||||
* 如果为{@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{
|
||||
}
|
||||
|
||||
/**
|
||||
* 格式化日期时间为指定格式
|
||||
* 格式化日期时间为指定格式<br>
|
||||
* 如果为{@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开始的毫秒数)<br>
|
||||
* 如果为{@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();
|
||||
}
|
||||
|
||||
|
@ -2876,7 +2876,12 @@ public class FileUtil extends PathUtil {
|
||||
}
|
||||
|
||||
/**
|
||||
* 将列表写入文件,追加模式
|
||||
* 将列表写入文件,追加模式,策略为:
|
||||
* <ul>
|
||||
* <li>当文件为空,从开头追加,尾部不加空行</li>
|
||||
* <li>当有内容,换行追加,尾部不加空行</li>
|
||||
* <li>当有内容,并末尾有空行,依旧换行追加</li>
|
||||
* </ul>
|
||||
*
|
||||
* @param <T> 集合元素类型
|
||||
* @param list 列表
|
||||
|
@ -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));
|
||||
|
@ -122,7 +122,7 @@ public class Opt<T> {
|
||||
try {
|
||||
return Opt.ofNullable(supplier.call());
|
||||
} catch (Exception e) {
|
||||
final Opt<T> empty = Opt.empty();
|
||||
final Opt<T> empty = new Opt<>(null);
|
||||
empty.exception = e;
|
||||
return empty;
|
||||
}
|
||||
|
@ -1354,4 +1354,23 @@ public class MapUtil {
|
||||
}
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 从Map中获取指定键列表对应的值列表<br>
|
||||
* 如果key在map中不存在或key对应值为null,则返回值列表对应位置的值也为null
|
||||
*
|
||||
* @param <K> 键类型
|
||||
* @param <V> 值类型
|
||||
* @param map {@link Map}
|
||||
* @param keys 键列表
|
||||
* @return 值列表
|
||||
* @since 5.7.20
|
||||
*/
|
||||
public static <K, V> ArrayList<V> valuesOfKeys(Map<K, V> map, Iterator<K> keys) {
|
||||
final ArrayList<V> values = new ArrayList<>();
|
||||
while (keys.hasNext()) {
|
||||
values.add(map.get(keys.next()));
|
||||
}
|
||||
return values;
|
||||
}
|
||||
}
|
||||
|
@ -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));
|
||||
}
|
||||
|
@ -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());
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取主机名称,一次获取会缓存名称
|
||||
*
|
||||
|
@ -117,7 +117,10 @@ public class UrlPath {
|
||||
}
|
||||
|
||||
/**
|
||||
* 构建path,前面带'/'
|
||||
* 构建path,前面带'/'<br>
|
||||
* <pre>
|
||||
* path = path-abempty / path-absolute / path-noscheme / path-rootless / path-empty
|
||||
* </pre>
|
||||
*
|
||||
* @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)) {
|
||||
// 空白追加是保证以/开头
|
||||
|
@ -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<Collector.Characteristics> CH_ID
|
||||
= Collections.unmodifiableSet(EnumSet.of(Collector.Characteristics.IDENTITY_FINISH));
|
||||
/**
|
||||
* 说明不包含IDENTITY_FINISH特征
|
||||
*/
|
||||
public static final Set<Collector.Characteristics> 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<Map<K, A>> merger = (m1, m2) -> {
|
||||
for (Map.Entry<K, A> e : m2.entrySet()) {
|
||||
m1.merge(e.getKey(), e.getValue(), downstream.combiner());
|
||||
}
|
||||
return m1;
|
||||
};
|
||||
BinaryOperator<Map<K, A>> merger = mapMerger(downstream.combiner());
|
||||
@SuppressWarnings("unchecked")
|
||||
Supplier<Map<K, A>> mangledFactory = (Supplier<Map<K, A>>) 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<A, A> downstreamFinisher = (Function<A, A>) 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 <T> 实体类型
|
||||
* @param <K> map中key的类型
|
||||
* @param <U> map中value的类型
|
||||
* @return 对null友好的 toMap 操作的 {@link Collector}实现
|
||||
*/
|
||||
public static <T, K, U>
|
||||
Collector<T, ?, Map<K, U>> toMap(Function<? super T, ? extends K> keyMapper,
|
||||
Function<? super T, ? extends U> valueMapper,
|
||||
BinaryOperator<U> 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 <T> 实体类型
|
||||
* @param <K> map中key的类型
|
||||
* @param <U> map中value的类型
|
||||
* @param <M> map的类型
|
||||
* @return 对null友好的 toMap 操作的 {@link Collector}实现
|
||||
*/
|
||||
public static <T, K, U, M extends Map<K, U>>
|
||||
Collector<T, ?, M> toMap(Function<? super T, ? extends K> keyMapper,
|
||||
Function<? super T, ? extends U> valueMapper,
|
||||
BinaryOperator<U> mergeFunction,
|
||||
Supplier<M> mapSupplier) {
|
||||
BiConsumer<M, T> 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 <K> key的类型
|
||||
* @param <V> value的类型
|
||||
* @param <M> map
|
||||
* @return 用户合并map的BinaryOperator
|
||||
*/
|
||||
public static <K, V, M extends Map<K, V>> BinaryOperator<M> mapMerger(BinaryOperator<V> mergeFunction) {
|
||||
return (m1, m2) -> {
|
||||
for (Map.Entry<K, V> e : m2.entrySet()) {
|
||||
m1.merge(e.getKey(), e.getValue(), mergeFunction);
|
||||
}
|
||||
return m1;
|
||||
};
|
||||
}
|
||||
|
||||
|
||||
}
|
||||
|
@ -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
|
||||
|
@ -87,9 +87,8 @@ public class SplitIter extends ComputeIter<String> 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()) {
|
||||
// 发现空串且需要忽略时,跳过之
|
||||
|
@ -27,7 +27,6 @@ public class ThreadUtil {
|
||||
* 1. 初始线程数为corePoolSize指定的大小
|
||||
* 2. 没有最大线程数限制
|
||||
* 3. 默认使用LinkedBlockingQueue,默认队列大小为1024
|
||||
* 4. 当运行线程大于corePoolSize放入队列,队列满后抛出异常
|
||||
* </pre>
|
||||
*
|
||||
* @param corePoolSize 同时执行的线程数大小
|
||||
|
@ -10,6 +10,7 @@ import cn.hutool.core.text.escape.XmlUnescape;
|
||||
* 转义和反转义工具类Escape / Unescape<br>
|
||||
* escape采用ISO Latin字符集对指定的字符串进行编码。<br>
|
||||
* 所有的空格符、标点符号、特殊字符以及其他非ASCII字符都将被转化成%xx格式的字符编码(xx等于该字符在字符集表里面的编码的16进制数字)。
|
||||
* TODO 6.x迁移到core.text.escape包下
|
||||
*
|
||||
* @author xiaoleilu
|
||||
*/
|
||||
|
@ -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;
|
||||
|
@ -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);
|
||||
}
|
||||
|
||||
|
@ -150,6 +150,13 @@ public class CollStreamUtilTest {
|
||||
compare.put(2L, map2);
|
||||
Assert.assertEquals(compare, map);
|
||||
|
||||
// 对null友好
|
||||
Map<Long, Map<Long, Student>> termIdClassIdStudentMap = CollStreamUtil.group2Map(Arrays.asList(null, new Student(2, 2, 1, "王五")), Student::getTermId, Student::getClassId);
|
||||
Map<Long, Map<Long, Student>> termIdClassIdStudentCompareMap = new HashMap<Long, Map<Long, Student>>() {{
|
||||
put(null, MapUtil.of(null, null));
|
||||
put(2L, MapUtil.of(2L, new Student(2, 2, 1, "王五")));
|
||||
}};
|
||||
Assert.assertEquals(termIdClassIdStudentCompareMap, termIdClassIdStudentMap);
|
||||
}
|
||||
|
||||
@Test
|
||||
|
@ -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<String> set = CollUtil.newLinkedHashSet("a", "b", "", " ", "c");
|
||||
Set<String> filtered = CollUtil.filter(set, StrUtil::isNotBlank);
|
||||
|
||||
Assert.assertEquals(CollUtil.newLinkedHashSet("a", "b", "c"), filtered);
|
||||
}
|
||||
|
||||
@Test
|
||||
public void filterRemoveTest() {
|
||||
ArrayList<String> list = CollUtil.newArrayList("a", "b", "c");
|
||||
|
@ -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
|
||||
|
@ -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));
|
||||
}
|
||||
}
|
||||
|
@ -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<String> list = ListUtil.toList("a", "b", "c");
|
||||
FileUtil.appendLines(list, FileUtil.file("d:/test/appendLines.txt"), CharsetUtil.CHARSET_UTF_8);
|
||||
}
|
||||
}
|
||||
|
@ -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<Object> 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
|
||||
|
@ -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
|
||||
|
@ -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<String> 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));
|
||||
}
|
||||
}
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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))));
|
||||
}
|
||||
}
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,7 @@
|
||||
<parent>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>5.7.20-SNAPSHOT</version>
|
||||
<version>5.7.20</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hutool-cron</artifactId>
|
||||
|
@ -9,7 +9,7 @@
|
||||
<parent>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>5.7.20-SNAPSHOT</version>
|
||||
<version>5.7.20</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hutool-crypto</artifactId>
|
||||
|
@ -9,7 +9,7 @@
|
||||
<parent>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>5.7.20-SNAPSHOT</version>
|
||||
<version>5.7.20</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hutool-db</artifactId>
|
||||
|
@ -200,9 +200,9 @@ public abstract class AbstractDb implements Serializable {
|
||||
* 执行自定义的{@link PreparedStatement},结果使用{@link RsHandler}处理<br>
|
||||
* 此方法主要用于自定义场景,如游标查询等
|
||||
*
|
||||
* @param <T> 结果集需要处理的对象类型
|
||||
* @param <T> 结果集需要处理的对象类型
|
||||
* @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语义插入或更新数据<br>
|
||||
* 根据给定的字段名查询数据,如果存在则更新这些数据,否则执行插入
|
||||
* 如果方言未实现本方法,内部会自动调用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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量插入数据<br>
|
||||
* 需要注意的是,批量插入每一条数据结构必须一致。批量插入数据时会获取第一条数据的字段结构,之后的数据会按照这个格式插入。<br>
|
||||
@ -864,7 +884,7 @@ public abstract class AbstractDb implements Serializable {
|
||||
/**
|
||||
* 分页查询
|
||||
*
|
||||
* @param <T> 处理结果类型,可以将ResultSet转换为给定类型
|
||||
* @param <T> 处理结果类型,可以将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执行异常
|
||||
|
@ -86,6 +86,55 @@ public class DialectRunner implements Serializable {
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 更新或插入数据<br>
|
||||
* 此方法不会关闭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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 插入或更新数据<br>
|
||||
* 此方法不会关闭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];
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 插入数据<br>
|
||||
* 此方法不会关闭Connection
|
||||
@ -212,7 +261,7 @@ public class DialectRunner implements Serializable {
|
||||
* 获取查询结果总数,生成类似于 SELECT count(1) from (sql) hutool_alias_count_<br>
|
||||
* 此方法会重新构建{@link SqlBuilder},并去除末尾的order by子句
|
||||
*
|
||||
* @param conn 数据库连接对象
|
||||
* @param conn 数据库连接对象
|
||||
* @param sqlBuilder 查询语句
|
||||
* @return 复合条件的结果数
|
||||
* @throws SQLException SQL执行异常
|
||||
|
@ -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执行类<br>
|
||||
* 此执行类只接受方言参数,不需要数据源,只有在执行方法时需要数据库连接对象<br>
|
||||
* 此对象存在的意义在于,可以由使用者自定义数据库连接对象,并执行多个方法,方便事务的统一控制或减少连接对象的创建关闭
|
||||
* 此对象存在的意义在于,可以由使用者自定义数据库连接对象,并执行多个方法,方便事务的统一控制或减少连接对象的创建关闭<br>
|
||||
* 相比{@link DialectRunner},此类中提供了更多重载方法
|
||||
*
|
||||
* @author Luxiaolei
|
||||
*/
|
||||
@ -82,25 +82,6 @@ public class SqlConnRunner extends DialectRunner {
|
||||
|
||||
//---------------------------------------------------------------------------- CRUD start
|
||||
|
||||
/**
|
||||
* 插入或更新数据<br>
|
||||
* 此方法不会关闭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);
|
||||
}
|
||||
}
|
||||
|
||||
/**
|
||||
* 批量插入数据<br>
|
||||
* 需要注意的是,批量插入每一条数据结构必须一致。批量插入数据时会获取第一条数据的字段结构,之后的数据会按照这个格式插入。<br>
|
||||
|
@ -194,7 +194,7 @@ public class StatementUtil {
|
||||
* @throws SQLException SQL异常
|
||||
* @since 4.6.7
|
||||
*/
|
||||
public static PreparedStatement prepareStatementForBatch(Connection conn, String sql, List<String> fields, Entity... entities) throws SQLException {
|
||||
public static PreparedStatement prepareStatementForBatch(Connection conn, String sql, Iterable<String> fields, Entity... entities) throws SQLException {
|
||||
Assert.notBlank(sql, "Sql String must be not blank!");
|
||||
|
||||
sql = sql.trim();
|
||||
|
@ -37,7 +37,8 @@ public interface Dialect extends Serializable {
|
||||
// -------------------------------------------- Execute
|
||||
|
||||
/**
|
||||
* 构建用于插入的PreparedStatement
|
||||
* 构建用于插入的{@link PreparedStatement}<br>
|
||||
* 用户实现需按照数据库方言格式,将{@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<br>
|
||||
* 用户实现需按照数据库方言格式,将{@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}<br>
|
||||
* 用户实现需按照数据库方言格式,将{@link Query}转换为带有占位符的SQL语句及参数列表<br>
|
||||
* {@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}<br>
|
||||
* 用户实现需按照数据库方言格式,将{@link Entity}配合{@link Query}转换为带有占位符的SQL语句及参数列表<br>
|
||||
* 其中{@link Entity}中包含需要更新的数据信息,{@link Query}包含更新的查找条件信息。
|
||||
*
|
||||
* @param conn 数据库连接对象
|
||||
* @param entity 数据实体类(包含表名)
|
||||
@ -80,7 +86,9 @@ public interface Dialect extends Serializable {
|
||||
// -------------------------------------------- Query
|
||||
|
||||
/**
|
||||
* 构建用于获取多条记录的PreparedStatement
|
||||
* 构建用于获取多条记录的{@link PreparedStatement}<br>
|
||||
* 用户实现需按照数据库方言格式,将{@link Query}转换为带有占位符的SQL语句及参数列表<br>
|
||||
* {@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}<br>
|
||||
* 用户实现需按照数据库方言格式,将{@link Query}转换为带有占位符的SQL语句及参数列表<br>
|
||||
* {@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<br>
|
||||
* 构建用于分页查询的{@link PreparedStatement}<br>
|
||||
* 可以在此方法中使用{@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}<br>
|
||||
* 用户实现需按照数据库方言格式,将{@link Query}转换为带有占位符的SQL语句及参数列表<br>
|
||||
* {@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}<br>
|
||||
* 用户实现需按照数据库方言格式,将{@link Query}转换为带有占位符的SQL语句及参数列表<br>
|
||||
* {@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}<br>
|
||||
* 方言实现需实现此默认方法,如果没有实现,抛出{@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());
|
||||
}
|
||||
|
||||
|
||||
/**
|
||||
* 方言名
|
||||
*
|
||||
|
@ -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;
|
||||
|
@ -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";
|
||||
|
||||
}
|
||||
|
@ -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<String> 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语句基础上包装其分页的语句<br>
|
||||
* 各自数据库通过重写此方法实现最小改动情况下修改分页语句
|
||||
*
|
||||
*
|
||||
* @param find 标准查询语句
|
||||
* @param page 分页对象
|
||||
* @return 分页语句
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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}<br>
|
||||
* MySQL通过主键方式实现Upsert,故keys无效,生成SQL语法为:
|
||||
* <pre>
|
||||
* INSERT INTO demo(a,b,c) values(?, ?, ?) ON DUPLICATE KEY UPDATE a=values(a), b=values(b), c=values(c);
|
||||
* </pre>
|
||||
*
|
||||
* @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);
|
||||
}
|
||||
}
|
||||
|
@ -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();
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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);
|
||||
}
|
||||
}
|
||||
|
@ -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构建器<br>
|
||||
@ -57,6 +57,24 @@ public class SqlBuilder implements Builder<String> {
|
||||
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<String> {
|
||||
// --------------------------------------------------------------- Enums end
|
||||
|
||||
private final StringBuilder sql = new StringBuilder();
|
||||
/**
|
||||
* 字段列表(仅用于插入和更新)
|
||||
*/
|
||||
private final List<String> fields = new ArrayList<>();
|
||||
/**
|
||||
* 占位符对应的值列表
|
||||
*/
|
||||
@ -146,41 +160,29 @@ public class SqlBuilder implements Builder<String> {
|
||||
// 验证
|
||||
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<String, Object> 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<String> {
|
||||
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<String> {
|
||||
// 验证
|
||||
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<String, Object> 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<String> {
|
||||
}
|
||||
// --------------------------------------------------------------- Builder end
|
||||
|
||||
/**
|
||||
* 获得插入或更新的数据库字段列表
|
||||
*
|
||||
* @return 插入或更新的数据库字段列表
|
||||
*/
|
||||
public String[] getFieldArray() {
|
||||
return this.fields.toArray(new String[0]);
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得插入或更新的数据库字段列表
|
||||
*
|
||||
* @return 插入或更新的数据库字段列表
|
||||
*/
|
||||
public List<String> getFields() {
|
||||
return this.fields;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获得占位符对应的值列表<br>
|
||||
*
|
||||
@ -645,23 +630,5 @@ public class SqlBuilder implements Builder<String> {
|
||||
|
||||
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
|
||||
}
|
||||
|
@ -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;
|
||||
/**
|
||||
* 包装器<br>
|
||||
* 主要用于字段名的包装(在字段名的前后加字符,例如反引号来避免与数据库的关键字冲突)
|
||||
* @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 {
|
||||
/**
|
||||
* 包装字段名<br>
|
||||
* 有时字段与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<String> 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 {
|
||||
/**
|
||||
* 包装字段名<br>
|
||||
* 有时字段与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 {
|
||||
/**
|
||||
* 包装字段名<br>
|
||||
* 有时字段与SQL的某些关键字冲突,导致SQL出错,因此需要将字段名用单引号或者反引号包装起来,避免冲突
|
||||
*
|
||||
* @param fields 字段名
|
||||
* @return 包装后的字段名
|
||||
*/
|
||||
public Collection<String> wrap(Collection<String> fields){
|
||||
if(CollectionUtil.isEmpty(fields)) {
|
||||
public Collection<String> wrap(Collection<String> fields) {
|
||||
if (CollectionUtil.isEmpty(fields)) {
|
||||
return fields;
|
||||
}
|
||||
|
||||
@ -140,13 +156,14 @@ public class Wrapper {
|
||||
}
|
||||
|
||||
/**
|
||||
* 包装字段名<br>
|
||||
* 包装表名和字段名,此方法返回一个新的Entity实体类<br>
|
||||
* 有时字段与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 {
|
||||
/**
|
||||
* 包装字段名<br>
|
||||
* 有时字段与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;
|
||||
|
@ -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<Entity> query = Db.use(DS_GROUP_NAME).query("select * from test");
|
||||
@ -39,4 +39,12 @@ public class H2Test {
|
||||
List<Entity> 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"));
|
||||
}
|
||||
}
|
||||
|
@ -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"));
|
||||
}
|
||||
}
|
||||
|
@ -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"));
|
||||
}
|
||||
}
|
||||
|
@ -9,7 +9,7 @@
|
||||
<parent>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>5.7.20-SNAPSHOT</version>
|
||||
<version>5.7.20</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hutool-dfa</artifactId>
|
||||
|
@ -9,7 +9,7 @@
|
||||
<parent>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>5.7.20-SNAPSHOT</version>
|
||||
<version>5.7.20</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hutool-extra</artifactId>
|
||||
|
@ -9,7 +9,7 @@
|
||||
<parent>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>5.7.20-SNAPSHOT</version>
|
||||
<version>5.7.20</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hutool-http</artifactId>
|
||||
|
@ -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异常。<br>
|
||||
* 在Http协议中,对于Transfer-Encoding: Chunked在正常情况下末尾会写入一个Length为0的的chunk标识完整结束。<br>
|
||||
* 如果服务端未遵循这个规范或响应没有正常结束,会报EOF异常,此选项用于是否忽略这个异常。
|
||||
*
|
||||
* @return 是否忽略响应读取时可能的EOF异常
|
||||
* @since 5.7.20
|
||||
*/
|
||||
public static boolean isIgnoreEOFError() {
|
||||
return ignoreEOFError;
|
||||
}
|
||||
|
||||
/**
|
||||
* 设置是否忽略响应读取时可能的EOF异常。<br>
|
||||
* 在Http协议中,对于Transfer-Encoding: Chunked在正常情况下末尾会写入一个Length为0的的chunk标识完整结束。<br>
|
||||
* 如果服务端未遵循这个规范或响应没有正常结束,会报EOF异常,此选项用于是否忽略这个异常。
|
||||
*
|
||||
* @param customIgnoreEOFError 是否忽略响应读取时可能的EOF异常。
|
||||
* @since 5.7.20
|
||||
*/
|
||||
synchronized public static void setIgnoreEOFError(boolean customIgnoreEOFError) {
|
||||
ignoreEOFError = customIgnoreEOFError;
|
||||
}
|
||||
|
||||
/**
|
||||
* 获取Cookie管理器,用于自定义Cookie管理
|
||||
*
|
||||
|
@ -588,7 +588,8 @@ public class HttpResponse extends HttpBase<HttpResponse> 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;
|
||||
|
@ -9,7 +9,7 @@
|
||||
<parent>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>5.7.20-SNAPSHOT</version>
|
||||
<version>5.7.20</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hutool-json</artifactId>
|
||||
|
@ -671,7 +671,6 @@ public class JSONObject implements JSON, JSONGetter<String>, Map<String, Object>
|
||||
// 不支持对象类型转换为JSONObject
|
||||
throw new JSONException("Unsupported type [{}] to JSONObject!", source.getClass());
|
||||
}
|
||||
|
||||
}
|
||||
|
||||
/**
|
||||
|
43
hutool-json/src/test/java/cn/hutool/json/Issue2090Test.java
Executable file
43
hutool-json/src/test/java/cn/hutool/json/Issue2090Test.java
Executable file
@ -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;
|
||||
}
|
||||
}
|
@ -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;
|
||||
|
@ -9,7 +9,7 @@
|
||||
<parent>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>5.7.20-SNAPSHOT</version>
|
||||
<version>5.7.20</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hutool-jwt</artifactId>
|
||||
|
@ -9,7 +9,7 @@
|
||||
<parent>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>5.7.20-SNAPSHOT</version>
|
||||
<version>5.7.20</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hutool-log</artifactId>
|
||||
|
@ -9,7 +9,7 @@
|
||||
<parent>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>5.7.20-SNAPSHOT</version>
|
||||
<version>5.7.20</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hutool-poi</artifactId>
|
||||
|
@ -9,7 +9,7 @@
|
||||
<parent>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>5.7.20-SNAPSHOT</version>
|
||||
<version>5.7.20</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hutool-script</artifactId>
|
||||
|
@ -9,7 +9,7 @@
|
||||
<parent>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>5.7.20-SNAPSHOT</version>
|
||||
<version>5.7.20</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hutool-setting</artifactId>
|
||||
|
@ -9,7 +9,7 @@
|
||||
<parent>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>5.7.20-SNAPSHOT</version>
|
||||
<version>5.7.20</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hutool-socket</artifactId>
|
||||
|
@ -9,7 +9,7 @@
|
||||
<parent>
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>5.7.20-SNAPSHOT</version>
|
||||
<version>5.7.20</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hutool-system</artifactId>
|
||||
|
2
pom.xml
2
pom.xml
@ -8,7 +8,7 @@
|
||||
|
||||
<groupId>cn.hutool</groupId>
|
||||
<artifactId>hutool-parent</artifactId>
|
||||
<version>5.7.20-SNAPSHOT</version>
|
||||
<version>5.7.20</version>
|
||||
<name>hutool</name>
|
||||
<description>Hutool是一个小而全的Java工具类库,通过静态方法封装,降低相关API的学习成本,提高工作效率,使Java拥有函数式语言般的优雅,让Java语言也可以“甜甜的”。</description>
|
||||
<url>https://github.com/dromara/hutool</url>
|
||||
|
Loading…
x
Reference in New Issue
Block a user