Compare commits

...

11 Commits

Author SHA1 Message Date
Looly
b0e37e3ef3 增加分段锁实现SegmentLock(pr#1330@Gitee) 2025-04-15 11:27:53 +08:00
Looly
aa13f98934 fix test 2025-04-11 20:26:18 +08:00
Looly
1cfd6c7a5c fix test 2025-04-11 20:18:08 +08:00
Looly
c7c4457deb CharSequenceUtil增加toLoweCase和toUpperCase方法(issue#IC0H2B@Gitee) 2025-04-11 19:15:42 +08:00
Looly
a0eab6fb11 CharSequenceUtil增加toLoweCase和toUpperCase方法(issue#IC0H2B@Gitee) 2025-04-11 19:13:29 +08:00
Looly
4960c77a34 add plugin 2025-04-11 19:05:48 +08:00
Looly
1011c5ec60 增加Argon2类,实现Argon2算法(issue#3890@Github) 2025-04-11 10:45:14 +08:00
Looly
79485aca23 add hana 2025-04-11 09:55:00 +08:00
Looly
deee1f8d09 add hana 2025-04-11 09:51:47 +08:00
Looly
58d44d6086 add hana 2025-04-11 09:50:20 +08:00
Looly
581a5dd0fd fix comment 2025-04-08 16:22:12 +08:00
20 changed files with 1309 additions and 9 deletions

View File

@@ -153,7 +153,7 @@ public class ArrayUtil extends PrimitiveArrayUtil {
}
/**
* 是否都不为{@code null}或空对象或空白符的对象,通过{@link #hasBlank(CharSequence...)} 判断元素
* 是否都不为{@code null}或空对象或空白符的对象,通过{@link #hasBlank(CharSequence...)} 判断元素
*
* @param args 被检查的对象,一个或者多个
* @return 是否都不为空

View File

@@ -45,6 +45,7 @@ import java.util.function.Function;
* <li>{@code java.lang.Short}</li>
* <li>{@code java.lang.Integer}</li>
* <li>{@code java.util.concurrent.atomic.AtomicInteger}</li>
*
* <li>{@code java.lang.Long}</li>
* <li>{@code java.util.concurrent.atomic.AtomicLong}</li>
* <li>{@code java.lang.Float}</li>

View File

@@ -3664,6 +3664,68 @@ public class CharSequenceUtil extends StrValidator {
// region ----- lower and upper
/**
* 将字符串转为小写
*
* @param str 被转的字符串
* @return 转换后的字符串
* @see String#toLowerCase()
* @since 5.8.38
*/
public static String toLoweCase(final CharSequence str) {
return toLoweCase(str, Locale.getDefault());
}
/**
* 将字符串转为小写
*
* @param str 被转的字符串
* @param locale Locale
* @return 转换后的字符串
* @see String#toLowerCase()
* @since 6.0.0
*/
public static String toLoweCase(final CharSequence str, final Locale locale) {
if (null == str) {
return null;
}
if(0 == str.length()){
return EMPTY;
}
return str.toString().toLowerCase(locale);
}
/**
* 将字符串转为大写
*
* @param str 被转的字符串
* @return 转换后的字符串
* @see String#toUpperCase()
* @since 5.8.38
*/
public static String toUpperCase(final CharSequence str) {
return toUpperCase(str, Locale.getDefault());
}
/**
* 将字符串转为大写
*
* @param str 被转的字符串
* @param locale Locale
* @return 转换后的字符串
* @see String#toUpperCase()
* @since 6.0.0
*/
public static String toUpperCase(final CharSequence str, final Locale locale) {
if (null == str) {
return null;
}
if(0 == str.length()){
return EMPTY;
}
return str.toString().toUpperCase();
}
/**
* 原字符串首字母大写并在其首部添加指定字符串 例如str=name, preString=get =》 return getName
*

View File

@@ -213,7 +213,7 @@ public class StrValidator {
}
/**
* 是否都不为{@code null}或空对象或空白符的对象,通过{@link #hasBlank(CharSequence...)} 判断元素
* 是否都不为{@code null}或空对象或空白符的对象,通过{@link #hasBlank(CharSequence...)} 判断元素
*
* @param args 被检查的对象,一个或者多个
* @return 是否都不为空

View File

@@ -16,6 +16,9 @@
package org.dromara.hutool.core.thread.lock;
import java.util.concurrent.Semaphore;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import java.util.concurrent.locks.ReentrantReadWriteLock;
import java.util.concurrent.locks.StampedLock;
@@ -48,6 +51,105 @@ public class LockUtil {
return new ReentrantReadWriteLock(fair);
}
// region ----- SegmentLock
/**
* 创建分段锁(强引用),使用 ReentrantLock
*
* @param segments 分段数量,必须大于 0
* @return 分段锁实例
*/
public static SegmentLock<Lock> createSegmentLock(final int segments) {
return SegmentLock.lock(segments);
}
/**
* 创建分段读写锁(强引用),使用 ReentrantReadWriteLock
*
* @param segments 分段数量,必须大于 0
* @return 分段读写锁实例
*/
public static SegmentLock<ReadWriteLock> createSegmentReadWriteLock(final int segments) {
return SegmentLock.readWriteLock(segments);
}
/**
* 创建分段信号量(强引用)
*
* @param segments 分段数量,必须大于 0
* @param permits 每个信号量的许可数
* @return 分段信号量实例
*/
public static SegmentLock<Semaphore> createSegmentSemaphore(final int segments, final int permits) {
return SegmentLock.semaphore(segments, permits);
}
/**
* 创建弱引用分段锁,使用 ReentrantLock懒加载
*
* @param segments 分段数量,必须大于 0
* @return 弱引用分段锁实例
*/
public static SegmentLock<Lock> createLazySegmentLock(final int segments) {
return SegmentLock.lazyWeakLock(segments);
}
/**
* 根据 key 获取分段锁(强引用)
*
* @param segments 分段数量,必须大于 0
* @param key 用于映射分段的 key
* @return 对应的 Lock 实例
*/
public static Lock getSegmentLock(final int segments, final Object key) {
return SegmentLock.lock(segments).get(key);
}
/**
* 根据 key 获取分段读锁(强引用)
*
* @param segments 分段数量,必须大于 0
* @param key 用于映射分段的 key
* @return 对应的读锁实例
*/
public static Lock getSegmentReadLock(final int segments, final Object key) {
return SegmentLock.readWriteLock(segments).get(key).readLock();
}
/**
* 根据 key 获取分段写锁(强引用)
*
* @param segments 分段数量,必须大于 0
* @param key 用于映射分段的 key
* @return 对应的写锁实例
*/
public static Lock getSegmentWriteLock(final int segments, final Object key) {
return SegmentLock.readWriteLock(segments).get(key).writeLock();
}
/**
* 根据 key 获取分段信号量(强引用)
*
* @param segments 分段数量,必须大于 0
* @param permits 每个信号量的许可数
* @param key 用于映射分段的 key
* @return 对应的 Semaphore 实例
*/
public static Semaphore getSegmentSemaphore(final int segments, final int permits, final Object key) {
return SegmentLock.semaphore(segments, permits).get(key);
}
/**
* 根据 key 获取弱引用分段锁,懒加载
*
* @param segments 分段数量,必须大于 0
* @param key 用于映射分段的 key
* @return 对应的 Lock 实例
*/
public static Lock getLazySegmentLock(final int segments, final Object key) {
return SegmentLock.lazyWeakLock(segments).get(key);
}
// endregion
/**
* 获取单例的无锁对象
*

View File

@@ -0,0 +1,523 @@
/*
* Copyright (c) 2025 Hutool Team and hutool.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.dromara.hutool.core.thread.lock;
import org.dromara.hutool.core.collection.CollUtil;
import org.dromara.hutool.core.collection.ListUtil;
import org.dromara.hutool.core.lang.Assert;
import java.lang.ref.Reference;
import java.lang.ref.ReferenceQueue;
import java.lang.ref.WeakReference;
import java.util.Arrays;
import java.util.Collections;
import java.util.Date;
import java.util.List;
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.ConcurrentMap;
import java.util.concurrent.Semaphore;
import java.util.concurrent.TimeUnit;
import java.util.concurrent.atomic.AtomicReferenceArray;
import java.util.concurrent.locks.*;
import java.util.function.Supplier;
/**
* 分段锁工具类,支持 Lock、Semaphore 和 ReadWriteLock 的分段实现。
* <p>
* 通过将锁分成多个段segments不同的操作可以并发使用不同的段避免所有线程竞争同一把锁。
* 相等的 key 保证映射到同一段锁(如 key1.equals(key2) 时get(key1) 和 get(key2) 返回相同对象)。
* 但不同 key 可能因哈希冲突映射到同一段,段数越少冲突概率越高。
* <p>
* 支持两种实现:
* <ul>
* <li>强引用:创建时初始化所有段,内存占用稳定。</li>
* <li>弱引用:懒加载,首次使用时创建段,未使用时可被垃圾回收,适合大量段但使用较少的场景。</li>
* </ul>
*
* @param <L>
* @author Guava,dakuo
* @since 5.8.38
*/
public abstract class SegmentLock<L> {
/** 当段数大于此阈值时,使用 ConcurrentMap 替代大数组以节省内存(适用于懒加载场景) */
private static final int LARGE_LAZY_CUTOFF = 1024;
private SegmentLock() {}
/**
* 根据 key 获取对应的锁段,保证相同 key 返回相同对象。
*
* @param key 非空 key
* @return 对应的锁段
*/
public abstract L get(Object key);
/**
* 根据索引获取锁段,索引范围为 [0, size())。
*
* @param index 索引
* @return 指定索引的锁段
*/
public abstract L getAt(int index);
/**
* 计算 key 对应的段索引。
*
* @param key 非空 key
* @return 段索引
*/
abstract int indexFor(Object key);
/**
* 获取总段数。
*
* @return 段数
*/
public abstract int size();
/**
* 批量获取多个 key 对应的锁段列表,按索引升序排列,避免死锁。
*
* @param keys 非空 key 集合
* @return 锁段列表(可能有重复)
*/
@SuppressWarnings("unchecked")
public Iterable<L> bulkGet(final Iterable<?> keys) {
final List<Object> result = (List<Object>) ListUtil.of(keys);
if (CollUtil.isEmpty(result)) {
return Collections.emptyList();
}
final int[] stripes = new int[result.size()];
for (int i = 0; i < result.size(); i++) {
stripes[i] = indexFor(result.get(i));
}
Arrays.sort(stripes);
int previousStripe = stripes[0];
result.set(0, getAt(previousStripe));
for (int i = 1; i < result.size(); i++) {
final int currentStripe = stripes[i];
if (currentStripe == previousStripe) {
result.set(i, result.get(i - 1));
} else {
result.set(i, getAt(currentStripe));
previousStripe = currentStripe;
}
}
final List<L> asStripes = (List<L>) result;
return Collections.unmodifiableList(asStripes);
}
// 静态工厂方法
/**
* 创建强引用的分段锁,所有段在创建时初始化。
*
* @param stripes 段数
* @param supplier 锁提供者
* @param <L> 锁类型
* @return 分段锁实例
*/
public static <L> SegmentLock<L> custom(final int stripes, final Supplier<L> supplier) {
return new CompactSegmentLock<>(stripes, supplier);
}
/**
* 创建强引用的可重入锁分段实例。
*
* @param stripes 段数
* @return 分段锁实例
*/
public static SegmentLock<Lock> lock(final int stripes) {
return custom(stripes, PaddedLock::new);
}
/**
* 创建弱引用的可重入锁分段实例,懒加载。
*
* @param stripes 段数
* @return 分段锁实例
*/
public static SegmentLock<Lock> lazyWeakLock(final int stripes) {
return lazyWeakCustom(stripes, () -> new ReentrantLock(false));
}
/**
* 创建弱引用的分段锁,懒加载。
*
* @param stripes 段数
* @param supplier 锁提供者
* @param <L> 锁类型
* @return 分段锁实例
*/
private static <L> SegmentLock<L> lazyWeakCustom(final int stripes, final Supplier<L> supplier) {
return stripes < LARGE_LAZY_CUTOFF
? new SmallLazySegmentLock<>(stripes, supplier)
: new LargeLazySegmentLock<>(stripes, supplier);
}
/**
* 创建强引用的信号量分段实例。
*
* @param stripes 段数
* @param permits 每个信号量的许可数
* @return 分段信号量实例
*/
public static SegmentLock<Semaphore> semaphore(final int stripes, final int permits) {
return custom(stripes, () -> new PaddedSemaphore(permits));
}
/**
* 创建弱引用的信号量分段实例,懒加载。
*
* @param stripes 段数
* @param permits 每个信号量的许可数
* @return 分段信号量实例
*/
public static SegmentLock<Semaphore> lazyWeakSemaphore(final int stripes, final int permits) {
return lazyWeakCustom(stripes, () -> new Semaphore(permits, false));
}
/**
* 创建强引用的读写锁分段实例。
*
* @param stripes 段数
* @return 分段读写锁实例
*/
public static SegmentLock<ReadWriteLock> readWriteLock(final int stripes) {
return custom(stripes, ReentrantReadWriteLock::new);
}
/**
* 创建弱引用的读写锁分段实例,懒加载。
*
* @param stripes 段数
* @return 分段读写锁实例
*/
public static SegmentLock<ReadWriteLock> lazyWeakReadWriteLock(final int stripes) {
return lazyWeakCustom(stripes, WeakSafeReadWriteLock::new);
}
// 内部实现类
/**
* 弱引用安全的读写锁实现,确保读锁和写锁持有对自身的强引用。
*/
private static final class WeakSafeReadWriteLock implements ReadWriteLock {
private final ReadWriteLock delegate;
WeakSafeReadWriteLock() {
this.delegate = new ReentrantReadWriteLock();
}
@Override
public Lock readLock() {
return new WeakSafeLock(delegate.readLock(), this);
}
@Override
public Lock writeLock() {
return new WeakSafeLock(delegate.writeLock(), this);
}
}
/**
* 弱引用安全的锁包装类,确保持有强引用。
*/
private static final class WeakSafeLock implements Lock {
private final Lock delegate;
private final WeakSafeReadWriteLock strongReference;
WeakSafeLock(final Lock delegate, final WeakSafeReadWriteLock strongReference) {
this.delegate = delegate;
this.strongReference = strongReference;
}
@Override
public void lock() {
delegate.lock();
}
@Override
public void lockInterruptibly() throws InterruptedException {
delegate.lockInterruptibly();
}
@Override
public boolean tryLock() {
return delegate.tryLock();
}
@Override
public boolean tryLock(final long time, final java.util.concurrent.TimeUnit unit) throws InterruptedException {
return delegate.tryLock(time, unit);
}
@Override
public void unlock() {
delegate.unlock();
}
@Override
public Condition newCondition() {
return new WeakSafeCondition(delegate.newCondition(), strongReference);
}
}
/**
* 弱引用安全的条件包装类。
*/
private static final class WeakSafeCondition implements Condition {
private final Condition delegate;
/** 防止垃圾回收 */
@SuppressWarnings("FieldCanBeLocal")
private final WeakSafeReadWriteLock strongReference;
WeakSafeCondition(final Condition delegate, final WeakSafeReadWriteLock strongReference) {
this.delegate = delegate;
this.strongReference = strongReference;
}
@Override
public void await() throws InterruptedException {
delegate.await();
}
@Override
public void awaitUninterruptibly() {
delegate.awaitUninterruptibly();
}
@Override
public long awaitNanos(final long nanosTimeout) throws InterruptedException {
return delegate.awaitNanos(nanosTimeout);
}
@Override
public boolean await(final long time, final TimeUnit unit) throws InterruptedException {
return delegate.await(time, unit);
}
@Override
public boolean awaitUntil(final Date deadline) throws InterruptedException {
return delegate.awaitUntil(deadline);
}
@Override
public void signal() {
delegate.signal();
}
@Override
public void signalAll() {
delegate.signalAll();
}
}
/**
* 抽象基类,确保段数为 2 的幂。
*/
private abstract static class PowerOfTwoSegmentLock<L> extends SegmentLock<L> {
final int mask;
PowerOfTwoSegmentLock(final int stripes) {
Assert.isTrue(stripes > 0, "Segment count must be positive");
this.mask = stripes > Integer.MAX_VALUE / 2 ? ALL_SET : ceilToPowerOfTwo(stripes) - 1;
}
@Override
final int indexFor(final Object key) {
final int hash = smear(key.hashCode());
return hash & mask;
}
@Override
public final L get(final Object key) {
return getAt(indexFor(key));
}
}
/**
* 强引用实现,使用固定数组存储段。
*/
private static class CompactSegmentLock<L> extends PowerOfTwoSegmentLock<L> {
private final Object[] array;
CompactSegmentLock(final int stripes, final Supplier<L> supplier) {
super(stripes);
Assert.isTrue(stripes <= Integer.MAX_VALUE / 2, "Segment count must be <= 2^30");
this.array = new Object[mask + 1];
for (int i = 0; i < array.length; i++) {
array[i] = supplier.get();
}
}
@SuppressWarnings("unchecked")
@Override
public L getAt(final int index) {
if (index < 0 || index >= array.length) {
throw new IllegalArgumentException("Index " + index + " out of bounds for size " + array.length);
}
return (L) array[index];
}
@Override
public int size() {
return array.length;
}
}
/**
* 小规模弱引用实现,使用 AtomicReferenceArray 存储段。
*/
private static class SmallLazySegmentLock<L> extends PowerOfTwoSegmentLock<L> {
final AtomicReferenceArray<ArrayReference<? extends L>> locks;
final Supplier<L> supplier;
final int size;
final ReferenceQueue<L> queue = new ReferenceQueue<>();
SmallLazySegmentLock(final int stripes, final Supplier<L> supplier) {
super(stripes);
this.size = (mask == ALL_SET) ? Integer.MAX_VALUE : mask + 1;
this.locks = new AtomicReferenceArray<>(size);
this.supplier = supplier;
}
@Override
public L getAt(final int index) {
if (size != Integer.MAX_VALUE) {
Assert.isTrue(index >= 0 && index < size, "Index out of bounds");
}
ArrayReference<? extends L> existingRef = locks.get(index);
L existing = existingRef == null ? null : existingRef.get();
if (existing != null) {
return existing;
}
final L created = supplier.get();
final ArrayReference<L> newRef = new ArrayReference<>(created, index, queue);
while (!locks.compareAndSet(index, existingRef, newRef)) {
existingRef = locks.get(index);
existing = existingRef == null ? null : existingRef.get();
if (existing != null) {
return existing;
}
}
drainQueue();
return created;
}
private void drainQueue() {
Reference<? extends L> ref;
while ((ref = queue.poll()) != null) {
final ArrayReference<? extends L> arrayRef = (ArrayReference<? extends L>) ref;
locks.compareAndSet(arrayRef.index, arrayRef, null);
}
}
@Override
public int size() {
return size;
}
private static final class ArrayReference<L> extends WeakReference<L> {
final int index;
ArrayReference(final L referent, final int index, final ReferenceQueue<L> queue) {
super(referent, queue);
this.index = index;
}
}
}
/**
* 大规模弱引用实现,使用 ConcurrentMap 存储段。
*/
private static class LargeLazySegmentLock<L> extends PowerOfTwoSegmentLock<L> {
final ConcurrentMap<Integer, L> locks;
final Supplier<L> supplier;
final int size;
LargeLazySegmentLock(final int stripes, final Supplier<L> supplier) {
super(stripes);
this.size = (mask == ALL_SET) ? Integer.MAX_VALUE : mask + 1;
this.locks = new ConcurrentHashMap<>();
this.supplier = supplier;
}
@Override
public L getAt(final int index) {
if (size != Integer.MAX_VALUE) {
Assert.isTrue(index >= 0 && index < size, "Index out of bounds");
}
L existing = locks.get(index);
if (existing != null) {
return existing;
}
final L created = supplier.get();
existing = locks.putIfAbsent(index, created);
return existing != null ? existing : created;
}
@Override
public int size() {
return size;
}
}
private static final int ALL_SET = ~0;
private static int ceilToPowerOfTwo(final int x) {
return 1 << (Integer.SIZE - Integer.numberOfLeadingZeros(x - 1));
}
private static int smear(int hashCode) {
hashCode ^= (hashCode >>> 20) ^ (hashCode >>> 12);
return hashCode ^ (hashCode >>> 7) ^ (hashCode >>> 4);
}
/**
* 填充锁,避免缓存行干扰。
*/
private static class PaddedLock extends ReentrantLock {
private static final long serialVersionUID = 1L;
long unused1;
long unused2;
long unused3;
PaddedLock() {
super(false);
}
}
/**
* 填充信号量,避免缓存行干扰。
*/
private static class PaddedSemaphore extends Semaphore {
private static final long serialVersionUID = 1L;
long unused1;
long unused2;
long unused3;
PaddedSemaphore(final int permits) {
super(permits, false);
}
}
}

View File

@@ -516,7 +516,11 @@ public class FileUtilTest {
final String parseSmbPath = FileUtil.getAbsolutePath(smbPath);
assertEquals(smbPath, parseSmbPath);
assertTrue(FileUtil.isAbsolutePath(smbPath));
assertTrue(Paths.get(smbPath).isAbsolute());
if(FileUtil.isWindows()){
// 在Windows下`\`路径是绝对路径也表示SMB路径
// 但是在Linux下`\`表示转义字符,并不被识别为路径
assertTrue(Paths.get(smbPath).isAbsolute());
}
}
@Test

View File

@@ -0,0 +1,203 @@
package org.dromara.hutool.core.thread.lock;
import org.dromara.hutool.core.collection.ListUtil;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.*;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.Lock;
import java.util.concurrent.locks.ReadWriteLock;
import static org.junit.jupiter.api.Assertions.*;
public class SegmentLockTest {
private static final int SEGMENT_COUNT = 4;
private SegmentLock<Lock> strongLock;
private SegmentLock<Lock> weakLock;
private SegmentLock<Semaphore> semaphore;
private SegmentLock<ReadWriteLock> readWriteLock;
@BeforeEach
public void setUp() {
strongLock = SegmentLock.lock(SEGMENT_COUNT);
weakLock = SegmentLock.lazyWeakLock(SEGMENT_COUNT);
semaphore = SegmentLock.semaphore(SEGMENT_COUNT, 2);
readWriteLock = SegmentLock.readWriteLock(SEGMENT_COUNT);
}
@Test
public void testSize() {
assertEquals(SEGMENT_COUNT, strongLock.size());
assertEquals(SEGMENT_COUNT, weakLock.size());
assertEquals(SEGMENT_COUNT, semaphore.size());
assertEquals(SEGMENT_COUNT, readWriteLock.size());
}
@SuppressWarnings("StringOperationCanBeSimplified")
@Test
public void testGetWithSameKey() {
// 相同 key 应返回相同锁
final String key1 = "testKey";
final String key2 = new String("testKey"); // equals 但不同对象
final Lock lock1 = strongLock.get(key1);
final Lock lock2 = strongLock.get(key2);
assertSame(lock1, lock2, "相同 key 应返回同一锁对象");
final Lock weakLock1 = weakLock.get(key1);
final Lock weakLock2 = weakLock.get(key2);
assertSame(weakLock1, weakLock2, "弱引用锁相同 key 应返回同一锁对象");
}
@Test
public void testGetAt() {
for (int i = 0; i < SEGMENT_COUNT; i++) {
final Lock lock = strongLock.getAt(i);
assertNotNull(lock, "getAt 返回的锁不应为 null");
}
assertThrows(IllegalArgumentException.class, () -> strongLock.getAt(SEGMENT_COUNT),
"超出段数的索引应抛出异常");
}
@Test
public void testBulkGet() {
final List<String> keys = ListUtil.of("key1", "key2", "key3");
final Iterable<Lock> locks = strongLock.bulkGet(keys);
final List<Lock> lockList = ListUtil.of(locks);
assertEquals(3, lockList.size(), "bulkGet 返回的锁数量应与 key 数量一致");
// 检查顺序性
int prevIndex = -1;
for (final Lock lock : lockList) {
final int index = findIndex(strongLock, lock);
assertTrue(index >= prevIndex, "bulkGet 返回的锁应按索引升序");
prevIndex = index;
}
}
@Test
public void testLockConcurrency() throws InterruptedException {
final int threadCount = SEGMENT_COUNT * 2;
final CountDownLatch startLatch = new CountDownLatch(1);
final CountDownLatch endLatch = new CountDownLatch(threadCount);
final ExecutorService executor = Executors.newFixedThreadPool(threadCount);
final List<String> keys = new ArrayList<>();
for (int i = 0; i < threadCount; i++) {
keys.add("key" + i);
}
for (int i = 0; i < threadCount; i++) {
final String key = keys.get(i);
executor.submit(() -> {
try {
startLatch.await();
final Lock lock = strongLock.get(key);
lock.lock();
try {
Thread.sleep(100); // 模拟工作
} finally {
lock.unlock();
}
} catch (final InterruptedException e) {
Thread.currentThread().interrupt();
} finally {
endLatch.countDown();
}
});
}
startLatch.countDown();
assertTrue(endLatch.await(2000, java.util.concurrent.TimeUnit.MILLISECONDS),
"并发锁测试应在 2 秒内完成");
executor.shutdown();
}
@Test
public void testSemaphore() {
final Semaphore sem = semaphore.get("testKey");
assertEquals(2, sem.availablePermits(), "信号量初始许可应为 2");
sem.acquireUninterruptibly(2);
assertEquals(0, sem.availablePermits(), "获取所有许可后应为 0");
sem.release(1);
assertEquals(1, sem.availablePermits(), "释放一个许可后应为 1");
}
@SuppressWarnings("ResultOfMethodCallIgnored")
@Test
public void testReadWriteLock() throws InterruptedException {
final ReadWriteLock rwLock = readWriteLock.get("testKey");
final Lock readLock = rwLock.readLock();
final Lock writeLock = rwLock.writeLock();
// 测试读锁可重入
readLock.lock();
assertTrue(readLock.tryLock(), "读锁应允许多个线程同时持有");
readLock.unlock();
readLock.unlock();
final CountDownLatch latch = new CountDownLatch(1);
final ExecutorService executor = Executors.newSingleThreadExecutor();
final AtomicBoolean readLockAcquired = new AtomicBoolean(false);
writeLock.lock();
executor.submit(() -> {
readLockAcquired.set(readLock.tryLock());
latch.countDown();
});
latch.await(500, TimeUnit.MILLISECONDS);
assertFalse(readLockAcquired.get(), "写锁持有时读锁应失败");
writeLock.unlock();
executor.shutdown();
executor.awaitTermination(1, TimeUnit.SECONDS);
}
@Test
public void testWeakReferenceCleanup() throws InterruptedException {
final SegmentLock<Lock> weakLockLarge = SegmentLock.lazyWeakLock(1024); // 超过 LARGE_LAZY_CUTOFF
final Lock lock = weakLockLarge.get("testKey");
System.gc();
Thread.sleep(100);
// 弱引用锁未被其他引用,应仍可获取
final Lock lockAgain = weakLockLarge.get("testKey");
assertSame(lock, lockAgain, "弱引用锁未被回收时应返回同一对象");
}
@Test
public void testInvalidSegmentCount() {
assertThrows(IllegalArgumentException.class, () -> SegmentLock.lock(0),
"段数为 0 应抛出异常");
assertThrows(IllegalArgumentException.class, () -> SegmentLock.lock(-1),
"负段数应抛出异常");
}
@Test
public void testHashDistribution() {
final SegmentLock<Lock> lock = SegmentLock.lock(4);
final int[] counts = new int[4];
for (int i = 0; i < 100; i++) {
final int index = findIndex(lock, lock.get("key" + i));
counts[index]++;
}
for (final int count : counts) {
assertTrue(count > 0, "每个段都应至少被分配到一个 key");
}
}
private int findIndex(final SegmentLock<Lock> lock, final Lock target) {
for (int i = 0; i < lock.size(); i++) {
if (lock.getAt(i) == target) {
return i;
}
}
return -1;
}
}

View File

@@ -0,0 +1,168 @@
/*
* Copyright (c) 2025 Hutool Team and hutool.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.dromara.hutool.crypto.digest;
import org.bouncycastle.crypto.generators.Argon2BytesGenerator;
import org.bouncycastle.crypto.params.Argon2Parameters;
/**
* Argon2加密实现
*
* @author changhr2013
* @author Looly
* @since 5.8.38
*/
public class Argon2 {
/**
* 默认hash长度
*/
public static final int DEFAULT_HASH_LENGTH = 32;
private int hashLength = DEFAULT_HASH_LENGTH;
private final Argon2Parameters.Builder paramsBuilder;
/**
* 构造,默认使用{@link Argon2Parameters#ARGON2_id}类型
*/
public Argon2(){
this(Argon2Parameters.ARGON2_id);
}
/**
* 构造
*
* @param type {@link Argon2Parameters#ARGON2_d}、{@link Argon2Parameters#ARGON2_i}、{@link Argon2Parameters#ARGON2_id}
*/
public Argon2(final int type){
this(new Argon2Parameters.Builder(type));
}
/**
* 构造
*
* @param paramsBuilder 参数构造器
*/
public Argon2(final Argon2Parameters.Builder paramsBuilder){
this.paramsBuilder = paramsBuilder;
}
/**
* 设置hash长度
*
* @param hashLength hash长度
* @return this
*/
public Argon2 setHashLength(final int hashLength){
this.hashLength = hashLength;
return this;
}
/**
* 设置版本
*
* @param version 版本
* @return this
* @see Argon2Parameters#ARGON2_VERSION_10
* @see Argon2Parameters#ARGON2_VERSION_13
*/
public Argon2 setVersion(final int version){
this.paramsBuilder.withVersion(version);
return this;
}
/**
* 设置盐
*
* @param salt 盐
* @return this
*/
public Argon2 setSalt(final byte[] salt){
this.paramsBuilder.withSalt(salt);
return this;
}
/**
* 设置可选的密钥数据,用于增加哈希的复杂性
*
* @param secret 密钥
* @return this
*/
public Argon2 setSecret(final byte[] secret){
this.paramsBuilder.withSecret(secret);
return this;
}
/**
* @param additional 附加数据
* @return this
*/
public Argon2 setAdditional(final byte[] additional){
this.paramsBuilder.withAdditional(additional);
return this;
}
/**
* 设置迭代次数<br>
* 迭代次数越多,生成哈希的时间就越长,破解哈希就越困难
*
* @param iterations 迭代次数
* @return this
*/
public Argon2 setIterations(final int iterations){
this.paramsBuilder.withIterations(iterations);
return this;
}
/**
* 设置内存单位KB<br>
* 内存越大,生成哈希的时间就越长,破解哈希就越困难
*
* @param memoryAsKB 内存单位KB
* @return this
*/
public Argon2 setMemoryAsKB(final int memoryAsKB){
this.paramsBuilder.withMemoryAsKB(memoryAsKB);
return this;
}
/**
* 设置并行度,即同时使用的核心数<br>
* 值越高,生成哈希的时间就越长,破解哈希就越困难
*
* @param parallelism 并行度
* @return this
*/
public Argon2 setParallelism(final int parallelism){
this.paramsBuilder.withParallelism(parallelism);
return this;
}
/**
* 生成hash值
*
* @param password 密码
* @return hash值
*/
public byte[] digest(final char[] password){
final Argon2BytesGenerator generator = new Argon2BytesGenerator();
generator.init(paramsBuilder.build());
final byte[] result = new byte[hashLength];
generator.generateBytes(password, result);
return result;
}
}

View File

@@ -0,0 +1,22 @@
package org.dromara.hutool.crypto.digest;
import org.dromara.hutool.core.codec.binary.Base64;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
public class Argon2Test {
@Test
public void argon2Test() {
final Argon2 argon2 = new Argon2();
final byte[] digest = argon2.digest("123456".toCharArray());
Assertions.assertEquals("wVGMOdzf5EdKGANPeHjaUnaFEJA0BnAq6HcF2psFmFo=", Base64.encode(digest));
}
@Test
public void argon2WithSaltTest() {
final Argon2 argon2 = new Argon2();
argon2.setSalt("123456".getBytes());
final byte[] digest = argon2.digest("123456".toCharArray());
Assertions.assertEquals("sEpbXTdMWra36JXPVxrZMm3xyoR5GkMlLhtW0Kwp9Ag=", Base64.encode(digest));
}
}

View File

@@ -164,5 +164,11 @@
<version>2.4.12</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.sap.cloud.db.jdbc</groupId>
<artifactId>ngdbc</artifactId>
<version>2.24.7</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>

View File

@@ -89,6 +89,9 @@ public class DialectFactory {
} else if (DriverNames.DRIVER_GOLDENDB.equalsIgnoreCase(driverName)) {
// MySQL兼容
return new MysqlDialect(dbConfig);
} else if (DriverNames.DRIVER_HANA.equalsIgnoreCase(driverName)) {
// SAP HANA
return new HanaDialect(dbConfig);
}
}
// 无法识别可支持的数据库类型默认使用ANSI方言可兼容大部分SQL语句

View File

@@ -25,7 +25,7 @@ import org.dromara.hutool.core.text.StrUtil;
* @author Looly
*/
public enum DialectName {
ANSI, MYSQL, ORACLE, POSTGRESQL, SQLITE3, H2, SQLSERVER, SQLSERVER2012, PHOENIX, DM;
ANSI, MYSQL, ORACLE, POSTGRESQL, SQLITE3, H2, SQLSERVER, SQLSERVER2012, PHOENIX, DM, HANA;
/**
* 是否为指定数据库方言,检查时不分区大小写

View File

@@ -0,0 +1,107 @@
/*
* Copyright (c) 2025 Hutool Team and hutool.cn
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
package org.dromara.hutool.db.dialect.impl;
import org.dromara.hutool.core.text.StrUtil;
import org.dromara.hutool.db.DbException;
import org.dromara.hutool.db.Entity;
import org.dromara.hutool.db.config.DbConfig;
import org.dromara.hutool.db.dialect.DialectName;
import org.dromara.hutool.db.sql.QuoteWrapper;
import org.dromara.hutool.db.sql.SqlBuilder;
import org.dromara.hutool.db.sql.StatementUtil;
import java.sql.Connection;
import java.sql.PreparedStatement;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
/**
* Hana数据库方言
*
* @author daoyou.dev
*/
public class HanaDialect extends AnsiSqlDialect {
private static final long serialVersionUID = 1L;
/**
* 构造
*
* @param config 数据库配置对象
*/
public HanaDialect(final DbConfig config) {
super(config);
quoteWrapper = new QuoteWrapper('"');
}
@Override
public String dialectName() {
return DialectName.HANA.name();
}
/**
* 构建用于upsert的{@link PreparedStatement}。
* SAP HANA 使用 MERGE INTO 语法来实现 UPSERT 操作。
* <p>
* 生成 SQL 语法为:
* <pre>{@code
* MERGE INTO demo AS target
* USING (SELECT ? AS a, ? AS b, ? AS c FROM DUMMY) AS source
* ON target.id = source.id
* WHEN MATCHED THEN
* UPDATE SET target.a = source.a, target.b = source.b, target.c = source.c
* WHEN NOT MATCHED THEN
* INSERT (a, b, c) VALUES (source.a, source.b, source.c);
* }</pre>
*
* @param conn 数据库连接对象
* @param entity 数据实体类(包含表名)
* @param keys 主键字段数组,通常用于确定匹配条件(联合主键)
* @return PreparedStatement
* @throws DbException SQL 执行异常
*/
@Override
public PreparedStatement psForUpsert(final Connection conn, final Entity entity, final String... keys) throws DbException {
SqlBuilder.validateEntity(entity);
final SqlBuilder builder = SqlBuilder.of(quoteWrapper);
final List<String> columns = new ArrayList<>();
// 构建字段部分和参数占位符部分
entity.forEach((field, value) -> {
if (StrUtil.isNotBlank(field)) {
columns.add(quoteWrapper != null ? quoteWrapper.wrap(field) : field);
builder.addParams(value);
}
});
String tableName = entity.getTableName();
if (quoteWrapper != null) {
tableName = quoteWrapper.wrap(tableName);
}
// 构建 UPSERT 语句
builder.append("UPSERT ").append(tableName).append(" (");
builder.append(String.join(", ", columns));
builder.append(") VALUES (");
builder.append(String.join(", ", Collections.nCopies(columns.size(), "?")));
builder.append(") WITH PRIMARY KEY");
return StatementUtil.prepareStatement(false, this.dbConfig, conn, builder.build(), builder.getParamValueArray());
}
}

View File

@@ -33,7 +33,7 @@ import java.util.List;
* @author Looly
* @since 6.0.0
*/
public class DriverIdentifier implements DriverNames{
public class DriverIdentifier implements DriverNames {
/**
* 单例驱动识别器
@@ -131,7 +131,9 @@ public class DriverIdentifier implements DriverNames{
new StartsWithDriverMatcher(DRIVER_GAUSS, "jdbc:zenith:"),
new StartsWithDriverMatcher(DRIVER_OPENGAUSS, "jdbc:opengauss:"),
// 中兴GoldenDB
new StartsWithDriverMatcher(DRIVER_GOLDENDB, "jdbc:goldendb:")
new StartsWithDriverMatcher(DRIVER_GOLDENDB, "jdbc:goldendb:"),
// SAP HANA
new StartsWithDriverMatcher(DRIVER_HANA, "jdbc:sap:")
);
}

View File

@@ -252,9 +252,12 @@ public interface DriverNames {
* JDBC 驱动 Greenplum
*/
String DRIVER_GREENPLUM = "com.pivotal.jdbc.GreenplumDriver";
/**
* JDBC 驱动 GoldenDB
*/
String DRIVER_GOLDENDB = "com.goldendb.jdbc.Driver";
/**
* JDBC 驱动 Sap Hana
*/
String DRIVER_HANA = "com.sap.db.jdbc.Driver";
}

View File

@@ -0,0 +1,82 @@
package org.dromara.hutool.db;
import org.dromara.hutool.core.lang.Console;
import org.junit.jupiter.api.Disabled;
import org.junit.jupiter.api.Test;
import java.sql.SQLException;
import java.util.List;
import static org.junit.jupiter.api.Assertions.assertEquals;
public class HanaTest {
//@BeforeAll
public static void createTable() {
final Db db = Db.of("hana");
final long count = db.count("SELECT * FROM SYS.TABLES WHERE TABLE_NAME = ? AND SCHEMA_NAME = CURRENT_SCHEMA", "user");
if (count > 0) {
db.execute("drop table \"user\"");
}
db.execute("CREATE COLUMN TABLE \"user\" (\"id\" INT NOT NULL, \"account\" VARCHAR(255), \"name\" VARCHAR(255), \"text\" VARCHAR(255), \"test1\" VARCHAR(255), \"pass\" VARCHAR(255), PRIMARY KEY (\"id\"))");
}
@Test
@Disabled
public void insertTest() {
for (int id = 100; id < 200; id++) {
Db.of("hana").insert(Entity.of("user")//
.set("id", id)//
.set("name", "测试用户" + id)//
.set("text", "描述" + id)//
.set("test1", "t" + id)//
);
}
}
/**
* 事务测试<br>
* 更新三条信息低2条后抛出异常正常情况下三条都应该不变
*
* @throws SQLException SQL异常
*/
@Test
@Disabled
public void txTest() throws SQLException {
Db.of("hana").tx(db -> {
final int update = db.update(Entity.of("user").set("text", "描述100"), Entity.of().set("id", 100));
db.update(Entity.of("user").set("text", "描述101"), Entity.of().set("id", 101));
if (1 == update) {
// 手动指定异常,然后测试回滚触发
throw new RuntimeException("Error");
}
db.update(Entity.of("user").set("text", "描述102"), Entity.of().set("id", 102));
});
}
@Test
@Disabled
public void pageTest() {
final PageResult<Entity> result = Db.of("hana").page(Entity.of("\"user\""), new Page(2, 10));
for (final Entity entity : result) {
Console.log(entity.get("id"));
}
}
@Test
@Disabled
public void getTimeStampTest() {
final List<Entity> all = Db.of("hana").findAll("user");
Console.log(all);
}
@Test
@Disabled
public void upsertTest() {
final Db db = Db.of("hana");
db.insert(Entity.of("user").set("id", 1).set("account", "ice").set("pass", "123456"));
db.upsert(Entity.of("user").set("id", 1).set("account", "daoyou").set("pass", "a123456").set("name", "道友"));
final Entity user = db.get(Entity.of("user").set("id", 1));
System.out.println("user=======" + user.getStr("account") + "___" + user.getStr("pass"));
assertEquals("daoyou", user.getStr("account"));
}
}

View File

@@ -108,3 +108,9 @@ url = jdbc:oceanbase://localhost:2881/test
user = root
pass = 123456
remarks = true
[hana]
url = jdbc:sap://localhost:30015/HAP_CONN?autoReconnect=true
user = DB
pass = 123456
remarks = true

View File

@@ -16,7 +16,6 @@
package org.dromara.hutool.poi.csv;
import org.dromara.hutool.core.io.file.FileUtil;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
@@ -36,8 +35,9 @@ public class Issue3705Test {
csvWriter.flush();
}
// CsvWriteConfig中默认为`\r\n`
Assertions.assertEquals(
"\"2024-08-20 14:24:35,\"" + FileUtil.getLineSeparator() + "最后一行",
"\"2024-08-20 14:24:35,\"\r\n最后一行",
stringWriter.toString());
}
}

View File

@@ -241,6 +241,12 @@
<generateBackupPoms>false</generateBackupPoms>
</configuration>
</plugin>
<!-- 单元测试 -->
<plugin>
<groupId>org.apache.maven.plugins</groupId>
<artifactId>maven-surefire-plugin</artifactId>
<version>3.5.3</version>
</plugin>
<!-- 测试覆盖度 -->
<plugin>
<groupId>org.codehaus.mojo</groupId>