From 2441c92adadf6ae53b761d83d3817b9b47802163 Mon Sep 17 00:00:00 2001
From: Looly
Date: Tue, 7 Apr 2026 10:24:34 +0800
Subject: [PATCH 01/11] =?UTF-8?q?=E4=BF=AE=E5=A4=8DAI=20SPI=20classloader?=
=?UTF-8?q?=E6=89=BE=E4=B8=8D=E5=88=B0=E5=AE=9E=E7=8E=B0=E9=97=AE=E9=A2=98?=
=?UTF-8?q?=EF=BC=88issue#4241@Github=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
CHANGELOG.md | 3 ++-
hutool-ai/src/main/java/cn/hutool/ai/AIServiceFactory.java | 6 ++----
.../src/main/java/cn/hutool/ai/core/AIConfigRegistry.java | 6 ++----
3 files changed, 6 insertions(+), 9 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 01e73fbc03..3a6812c173 100755
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,12 +1,13 @@
# 🚀Changelog
-------------------------------------------------------------------------------------------------------------
-# 5.8.45(2026-03-20)
+# 5.8.45(2026-04-07)
### 🐣新特性
* 【core 】 `AnnotationUtil`新增两级缓存架构,提升高频注解解析性能(pr#1434@Gitee)
### 🐞Bug修复
* 【db 】 修复`Page`和`PageResult`首页调用问题(issue#IH7A18@Gitee)
+* 【ai 】 修复AI SPI classloader找不到实现问题(issue#4241@Github)
-------------------------------------------------------------------------------------------------------------
# 5.8.44(2026-03-11)
diff --git a/hutool-ai/src/main/java/cn/hutool/ai/AIServiceFactory.java b/hutool-ai/src/main/java/cn/hutool/ai/AIServiceFactory.java
index 23cb874037..1f8d27a2da 100644
--- a/hutool-ai/src/main/java/cn/hutool/ai/AIServiceFactory.java
+++ b/hutool-ai/src/main/java/cn/hutool/ai/AIServiceFactory.java
@@ -22,7 +22,6 @@ import cn.hutool.ai.core.AIServiceProvider;
import cn.hutool.core.util.ServiceLoaderUtil;
import java.util.Map;
-import java.util.ServiceLoader;
import java.util.concurrent.ConcurrentHashMap;
/**
@@ -37,9 +36,8 @@ public class AIServiceFactory {
// 加载所有 AIModelProvider 实现类
static {
- final ServiceLoader loader = ServiceLoaderUtil.load(AIServiceProvider.class);
- for (final AIServiceProvider provider : loader) {
- providers.put(provider.getServiceName().toLowerCase(), provider);
+ for (final AIServiceProvider provider : ServiceLoaderUtil.load(AIServiceProvider.class, AIServiceProvider.class.getClassLoader())) {
+ providers.putIfAbsent(provider.getServiceName().toLowerCase(), provider);
}
}
diff --git a/hutool-ai/src/main/java/cn/hutool/ai/core/AIConfigRegistry.java b/hutool-ai/src/main/java/cn/hutool/ai/core/AIConfigRegistry.java
index 6166440495..5bca57b856 100644
--- a/hutool-ai/src/main/java/cn/hutool/ai/core/AIConfigRegistry.java
+++ b/hutool-ai/src/main/java/cn/hutool/ai/core/AIConfigRegistry.java
@@ -19,7 +19,6 @@ package cn.hutool.ai.core;
import cn.hutool.core.util.ServiceLoaderUtil;
import java.util.Map;
-import java.util.ServiceLoader;
import java.util.concurrent.ConcurrentHashMap;
/**
@@ -34,9 +33,8 @@ public class AIConfigRegistry {
// 加载所有 AIConfig 实现类
static {
- final ServiceLoader loader = ServiceLoaderUtil.load(AIConfig.class);
- for (final AIConfig config : loader) {
- configClasses.put(config.getModelName().toLowerCase(), config.getClass());
+ for (final AIConfig config : ServiceLoaderUtil.load(AIConfig.class, AIConfig.class.getClassLoader())) {
+ configClasses.putIfAbsent(config.getModelName().toLowerCase(), config.getClass());
}
}
From 49858088d36107851575196c1acd89a3f1c764f2 Mon Sep 17 00:00:00 2001
From: Looly
Date: Tue, 7 Apr 2026 10:49:36 +0800
Subject: [PATCH 02/11] =?UTF-8?q?=E4=BF=AE=E5=A4=8DAI=20SPI=20classloader?=
=?UTF-8?q?=E6=89=BE=E4=B8=8D=E5=88=B0=E5=AE=9E=E7=8E=B0=E9=97=AE=E9=A2=98?=
=?UTF-8?q?=EF=BC=88issue#4241@Github=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
hutool-ai/src/main/java/cn/hutool/ai/AIServiceFactory.java | 4 ++++
.../src/main/java/cn/hutool/ai/core/AIConfigRegistry.java | 4 ++++
2 files changed, 8 insertions(+)
diff --git a/hutool-ai/src/main/java/cn/hutool/ai/AIServiceFactory.java b/hutool-ai/src/main/java/cn/hutool/ai/AIServiceFactory.java
index 1f8d27a2da..928bf27204 100644
--- a/hutool-ai/src/main/java/cn/hutool/ai/AIServiceFactory.java
+++ b/hutool-ai/src/main/java/cn/hutool/ai/AIServiceFactory.java
@@ -36,6 +36,10 @@ public class AIServiceFactory {
// 加载所有 AIModelProvider 实现类
static {
+ for (final AIServiceProvider provider : ServiceLoaderUtil.load(AIServiceProvider.class)) {
+ providers.putIfAbsent(provider.getServiceName().toLowerCase(), provider);
+ }
+ // issue#4241@github,多线程和Spring环境下可能导致SPI文件找不到问题
for (final AIServiceProvider provider : ServiceLoaderUtil.load(AIServiceProvider.class, AIServiceProvider.class.getClassLoader())) {
providers.putIfAbsent(provider.getServiceName().toLowerCase(), provider);
}
diff --git a/hutool-ai/src/main/java/cn/hutool/ai/core/AIConfigRegistry.java b/hutool-ai/src/main/java/cn/hutool/ai/core/AIConfigRegistry.java
index 5bca57b856..d6070bda0b 100644
--- a/hutool-ai/src/main/java/cn/hutool/ai/core/AIConfigRegistry.java
+++ b/hutool-ai/src/main/java/cn/hutool/ai/core/AIConfigRegistry.java
@@ -33,6 +33,10 @@ public class AIConfigRegistry {
// 加载所有 AIConfig 实现类
static {
+ for (final AIConfig config : ServiceLoaderUtil.load(AIConfig.class)) {
+ configClasses.putIfAbsent(config.getModelName().toLowerCase(), config.getClass());
+ }
+ // issue#4241@github,多线程和Spring环境下可能导致SPI文件找不到问题
for (final AIConfig config : ServiceLoaderUtil.load(AIConfig.class, AIConfig.class.getClassLoader())) {
configClasses.putIfAbsent(config.getModelName().toLowerCase(), config.getClass());
}
From 9505fdab708dc417331544b0d444831132341947 Mon Sep 17 00:00:00 2001
From: Looly
Date: Sat, 9 May 2026 10:31:25 +0800
Subject: [PATCH 03/11] =?UTF-8?q?=E4=BF=AE=E5=A4=8D`ExpressionEngine`?=
=?UTF-8?q?=E4=B8=ADSpELEngine=E3=80=81MVEL=E7=99=BD=E5=90=8D=E5=8D=95?=
=?UTF-8?q?=E6=97=A0=E6=95=88=E9=97=AE=E9=A2=98?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
CHANGELOG.md | 5 +-
.../core/io/ValidateObjectInputStream.java | 9 +-
.../java/cn/hutool/core/util/JNDIUtil.java | 87 ++++++++++++++++++-
.../expression/engine/mvel/MvelEngine.java | 12 +++
.../expression/engine/rhino/RhinoEngine.java | 12 +++
.../expression/engine/spel/SpELEngine.java | 21 ++++-
6 files changed, 131 insertions(+), 15 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 3a6812c173..9eaea475ba 100755
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,13 +1,16 @@
# 🚀Changelog
-------------------------------------------------------------------------------------------------------------
-# 5.8.45(2026-04-07)
+# 5.8.45(2026-05-09)
### 🐣新特性
* 【core 】 `AnnotationUtil`新增两级缓存架构,提升高频注解解析性能(pr#1434@Gitee)
### 🐞Bug修复
* 【db 】 修复`Page`和`PageResult`首页调用问题(issue#IH7A18@Gitee)
* 【ai 】 修复AI SPI classloader找不到实现问题(issue#4241@Github)
+* 【extra 】 修复`ExpressionEngine`中SpELEngine、MVEL白名单无效问题(issue#4249@Github)
+* 【core 】 修复`JNDIUtil`远程加载漏洞(issue#4249@Github)
+* 【core 】 修复`ValidateObjectInputStream`白名单规则问题(issue#4249@Github)
-------------------------------------------------------------------------------------------------------------
# 5.8.44(2026-03-11)
diff --git a/hutool-core/src/main/java/cn/hutool/core/io/ValidateObjectInputStream.java b/hutool-core/src/main/java/cn/hutool/core/io/ValidateObjectInputStream.java
index e643895077..3d9408e155 100644
--- a/hutool-core/src/main/java/cn/hutool/core/io/ValidateObjectInputStream.java
+++ b/hutool-core/src/main/java/cn/hutool/core/io/ValidateObjectInputStream.java
@@ -85,14 +85,7 @@ public class ValidateObjectInputStream extends ObjectInputStream {
}
}
- if(CollUtil.isEmpty(this.whiteClassSet)){
- return;
- }
- if(className.startsWith("java.")){
- // java中的类默认在白名单中
- return;
- }
- if(this.whiteClassSet.contains(className)){
+ if(CollUtil.isEmpty(this.whiteClassSet) || this.whiteClassSet.contains(className)){
return;
}
diff --git a/hutool-core/src/main/java/cn/hutool/core/util/JNDIUtil.java b/hutool-core/src/main/java/cn/hutool/core/util/JNDIUtil.java
index 3484be1b93..7a0813baa3 100755
--- a/hutool-core/src/main/java/cn/hutool/core/util/JNDIUtil.java
+++ b/hutool-core/src/main/java/cn/hutool/core/util/JNDIUtil.java
@@ -8,7 +8,9 @@ import javax.naming.InitialContext;
import javax.naming.NamingException;
import javax.naming.directory.Attributes;
import javax.naming.directory.InitialDirContext;
+import java.util.Arrays;
import java.util.Hashtable;
+import java.util.List;
import java.util.Map;
/**
@@ -20,15 +22,20 @@ import java.util.Map;
* 见:https://blog.csdn.net/u010430304/article/details/54601302
*
*
- * @author loolY
+ * @author looly
* @since 5.7.7
*/
public class JNDIUtil {
/**
* 创建{@link InitialDirContext}
+ * 建议在应用启动时设置系统属性(禁用远程 codebase 加载)
+ * {@code
+ * System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "false");
+ * System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "false");
+ * }
*
- * @param environment 环境参数,{@code null}表示无参数
+ * @param environment 环境参数,如@{code java.naming.factory.initial}和{@code java.naming.provider.url},{@code null}表示无参数
* @return {@link InitialDirContext}
*/
public static InitialDirContext createInitialDirContext(Map environment) {
@@ -36,6 +43,10 @@ public class JNDIUtil {
if (MapUtil.isEmpty(environment)) {
return new InitialDirContext();
}
+
+ // issue#4249 修复JNDI注入漏洞
+ validateEnvironment(environment);
+
return new InitialDirContext(Convert.convert(Hashtable.class, environment));
} catch (NamingException e) {
throw new UtilException(e);
@@ -43,9 +54,14 @@ public class JNDIUtil {
}
/**
- * 创建{@link InitialContext}
+ * 创建{@link InitialContext}
+ * 建议在应用启动时设置系统属性(禁用远程 codebase 加载)
+ * {@code
+ * System.setProperty("com.sun.jndi.rmi.object.trustURLCodebase", "false");
+ * System.setProperty("com.sun.jndi.ldap.object.trustURLCodebase", "false");
+ * }
*
- * @param environment 环境参数,{@code null}表示无参数
+ * @param environment 环境参数,如@{code java.naming.factory.initial}和{@code java.naming.provider.url},{@code null}表示无参数
* @return {@link InitialContext}
*/
public static InitialContext createInitialContext(Map environment) {
@@ -53,6 +69,10 @@ public class JNDIUtil {
if (MapUtil.isEmpty(environment)) {
return new InitialContext();
}
+
+ // issue#4249 修复JNDI注入漏洞
+ validateEnvironment(environment);
+
return new InitialContext(Convert.convert(Hashtable.class, environment));
} catch (NamingException e) {
throw new UtilException(e);
@@ -74,4 +94,63 @@ public class JNDIUtil {
throw new UtilException(e);
}
}
+
+ private static final List SAFE_PROTOCOLS = Arrays.asList(
+ "java:",
+ "dns:"
+ );
+
+ /**
+ * 验证并过滤environment中的危险属性
+ *
+ * @param environment 原始环境参数
+ * @return 过滤后的环境参数
+ */
+ private static Map validateEnvironment(Map environment) {
+ if (MapUtil.isNotEmpty(environment)) {
+ // 检查 PROVIDER_URL
+ String providerUrl = environment.get("java.naming.provider.url");
+ if (StrUtil.isNotBlank(providerUrl) && !isSafeProtocol(providerUrl)) {
+ throw new UtilException("JNDI protocol not allowed: " + providerUrl);
+ }
+
+ // 检查 INITIAL_CONTEXT_FACTORY
+ String factory = environment.get("java.naming.factory.initial");
+ if (StrUtil.isNotBlank(factory)) {
+ // 只允许安全的工厂类
+ if (!factory.startsWith("com.sun.jndi.dns.") &&
+ !factory.startsWith("com.sun.jndi.ldap.") &&
+ !factory.startsWith("com.sun.jndi.rmi.")) {
+ throw new UtilException("JNDI factory not allowed: " + factory);
+ }
+ }
+ }
+
+ return environment;
+ }
+
+ /**
+ * 检查URL是否在协议白名单内
+ *
+ * @param url 要检查的URL
+ * @param allowedProtocols 允许的协议列表,{@code null}或空表示使用默认安全协议
+ * @return 是否安全
+ */
+ private static boolean isSafeProtocol(String url, String... allowedProtocols) {
+ if (StrUtil.isBlank(url)) {
+ return false;
+ }
+
+ List protocols = (allowedProtocols != null && allowedProtocols.length > 0)
+ ? Arrays.asList(allowedProtocols)
+ : SAFE_PROTOCOLS;
+
+ String lowerUrl = url.toLowerCase();
+ for (String protocol : protocols) {
+ if (lowerUrl.startsWith(protocol.toLowerCase())) {
+ return true;
+ }
+ }
+ return false;
+ }
}
diff --git a/hutool-extra/src/main/java/cn/hutool/extra/expression/engine/mvel/MvelEngine.java b/hutool-extra/src/main/java/cn/hutool/extra/expression/engine/mvel/MvelEngine.java
index b580418818..ae85d47666 100644
--- a/hutool-extra/src/main/java/cn/hutool/extra/expression/engine/mvel/MvelEngine.java
+++ b/hutool-extra/src/main/java/cn/hutool/extra/expression/engine/mvel/MvelEngine.java
@@ -1,6 +1,8 @@
package cn.hutool.extra.expression.engine.mvel;
+import cn.hutool.core.collection.CollUtil;
import cn.hutool.extra.expression.ExpressionEngine;
+import cn.hutool.extra.expression.ExpressionException;
import org.mvel2.MVEL;
import java.util.Collection;
@@ -27,6 +29,16 @@ public class MvelEngine implements ExpressionEngine {
@Override
public Object eval(String expression, Map context, Collection> allowClassSet) {
+
+ // issue#4249 检查context的value类型是否在白名单中,不在则抛出异常
+ if(CollUtil.isNotEmpty(allowClassSet)){
+ context.values().forEach(value -> {
+ if(!allowClassSet.contains(value.getClass())){
+ throw new ExpressionException("Value type [{}] is not in allowClassSet [{}]", value.getClass(), allowClassSet);
+ }
+ });
+ }
+
return MVEL.eval(expression, context);
}
diff --git a/hutool-extra/src/main/java/cn/hutool/extra/expression/engine/rhino/RhinoEngine.java b/hutool-extra/src/main/java/cn/hutool/extra/expression/engine/rhino/RhinoEngine.java
index 7e2d406cb6..8adfa4a296 100644
--- a/hutool-extra/src/main/java/cn/hutool/extra/expression/engine/rhino/RhinoEngine.java
+++ b/hutool-extra/src/main/java/cn/hutool/extra/expression/engine/rhino/RhinoEngine.java
@@ -1,7 +1,9 @@
package cn.hutool.extra.expression.engine.rhino;
+import cn.hutool.core.collection.CollUtil;
import cn.hutool.core.map.MapUtil;
import cn.hutool.extra.expression.ExpressionEngine;
+import cn.hutool.extra.expression.ExpressionException;
import org.mozilla.javascript.Context;
import org.mozilla.javascript.Scriptable;
import org.mozilla.javascript.ScriptableObject;
@@ -24,6 +26,16 @@ public class RhinoEngine implements ExpressionEngine {
@Override
public Object eval(String expression, Map context, Collection> allowClassSet) {
+
+ // issue#4249 检查context的value类型是否在白名单中,不在则抛出异常
+ if(CollUtil.isNotEmpty(allowClassSet)){
+ context.values().forEach(value -> {
+ if(!allowClassSet.contains(value.getClass())){
+ throw new ExpressionException("Value type [{}] is not in allowClassSet [{}]", value.getClass(), allowClassSet);
+ }
+ });
+ }
+
final Context ctx = Context.enter();
final Scriptable scope = ctx.initStandardObjects();
if (MapUtil.isNotEmpty(context)) {
diff --git a/hutool-extra/src/main/java/cn/hutool/extra/expression/engine/spel/SpELEngine.java b/hutool-extra/src/main/java/cn/hutool/extra/expression/engine/spel/SpELEngine.java
index 06c647dc61..8c7766701e 100644
--- a/hutool-extra/src/main/java/cn/hutool/extra/expression/engine/spel/SpELEngine.java
+++ b/hutool-extra/src/main/java/cn/hutool/extra/expression/engine/spel/SpELEngine.java
@@ -1,10 +1,12 @@
package cn.hutool.extra.expression.engine.spel;
+import cn.hutool.core.collection.CollUtil;
import cn.hutool.extra.expression.ExpressionEngine;
+import cn.hutool.extra.expression.ExpressionException;
import org.springframework.expression.EvaluationContext;
import org.springframework.expression.ExpressionParser;
import org.springframework.expression.spel.standard.SpelExpressionParser;
-import org.springframework.expression.spel.support.StandardEvaluationContext;
+import org.springframework.expression.spel.support.SimpleEvaluationContext;
import java.util.Collection;
import java.util.Map;
@@ -29,7 +31,22 @@ public class SpELEngine implements ExpressionEngine {
@Override
public Object eval(String expression, Map context, Collection> allowClassSet) {
- final EvaluationContext evaluationContext = new StandardEvaluationContext();
+ // final EvaluationContext evaluationContext = new StandardEvaluationContext();
+
+ // issue#4249 检查context的value类型是否在白名单中,不在则抛出异常
+ if(CollUtil.isNotEmpty(allowClassSet)){
+ context.values().forEach(value -> {
+ if(!allowClassSet.contains(value.getClass())){
+ throw new ExpressionException("Value type [{}] is not in allowClassSet [{}]", value.getClass(), allowClassSet);
+ }
+ });
+ }
+
+ EvaluationContext evaluationContext = SimpleEvaluationContext
+ .forReadOnlyDataBinding()
+ .withInstanceMethods() // 仅允许调用白名单类的实例方法
+ .build();
+
context.forEach(evaluationContext::setVariable);
return parser.parseExpression(expression).getValue(evaluationContext);
}
From 076f5096df9e264245cf383c0a2ac130ec3a8ad5 Mon Sep 17 00:00:00 2001
From: Looly
Date: Sat, 9 May 2026 10:50:50 +0800
Subject: [PATCH 04/11] update thymeleaf
---
hutool-extra/pom.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/hutool-extra/pom.xml b/hutool-extra/pom.xml
index 1b9da409db..fcacebbe27 100755
--- a/hutool-extra/pom.xml
+++ b/hutool-extra/pom.xml
@@ -24,7 +24,7 @@
1.4.2
2.3.32
5.1.3
- 3.1.2.RELEASE
+ 3.1.5.RELEASE
1.6.2
0.1.55
0.38.0
From cc6df6990bfd2597300cd0a7ca6b0152eec450f0 Mon Sep 17 00:00:00 2001
From: Looly
Date: Sat, 9 May 2026 10:52:03 +0800
Subject: [PATCH 05/11] update postgre
---
hutool-db/pom.xml | 2 +-
1 file changed, 1 insertion(+), 1 deletion(-)
diff --git a/hutool-db/pom.xml b/hutool-db/pom.xml
index 4c51456538..a626ceb9df 100755
--- a/hutool-db/pom.xml
+++ b/hutool-db/pom.xml
@@ -145,7 +145,7 @@
org.postgresql
postgresql
- 42.7.3
+ 42.7.11
test
From 6d8dd9729222712a444eb6a2b45ae50e5bba43c4 Mon Sep 17 00:00:00 2001
From: Looly
Date: Thu, 14 May 2026 16:29:23 +0800
Subject: [PATCH 06/11] =?UTF-8?q?`RegexPool.PLATE=5FNUMBER`=E6=96=B0?=
=?UTF-8?q?=E5=A2=9E=E7=B2=A4AP=E5=8F=B7=E6=AE=B5=E6=94=AF=E6=8C=81?=
=?UTF-8?q?=EF=BC=88issue#IJNDJR@Gitee=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
CHANGELOG.md | 3 ++-
.../src/main/java/cn/hutool/core/lang/RegexPool.java | 2 +-
.../src/test/java/cn/hutool/core/lang/ValidatorTest.java | 9 +++++++++
3 files changed, 12 insertions(+), 2 deletions(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index 9eaea475ba..e1ce02d0da 100755
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -1,9 +1,10 @@
# 🚀Changelog
-------------------------------------------------------------------------------------------------------------
-# 5.8.45(2026-05-09)
+# 5.8.45(2026-05-14)
### 🐣新特性
* 【core 】 `AnnotationUtil`新增两级缓存架构,提升高频注解解析性能(pr#1434@Gitee)
+* 【core 】 `RegexPool.PLATE_NUMBER`新增粤AP号段支持(issue#IJNDJR@Gitee)
### 🐞Bug修复
* 【db 】 修复`Page`和`PageResult`首页调用问题(issue#IH7A18@Gitee)
diff --git a/hutool-core/src/main/java/cn/hutool/core/lang/RegexPool.java b/hutool-core/src/main/java/cn/hutool/core/lang/RegexPool.java
index e209b8a16d..d9c8ea147e 100755
--- a/hutool-core/src/main/java/cn/hutool/core/lang/RegexPool.java
+++ b/hutool-core/src/main/java/cn/hutool/core/lang/RegexPool.java
@@ -145,7 +145,7 @@ public interface RegexPool {
*/
String PLATE_NUMBER =
//https://gitee.com/chinabugotech/hutool/issues/I1B77H?from=project-issue
- "^(([京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-Z](([0-9]{5}[ABCDEFGHJK])|([ABCDEFGHJK]([A-HJ-NP-Z0-9])[0-9]{4})))|" +
+ "^(([京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-Z](([0-9]{5}[ABCDEFGHJK])|([ABCDEFGHJKP]([A-HJ-NP-Z0-9])[0-9]{4})))|" +
//https://gitee.com/chinabugotech/hutool/issues/I1BJHE?from=project-issue
"([京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领]\\d{3}\\d{1,3}[领])|" +
"([京津沪渝冀豫云辽黑湘皖鲁新苏浙赣鄂桂甘晋蒙陕吉闽贵粤青藏川宁琼使领][A-Z][A-HJ-NP-Z0-9]{4}[A-HJ-NP-Z0-9挂学警港澳使领]))$";
diff --git a/hutool-core/src/test/java/cn/hutool/core/lang/ValidatorTest.java b/hutool-core/src/test/java/cn/hutool/core/lang/ValidatorTest.java
index c09cb77df5..125a46591b 100755
--- a/hutool-core/src/test/java/cn/hutool/core/lang/ValidatorTest.java
+++ b/hutool-core/src/test/java/cn/hutool/core/lang/ValidatorTest.java
@@ -167,6 +167,15 @@ public class ValidatorTest {
assertTrue(Validator.isPlateNumber("闽20401领"));
//issue#3979
assertTrue(Validator.isPlateNumber("沪AE22075"));
+
+ // issue#IJNDJR
+ assertTrue(Validator.isPlateNumber("粤AP00000"));
+ assertTrue(Validator.isPlateNumber("粤AP00001"));
+ assertTrue(Validator.isPlateNumber("粤AP10000"));
+ assertTrue(Validator.isPlateNumber("粤AP20000"));
+ assertTrue(Validator.isPlateNumber("粤AP30000"));
+ assertTrue(Validator.isPlateNumber("粤AP40000"));
+ assertTrue(Validator.isPlateNumber("粤AP50000"));
}
@Test
From eaecb109cfa374b7275cd31fed8883ef5336200e Mon Sep 17 00:00:00 2001
From: Looly
Date: Thu, 14 May 2026 22:30:38 +0800
Subject: [PATCH 07/11] =?UTF-8?q?=E4=BF=AE=E5=A4=8D`VersionUtil`=E6=AF=94?=
=?UTF-8?q?=E5=AF=B9null=E6=97=B6=E7=BB=93=E6=9E=9C=E5=BC=82=E5=B8=B8?=
=?UTF-8?q?=E9=97=AE=E9=A2=98=EF=BC=88issue#IJNFQZ@Gitee=EF=BC=89?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
---
CHANGELOG.md | 1 +
.../src/main/java/cn/hutool/core/util/VersionUtil.java | 5 ++++-
.../src/test/java/cn/hutool/core/util/VersionUtilTest.java | 7 +++++++
3 files changed, 12 insertions(+), 1 deletion(-)
diff --git a/CHANGELOG.md b/CHANGELOG.md
index e1ce02d0da..b9073bd72c 100755
--- a/CHANGELOG.md
+++ b/CHANGELOG.md
@@ -12,6 +12,7 @@
* 【extra 】 修复`ExpressionEngine`中SpELEngine、MVEL白名单无效问题(issue#4249@Github)
* 【core 】 修复`JNDIUtil`远程加载漏洞(issue#4249@Github)
* 【core 】 修复`ValidateObjectInputStream`白名单规则问题(issue#4249@Github)
+* 【core 】 修复`VersionUtil`比对null时结果异常问题(issue#IJNFQZ@Gitee)
-------------------------------------------------------------------------------------------------------------
# 5.8.44(2026-03-11)
diff --git a/hutool-core/src/main/java/cn/hutool/core/util/VersionUtil.java b/hutool-core/src/main/java/cn/hutool/core/util/VersionUtil.java
index aa666c97d3..706483f0da 100644
--- a/hutool-core/src/main/java/cn/hutool/core/util/VersionUtil.java
+++ b/hutool-core/src/main/java/cn/hutool/core/util/VersionUtil.java
@@ -142,7 +142,7 @@ public class VersionUtil {
throw new UtilException("非法的版本分隔符:" + versionsDelimiter);
}
- if (StrUtil.isBlank(versionEl) || StrUtil.isBlank(currentVersion)) {
+ if (StrUtil.isBlank(versionEl)) {
return false;
}
String trimmedVersion = StrUtil.trim(currentVersion);
@@ -158,6 +158,9 @@ public class VersionUtil {
if (matcher.find()) {
String op = matcher.group();
String ver = StrUtil.removePrefix(el, op);
+ if("null".equalsIgnoreCase( ver)){
+ ver = null;
+ }
switch (op) {
case ">=":
case "≥":
diff --git a/hutool-core/src/test/java/cn/hutool/core/util/VersionUtilTest.java b/hutool-core/src/test/java/cn/hutool/core/util/VersionUtilTest.java
index e89abd046b..f5a7a640a8 100644
--- a/hutool-core/src/test/java/cn/hutool/core/util/VersionUtilTest.java
+++ b/hutool-core/src/test/java/cn/hutool/core/util/VersionUtilTest.java
@@ -1,6 +1,7 @@
package cn.hutool.core.util;
import cn.hutool.core.collection.ListUtil;
+import cn.hutool.core.comparator.VersionComparator;
import cn.hutool.core.exceptions.UtilException;
import org.junit.jupiter.api.Test;
@@ -104,4 +105,10 @@ class VersionUtilTest {
assertTrue(VersionUtil.matchEl("999.999.999", "-"));
}
+ @Test
+ void issueIJNFQZTest(){
+ assertEquals(1, VersionComparator.INSTANCE.compare("1.0", null));
+ assertEquals(1, StrUtil.compareVersion("1.0", null));
+ assertTrue(VersionUtil.isGreaterThan("1.0", null));
+ }
}
From 64fc614618759b62bb0db27e7c9fa585a7afe2a8 Mon Sep 17 00:00:00 2001
From: Faerytale <12410707@mail.sustech.edu.cn>
Date: Tue, 19 May 2026 02:15:39 +0800
Subject: [PATCH 08/11] =?UTF-8?q?fix:=20BeanConverter=E5=92=8CMapConverter?=
=?UTF-8?q?=E5=AF=B9=E6=BA=90Bean=E4=BD=BF=E7=94=A8isReadableBean=E6=9B=BF?=
=?UTF-8?q?=E4=BB=A3isBean?=
MIME-Version: 1.0
Content-Type: text/plain; charset=UTF-8
Content-Transfer-Encoding: 8bit
修了 #4245 的问题。当 List 里的元素类用了 @Setter(AccessLevel.PROTECTED)
只有 protected setter 的时候,BeanUtil.copyProperties 会抛
ConvertException: Unsupported source type,但实际上这个类有 public getter,
作为源对象完全可以被读取。
原因是 BeanConverter 和 MapConverter 在处理源对象时用 isBean() 做检查,
但 isBean() 看的是有没有 public setter,这是判断"能不能写"的标准,
而源对象需要的是"能不能读"。BeanUtil 里其实已经有 isReadableBean() 方法
(检查 public getter),正好适合这个场景,换过来就行。两个转换器各改了一行,
另外补了两个单测覆盖这种情况。
---
.../core/convert/impl/BeanConverter.java | 2 +-
.../core/convert/impl/MapConverter.java | 2 +-
.../cn/hutool/core/bean/BeanUtilTest.java | 81 +++++++++++++++++++
3 files changed, 83 insertions(+), 2 deletions(-)
diff --git a/hutool-core/src/main/java/cn/hutool/core/convert/impl/BeanConverter.java b/hutool-core/src/main/java/cn/hutool/core/convert/impl/BeanConverter.java
index 42abb70f6c..7cfd124ef2 100644
--- a/hutool-core/src/main/java/cn/hutool/core/convert/impl/BeanConverter.java
+++ b/hutool-core/src/main/java/cn/hutool/core/convert/impl/BeanConverter.java
@@ -79,7 +79,7 @@ public class BeanConverter extends AbstractConverter {
if(value instanceof Map ||
value instanceof ValueProvider ||
- BeanUtil.isBean(value.getClass())) {
+ BeanUtil.isReadableBean(value.getClass())) {
if(value instanceof Map && this.beanClass.isInterface()) {
// 将Map动态代理为Bean
return MapProxy.create((Map, ?>)value).toProxyBean(this.beanClass);
diff --git a/hutool-core/src/main/java/cn/hutool/core/convert/impl/MapConverter.java b/hutool-core/src/main/java/cn/hutool/core/convert/impl/MapConverter.java
index f01a46c049..6f0f3375c8 100644
--- a/hutool-core/src/main/java/cn/hutool/core/convert/impl/MapConverter.java
+++ b/hutool-core/src/main/java/cn/hutool/core/convert/impl/MapConverter.java
@@ -73,7 +73,7 @@ public class MapConverter extends AbstractConverter