Prepare release

This commit is contained in:
Looly 2022-01-20 12:17:05 +08:00
commit 4fa050c56f
77 changed files with 871 additions and 236 deletions

View File

@ -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 】 增加KetamaHashissue#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)

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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;

View File

@ -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);
}
/**

View File

@ -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

View File

@ -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();

View File

@ -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();
}
/**

View File

@ -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);
}
}

View File

@ -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();
}

View File

@ -2876,7 +2876,12 @@ public class FileUtil extends PathUtil {
}
/**
* 将列表写入文件追加模式
* 将列表写入文件追加模式策略为
* <ul>
* <li>当文件为空从开头追加尾部不加空行</li>
* <li>当有内容换行追加尾部不加空行</li>
* <li>当有内容并末尾有空行依旧换行追加</li>
* </ul>
*
* @param <T> 集合元素类型
* @param list 列表

View File

@ -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));

View File

@ -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;
}

View File

@ -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;
}
}

View File

@ -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));
}

View File

@ -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());
}
/**
* 获取主机名称一次获取会缓存名称
*

View File

@ -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)) {
// 空白追加是保证以/开头

View File

@ -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;
};
}
}

View File

@ -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

View File

@ -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()) {
// 发现空串且需要忽略时跳过之

View File

@ -27,7 +27,6 @@ public class ThreadUtil {
* 1. 初始线程数为corePoolSize指定的大小
* 2. 没有最大线程数限制
* 3. 默认使用LinkedBlockingQueue默认队列大小为1024
* 4. 当运行线程大于corePoolSize放入队列队列满后抛出异常
* </pre>
*
* @param corePoolSize 同时执行的线程数大小

View File

@ -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
*/

View File

@ -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;

View File

@ -516,15 +516,15 @@ public class RandomUtil {
}
/**
* 获得一个随机的字符串只包含数字和字符 并排除指定字符串
* 获得一个随机的字符串只包含数字和小写字母 并排除指定字符串
*
* @param length 字符串的长度
* @param elemData 要排除的字符串,去重容易混淆的字符串oO0lL1q9QpP
* @param elemData 要排除的字符串,去重容易混淆的字符串oO0lL1q9QpP不区分大小写
* @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);
}

View File

@ -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

View File

@ -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");

View File

@ -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

View File

@ -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));
}
}

View File

@ -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);
}
}

View File

@ -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

View File

@ -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

View File

@ -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));
}
}

View File

@ -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);
}
}

View File

@ -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))));
}
}
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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执行异常

View File

@ -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执行异常

View File

@ -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>

View File

@ -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();

View File

@ -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());
}
/**
* 方言名
*

View File

@ -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;

View File

@ -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";
}

View File

@ -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 分页语句

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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();

View File

@ -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);
}
}

View File

@ -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);
}
}

View File

@ -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
}

View File

@ -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;

View File

@ -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"));
}
}

View File

@ -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"));
}
}

View File

@ -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"));
}
}

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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管理
*

View File

@ -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;

View File

@ -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>

View File

@ -671,7 +671,6 @@ public class JSONObject implements JSON, JSONGetter<String>, Map<String, Object>
// 不支持对象类型转换为JSONObject
throw new JSONException("Unsupported type [{}] to JSONObject!", source.getClass());
}
}
/**

View 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;
}
}

View File

@ -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;

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>

View File

@ -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>