Merge pull request #3876 from winlans/v5-dev

JsonUtil.toBean泛型数组类型丢失修复以及新增VersionUtil版本比较工具
This commit is contained in:
Golden Looly 2025-03-03 11:22:09 +08:00 committed by GitHub
commit 5b34544b8f
No known key found for this signature in database
GPG Key ID: B5690EEEBB952194
6 changed files with 351 additions and 6 deletions

View File

@ -2,8 +2,10 @@ package cn.hutool.core.lang.reflect;
import cn.hutool.core.convert.Convert;
import cn.hutool.core.map.WeakConcurrentMap;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.TypeUtil;
import java.lang.reflect.GenericArrayType;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
@ -57,6 +59,22 @@ public class ActualTypeMapperPool {
return result;
}
public static Type getActualType(Type type, GenericArrayType genericArrayType) {
final Map<Type, Type> typeTypeMap = get(type);
Type actualType = typeTypeMap.get(genericArrayType);
if (actualType == null) {
Type componentType = typeTypeMap.get(genericArrayType.getGenericComponentType());
if (!(componentType instanceof Class<?>)) {
return null;
}
actualType = ArrayUtil.getArrayType((Class<?>) componentType);
typeTypeMap.put(genericArrayType, actualType);
}
return actualType;
}
/**
* 获取指定泛型变量对应的真实类型<br>
* 由于子类中泛型参数实现和父类接口中泛型定义位置是一一对应的因此可以通过对应关系找到泛型实现类型<br>

View File

@ -3,12 +3,7 @@ package cn.hutool.core.util;
import cn.hutool.core.lang.ParameterizedTypeImpl;
import cn.hutool.core.lang.reflect.ActualTypeMapperPool;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.lang.reflect.ParameterizedType;
import java.lang.reflect.Type;
import java.lang.reflect.TypeVariable;
import java.lang.reflect.WildcardType;
import java.lang.reflect.*;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;
@ -412,6 +407,9 @@ public class TypeUtil {
if (typeVariable instanceof TypeVariable) {
return ActualTypeMapperPool.getActualType(type, (TypeVariable<?>) typeVariable);
}
if (typeVariable instanceof GenericArrayType) {
return ActualTypeMapperPool.getActualType(type, (GenericArrayType) typeVariable);
}
// 没有需要替换的泛型变量原样输出
return typeVariable;

View File

@ -0,0 +1,196 @@
package cn.hutool.core.util;
import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.exceptions.UtilException;
import java.util.Collection;
import java.util.List;
import java.util.Objects;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* 版本对比工具
* {@link cn.hutool.core.comparator.VersionComparator} 的封装
* 最主要功能包括
*
*
* <pre>
* 1. 版本表达式匹配
* 2. 单个版本匹配
* </pre>
*
* @author winlans
* @see cn.hutool.core.comparator.VersionComparator
*/
public class VersionUtil {
private static final Pattern COMPARE_REG = Pattern.compile("^[<>≥≤]=?");
// 默认多版本分隔符
private static final String defaultVersionsDelimiter = ";";
/**
* 是否匹配任意一个版本
*
* @param currentVersion 当前版本
* @param compareVersions 待匹配的版本列表
* @return true 包含待匹配的版本
*/
public static boolean anyMatch(String currentVersion, Collection<String> compareVersions) {
return matchEl(currentVersion, CollUtil.join(compareVersions, defaultVersionsDelimiter));
}
public static boolean anyMatch(String currentVersion, String... compareVersions) {
return matchEl(currentVersion, ArrayUtil.join(compareVersions, defaultVersionsDelimiter));
}
/**
* 当前版本大于待比较版本
*
* @param currentVersion 当前本本
* @param compareVersion 待比较版本
* @return true 当前版本大于待比较版本
*/
public static boolean isGreaterThan(String currentVersion, String compareVersion) {
return matchEl(currentVersion, ">" + compareVersion);
}
/**
* 当前版本大于等于待比较版本
*
* @param currentVersion 当前本本
* @param compareVersion 待比较版本
* @return true 当前版本大于等于待比较版本
*/
public static boolean isGreaterThanOrEqual(String currentVersion, String compareVersion) {
return matchEl(currentVersion, ">=" + compareVersion);
}
/**
* 当前版本小于待比较版本
*
* @param currentVersion 当前本本
* @param compareVersion 待比较版本
* @return true 当前版本小于待比较版本
*/
public static boolean isLessThan(String currentVersion, String compareVersion) {
return matchEl(currentVersion, "<" + compareVersion);
}
/**
* 当前版本小于等于待比较版本
*
* @param currentVersion 当前本本
* @param compareVersion 待比较版本
* @return true 当前版本小于等于待比较版本
*/
public static boolean isLessThanOrEqual(String currentVersion, String compareVersion) {
return matchEl(currentVersion, "<=" + compareVersion);
}
/**
* 当前版本是否满足版本表达式
* <pre>
* matchEl("1.0.2", ">=1.0.2") == true
* matchEl("1.0.2", "<1.0.1;1.0.2") == true
* matchEl("1.0.2", "<1.0.2") == false
* matchEl("1.0.2", "1.0.0-1.1.1") == true
* matchEl("1.0.2", "1.0.0-1.1.1") == true
* </pre>
*
* @param currentVersion 当前本本
* @param versionEl 版本表达式
* @return true 当前版本小于等于待比较版本
*/
public static boolean matchEl(String currentVersion, String versionEl) {
return matchEl(currentVersion, versionEl, defaultVersionsDelimiter);
}
/**
* 当前版本是否满足版本表达式
* <pre>
* matchEl("1.0.2", ">=1.0.2", ";") == true
* matchEl("1.0.2", "<1.0.1,1.0.2", ",") == true
* matchEl("1.0.2", "<1.0.2", ";") == false
* matchEl("1.0.2", "1.0.0-1.1.1", ",") == true
* matchEl("1.0.2", "1.0.1,1.0.2-1.1.1", ",") == true
* </pre>
*
* @param currentVersion 当前本本
* @param versionEl 版本表达式可以匹配多个条件使用指定的分隔符默认;分隔,
* {@code '-'}表示范围包含左右版本,如果 {@code '-'}的左边没有表示小于等于某个版本号 右边表示大于等于某个版本号
* 支持比较符号{@code '>'},{@code '<'}, {@code '>='},{@code '<='}{@code '≤'}{@code '≥'}
*
* <ul>
* <li>{@code 1.0.1-1.2.4, 1.9.8} 表示版本号 大于等于{@code 1.0.1}且小于等于{@code 1.2.4} 版本{@code 1.9.8}</li>
* <li>{@code >=2.0.0, 1.9.8} 表示版本号 大于等于{@code 2.0.0} 版本{@code 1.9.8}</li>
* </ul>
* @param versionsDelimiter 多表达式分隔符
* @return true 当前版本小于等于待比较版本
*/
public static boolean matchEl(String currentVersion, String versionEl, String versionsDelimiter) {
if (StrUtil.isBlank(versionsDelimiter)
|| StrUtil.equals("-", versionsDelimiter)
|| ReUtil.isMatch(COMPARE_REG, versionsDelimiter)) {
throw new UtilException("非法的版本分隔符:" + versionsDelimiter);
}
if (StrUtil.isBlank(versionEl) || StrUtil.isBlank(currentVersion)) {
return false;
}
String trimmedVersion = StrUtil.trim(currentVersion);
List<String> els = StrUtil.split(versionEl, versionsDelimiter, true, true);
if (CollUtil.isEmpty(els)) {
return false;
}
for (String el : els) {
el = el.trim();
Matcher matcher = COMPARE_REG.matcher(el);
if (matcher.find()) {
String op = matcher.group();
String ver = StrUtil.removePrefix(el, op);
switch (op) {
case ">=":
case "":
if (StrUtil.compareVersion(trimmedVersion, ver) >= 0) {
return true;
}
break;
case "<=":
case "":
if (StrUtil.compareVersion(trimmedVersion, ver) <= 0) {
return true;
}
break;
case "<":
if (StrUtil.compareVersion(trimmedVersion, ver) < 0) {
return true;
}
break;
case ">":
if (StrUtil.compareVersion(trimmedVersion, ver) > 0) {
return true;
}
break;
default:
return false;
}
} else if (StrUtil.contains(el, "-")) {
String[] pair = el.split("-");
String left = StrUtil.blankToDefault(StrUtil.trim(pair[0]), "");
String right = StrUtil.blankToDefault(StrUtil.trim(pair[1]), "");
boolean leftMatch = StrUtil.isBlank(left) || StrUtil.compareVersion(left, trimmedVersion) <= 0;
boolean rightMatch = StrUtil.isBlank(right) || StrUtil.compareVersion(right, trimmedVersion) >= 0;
if (leftMatch && rightMatch) {
return true;
}
} else if (Objects.equals(trimmedVersion, el)) {
return true;
}
}
return false;
}
}

