Compare commits

...

14 Commits

Author SHA1 Message Date
Looly
d2485f7612 增强HexUtil自动去除0x#前缀(pr#4163@Github) 2025-11-26 20:02:20 +08:00
Looly
b3d17e70ae add FontStyle 2025-11-26 19:40:52 +08:00
Looly
c34bdaf776 add SIEVE算法 2025-11-26 17:44:12 +08:00
Golden Looly
6088516bf2
Merge pull request #4157 from Lettuceleaves/feat/sieve-cache
Feat/sieve cache
2025-11-26 17:39:33 +08:00
Looly
7393f66d06 提供 core 模块下的 text 中布隆过滤器的单过滤器多哈希函数支持 2025-11-26 17:35:20 +08:00
Golden Looly
4d4bf08fc1
Merge pull request #4139 from Lettuceleaves/feat/bloom-multi-hash
Feat/bloom multi hash
2025-11-26 17:27:24 +08:00
Looly
28502528f5 修复DateModifier处理AM和PM的ceiling和round问题(pr#4161@Github) 2025-11-26 17:24:22 +08:00
Looly
bdbe51fe31 修复ReflectUtil.newInstanceIfPossible传入Object逻辑错误(pr#4160@Github) 2025-11-26 16:30:29 +08:00
LettuceLeaves
da8971a352 fix(core):修复在扫描时淘汰热数据的问题 2025-11-26 02:00:17 +08:00
LettuceLeaves
d2d4f8499b test(core):添加Sieve缓存测试 2025-11-26 01:56:18 +08:00
LettuceLeaves
2db66af020 feat(core):实现Sieve缓存 2025-11-26 00:55:43 +08:00
LettuceLeaves
efee8a14b0 test(core):添加单布隆过滤器多哈希函数的测试用例 2025-11-21 01:50:34 +08:00
LettuceLeaves
6ad6a8022e fix(core):使用位运算解决极端情况下Math.abs()失败导致的BitSet报错 2025-11-21 01:37:52 +08:00
LettuceLeaves
a31e3ff096 feat(core):单个布隆过滤器允许使用多个哈希函数 2025-11-21 01:11:54 +08:00
15 changed files with 833 additions and 84 deletions

View File

@ -0,0 +1,288 @@
/*
* Copyright (c) 2013-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 cn.hutool.v7.core.cache.impl;
import cn.hutool.v7.core.lang.mutable.Mutable;
import cn.hutool.v7.core.lang.mutable.MutableObj;
import java.io.Serial;
import java.util.HashMap;
import java.util.Iterator;
import java.util.concurrent.locks.ReentrantLock;
/**
* SIEVE 缓存算法实现<br>
* <p>
* SIEVE 是一种比 LRU 更简单且通常更高效的缓存算法<br>
* 核心特性<br>
* 缓存命中时仅将节点的 {@code visited} 标记设为 true不移动节点位置<br>
* 淘汰时使用 {@code hand} 指针从尾部扫描淘汰 {@code visited=false} 的节点<br>
* 新加入节点 {@code visited = false} 且置于头部Hand 指针扫描时会优先淘汰它提供抗扫描能力<br>
* </p>
* 来自<a href="https://github.com/chinabugotech/hutool/pull/4157">pr#4157@Github</a>
*
* @param <K> 键类型
* @param <V> 值类型
* @author Lettuceleaves
*/
public class SieveCache<K, V> extends LockedCache<K, V> {
@Serial
private static final long serialVersionUID = 1L;
/**
* 双向链表头节点
*/
private SieveCacheObj<K, V> head;
/**
* 双向链表尾节点
*/
private SieveCacheObj<K, V> tail;
/**
* 下一次扫描的起始位置
*/
private SieveCacheObj<K, V> hand;
/**
* 构造<br>
* 默认无超时
*
* @param capacity 容量
*/
public SieveCache(final int capacity) {
this(capacity, 0);
}
/**
* 构造
*
* @param capacity 容量
* @param timeout 默认超时时间单位毫秒
*/
public SieveCache(int capacity, final long timeout) {
if (Integer.MAX_VALUE == capacity) {
capacity -= 1;
}
this.capacity = capacity;
this.timeout = timeout;
// 这里的设置 capacity + 1, 1.0f 避免触发扩容
this.cacheMap = new HashMap<>(capacity + 1, 1.0f);
this.lock = new ReentrantLock();
}
@Override
protected void putWithoutLock(final K key, final V object, final long timeout) {
final Mutable<K> keyObj = MutableObj.of(key);
SieveCacheObj<K, V> co = (SieveCacheObj<K, V>) cacheMap.get(keyObj);
if (co != null) {
final SieveCacheObj<K, V> newCo = new SieveCacheObj<>(key, object, timeout);
// 新加入的节点默认刚访问过防止立刻被淘汰
newCo.visited = true;
// 替换旧节点
replaceNode(co, newCo);
cacheMap.put(keyObj, newCo);
} else {
co = new SieveCacheObj<>(key, object, timeout);
cacheMap.put(keyObj, co);
addToHead(co);
co.visited = false;
if (cacheMap.size() > capacity) {
pruneCache();
}
}
}
/**
* 在双向链表中用 newNode 替换 oldNode保持链表结构不变
*
* @param oldNode 旧节点
* @param newNode 新节点
*/
private void replaceNode(final SieveCacheObj<K, V> oldNode, final SieveCacheObj<K, V> newNode) {
newNode.prev = oldNode.prev;
newNode.next = oldNode.next;
// 更新前向指针
if (oldNode.prev != null) {
oldNode.prev.next = newNode;
} else {
head = newNode;
}
// 更新后向指针
if (oldNode.next != null) {
oldNode.next.prev = newNode;
} else {
tail = newNode;
}
// 将hand转移至新节点防止扫描时淘汰热点数据
if (hand == oldNode) {
hand = newNode;
}
oldNode.prev = null;
oldNode.next = null;
}
@Override
protected CacheObj<K, V> getOrRemoveExpiredWithoutLock(final K key) {
final Mutable<K> keyObj = MutableObj.of(key);
final SieveCacheObj<K, V> co = (SieveCacheObj<K, V>) cacheMap.get(keyObj);
if (null != co) {
if (co.isExpired()) {
removeWithoutLock(key);
return null;
}
co.visited = true;
co.lastAccess = System.currentTimeMillis();
}
return co;
}
@Override
protected CacheObj<K, V> removeWithoutLock(final K key) {
final Mutable<K> keyObj = MutableObj.of(key);
final SieveCacheObj<K, V> co = (SieveCacheObj<K, V>) cacheMap.remove(keyObj);
if (co != null) {
removeNode(co);
}
return co;
}
/**
* 优先清理过期对象如果容量仍溢出反向扫描visited为false的节点设置true节点为false
*/
@Override
protected int pruneCache() {
int count = 0;
if (isPruneExpiredActive()) {
final Iterator<CacheObj<K, V>> values = cacheObjIter();
CacheObj<K, V> co;
while (values.hasNext()) {
co = values.next();
if (co.isExpired()) {
values.remove();
removeNode((SieveCacheObj<K, V>) co);
onRemove(co.key, co.obj);
count++;
}
}
}
if (cacheMap.size() > capacity) {
if (hand == null) {
hand = tail;
}
while (cacheMap.size() > capacity) {
if (hand == null) {
hand = tail;
}
if (!hand.visited) {
final SieveCacheObj<K, V> victim = hand;
hand = hand.prev;
final Mutable<K> keyObj = MutableObj.of(victim.key);
cacheMap.remove(keyObj);
removeNode(victim);
onRemove(victim.key, victim.obj);
count++;
} else {
hand.visited = false;
hand = hand.prev;
}
}
}
return count;
}
/**
* 将节点加入链表头部
*
* @param node 节点
*/
private void addToHead(final SieveCacheObj<K, V> node) {
node.next = head;
node.prev = null;
if (head != null) {
head.prev = node;
}
head = node;
if (tail == null) {
tail = node;
}
}
/**
* 从链表中移除节点
*
* @param node 节点
*/
private void removeNode(final SieveCacheObj<K, V> node) {
if (node == hand) {
hand = node.prev;
}
if (node.prev != null) {
node.prev.next = node.next;
} else {
head = node.next;
}
if (node.next != null) {
node.next.prev = node.prev;
} else {
tail = node.prev;
}
node.next = null;
node.prev = null;
}
/**
* 给节点添加visited属性用于Sieve缓存淘汰策略
*/
private static class SieveCacheObj<K, V> extends CacheObj<K, V> {
@Serial
private static final long serialVersionUID = 1L;
/**
* 是否被访问过
*/
boolean visited = false;
/**
* 前向节点
*/
SieveCacheObj<K, V> prev;
/**
* 后向节点
*/
SieveCacheObj<K, V> next;
protected SieveCacheObj(final K key, final V obj, final long ttl) {
super(key, obj, ttl);
}
}
}

View File

@ -175,7 +175,7 @@ public class HexUtil extends Hex {
* @since 5.7.4 * @since 5.7.4
*/ */
public static int hexToInt(final String value) { public static int hexToInt(final String value) {
return Integer.parseInt(value, 16); return Integer.parseInt(removeHexPrefix(value), 16);
} }
/** /**
@ -197,7 +197,7 @@ public class HexUtil extends Hex {
* @since 5.7.4 * @since 5.7.4
*/ */
public static long hexToLong(final String value) { public static long hexToLong(final String value) {
return Long.parseLong(value, 16); return Long.parseLong(removeHexPrefix(value), 16);
} }
/** /**
@ -223,7 +223,7 @@ public class HexUtil extends Hex {
if (null == hexStr) { if (null == hexStr) {
return null; return null;
} }
return new BigInteger(hexStr, 16); return new BigInteger(removeHexPrefix(hexStr), 16);
} }
/** /**
@ -263,4 +263,24 @@ public class HexUtil extends Hex {
return builder.toString(); return builder.toString();
} }
/**
* 移除Hex字符串前缀前缀包括0x, 0X, #
*
* @param hexStr 16进制字符串
* @return 移除前缀后的字符串
*/
private static String removeHexPrefix(final String hexStr) {
if (StrUtil.length(hexStr) > 1) {
final char c0 = hexStr.charAt(0);
switch (c0) {
case '0':
if (hexStr.charAt(1) == 'x' || hexStr.charAt(1) == 'X') {
return hexStr.substring(2);
}
case '#':
return hexStr.substring(1);
}
}
return hexStr;
}
} }

View File

@ -89,7 +89,7 @@ public class DateModifier {
case ROUND: case ROUND:
final int min = isAM ? 0 : 12; final int min = isAM ? 0 : 12;
final int max = isAM ? 11 : 23; final int max = isAM ? 11 : 23;
final int href = (max - min) / 2 + 1; final int href = min + (max - min) / 2 + 1;
final int value = calendar.get(Calendar.HOUR_OF_DAY); final int value = calendar.get(Calendar.HOUR_OF_DAY);
calendar.set(Calendar.HOUR_OF_DAY, (value < href) ? min : max); calendar.set(Calendar.HOUR_OF_DAY, (value < href) ? min : max);
break; break;

View File

@ -133,9 +133,11 @@ public class ConstructorUtil {
* 对于某些特殊的接口按照其默认实现实例化例如 * 对于某些特殊的接口按照其默认实现实例化例如
* <pre> * <pre>
* Map - HashMap * Map - HashMap
* Collction - ArrayList
* List - ArrayList * List - ArrayList
* Set - HashSet * Set - HashSet
* Queue - LinkedList
* Deque - LinkedList
* Collection- ArrayList
* </pre> * </pre>
* *
* @param <T> 对象类型 * @param <T> 对象类型

View File

@ -46,7 +46,7 @@ import java.util.*;
* *
* @param <T> 对象类型 * @param <T> 对象类型
*/ */
public class PossibleObjectCreator<T> implements ObjectCreator<T> { public record PossibleObjectCreator<T>(Class<T> clazz) implements ObjectCreator<T> {
/** /**
* 创建默认的对象实例化器 * 创建默认的对象实例化器
@ -59,8 +59,6 @@ public class PossibleObjectCreator<T> implements ObjectCreator<T> {
return new PossibleObjectCreator<>(clazz); return new PossibleObjectCreator<>(clazz);
} }
final Class<T> clazz;
/** /**
* 构造 * 构造
* *
@ -120,20 +118,34 @@ public class PossibleObjectCreator<T> implements ObjectCreator<T> {
} }
/** /**
* 某些特殊接口的实例化按照默认实现进行 * 对于某些特殊的接口按照其默认实现实例化例如
* <pre>
* Map - HashMap
* List - ArrayList
* Set - HashSet
* Queue - LinkedList
* Deque - LinkedList
* Collection- ArrayList
* </pre>
* *
* @param type 类型 * @param type 类型
* @return 默认类型 * @return 默认类型
*/ */
private static Class<?> resolveType(final Class<?> type) { private static Class<?> resolveType(final Class<?> type) {
if (type.isAssignableFrom(AbstractMap.class)) { if (Object.class != type) {
return HashMap.class; if (type.isAssignableFrom(AbstractMap.class)) {
} else if (type.isAssignableFrom(List.class)) { return HashMap.class;
return ArrayList.class; } else if (type.isAssignableFrom(List.class)) {
} else if (type == SortedSet.class) { return ArrayList.class;
return TreeSet.class; } else if (type == SortedSet.class) {
} else if (type.isAssignableFrom(Set.class)) { return TreeSet.class;
return HashSet.class; } else if (type.isAssignableFrom(Set.class)) {
return HashSet.class;
} else if (type.isAssignableFrom(Queue.class)) {
return LinkedList.class;
} else if (type.isAssignableFrom(Deque.class)) {
return LinkedList.class;
}
} }
return type; return type;

View File

@ -30,7 +30,7 @@ public abstract class AbstractFilter implements BloomFilter {
@Serial @Serial
private static final long serialVersionUID = 1L; private static final long serialVersionUID = 1L;
private final BitSet bitSet; protected final BitSet bitSet;
/** /**
* 容量 * 容量
*/ */
@ -50,12 +50,12 @@ public abstract class AbstractFilter implements BloomFilter {
@Override @Override
public boolean contains(final String str) { public boolean contains(final String str) {
return bitSet.get(Math.abs(hash(str))); return bitSet.get(hash(str));
} }
@Override @Override
public boolean add(final String str) { public boolean add(final String str) {
final int hash = Math.abs(hash(str)); final int hash = hash(str);
if (bitSet.get(hash)) { if (bitSet.get(hash)) {
return false; return false;
} }

View File

@ -16,8 +16,13 @@
package cn.hutool.v7.core.text.bloom; package cn.hutool.v7.core.text.bloom;
import cn.hutool.v7.core.lang.Assert;
import java.io.Serial; import java.io.Serial;
import java.util.function.Function; import java.util.Arrays;
import java.util.Collections;
import java.util.List;
import java.util.function.ToIntFunction;
/** /**
* 基于Hash函数方法的{@link BloomFilter} * 基于Hash函数方法的{@link BloomFilter}
@ -32,27 +37,71 @@ public class FuncFilter extends AbstractFilter {
/** /**
* 创建FuncFilter * 创建FuncFilter
* *
* @param size 最大值 * @param size 最大值
* @param hashFunc Hash函数 * @param hashFuncs Hash函数
* @return FuncFilter * @return FuncFilter
*/ */
public static FuncFilter of(final int size, final Function<String, Number> hashFunc) { @SafeVarargs
return new FuncFilter(size, hashFunc); public static FuncFilter of(final int size, final ToIntFunction<String>... hashFuncs) {
return new FuncFilter(size, hashFuncs);
} }
private final Function<String, Number> hashFunc; // 允许接收多个哈希函数
private final List<ToIntFunction<String>> hashFuncs;
/** /**
* @param size 最大值 * @param size 最大值
* @param hashFunc Hash函数 * @param hashFuncs Hash函数
*/ */
public FuncFilter(final int size, final Function<String, Number> hashFunc) { @SafeVarargs
public FuncFilter(final int size, final ToIntFunction<String>... hashFuncs) {
super(size); super(size);
this.hashFunc = hashFunc; Assert.notEmpty(hashFuncs, "Hash functions must not be empty");
this.hashFuncs = Collections.unmodifiableList(Arrays.asList(hashFuncs));
}
/**
* 兼容父类如果存在多个哈希函数就使用第一个
*
* @param str 字符串
*/
@Override
public int hash(final String str) {
return hash(str, hashFuncs.get(0));
}
/**
*
* @param str 字符串
* @param hashFunc 哈希函数
* @return HashCode 指定哈希函数的计算结果
*/
public int hash(final String str, final ToIntFunction<String> hashFunc) {
// 通过位运算获取正数
return (hashFunc.applyAsInt(str) & 0x7FFFFFFF) % size;
} }
@Override @Override
public int hash(final String str) { public boolean contains(final String str) {
return hashFunc.apply(str).intValue() % size; for (final ToIntFunction<String> hashFunc : hashFuncs) {
if (!bitSet.get(hash(str, hashFunc))) {
return false;
}
}
return true;
}
@Override
public boolean add(final String str) {
boolean add = false;
int hash;
for (final ToIntFunction<String> hashFunc : hashFuncs) {
hash = hash(str, hashFunc);
if (!bitSet.get(hash)) {
bitSet.set(hash);
add = true;
}
}
return add;
} }
} }

View File

@ -0,0 +1,155 @@
/*
* Copyright (c) 2013-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 cn.hutool.v7.core.cache;
import cn.hutool.v7.core.cache.impl.SieveCache;
import cn.hutool.v7.core.thread.ThreadUtil;
import cn.hutool.v7.core.util.RandomUtil;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test;
import java.util.concurrent.CountDownLatch;
import java.util.concurrent.atomic.AtomicInteger;
/**
* SIEVE 缓存算法单元测试
*/
public class SieveCacheTest {
@Test
public void evictionLogicTest() {
final SieveCache<String, String> cache = new SieveCache<>(3);
cache.put("A", "A");
cache.put("B", "B");
cache.put("C", "C");
cache.get("A");
cache.put("D", "D");
Assertions.assertEquals("A", cache.get("A"));
Assertions.assertEquals("C", cache.get("C"));
Assertions.assertEquals("D", cache.get("D"));
Assertions.assertNull(cache.get("B"), "B 应该被淘汰,因为它是未访问过的节点");
}
@Test
public void expiryTest() {
final SieveCache<String, String> cache = new SieveCache<>(3);
cache.put("k1", "v1", 100);
cache.put("k2", "v2", 10000);
ThreadUtil.sleep(200);
Assertions.assertNull(cache.get("k1"), "k1 应该过期");
Assertions.assertEquals("v2", cache.get("k2"), "k2 应该存在");
Assertions.assertEquals(1, cache.size(), "size 应该为 1");
}
@Test
public void listenerTest() {
final AtomicInteger removeCount = new AtomicInteger();
final SieveCache<Integer, Integer> cache = new SieveCache<>(2);
cache.setListener((key, value) -> {
removeCount.incrementAndGet();
});
cache.put(1, 1);
cache.put(2, 2);
cache.put(3, 3);
Assertions.assertEquals(1, removeCount.get());
}
@Test
public void concurrencyPressureTest() throws InterruptedException {
final int threadCount = 20;
final int loopCount = 2000;
final int capacity = 100;
final SieveCache<String, String> cache = new SieveCache<>(capacity);
final CountDownLatch latch = new CountDownLatch(threadCount);
final AtomicInteger errorCount = new AtomicInteger(0);
for (int i = 0; i < threadCount; i++) {
new Thread(() -> {
try {
for (int j = 0; j < loopCount; j++) {
final String key = String.valueOf(RandomUtil.randomInt(0, 1000));
if (RandomUtil.randomBoolean()) {
cache.put(key, "val-" + key);
} else {
cache.get(key);
}
}
} catch (final Exception e) {
errorCount.incrementAndGet();
} finally {
latch.countDown();
}
}).start();
}
latch.await();
Assertions.assertEquals(0, errorCount.get(), "并发执行不应出现异常");
Assertions.assertTrue(cache.size() <= capacity, "缓存大小不应超过容量");
int iteratorCount = 0;
for (final String ignored : cache) {
iteratorCount++;
}
Assertions.assertEquals(cache.size(), iteratorCount, "迭代器数量与 size() 应一致");
}
/**
* 抗扫描能力测试
* 如果扫描数据量过大 50% 容量且热点数据无访问热点数据的保护位会被耗尽因此这里仅模拟少量数据的扫描攻击
*/
@Test
public void scanResistanceTest() {
final int capacity = 10;
final SieveCache<Integer, Integer> cache = new SieveCache<>(capacity);
// 填满热点数据
for (int i = 0; i < capacity; i++) {
cache.put(i, i);
}
// 模拟热点访问
for (int i = 0; i < capacity; i++) {
cache.get(i);
}
// 插入 1 个冷数据
cache.put(10, 10);
int retainedHotItems = 0;
for (int i = 0; i < capacity; i++) {
if (cache.get(i) != null) {
retainedHotItems++;
}
}
Assertions.assertNull(cache.get(10), "冷数据 (10) 应该被淘汰");
Assertions.assertEquals(capacity, retainedHotItems, "所有热点数据 (0-9) 应该被保留");
}
}

View File

@ -1149,4 +1149,31 @@ public class DateUtilTest {
final DateTime dt = DateUtil.parse(dateStr1); final DateTime dt = DateUtil.parse(dateStr1);
assertEquals("2025-07-28 20:00:00", dt.toString()); assertEquals("2025-07-28 20:00:00", dt.toString());
} }
@Test
public void cellingAmPmTest(){
final String dateStr2 = "2020-02-29 10:59:34";
final Date date2 = DateUtil.parse(dateStr2);
DateTime dateTime = DateUtil.ceiling(date2, DateField.AM_PM, false);
assertEquals("2020-02-29 11:59:59.999", dateTime.toString(DateFormatPool.NORM_DATETIME_MS_PATTERN));
dateTime = DateUtil.ceiling(date2, DateField.AM_PM, true);
assertEquals("2020-02-29 11:59:59.000", dateTime.toString(DateFormatPool.NORM_DATETIME_MS_PATTERN));
}
@Test void roundAmPmTest() {
final String dateStr = "2020-02-29 13:59:34";
final Date date = DateUtil.parse(dateStr);
final DateTime dateTime = DateUtil.round(date, DateField.AM_PM);
assertEquals("2020-02-29 12:59:59.000", dateTime.toString(DateFormatPool.NORM_DATETIME_MS_PATTERN));
final String dateStr2 = "2020-02-29 18:59:34";
final Date date2 = DateUtil.parse(dateStr2);
final DateTime dateTime2 = DateUtil.round(date2, DateField.AM_PM);
assertEquals("2020-02-29 23:59:59.000", dateTime2.toString(DateFormatPool.NORM_DATETIME_MS_PATTERN));
}
} }

View File

@ -16,42 +16,42 @@
package cn.hutool.v7.core.reflect; package cn.hutool.v7.core.reflect;
import cn.hutool.v7.core.date.Week;
import lombok.AllArgsConstructor; import lombok.AllArgsConstructor;
import lombok.Data; import lombok.Data;
import lombok.NoArgsConstructor; import lombok.NoArgsConstructor;
import cn.hutool.v7.core.date.Week;
import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import java.util.Collection; import java.util.*;
import java.util.Hashtable;
import java.util.Map; import static org.junit.jupiter.api.Assertions.*;
public class ConstructorUtilTest { public class ConstructorUtilTest {
@Test @Test
public void noneStaticInnerClassTest() { public void noneStaticInnerClassTest() {
final ReflectTestBeans.NoneStaticClass testAClass = ConstructorUtil.newInstanceIfPossible(ReflectTestBeans.NoneStaticClass.class); final ReflectTestBeans.NoneStaticClass testAClass = ConstructorUtil.newInstanceIfPossible(ReflectTestBeans.NoneStaticClass.class);
Assertions.assertNotNull(testAClass); assertNotNull(testAClass);
Assertions.assertEquals(2, testAClass.getA()); assertEquals(2, testAClass.getA());
} }
@Test @Test
public void newInstanceIfPossibleTest(){ public void newInstanceIfPossibleTest(){
//noinspection ConstantConditions //noinspection ConstantConditions
final int intValue = ConstructorUtil.newInstanceIfPossible(int.class); final int intValue = ConstructorUtil.newInstanceIfPossible(int.class);
Assertions.assertEquals(0, intValue); assertEquals(0, intValue);
final Integer integer = ConstructorUtil.newInstanceIfPossible(Integer.class); final Integer integer = ConstructorUtil.newInstanceIfPossible(Integer.class);
Assertions.assertEquals(Integer.valueOf(0), integer); assertEquals(Integer.valueOf(0), integer);
final Map<?, ?> map = ConstructorUtil.newInstanceIfPossible(Map.class); final Map<?, ?> map = ConstructorUtil.newInstanceIfPossible(Map.class);
Assertions.assertNotNull(map); assertNotNull(map);
final Collection<?> collection = ConstructorUtil.newInstanceIfPossible(Collection.class); final Collection<?> collection = ConstructorUtil.newInstanceIfPossible(Collection.class);
Assertions.assertNotNull(collection); assertNotNull(collection);
final Week week = ConstructorUtil.newInstanceIfPossible(Week.class); final Week week = ConstructorUtil.newInstanceIfPossible(Week.class);
Assertions.assertEquals(Week.SUNDAY, week); assertEquals(Week.SUNDAY, week);
final int[] intArray = ConstructorUtil.newInstanceIfPossible(int[].class); final int[] intArray = ConstructorUtil.newInstanceIfPossible(int[].class);
Assertions.assertArrayEquals(new int[0], intArray); Assertions.assertArrayEquals(new int[0], intArray);
@ -61,20 +61,20 @@ public class ConstructorUtilTest {
void newInstanceTest() { void newInstanceTest() {
final TestBean testBean = ConstructorUtil.newInstance(TestBean.class); final TestBean testBean = ConstructorUtil.newInstance(TestBean.class);
Assertions.assertNull(testBean.getA()); Assertions.assertNull(testBean.getA());
Assertions.assertEquals(0, testBean.getB()); assertEquals(0, testBean.getB());
} }
@Test @Test
void newInstanceAllArgsTest() { void newInstanceAllArgsTest() {
final TestBean testBean = ConstructorUtil.newInstance(TestBean.class, "aValue", 1); final TestBean testBean = ConstructorUtil.newInstance(TestBean.class, "aValue", 1);
Assertions.assertEquals("aValue", testBean.getA()); assertEquals("aValue", testBean.getA());
Assertions.assertEquals(1, testBean.getB()); assertEquals(1, testBean.getB());
} }
@Test @Test
void newInstanceHashtableTest() { void newInstanceHashtableTest() {
final Hashtable<?, ?> testBean = ConstructorUtil.newInstance(Hashtable.class); final Hashtable<?, ?> testBean = ConstructorUtil.newInstance(Hashtable.class);
Assertions.assertNotNull(testBean); assertNotNull(testBean);
} }
@Data @Data
@ -84,4 +84,43 @@ public class ConstructorUtilTest {
private String a; private String a;
private int b; private int b;
} }
@Test
public void newInstanceIfPossibleTest2() {
// 测试Object.class不应该被错误地实例化为HashMap应该返回Object实例
final Object objectInstance = ConstructorUtil.newInstanceIfPossible(Object.class);
assertNotNull(objectInstance);
assertEquals(Object.class, objectInstance.getClass());
// 测试Map.class能够正确实例化为HashMap
final Map<?, ?> mapInstance = ConstructorUtil.newInstanceIfPossible(Map.class);
assertNotNull(mapInstance);
assertInstanceOf(HashMap.class, mapInstance);
// 测试Collection.class能够正确实例化为ArrayList
final Collection<?> collectionInstance = ConstructorUtil.newInstanceIfPossible(Collection.class);
assertNotNull(collectionInstance);
assertInstanceOf(ArrayList.class, collectionInstance);
// 测试List.class能够正确实例化为ArrayList
final List<?> listInstance = ConstructorUtil.newInstanceIfPossible(List.class);
assertNotNull(listInstance);
assertInstanceOf(ArrayList.class, listInstance);
// 测试Set.class能够正确实例化为HashSet
final Set<?> setInstance = ConstructorUtil.newInstanceIfPossible(Set.class);
assertNotNull(setInstance);
assertInstanceOf(HashSet.class, setInstance);
// 测试Queue接口能够正确实例化为LinkedList
final Queue<?> queueInstance = ConstructorUtil.newInstanceIfPossible(Queue.class);
assertNotNull(queueInstance);
assertInstanceOf(LinkedList.class, queueInstance);
// 测试Deque接口能够正确实例化为LinkedList
final Deque<?> dequeInstance = ConstructorUtil.newInstanceIfPossible(Deque.class);
assertNotNull(dequeInstance);
assertInstanceOf(LinkedList.class, dequeInstance);
}
} }

View File

@ -22,11 +22,12 @@ import org.junit.jupiter.api.Test;
public class BitMapBloomFilterTest { public class BitMapBloomFilterTest {
private static final int SIZE = 2 * 1024 * 1024 * 8;
@Test @Test
public void filterTest() { public void filterTest() {
final int size = 2 * 1024 * 1024 * 8;
final CombinedBloomFilter filter = new CombinedBloomFilter(FuncFilter.of(size, HashUtil::rsHash)); final CombinedBloomFilter filter = new CombinedBloomFilter(FuncFilter.of(SIZE, HashUtil::rsHash));
filter.add("123"); filter.add("123");
filter.add("abc"); filter.add("abc");
filter.add("ddd"); filter.add("ddd");
@ -35,4 +36,68 @@ public class BitMapBloomFilterTest {
Assertions.assertTrue(filter.contains("ddd")); Assertions.assertTrue(filter.contains("ddd"));
Assertions.assertTrue(filter.contains("123")); Assertions.assertTrue(filter.contains("123"));
} }
@Test
public void multiHashFuncTest() {
final FuncFilter filter = FuncFilter.of(SIZE,
HashUtil::rsHash,
HashUtil::jsHash,
HashUtil::pjwHash,
HashUtil::elfHash,
HashUtil::bkdrHash,
HashUtil::sdbmHash,
HashUtil::djbHash,
HashUtil::dekHash,
HashUtil::apHash,
HashUtil::javaDefaultHash
);
filter.add("Hutool");
filter.add("BloomFilter");
filter.add("Java");
Assertions.assertTrue(filter.contains("Hutool"));
Assertions.assertTrue(filter.contains("BloomFilter"));
Assertions.assertTrue(filter.contains("Java"));
Assertions.assertFalse(filter.contains("Python"));
Assertions.assertFalse(filter.contains("Go"));
Assertions.assertFalse(filter.contains("hutool"));
}
@Test
public void combinedMultiHashTest() {
final FuncFilter multiHashFuncFilter = FuncFilter.of(SIZE,
HashUtil::bkdrHash,
HashUtil::apHash,
HashUtil::djbHash
);
final CombinedBloomFilter filter = new CombinedBloomFilter(multiHashFuncFilter);
filter.add("123123WASD-WASD");
Assertions.assertTrue(filter.contains("123123WASD-WASD"));
Assertions.assertFalse(filter.contains("123123WASD-WASD-false"));
}
@Test
public void chineseStringWithThreeHashesTest() {
final FuncFilter filter = FuncFilter.of(SIZE,
HashUtil::bkdrHash,
HashUtil::apHash,
HashUtil::djbHash
);
final String s1 = "你好世界";
final String s2 = "双亲委派";
final String s3 = "测试工程师";
filter.add(s1);
filter.add(s2);
filter.add(s3);
Assertions.assertTrue(filter.contains(s1), "应包含: " + s1);
Assertions.assertTrue(filter.contains(s2), "应包含: " + s2);
Assertions.assertTrue(filter.contains(s3), "应包含: " + s3);
Assertions.assertFalse(filter.contains("我好世界"), "多字");
Assertions.assertFalse(filter.contains("父亲委派"), "改字");
Assertions.assertFalse(filter.contains("测试"), "子串");
Assertions.assertFalse(filter.contains(""), "空串");
Assertions.assertFalse(filter.contains("👍"), "未添加的");
}
} }

View File

@ -20,8 +20,11 @@ import cn.hutool.v7.core.codec.binary.HexUtil;
import org.junit.jupiter.api.Assertions; import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.Test; import org.junit.jupiter.api.Test;
import java.math.BigInteger;
import java.nio.charset.StandardCharsets; import java.nio.charset.StandardCharsets;
import static org.junit.jupiter.api.Assertions.assertEquals;
/** /**
* HexUtil单元测试 * HexUtil单元测试
* @author Looly * @author Looly
@ -36,22 +39,22 @@ public class HexUtilTest {
final String hex = HexUtil.encodeStr(str, CharsetUtil.UTF_8); final String hex = HexUtil.encodeStr(str, CharsetUtil.UTF_8);
final String decodedStr = HexUtil.decodeStr(hex); final String decodedStr = HexUtil.decodeStr(hex);
Assertions.assertEquals(str, decodedStr); assertEquals(str, decodedStr);
} }
@Test @Test
public void issueI50MI6Test(){ public void issueI50MI6Test(){
final String s = HexUtil.encodeStr("".getBytes(StandardCharsets.UTF_16BE)); final String s = HexUtil.encodeStr("".getBytes(StandardCharsets.UTF_16BE));
Assertions.assertEquals("70df", s); assertEquals("70df", s);
} }
@Test @Test
public void toUnicodeHexTest() { public void toUnicodeHexTest() {
String unicodeHex = HexUtil.toUnicodeHex('\u2001'); String unicodeHex = HexUtil.toUnicodeHex('\u2001');
Assertions.assertEquals("\\u2001", unicodeHex); assertEquals("\\u2001", unicodeHex);
unicodeHex = HexUtil.toUnicodeHex('你'); unicodeHex = HexUtil.toUnicodeHex('你');
Assertions.assertEquals("\\u4f60", unicodeHex); assertEquals("\\u4f60", unicodeHex);
} }
@Test @Test
@ -86,20 +89,50 @@ public class HexUtilTest {
public void formatHexTest(){ public void formatHexTest(){
final String hex = "e8c670380cb220095268f40221fc748fa6ac39d6e930e63c30da68bad97f885d"; final String hex = "e8c670380cb220095268f40221fc748fa6ac39d6e930e63c30da68bad97f885d";
final String formatHex = HexUtil.format(hex); final String formatHex = HexUtil.format(hex);
Assertions.assertEquals("e8 c6 70 38 0c b2 20 09 52 68 f4 02 21 fc 74 8f a6 ac 39 d6 e9 30 e6 3c 30 da 68 ba d9 7f 88 5d", formatHex); assertEquals("e8 c6 70 38 0c b2 20 09 52 68 f4 02 21 fc 74 8f a6 ac 39 d6 e9 30 e6 3c 30 da 68 ba d9 7f 88 5d", formatHex);
} }
@Test @Test
public void formatHexTest2(){ public void formatHexTest2(){
final String hex = "e8c670380cb220095268f40221fc748fa6"; final String hex = "e8c670380cb220095268f40221fc748fa6";
final String formatHex = HexUtil.format(hex, "0x"); final String formatHex = HexUtil.format(hex, "0x");
Assertions.assertEquals("0xe8 0xc6 0x70 0x38 0x0c 0xb2 0x20 0x09 0x52 0x68 0xf4 0x02 0x21 0xfc 0x74 0x8f 0xa6", formatHex); assertEquals("0xe8 0xc6 0x70 0x38 0x0c 0xb2 0x20 0x09 0x52 0x68 0xf4 0x02 0x21 0xfc 0x74 0x8f 0xa6", formatHex);
} }
@Test @Test
public void decodeHexTest(){ public void decodeHexTest(){
final String s = HexUtil.encodeStr("6"); final String s = HexUtil.encodeStr("6");
final String s1 = HexUtil.decodeStr(s); final String s1 = HexUtil.decodeStr(s);
Assertions.assertEquals("6", s1); assertEquals("6", s1);
}
@Test
public void hexToIntTest() {
final String hex1 = "FF";
assertEquals(255, HexUtil.hexToInt(hex1));
final String hex2 = "0xFF";
assertEquals(255, HexUtil.hexToInt(hex2));
final String hex3 = "#FF";
assertEquals(255, HexUtil.hexToInt(hex3));
}
@Test
public void hexToLongTest() {
final String hex1 = "FF";
assertEquals(255L, HexUtil.hexToLong(hex1));
final String hex2 = "0xFF";
assertEquals(255L, HexUtil.hexToLong(hex2));
final String hex3 = "#FF";
assertEquals(255L, HexUtil.hexToLong(hex3));
}
@Test
public void toBigIntegerTest() {
final String hex1 = "FF";
assertEquals(new BigInteger("FF", 16), HexUtil.toBigInteger(hex1));
final String hex2 = "0xFF";
assertEquals(new BigInteger("FF", 16), HexUtil.toBigInteger(hex2));
final String hex3 = "#FF";
assertEquals(new BigInteger("FF", 16), HexUtil.toBigInteger(hex3));
} }
} }

View File

@ -0,0 +1,57 @@
package cn.hutool.v7.poi.word;
import org.apache.poi.xwpf.usermodel.XWPFRun;
import java.awt.Color;
import java.awt.Font;
/**
* 字体样式
*
* @param font 字体信息
* @param color 字体颜色
* @author looly
* @since 7.0.0
*/
public record FontStyle(Font font, Color color) {
/**
* 构造
*
* @param name 字体名称
* @param style 字体样式{@link Font#PLAIN}, {@link Font#BOLD}, {@link Font#ITALIC}
* @param size 字体大小
*/
@SuppressWarnings("MagicConstant")
public FontStyle(final String name, final int style, final int size) {
this(new Font(name, style, size), null);
}
/**
* 构造
*
* @param name 字体名称
* @param style 字体样式{@link Font#PLAIN}, {@link Font#BOLD}, {@link Font#ITALIC}
* @param size 字体大小
* @param color 字体颜色
*/
@SuppressWarnings("MagicConstant")
public FontStyle(final String name, final int style, final int size, final Color color) {
this(new Font(name, style, size), color);
}
/**
* 填充字体样式到段落
*
* @param run 段落对象
*/
public void fill(final XWPFRun run) {
run.setFontFamily(font.getFamily());
run.setFontSize(font.getSize());
run.setBold(font.isBold());
run.setItalic(font.isItalic());
if (null != color) {
run.setColor(String.format("%02X", color.getRGB()));
}
}
}

View File

@ -16,13 +16,13 @@
package cn.hutool.v7.poi.word; package cn.hutool.v7.poi.word;
import org.apache.poi.common.usermodel.PictureType; import cn.hutool.v7.core.array.ArrayUtil;
import cn.hutool.v7.core.io.file.FileUtil;
import cn.hutool.v7.core.io.IORuntimeException; import cn.hutool.v7.core.io.IORuntimeException;
import cn.hutool.v7.core.io.IoUtil; import cn.hutool.v7.core.io.IoUtil;
import cn.hutool.v7.core.io.file.FileUtil;
import cn.hutool.v7.core.lang.Assert; import cn.hutool.v7.core.lang.Assert;
import cn.hutool.v7.core.array.ArrayUtil;
import cn.hutool.v7.poi.POIException; import cn.hutool.v7.poi.POIException;
import org.apache.poi.common.usermodel.PictureType;
import org.apache.poi.openxml4j.exceptions.InvalidFormatException; import org.apache.poi.openxml4j.exceptions.InvalidFormatException;
import org.apache.poi.util.Units; import org.apache.poi.util.Units;
import org.apache.poi.xwpf.usermodel.ParagraphAlignment; import org.apache.poi.xwpf.usermodel.ParagraphAlignment;
@ -30,7 +30,6 @@ import org.apache.poi.xwpf.usermodel.XWPFDocument;
import org.apache.poi.xwpf.usermodel.XWPFParagraph; import org.apache.poi.xwpf.usermodel.XWPFParagraph;
import org.apache.poi.xwpf.usermodel.XWPFRun; import org.apache.poi.xwpf.usermodel.XWPFRun;
import java.awt.Font;
import java.io.*; import java.io.*;
/** /**
@ -51,7 +50,7 @@ public class Word07Writer implements Closeable {
*/ */
protected boolean isClosed; protected boolean isClosed;
// region ----- Constructor start // region ----- Constructor
/** /**
* 构造 * 构造
@ -111,14 +110,15 @@ public class Word07Writer implements Closeable {
return this; return this;
} }
// region ----- addText
/** /**
* 增加一个段落 * 增加一个段落
* *
* @param font 字体信息{@link Font} * @param font 字体信息{@link FontStyle}
* @param texts 段落中的文本支持多个文本作为一个段落 * @param texts 段落中的文本支持多个文本作为一个段落
* @return this * @return this
*/ */
public Word07Writer addText(final Font font, final String... texts) { public Word07Writer addText(final FontStyle font, final String... texts) {
return addText(null, font, texts); return addText(null, font, texts);
} }
@ -126,11 +126,11 @@ public class Word07Writer implements Closeable {
* 增加一个段落 * 增加一个段落
* *
* @param align 段落对齐方式{@link ParagraphAlignment} * @param align 段落对齐方式{@link ParagraphAlignment}
* @param font 字体信息{@link Font} * @param font 字体信息{@link FontStyle}
* @param texts 段落中的文本支持多个文本作为一个段落 * @param texts 段落中的文本支持多个文本作为一个段落
* @return this * @return this
*/ */
public Word07Writer addText(final ParagraphAlignment align, final Font font, final String... texts) { public Word07Writer addText(final ParagraphAlignment align, final FontStyle font, final String... texts) {
final XWPFParagraph p = this.doc.createParagraph(); final XWPFParagraph p = this.doc.createParagraph();
if (null != align) { if (null != align) {
p.setAlignment(align); p.setAlignment(align);
@ -141,15 +141,13 @@ public class Word07Writer implements Closeable {
run = p.createRun(); run = p.createRun();
run.setText(text); run.setText(text);
if (null != font) { if (null != font) {
run.setFontFamily(font.getFamily()); font.fill(run);
run.setFontSize(font.getSize());
run.setBold(font.isBold());
run.setItalic(font.isItalic());
} }
} }
} }
return this; return this;
} }
// endregion
/** /**
* 增加表格数据 * 增加表格数据
@ -164,6 +162,7 @@ public class Word07Writer implements Closeable {
return this; return this;
} }
// region ----- addPicture
/** /**
* 增加图片单独成段落 * 增加图片单独成段落
* *
@ -249,7 +248,9 @@ public class Word07Writer implements Closeable {
} }
return this; return this;
} }
// endregion
// region ----- flush
/** /**
* 将Excel Workbook刷出到预定义的文件<br> * 将Excel Workbook刷出到预定义的文件<br>
* 如果用户未自定义输出的文件将抛出{@link NullPointerException}<br> * 如果用户未自定义输出的文件将抛出{@link NullPointerException}<br>
@ -308,6 +309,7 @@ public class Word07Writer implements Closeable {
} }
return this; return this;
} }
// endregion
/** /**
* 关闭Word文档<br> * 关闭Word文档<br>

View File

@ -40,8 +40,8 @@ public class WordWriterTest {
@Disabled @Disabled
public void writeTest() { public void writeTest() {
final Word07Writer writer = new Word07Writer(); final Word07Writer writer = new Word07Writer();
writer.addText(new Font("方正小标宋简体", Font.PLAIN, 22), "我是第一部分", "我是第二部分"); writer.addText(new FontStyle("方正小标宋简体", Font.PLAIN, 22), "我是第一部分", "我是第二部分");
writer.addText(new Font("宋体", Font.PLAIN, 22), "我是正文第一部分", "我是正文第二部分"); writer.addText(new FontStyle("宋体", Font.PLAIN, 22), "我是正文第一部分", "我是正文第二部分");
writer.flush(FileUtil.file("e:/wordWrite.docx")); writer.flush(FileUtil.file("e:/wordWrite.docx"));
writer.close(); writer.close();
Console.log("OK"); Console.log("OK");
@ -60,7 +60,7 @@ public class WordWriterTest {
@Test @Test
@Disabled @Disabled
public void writeTableTest(){ public void writeTableTest() {
final Word07Writer writer = new Word07Writer(); final Word07Writer writer = new Word07Writer();
final Map<String, Object> map = new LinkedHashMap<>(); final Map<String, Object> map = new LinkedHashMap<>();
map.put("姓名", "张三"); map.put("姓名", "张三");
@ -94,9 +94,9 @@ public class WordWriterTest {
final ArrayList<Map<String, Object>> mapArrayList = ListUtil.of(data, data2); final ArrayList<Map<String, Object>> mapArrayList = ListUtil.of(data, data2);
// 添加段落标题 // 添加段落标题
writer.addText(new Font("方正小标宋简体", Font.PLAIN, 22), "我是第一部分"); writer.addText(new FontStyle("方正小标宋简体", Font.PLAIN, 22), "我是第一部分");
// 添加段落正文 // 添加段落正文
writer.addText(new Font("宋体", Font.PLAIN, 13), "我是正文第一部分"); writer.addText(new FontStyle("宋体", Font.PLAIN, 13), "我是正文第一部分");
writer.addTable(mapArrayList); writer.addTable(mapArrayList);
// 写出到文件 // 写出到文件
writer.flush(FileUtil.file("d:/test/a.docx")); writer.flush(FileUtil.file("d:/test/a.docx"));
@ -105,7 +105,7 @@ public class WordWriterTest {
} }
@Test @Test
public void overflowTest(){ public void overflowTest() {
final Word07Writer word07Writer = new Word07Writer(); final Word07Writer word07Writer = new Word07Writer();
final List<Object> list = ListUtil.of(false); final List<Object> list = ListUtil.of(false);
final List<Object> list2 = ListUtil.of(false); final List<Object> list2 = ListUtil.of(false);
@ -117,23 +117,23 @@ public class WordWriterTest {
@Test @Test
@Disabled @Disabled
public void writeBeanAsTableTest(){ public void writeBeanAsTableTest() {
final List<Vo> of = ListUtil.of( final List<Vo> of = ListUtil.of(
new Vo("测试1", new BigDecimal(12), new BigDecimal(2)), new Vo("测试1", new BigDecimal(12), new BigDecimal(2)),
new Vo("测试2", new BigDecimal(13), new BigDecimal(2)), new Vo("测试2", new BigDecimal(13), new BigDecimal(2)),
new Vo("测试3", new BigDecimal(15), new BigDecimal(3)), new Vo("测试3", new BigDecimal(15), new BigDecimal(3)),
new Vo("测试4", new BigDecimal(112), new BigDecimal(5)) new Vo("测试4", new BigDecimal(112), new BigDecimal(5))
); );
WordUtil.getWriter() WordUtil.getWriter()
.addTable(of) .addTable(of)
.flush(FileUtil.file("d:/test/beanValueTest.docx")) .flush(FileUtil.file("d:/test/beanValueTest.docx"))
.close(); .close();
} }
@Data @Data
@AllArgsConstructor @AllArgsConstructor
private static class Vo{ private static class Vo {
private String name; private String name;
private BigDecimal amount; private BigDecimal amount;
private BigDecimal onYear; private BigDecimal onYear;