View File

@ -4,8 +4,12 @@ import java.lang.reflect.Method;
import java.lang.reflect.Type;
import java.util.ArrayList;
import java.util.List;
import cn.hutool.core.lang.TypeReference;
import lombok.Data;
import static org.junit.jupiter.api.Assertions.*;
import org.junit.jupiter.api.Test;
public class TypeUtilTest {
@ -97,4 +101,29 @@ public class TypeUtilTest {
private T level;
}
/**
* fix github:issue#3873
*/
@Test
public void getActualTypeForGenericArrayTest() {
TypeReference<GenericArray<GenericArrayEle>> typeReference = new TypeReference<GenericArray<GenericArrayEle>>() {
};
Type levelType = TypeUtil.getFieldType(GenericArray.class, "level");
Type actualType = TypeUtil.getActualType(typeReference.getType(), levelType);
assertEquals(ArrayUtil.getArrayType(GenericArrayEle.class), actualType);
}
@Data
public static class GenericArray<T> {
private T[] level;
}
@Data
public static class GenericArrayEle {
private Long uid;
}
}

View File

@ -0,0 +1,79 @@
package cn.hutool.core.util;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.exceptions.UtilException;
import org.junit.jupiter.api.Test;
import java.util.List;
import static org.junit.jupiter.api.Assertions.*;
class VersionUtilTest {
@Test
void isGreaterThan() {
String currentVersion = " 1.0.2";
assertTrue(VersionUtil.isGreaterThan(currentVersion, "1.0.1"));
assertTrue(VersionUtil.isGreaterThan(currentVersion, "1"));
assertFalse(VersionUtil.isGreaterThan(currentVersion, "1.1"));
}
@Test
void isGreaterThanOrEqual() {
String currentVersion = "1.0.2 ";
assertTrue(VersionUtil.isGreaterThanOrEqual(currentVersion, "1.0.1"));
assertTrue(VersionUtil.isGreaterThanOrEqual(currentVersion, "1.0.2"));
assertFalse(VersionUtil.isGreaterThanOrEqual(currentVersion, "1.1"));
}
@Test
void isLessThan() {
String currentVersion = "1.0.2";
assertTrue(VersionUtil.isLessThan(currentVersion, "1.0.3"));
assertFalse(VersionUtil.isLessThan(currentVersion, "1"));
assertTrue(VersionUtil.isLessThan(currentVersion, "1.1"));
assertFalse(VersionUtil.isLessThan(currentVersion, "1.0.2"));
}
@Test
void isLessThanOrEqual() {
String currentVersion = "1.0.2";
assertTrue(VersionUtil.isLessThanOrEqual(currentVersion, "1.0.2"));
assertFalse(VersionUtil.isLessThanOrEqual(currentVersion, "1.0.1"));
assertFalse(VersionUtil.isLessThanOrEqual(currentVersion, "1.1"));
}
@Test
void matchEl() {
String currentVersion = "1.0.2";
assertTrue(VersionUtil.matchEl(currentVersion, "1.0.1;1.0.2"));
assertFalse(VersionUtil.matchEl(currentVersion, "1.0.1;1.0.3"));
assertTrue(VersionUtil.matchEl(currentVersion, "1.0.9;1.0.1-1.0.2"));
assertTrue(VersionUtil.matchEl(currentVersion, "1.0.9;1.0.1-1.0.3"));
assertTrue(VersionUtil.matchEl(currentVersion, "1.0.9,1.0.1-1.0.3", ","));
}
@Test
void matchEl_Exception_whenVersionDelimiterIllegal() {
List<String> illegalDelimiters = ListUtil.of("-", ">", ">=", "<", "<=", "", "", null, "", " ");
for (String illegalDelimiter : illegalDelimiters) {
assertThrows(UtilException.class, () -> {
String currentVersion = "1.0.2";
VersionUtil.matchEl(currentVersion, "1.0.1;1.0.2", illegalDelimiter);
});
}
}
@Test
void anyMatch() {
String currentVersion = "1.0.2";
assertTrue(VersionUtil.anyMatch(currentVersion, ListUtil.of("1.0.1", "1.0.3", "1.0.2")));
assertTrue(VersionUtil.anyMatch(currentVersion, "1.0.1", "1.0.2"));
}
@Test
void testMatchEl() {
}
}

View File

@ -4,7 +4,9 @@ import cn.hutool.core.collection.CollectionUtil;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.date.DateUtil;
import cn.hutool.core.lang.Console;
import cn.hutool.core.lang.TypeReference;
import cn.hutool.core.map.MapUtil;
import cn.hutool.core.util.ArrayUtil;
import cn.hutool.core.util.NumberUtil;
import cn.hutool.json.test.bean.Price;
import cn.hutool.json.test.bean.UserA;
@ -290,4 +292,27 @@ public class JSONUtilTest {
final String jsonStr = JSONUtil.toJsonStr(userId);
assertEquals("{}", jsonStr);
}
/**
* 类型引用数组泛型丢失
*/
@Test
public void issue3873Test() {
String json = "{\"results\":[{\"uid\":\"1\"}],\"offset\":0,\"limit\":20,\"total\":0}";
Results<Index> deserialize = JSONUtil.toBean(json, (new TypeReference<Results<Index>>() {
}), false);
assertEquals(Results.class, deserialize.getClass());
assertEquals(ArrayUtil.getArrayType(Index.class), deserialize.results.getClass());
}
@Data
public static class Results<T> {
public T[] results;
}
@Data
public static class Index {
public String uid;
}
}