From bd97b5f58970eecd88db3d5b7a682f8b58e5b3de Mon Sep 17 00:00:00 2001 From: slievrly Date: Sun, 7 Jun 2026 22:54:42 +0800 Subject: [PATCH 1/4] refactor: merge threadpool and json-common modules into layered structure MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit Eliminate two top-level modules (threadpool, json-common) by distributing their code into existing modules based on dependency constraints, and create dedicated jdk17/jdk21 modules for version-specific extensions. threadpool refactoring: - Move SPI interface + platform impl (5 files) → common module - Move Factory + RuntimeEnvironment (2 files) → core module - Rename threadpool-loom → jdk21 (artifactId: seata-jdk21) - discovery-core/consul/etcd3/redis: use PlatformThreadPoolProvider directly to avoid core→discovery-core→core circular dependency json-common refactoring: - Move JsonSerializer SPI + 4 impl classes + AllowlistManager → common - Move JsonUtil (depends on ConfigurationFactory) → core module - Rename json-common-jackson3 → jdk17 (artifactId: seata-jdk17) - Remove json-common-core dependency from 5 downstream modules (available transitively through core) Co-Authored-By: Claude Opus 4.6 --- bom/pom.xml | 7 +- common/pom.xml | 15 + .../common/json/JsonAllowlistManager.java | 301 +++++++++++++++ .../seata/common/json/JsonSerializer.java | 91 +++++ .../common/json/JsonSerializerFactory.java | 60 +++ .../json/impl/Fastjson2JsonSerializer.java | 177 +++++++++ .../json/impl/FastjsonJsonSerializer.java | 177 +++++++++ .../common/json/impl/GsonJsonSerializer.java | 116 ++++++ .../json/impl/JacksonJsonSerializer.java | 211 +++++++++++ .../thread/PlatformThreadPoolExecutor.java | 40 ++ .../thread/PlatformThreadPoolProvider.java | 59 +++ .../common/thread/ThreadPoolProvider.java | 42 +++ .../thread/ThreadPoolProviderOrders.java | 27 ++ .../seata/common/thread/ThreadPoolType.java | 61 +++ ...rg.apache.seata.common.json.JsonSerializer | 20 + ...che.seata.common.thread.ThreadPoolProvider | 17 + compatible/pom.xml | 5 - core/pom.xml | 5 - .../apache/seata/common/json/JsonUtil.java | 92 +++++ .../thread/ThreadPoolExecutorFactory.java | 195 ++++++++++ .../thread/ThreadPoolRuntimeEnvironment.java | 108 ++++++ .../thread/ThreadPoolExecutorFactoryTest.java | 181 +++++++++ .../consul/ConsulRegistryServiceImpl.java | 19 +- discovery/seata-discovery-core/pom.xml | 6 - .../registry/RegistryHeartBeats.java | 7 +- .../etcd3/EtcdRegistryServiceImpl.java | 19 +- .../redis/RedisRegistryServiceImpl.java | 15 +- integration-tx-api/pom.xml | 5 - jdk17/pom.xml | 53 +++ .../json/impl/Jackson3JsonSerializer.java | 185 +++++++++ ...rg.apache.seata.common.json.JsonSerializer | 17 + .../common/json/Jackson3AllowlistTest.java | 243 ++++++++++++ .../json/Jackson3JsonSerializerTest.java | 350 ++++++++++++++++++ jdk21/pom.xml | 52 +++ .../VirtualScheduledThreadPoolExecutor.java | 31 ++ .../thread/VirtualThreadFactoryHelper.java | 39 ++ .../thread/VirtualThreadPoolExecutor.java | 47 +++ .../thread/VirtualThreadPoolProvider.java | 53 +++ ...che.seata.common.thread.ThreadPoolProvider | 17 + .../thread/VirtualThreadPoolProviderTest.java | 99 +++++ pom.xml | 14 +- rm-datasource/pom.xml | 5 - saga/pom.xml | 6 - server/pom.xml | 2 +- .../seata-spring-autoconfigure-client/pom.xml | 5 - 45 files changed, 3227 insertions(+), 69 deletions(-) create mode 100644 common/src/main/java/org/apache/seata/common/json/JsonAllowlistManager.java create mode 100644 common/src/main/java/org/apache/seata/common/json/JsonSerializer.java create mode 100644 common/src/main/java/org/apache/seata/common/json/JsonSerializerFactory.java create mode 100644 common/src/main/java/org/apache/seata/common/json/impl/Fastjson2JsonSerializer.java create mode 100644 common/src/main/java/org/apache/seata/common/json/impl/FastjsonJsonSerializer.java create mode 100644 common/src/main/java/org/apache/seata/common/json/impl/GsonJsonSerializer.java create mode 100644 common/src/main/java/org/apache/seata/common/json/impl/JacksonJsonSerializer.java create mode 100644 common/src/main/java/org/apache/seata/common/thread/PlatformThreadPoolExecutor.java create mode 100644 common/src/main/java/org/apache/seata/common/thread/PlatformThreadPoolProvider.java create mode 100644 common/src/main/java/org/apache/seata/common/thread/ThreadPoolProvider.java create mode 100644 common/src/main/java/org/apache/seata/common/thread/ThreadPoolProviderOrders.java create mode 100644 common/src/main/java/org/apache/seata/common/thread/ThreadPoolType.java create mode 100644 common/src/main/resources/META-INF/services/org.apache.seata.common.json.JsonSerializer create mode 100644 common/src/main/resources/META-INF/services/org.apache.seata.common.thread.ThreadPoolProvider create mode 100644 core/src/main/java/org/apache/seata/common/json/JsonUtil.java create mode 100644 core/src/main/java/org/apache/seata/common/thread/ThreadPoolExecutorFactory.java create mode 100644 core/src/main/java/org/apache/seata/common/thread/ThreadPoolRuntimeEnvironment.java create mode 100644 core/src/test/java/org/apache/seata/common/thread/ThreadPoolExecutorFactoryTest.java create mode 100644 jdk17/pom.xml create mode 100644 jdk17/src/main/java/org/apache/seata/common/json/impl/Jackson3JsonSerializer.java create mode 100644 jdk17/src/main/resources/META-INF/services/org.apache.seata.common.json.JsonSerializer create mode 100644 jdk17/src/test/java/org/apache/seata/common/json/Jackson3AllowlistTest.java create mode 100644 jdk17/src/test/java/org/apache/seata/common/json/Jackson3JsonSerializerTest.java create mode 100644 jdk21/pom.xml create mode 100644 jdk21/src/main/java/org/apache/seata/common/thread/VirtualScheduledThreadPoolExecutor.java create mode 100644 jdk21/src/main/java/org/apache/seata/common/thread/VirtualThreadFactoryHelper.java create mode 100644 jdk21/src/main/java/org/apache/seata/common/thread/VirtualThreadPoolExecutor.java create mode 100644 jdk21/src/main/java/org/apache/seata/common/thread/VirtualThreadPoolProvider.java create mode 100644 jdk21/src/main/resources/META-INF/services/org.apache.seata.common.thread.ThreadPoolProvider create mode 100644 jdk21/src/test/java/org/apache/seata/common/thread/VirtualThreadPoolProviderTest.java diff --git a/bom/pom.xml b/bom/pom.xml index a75784c55cb..2f13833be79 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -48,12 +48,7 @@ org.apache.seata - seata-threadpool - ${project.version} - - - org.apache.seata - seata-threadpool-loom + seata-jdk21 ${project.version} diff --git a/common/pom.xml b/common/pom.xml index 5b157d750a4..947f30a7628 100644 --- a/common/pom.xml +++ b/common/pom.xml @@ -52,5 +52,20 @@ okhttp provided + + com.alibaba + fastjson + provided + + + com.alibaba.fastjson2 + fastjson2 + provided + + + com.google.code.gson + gson + provided + diff --git a/common/src/main/java/org/apache/seata/common/json/JsonAllowlistManager.java b/common/src/main/java/org/apache/seata/common/json/JsonAllowlistManager.java new file mode 100644 index 00000000000..f40264d88b2 --- /dev/null +++ b/common/src/main/java/org/apache/seata/common/json/JsonAllowlistManager.java @@ -0,0 +1,301 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.seata.common.json; + +import java.util.Map; +import java.util.Set; +import java.util.concurrent.ConcurrentHashMap; + +/** + * JSON deserialization allowlist manager. + */ +public class JsonAllowlistManager { + + private static final JsonAllowlistManager INSTANCE = new JsonAllowlistManager(); + + /** + * Built-in exact match allowlist + */ + private final Set builtinClasses = ConcurrentHashMap.newKeySet(); + + /** + * Built-in prefix match allowlist + */ + private final Set builtinPrefixes = ConcurrentHashMap.newKeySet(); + + /** + * User-defined exact match allowlist + */ + private final Set userClasses = ConcurrentHashMap.newKeySet(); + + /** + * User-defined prefix match allowlist + */ + private final Set userPrefixes = ConcurrentHashMap.newKeySet(); + + /** + * Check result cache (className -> allowed) + */ + private final Map cache = new ConcurrentHashMap<>(); + + private JsonAllowlistManager() { + initBuiltinAllowlist(); + } + + public static JsonAllowlistManager getInstance() { + return INSTANCE; + } + + /** + * Load user allowlist from configuration string + */ + public void loadUserAllowlist(String config) { + userClasses.clear(); + userPrefixes.clear(); + cache.clear(); + if (config == null || config.isEmpty()) { + return; + } + for (String item : config.split(",")) { + String trimmed = item.trim(); + if (trimmed.isEmpty()) { + continue; + } + if (trimmed.endsWith(".")) { + userPrefixes.add(trimmed); + } else { + userClasses.add(trimmed); + } + } + } + + /** + * Add a class to user allowlist programmatically + */ + public void addUserClass(String className) { + if (className != null && !className.isEmpty()) { + userClasses.add(className); + cache.remove(className); + } + } + + /** + * Add a prefix to user allowlist programmatically + */ + public void addUserPrefix(String prefix) { + if (prefix != null && !prefix.isEmpty()) { + userPrefixes.add(prefix); + cache.clear(); + } + } + + /** + * Check if a class is allowed for deserialization + */ + public boolean isAllowed(String className) { + if (className == null) { + return false; + } + return cache.computeIfAbsent(className, this::doCheck); + } + + /** + * Check if a class is allowed, throw SecurityException if not + */ + public void checkClass(String className) { + if (!isAllowed(className)) { + throw new SecurityException("Class not in JSON deserialization allowlist: " + className + + ". Please add it to seata.json.allowlist configuration."); + } + } + + /** + * Clear user allowlist and cache. + */ + public void clearUserAllowlist() { + userClasses.clear(); + userPrefixes.clear(); + cache.clear(); + } + + private boolean doCheck(String className) { + if (isExactOrPrefixAllowed(className)) { + return true; + } + if (isPrimitiveArrayDescriptor(className)) { + return true; + } + String componentClassName = extractArrayComponentClassName(className); + return componentClassName != null && isExactOrPrefixAllowed(componentClassName); + } + + /** + * Check if className is a multi-dimensional primitive array descriptor (e.g. "[[I", "[[Z"). + * Single-dimensional primitive arrays (e.g. "[I") are already in the builtin allowlist. + */ + private boolean isPrimitiveArrayDescriptor(String className) { + if (className == null || !className.startsWith("[[")) { + return false; + } + String stripped = className; + while (stripped.startsWith("[")) { + stripped = stripped.substring(1); + } + return stripped.length() == 1 && PRIMITIVE_DESCRIPTORS.indexOf(stripped.charAt(0)) >= 0; + } + + private boolean isExactOrPrefixAllowed(String className) { + if (builtinClasses.contains(className)) { + return true; + } + for (String prefix : builtinPrefixes) { + if (className.startsWith(prefix)) { + return true; + } + } + if (userClasses.contains(className)) { + return true; + } + for (String prefix : userPrefixes) { + if (className.startsWith(prefix)) { + return true; + } + } + return false; + } + + private static final String PRIMITIVE_DESCRIPTORS = "BCSIJFDZ"; + + private String extractArrayComponentClassName(String className) { + if (className == null || className.isEmpty()) { + return null; + } + + // Canonical name format: e.g. "int[][]", "String[]" + String normalized = className; + while (normalized.endsWith("[]")) { + normalized = normalized.substring(0, normalized.length() - 2); + } + if (!normalized.equals(className)) { + return normalized; + } + + // JVM descriptor format: e.g. "[[I", "[Ljava.lang.String;" + if (!normalized.startsWith("[")) { + return null; + } + while (normalized.startsWith("[")) { + normalized = normalized.substring(1); + } + // Primitive descriptor (e.g. "I", "Z") — handled by isPrimitiveArrayDescriptor + if (normalized.length() == 1) { + return null; + } + // Object descriptor: Lclassname; + if (normalized.startsWith("L") && normalized.endsWith(";")) { + return normalized.substring(1, normalized.length() - 1); + } + return null; + } + + private void initBuiltinAllowlist() { + + builtinClasses.add("java.lang.Boolean"); + builtinClasses.add("java.lang.Byte"); + builtinClasses.add("java.lang.Character"); + builtinClasses.add("java.lang.Short"); + builtinClasses.add("java.lang.Integer"); + builtinClasses.add("java.lang.Long"); + builtinClasses.add("java.lang.Float"); + builtinClasses.add("java.lang.Double"); + builtinClasses.add("java.lang.String"); + builtinClasses.add("java.lang.Number"); + builtinClasses.add("java.math.BigDecimal"); + builtinClasses.add("java.math.BigInteger"); + + // byte[] + builtinClasses.add("[B"); + // char[] + builtinClasses.add("[C"); + // short[] + builtinClasses.add("[S"); + // int[] + builtinClasses.add("[I"); + // long[] + builtinClasses.add("[J"); + // float[] + builtinClasses.add("[F"); + // double[] + builtinClasses.add("[D"); + // boolean[] + builtinClasses.add("[Z"); + builtinClasses.add("[Ljava.lang.String;"); + builtinClasses.add("[Ljava.lang.Object;"); + + builtinClasses.add("java.util.Date"); + builtinClasses.add("java.util.Calendar"); + builtinClasses.add("java.util.GregorianCalendar"); + builtinClasses.add("java.sql.Date"); + builtinClasses.add("java.sql.Time"); + builtinClasses.add("java.sql.Timestamp"); + builtinClasses.add("java.time.LocalDateTime"); + builtinClasses.add("java.time.LocalDate"); + builtinClasses.add("java.time.LocalTime"); + builtinClasses.add("java.time.Instant"); + builtinClasses.add("java.time.Duration"); + builtinClasses.add("java.time.Period"); + builtinClasses.add("java.time.ZonedDateTime"); + builtinClasses.add("java.time.OffsetDateTime"); + builtinClasses.add("java.time.OffsetTime"); + builtinClasses.add("java.time.Year"); + builtinClasses.add("java.time.YearMonth"); + builtinClasses.add("java.time.MonthDay"); + builtinClasses.add("java.time.ZoneId"); + builtinClasses.add("java.time.ZoneOffset"); + + builtinClasses.add("java.util.ArrayList"); + builtinClasses.add("java.util.LinkedList"); + builtinClasses.add("java.util.Vector"); + builtinClasses.add("java.util.Stack"); + builtinClasses.add("java.util.HashSet"); + builtinClasses.add("java.util.LinkedHashSet"); + builtinClasses.add("java.util.TreeSet"); + builtinClasses.add("java.util.HashMap"); + builtinClasses.add("java.util.LinkedHashMap"); + builtinClasses.add("java.util.TreeMap"); + builtinClasses.add("java.util.Hashtable"); + builtinClasses.add("java.util.Properties"); + builtinClasses.add("java.util.concurrent.ConcurrentHashMap"); + builtinClasses.add("java.util.concurrent.CopyOnWriteArrayList"); + builtinClasses.add("java.util.concurrent.CopyOnWriteArraySet"); + builtinClasses.add("java.util.concurrent.ConcurrentSkipListMap"); + builtinClasses.add("java.util.concurrent.ConcurrentSkipListSet"); + + builtinClasses.add("java.util.UUID"); + builtinClasses.add("java.util.Locale"); + builtinClasses.add("java.util.Currency"); + builtinClasses.add("java.util.Optional"); + builtinClasses.add("java.net.URL"); + builtinClasses.add("java.net.URI"); + builtinClasses.add("java.io.File"); + builtinClasses.add("java.nio.file.Path"); + builtinClasses.add("java.util.regex.Pattern"); + + builtinPrefixes.add("org.apache.seata."); + builtinPrefixes.add("io.seata."); + } +} diff --git a/common/src/main/java/org/apache/seata/common/json/JsonSerializer.java b/common/src/main/java/org/apache/seata/common/json/JsonSerializer.java new file mode 100644 index 00000000000..f4a77bbe59f --- /dev/null +++ b/common/src/main/java/org/apache/seata/common/json/JsonSerializer.java @@ -0,0 +1,91 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.seata.common.json; + +import java.lang.reflect.Type; + +/** + * The json serializer interface. + */ +public interface JsonSerializer { + + /** + * Serializes the specified object into its JSON string representation. + * + * @param object the object to serialize + * @return the JSON string representation of the object + */ + String toJSONString(Object object); + + /** + * Deserializes the specified JSON string into an object of the given class type. + * + * @param text the JSON string to parse + * @param clazz the class of T + * @param the type of the desired object + * @return the deserialized object of type T + */ + T parseObject(String text, Class clazz); + + /** + * Deserializes the specified JSON string into an object of the given type. + * + * @param text the JSON string to parse + * @param type the type to deserialize into + * @param the type of the desired object + * @return the deserialized object of type T + */ + T parseObjectWithType(String text, Type type); + + /** + * Checks whether the given JSON string uses auto type features (such as type information for polymorphic deserialization). + * + * @param json the JSON string to check + * @return true if auto type is used in the JSON, false otherwise + */ + boolean useAutoType(String json); + + /** + * Serializes the specified object into its JSON string representation. + * + * @param o the object to serialize + * @param prettyPrint whether to format the JSON string for readability + * @return the JSON string representation of the object + */ + String toJSONString(Object o, boolean prettyPrint); + + /** + * Serializes the specified object into its JSON string representation, with options to ignore auto type and pretty print. + * + * @param o the object to serialize + * @param ignoreAutoType whether to ignore auto type information during serialization + * @param prettyPrint whether to format the JSON string for readability + * @return the JSON string representation of the object + */ + String toJSONString(Object o, boolean ignoreAutoType, boolean prettyPrint); + + /** + * Deserializes the specified JSON string into an object of the given class type, with an option to ignore auto type information. + * + * @param json the JSON string to parse + * @param type the class of T + * @param ignoreAutoType whether to ignore auto type information during deserialization + * @param the type of the desired object + * @return the deserialized object of type T + */ + T parseObject(String json, Class type, boolean ignoreAutoType); +} diff --git a/common/src/main/java/org/apache/seata/common/json/JsonSerializerFactory.java b/common/src/main/java/org/apache/seata/common/json/JsonSerializerFactory.java new file mode 100644 index 00000000000..9d65c3de500 --- /dev/null +++ b/common/src/main/java/org/apache/seata/common/json/JsonSerializerFactory.java @@ -0,0 +1,60 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.seata.common.json; + +import org.apache.seata.common.loader.EnhancedServiceLoader; +import org.apache.seata.common.util.CollectionUtils; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Map; +import java.util.Optional; +import java.util.concurrent.ConcurrentHashMap; + +public class JsonSerializerFactory { + + private static final Logger LOGGER = LoggerFactory.getLogger(JsonSerializerFactory.class); + + private static final String DEFAULT_SERIALIZER = "jackson"; + + private static final String JACKSON3_SERIALIZER = "jackson3"; + + private static final Map INSTANCES = new ConcurrentHashMap<>(); + + private JsonSerializerFactory() {} + + /** + * Get JsonSerializer instance by name. + * + * @param name the serializer name (e.g., "fastjson", "fastjson2", "jackson", "jackson3", "gson") + * @return the JsonSerializer instance + */ + public static JsonSerializer getSerializer(String name) { + final String serializerName = Optional.ofNullable(name).orElse(DEFAULT_SERIALIZER); + return CollectionUtils.computeIfAbsent(INSTANCES, serializerName, key -> { + try { + return EnhancedServiceLoader.load(JsonSerializer.class, key); + } catch (Exception e) { + if (JACKSON3_SERIALIZER.equals(key)) { + LOGGER.warn("Jackson3 serializer is not available (requires JDK 17+), falling back to jackson.", e); + return EnhancedServiceLoader.load(JsonSerializer.class, DEFAULT_SERIALIZER); + } + throw e; + } + }); + } +} diff --git a/common/src/main/java/org/apache/seata/common/json/impl/Fastjson2JsonSerializer.java b/common/src/main/java/org/apache/seata/common/json/impl/Fastjson2JsonSerializer.java new file mode 100644 index 00000000000..f5599b267c4 --- /dev/null +++ b/common/src/main/java/org/apache/seata/common/json/impl/Fastjson2JsonSerializer.java @@ -0,0 +1,177 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.seata.common.json.impl; + +import com.alibaba.fastjson2.JSON; +import com.alibaba.fastjson2.JSONReader; +import com.alibaba.fastjson2.JSONWriter; +import com.alibaba.fastjson2.filter.ContextAutoTypeBeforeHandler; +import com.alibaba.fastjson2.util.TypeUtils; +import org.apache.seata.common.exception.JsonParseException; +import org.apache.seata.common.json.JsonAllowlistManager; +import org.apache.seata.common.json.JsonSerializer; +import org.apache.seata.common.loader.LoadLevel; + +import java.lang.reflect.Type; +import java.util.regex.Pattern; + +/** + * Fastjson2 implementation of JsonSerializer + */ +@LoadLevel(name = Fastjson2JsonSerializer.NAME) +public class Fastjson2JsonSerializer implements JsonSerializer { + + public static final String NAME = "fastjson2"; + + private static final Pattern AUTOTYPE_PATTERN = Pattern.compile("\"@type\"\\s*:"); + + private static final JSONWriter.Feature[] SERIALIZER_FEATURES = + new JSONWriter.Feature[] {JSONWriter.Feature.WriteClassName}; + + private static final JSONWriter.Feature[] SERIALIZER_FEATURES_PRETTY = + new JSONWriter.Feature[] {JSONWriter.Feature.WriteClassName, JSONWriter.Feature.PrettyFormat}; + + private static final JSONWriter.Feature[] FEATURES_PRETTY = + new JSONWriter.Feature[] {JSONWriter.Feature.PrettyFormat}; + + private static final AllowlistAutoTypeHandler ALLOWLIST_HANDLER = + new AllowlistAutoTypeHandler("org.apache.seata.", "io.seata."); + + @Override + public String toJSONString(Object object) { + try { + return JSON.toJSONString(object, SERIALIZER_FEATURES); + } catch (Exception e) { + throw new JsonParseException("Fastjson2 serialize error", e); + } + } + + @Override + public T parseObject(String text, Class clazz) { + if (text == null || clazz == null) { + return null; + } + try { + return JSON.parseObject(text, clazz); + } catch (Exception e) { + throw new JsonParseException("Fastjson2 deserialize error", e); + } + } + + @Override + public T parseObjectWithType(String text, Type type) { + if (text == null || type == null) { + return null; + } + try { + return JSON.parseObject(text, type, ALLOWLIST_HANDLER, JSONReader.Feature.SupportAutoType); + } catch (SecurityException e) { + throw e; + } catch (Exception e) { + rethrowIfSecurityException(e); + throw new JsonParseException("Fastjson2 deserialize error", e); + } + } + + @Override + public boolean useAutoType(String json) { + return json != null && AUTOTYPE_PATTERN.matcher(json).find(); + } + + @Override + public String toJSONString(Object object, boolean prettyPrint) { + return toJSONString(object, false, prettyPrint); + } + + @Override + public String toJSONString(Object object, boolean ignoreAutoType, boolean prettyPrint) { + try { + if (prettyPrint) { + if (ignoreAutoType) { + return JSON.toJSONString(object, FEATURES_PRETTY); + } else { + return JSON.toJSONString(object, SERIALIZER_FEATURES_PRETTY); + } + } else { + if (ignoreAutoType) { + return JSON.toJSONString(object); + } else { + return JSON.toJSONString(object, SERIALIZER_FEATURES); + } + } + } catch (Exception e) { + throw new JsonParseException("Fastjson2 serialize error", e); + } + } + + @Override + public T parseObject(String text, Class type, boolean ignoreAutoType) { + if (text == null || type == null) { + return null; + } + try { + if ("[]".equals(text) && (java.util.Collection.class.isAssignableFrom(type) || type == Object.class)) { + return (T) new java.util.ArrayList<>(); + } + + if (ignoreAutoType) { + return JSON.parseObject(text, type); + } else { + return JSON.parseObject(text, type, ALLOWLIST_HANDLER, JSONReader.Feature.SupportAutoType); + } + } catch (SecurityException e) { + throw e; + } catch (Exception e) { + rethrowIfSecurityException(e); + throw new JsonParseException("Fastjson2 deserialize error", e); + } + } + + private static void rethrowIfSecurityException(Throwable e) { + Throwable cause = e.getCause(); + while (cause != null) { + if (cause instanceof SecurityException) { + throw (SecurityException) cause; + } + cause = cause.getCause(); + } + } + + /** + * Extends ContextAutoTypeBeforeHandler (like Dubbo) for hash-optimized prefix matching. + * Built-in basic types are included via includeBasic=true, seata prefixes are passed + * to the constructor. User-defined allowlist entries are checked via JsonAllowlistManager fallback. + */ + private static class AllowlistAutoTypeHandler extends ContextAutoTypeBeforeHandler { + + AllowlistAutoTypeHandler(String... acceptNames) { + super(true, acceptNames); + } + + @Override + public Class apply(String typeName, Class expectClass, long features) { + Class clazz = super.apply(typeName, expectClass, features); + if (clazz != null) { + return clazz; + } + + // Throws SecurityException if not allowed + JsonAllowlistManager.getInstance().checkClass(typeName); + return TypeUtils.loadClass(typeName); + } + } +} diff --git a/common/src/main/java/org/apache/seata/common/json/impl/FastjsonJsonSerializer.java b/common/src/main/java/org/apache/seata/common/json/impl/FastjsonJsonSerializer.java new file mode 100644 index 00000000000..d7f09d3db68 --- /dev/null +++ b/common/src/main/java/org/apache/seata/common/json/impl/FastjsonJsonSerializer.java @@ -0,0 +1,177 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.seata.common.json.impl; + +import com.alibaba.fastjson.JSON; +import com.alibaba.fastjson.parser.Feature; +import com.alibaba.fastjson.parser.ParserConfig; +import com.alibaba.fastjson.serializer.SerializerFeature; +import org.apache.seata.common.exception.JsonParseException; +import org.apache.seata.common.json.JsonAllowlistManager; +import org.apache.seata.common.json.JsonSerializer; +import org.apache.seata.common.loader.LoadLevel; + +import java.lang.reflect.Type; +import java.util.regex.Pattern; + +/** + * FastJSON implementation of JsonSerializer + */ +@LoadLevel(name = FastjsonJsonSerializer.NAME) +public class FastjsonJsonSerializer implements JsonSerializer { + + private static final Pattern AUTOTYPE_PATTERN = Pattern.compile("\"@type\"\\s*:"); + + private static final SerializerFeature[] SERIALIZER_FEATURES = new SerializerFeature[] { + SerializerFeature.DisableCircularReferenceDetect, + SerializerFeature.WriteDateUseDateFormat, + SerializerFeature.WriteClassName + }; + + private static final SerializerFeature[] SERIALIZER_FEATURES_PRETTY = new SerializerFeature[] { + SerializerFeature.DisableCircularReferenceDetect, + SerializerFeature.WriteDateUseDateFormat, + SerializerFeature.WriteClassName, + SerializerFeature.PrettyFormat + }; + + private static final SerializerFeature[] FEATURES_PRETTY = new SerializerFeature[] { + SerializerFeature.DisableCircularReferenceDetect, + SerializerFeature.WriteDateUseDateFormat, + SerializerFeature.PrettyFormat + }; + + private static final Feature[] READER_FEATURES_SUPPORT_AUTO_TYPE = + new Feature[] {Feature.SupportAutoType, Feature.OrderedField}; + + private static final Feature[] READER_FEATURES_IGNORE_AUTO_TYPE = + new Feature[] {Feature.IgnoreAutoType, Feature.OrderedField}; + + private static final ParserConfig ALLOWLIST_PARSER_CONFIG = new ParserConfig(); + + static { + ALLOWLIST_PARSER_CONFIG.setAutoTypeSupport(true); + ALLOWLIST_PARSER_CONFIG.addAutoTypeCheckHandler((typeName, expectClass, features) -> { + JsonAllowlistManager.getInstance().checkClass(typeName); + return null; + }); + } + + public static final String NAME = "fastjson"; + + @Override + public String toJSONString(Object object) { + try { + return JSON.toJSONString(object); + } catch (Exception e) { + throw new JsonParseException("FastJSON serialize error", e); + } + } + + @Override + public T parseObject(String text, Class clazz) { + if (text == null || clazz == null) { + return null; + } + try { + return JSON.parseObject(text, clazz); + } catch (Exception e) { + throw new JsonParseException("FastJSON deserialize error", e); + } + } + + @Override + public T parseObjectWithType(String text, Type type) { + if (text == null || type == null) { + return null; + } + try { + return JSON.parseObject(text, type, ALLOWLIST_PARSER_CONFIG, Feature.SupportAutoType, Feature.OrderedField); + } catch (SecurityException e) { + throw e; + } catch (Exception e) { + rethrowIfSecurityException(e); + throw new JsonParseException("FastJSON deserialize error", e); + } + } + + // advanced methods for Saga + @Override + public boolean useAutoType(String json) { + return json != null && AUTOTYPE_PATTERN.matcher(json).find(); + } + + @Override + public String toJSONString(Object object, boolean prettyPrint) { + return toJSONString(object, false, prettyPrint); + } + + @Override + public String toJSONString(Object object, boolean ignoreAutoType, boolean prettyPrint) { + try { + if (prettyPrint) { + if (ignoreAutoType) { + return JSON.toJSONString(object, FEATURES_PRETTY); + } else { + return JSON.toJSONString(object, SERIALIZER_FEATURES_PRETTY); + } + } else { + if (ignoreAutoType) { + return JSON.toJSONString(object); + } else { + return JSON.toJSONString(object, SERIALIZER_FEATURES); + } + } + } catch (Exception e) { + throw new JsonParseException("FastJSON serialize error", e); + } + } + + @Override + public T parseObject(String text, Class type, boolean ignoreAutoType) { + if (text == null || type == null) { + return null; + } + try { + if ("[]".equals(text) && (java.util.Collection.class.isAssignableFrom(type) || type == Object.class)) { + return (T) new java.util.ArrayList<>(); + } + + if (ignoreAutoType) { + return JSON.parseObject(text, type, READER_FEATURES_IGNORE_AUTO_TYPE); + } else { + return JSON.parseObject( + text, type, ALLOWLIST_PARSER_CONFIG, Feature.SupportAutoType, Feature.OrderedField); + } + } catch (SecurityException e) { + throw e; + } catch (Exception e) { + rethrowIfSecurityException(e); + throw new JsonParseException("FastJSON deserialize error", e); + } + } + + private static void rethrowIfSecurityException(Throwable e) { + Throwable cause = e.getCause(); + while (cause != null) { + if (cause instanceof SecurityException) { + throw (SecurityException) cause; + } + cause = cause.getCause(); + } + } +} diff --git a/common/src/main/java/org/apache/seata/common/json/impl/GsonJsonSerializer.java b/common/src/main/java/org/apache/seata/common/json/impl/GsonJsonSerializer.java new file mode 100644 index 00000000000..453f0ba3990 --- /dev/null +++ b/common/src/main/java/org/apache/seata/common/json/impl/GsonJsonSerializer.java @@ -0,0 +1,116 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.seata.common.json.impl; + +import com.google.gson.Gson; +import com.google.gson.GsonBuilder; +import org.apache.seata.common.exception.JsonParseException; +import org.apache.seata.common.json.JsonSerializer; +import org.apache.seata.common.loader.LoadLevel; + +import java.lang.reflect.Modifier; +import java.lang.reflect.Type; + +/** + * Gson implementation of JsonSerializer + */ +@LoadLevel(name = GsonJsonSerializer.NAME) +public class GsonJsonSerializer implements JsonSerializer { + + private final Gson gson = new GsonBuilder() + .excludeFieldsWithModifiers(Modifier.STATIC, Modifier.TRANSIENT) + .create(); + + private final Gson gsonPretty = new GsonBuilder() + .excludeFieldsWithModifiers(Modifier.STATIC, Modifier.TRANSIENT) + .setPrettyPrinting() + .create(); + + public static final String NAME = "gson"; + + @Override + public String toJSONString(Object object) { + try { + return gson.toJson(object); + } catch (Exception e) { + throw new JsonParseException("Gson serialize error", e); + } + } + + @Override + public T parseObject(String text, Class clazz) { + if (text == null || clazz == null) { + return null; + } + try { + return gson.fromJson(text, clazz); + } catch (Exception e) { + throw new JsonParseException("Gson deserialize error", e); + } + } + + @Override + public T parseObjectWithType(String text, Type type) { + if (text == null || type == null) { + return null; + } + try { + return gson.fromJson(text, type); + } catch (Exception e) { + throw new JsonParseException("Gson deserialize error", e); + } + } + + // advanced methods for Saga + @Override + public boolean useAutoType(String json) { + return false; + } + + @Override + public String toJSONString(Object object, boolean prettyPrint) { + return toJSONString(object, false, prettyPrint); + } + + @Override + public String toJSONString(Object object, boolean ignoreAutoType, boolean prettyPrint) { + try { + if (prettyPrint) { + return gsonPretty.toJson(object); + } else { + return gson.toJson(object); + } + } catch (Exception e) { + throw new JsonParseException("Gson serialize error", e); + } + } + + @Override + public T parseObject(String text, Class type, boolean ignoreAutoType) { + if (text == null || type == null) { + return null; + } + try { + if ("[]".equals(text) && (java.util.Collection.class.isAssignableFrom(type) || type == Object.class)) { + return (T) new java.util.ArrayList<>(); + } + return gson.fromJson(text, type); + } catch (Exception e) { + throw new JsonParseException("Gson deserialize error", e); + } + } +} diff --git a/common/src/main/java/org/apache/seata/common/json/impl/JacksonJsonSerializer.java b/common/src/main/java/org/apache/seata/common/json/impl/JacksonJsonSerializer.java new file mode 100644 index 00000000000..a2df4fd455b --- /dev/null +++ b/common/src/main/java/org/apache/seata/common/json/impl/JacksonJsonSerializer.java @@ -0,0 +1,211 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.seata.common.json.impl; + +import com.fasterxml.jackson.annotation.JsonInclude; +import com.fasterxml.jackson.annotation.JsonTypeInfo; +import com.fasterxml.jackson.core.JsonProcessingException; +import com.fasterxml.jackson.databind.DeserializationFeature; +import com.fasterxml.jackson.databind.JavaType; +import com.fasterxml.jackson.databind.MapperFeature; +import com.fasterxml.jackson.databind.ObjectMapper; +import com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping; +import com.fasterxml.jackson.databind.cfg.MapperConfig; +import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator; +import org.apache.seata.common.exception.JsonParseException; +import org.apache.seata.common.json.JsonAllowlistManager; +import org.apache.seata.common.json.JsonSerializer; +import org.apache.seata.common.loader.LoadLevel; + +import java.io.IOException; +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +/** + * Jackson implementation of JsonSerializer + */ +@LoadLevel(name = JacksonJsonSerializer.NAME) +public class JacksonJsonSerializer implements JsonSerializer { + public static final String NAME = "jackson"; + + private static final Pattern AUTOTYPE_PATTERN = Pattern.compile("\"@type\"\\s*:"); + + private final ObjectMapper defaultObjectMapper; + + private final ObjectMapper objectMapperWithAutoType; + + private final ObjectMapper mapper = new ObjectMapper(); + + public JacksonJsonSerializer() { + this.defaultObjectMapper = new ObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .disableDefaultTyping() + .enable(MapperFeature.PROPAGATE_TRANSIENT_MARKER) + .setSerializationInclusion(JsonInclude.Include.NON_NULL); + + AllowlistTypeValidator validator = new AllowlistTypeValidator(); + ObjectMapper.DefaultTypeResolverBuilder typer = + new ObjectMapper.DefaultTypeResolverBuilder(DefaultTyping.NON_FINAL, validator); + typer.init(JsonTypeInfo.Id.CLASS, null); + typer.inclusion(JsonTypeInfo.As.PROPERTY); + typer.typeProperty("@type"); + + this.objectMapperWithAutoType = new ObjectMapper() + .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) + .setDefaultTyping(typer) + .enable(MapperFeature.PROPAGATE_TRANSIENT_MARKER) + .setSerializationInclusion(JsonInclude.Include.NON_NULL); + + this.mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); + this.mapper.activateDefaultTyping( + this.mapper.getPolymorphicTypeValidator(), + ObjectMapper.DefaultTyping.NON_FINAL, + JsonTypeInfo.As.PROPERTY); + this.mapper.setConfig(this.mapper.getSerializationConfig().with(MapperFeature.PROPAGATE_TRANSIENT_MARKER)); + this.mapper.setConfig(this.mapper.getDeserializationConfig().with(MapperFeature.PROPAGATE_TRANSIENT_MARKER)); + this.mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); + } + + @Override + public String toJSONString(Object object) { + try { + return mapper.writeValueAsString(object); + } catch (JsonProcessingException e) { + throw new JsonParseException("Jackson serialize error", e); + } + } + + @Override + public T parseObject(String text, Class clazz) { + if (text == null || clazz == null) { + return null; + } + try { + return defaultObjectMapper.readValue(text, clazz); + } catch (IOException e) { + throw new JsonParseException("Jackson deserialize error", e); + } + } + + @Override + public T parseObjectWithType(String text, Type type) { + if (text == null || type == null) { + return null; + } + try { + return objectMapperWithAutoType.readValue(text, objectMapperWithAutoType.constructType(type)); + } catch (SecurityException e) { + throw e; + } catch (IOException e) { + rethrowIfSecurityException(e); + throw new JsonParseException("Jackson deserialize error", e); + } + } + + // advanced methods for Saga + @Override + public boolean useAutoType(String json) { + return json != null && AUTOTYPE_PATTERN.matcher(json).find(); + } + + @Override + public String toJSONString(Object o, boolean prettyPrint) { + return toJSONString(o, false, prettyPrint); + } + + @Override + public String toJSONString(Object o, boolean ignoreAutoType, boolean prettyPrint) { + try { + if (o instanceof List && ((List) o).isEmpty()) { + return "[]"; + } + if (prettyPrint) { + if (ignoreAutoType) { + return defaultObjectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(o); + } else { + return objectMapperWithAutoType + .writerWithDefaultPrettyPrinter() + .writeValueAsString(o); + } + } else { + if (ignoreAutoType) { + return defaultObjectMapper.writeValueAsString(o); + } else { + return objectMapperWithAutoType.writeValueAsString(o); + } + } + } catch (JsonProcessingException e) { + throw new JsonParseException("Jackson serialize error", e); + } + } + + @Override + public T parseObject(String json, Class type, boolean ignoreAutoType) { + if (json == null || type == null) { + return null; + } + try { + if ("[]".equals(json) && (java.util.Collection.class.isAssignableFrom(type) || type == Object.class)) { + return (T) new ArrayList<>(0); + } + if (ignoreAutoType) { + return defaultObjectMapper.readValue(json, type); + } else { + return objectMapperWithAutoType.readValue(json, type); + } + } catch (SecurityException e) { + throw e; + } catch (IOException e) { + rethrowIfSecurityException(e); + throw new JsonParseException("Jackson deserialize error", e); + } + } + + private static void rethrowIfSecurityException(Throwable e) { + Throwable cause = e.getCause(); + while (cause != null) { + if (cause instanceof SecurityException) { + throw (SecurityException) cause; + } + cause = cause.getCause(); + } + } + + private static class AllowlistTypeValidator extends PolymorphicTypeValidator.Base { + private static final long serialVersionUID = 1L; + + @Override + public Validity validateBaseType(MapperConfig config, JavaType baseType) { + return Validity.INDETERMINATE; + } + + @Override + public Validity validateSubClassName(MapperConfig config, JavaType baseType, String subClassName) { + // Throws SecurityException if not allowed + JsonAllowlistManager.getInstance().checkClass(subClassName); + return Validity.ALLOWED; + } + + @Override + public Validity validateSubType(MapperConfig config, JavaType baseType, JavaType subType) { + JsonAllowlistManager.getInstance().checkClass(subType.getRawClass().getName()); + return Validity.ALLOWED; + } + } +} diff --git a/common/src/main/java/org/apache/seata/common/thread/PlatformThreadPoolExecutor.java b/common/src/main/java/org/apache/seata/common/thread/PlatformThreadPoolExecutor.java new file mode 100644 index 00000000000..6ed58894494 --- /dev/null +++ b/common/src/main/java/org/apache/seata/common/thread/PlatformThreadPoolExecutor.java @@ -0,0 +1,40 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.seata.common.thread; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +/** + * Historical platform-thread-backed thread pool implementation. + */ +public class PlatformThreadPoolExecutor extends ThreadPoolExecutor { + + public PlatformThreadPoolExecutor( + int corePoolSize, + int maximumPoolSize, + long keepAliveTime, + TimeUnit unit, + BlockingQueue workQueue, + ThreadFactory threadFactory, + RejectedExecutionHandler rejectedHandler) { + super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, rejectedHandler); + } +} diff --git a/common/src/main/java/org/apache/seata/common/thread/PlatformThreadPoolProvider.java b/common/src/main/java/org/apache/seata/common/thread/PlatformThreadPoolProvider.java new file mode 100644 index 00000000000..961eca35e68 --- /dev/null +++ b/common/src/main/java/org/apache/seata/common/thread/PlatformThreadPoolProvider.java @@ -0,0 +1,59 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.seata.common.thread; + +import org.apache.seata.common.loader.LoadLevel; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +/** + * Default SPI implementation that preserves the current Seata thread pool behavior. + */ +@LoadLevel(name = "platform", order = ThreadPoolProviderOrders.DEFAULT_PROVIDER_ORDER) +public class PlatformThreadPoolProvider implements ThreadPoolProvider { + + @Override + public ThreadPoolExecutor newThreadPoolExecutor( + String threadPrefix, + int corePoolSize, + int maximumPoolSize, + long keepAliveTime, + TimeUnit unit, + BlockingQueue workQueue, + boolean daemon, + RejectedExecutionHandler rejectedHandler) { + return new PlatformThreadPoolExecutor( + corePoolSize, + maximumPoolSize, + keepAliveTime, + unit, + workQueue, + new NamedThreadFactory(threadPrefix, maximumPoolSize, daemon), + rejectedHandler); + } + + @Override + public ScheduledThreadPoolExecutor newScheduledThreadPoolExecutor( + String threadPrefix, int corePoolSize, boolean daemon, RejectedExecutionHandler rejectedHandler) { + return new ScheduledThreadPoolExecutor( + corePoolSize, new NamedThreadFactory(threadPrefix, corePoolSize, daemon), rejectedHandler); + } +} diff --git a/common/src/main/java/org/apache/seata/common/thread/ThreadPoolProvider.java b/common/src/main/java/org/apache/seata/common/thread/ThreadPoolProvider.java new file mode 100644 index 00000000000..b788f2c10da --- /dev/null +++ b/common/src/main/java/org/apache/seata/common/thread/ThreadPoolProvider.java @@ -0,0 +1,42 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.seata.common.thread; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +/** + * SPI abstraction used by Seata managed thread pools. + */ +public interface ThreadPoolProvider { + + ThreadPoolExecutor newThreadPoolExecutor( + String threadPrefix, + int corePoolSize, + int maximumPoolSize, + long keepAliveTime, + TimeUnit unit, + BlockingQueue workQueue, + boolean daemon, + RejectedExecutionHandler rejectedHandler); + + ScheduledThreadPoolExecutor newScheduledThreadPoolExecutor( + String threadPrefix, int corePoolSize, boolean daemon, RejectedExecutionHandler rejectedHandler); +} diff --git a/common/src/main/java/org/apache/seata/common/thread/ThreadPoolProviderOrders.java b/common/src/main/java/org/apache/seata/common/thread/ThreadPoolProviderOrders.java new file mode 100644 index 00000000000..d8e1c32ae7a --- /dev/null +++ b/common/src/main/java/org/apache/seata/common/thread/ThreadPoolProviderOrders.java @@ -0,0 +1,27 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.seata.common.thread; + +/** + * Shared order constants for {@link ThreadPoolProvider} SPI implementations. + */ +public final class ThreadPoolProviderOrders { + + public static final int DEFAULT_PROVIDER_ORDER = 0; + + private ThreadPoolProviderOrders() {} +} diff --git a/common/src/main/java/org/apache/seata/common/thread/ThreadPoolType.java b/common/src/main/java/org/apache/seata/common/thread/ThreadPoolType.java new file mode 100644 index 00000000000..4bc05c7fedd --- /dev/null +++ b/common/src/main/java/org/apache/seata/common/thread/ThreadPoolType.java @@ -0,0 +1,61 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.seata.common.thread; + +import org.apache.seata.common.util.StringUtils; + +/** + * Supported Seata thread pool modes. + */ +public enum ThreadPoolType { + /** + * Automatic selection: uses virtual threads when running on JDK 25 or later (with the loom extension + * present), otherwise falls back to platform threads. + */ + AUTO("auto"), + /** + * Always uses platform (OS) threads regardless of the JDK version. + */ + PLATFORM("platform"), + /** + * Prefers virtual threads when running on JDK 21 or later (with the loom extension present), + * otherwise falls back to platform threads. + */ + VIRTUAL("virtual"); + + private final String code; + + ThreadPoolType(String code) { + this.code = code; + } + + public String getCode() { + return code; + } + + public static ThreadPoolType from(String code) { + if (StringUtils.isBlank(code)) { + return AUTO; + } + for (ThreadPoolType threadPoolType : values()) { + if (threadPoolType.code.equalsIgnoreCase(code)) { + return threadPoolType; + } + } + return AUTO; + } +} diff --git a/common/src/main/resources/META-INF/services/org.apache.seata.common.json.JsonSerializer b/common/src/main/resources/META-INF/services/org.apache.seata.common.json.JsonSerializer new file mode 100644 index 00000000000..ed39af9308e --- /dev/null +++ b/common/src/main/resources/META-INF/services/org.apache.seata.common.json.JsonSerializer @@ -0,0 +1,20 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# +org.apache.seata.common.json.impl.FastjsonJsonSerializer +org.apache.seata.common.json.impl.Fastjson2JsonSerializer +org.apache.seata.common.json.impl.JacksonJsonSerializer +org.apache.seata.common.json.impl.GsonJsonSerializer \ No newline at end of file diff --git a/common/src/main/resources/META-INF/services/org.apache.seata.common.thread.ThreadPoolProvider b/common/src/main/resources/META-INF/services/org.apache.seata.common.thread.ThreadPoolProvider new file mode 100644 index 00000000000..7c56610bd3e --- /dev/null +++ b/common/src/main/resources/META-INF/services/org.apache.seata.common.thread.ThreadPoolProvider @@ -0,0 +1,17 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# +org.apache.seata.common.thread.PlatformThreadPoolProvider diff --git a/compatible/pom.xml b/compatible/pom.xml index 2ceb1ab96a0..b8e1d7e7aee 100644 --- a/compatible/pom.xml +++ b/compatible/pom.xml @@ -40,11 +40,6 @@ seata-saga-engine ${project.version} - - org.apache.seata - json-common-core - ${project.version} - org.apache.seata seata-saga-engine-store diff --git a/core/pom.xml b/core/pom.xml index e0b2395a499..48d5fd29f7c 100644 --- a/core/pom.xml +++ b/core/pom.xml @@ -42,11 +42,6 @@ seata-discovery-core ${project.version} - - ${project.groupId} - seata-threadpool - ${project.version} - io.netty netty-all diff --git a/core/src/main/java/org/apache/seata/common/json/JsonUtil.java b/core/src/main/java/org/apache/seata/common/json/JsonUtil.java new file mode 100644 index 00000000000..574edf53073 --- /dev/null +++ b/core/src/main/java/org/apache/seata/common/json/JsonUtil.java @@ -0,0 +1,92 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.seata.common.json; + +import org.apache.seata.common.ConfigurationKeys; +import org.apache.seata.common.Constants; +import org.apache.seata.common.DefaultValues; +import org.apache.seata.common.exception.JsonParseException; +import org.apache.seata.common.util.StringUtils; +import org.apache.seata.config.Configuration; +import org.apache.seata.config.ConfigurationFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Objects; + +/** + * Unified JSON utility class + */ +public final class JsonUtil { + + private static final Logger LOGGER = LoggerFactory.getLogger(JsonUtil.class); + + private static final String CONFIG_JSON_SERIALIZER_NAME = + resolveJsonSerializerName(ConfigurationFactory.getInstance()); + + private static final JsonSerializer DEFAULT_SERIALIZER = + JsonSerializerFactory.getSerializer(CONFIG_JSON_SERIALIZER_NAME); + + static String resolveJsonSerializerName(Configuration configuration) { + String serializerType = configuration.getConfig(ConfigurationKeys.JSON_SERIALIZER_TYPE); + if (StringUtils.isNotBlank(serializerType)) { + return serializerType; + } + + String deprecatedSerializerType = + configuration.getConfig(ConfigurationKeys.TCC_BUSINESS_ACTION_CONTEXT_JSON_PARSER_NAME); + if (StringUtils.isNotBlank(deprecatedSerializerType)) { + LOGGER.warn( + "The config '{}' is deprecated since 2.7.0 and will be removed in a future version. Please use '{}' instead.", + ConfigurationKeys.TCC_BUSINESS_ACTION_CONTEXT_JSON_PARSER_NAME, + ConfigurationKeys.JSON_SERIALIZER_TYPE); + return deprecatedSerializerType; + } + + return DefaultValues.BUSINESS_ACTION_CONTEXT_JSON_PARSER; + } + + /** + * Serialize the given object to JSON string + * + * @param object the object to serialize + * @return the JSON string representation + * @throws JsonParseException if serialization fails + */ + public static String toJSONString(Object object) { + return DEFAULT_SERIALIZER.toJSONString(object); + } + + /** + * Deserialize the given JSON string to an object of the specified class + * + * @param the type of the object + * @param text the JSON string + * @param clazz the class to deserialize to + * @return the deserialized object + * @throws JsonParseException if deserialization fails + */ + public static T parseObject(String text, Class clazz) { + if (Objects.isNull(text) || Objects.isNull(clazz)) { + return null; + } + String jsonParseName = text.startsWith(Constants.JACKSON_JSON_TEXT_PREFIX) + ? Constants.JACKSON_JSON_PARSER_NAME + : CONFIG_JSON_SERIALIZER_NAME; + return JsonSerializerFactory.getSerializer(jsonParseName).parseObject(text, clazz); + } +} diff --git a/core/src/main/java/org/apache/seata/common/thread/ThreadPoolExecutorFactory.java b/core/src/main/java/org/apache/seata/common/thread/ThreadPoolExecutorFactory.java new file mode 100644 index 00000000000..e1b3090adf2 --- /dev/null +++ b/core/src/main/java/org/apache/seata/common/thread/ThreadPoolExecutorFactory.java @@ -0,0 +1,195 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.seata.common.thread; + +import org.apache.seata.common.loader.EnhancedServiceLoader; +import org.apache.seata.common.loader.EnhancedServiceNotFoundException; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.Objects; +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +/** + * Central factory used by Seata managed thread pools. + */ +public final class ThreadPoolExecutorFactory { + + private static final Logger LOGGER = LoggerFactory.getLogger(ThreadPoolExecutorFactory.class); + + private ThreadPoolExecutorFactory() {} + + public static ThreadFactory newThreadFactory(String threadPrefix, int totalSize) { + return newThreadFactory(threadPrefix, totalSize, true); + } + + public static ThreadFactory newThreadFactory(String threadPrefix, int totalSize, boolean daemon) { + Objects.requireNonNull(threadPrefix, "threadPrefix must not be null"); + return new NamedThreadFactory(threadPrefix, totalSize, daemon); + } + + public static ThreadPoolExecutor newThreadPoolExecutor( + String threadPrefix, + int corePoolSize, + int maximumPoolSize, + long keepAliveTime, + TimeUnit unit, + BlockingQueue workQueue) { + return newThreadPoolExecutor(threadPrefix, corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, true); + } + + public static ThreadPoolExecutor newThreadPoolExecutor( + String threadPrefix, + int corePoolSize, + int maximumPoolSize, + long keepAliveTime, + TimeUnit unit, + BlockingQueue workQueue, + boolean daemon) { + return newThreadPoolExecutor( + threadPrefix, + corePoolSize, + maximumPoolSize, + keepAliveTime, + unit, + workQueue, + daemon, + new ThreadPoolExecutor.AbortPolicy()); + } + + public static ThreadPoolExecutor newThreadPoolExecutor( + String threadPrefix, + int corePoolSize, + int maximumPoolSize, + long keepAliveTime, + TimeUnit unit, + BlockingQueue workQueue, + RejectedExecutionHandler rejectedHandler) { + return newThreadPoolExecutor( + threadPrefix, corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, true, rejectedHandler); + } + + public static ThreadPoolExecutor newThreadPoolExecutor( + String threadPrefix, + int corePoolSize, + int maximumPoolSize, + long keepAliveTime, + TimeUnit unit, + BlockingQueue workQueue, + boolean daemon, + RejectedExecutionHandler rejectedHandler) { + validateThreadPoolArguments(threadPrefix, corePoolSize, maximumPoolSize, keepAliveTime, unit); + return resolveThreadPoolProvider() + .newThreadPoolExecutor( + threadPrefix, + corePoolSize, + maximumPoolSize, + keepAliveTime, + unit, + Objects.requireNonNull(workQueue, "workQueue must not be null"), + daemon, + Objects.requireNonNull(rejectedHandler, "rejectedHandler must not be null")); + } + + public static ScheduledThreadPoolExecutor newScheduledThreadPoolExecutor(String threadPrefix, int corePoolSize) { + return newScheduledThreadPoolExecutor(threadPrefix, corePoolSize, true); + } + + public static ScheduledThreadPoolExecutor newScheduledThreadPoolExecutor( + String threadPrefix, int corePoolSize, boolean daemon) { + return newScheduledThreadPoolExecutor(threadPrefix, corePoolSize, daemon, new ThreadPoolExecutor.AbortPolicy()); + } + + public static ScheduledThreadPoolExecutor newScheduledThreadPoolExecutor( + String threadPrefix, int corePoolSize, boolean daemon, RejectedExecutionHandler rejectedHandler) { + validateScheduledThreadPoolArguments(threadPrefix, corePoolSize); + return resolveThreadPoolProvider() + .newScheduledThreadPoolExecutor( + threadPrefix, + corePoolSize, + daemon, + Objects.requireNonNull(rejectedHandler, "rejectedHandler must not be null")); + } + + private static void validateThreadPoolArguments( + String threadPrefix, int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit) { + Objects.requireNonNull(threadPrefix, "threadPrefix must not be null"); + Objects.requireNonNull(unit, "timeUnit must not be null"); + if (corePoolSize < 0) { + throw new IllegalArgumentException("corePoolSize must not be negative"); + } + if (maximumPoolSize <= 0) { + throw new IllegalArgumentException("maximumPoolSize must be greater than zero"); + } + if (maximumPoolSize < corePoolSize) { + throw new IllegalArgumentException("maximumPoolSize must be greater than or equal to corePoolSize"); + } + if (keepAliveTime < 0) { + throw new IllegalArgumentException("keepAliveTime must not be negative"); + } + } + + private static void validateScheduledThreadPoolArguments(String threadPrefix, int corePoolSize) { + Objects.requireNonNull(threadPrefix, "threadPrefix must not be null"); + if (corePoolSize < 0) { + throw new IllegalArgumentException("corePoolSize must not be negative"); + } + } + + private static ThreadPoolProvider resolveThreadPoolProvider() { + ThreadPoolType threadPoolType = ThreadPoolRuntimeEnvironment.resolveThreadPoolType(); + if (threadPoolType == ThreadPoolType.VIRTUAL && ThreadPoolProviderHolder.VIRTUAL_THREAD_POOL_PROVIDER != null) { + return ThreadPoolProviderHolder.VIRTUAL_THREAD_POOL_PROVIDER; + } + if (threadPoolType == ThreadPoolType.VIRTUAL) { + // loadOptional() already warned that the provider is absent at initialisation time. + // This second warning is intentionally kept to surface the per-request fallback + // decision so that operators can correlate the missing-provider startup message + // with the actual thread-pool mode that is in effect. + LOGGER.warn( + "Virtual thread pool was selected but the virtual-thread SPI provider (seata-threadpool-virtual) " + + "is not present on the classpath. Falling back to platform threads."); + } + return ThreadPoolProviderHolder.PLATFORM_THREAD_POOL_PROVIDER; + } + + private static final class ThreadPoolProviderHolder { + private static final ThreadPoolProvider PLATFORM_THREAD_POOL_PROVIDER = + EnhancedServiceLoader.load(ThreadPoolProvider.class, ThreadPoolType.PLATFORM.getCode()); + private static final ThreadPoolProvider VIRTUAL_THREAD_POOL_PROVIDER = + loadOptional(ThreadPoolType.VIRTUAL.getCode()); + + private ThreadPoolProviderHolder() {} + } + + private static ThreadPoolProvider loadOptional(String activateName) { + try { + return EnhancedServiceLoader.load(ThreadPoolProvider.class, activateName); + } catch (EnhancedServiceNotFoundException ignored) { + LOGGER.warn( + "Virtual thread pool SPI provider '{}' is not available on the classpath. " + + "Virtual threads cannot be used.", + activateName); + return null; + } + } +} diff --git a/core/src/main/java/org/apache/seata/common/thread/ThreadPoolRuntimeEnvironment.java b/core/src/main/java/org/apache/seata/common/thread/ThreadPoolRuntimeEnvironment.java new file mode 100644 index 00000000000..bfcfce94f35 --- /dev/null +++ b/core/src/main/java/org/apache/seata/common/thread/ThreadPoolRuntimeEnvironment.java @@ -0,0 +1,108 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.seata.common.thread; + +import org.apache.seata.common.ConfigurationKeys; +import org.apache.seata.common.DefaultValues; +import org.apache.seata.config.Configuration; +import org.apache.seata.config.ConfigurationFactory; +import org.slf4j.Logger; +import org.slf4j.LoggerFactory; + +import java.util.function.IntSupplier; +import java.util.function.Supplier; + +/** + * Runtime helper used to resolve the thread pool mode. + */ +final class ThreadPoolRuntimeEnvironment { + + private static final Logger LOGGER = LoggerFactory.getLogger(ThreadPoolRuntimeEnvironment.class); + + private static final Supplier DEFAULT_THREAD_POOL_TYPE_SUPPLIER = + ThreadPoolRuntimeEnvironment::loadConfiguredThreadPoolType; + private static final IntSupplier DEFAULT_JDK_FEATURE_SUPPLIER = ThreadPoolRuntimeEnvironment::javaFeatureVersion; + + private static volatile Supplier threadPoolTypeSupplier = DEFAULT_THREAD_POOL_TYPE_SUPPLIER; + private static volatile IntSupplier jdkFeatureSupplier = DEFAULT_JDK_FEATURE_SUPPLIER; + + private ThreadPoolRuntimeEnvironment() {} + + static ThreadPoolType resolveThreadPoolType() { + ThreadPoolType configuredType = ThreadPoolType.from(threadPoolTypeSupplier.get()); + if (configuredType == ThreadPoolType.PLATFORM) { + return ThreadPoolType.PLATFORM; + } + int jdkFeature = jdkFeatureSupplier.getAsInt(); + if (configuredType == ThreadPoolType.VIRTUAL) { + if (jdkFeature < 21) { + LOGGER.warn( + "transport.threadpool=virtual is configured but the current JDK feature version is {} (<21). " + + "Virtual threads are not supported; falling back to platform threads.", + jdkFeature); + } + return jdkFeature >= 21 ? ThreadPoolType.VIRTUAL : ThreadPoolType.PLATFORM; + } + return jdkFeature >= 25 ? ThreadPoolType.VIRTUAL : ThreadPoolType.PLATFORM; + } + + static void setThreadPoolTypeSupplier(Supplier supplier) { + threadPoolTypeSupplier = supplier == null ? DEFAULT_THREAD_POOL_TYPE_SUPPLIER : supplier; + } + + static void setJdkFeatureSupplier(IntSupplier supplier) { + jdkFeatureSupplier = supplier == null ? DEFAULT_JDK_FEATURE_SUPPLIER : supplier; + } + + static void reset() { + threadPoolTypeSupplier = DEFAULT_THREAD_POOL_TYPE_SUPPLIER; + jdkFeatureSupplier = DEFAULT_JDK_FEATURE_SUPPLIER; + } + + private static String loadConfiguredThreadPoolType() { + Configuration configuration = ConfigurationFactory.getInstance(); + return configuration.getConfig( + ConfigurationKeys.TRANSPORT_THREADPOOL, DefaultValues.DEFAULT_TRANSPORT_THREADPOOL); + } + + static int javaFeatureVersion() { + String specVersion = System.getProperty("java.specification.version", "1.8"); + if (specVersion != null) { + specVersion = specVersion.trim(); + } + if (specVersion.startsWith("1.")) { + // Java 8 and earlier: "1.8", "1.7", etc. + // Be tolerant of values like "1.8 ", "1.8.0", or "1.x". + String legacyPart = specVersion.substring(2); + int dotIndex = legacyPart.indexOf('.'); + if (dotIndex >= 0) { + legacyPart = legacyPart.substring(0, dotIndex); + } + try { + return Integer.parseInt(legacyPart); + } catch (NumberFormatException e) { + return 8; + } + } + // Java 9+: "9", "11", "17", "21", "25", etc. + try { + return Integer.parseInt(specVersion); + } catch (NumberFormatException e) { + return 8; + } + } +} diff --git a/core/src/test/java/org/apache/seata/common/thread/ThreadPoolExecutorFactoryTest.java b/core/src/test/java/org/apache/seata/common/thread/ThreadPoolExecutorFactoryTest.java new file mode 100644 index 00000000000..d72c957d218 --- /dev/null +++ b/core/src/test/java/org/apache/seata/common/thread/ThreadPoolExecutorFactoryTest.java @@ -0,0 +1,181 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.seata.common.thread; + +import org.apache.seata.common.DefaultValues; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.params.ParameterizedTest; +import org.junit.jupiter.params.provider.CsvSource; + +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadFactory; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; + +/** + * Tests for {@link ThreadPoolExecutorFactory}. + */ +public class ThreadPoolExecutorFactoryTest { + + @AfterEach + public void tearDown() { + ThreadPoolRuntimeEnvironment.reset(); + } + + @Test + public void testNewThreadFactoryCreatesThreadsWithExpectedName() { + ThreadFactory threadFactory = ThreadPoolExecutorFactory.newThreadFactory("factoryTest", 2, true); + + Thread thread = threadFactory.newThread(() -> {}); + + assertThat(thread.getName()).startsWith("factoryTest"); + assertThat(thread.isDaemon()).isTrue(); + } + + @Test + public void testNewThreadPoolExecutor() { + ThreadPoolRuntimeEnvironment.setThreadPoolTypeSupplier(() -> "platform"); + ThreadPoolExecutor executor = ThreadPoolExecutorFactory.newThreadPoolExecutor( + "poolTest", 1, 2, 60, TimeUnit.SECONDS, new LinkedBlockingQueue()); + try { + Thread thread = executor.getThreadFactory().newThread(() -> {}); + + assertThat(executor).isInstanceOf(PlatformThreadPoolExecutor.class); + assertThat(executor.getCorePoolSize()).isEqualTo(1); + assertThat(executor.getMaximumPoolSize()).isEqualTo(2); + assertThat(thread.getName()).startsWith("poolTest"); + assertThat(thread.isDaemon()).isTrue(); + } finally { + executor.shutdownNow(); + } + } + + @Test + public void testNewScheduledThreadPoolExecutor() { + ThreadPoolRuntimeEnvironment.setThreadPoolTypeSupplier(() -> "platform"); + ScheduledThreadPoolExecutor executor = + ThreadPoolExecutorFactory.newScheduledThreadPoolExecutor("scheduleTest", 1, true); + try { + Thread thread = executor.getThreadFactory().newThread(() -> {}); + + assertThat(executor.getCorePoolSize()).isEqualTo(1); + assertThat(thread.getName()).startsWith("scheduleTest"); + assertThat(thread.isDaemon()).isTrue(); + } finally { + executor.shutdownNow(); + } + } + + @Test + public void testNewThreadPoolExecutorAllowsZeroCorePoolSize() { + ThreadPoolRuntimeEnvironment.setThreadPoolTypeSupplier(() -> "platform"); + ThreadPoolExecutor executor = ThreadPoolExecutorFactory.newThreadPoolExecutor( + "zeroCorePool", 0, 1, 60, TimeUnit.SECONDS, new LinkedBlockingQueue()); + try { + assertThat(executor.getCorePoolSize()).isZero(); + assertThat(executor.getMaximumPoolSize()).isEqualTo(1); + } finally { + executor.shutdownNow(); + } + } + + @Test + public void testNewScheduledThreadPoolExecutorAllowsZeroCorePoolSize() { + ThreadPoolRuntimeEnvironment.setThreadPoolTypeSupplier(() -> "platform"); + ScheduledThreadPoolExecutor executor = + ThreadPoolExecutorFactory.newScheduledThreadPoolExecutor("zeroSchedule", 0, true); + try { + assertThat(executor.getCorePoolSize()).isZero(); + } finally { + executor.shutdownNow(); + } + } + + @Test + public void testVirtualThreadPoolFallsBackToPlatformWithoutLoomProvider() { + ThreadPoolRuntimeEnvironment.setThreadPoolTypeSupplier(() -> "virtual"); + ThreadPoolRuntimeEnvironment.setJdkFeatureSupplier(() -> 21); + + ThreadPoolExecutor executor = ThreadPoolExecutorFactory.newThreadPoolExecutor( + "virtualFallback", 1, 2, 60, TimeUnit.SECONDS, new LinkedBlockingQueue()); + try { + assertThat(executor).isInstanceOf(PlatformThreadPoolExecutor.class); + assertThat(executor.getMaximumPoolSize()).isEqualTo(2); + } finally { + executor.shutdownNow(); + } + } + + @Test + public void testAutoThreadPoolFallsBackToPlatformBeforeLoomAvailable() { + ThreadPoolRuntimeEnvironment.setThreadPoolTypeSupplier(() -> "auto"); + ThreadPoolRuntimeEnvironment.setJdkFeatureSupplier(() -> 25); + + ThreadPoolExecutor executor = ThreadPoolExecutorFactory.newThreadPoolExecutor( + "autoFallback", 1, 2, 60, TimeUnit.SECONDS, new LinkedBlockingQueue()); + try { + assertThat(executor).isInstanceOf(PlatformThreadPoolExecutor.class); + } finally { + executor.shutdownNow(); + } + } + + @Test + public void testVirtualScheduledThreadPoolFallsBackToPlatformWithoutLoomProvider() { + ThreadPoolRuntimeEnvironment.setThreadPoolTypeSupplier(() -> "virtual"); + ThreadPoolRuntimeEnvironment.setJdkFeatureSupplier(() -> 21); + + ScheduledThreadPoolExecutor executor = + ThreadPoolExecutorFactory.newScheduledThreadPoolExecutor("virtualScheduleFallback", 1, true); + try { + Thread thread = executor.getThreadFactory().newThread(() -> {}); + + assertThat(executor.getCorePoolSize()).isEqualTo(1); + assertThat(thread.getName()).startsWith("virtualScheduleFallback"); + assertThat(thread.isDaemon()).isTrue(); + } finally { + executor.shutdownNow(); + } + } + + @ParameterizedTest + @CsvSource({"1.8, 8", "1.7, 7", "9, 9", "11, 11", "17, 17", "21, 21", "25, 25"}) + public void testJavaFeatureVersionParsing(String specVersion, int expectedFeature) { + String previousVersion = System.getProperty("java.specification.version"); + try { + System.setProperty("java.specification.version", specVersion); + assertThat(ThreadPoolRuntimeEnvironment.javaFeatureVersion()).isEqualTo(expectedFeature); + } finally { + if (previousVersion != null) { + System.setProperty("java.specification.version", previousVersion); + } else { + System.clearProperty("java.specification.version"); + } + } + } + + @Test + public void testThreadPoolTypeAutoCodeIsStable() { + assertThat(ThreadPoolType.AUTO.getCode()).isEqualTo("auto"); + assertThat(ThreadPoolType.from(DefaultValues.DEFAULT_TRANSPORT_THREADPOOL)) + .isEqualTo(ThreadPoolType.AUTO); + } +} diff --git a/discovery/seata-discovery-consul/src/main/java/org/apache/seata/discovery/registry/consul/ConsulRegistryServiceImpl.java b/discovery/seata-discovery-consul/src/main/java/org/apache/seata/discovery/registry/consul/ConsulRegistryServiceImpl.java index 8aae9178716..4f0f222237b 100644 --- a/discovery/seata-discovery-consul/src/main/java/org/apache/seata/discovery/registry/consul/ConsulRegistryServiceImpl.java +++ b/discovery/seata-discovery-consul/src/main/java/org/apache/seata/discovery/registry/consul/ConsulRegistryServiceImpl.java @@ -22,7 +22,7 @@ import com.ecwid.consul.v1.agent.model.NewService; import com.ecwid.consul.v1.health.HealthServicesRequest; import com.ecwid.consul.v1.health.model.HealthService; -import org.apache.seata.common.thread.ThreadPoolExecutorFactory; +import org.apache.seata.common.thread.PlatformThreadPoolProvider; import org.apache.seata.common.util.NetUtil; import org.apache.seata.common.util.StringUtils; import org.apache.seata.config.Configuration; @@ -96,13 +96,16 @@ private ConsulRegistryServiceImpl() { clusterAddressMap = new ConcurrentHashMap<>(MAP_INITIAL_CAPACITY); listenerMap = new ConcurrentHashMap<>(MAP_INITIAL_CAPACITY); notifiers = new ConcurrentHashMap<>(MAP_INITIAL_CAPACITY); - notifierExecutor = ThreadPoolExecutorFactory.newThreadPoolExecutor( - "services-consul-notifier", - THREAD_POOL_NUM, - THREAD_POOL_NUM, - Integer.MAX_VALUE, - TimeUnit.MILLISECONDS, - new LinkedBlockingQueue<>()); + notifierExecutor = new PlatformThreadPoolProvider() + .newThreadPoolExecutor( + "services-consul-notifier", + THREAD_POOL_NUM, + THREAD_POOL_NUM, + Integer.MAX_VALUE, + TimeUnit.MILLISECONDS, + new LinkedBlockingQueue<>(), + true, + new java.util.concurrent.ThreadPoolExecutor.AbortPolicy()); } /** diff --git a/discovery/seata-discovery-core/pom.xml b/discovery/seata-discovery-core/pom.xml index ef149884506..98e7d06e5a4 100644 --- a/discovery/seata-discovery-core/pom.xml +++ b/discovery/seata-discovery-core/pom.xml @@ -35,12 +35,6 @@ seata-config-core ${project.version} - - ${project.groupId} - seata-threadpool - ${project.version} - - ch.qos.logback logback-classic diff --git a/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/registry/RegistryHeartBeats.java b/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/registry/RegistryHeartBeats.java index c85b16c3730..04864792bcc 100644 --- a/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/registry/RegistryHeartBeats.java +++ b/discovery/seata-discovery-core/src/main/java/org/apache/seata/discovery/registry/RegistryHeartBeats.java @@ -16,7 +16,7 @@ */ package org.apache.seata.discovery.registry; -import org.apache.seata.common.thread.ThreadPoolExecutorFactory; +import org.apache.seata.common.thread.PlatformThreadPoolProvider; import org.apache.seata.config.Configuration; import org.apache.seata.config.ConfigurationFactory; import org.slf4j.Logger; @@ -24,6 +24,7 @@ import java.net.InetSocketAddress; import java.util.concurrent.ScheduledExecutorService; +import java.util.concurrent.ThreadPoolExecutor; import java.util.concurrent.TimeUnit; /** @@ -42,8 +43,8 @@ public class RegistryHeartBeats { private static final long DEFAULT_HEARTBEAT_PERIOD = 60 * 1000; private static final boolean DEFAULT_HEARTBEAT_ENABLED = Boolean.TRUE; - private static final ScheduledExecutorService HEARTBEAT_SCHEDULED = - ThreadPoolExecutorFactory.newScheduledThreadPoolExecutor("seata-discovery-heartbeat", 1, true); + private static final ScheduledExecutorService HEARTBEAT_SCHEDULED = new PlatformThreadPoolProvider() + .newScheduledThreadPoolExecutor("seata-discovery-heartbeat", 1, true, new ThreadPoolExecutor.AbortPolicy()); public static void addHeartBeat(String registryType, InetSocketAddress serverAddress, ReRegister reRegister) { addHeartBeat(registryType, serverAddress, getHeartbeatPeriod(registryType), reRegister); diff --git a/discovery/seata-discovery-etcd3/src/main/java/org/apache/seata/discovery/registry/etcd3/EtcdRegistryServiceImpl.java b/discovery/seata-discovery-etcd3/src/main/java/org/apache/seata/discovery/registry/etcd3/EtcdRegistryServiceImpl.java index be5bf174a39..58697840451 100644 --- a/discovery/seata-discovery-etcd3/src/main/java/org/apache/seata/discovery/registry/etcd3/EtcdRegistryServiceImpl.java +++ b/discovery/seata-discovery-etcd3/src/main/java/org/apache/seata/discovery/registry/etcd3/EtcdRegistryServiceImpl.java @@ -28,7 +28,7 @@ import io.etcd.jetcd.options.WatchOption; import io.etcd.jetcd.watch.WatchResponse; import org.apache.seata.common.exception.ShouldNeverHappenException; -import org.apache.seata.common.thread.ThreadPoolExecutorFactory; +import org.apache.seata.common.thread.PlatformThreadPoolProvider; import org.apache.seata.common.util.NetUtil; import org.apache.seata.common.util.StringUtils; import org.apache.seata.config.Configuration; @@ -104,13 +104,16 @@ private EtcdRegistryServiceImpl() { clusterAddressMap = new ConcurrentHashMap<>(MAP_INITIAL_CAPACITY); listenerMap = new ConcurrentHashMap<>(MAP_INITIAL_CAPACITY); watcherMap = new ConcurrentHashMap<>(MAP_INITIAL_CAPACITY); - executorService = ThreadPoolExecutorFactory.newThreadPoolExecutor( - "registry-etcd3", - THREAD_POOL_SIZE, - THREAD_POOL_SIZE, - Integer.MAX_VALUE, - TimeUnit.MILLISECONDS, - new LinkedBlockingQueue<>()); + executorService = new PlatformThreadPoolProvider() + .newThreadPoolExecutor( + "registry-etcd3", + THREAD_POOL_SIZE, + THREAD_POOL_SIZE, + Integer.MAX_VALUE, + TimeUnit.MILLISECONDS, + new LinkedBlockingQueue<>(), + true, + new java.util.concurrent.ThreadPoolExecutor.AbortPolicy()); } /** diff --git a/discovery/seata-discovery-redis/src/main/java/org/apache/seata/discovery/registry/redis/RedisRegistryServiceImpl.java b/discovery/seata-discovery-redis/src/main/java/org/apache/seata/discovery/registry/redis/RedisRegistryServiceImpl.java index e7712f8d286..2741bcb1d18 100644 --- a/discovery/seata-discovery-redis/src/main/java/org/apache/seata/discovery/registry/redis/RedisRegistryServiceImpl.java +++ b/discovery/seata-discovery-redis/src/main/java/org/apache/seata/discovery/registry/redis/RedisRegistryServiceImpl.java @@ -19,7 +19,7 @@ import org.apache.commons.pool2.impl.GenericObjectPoolConfig; import org.apache.seata.common.ConfigurationKeys; import org.apache.seata.common.exception.ShouldNeverHappenException; -import org.apache.seata.common.thread.ThreadPoolExecutorFactory; +import org.apache.seata.common.thread.PlatformThreadPoolProvider; import org.apache.seata.common.util.CollectionUtils; import org.apache.seata.common.util.NetUtil; import org.apache.seata.common.util.StringUtils; @@ -74,10 +74,19 @@ public class RedisRegistryServiceImpl implements RegistryService private String transactionServiceGroup; + private static final PlatformThreadPoolProvider THREAD_POOL_PROVIDER = new PlatformThreadPoolProvider(); private ScheduledExecutorService threadPoolExecutorForSubscribe = - ThreadPoolExecutorFactory.newScheduledThreadPoolExecutor("RedisRegistryService-subscribe", 1); + THREAD_POOL_PROVIDER.newScheduledThreadPoolExecutor( + "RedisRegistryService-subscribe", + 1, + true, + new java.util.concurrent.ThreadPoolExecutor.AbortPolicy()); private ScheduledExecutorService threadPoolExecutorForUpdateMap = - ThreadPoolExecutorFactory.newScheduledThreadPoolExecutor("RedisRegistryService-updateClusterAddrMap", 1); + THREAD_POOL_PROVIDER.newScheduledThreadPoolExecutor( + "RedisRegistryService-updateClusterAddrMap", + 1, + true, + new java.util.concurrent.ThreadPoolExecutor.AbortPolicy()); private RedisRegistryServiceImpl() { Configuration seataConfig = ConfigurationFactory.CURRENT_FILE_INSTANCE; diff --git a/integration-tx-api/pom.xml b/integration-tx-api/pom.xml index 46200183000..c08e5127e33 100644 --- a/integration-tx-api/pom.xml +++ b/integration-tx-api/pom.xml @@ -57,11 +57,6 @@ seata-common ${project.version} - - ${project.groupId} - json-common-core - ${project.version} - net.bytebuddy byte-buddy diff --git a/jdk17/pom.xml b/jdk17/pom.xml new file mode 100644 index 00000000000..a451c5f9911 --- /dev/null +++ b/jdk17/pom.xml @@ -0,0 +1,53 @@ + + + + + org.apache.seata + seata-parent + ${revision} + + 4.0.0 + seata-jdk17 + jar + seata-jdk17 ${project.version} + JDK 17+ extensions for Seata (Jackson 3 JSON serializer) + + + 17 + 17 + 17 + + + + + ${project.groupId} + seata-common + ${project.version} + + + tools.jackson.core + jackson-databind + provided + + + + diff --git a/jdk17/src/main/java/org/apache/seata/common/json/impl/Jackson3JsonSerializer.java b/jdk17/src/main/java/org/apache/seata/common/json/impl/Jackson3JsonSerializer.java new file mode 100644 index 00000000000..86c361a3b9f --- /dev/null +++ b/jdk17/src/main/java/org/apache/seata/common/json/impl/Jackson3JsonSerializer.java @@ -0,0 +1,185 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.seata.common.json.impl; + +import org.apache.seata.common.exception.JsonParseException; +import org.apache.seata.common.json.JsonAllowlistManager; +import org.apache.seata.common.json.JsonSerializer; +import org.apache.seata.common.loader.LoadLevel; +import tools.jackson.core.JacksonException; +import tools.jackson.databind.DatabindContext; +import tools.jackson.databind.DefaultTyping; +import tools.jackson.databind.DeserializationFeature; +import tools.jackson.databind.JavaType; +import tools.jackson.databind.ObjectMapper; +import tools.jackson.databind.json.JsonMapper; +import tools.jackson.databind.jsontype.PolymorphicTypeValidator; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; +import java.util.regex.Pattern; + +/** + * Jackson 3.x implementation of JsonSerializer + */ +@LoadLevel(name = Jackson3JsonSerializer.NAME) +public class Jackson3JsonSerializer implements JsonSerializer { + + public static final String NAME = "jackson3"; + + private static final Pattern AUTOTYPE_PATTERN = Pattern.compile("\"@type\"\\s*:"); + + private final ObjectMapper defaultObjectMapper; + + private final ObjectMapper objectMapperWithAutoType; + + public Jackson3JsonSerializer() { + this.defaultObjectMapper = JsonMapper.builder() + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .build(); + + AllowlistTypeValidator validator = new AllowlistTypeValidator(); + this.objectMapperWithAutoType = JsonMapper.builder() + .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) + .activateDefaultTypingAsProperty(validator, DefaultTyping.NON_FINAL, "@type") + .build(); + } + + @Override + public String toJSONString(Object object) { + try { + if (object instanceof List && ((List) object).isEmpty()) { + return "[]"; + } + return objectMapperWithAutoType.writeValueAsString(object); + } catch (JacksonException e) { + throw new JsonParseException("Jackson3 serialize error", e); + } + } + + @Override + public T parseObject(String text, Class clazz) { + if (text == null || clazz == null) { + return null; + } + try { + return defaultObjectMapper.readValue(text, clazz); + } catch (JacksonException e) { + throw new JsonParseException("Jackson3 deserialize error", e); + } + } + + @Override + public T parseObjectWithType(String text, Type type) { + if (text == null || type == null) { + return null; + } + try { + return objectMapperWithAutoType.readValue(text, objectMapperWithAutoType.constructType(type)); + } catch (SecurityException e) { + throw e; + } catch (JacksonException e) { + rethrowIfSecurityException(e); + throw new JsonParseException("Jackson3 deserialize error", e); + } + } + + @Override + public boolean useAutoType(String json) { + return json != null && AUTOTYPE_PATTERN.matcher(json).find(); + } + + @Override + public String toJSONString(Object o, boolean prettyPrint) { + return toJSONString(o, false, prettyPrint); + } + + @Override + public String toJSONString(Object o, boolean ignoreAutoType, boolean prettyPrint) { + try { + if (o instanceof List && ((List) o).isEmpty()) { + return "[]"; + } + ObjectMapper mapper = ignoreAutoType ? defaultObjectMapper : objectMapperWithAutoType; + if (prettyPrint) { + return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(o); + } else { + return mapper.writeValueAsString(o); + } + } catch (JacksonException e) { + throw new JsonParseException("Jackson3 serialize error", e); + } + } + + @Override + public T parseObject(String json, Class type, boolean ignoreAutoType) { + if (json == null || type == null) { + return null; + } + try { + if ("[]".equals(json) && (java.util.Collection.class.isAssignableFrom(type) || type == Object.class)) { + return (T) new ArrayList<>(0); + } + if (ignoreAutoType) { + return defaultObjectMapper.readValue(json, type); + } else { + return objectMapperWithAutoType.readValue(json, type); + } + } catch (SecurityException e) { + throw e; + } catch (JacksonException e) { + rethrowIfSecurityException(e); + throw new JsonParseException("Jackson3 deserialize error", e); + } + } + + private static void rethrowIfSecurityException(Throwable e) { + Throwable cause = e.getCause(); + while (cause != null) { + if (cause instanceof SecurityException) { + throw (SecurityException) cause; + } + cause = cause.getCause(); + } + } + + /** + * Jackson 3.x native PolymorphicTypeValidator that delegates to JsonAllowlistManager + */ + private static class AllowlistTypeValidator extends PolymorphicTypeValidator.Base { + private static final long serialVersionUID = 1L; + + @Override + public Validity validateBaseType(DatabindContext ctxt, JavaType baseType) { + return Validity.INDETERMINATE; + } + + @Override + public Validity validateSubClassName(DatabindContext ctxt, JavaType baseType, String subClassName) { + // Throws SecurityException if not allowed + JsonAllowlistManager.getInstance().checkClass(subClassName); + return Validity.ALLOWED; + } + + @Override + public Validity validateSubType(DatabindContext ctxt, JavaType baseType, JavaType subType) { + JsonAllowlistManager.getInstance().checkClass(subType.getRawClass().getName()); + return Validity.ALLOWED; + } + } +} diff --git a/jdk17/src/main/resources/META-INF/services/org.apache.seata.common.json.JsonSerializer b/jdk17/src/main/resources/META-INF/services/org.apache.seata.common.json.JsonSerializer new file mode 100644 index 00000000000..f396fca1218 --- /dev/null +++ b/jdk17/src/main/resources/META-INF/services/org.apache.seata.common.json.JsonSerializer @@ -0,0 +1,17 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# +org.apache.seata.common.json.impl.Jackson3JsonSerializer diff --git a/jdk17/src/test/java/org/apache/seata/common/json/Jackson3AllowlistTest.java b/jdk17/src/test/java/org/apache/seata/common/json/Jackson3AllowlistTest.java new file mode 100644 index 00000000000..418829c2cfd --- /dev/null +++ b/jdk17/src/test/java/org/apache/seata/common/json/Jackson3AllowlistTest.java @@ -0,0 +1,243 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.seata.common.json; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * Tests for Jackson3 serializer with allowlist security check + */ +public class Jackson3AllowlistTest { + + private JsonSerializer jsonSerializer; + + @BeforeEach + void setUp() { + jsonSerializer = JsonSerializerFactory.getSerializer("jackson3"); + } + + @AfterEach + void tearDown() { + JsonAllowlistManager.getInstance().clearUserAllowlist(); + } + + @Test + public void testParseObject_allowedSeataClass() { + + String json = + "{\"@type\":\"org.apache.seata.common.json.Jackson3AllowlistTest$AllowedTestClass\",\"name\":\"test\"}"; + + AllowedTestClass result = jsonSerializer.parseObject(json, AllowedTestClass.class, false); + + assertThat(result).isNotNull(); + assertThat(result.getName()).isEqualTo("test"); + } + + @Test + public void testParseObject_allowedJavaClass() { + + String json = "{\"@type\":\"java.util.HashMap\"}"; + + Object result = jsonSerializer.parseObject(json, Object.class, false); + + assertThat(result).isNotNull(); + } + + @Test + public void testParseObject_notAllowedClass() { + + String json = "{\"@type\":\"com.malicious.EvilClass\",\"command\":\"rm -rf /\"}"; + + assertThatThrownBy(() -> jsonSerializer.parseObject(json, Object.class, false)) + .isInstanceOf(SecurityException.class) + .hasMessageContaining("not in JSON deserialization allowlist") + .hasMessageContaining("com.malicious.EvilClass"); + } + + @Test + public void testParseObject_userAllowedClass() { + + JsonAllowlistManager.getInstance().addUserClass("com.example.UserClass"); + + String json = "{\"@type\":\"com.example.UserClass\",\"data\":\"test\"}"; + + try { + jsonSerializer.parseObject(json, Object.class, false); + } catch (SecurityException e) { + throw e; + } catch (Exception e) { + assertThat(e).isNotInstanceOf(SecurityException.class); + } + } + + @Test + public void testParseObject_userAllowedPrefix() { + JsonAllowlistManager.getInstance().addUserPrefix("com.mycompany.model."); + + String json = "{\"@type\":\"com.mycompany.model.User\",\"id\":1}"; + + try { + jsonSerializer.parseObject(json, Object.class, false); + } catch (SecurityException e) { + throw e; + } catch (Exception e) { + + assertThat(e).isNotInstanceOf(SecurityException.class); + } + } + + @Test + public void testParseObject_ignoreAutoType_bypasses_check() { + + String json = "{\"@type\":\"com.malicious.EvilClass\",\"command\":\"rm -rf /\"}"; + + try { + jsonSerializer.parseObject(json, Object.class, true); + } catch (SecurityException e) { + throw new AssertionError("Should not throw SecurityException when ignoreAutoType=true", e); + } catch (Exception e) { + + assertThat(e).isNotInstanceOf(SecurityException.class); + } + } + + @Test + public void testParseObject_noAutoType_bypasses_check() { + String json = "{\"name\":\"test\",\"value\":123}"; + + TestObject result = jsonSerializer.parseObject(json, TestObject.class); + + assertThat(result).isNotNull(); + assertThat(result.getName()).isEqualTo("test"); + } + + @Test + public void testParseObject_multipleAutoTypes() { + + String json = "{\"@type\":\"org.apache.seata.common.json.Jackson3AllowlistTest$ContainerClass\"," + + "\"inner\":{\"@type\":\"org.apache.seata.common.json.Jackson3AllowlistTest$AllowedTestClass\",\"name\":\"nested\"}}"; + + ContainerClass result = jsonSerializer.parseObject(json, ContainerClass.class, false); + + assertThat(result).isNotNull(); + } + + @Test + public void testParseObject_multipleAutoTypes_oneNotAllowed() { + + String json = "{\"@type\":\"org.apache.seata.common.json.Jackson3AllowlistTest$ContainerClass\"," + + "\"inner\":{\"@type\":\"com.malicious.EvilClass\",\"name\":\"evil\"}}"; + + assertThatThrownBy(() -> jsonSerializer.parseObject(json, ContainerClass.class, false)) + .isInstanceOf(SecurityException.class) + .hasMessageContaining("com.malicious.EvilClass"); + } + + @Test + public void testParseObject_atTypeInStringValue_notBlocked() { + String json = "{\"@type\":\"org.apache.seata.common.json.Jackson3AllowlistTest$TestObject\"," + + "\"name\":\"the \\\"@type\\\" field is important\",\"value\":123}"; + + TestObject result = jsonSerializer.parseObject(json, TestObject.class, false); + + assertThat(result).isNotNull(); + assertThat(result.getName()).isEqualTo("the \"@type\" field is important"); + } + + @Test + public void testLoadUserAllowlist_thenParse() { + JsonAllowlistManager.getInstance().loadUserAllowlist("com.trusted.model.,com.trusted.dto.SpecificDTO"); + + String json1 = "{\"@type\":\"com.trusted.model.User\",\"id\":1}"; + try { + jsonSerializer.parseObject(json1, Object.class, false); + } catch (SecurityException e) { + throw e; + } catch (Exception e) { + + } + + String json2 = "{\"@type\":\"com.trusted.dto.SpecificDTO\",\"data\":\"test\"}"; + try { + jsonSerializer.parseObject(json2, Object.class, false); + } catch (SecurityException e) { + throw e; + } catch (Exception e) { + + } + + String json3 = "{\"@type\":\"com.untrusted.EvilClass\",\"data\":\"evil\"}"; + assertThatThrownBy(() -> jsonSerializer.parseObject(json3, Object.class, false)) + .isInstanceOf(SecurityException.class); + } + + public static class TestObject { + private String name; + private int value; + + public TestObject() {} + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getValue() { + return value; + } + + public void setValue(int value) { + this.value = value; + } + } + + public static class AllowedTestClass { + private String name; + + public AllowedTestClass() {} + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + } + + public static class ContainerClass { + private AllowedTestClass inner; + + public ContainerClass() {} + + public AllowedTestClass getInner() { + return inner; + } + + public void setInner(AllowedTestClass inner) { + this.inner = inner; + } + } +} diff --git a/jdk17/src/test/java/org/apache/seata/common/json/Jackson3JsonSerializerTest.java b/jdk17/src/test/java/org/apache/seata/common/json/Jackson3JsonSerializerTest.java new file mode 100644 index 00000000000..89c7e6f183a --- /dev/null +++ b/jdk17/src/test/java/org/apache/seata/common/json/Jackson3JsonSerializerTest.java @@ -0,0 +1,350 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.seata.common.json; + +import org.apache.seata.common.exception.JsonParseException; +import org.apache.seata.common.json.impl.Jackson3JsonSerializer; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import tools.jackson.core.type.TypeReference; + +import java.lang.reflect.Type; +import java.util.ArrayList; +import java.util.List; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +public class Jackson3JsonSerializerTest { + + private JsonSerializer jsonSerializer; + + @BeforeEach + void setUp() { + jsonSerializer = JsonSerializerFactory.getSerializer("jackson3"); + } + + @Test + public void testToJSONString_basicObject() { + TestObject obj = new TestObject("test", 123); + String json = jsonSerializer.toJSONString(obj); + + assertThat(json).contains("\"name\":\"test\""); + assertThat(json).contains("\"value\":123"); + } + + @Test + public void testParseObject_basicObject() { + String json = + "{\"@type\":\"org.apache.seata.common.json.Jackson3JsonSerializerTest$TestObject\",\"name\":\"test\",\"value\":123}"; + TestObject obj = jsonSerializer.parseObject(json, TestObject.class); + + assertThat(obj).isNotNull(); + assertThat(obj.getName()).isEqualTo("test"); + assertThat(obj.getValue()).isEqualTo(123); + } + + @Test + public void testToJSONString_and_parseObject() { + TestObject original = new TestObject("school", 456); + String json = jsonSerializer.toJSONString(original); + TestObject restored = jsonSerializer.parseObject(json, TestObject.class); + + assertThat(restored.getName()).isEqualTo(original.getName()); + assertThat(restored.getValue()).isEqualTo(original.getValue()); + } + + @Test + public void testUseAutoType_withType() { + String json = "{\"@type\":\"some.type\",\"name\":\"test\"}"; + boolean hasAutoType = jsonSerializer.useAutoType(json); + assertThat(hasAutoType).isTrue(); + } + + @Test + public void testUseAutoType_withoutType() { + String json = "{\"name\":\"test\"}"; + boolean hasAutoType = jsonSerializer.useAutoType(json); + assertThat(hasAutoType).isFalse(); + } + + @Test + public void testToJSONString_withAutoType() { + TestObject obj = new TestObject("withType", 789); + String jsonWithAutoType = jsonSerializer.toJSONString(obj, false, false); + + assertThat(jsonWithAutoType).isNotNull(); + assertThat(jsonWithAutoType).contains("@type"); + assertThat(jsonWithAutoType).contains("\"name\":\"withType\""); + assertThat(jsonWithAutoType).contains("\"value\":789"); + } + + @Test + public void testToJSONString_singleArg_containsAutoType() { + TestObject obj = new TestObject("singleArg", 321); + String json = jsonSerializer.toJSONString(obj); + + assertThat(json).contains("@type"); + assertThat(json).contains("\"name\":\"singleArg\""); + assertThat(json).contains("\"value\":321"); + } + + @Test + public void testEmptyList_toJSONString_singleArg() { + List emptyList = new ArrayList<>(); + assertThat(jsonSerializer.toJSONString(emptyList)).isEqualTo("[]"); + } + + @Test + public void testParseObject_withAutoType() { + TestObject original = new TestObject("autoTypeTest", 999); + String jsonWithAutoType = jsonSerializer.toJSONString(original, false, false); + + TestObject restored = jsonSerializer.parseObject(jsonWithAutoType, TestObject.class, false); + + assertThat(restored).isNotNull(); + assertThat(restored.getName()).isEqualTo("autoTypeTest"); + assertThat(restored.getValue()).isEqualTo(999); + } + + @Test + public void testEmptyList_serialization() { + List emptyList = new ArrayList<>(); + String json = jsonSerializer.toJSONString(emptyList, false, false); + assertThat(json).isEqualTo("[]"); + } + + @Test + public void testEmptyList_deserialization() { + String json = "[]"; + List list = jsonSerializer.parseObject(json, List.class, false); + assertThat(list).isEmpty(); + } + + @Test + public void testNullInput_toJSONString() { + String json = jsonSerializer.toJSONString(null); + assertThat(json).isEqualTo("null"); + } + + @Test + public void testNullInput_parseObject() { + TestObject obj = jsonSerializer.parseObject(null, TestObject.class); + assertThat(obj).isNull(); + + TestObject obj2 = jsonSerializer.parseObject("{}", null); + assertThat(obj2).isNull(); + } + + @Test + public void testParseObject_invalidJson() { + assertThatThrownBy(() -> jsonSerializer.parseObject("{invalid json}", TestObject.class)) + .isInstanceOf(JsonParseException.class) + .hasMessageContaining("Jackson3 deserialize error"); + } + + @Test + public void testFactoryReturnsCorrectInstance() { + JsonSerializer serializer = JsonSerializerFactory.getSerializer("jackson3"); + assertThat(serializer).isNotNull(); + assertThat(serializer).isInstanceOf(Jackson3JsonSerializer.class); + } + + @Test + public void testToJSONString_prettyPrint() { + TestObject obj = new TestObject("pretty", 789); + String prettyJson = jsonSerializer.toJSONString(obj, true); + + assertThat(prettyJson).contains("\n"); + } + + @Test + public void testParseObject_ignoreAutoType() { + TestObject obj = new TestObject("ignored", 222); + String json = jsonSerializer.toJSONString(obj); + + TestObject restored = jsonSerializer.parseObject(json, TestObject.class, true); + + assertThat(restored).isNotNull(); + assertThat(restored.getName()).isEqualTo("ignored"); + assertThat(restored.getValue()).isEqualTo(222); + } + + @Test + public void testParseObject_nullJson() { + assertThat(jsonSerializer.parseObject(null, TestObject.class, false)).isNull(); + } + + @Test + public void testParseObject_emptyList() { + String json = "[]"; + List list = jsonSerializer.parseObject(json, List.class, false); + assertThat(list).isEmpty(); + } + + @Test + public void testParseObject_withType() { + String json = + "{\"@type\":\"org.apache.seata.common.json.Jackson3JsonSerializerTest$TestObject\",\"name\":\"test\",\"value\":123}"; + Type type = new TypeReference() {}.getType(); + + TestObject obj = jsonSerializer.parseObjectWithType(json, type); + assertThat(obj).isNotNull(); + assertThat(obj.getName()).isEqualTo("test"); + assertThat(obj.getValue()).isEqualTo(123); + + assertThatThrownBy(() -> jsonSerializer.parseObjectWithType("{invalid json}", type)) + .isInstanceOf(JsonParseException.class) + .hasMessageContaining("Jackson3 deserialize error"); + } + + @Test + public void testUseAutoType() { + String jsonWithAutoType = "{\"@type\":\"some.type\",\"name\":\"test\"}"; + assertThat(jsonSerializer.useAutoType(jsonWithAutoType)).isTrue(); + + String jsonWithoutAutoType = "{\"name\":\"test\"}"; + assertThat(jsonSerializer.useAutoType(jsonWithoutAutoType)).isFalse(); + + String jsonWithTypeInValue = "{\"comment\":\"this has @type in it\"}"; + assertThat(jsonSerializer.useAutoType(jsonWithTypeInValue)).isFalse(); + + assertThat(jsonSerializer.useAutoType(null)).isFalse(); + + assertThat(jsonSerializer.useAutoType("")).isFalse(); + } + + @Test + public void testToJSONString_withPrettyPrint() { + TestObject obj = new TestObject("pretty", 789); + + String prettyJson = jsonSerializer.toJSONString(obj, true); + assertThat(prettyJson).contains("\n"); + assertThat(prettyJson).contains("@type"); + + String normalJson = jsonSerializer.toJSONString(obj, false); + assertThat(normalJson).doesNotContain("\n"); + assertThat(normalJson).contains("@type"); + } + + @Test + public void testToJSONString_withIgnoreAutoTypeAndPrettyPrint() { + TestObject obj = new TestObject("noType", 111); + + String jsonIgnorePretty = jsonSerializer.toJSONString(obj, true, true); + assertThat(jsonIgnorePretty).contains("\n"); + assertThat(jsonIgnorePretty).doesNotContain("@type"); + + String jsonIgnoreNormal = jsonSerializer.toJSONString(obj, true, false); + assertThat(jsonIgnoreNormal).doesNotContain("\n"); + assertThat(jsonIgnoreNormal).doesNotContain("@type"); + + String jsonNoIgnorePretty = jsonSerializer.toJSONString(obj, false, true); + assertThat(jsonNoIgnorePretty).contains("\n"); + assertThat(jsonNoIgnorePretty).contains("@type"); + + String jsonNoIgnoreNormal = jsonSerializer.toJSONString(obj, false, false); + assertThat(jsonNoIgnoreNormal).doesNotContain("\n"); + assertThat(jsonNoIgnoreNormal).contains("@type"); + + List emptyList = new ArrayList<>(); + String emptyListJson = jsonSerializer.toJSONString(emptyList, false, false); + assertThat(emptyListJson).isEqualTo("[]"); + } + + @Test + public void testParseObject_withIgnoreAutoType() { + String jsonWithAutoType = jsonSerializer.toJSONString(new TestObject("ignored", 222), false, false); + + TestObject objIgnore = jsonSerializer.parseObject(jsonWithAutoType, TestObject.class, true); + assertThat(objIgnore).isNotNull(); + assertThat(objIgnore.getName()).isEqualTo("ignored"); + assertThat(objIgnore.getValue()).isEqualTo(222); + + TestObject objNoIgnore = jsonSerializer.parseObject(jsonWithAutoType, TestObject.class, false); + assertThat(objNoIgnore).isNotNull(); + assertThat(objNoIgnore.getName()).isEqualTo("ignored"); + assertThat(objNoIgnore.getValue()).isEqualTo(222); + + List emptyList = jsonSerializer.parseObject("[]", List.class, false); + assertThat(emptyList).isEmpty(); + + List emptyListIgnore = jsonSerializer.parseObject("[]", List.class, true); + assertThat(emptyListIgnore).isEmpty(); + + assertThat(jsonSerializer.parseObject(null, TestObject.class, false)).isNull(); + + assertThatThrownBy(() -> jsonSerializer.parseObject("{invalid json}", TestObject.class, false)) + .isInstanceOf(JsonParseException.class) + .hasMessageContaining("Jackson3 deserialize error"); + } + + /** + * The "[]" shortcut in parseObject(String, Class, boolean) must only apply when the + * target type is a Collection. Otherwise it would silently return an empty ArrayList + * cast to T, causing a ClassCastException at the call site. The exact behaviour of the + * underlying library when asked to map "[]" to a POJO may vary (return null, return an + * empty POJO, or throw); the only requirement enforced here is that the result is never + * an ArrayList. + */ + @Test + public void testParseObject_emptyArrayJson_nonCollectionType_doesNotReturnArrayList() { + assertEmptyArrayDoesNotProduceCollection(true); + assertEmptyArrayDoesNotProduceCollection(false); + } + + private void assertEmptyArrayDoesNotProduceCollection(boolean ignoreAutoType) { + Object result; + try { + result = jsonSerializer.parseObject("[]", TestObject.class, ignoreAutoType); + } catch (JsonParseException ignored) { + // throwing is also an acceptable outcome + return; + } + if (result != null) { + assertThat(result).isNotInstanceOf(java.util.Collection.class); + } + } + + public static class TestObject { + private String name; + private int value; + + public TestObject() {} + + public TestObject(String name, int value) { + this.name = name; + this.value = value; + } + + public String getName() { + return name; + } + + public void setName(String name) { + this.name = name; + } + + public int getValue() { + return value; + } + + public void setValue(int value) { + this.value = value; + } + } +} diff --git a/jdk21/pom.xml b/jdk21/pom.xml new file mode 100644 index 00000000000..859b53663f0 --- /dev/null +++ b/jdk21/pom.xml @@ -0,0 +1,52 @@ + + + + + org.apache.seata + seata-parent + ${revision} + + 4.0.0 + seata-jdk21 + jar + seata-jdk21 ${project.version} + JDK 21+ extensions for Seata (virtual thread pool provider) + + + 21 + 21 + + + + + org.apache.seata + seata-common + ${project.version} + + + org.apache.seata + seata-core + ${project.version} + test + + + diff --git a/jdk21/src/main/java/org/apache/seata/common/thread/VirtualScheduledThreadPoolExecutor.java b/jdk21/src/main/java/org/apache/seata/common/thread/VirtualScheduledThreadPoolExecutor.java new file mode 100644 index 00000000000..e7740c93dae --- /dev/null +++ b/jdk21/src/main/java/org/apache/seata/common/thread/VirtualScheduledThreadPoolExecutor.java @@ -0,0 +1,31 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.seata.common.thread; + +import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.ScheduledThreadPoolExecutor; + +/** + * Virtual-thread-backed scheduled thread pool implementation. + */ +public class VirtualScheduledThreadPoolExecutor extends ScheduledThreadPoolExecutor { + + public VirtualScheduledThreadPoolExecutor( + String threadPrefix, int corePoolSize, boolean daemon, RejectedExecutionHandler rejectedHandler) { + super(corePoolSize, VirtualThreadFactoryHelper.newThreadFactory(threadPrefix, daemon), rejectedHandler); + } +} diff --git a/jdk21/src/main/java/org/apache/seata/common/thread/VirtualThreadFactoryHelper.java b/jdk21/src/main/java/org/apache/seata/common/thread/VirtualThreadFactoryHelper.java new file mode 100644 index 00000000000..a0ca608fc8f --- /dev/null +++ b/jdk21/src/main/java/org/apache/seata/common/thread/VirtualThreadFactoryHelper.java @@ -0,0 +1,39 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.seata.common.thread; + +import java.util.concurrent.ThreadFactory; + +/** + * Helper for creating consistently named virtual-thread factories. + */ +final class VirtualThreadFactoryHelper { + + private VirtualThreadFactoryHelper() {} + + static ThreadFactory newThreadFactory(String threadPrefix, boolean daemon) { + if (!daemon) { + throw new IllegalArgumentException( + "Virtual threads are always daemon threads; daemon=false is not supported"); + } + return Thread.ofVirtual().name(normalizePrefix(threadPrefix), 1).factory(); + } + + private static String normalizePrefix(String threadPrefix) { + return threadPrefix.endsWith("-") ? threadPrefix : threadPrefix + "-"; + } +} diff --git a/jdk21/src/main/java/org/apache/seata/common/thread/VirtualThreadPoolExecutor.java b/jdk21/src/main/java/org/apache/seata/common/thread/VirtualThreadPoolExecutor.java new file mode 100644 index 00000000000..e0979228155 --- /dev/null +++ b/jdk21/src/main/java/org/apache/seata/common/thread/VirtualThreadPoolExecutor.java @@ -0,0 +1,47 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.seata.common.thread; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +/** + * Virtual-thread-backed thread pool implementation. + */ +public class VirtualThreadPoolExecutor extends ThreadPoolExecutor { + + public VirtualThreadPoolExecutor( + String threadPrefix, + int corePoolSize, + int maximumPoolSize, + long keepAliveTime, + TimeUnit unit, + BlockingQueue workQueue, + boolean daemon, + RejectedExecutionHandler rejectedHandler) { + super( + corePoolSize, + maximumPoolSize, + keepAliveTime, + unit, + workQueue, + VirtualThreadFactoryHelper.newThreadFactory(threadPrefix, daemon), + rejectedHandler); + } +} diff --git a/jdk21/src/main/java/org/apache/seata/common/thread/VirtualThreadPoolProvider.java b/jdk21/src/main/java/org/apache/seata/common/thread/VirtualThreadPoolProvider.java new file mode 100644 index 00000000000..5a0c2b10ba4 --- /dev/null +++ b/jdk21/src/main/java/org/apache/seata/common/thread/VirtualThreadPoolProvider.java @@ -0,0 +1,53 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.seata.common.thread; + +import org.apache.seata.common.loader.LoadLevel; + +import java.util.concurrent.BlockingQueue; +import java.util.concurrent.RejectedExecutionHandler; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +/** + * JDK 21+ SPI implementation that creates virtual-thread-backed business pools. + * Virtual threads are always daemon threads, so {@code daemon=false} is rejected. + */ +@LoadLevel(name = "virtual", order = Integer.MIN_VALUE) +public class VirtualThreadPoolProvider implements ThreadPoolProvider { + + @Override + public ThreadPoolExecutor newThreadPoolExecutor( + String threadPrefix, + int corePoolSize, + int maximumPoolSize, + long keepAliveTime, + TimeUnit unit, + BlockingQueue workQueue, + boolean daemon, + RejectedExecutionHandler rejectedHandler) { + return new VirtualThreadPoolExecutor( + threadPrefix, corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, daemon, rejectedHandler); + } + + @Override + public ScheduledThreadPoolExecutor newScheduledThreadPoolExecutor( + String threadPrefix, int corePoolSize, boolean daemon, RejectedExecutionHandler rejectedHandler) { + return new VirtualScheduledThreadPoolExecutor(threadPrefix, corePoolSize, daemon, rejectedHandler); + } +} diff --git a/jdk21/src/main/resources/META-INF/services/org.apache.seata.common.thread.ThreadPoolProvider b/jdk21/src/main/resources/META-INF/services/org.apache.seata.common.thread.ThreadPoolProvider new file mode 100644 index 00000000000..602fa94509c --- /dev/null +++ b/jdk21/src/main/resources/META-INF/services/org.apache.seata.common.thread.ThreadPoolProvider @@ -0,0 +1,17 @@ +# +# Licensed to the Apache Software Foundation (ASF) under one or more +# contributor license agreements. See the NOTICE file distributed with +# this work for additional information regarding copyright ownership. +# The ASF licenses this file to You 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. +# +org.apache.seata.common.thread.VirtualThreadPoolProvider diff --git a/jdk21/src/test/java/org/apache/seata/common/thread/VirtualThreadPoolProviderTest.java b/jdk21/src/test/java/org/apache/seata/common/thread/VirtualThreadPoolProviderTest.java new file mode 100644 index 00000000000..b283f246faf --- /dev/null +++ b/jdk21/src/test/java/org/apache/seata/common/thread/VirtualThreadPoolProviderTest.java @@ -0,0 +1,99 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You 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.apache.seata.common.thread; + +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.Test; +import org.junit.jupiter.api.condition.EnabledIfSystemProperty; + +import java.util.concurrent.LinkedBlockingQueue; +import java.util.concurrent.ScheduledThreadPoolExecutor; +import java.util.concurrent.ThreadPoolExecutor; +import java.util.concurrent.TimeUnit; + +import static org.assertj.core.api.Assertions.assertThat; +import static org.assertj.core.api.Assertions.assertThatThrownBy; + +/** + * Tests for loom-backed thread pool providers. + */ +@EnabledIfSystemProperty(named = "java.specification.version", matches = "(21|2[2-9]|[3-9][0-9])") +public class VirtualThreadPoolProviderTest { + + @AfterEach + public void tearDown() { + ThreadPoolRuntimeEnvironment.reset(); + } + + @Test + public void testVirtualThreadPoolExecutorKeepsConfiguredBounds() { + ThreadPoolRuntimeEnvironment.setThreadPoolTypeSupplier(() -> "virtual"); + ThreadPoolRuntimeEnvironment.setJdkFeatureSupplier(() -> 21); + LinkedBlockingQueue workQueue = new LinkedBlockingQueue<>(); + + ThreadPoolExecutor executor = + ThreadPoolExecutorFactory.newThreadPoolExecutor("virtualPool", 1, 2, 60, TimeUnit.SECONDS, workQueue); + try { + Thread thread = executor.getThreadFactory().newThread(() -> {}); + + assertThat(executor).isInstanceOf(VirtualThreadPoolExecutor.class); + assertThat(executor.getMaximumPoolSize()).isEqualTo(2); + assertThat(executor.getKeepAliveTime(TimeUnit.SECONDS)).isEqualTo(60); + assertThat(executor.getQueue()).isSameAs(workQueue); + assertThat(thread.isVirtual()).isTrue(); + assertThat(thread.isDaemon()).isTrue(); + assertThat(thread.getName()).startsWith("virtualPool-"); + } finally { + executor.shutdownNow(); + } + } + + @Test + public void testVirtualScheduledThreadPoolExecutorUsesVirtualThreads() { + ThreadPoolRuntimeEnvironment.setThreadPoolTypeSupplier(() -> "virtual"); + ThreadPoolRuntimeEnvironment.setJdkFeatureSupplier(() -> 21); + + ScheduledThreadPoolExecutor executor = + ThreadPoolExecutorFactory.newScheduledThreadPoolExecutor("virtualSchedule", 1, true); + try { + Thread thread = executor.getThreadFactory().newThread(() -> {}); + + assertThat(executor).isInstanceOf(VirtualScheduledThreadPoolExecutor.class); + assertThat(thread.isVirtual()).isTrue(); + assertThat(thread.isDaemon()).isTrue(); + assertThat(thread.getName()).startsWith("virtualSchedule-"); + } finally { + executor.shutdownNow(); + } + } + + @Test + public void testVirtualThreadPoolsRejectNonDaemonRequests() { + ThreadPoolRuntimeEnvironment.setThreadPoolTypeSupplier(() -> "virtual"); + ThreadPoolRuntimeEnvironment.setJdkFeatureSupplier(() -> 21); + + assertThatThrownBy(() -> ThreadPoolExecutorFactory.newThreadPoolExecutor( + "virtualReject", 1, 1, 0, TimeUnit.MILLISECONDS, new LinkedBlockingQueue(), false)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("always daemon"); + + assertThatThrownBy(() -> + ThreadPoolExecutorFactory.newScheduledThreadPoolExecutor("virtualRejectSchedule", 1, false)) + .isInstanceOf(IllegalArgumentException.class) + .hasMessageContaining("always daemon"); + } +} diff --git a/pom.xml b/pom.xml index 05129717f49..5eef17fa251 100644 --- a/pom.xml +++ b/pom.xml @@ -39,7 +39,6 @@ bom common config - threadpool core compatible @@ -63,7 +62,6 @@ test-suite/test-new-version test-suite/test-old-version test-suite/seata-benchmark-cli - json-common @@ -129,6 +127,16 @@ + + + JDK17Plus + + [17,) + + + jdk17 + + JDK21Plus @@ -136,7 +144,7 @@ [21,) - threadpool-loom + jdk21 diff --git a/rm-datasource/pom.xml b/rm-datasource/pom.xml index c08172c2943..ddcb08734a8 100644 --- a/rm-datasource/pom.xml +++ b/rm-datasource/pom.xml @@ -159,10 +159,5 @@ oscarJDBC8 test - - ${project.groupId} - json-common-core - ${project.version} - diff --git a/saga/pom.xml b/saga/pom.xml index a5919647bf4..37376d671e9 100644 --- a/saga/pom.xml +++ b/saga/pom.xml @@ -48,12 +48,6 @@ ${project.version} - - ${project.groupId} - json-common-core - ${project.version} - - com.alibaba fastjson diff --git a/server/pom.xml b/server/pom.xml index 9de1f407868..851a4585939 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -486,7 +486,7 @@ ${project.groupId} - seata-threadpool-loom + seata-jdk21 ${project.version} diff --git a/spring/seata-spring-autoconfigure/seata-spring-autoconfigure-client/pom.xml b/spring/seata-spring-autoconfigure/seata-spring-autoconfigure-client/pom.xml index f0c5fb6fa3c..998d81f144f 100644 --- a/spring/seata-spring-autoconfigure/seata-spring-autoconfigure-client/pom.xml +++ b/spring/seata-spring-autoconfigure/seata-spring-autoconfigure-client/pom.xml @@ -37,11 +37,6 @@ seata-spring-autoconfigure-core ${project.version} - - org.apache.seata - json-common-core - ${project.version} - javax.annotation javax.annotation-api From f23fb0807e0d44c853df31160c0df0e0404504f8 Mon Sep 17 00:00:00 2001 From: slievrly Date: Sun, 7 Jun 2026 23:29:28 +0800 Subject: [PATCH 2/4] fix: address PR review comments for module refactoring MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Fix hardcoded module name in warn log: "seata-threadpool-virtual" → "seata-jdk21" to match the renamed artifact - Add seata-jdk17 to bom/pom.xml dependency management so downstream builds using seata-bom can consume the Jackson3 extension Co-Authored-By: Claude Opus 4.6 --- bom/pom.xml | 5 +++++ .../seata/common/thread/ThreadPoolExecutorFactory.java | 2 +- 2 files changed, 6 insertions(+), 1 deletion(-) diff --git a/bom/pom.xml b/bom/pom.xml index 2f13833be79..307591fbb01 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -46,6 +46,11 @@ seata-common ${project.version} + + org.apache.seata + seata-jdk17 + ${project.version} + org.apache.seata seata-jdk21 diff --git a/core/src/main/java/org/apache/seata/common/thread/ThreadPoolExecutorFactory.java b/core/src/main/java/org/apache/seata/common/thread/ThreadPoolExecutorFactory.java index e1b3090adf2..21677cfa50b 100644 --- a/core/src/main/java/org/apache/seata/common/thread/ThreadPoolExecutorFactory.java +++ b/core/src/main/java/org/apache/seata/common/thread/ThreadPoolExecutorFactory.java @@ -166,7 +166,7 @@ private static ThreadPoolProvider resolveThreadPoolProvider() { // decision so that operators can correlate the missing-provider startup message // with the actual thread-pool mode that is in effect. LOGGER.warn( - "Virtual thread pool was selected but the virtual-thread SPI provider (seata-threadpool-virtual) " + "Virtual thread pool was selected but the virtual-thread SPI provider (seata-jdk21) " + "is not present on the classpath. Falling back to platform threads."); } return ThreadPoolProviderHolder.PLATFORM_THREAD_POOL_PROVIDER; From e4d479ac9f1e52ce05afa32c8294c2a310e6bc6c Mon Sep 17 00:00:00 2001 From: slievrly Date: Sun, 7 Jun 2026 23:34:16 +0800 Subject: [PATCH 3/4] style: apply spotless formatting Co-Authored-By: Claude Opus 4.6 --- .../seata/common/thread/ThreadPoolExecutorFactory.java | 5 ++--- 1 file changed, 2 insertions(+), 3 deletions(-) diff --git a/core/src/main/java/org/apache/seata/common/thread/ThreadPoolExecutorFactory.java b/core/src/main/java/org/apache/seata/common/thread/ThreadPoolExecutorFactory.java index 21677cfa50b..44e2006b63b 100644 --- a/core/src/main/java/org/apache/seata/common/thread/ThreadPoolExecutorFactory.java +++ b/core/src/main/java/org/apache/seata/common/thread/ThreadPoolExecutorFactory.java @@ -165,9 +165,8 @@ private static ThreadPoolProvider resolveThreadPoolProvider() { // This second warning is intentionally kept to surface the per-request fallback // decision so that operators can correlate the missing-provider startup message // with the actual thread-pool mode that is in effect. - LOGGER.warn( - "Virtual thread pool was selected but the virtual-thread SPI provider (seata-jdk21) " - + "is not present on the classpath. Falling back to platform threads."); + LOGGER.warn("Virtual thread pool was selected but the virtual-thread SPI provider (seata-jdk21) " + + "is not present on the classpath. Falling back to platform threads."); } return ThreadPoolProviderHolder.PLATFORM_THREAD_POOL_PROVIDER; } From 8f232243699b2a4ee0ef18ef99675c93f0c85e27 Mon Sep 17 00:00:00 2001 From: slievrly Date: Mon, 8 Jun 2026 10:14:32 +0800 Subject: [PATCH 4/4] refactor: rename jdk17/jdk21 to spi-jdk17/spi-jdk21 and clean up old modules MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit - Rename directories jdk17 → spi-jdk17, jdk21 → spi-jdk21 - Rename artifactIds seata-jdk17 → seata-spi-jdk17, seata-jdk21 → seata-spi-jdk21 - Delete old modules: threadpool/, threadpool-loom/, json-common/ - Fix @deprecated javadoc references: "json-common-core module" → "seata-common module" - Update warn log module name to seata-spi-jdk21 Co-Authored-By: Claude Opus 4.6 --- bom/pom.xml | 4 +- .../thread/ThreadPoolExecutorFactory.java | 2 +- .../integration/tx/api/json/JsonParser.java | 2 +- .../tx/api/json/JsonParserFactory.java | 2 +- .../tx/api/json/JsonParserWrap.java | 2 +- .../integration/tx/api/util/JsonUtil.java | 2 +- json-common/json-common-core/pom.xml | 67 --- .../common/json/JsonAllowlistManager.java | 301 -------------- .../seata/common/json/JsonSerializer.java | 91 ----- .../common/json/JsonSerializerFactory.java | 60 --- .../apache/seata/common/json/JsonUtil.java | 92 ----- .../json/impl/Fastjson2JsonSerializer.java | 177 -------- .../json/impl/FastjsonJsonSerializer.java | 177 -------- .../common/json/impl/GsonJsonSerializer.java | 116 ------ .../json/impl/JacksonJsonSerializer.java | 211 ---------- ...rg.apache.seata.common.json.JsonSerializer | 20 - .../common/json/Fastjson2AllowlistTest.java | 231 ----------- .../json/Fastjson2JsonSerializerTest.java | 320 --------------- .../common/json/FastjsonAllowlistTest.java | 232 ----------- .../json/FastjsonJsonSerializerTest.java | 312 -------------- .../common/json/GsonJsonSerializerTest.java | 265 ------------ .../common/json/JacksonAllowlistTest.java | 249 ------------ .../json/JacksonJsonSerializerTest.java | 380 ------------------ .../common/json/JsonAllowlistManagerTest.java | 340 ---------------- .../json/JsonSerializerFactoryTest.java | 145 ------- .../seata/common/json/JsonUtilTest.java | 202 ---------- .../src/test/resources/file.conf | 25 -- .../src/test/resources/registry.conf | 34 -- json-common/json-common-jackson3/pom.xml | 53 --- .../json/impl/Jackson3JsonSerializer.java | 185 --------- ...rg.apache.seata.common.json.JsonSerializer | 17 - .../common/json/Jackson3AllowlistTest.java | 243 ----------- .../json/Jackson3JsonSerializerTest.java | 350 ---------------- json-common/pom.xml | 51 --- pom.xml | 4 +- .../saga/statelang/parser/JsonParser.java | 2 +- .../statelang/parser/JsonParserFactory.java | 2 +- .../statelang/parser/impl/FastjsonParser.java | 2 +- .../parser/impl/JacksonJsonParser.java | 2 +- server/pom.xml | 2 +- {jdk17 => spi-jdk17}/pom.xml | 6 +- .../json/impl/Jackson3JsonSerializer.java | 0 ...rg.apache.seata.common.json.JsonSerializer | 0 .../common/json/Jackson3AllowlistTest.java | 0 .../json/Jackson3JsonSerializerTest.java | 0 {jdk21 => spi-jdk21}/pom.xml | 6 +- .../VirtualScheduledThreadPoolExecutor.java | 0 .../thread/VirtualThreadFactoryHelper.java | 0 .../thread/VirtualThreadPoolExecutor.java | 0 .../thread/VirtualThreadPoolProvider.java | 0 ...che.seata.common.thread.ThreadPoolProvider | 0 .../thread/VirtualThreadPoolProviderTest.java | 0 .../seata/rm/tcc/json/FastJsonParser.java | 2 +- .../seata/rm/tcc/json/GsonJsonParser.java | 2 +- .../seata/rm/tcc/json/JacksonJsonParser.java | 2 +- threadpool-loom/pom.xml | 46 --- .../VirtualScheduledThreadPoolExecutor.java | 31 -- .../thread/VirtualThreadFactoryHelper.java | 39 -- .../thread/VirtualThreadPoolExecutor.java | 47 --- .../thread/VirtualThreadPoolProvider.java | 53 --- ...che.seata.common.thread.ThreadPoolProvider | 17 - .../thread/VirtualThreadPoolProviderTest.java | 99 ----- threadpool/pom.xml | 46 --- .../thread/PlatformThreadPoolExecutor.java | 40 -- .../thread/PlatformThreadPoolProvider.java | 59 --- .../thread/ThreadPoolExecutorFactory.java | 195 --------- .../common/thread/ThreadPoolProvider.java | 42 -- .../thread/ThreadPoolProviderOrders.java | 27 -- .../thread/ThreadPoolRuntimeEnvironment.java | 108 ----- .../seata/common/thread/ThreadPoolType.java | 61 --- ...che.seata.common.thread.ThreadPoolProvider | 17 - .../thread/ThreadPoolExecutorFactoryTest.java | 181 --------- 72 files changed, 23 insertions(+), 6077 deletions(-) delete mode 100644 json-common/json-common-core/pom.xml delete mode 100644 json-common/json-common-core/src/main/java/org/apache/seata/common/json/JsonAllowlistManager.java delete mode 100644 json-common/json-common-core/src/main/java/org/apache/seata/common/json/JsonSerializer.java delete mode 100644 json-common/json-common-core/src/main/java/org/apache/seata/common/json/JsonSerializerFactory.java delete mode 100644 json-common/json-common-core/src/main/java/org/apache/seata/common/json/JsonUtil.java delete mode 100644 json-common/json-common-core/src/main/java/org/apache/seata/common/json/impl/Fastjson2JsonSerializer.java delete mode 100644 json-common/json-common-core/src/main/java/org/apache/seata/common/json/impl/FastjsonJsonSerializer.java delete mode 100644 json-common/json-common-core/src/main/java/org/apache/seata/common/json/impl/GsonJsonSerializer.java delete mode 100644 json-common/json-common-core/src/main/java/org/apache/seata/common/json/impl/JacksonJsonSerializer.java delete mode 100644 json-common/json-common-core/src/main/resources/META-INF/services/org.apache.seata.common.json.JsonSerializer delete mode 100644 json-common/json-common-core/src/test/java/org/apache/seata/common/json/Fastjson2AllowlistTest.java delete mode 100644 json-common/json-common-core/src/test/java/org/apache/seata/common/json/Fastjson2JsonSerializerTest.java delete mode 100644 json-common/json-common-core/src/test/java/org/apache/seata/common/json/FastjsonAllowlistTest.java delete mode 100644 json-common/json-common-core/src/test/java/org/apache/seata/common/json/FastjsonJsonSerializerTest.java delete mode 100644 json-common/json-common-core/src/test/java/org/apache/seata/common/json/GsonJsonSerializerTest.java delete mode 100644 json-common/json-common-core/src/test/java/org/apache/seata/common/json/JacksonAllowlistTest.java delete mode 100644 json-common/json-common-core/src/test/java/org/apache/seata/common/json/JacksonJsonSerializerTest.java delete mode 100644 json-common/json-common-core/src/test/java/org/apache/seata/common/json/JsonAllowlistManagerTest.java delete mode 100644 json-common/json-common-core/src/test/java/org/apache/seata/common/json/JsonSerializerFactoryTest.java delete mode 100644 json-common/json-common-core/src/test/java/org/apache/seata/common/json/JsonUtilTest.java delete mode 100644 json-common/json-common-core/src/test/resources/file.conf delete mode 100644 json-common/json-common-core/src/test/resources/registry.conf delete mode 100644 json-common/json-common-jackson3/pom.xml delete mode 100644 json-common/json-common-jackson3/src/main/java/org/apache/seata/common/json/impl/Jackson3JsonSerializer.java delete mode 100644 json-common/json-common-jackson3/src/main/resources/META-INF/services/org.apache.seata.common.json.JsonSerializer delete mode 100644 json-common/json-common-jackson3/src/test/java/org/apache/seata/common/json/Jackson3AllowlistTest.java delete mode 100644 json-common/json-common-jackson3/src/test/java/org/apache/seata/common/json/Jackson3JsonSerializerTest.java delete mode 100644 json-common/pom.xml rename {jdk17 => spi-jdk17}/pom.xml (91%) rename {jdk17 => spi-jdk17}/src/main/java/org/apache/seata/common/json/impl/Jackson3JsonSerializer.java (100%) rename {jdk17 => spi-jdk17}/src/main/resources/META-INF/services/org.apache.seata.common.json.JsonSerializer (100%) rename {jdk17 => spi-jdk17}/src/test/java/org/apache/seata/common/json/Jackson3AllowlistTest.java (100%) rename {jdk17 => spi-jdk17}/src/test/java/org/apache/seata/common/json/Jackson3JsonSerializerTest.java (100%) rename {jdk21 => spi-jdk21}/pom.xml (90%) rename {jdk21 => spi-jdk21}/src/main/java/org/apache/seata/common/thread/VirtualScheduledThreadPoolExecutor.java (100%) rename {jdk21 => spi-jdk21}/src/main/java/org/apache/seata/common/thread/VirtualThreadFactoryHelper.java (100%) rename {jdk21 => spi-jdk21}/src/main/java/org/apache/seata/common/thread/VirtualThreadPoolExecutor.java (100%) rename {jdk21 => spi-jdk21}/src/main/java/org/apache/seata/common/thread/VirtualThreadPoolProvider.java (100%) rename {jdk21 => spi-jdk21}/src/main/resources/META-INF/services/org.apache.seata.common.thread.ThreadPoolProvider (100%) rename {jdk21 => spi-jdk21}/src/test/java/org/apache/seata/common/thread/VirtualThreadPoolProviderTest.java (100%) delete mode 100644 threadpool-loom/pom.xml delete mode 100644 threadpool-loom/src/main/java/org/apache/seata/common/thread/VirtualScheduledThreadPoolExecutor.java delete mode 100644 threadpool-loom/src/main/java/org/apache/seata/common/thread/VirtualThreadFactoryHelper.java delete mode 100644 threadpool-loom/src/main/java/org/apache/seata/common/thread/VirtualThreadPoolExecutor.java delete mode 100644 threadpool-loom/src/main/java/org/apache/seata/common/thread/VirtualThreadPoolProvider.java delete mode 100644 threadpool-loom/src/main/resources/META-INF/services/org.apache.seata.common.thread.ThreadPoolProvider delete mode 100644 threadpool-loom/src/test/java/org/apache/seata/common/thread/VirtualThreadPoolProviderTest.java delete mode 100644 threadpool/pom.xml delete mode 100644 threadpool/src/main/java/org/apache/seata/common/thread/PlatformThreadPoolExecutor.java delete mode 100644 threadpool/src/main/java/org/apache/seata/common/thread/PlatformThreadPoolProvider.java delete mode 100644 threadpool/src/main/java/org/apache/seata/common/thread/ThreadPoolExecutorFactory.java delete mode 100644 threadpool/src/main/java/org/apache/seata/common/thread/ThreadPoolProvider.java delete mode 100644 threadpool/src/main/java/org/apache/seata/common/thread/ThreadPoolProviderOrders.java delete mode 100644 threadpool/src/main/java/org/apache/seata/common/thread/ThreadPoolRuntimeEnvironment.java delete mode 100644 threadpool/src/main/java/org/apache/seata/common/thread/ThreadPoolType.java delete mode 100644 threadpool/src/main/resources/META-INF/services/org.apache.seata.common.thread.ThreadPoolProvider delete mode 100644 threadpool/src/test/java/org/apache/seata/common/thread/ThreadPoolExecutorFactoryTest.java diff --git a/bom/pom.xml b/bom/pom.xml index 307591fbb01..969f8487e99 100644 --- a/bom/pom.xml +++ b/bom/pom.xml @@ -48,12 +48,12 @@ org.apache.seata - seata-jdk17 + seata-spi-jdk17 ${project.version} org.apache.seata - seata-jdk21 + seata-spi-jdk21 ${project.version} diff --git a/core/src/main/java/org/apache/seata/common/thread/ThreadPoolExecutorFactory.java b/core/src/main/java/org/apache/seata/common/thread/ThreadPoolExecutorFactory.java index 44e2006b63b..510c5c1fa36 100644 --- a/core/src/main/java/org/apache/seata/common/thread/ThreadPoolExecutorFactory.java +++ b/core/src/main/java/org/apache/seata/common/thread/ThreadPoolExecutorFactory.java @@ -165,7 +165,7 @@ private static ThreadPoolProvider resolveThreadPoolProvider() { // This second warning is intentionally kept to surface the per-request fallback // decision so that operators can correlate the missing-provider startup message // with the actual thread-pool mode that is in effect. - LOGGER.warn("Virtual thread pool was selected but the virtual-thread SPI provider (seata-jdk21) " + LOGGER.warn("Virtual thread pool was selected but the virtual-thread SPI provider (seata-spi-jdk21) " + "is not present on the classpath. Falling back to platform threads."); } return ThreadPoolProviderHolder.PLATFORM_THREAD_POOL_PROVIDER; diff --git a/integration-tx-api/src/main/java/org/apache/seata/integration/tx/api/json/JsonParser.java b/integration-tx-api/src/main/java/org/apache/seata/integration/tx/api/json/JsonParser.java index 6fe82c5e01c..516672347df 100644 --- a/integration-tx-api/src/main/java/org/apache/seata/integration/tx/api/json/JsonParser.java +++ b/integration-tx-api/src/main/java/org/apache/seata/integration/tx/api/json/JsonParser.java @@ -19,7 +19,7 @@ import java.io.IOException; /** - * @deprecated use {@link org.apache.seata.common.json.JsonSerializer} in json-common-core module instead. + * @deprecated use {@link org.apache.seata.common.json.JsonSerializer} in seata-common module instead. */ @Deprecated public interface JsonParser { diff --git a/integration-tx-api/src/main/java/org/apache/seata/integration/tx/api/json/JsonParserFactory.java b/integration-tx-api/src/main/java/org/apache/seata/integration/tx/api/json/JsonParserFactory.java index c5b38833a53..5f3bec98cc1 100644 --- a/integration-tx-api/src/main/java/org/apache/seata/integration/tx/api/json/JsonParserFactory.java +++ b/integration-tx-api/src/main/java/org/apache/seata/integration/tx/api/json/JsonParserFactory.java @@ -25,7 +25,7 @@ import java.util.concurrent.ConcurrentHashMap; /** - * @deprecated use {@link org.apache.seata.common.json.JsonSerializerFactory} in json-common-core module instead. + * @deprecated use {@link org.apache.seata.common.json.JsonSerializerFactory} in seata-common module instead. */ @Deprecated public class JsonParserFactory { diff --git a/integration-tx-api/src/main/java/org/apache/seata/integration/tx/api/json/JsonParserWrap.java b/integration-tx-api/src/main/java/org/apache/seata/integration/tx/api/json/JsonParserWrap.java index a09b6054001..99bf50b1ace 100644 --- a/integration-tx-api/src/main/java/org/apache/seata/integration/tx/api/json/JsonParserWrap.java +++ b/integration-tx-api/src/main/java/org/apache/seata/integration/tx/api/json/JsonParserWrap.java @@ -19,7 +19,7 @@ import org.apache.seata.common.exception.JsonParseException; /** - * @deprecated use {@link org.apache.seata.common.json.JsonSerializer} in json-common-core module instead. + * @deprecated use {@link org.apache.seata.common.json.JsonSerializer} in seata-common module instead. */ @Deprecated public class JsonParserWrap implements JsonParser { diff --git a/integration-tx-api/src/main/java/org/apache/seata/integration/tx/api/util/JsonUtil.java b/integration-tx-api/src/main/java/org/apache/seata/integration/tx/api/util/JsonUtil.java index 23601b2a1b2..7963cc3e9d3 100644 --- a/integration-tx-api/src/main/java/org/apache/seata/integration/tx/api/util/JsonUtil.java +++ b/integration-tx-api/src/main/java/org/apache/seata/integration/tx/api/util/JsonUtil.java @@ -25,7 +25,7 @@ import java.util.Objects; /** - * @deprecated use {@link org.apache.seata.common.json.JsonUtil} in json-common-core module instead. + * @deprecated use {@link org.apache.seata.common.json.JsonUtil} in seata-common module instead. */ @Deprecated public class JsonUtil { diff --git a/json-common/json-common-core/pom.xml b/json-common/json-common-core/pom.xml deleted file mode 100644 index 9c69f312cd1..00000000000 --- a/json-common/json-common-core/pom.xml +++ /dev/null @@ -1,67 +0,0 @@ - - - - - org.apache.seata - json-common - ${revision} - - 4.0.0 - json-common-core - jar - json-common-core ${project.version} - jsonUtil core for Seata modules - - - - ${project.groupId} - seata-common - ${project.version} - - - ${project.groupId} - seata-core - ${project.version} - - - com.alibaba - fastjson - provided - - - com.alibaba.fastjson2 - fastjson2 - provided - - - com.fasterxml.jackson.core - jackson-databind - provided - - - com.google.code.gson - gson - provided - - - - diff --git a/json-common/json-common-core/src/main/java/org/apache/seata/common/json/JsonAllowlistManager.java b/json-common/json-common-core/src/main/java/org/apache/seata/common/json/JsonAllowlistManager.java deleted file mode 100644 index f40264d88b2..00000000000 --- a/json-common/json-common-core/src/main/java/org/apache/seata/common/json/JsonAllowlistManager.java +++ /dev/null @@ -1,301 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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.apache.seata.common.json; - -import java.util.Map; -import java.util.Set; -import java.util.concurrent.ConcurrentHashMap; - -/** - * JSON deserialization allowlist manager. - */ -public class JsonAllowlistManager { - - private static final JsonAllowlistManager INSTANCE = new JsonAllowlistManager(); - - /** - * Built-in exact match allowlist - */ - private final Set builtinClasses = ConcurrentHashMap.newKeySet(); - - /** - * Built-in prefix match allowlist - */ - private final Set builtinPrefixes = ConcurrentHashMap.newKeySet(); - - /** - * User-defined exact match allowlist - */ - private final Set userClasses = ConcurrentHashMap.newKeySet(); - - /** - * User-defined prefix match allowlist - */ - private final Set userPrefixes = ConcurrentHashMap.newKeySet(); - - /** - * Check result cache (className -> allowed) - */ - private final Map cache = new ConcurrentHashMap<>(); - - private JsonAllowlistManager() { - initBuiltinAllowlist(); - } - - public static JsonAllowlistManager getInstance() { - return INSTANCE; - } - - /** - * Load user allowlist from configuration string - */ - public void loadUserAllowlist(String config) { - userClasses.clear(); - userPrefixes.clear(); - cache.clear(); - if (config == null || config.isEmpty()) { - return; - } - for (String item : config.split(",")) { - String trimmed = item.trim(); - if (trimmed.isEmpty()) { - continue; - } - if (trimmed.endsWith(".")) { - userPrefixes.add(trimmed); - } else { - userClasses.add(trimmed); - } - } - } - - /** - * Add a class to user allowlist programmatically - */ - public void addUserClass(String className) { - if (className != null && !className.isEmpty()) { - userClasses.add(className); - cache.remove(className); - } - } - - /** - * Add a prefix to user allowlist programmatically - */ - public void addUserPrefix(String prefix) { - if (prefix != null && !prefix.isEmpty()) { - userPrefixes.add(prefix); - cache.clear(); - } - } - - /** - * Check if a class is allowed for deserialization - */ - public boolean isAllowed(String className) { - if (className == null) { - return false; - } - return cache.computeIfAbsent(className, this::doCheck); - } - - /** - * Check if a class is allowed, throw SecurityException if not - */ - public void checkClass(String className) { - if (!isAllowed(className)) { - throw new SecurityException("Class not in JSON deserialization allowlist: " + className - + ". Please add it to seata.json.allowlist configuration."); - } - } - - /** - * Clear user allowlist and cache. - */ - public void clearUserAllowlist() { - userClasses.clear(); - userPrefixes.clear(); - cache.clear(); - } - - private boolean doCheck(String className) { - if (isExactOrPrefixAllowed(className)) { - return true; - } - if (isPrimitiveArrayDescriptor(className)) { - return true; - } - String componentClassName = extractArrayComponentClassName(className); - return componentClassName != null && isExactOrPrefixAllowed(componentClassName); - } - - /** - * Check if className is a multi-dimensional primitive array descriptor (e.g. "[[I", "[[Z"). - * Single-dimensional primitive arrays (e.g. "[I") are already in the builtin allowlist. - */ - private boolean isPrimitiveArrayDescriptor(String className) { - if (className == null || !className.startsWith("[[")) { - return false; - } - String stripped = className; - while (stripped.startsWith("[")) { - stripped = stripped.substring(1); - } - return stripped.length() == 1 && PRIMITIVE_DESCRIPTORS.indexOf(stripped.charAt(0)) >= 0; - } - - private boolean isExactOrPrefixAllowed(String className) { - if (builtinClasses.contains(className)) { - return true; - } - for (String prefix : builtinPrefixes) { - if (className.startsWith(prefix)) { - return true; - } - } - if (userClasses.contains(className)) { - return true; - } - for (String prefix : userPrefixes) { - if (className.startsWith(prefix)) { - return true; - } - } - return false; - } - - private static final String PRIMITIVE_DESCRIPTORS = "BCSIJFDZ"; - - private String extractArrayComponentClassName(String className) { - if (className == null || className.isEmpty()) { - return null; - } - - // Canonical name format: e.g. "int[][]", "String[]" - String normalized = className; - while (normalized.endsWith("[]")) { - normalized = normalized.substring(0, normalized.length() - 2); - } - if (!normalized.equals(className)) { - return normalized; - } - - // JVM descriptor format: e.g. "[[I", "[Ljava.lang.String;" - if (!normalized.startsWith("[")) { - return null; - } - while (normalized.startsWith("[")) { - normalized = normalized.substring(1); - } - // Primitive descriptor (e.g. "I", "Z") — handled by isPrimitiveArrayDescriptor - if (normalized.length() == 1) { - return null; - } - // Object descriptor: Lclassname; - if (normalized.startsWith("L") && normalized.endsWith(";")) { - return normalized.substring(1, normalized.length() - 1); - } - return null; - } - - private void initBuiltinAllowlist() { - - builtinClasses.add("java.lang.Boolean"); - builtinClasses.add("java.lang.Byte"); - builtinClasses.add("java.lang.Character"); - builtinClasses.add("java.lang.Short"); - builtinClasses.add("java.lang.Integer"); - builtinClasses.add("java.lang.Long"); - builtinClasses.add("java.lang.Float"); - builtinClasses.add("java.lang.Double"); - builtinClasses.add("java.lang.String"); - builtinClasses.add("java.lang.Number"); - builtinClasses.add("java.math.BigDecimal"); - builtinClasses.add("java.math.BigInteger"); - - // byte[] - builtinClasses.add("[B"); - // char[] - builtinClasses.add("[C"); - // short[] - builtinClasses.add("[S"); - // int[] - builtinClasses.add("[I"); - // long[] - builtinClasses.add("[J"); - // float[] - builtinClasses.add("[F"); - // double[] - builtinClasses.add("[D"); - // boolean[] - builtinClasses.add("[Z"); - builtinClasses.add("[Ljava.lang.String;"); - builtinClasses.add("[Ljava.lang.Object;"); - - builtinClasses.add("java.util.Date"); - builtinClasses.add("java.util.Calendar"); - builtinClasses.add("java.util.GregorianCalendar"); - builtinClasses.add("java.sql.Date"); - builtinClasses.add("java.sql.Time"); - builtinClasses.add("java.sql.Timestamp"); - builtinClasses.add("java.time.LocalDateTime"); - builtinClasses.add("java.time.LocalDate"); - builtinClasses.add("java.time.LocalTime"); - builtinClasses.add("java.time.Instant"); - builtinClasses.add("java.time.Duration"); - builtinClasses.add("java.time.Period"); - builtinClasses.add("java.time.ZonedDateTime"); - builtinClasses.add("java.time.OffsetDateTime"); - builtinClasses.add("java.time.OffsetTime"); - builtinClasses.add("java.time.Year"); - builtinClasses.add("java.time.YearMonth"); - builtinClasses.add("java.time.MonthDay"); - builtinClasses.add("java.time.ZoneId"); - builtinClasses.add("java.time.ZoneOffset"); - - builtinClasses.add("java.util.ArrayList"); - builtinClasses.add("java.util.LinkedList"); - builtinClasses.add("java.util.Vector"); - builtinClasses.add("java.util.Stack"); - builtinClasses.add("java.util.HashSet"); - builtinClasses.add("java.util.LinkedHashSet"); - builtinClasses.add("java.util.TreeSet"); - builtinClasses.add("java.util.HashMap"); - builtinClasses.add("java.util.LinkedHashMap"); - builtinClasses.add("java.util.TreeMap"); - builtinClasses.add("java.util.Hashtable"); - builtinClasses.add("java.util.Properties"); - builtinClasses.add("java.util.concurrent.ConcurrentHashMap"); - builtinClasses.add("java.util.concurrent.CopyOnWriteArrayList"); - builtinClasses.add("java.util.concurrent.CopyOnWriteArraySet"); - builtinClasses.add("java.util.concurrent.ConcurrentSkipListMap"); - builtinClasses.add("java.util.concurrent.ConcurrentSkipListSet"); - - builtinClasses.add("java.util.UUID"); - builtinClasses.add("java.util.Locale"); - builtinClasses.add("java.util.Currency"); - builtinClasses.add("java.util.Optional"); - builtinClasses.add("java.net.URL"); - builtinClasses.add("java.net.URI"); - builtinClasses.add("java.io.File"); - builtinClasses.add("java.nio.file.Path"); - builtinClasses.add("java.util.regex.Pattern"); - - builtinPrefixes.add("org.apache.seata."); - builtinPrefixes.add("io.seata."); - } -} diff --git a/json-common/json-common-core/src/main/java/org/apache/seata/common/json/JsonSerializer.java b/json-common/json-common-core/src/main/java/org/apache/seata/common/json/JsonSerializer.java deleted file mode 100644 index f4a77bbe59f..00000000000 --- a/json-common/json-common-core/src/main/java/org/apache/seata/common/json/JsonSerializer.java +++ /dev/null @@ -1,91 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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.apache.seata.common.json; - -import java.lang.reflect.Type; - -/** - * The json serializer interface. - */ -public interface JsonSerializer { - - /** - * Serializes the specified object into its JSON string representation. - * - * @param object the object to serialize - * @return the JSON string representation of the object - */ - String toJSONString(Object object); - - /** - * Deserializes the specified JSON string into an object of the given class type. - * - * @param text the JSON string to parse - * @param clazz the class of T - * @param the type of the desired object - * @return the deserialized object of type T - */ - T parseObject(String text, Class clazz); - - /** - * Deserializes the specified JSON string into an object of the given type. - * - * @param text the JSON string to parse - * @param type the type to deserialize into - * @param the type of the desired object - * @return the deserialized object of type T - */ - T parseObjectWithType(String text, Type type); - - /** - * Checks whether the given JSON string uses auto type features (such as type information for polymorphic deserialization). - * - * @param json the JSON string to check - * @return true if auto type is used in the JSON, false otherwise - */ - boolean useAutoType(String json); - - /** - * Serializes the specified object into its JSON string representation. - * - * @param o the object to serialize - * @param prettyPrint whether to format the JSON string for readability - * @return the JSON string representation of the object - */ - String toJSONString(Object o, boolean prettyPrint); - - /** - * Serializes the specified object into its JSON string representation, with options to ignore auto type and pretty print. - * - * @param o the object to serialize - * @param ignoreAutoType whether to ignore auto type information during serialization - * @param prettyPrint whether to format the JSON string for readability - * @return the JSON string representation of the object - */ - String toJSONString(Object o, boolean ignoreAutoType, boolean prettyPrint); - - /** - * Deserializes the specified JSON string into an object of the given class type, with an option to ignore auto type information. - * - * @param json the JSON string to parse - * @param type the class of T - * @param ignoreAutoType whether to ignore auto type information during deserialization - * @param the type of the desired object - * @return the deserialized object of type T - */ - T parseObject(String json, Class type, boolean ignoreAutoType); -} diff --git a/json-common/json-common-core/src/main/java/org/apache/seata/common/json/JsonSerializerFactory.java b/json-common/json-common-core/src/main/java/org/apache/seata/common/json/JsonSerializerFactory.java deleted file mode 100644 index 9d65c3de500..00000000000 --- a/json-common/json-common-core/src/main/java/org/apache/seata/common/json/JsonSerializerFactory.java +++ /dev/null @@ -1,60 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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.apache.seata.common.json; - -import org.apache.seata.common.loader.EnhancedServiceLoader; -import org.apache.seata.common.util.CollectionUtils; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.Map; -import java.util.Optional; -import java.util.concurrent.ConcurrentHashMap; - -public class JsonSerializerFactory { - - private static final Logger LOGGER = LoggerFactory.getLogger(JsonSerializerFactory.class); - - private static final String DEFAULT_SERIALIZER = "jackson"; - - private static final String JACKSON3_SERIALIZER = "jackson3"; - - private static final Map INSTANCES = new ConcurrentHashMap<>(); - - private JsonSerializerFactory() {} - - /** - * Get JsonSerializer instance by name. - * - * @param name the serializer name (e.g., "fastjson", "fastjson2", "jackson", "jackson3", "gson") - * @return the JsonSerializer instance - */ - public static JsonSerializer getSerializer(String name) { - final String serializerName = Optional.ofNullable(name).orElse(DEFAULT_SERIALIZER); - return CollectionUtils.computeIfAbsent(INSTANCES, serializerName, key -> { - try { - return EnhancedServiceLoader.load(JsonSerializer.class, key); - } catch (Exception e) { - if (JACKSON3_SERIALIZER.equals(key)) { - LOGGER.warn("Jackson3 serializer is not available (requires JDK 17+), falling back to jackson.", e); - return EnhancedServiceLoader.load(JsonSerializer.class, DEFAULT_SERIALIZER); - } - throw e; - } - }); - } -} diff --git a/json-common/json-common-core/src/main/java/org/apache/seata/common/json/JsonUtil.java b/json-common/json-common-core/src/main/java/org/apache/seata/common/json/JsonUtil.java deleted file mode 100644 index 574edf53073..00000000000 --- a/json-common/json-common-core/src/main/java/org/apache/seata/common/json/JsonUtil.java +++ /dev/null @@ -1,92 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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.apache.seata.common.json; - -import org.apache.seata.common.ConfigurationKeys; -import org.apache.seata.common.Constants; -import org.apache.seata.common.DefaultValues; -import org.apache.seata.common.exception.JsonParseException; -import org.apache.seata.common.util.StringUtils; -import org.apache.seata.config.Configuration; -import org.apache.seata.config.ConfigurationFactory; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.Objects; - -/** - * Unified JSON utility class - */ -public final class JsonUtil { - - private static final Logger LOGGER = LoggerFactory.getLogger(JsonUtil.class); - - private static final String CONFIG_JSON_SERIALIZER_NAME = - resolveJsonSerializerName(ConfigurationFactory.getInstance()); - - private static final JsonSerializer DEFAULT_SERIALIZER = - JsonSerializerFactory.getSerializer(CONFIG_JSON_SERIALIZER_NAME); - - static String resolveJsonSerializerName(Configuration configuration) { - String serializerType = configuration.getConfig(ConfigurationKeys.JSON_SERIALIZER_TYPE); - if (StringUtils.isNotBlank(serializerType)) { - return serializerType; - } - - String deprecatedSerializerType = - configuration.getConfig(ConfigurationKeys.TCC_BUSINESS_ACTION_CONTEXT_JSON_PARSER_NAME); - if (StringUtils.isNotBlank(deprecatedSerializerType)) { - LOGGER.warn( - "The config '{}' is deprecated since 2.7.0 and will be removed in a future version. Please use '{}' instead.", - ConfigurationKeys.TCC_BUSINESS_ACTION_CONTEXT_JSON_PARSER_NAME, - ConfigurationKeys.JSON_SERIALIZER_TYPE); - return deprecatedSerializerType; - } - - return DefaultValues.BUSINESS_ACTION_CONTEXT_JSON_PARSER; - } - - /** - * Serialize the given object to JSON string - * - * @param object the object to serialize - * @return the JSON string representation - * @throws JsonParseException if serialization fails - */ - public static String toJSONString(Object object) { - return DEFAULT_SERIALIZER.toJSONString(object); - } - - /** - * Deserialize the given JSON string to an object of the specified class - * - * @param the type of the object - * @param text the JSON string - * @param clazz the class to deserialize to - * @return the deserialized object - * @throws JsonParseException if deserialization fails - */ - public static T parseObject(String text, Class clazz) { - if (Objects.isNull(text) || Objects.isNull(clazz)) { - return null; - } - String jsonParseName = text.startsWith(Constants.JACKSON_JSON_TEXT_PREFIX) - ? Constants.JACKSON_JSON_PARSER_NAME - : CONFIG_JSON_SERIALIZER_NAME; - return JsonSerializerFactory.getSerializer(jsonParseName).parseObject(text, clazz); - } -} diff --git a/json-common/json-common-core/src/main/java/org/apache/seata/common/json/impl/Fastjson2JsonSerializer.java b/json-common/json-common-core/src/main/java/org/apache/seata/common/json/impl/Fastjson2JsonSerializer.java deleted file mode 100644 index f5599b267c4..00000000000 --- a/json-common/json-common-core/src/main/java/org/apache/seata/common/json/impl/Fastjson2JsonSerializer.java +++ /dev/null @@ -1,177 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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.apache.seata.common.json.impl; - -import com.alibaba.fastjson2.JSON; -import com.alibaba.fastjson2.JSONReader; -import com.alibaba.fastjson2.JSONWriter; -import com.alibaba.fastjson2.filter.ContextAutoTypeBeforeHandler; -import com.alibaba.fastjson2.util.TypeUtils; -import org.apache.seata.common.exception.JsonParseException; -import org.apache.seata.common.json.JsonAllowlistManager; -import org.apache.seata.common.json.JsonSerializer; -import org.apache.seata.common.loader.LoadLevel; - -import java.lang.reflect.Type; -import java.util.regex.Pattern; - -/** - * Fastjson2 implementation of JsonSerializer - */ -@LoadLevel(name = Fastjson2JsonSerializer.NAME) -public class Fastjson2JsonSerializer implements JsonSerializer { - - public static final String NAME = "fastjson2"; - - private static final Pattern AUTOTYPE_PATTERN = Pattern.compile("\"@type\"\\s*:"); - - private static final JSONWriter.Feature[] SERIALIZER_FEATURES = - new JSONWriter.Feature[] {JSONWriter.Feature.WriteClassName}; - - private static final JSONWriter.Feature[] SERIALIZER_FEATURES_PRETTY = - new JSONWriter.Feature[] {JSONWriter.Feature.WriteClassName, JSONWriter.Feature.PrettyFormat}; - - private static final JSONWriter.Feature[] FEATURES_PRETTY = - new JSONWriter.Feature[] {JSONWriter.Feature.PrettyFormat}; - - private static final AllowlistAutoTypeHandler ALLOWLIST_HANDLER = - new AllowlistAutoTypeHandler("org.apache.seata.", "io.seata."); - - @Override - public String toJSONString(Object object) { - try { - return JSON.toJSONString(object, SERIALIZER_FEATURES); - } catch (Exception e) { - throw new JsonParseException("Fastjson2 serialize error", e); - } - } - - @Override - public T parseObject(String text, Class clazz) { - if (text == null || clazz == null) { - return null; - } - try { - return JSON.parseObject(text, clazz); - } catch (Exception e) { - throw new JsonParseException("Fastjson2 deserialize error", e); - } - } - - @Override - public T parseObjectWithType(String text, Type type) { - if (text == null || type == null) { - return null; - } - try { - return JSON.parseObject(text, type, ALLOWLIST_HANDLER, JSONReader.Feature.SupportAutoType); - } catch (SecurityException e) { - throw e; - } catch (Exception e) { - rethrowIfSecurityException(e); - throw new JsonParseException("Fastjson2 deserialize error", e); - } - } - - @Override - public boolean useAutoType(String json) { - return json != null && AUTOTYPE_PATTERN.matcher(json).find(); - } - - @Override - public String toJSONString(Object object, boolean prettyPrint) { - return toJSONString(object, false, prettyPrint); - } - - @Override - public String toJSONString(Object object, boolean ignoreAutoType, boolean prettyPrint) { - try { - if (prettyPrint) { - if (ignoreAutoType) { - return JSON.toJSONString(object, FEATURES_PRETTY); - } else { - return JSON.toJSONString(object, SERIALIZER_FEATURES_PRETTY); - } - } else { - if (ignoreAutoType) { - return JSON.toJSONString(object); - } else { - return JSON.toJSONString(object, SERIALIZER_FEATURES); - } - } - } catch (Exception e) { - throw new JsonParseException("Fastjson2 serialize error", e); - } - } - - @Override - public T parseObject(String text, Class type, boolean ignoreAutoType) { - if (text == null || type == null) { - return null; - } - try { - if ("[]".equals(text) && (java.util.Collection.class.isAssignableFrom(type) || type == Object.class)) { - return (T) new java.util.ArrayList<>(); - } - - if (ignoreAutoType) { - return JSON.parseObject(text, type); - } else { - return JSON.parseObject(text, type, ALLOWLIST_HANDLER, JSONReader.Feature.SupportAutoType); - } - } catch (SecurityException e) { - throw e; - } catch (Exception e) { - rethrowIfSecurityException(e); - throw new JsonParseException("Fastjson2 deserialize error", e); - } - } - - private static void rethrowIfSecurityException(Throwable e) { - Throwable cause = e.getCause(); - while (cause != null) { - if (cause instanceof SecurityException) { - throw (SecurityException) cause; - } - cause = cause.getCause(); - } - } - - /** - * Extends ContextAutoTypeBeforeHandler (like Dubbo) for hash-optimized prefix matching. - * Built-in basic types are included via includeBasic=true, seata prefixes are passed - * to the constructor. User-defined allowlist entries are checked via JsonAllowlistManager fallback. - */ - private static class AllowlistAutoTypeHandler extends ContextAutoTypeBeforeHandler { - - AllowlistAutoTypeHandler(String... acceptNames) { - super(true, acceptNames); - } - - @Override - public Class apply(String typeName, Class expectClass, long features) { - Class clazz = super.apply(typeName, expectClass, features); - if (clazz != null) { - return clazz; - } - - // Throws SecurityException if not allowed - JsonAllowlistManager.getInstance().checkClass(typeName); - return TypeUtils.loadClass(typeName); - } - } -} diff --git a/json-common/json-common-core/src/main/java/org/apache/seata/common/json/impl/FastjsonJsonSerializer.java b/json-common/json-common-core/src/main/java/org/apache/seata/common/json/impl/FastjsonJsonSerializer.java deleted file mode 100644 index d7f09d3db68..00000000000 --- a/json-common/json-common-core/src/main/java/org/apache/seata/common/json/impl/FastjsonJsonSerializer.java +++ /dev/null @@ -1,177 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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.apache.seata.common.json.impl; - -import com.alibaba.fastjson.JSON; -import com.alibaba.fastjson.parser.Feature; -import com.alibaba.fastjson.parser.ParserConfig; -import com.alibaba.fastjson.serializer.SerializerFeature; -import org.apache.seata.common.exception.JsonParseException; -import org.apache.seata.common.json.JsonAllowlistManager; -import org.apache.seata.common.json.JsonSerializer; -import org.apache.seata.common.loader.LoadLevel; - -import java.lang.reflect.Type; -import java.util.regex.Pattern; - -/** - * FastJSON implementation of JsonSerializer - */ -@LoadLevel(name = FastjsonJsonSerializer.NAME) -public class FastjsonJsonSerializer implements JsonSerializer { - - private static final Pattern AUTOTYPE_PATTERN = Pattern.compile("\"@type\"\\s*:"); - - private static final SerializerFeature[] SERIALIZER_FEATURES = new SerializerFeature[] { - SerializerFeature.DisableCircularReferenceDetect, - SerializerFeature.WriteDateUseDateFormat, - SerializerFeature.WriteClassName - }; - - private static final SerializerFeature[] SERIALIZER_FEATURES_PRETTY = new SerializerFeature[] { - SerializerFeature.DisableCircularReferenceDetect, - SerializerFeature.WriteDateUseDateFormat, - SerializerFeature.WriteClassName, - SerializerFeature.PrettyFormat - }; - - private static final SerializerFeature[] FEATURES_PRETTY = new SerializerFeature[] { - SerializerFeature.DisableCircularReferenceDetect, - SerializerFeature.WriteDateUseDateFormat, - SerializerFeature.PrettyFormat - }; - - private static final Feature[] READER_FEATURES_SUPPORT_AUTO_TYPE = - new Feature[] {Feature.SupportAutoType, Feature.OrderedField}; - - private static final Feature[] READER_FEATURES_IGNORE_AUTO_TYPE = - new Feature[] {Feature.IgnoreAutoType, Feature.OrderedField}; - - private static final ParserConfig ALLOWLIST_PARSER_CONFIG = new ParserConfig(); - - static { - ALLOWLIST_PARSER_CONFIG.setAutoTypeSupport(true); - ALLOWLIST_PARSER_CONFIG.addAutoTypeCheckHandler((typeName, expectClass, features) -> { - JsonAllowlistManager.getInstance().checkClass(typeName); - return null; - }); - } - - public static final String NAME = "fastjson"; - - @Override - public String toJSONString(Object object) { - try { - return JSON.toJSONString(object); - } catch (Exception e) { - throw new JsonParseException("FastJSON serialize error", e); - } - } - - @Override - public T parseObject(String text, Class clazz) { - if (text == null || clazz == null) { - return null; - } - try { - return JSON.parseObject(text, clazz); - } catch (Exception e) { - throw new JsonParseException("FastJSON deserialize error", e); - } - } - - @Override - public T parseObjectWithType(String text, Type type) { - if (text == null || type == null) { - return null; - } - try { - return JSON.parseObject(text, type, ALLOWLIST_PARSER_CONFIG, Feature.SupportAutoType, Feature.OrderedField); - } catch (SecurityException e) { - throw e; - } catch (Exception e) { - rethrowIfSecurityException(e); - throw new JsonParseException("FastJSON deserialize error", e); - } - } - - // advanced methods for Saga - @Override - public boolean useAutoType(String json) { - return json != null && AUTOTYPE_PATTERN.matcher(json).find(); - } - - @Override - public String toJSONString(Object object, boolean prettyPrint) { - return toJSONString(object, false, prettyPrint); - } - - @Override - public String toJSONString(Object object, boolean ignoreAutoType, boolean prettyPrint) { - try { - if (prettyPrint) { - if (ignoreAutoType) { - return JSON.toJSONString(object, FEATURES_PRETTY); - } else { - return JSON.toJSONString(object, SERIALIZER_FEATURES_PRETTY); - } - } else { - if (ignoreAutoType) { - return JSON.toJSONString(object); - } else { - return JSON.toJSONString(object, SERIALIZER_FEATURES); - } - } - } catch (Exception e) { - throw new JsonParseException("FastJSON serialize error", e); - } - } - - @Override - public T parseObject(String text, Class type, boolean ignoreAutoType) { - if (text == null || type == null) { - return null; - } - try { - if ("[]".equals(text) && (java.util.Collection.class.isAssignableFrom(type) || type == Object.class)) { - return (T) new java.util.ArrayList<>(); - } - - if (ignoreAutoType) { - return JSON.parseObject(text, type, READER_FEATURES_IGNORE_AUTO_TYPE); - } else { - return JSON.parseObject( - text, type, ALLOWLIST_PARSER_CONFIG, Feature.SupportAutoType, Feature.OrderedField); - } - } catch (SecurityException e) { - throw e; - } catch (Exception e) { - rethrowIfSecurityException(e); - throw new JsonParseException("FastJSON deserialize error", e); - } - } - - private static void rethrowIfSecurityException(Throwable e) { - Throwable cause = e.getCause(); - while (cause != null) { - if (cause instanceof SecurityException) { - throw (SecurityException) cause; - } - cause = cause.getCause(); - } - } -} diff --git a/json-common/json-common-core/src/main/java/org/apache/seata/common/json/impl/GsonJsonSerializer.java b/json-common/json-common-core/src/main/java/org/apache/seata/common/json/impl/GsonJsonSerializer.java deleted file mode 100644 index 453f0ba3990..00000000000 --- a/json-common/json-common-core/src/main/java/org/apache/seata/common/json/impl/GsonJsonSerializer.java +++ /dev/null @@ -1,116 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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.apache.seata.common.json.impl; - -import com.google.gson.Gson; -import com.google.gson.GsonBuilder; -import org.apache.seata.common.exception.JsonParseException; -import org.apache.seata.common.json.JsonSerializer; -import org.apache.seata.common.loader.LoadLevel; - -import java.lang.reflect.Modifier; -import java.lang.reflect.Type; - -/** - * Gson implementation of JsonSerializer - */ -@LoadLevel(name = GsonJsonSerializer.NAME) -public class GsonJsonSerializer implements JsonSerializer { - - private final Gson gson = new GsonBuilder() - .excludeFieldsWithModifiers(Modifier.STATIC, Modifier.TRANSIENT) - .create(); - - private final Gson gsonPretty = new GsonBuilder() - .excludeFieldsWithModifiers(Modifier.STATIC, Modifier.TRANSIENT) - .setPrettyPrinting() - .create(); - - public static final String NAME = "gson"; - - @Override - public String toJSONString(Object object) { - try { - return gson.toJson(object); - } catch (Exception e) { - throw new JsonParseException("Gson serialize error", e); - } - } - - @Override - public T parseObject(String text, Class clazz) { - if (text == null || clazz == null) { - return null; - } - try { - return gson.fromJson(text, clazz); - } catch (Exception e) { - throw new JsonParseException("Gson deserialize error", e); - } - } - - @Override - public T parseObjectWithType(String text, Type type) { - if (text == null || type == null) { - return null; - } - try { - return gson.fromJson(text, type); - } catch (Exception e) { - throw new JsonParseException("Gson deserialize error", e); - } - } - - // advanced methods for Saga - @Override - public boolean useAutoType(String json) { - return false; - } - - @Override - public String toJSONString(Object object, boolean prettyPrint) { - return toJSONString(object, false, prettyPrint); - } - - @Override - public String toJSONString(Object object, boolean ignoreAutoType, boolean prettyPrint) { - try { - if (prettyPrint) { - return gsonPretty.toJson(object); - } else { - return gson.toJson(object); - } - } catch (Exception e) { - throw new JsonParseException("Gson serialize error", e); - } - } - - @Override - public T parseObject(String text, Class type, boolean ignoreAutoType) { - if (text == null || type == null) { - return null; - } - try { - if ("[]".equals(text) && (java.util.Collection.class.isAssignableFrom(type) || type == Object.class)) { - return (T) new java.util.ArrayList<>(); - } - return gson.fromJson(text, type); - } catch (Exception e) { - throw new JsonParseException("Gson deserialize error", e); - } - } -} diff --git a/json-common/json-common-core/src/main/java/org/apache/seata/common/json/impl/JacksonJsonSerializer.java b/json-common/json-common-core/src/main/java/org/apache/seata/common/json/impl/JacksonJsonSerializer.java deleted file mode 100644 index a2df4fd455b..00000000000 --- a/json-common/json-common-core/src/main/java/org/apache/seata/common/json/impl/JacksonJsonSerializer.java +++ /dev/null @@ -1,211 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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.apache.seata.common.json.impl; - -import com.fasterxml.jackson.annotation.JsonInclude; -import com.fasterxml.jackson.annotation.JsonTypeInfo; -import com.fasterxml.jackson.core.JsonProcessingException; -import com.fasterxml.jackson.databind.DeserializationFeature; -import com.fasterxml.jackson.databind.JavaType; -import com.fasterxml.jackson.databind.MapperFeature; -import com.fasterxml.jackson.databind.ObjectMapper; -import com.fasterxml.jackson.databind.ObjectMapper.DefaultTyping; -import com.fasterxml.jackson.databind.cfg.MapperConfig; -import com.fasterxml.jackson.databind.jsontype.PolymorphicTypeValidator; -import org.apache.seata.common.exception.JsonParseException; -import org.apache.seata.common.json.JsonAllowlistManager; -import org.apache.seata.common.json.JsonSerializer; -import org.apache.seata.common.loader.LoadLevel; - -import java.io.IOException; -import java.lang.reflect.Type; -import java.util.ArrayList; -import java.util.List; -import java.util.regex.Pattern; - -/** - * Jackson implementation of JsonSerializer - */ -@LoadLevel(name = JacksonJsonSerializer.NAME) -public class JacksonJsonSerializer implements JsonSerializer { - public static final String NAME = "jackson"; - - private static final Pattern AUTOTYPE_PATTERN = Pattern.compile("\"@type\"\\s*:"); - - private final ObjectMapper defaultObjectMapper; - - private final ObjectMapper objectMapperWithAutoType; - - private final ObjectMapper mapper = new ObjectMapper(); - - public JacksonJsonSerializer() { - this.defaultObjectMapper = new ObjectMapper() - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) - .disableDefaultTyping() - .enable(MapperFeature.PROPAGATE_TRANSIENT_MARKER) - .setSerializationInclusion(JsonInclude.Include.NON_NULL); - - AllowlistTypeValidator validator = new AllowlistTypeValidator(); - ObjectMapper.DefaultTypeResolverBuilder typer = - new ObjectMapper.DefaultTypeResolverBuilder(DefaultTyping.NON_FINAL, validator); - typer.init(JsonTypeInfo.Id.CLASS, null); - typer.inclusion(JsonTypeInfo.As.PROPERTY); - typer.typeProperty("@type"); - - this.objectMapperWithAutoType = new ObjectMapper() - .configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false) - .setDefaultTyping(typer) - .enable(MapperFeature.PROPAGATE_TRANSIENT_MARKER) - .setSerializationInclusion(JsonInclude.Include.NON_NULL); - - this.mapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false); - this.mapper.activateDefaultTyping( - this.mapper.getPolymorphicTypeValidator(), - ObjectMapper.DefaultTyping.NON_FINAL, - JsonTypeInfo.As.PROPERTY); - this.mapper.setConfig(this.mapper.getSerializationConfig().with(MapperFeature.PROPAGATE_TRANSIENT_MARKER)); - this.mapper.setConfig(this.mapper.getDeserializationConfig().with(MapperFeature.PROPAGATE_TRANSIENT_MARKER)); - this.mapper.setSerializationInclusion(JsonInclude.Include.NON_NULL); - } - - @Override - public String toJSONString(Object object) { - try { - return mapper.writeValueAsString(object); - } catch (JsonProcessingException e) { - throw new JsonParseException("Jackson serialize error", e); - } - } - - @Override - public T parseObject(String text, Class clazz) { - if (text == null || clazz == null) { - return null; - } - try { - return defaultObjectMapper.readValue(text, clazz); - } catch (IOException e) { - throw new JsonParseException("Jackson deserialize error", e); - } - } - - @Override - public T parseObjectWithType(String text, Type type) { - if (text == null || type == null) { - return null; - } - try { - return objectMapperWithAutoType.readValue(text, objectMapperWithAutoType.constructType(type)); - } catch (SecurityException e) { - throw e; - } catch (IOException e) { - rethrowIfSecurityException(e); - throw new JsonParseException("Jackson deserialize error", e); - } - } - - // advanced methods for Saga - @Override - public boolean useAutoType(String json) { - return json != null && AUTOTYPE_PATTERN.matcher(json).find(); - } - - @Override - public String toJSONString(Object o, boolean prettyPrint) { - return toJSONString(o, false, prettyPrint); - } - - @Override - public String toJSONString(Object o, boolean ignoreAutoType, boolean prettyPrint) { - try { - if (o instanceof List && ((List) o).isEmpty()) { - return "[]"; - } - if (prettyPrint) { - if (ignoreAutoType) { - return defaultObjectMapper.writerWithDefaultPrettyPrinter().writeValueAsString(o); - } else { - return objectMapperWithAutoType - .writerWithDefaultPrettyPrinter() - .writeValueAsString(o); - } - } else { - if (ignoreAutoType) { - return defaultObjectMapper.writeValueAsString(o); - } else { - return objectMapperWithAutoType.writeValueAsString(o); - } - } - } catch (JsonProcessingException e) { - throw new JsonParseException("Jackson serialize error", e); - } - } - - @Override - public T parseObject(String json, Class type, boolean ignoreAutoType) { - if (json == null || type == null) { - return null; - } - try { - if ("[]".equals(json) && (java.util.Collection.class.isAssignableFrom(type) || type == Object.class)) { - return (T) new ArrayList<>(0); - } - if (ignoreAutoType) { - return defaultObjectMapper.readValue(json, type); - } else { - return objectMapperWithAutoType.readValue(json, type); - } - } catch (SecurityException e) { - throw e; - } catch (IOException e) { - rethrowIfSecurityException(e); - throw new JsonParseException("Jackson deserialize error", e); - } - } - - private static void rethrowIfSecurityException(Throwable e) { - Throwable cause = e.getCause(); - while (cause != null) { - if (cause instanceof SecurityException) { - throw (SecurityException) cause; - } - cause = cause.getCause(); - } - } - - private static class AllowlistTypeValidator extends PolymorphicTypeValidator.Base { - private static final long serialVersionUID = 1L; - - @Override - public Validity validateBaseType(MapperConfig config, JavaType baseType) { - return Validity.INDETERMINATE; - } - - @Override - public Validity validateSubClassName(MapperConfig config, JavaType baseType, String subClassName) { - // Throws SecurityException if not allowed - JsonAllowlistManager.getInstance().checkClass(subClassName); - return Validity.ALLOWED; - } - - @Override - public Validity validateSubType(MapperConfig config, JavaType baseType, JavaType subType) { - JsonAllowlistManager.getInstance().checkClass(subType.getRawClass().getName()); - return Validity.ALLOWED; - } - } -} diff --git a/json-common/json-common-core/src/main/resources/META-INF/services/org.apache.seata.common.json.JsonSerializer b/json-common/json-common-core/src/main/resources/META-INF/services/org.apache.seata.common.json.JsonSerializer deleted file mode 100644 index ed39af9308e..00000000000 --- a/json-common/json-common-core/src/main/resources/META-INF/services/org.apache.seata.common.json.JsonSerializer +++ /dev/null @@ -1,20 +0,0 @@ -# -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You 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. -# -org.apache.seata.common.json.impl.FastjsonJsonSerializer -org.apache.seata.common.json.impl.Fastjson2JsonSerializer -org.apache.seata.common.json.impl.JacksonJsonSerializer -org.apache.seata.common.json.impl.GsonJsonSerializer \ No newline at end of file diff --git a/json-common/json-common-core/src/test/java/org/apache/seata/common/json/Fastjson2AllowlistTest.java b/json-common/json-common-core/src/test/java/org/apache/seata/common/json/Fastjson2AllowlistTest.java deleted file mode 100644 index 16f6e1b9061..00000000000 --- a/json-common/json-common-core/src/test/java/org/apache/seata/common/json/Fastjson2AllowlistTest.java +++ /dev/null @@ -1,231 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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.apache.seata.common.json; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -/** - * Tests for Fastjson2 serializer with allowlist security check - */ -public class Fastjson2AllowlistTest { - - private JsonSerializer jsonSerializer; - - @BeforeEach - void setUp() { - jsonSerializer = JsonSerializerFactory.getSerializer("fastjson2"); - } - - @AfterEach - void tearDown() { - JsonAllowlistManager.getInstance().clearUserAllowlist(); - } - - @Test - public void testParseObject_allowedSeataClass() { - - String json = - "{\"@type\":\"org.apache.seata.common.json.Fastjson2AllowlistTest$AllowedTestClass\",\"name\":\"test\"}"; - - AllowedTestClass result = jsonSerializer.parseObject(json, AllowedTestClass.class, false); - - assertThat(result).isNotNull(); - assertThat(result.getName()).isEqualTo("test"); - } - - @Test - public void testParseObject_allowedJavaClass() { - - String json = "{\"@type\":\"java.util.HashMap\"}"; - - Object result = jsonSerializer.parseObject(json, Object.class, false); - - assertThat(result).isNotNull(); - } - - @Test - public void testParseObject_notAllowedClass() { - - String json = "{\"@type\":\"com.malicious.EvilClass\",\"command\":\"rm -rf /\"}"; - - assertThatThrownBy(() -> jsonSerializer.parseObject(json, Object.class, false)) - .isInstanceOf(SecurityException.class) - .hasMessageContaining("not in JSON deserialization allowlist") - .hasMessageContaining("com.malicious.EvilClass"); - } - - @Test - public void testParseObject_userAllowedClass() { - - JsonAllowlistManager.getInstance().addUserClass("com.example.UserClass"); - - String json = "{\"@type\":\"com.example.UserClass\",\"data\":\"test\"}"; - - Assertions.assertDoesNotThrow(() -> jsonSerializer.parseObject(json, Object.class, false)); - } - - @Test - public void testParseObject_userAllowedPrefix() { - JsonAllowlistManager.getInstance().addUserPrefix("com.mycompany.model."); - - String json = "{\"@type\":\"com.mycompany.model.User\",\"id\":1}"; - - Assertions.assertDoesNotThrow(() -> jsonSerializer.parseObject(json, Object.class, false)); - } - - @Test - public void testParseObject_ignoreAutoType_bypasses_check() { - - String json = "{\"@type\":\"com.malicious.EvilClass\",\"command\":\"rm -rf /\"}"; - - try { - jsonSerializer.parseObject(json, Object.class, true); - } catch (SecurityException e) { - throw new AssertionError("Should not throw SecurityException when ignoreAutoType=true", e); - } catch (Exception e) { - - assertThat(e).isNotInstanceOf(SecurityException.class); - } - } - - @Test - public void testParseObject_noAutoType_bypasses_check() { - - String json = "{\"name\":\"test\",\"value\":123}"; - - TestObject result = jsonSerializer.parseObject(json, TestObject.class, false); - - assertThat(result).isNotNull(); - assertThat(result.getName()).isEqualTo("test"); - } - - @Test - public void testParseObject_multipleAutoTypes() { - - String json = "{\"@type\":\"org.apache.seata.common.json.Fastjson2AllowlistTest$ContainerClass\"," - + "\"inner\":{\"@type\":\"org.apache.seata.common.json.Fastjson2AllowlistTest$AllowedTestClass\",\"name\":\"nested\"}}"; - - ContainerClass result = jsonSerializer.parseObject(json, ContainerClass.class, false); - - assertThat(result).isNotNull(); - } - - @Test - public void testParseObject_multipleAutoTypes_oneNotAllowed() { - - String json = "{\"@type\":\"org.apache.seata.common.json.Fastjson2AllowlistTest$ContainerClass\"," - + "\"inner\":{\"@type\":\"com.malicious.EvilClass\",\"name\":\"evil\"}}"; - - assertThatThrownBy(() -> jsonSerializer.parseObject(json, ContainerClass.class, false)) - .isInstanceOf(SecurityException.class) - .hasMessageContaining("com.malicious.EvilClass"); - } - - @Test - public void testParseObject_atTypeInStringValue_notBlocked() { - String json = "{\"description\":\"the \\\"@type\\\" field is important\",\"name\":\"test\"}"; - - TestObject result = jsonSerializer.parseObject(json, TestObject.class, false); - - assertThat(result).isNotNull(); - assertThat(result.getName()).isEqualTo("test"); - } - - @Test - public void testLoadUserAllowlist_thenParse() { - JsonAllowlistManager.getInstance().loadUserAllowlist("com.trusted.model.,com.trusted.dto.SpecificDTO"); - - String json1 = "{\"@type\":\"com.trusted.model.User\",\"id\":1}"; - try { - jsonSerializer.parseObject(json1, Object.class, false); - } catch (SecurityException e) { - throw e; - } catch (Exception e) { - - } - - String json2 = "{\"@type\":\"com.trusted.dto.SpecificDTO\",\"data\":\"test\"}"; - try { - jsonSerializer.parseObject(json2, Object.class, false); - } catch (SecurityException e) { - throw e; - } catch (Exception e) { - - } - - String json3 = "{\"@type\":\"com.untrusted.EvilClass\",\"data\":\"evil\"}"; - assertThatThrownBy(() -> jsonSerializer.parseObject(json3, Object.class, false)) - .isInstanceOf(SecurityException.class); - } - - public static class TestObject { - private String name; - private int value; - - public TestObject() {} - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public int getValue() { - return value; - } - - public void setValue(int value) { - this.value = value; - } - } - - public static class AllowedTestClass { - private String name; - - public AllowedTestClass() {} - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - } - - public static class ContainerClass { - private AllowedTestClass inner; - - public ContainerClass() {} - - public AllowedTestClass getInner() { - return inner; - } - - public void setInner(AllowedTestClass inner) { - this.inner = inner; - } - } -} diff --git a/json-common/json-common-core/src/test/java/org/apache/seata/common/json/Fastjson2JsonSerializerTest.java b/json-common/json-common-core/src/test/java/org/apache/seata/common/json/Fastjson2JsonSerializerTest.java deleted file mode 100644 index 060cdc0d203..00000000000 --- a/json-common/json-common-core/src/test/java/org/apache/seata/common/json/Fastjson2JsonSerializerTest.java +++ /dev/null @@ -1,320 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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.apache.seata.common.json; - -import com.alibaba.fastjson2.TypeReference; -import org.apache.seata.common.exception.JsonParseException; -import org.apache.seata.common.json.impl.Fastjson2JsonSerializer; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import java.lang.reflect.Type; -import java.util.ArrayList; -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -public class Fastjson2JsonSerializerTest { - - private JsonSerializer jsonSerializer; - - @BeforeEach - void setUp() { - jsonSerializer = JsonSerializerFactory.getSerializer("fastjson2"); - } - - @Test - public void testToJSONString_basicObject() { - TestObject obj = new TestObject("test", 123); - String json = jsonSerializer.toJSONString(obj); - - assertThat(json).contains("\"name\":\"test\""); - assertThat(json).contains("\"value\":123"); - } - - @Test - public void testParseObject_basicObject() { - String json = "{\"name\":\"test\",\"value\":123}"; - TestObject obj = jsonSerializer.parseObject(json, TestObject.class); - - assertThat(obj).isNotNull(); - assertThat(obj.getName()).isEqualTo("test"); - assertThat(obj.getValue()).isEqualTo(123); - } - - @Test - public void testToJSONString_and_parseObject() { - TestObject original = new TestObject("school", 456); - String json = jsonSerializer.toJSONString(original); - TestObject restored = jsonSerializer.parseObject(json, TestObject.class); - - assertThat(restored.getName()).isEqualTo(original.getName()); - assertThat(restored.getValue()).isEqualTo(original.getValue()); - } - - @Test - public void testUseAutoType_withType() { - String json = "{\"@type\":\"some.type\",\"name\":\"test\"}"; - boolean hasAutoType = jsonSerializer.useAutoType(json); - assertThat(hasAutoType).isTrue(); - } - - @Test - public void testUseAutoType_withoutType() { - String json = "{\"name\":\"test\"}"; - boolean hasAutoType = jsonSerializer.useAutoType(json); - assertThat(hasAutoType).isFalse(); - } - - @Test - public void testToJSONString_prettyPrint() { - TestObject obj = new TestObject("pretty", 789); - String prettyJson = jsonSerializer.toJSONString(obj, true); - - assertThat(prettyJson).contains("\n"); - } - - @Test - public void testToJSONString_ignoreAutoType() { - TestObject obj = new TestObject("noType", 111); - String jsonWithoutType = jsonSerializer.toJSONString(obj, true, true); - - assertThat(jsonWithoutType).doesNotContain("@type"); - } - - @Test - public void testParseObject_ignoreAutoType() { - String json = jsonSerializer.toJSONString(new TestObject("ignored", 222)); - - TestObject obj = jsonSerializer.parseObject(json, TestObject.class, true); - - assertThat(obj).isNotNull(); - assertThat(obj.getName()).isEqualTo("ignored"); - assertThat(obj.getValue()).isEqualTo(222); - } - - @Test - public void testEmptyList_serialization() { - List emptyList = new ArrayList<>(); - String json = jsonSerializer.toJSONString(emptyList, false, false); - assertThat(json).isEqualTo("[]"); - } - - @Test - public void testEmptyList_deserialization() { - String json = "[]"; - List list = jsonSerializer.parseObject(json, List.class, false); - assertThat(list).isEmpty(); - } - - @Test - public void testNullInput_toJSONString() { - String json = jsonSerializer.toJSONString(null); - assertThat(json).isEqualTo("null"); - } - - @Test - public void testNullInput_parseObject() { - TestObject obj = jsonSerializer.parseObject(null, TestObject.class); - assertThat(obj).isNull(); - - TestObject obj2 = jsonSerializer.parseObject("{}", null); - assertThat(obj2).isNull(); - } - - @Test - public void testParseObject_invalidJson() { - assertThatThrownBy(() -> jsonSerializer.parseObject("{invalid json}", TestObject.class)) - .isInstanceOf(JsonParseException.class) - .hasMessageContaining("Fastjson2 deserialize error"); - } - - @Test - public void testFactoryReturnsCorrectInstance() { - JsonSerializer serializer = JsonSerializerFactory.getSerializer("fastjson2"); - assertThat(serializer).isNotNull(); - assertThat(serializer).isInstanceOf(Fastjson2JsonSerializer.class); - } - - @Test - public void testToJSONString_withAutoType() { - TestObject obj = new TestObject("withType", 789); - String jsonWithAutoType = jsonSerializer.toJSONString(obj, false, false); - - assertThat(jsonWithAutoType).contains("@type"); - } - - @Test - public void testParseObject_withAutoType() { - TestObject original = new TestObject("autoTypeTest", 999); - String jsonWithAutoType = jsonSerializer.toJSONString(original, false, false); - - TestObject restored = jsonSerializer.parseObject(jsonWithAutoType, TestObject.class, false); - - assertThat(restored).isNotNull(); - assertThat(restored.getName()).isEqualTo("autoTypeTest"); - assertThat(restored.getValue()).isEqualTo(999); - } - - @Test - public void testParseObject_withType() { - String json = "{\"name\":\"test\",\"value\":123}"; - Type type = new TypeReference() {}.getType(); - - TestObject obj = jsonSerializer.parseObjectWithType(json, type); - assertThat(obj).isNotNull(); - assertThat(obj.getName()).isEqualTo("test"); - assertThat(obj.getValue()).isEqualTo(123); - - String listJson = "[{\"name\":\"item1\",\"value\":1},{\"name\":\"item2\",\"value\":2}]"; - Type listType = new TypeReference>() {}.getType(); - - List list = jsonSerializer.parseObjectWithType(listJson, listType); - assertThat(list).hasSize(2); - assertThat(list.get(0).getName()).isEqualTo("item1"); - assertThat(list.get(1).getValue()).isEqualTo(2); - } - - @Test - public void testParseObject_nullJson() { - assertThat(jsonSerializer.parseObject(null, TestObject.class, false)).isNull(); - } - - @Test - public void testParseObject_emptyList() { - String json = "[]"; - List list = jsonSerializer.parseObject(json, List.class, false); - assertThat(list).isEmpty(); - } - - @Test - public void testParseObject_withIgnoreAutoType() { - String jsonWithAutoType = jsonSerializer.toJSONString(new TestObject("ignored", 222)); - - TestObject objIgnore = jsonSerializer.parseObject(jsonWithAutoType, TestObject.class, true); - assertThat(objIgnore).isNotNull(); - assertThat(objIgnore.getName()).isEqualTo("ignored"); - assertThat(objIgnore.getValue()).isEqualTo(222); - - TestObject objNoIgnore = jsonSerializer.parseObject(jsonWithAutoType, TestObject.class, false); - assertThat(objNoIgnore).isNotNull(); - assertThat(objNoIgnore.getName()).isEqualTo("ignored"); - assertThat(objNoIgnore.getValue()).isEqualTo(222); - - List emptyList = jsonSerializer.parseObject("[]", List.class, false); - assertThat(emptyList).isEmpty(); - - assertThat(jsonSerializer.parseObject(null, TestObject.class, false)).isNull(); - - assertThatThrownBy(() -> jsonSerializer.parseObject("{invalid json}", TestObject.class, false)) - .isInstanceOf(JsonParseException.class) - .hasMessageContaining("Fastjson2 deserialize error"); - } - - /** - * The "[]" shortcut in parseObject(String, Class, boolean) must only apply when the - * target type is a Collection. Otherwise it would silently return an empty ArrayList - * cast to T, causing a ClassCastException at the call site. The exact behaviour of the - * underlying library when asked to map "[]" to a POJO may vary (return null, return an - * empty POJO, or throw); the only requirement enforced here is that the result is never - * an ArrayList. - */ - @Test - public void testParseObject_emptyArrayJson_nonCollectionType_doesNotReturnArrayList() { - assertEmptyArrayDoesNotProduceCollection(true); - assertEmptyArrayDoesNotProduceCollection(false); - } - - private void assertEmptyArrayDoesNotProduceCollection(boolean ignoreAutoType) { - Object result; - try { - result = jsonSerializer.parseObject("[]", TestObject.class, ignoreAutoType); - } catch (JsonParseException ignored) { - // throwing is also an acceptable outcome - return; - } - if (result != null) { - assertThat(result).isNotInstanceOf(java.util.Collection.class); - } - } - - @Test - public void testToJSONString_withIgnoreAutoTypeAndPrettyPrint() { - TestObject obj = new TestObject("noType", 111); - - String jsonIgnorePretty = jsonSerializer.toJSONString(obj, true, true); - assertThat(jsonIgnorePretty).contains("\n"); - assertThat(jsonIgnorePretty).doesNotContain("@type"); - - String jsonIgnoreNormal = jsonSerializer.toJSONString(obj, true, false); - assertThat(jsonIgnoreNormal).doesNotContain("\n"); - assertThat(jsonIgnoreNormal).doesNotContain("@type"); - - String jsonNoIgnorePretty = jsonSerializer.toJSONString(obj, false, true); - assertThat(jsonNoIgnorePretty).contains("\n"); - assertThat(jsonNoIgnorePretty).contains("@type"); - - String jsonNoIgnoreNormal = jsonSerializer.toJSONString(obj, false, false); - assertThat(jsonNoIgnoreNormal).doesNotContain("\n"); - assertThat(jsonNoIgnoreNormal).contains("@type"); - } - - @Test - public void testUseAutoType() { - String jsonWithAutoType = "{\"@type\":\"some.type\",\"name\":\"test\"}"; - assertThat(jsonSerializer.useAutoType(jsonWithAutoType)).isTrue(); - - String jsonWithoutAutoType = "{\"name\":\"test\"}"; - assertThat(jsonSerializer.useAutoType(jsonWithoutAutoType)).isFalse(); - - String jsonWithTypeInValue = "{\"comment\":\"this has @type in it\"}"; - assertThat(jsonSerializer.useAutoType(jsonWithTypeInValue)).isFalse(); - - assertThat(jsonSerializer.useAutoType(null)).isFalse(); - - assertThat(jsonSerializer.useAutoType("")).isFalse(); - } - - public static class TestObject { - private String name; - private int value; - - public TestObject() {} - - public TestObject(String name, int value) { - this.name = name; - this.value = value; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public int getValue() { - return value; - } - - public void setValue(int value) { - this.value = value; - } - } -} diff --git a/json-common/json-common-core/src/test/java/org/apache/seata/common/json/FastjsonAllowlistTest.java b/json-common/json-common-core/src/test/java/org/apache/seata/common/json/FastjsonAllowlistTest.java deleted file mode 100644 index fc792c4ad0e..00000000000 --- a/json-common/json-common-core/src/test/java/org/apache/seata/common/json/FastjsonAllowlistTest.java +++ /dev/null @@ -1,232 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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.apache.seata.common.json; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Assertions; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -/** - * Tests for FastJSON serializer with allowlist security check - */ -public class FastjsonAllowlistTest { - - private JsonSerializer jsonSerializer; - - @BeforeEach - void setUp() { - jsonSerializer = JsonSerializerFactory.getSerializer("fastjson"); - } - - @AfterEach - void tearDown() { - JsonAllowlistManager.getInstance().clearUserAllowlist(); - } - - @Test - public void testParseObject_allowedSeataClass() { - - String json = - "{\"@type\":\"org.apache.seata.common.json.FastjsonAllowlistTest$AllowedTestClass\",\"name\":\"test\"}"; - - AllowedTestClass result = jsonSerializer.parseObject(json, AllowedTestClass.class, false); - - assertThat(result).isNotNull(); - assertThat(result.getName()).isEqualTo("test"); - } - - @Test - public void testParseObject_allowedJavaClass() { - - String json = "{\"@type\":\"java.util.HashMap\"}"; - - Object result = jsonSerializer.parseObject(json, Object.class, false); - - assertThat(result).isNotNull(); - } - - @Test - public void testParseObject_notAllowedClass() { - - String json = "{\"@type\":\"com.malicious.EvilClass\",\"command\":\"rm -rf /\"}"; - - assertThatThrownBy(() -> jsonSerializer.parseObject(json, Object.class, false)) - .isInstanceOf(SecurityException.class) - .hasMessageContaining("not in JSON deserialization allowlist") - .hasMessageContaining("com.malicious.EvilClass"); - } - - @Test - public void testParseObject_userAllowedClass() { - - JsonAllowlistManager.getInstance().addUserClass("com.example.UserClass"); - - String json = "{\"@type\":\"com.example.UserClass\",\"data\":\"test\"}"; - - Assertions.assertDoesNotThrow(() -> jsonSerializer.parseObject(json, Object.class, false)); - } - - @Test - public void testParseObject_userAllowedPrefix() { - JsonAllowlistManager.getInstance().addUserPrefix("com.mycompany.model."); - - String json = "{\"@type\":\"com.mycompany.model.User\",\"id\":1}"; - - Assertions.assertDoesNotThrow(() -> jsonSerializer.parseObject(json, Object.class, false)); - } - - @Test - public void testParseObject_ignoreAutoType_bypasses_check() { - - String json = "{\"@type\":\"com.malicious.EvilClass\",\"command\":\"rm -rf /\"}"; - - try { - jsonSerializer.parseObject(json, Object.class, true); - } catch (SecurityException e) { - throw new AssertionError("Should not throw SecurityException when ignoreAutoType=true", e); - } catch (Exception e) { - - assertThat(e).isNotInstanceOf(SecurityException.class); - } - } - - @Test - public void testParseObject_noAutoType_bypasses_check() { - - String json = "{\"name\":\"test\",\"value\":123}"; - - TestObject result = jsonSerializer.parseObject(json, TestObject.class, false); - - assertThat(result).isNotNull(); - assertThat(result.getName()).isEqualTo("test"); - } - - @Test - public void testParseObject_multipleAutoTypes() { - - String json = "{\"@type\":\"org.apache.seata.common.json.FastjsonAllowlistTest$ContainerClass\"," - + "\"inner\":{\"@type\":\"org.apache.seata.common.json.FastjsonAllowlistTest$AllowedTestClass\",\"name\":\"nested\"}}"; - - ContainerClass result = jsonSerializer.parseObject(json, ContainerClass.class, false); - - assertThat(result).isNotNull(); - } - - @Test - public void testParseObject_multipleAutoTypes_oneNotAllowed() { - - String json = "{\"@type\":\"org.apache.seata.common.json.FastjsonAllowlistTest$ContainerClass\"," - + "\"inner\":{\"@type\":\"com.malicious.EvilClass\",\"name\":\"evil\"}}"; - - assertThatThrownBy(() -> jsonSerializer.parseObject(json, ContainerClass.class, false)) - .isInstanceOf(SecurityException.class) - .hasMessageContaining("com.malicious.EvilClass"); - } - - @Test - public void testParseObject_atTypeInStringValue_notBlocked() { - // @type appearing inside a string value should not be treated as AutoType metadata - String json = "{\"description\":\"the \\\"@type\\\" field is important\",\"name\":\"test\"}"; - - TestObject result = jsonSerializer.parseObject(json, TestObject.class, false); - - assertThat(result).isNotNull(); - assertThat(result.getName()).isEqualTo("test"); - } - - @Test - public void testLoadUserAllowlist_thenParse() { - JsonAllowlistManager.getInstance().loadUserAllowlist("com.trusted.model.,com.trusted.dto.SpecificDTO"); - - String json1 = "{\"@type\":\"com.trusted.model.User\",\"id\":1}"; - try { - jsonSerializer.parseObject(json1, Object.class, false); - } catch (SecurityException e) { - throw e; - } catch (Exception e) { - - } - - String json2 = "{\"@type\":\"com.trusted.dto.SpecificDTO\",\"data\":\"test\"}"; - try { - jsonSerializer.parseObject(json2, Object.class, false); - } catch (SecurityException e) { - throw e; - } catch (Exception e) { - - } - - String json3 = "{\"@type\":\"com.untrusted.EvilClass\",\"data\":\"evil\"}"; - assertThatThrownBy(() -> jsonSerializer.parseObject(json3, Object.class, false)) - .isInstanceOf(SecurityException.class); - } - - public static class TestObject { - private String name; - private int value; - - public TestObject() {} - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public int getValue() { - return value; - } - - public void setValue(int value) { - this.value = value; - } - } - - public static class AllowedTestClass { - private String name; - - public AllowedTestClass() {} - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - } - - public static class ContainerClass { - private AllowedTestClass inner; - - public ContainerClass() {} - - public AllowedTestClass getInner() { - return inner; - } - - public void setInner(AllowedTestClass inner) { - this.inner = inner; - } - } -} diff --git a/json-common/json-common-core/src/test/java/org/apache/seata/common/json/FastjsonJsonSerializerTest.java b/json-common/json-common-core/src/test/java/org/apache/seata/common/json/FastjsonJsonSerializerTest.java deleted file mode 100644 index 15f752fb35f..00000000000 --- a/json-common/json-common-core/src/test/java/org/apache/seata/common/json/FastjsonJsonSerializerTest.java +++ /dev/null @@ -1,312 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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.apache.seata.common.json; - -import com.alibaba.fastjson.TypeReference; -import org.apache.seata.common.exception.JsonParseException; -import org.apache.seata.common.json.impl.FastjsonJsonSerializer; -import org.apache.seata.common.json.impl.JacksonJsonSerializer; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import java.lang.reflect.Type; -import java.util.ArrayList; -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -public class FastjsonJsonSerializerTest { - - private JsonSerializer jsonSerializer; - - @BeforeEach - void setUp() { - // Use factory to get FastJSON serializer by name - jsonSerializer = JsonSerializerFactory.getSerializer("fastjson"); - } - - @Test - public void testToJSONString_basicObject() { - TestObject obj = new TestObject("test", 123); - String json = jsonSerializer.toJSONString(obj); - - assertThat(json).contains("\"name\":\"test\""); - assertThat(json).contains("\"value\":123"); - } - - @Test - public void testParseObject_basicObject() { - String json = "{\"name\":\"test\",\"value\":123}"; - TestObject obj = jsonSerializer.parseObject(json, TestObject.class); - - assertThat(obj).isNotNull(); - assertThat(obj.getName()).isEqualTo("test"); - assertThat(obj.getValue()).isEqualTo(123); - } - - @Test - public void testToJSONString_and_parseObject() { - TestObject original = new TestObject("school", 456); - String json = jsonSerializer.toJSONString(original); - TestObject restored = jsonSerializer.parseObject(json, TestObject.class); - - assertThat(restored.getName()).isEqualTo(original.getName()); - assertThat(restored.getValue()).isEqualTo(original.getValue()); - } - - @Test - public void testUseAutoType_withType() { - String json = "{\"@type\":\"some.type\",\"name\":\"test\"}"; - boolean hasAutoType = jsonSerializer.useAutoType(json); - assertThat(hasAutoType).isTrue(); - } - - @Test - public void testUseAutoType_withoutType() { - String json = "{\"name\":\"test\"}"; - boolean hasAutoType = jsonSerializer.useAutoType(json); - assertThat(hasAutoType).isFalse(); - } - - @Test - public void testUseAutoType_typeInValue() { - String json = "{\"comment\":\"this has @type in it\"}"; - boolean hasAutoType = jsonSerializer.useAutoType(json); - assertThat(hasAutoType).isFalse(); - } - - @Test - public void testToJSONString_prettyPrint() { - TestObject obj = new TestObject("pretty", 789); - String prettyJson = jsonSerializer.toJSONString(obj, true); - - assertThat(prettyJson).contains("\n"); - assertThat(prettyJson).contains("\t"); - } - - @Test - public void testToJSONString_ignoreAutoType() { - TestObject obj = new TestObject("noType", 111); - String jsonWithoutType = jsonSerializer.toJSONString(obj, true, true); - - assertThat(jsonWithoutType).doesNotContain("@type"); - } - - @Test - public void testParseObject_ignoreAutoType() { - String json = jsonSerializer.toJSONString(new TestObject("ignored", 222)); - - TestObject obj = jsonSerializer.parseObject(json, TestObject.class, true); - - assertThat(obj).isNotNull(); - assertThat(obj.getName()).isEqualTo("ignored"); - assertThat(obj.getValue()).isEqualTo(222); - } - - @Test - public void testEmptyList_serialization() { - List emptyList = new ArrayList<>(); - String json = jsonSerializer.toJSONString(emptyList, false, false); - assertThat(json).isEqualTo("[]"); - } - - @Test - public void testEmptyList_deserialization() { - String json = "[]"; - List list = jsonSerializer.parseObject(json, List.class, false); - assertThat(list).isEmpty(); - } - - @Test - public void testNullInput_toJSONString() { - String json = jsonSerializer.toJSONString(null); - assertThat(json).isEqualTo("null"); - } - - @Test - public void testNullInput_parseObject() { - TestObject obj = jsonSerializer.parseObject(null, TestObject.class); - assertThat(obj).isNull(); - - TestObject obj2 = jsonSerializer.parseObject("{}", null); - assertThat(obj2).isNull(); - } - - @Test - public void testParseObject_invalidJson() { - assertThatThrownBy(() -> jsonSerializer.parseObject("{invalid json}", TestObject.class)) - .isInstanceOf(JsonParseException.class) - .hasMessageContaining("FastJSON deserialize error"); - } - - @Test - public void testFactoryReturnsCorrectInstance() { - JsonSerializer serializer = JsonSerializerFactory.getSerializer("fastjson"); - assertThat(serializer).isNotNull(); - assertThat(serializer).isInstanceOf(FastjsonJsonSerializer.class); - } - - @Test - public void testFactoryReturnsDefaultInstance() { - JsonSerializer serializer = JsonSerializerFactory.getSerializer(null); - assertThat(serializer).isNotNull(); - assertThat(serializer).isInstanceOf(JacksonJsonSerializer.class); - } - - @Test - public void testToJSONString_emptyList() { - List emptyList = new ArrayList<>(); - String json = jsonSerializer.toJSONString(emptyList, false, false); - assertThat(json).isEqualTo("[]"); - } - - @Test - public void testToJSONString_withAutoType() { - TestObject obj = new TestObject("withType", 789); - String jsonWithAutoType = jsonSerializer.toJSONString(obj, false, false); - - assertThat(jsonWithAutoType).contains("@type"); - } - - @Test - public void testParseObject_nullJson() { - assertThat(jsonSerializer.parseObject(null, TestObject.class, false)).isNull(); - } - - @Test - public void testParseObject_emptyList() { - String json = "[]"; - List list = jsonSerializer.parseObject(json, List.class, false); - assertThat(list).isEmpty(); - } - - @Test - public void testParseObject_withAutoType() { - TestObject original = new TestObject("autoTypeTest", 999); - String jsonWithAutoType = jsonSerializer.toJSONString(original, false, false); - - TestObject restored = jsonSerializer.parseObject(jsonWithAutoType, TestObject.class, false); - - assertThat(restored).isNotNull(); - assertThat(restored.getName()).isEqualTo("autoTypeTest"); - assertThat(restored.getValue()).isEqualTo(999); - } - - @Test - public void testParseObject_withType() { - - String json = "{\"name\":\"test\",\"value\":123}"; - Type type = new TypeReference() {}.getType(); - - TestObject obj = jsonSerializer.parseObjectWithType(json, type); - assertThat(obj).isNotNull(); - assertThat(obj.getName()).isEqualTo("test"); - assertThat(obj.getValue()).isEqualTo(123); - - String listJson = "[{\"name\":\"item1\",\"value\":1},{\"name\":\"item2\",\"value\":2}]"; - Type listType = new TypeReference>() {}.getType(); - - List list = jsonSerializer.parseObjectWithType(listJson, listType); - assertThat(list).hasSize(2); - assertThat(list.get(0).getName()).isEqualTo("item1"); - assertThat(list.get(1).getValue()).isEqualTo(2); - } - - @Test - public void testParseObject_withIgnoreAutoType() { - - String jsonWithAutoType = jsonSerializer.toJSONString(new TestObject("ignored", 222)); - - TestObject objIgnore = jsonSerializer.parseObject(jsonWithAutoType, TestObject.class, true); - assertThat(objIgnore).isNotNull(); - assertThat(objIgnore.getName()).isEqualTo("ignored"); - assertThat(objIgnore.getValue()).isEqualTo(222); - - TestObject objNoIgnore = jsonSerializer.parseObject(jsonWithAutoType, TestObject.class, false); - assertThat(objNoIgnore).isNotNull(); - assertThat(objNoIgnore.getName()).isEqualTo("ignored"); - assertThat(objNoIgnore.getValue()).isEqualTo(222); - - List emptyListResult = jsonSerializer.parseObject("", List.class, false); - assertThat(emptyListResult).isNull(); - - List emptyList = jsonSerializer.parseObject("[]", List.class, false); - assertThat(emptyList).isEmpty(); - - assertThat(jsonSerializer.parseObject(null, TestObject.class, false)).isNull(); - - assertThatThrownBy(() -> jsonSerializer.parseObject("{invalid json}", TestObject.class, false)) - .isInstanceOf(JsonParseException.class) - .hasMessageContaining("FastJSON deserialize error"); - } - - /** - * The "[]" shortcut in parseObject(String, Class, boolean) must only apply when the - * target type is a Collection. Otherwise it would silently return an empty ArrayList - * cast to T, causing a ClassCastException at the call site. The exact behaviour of the - * underlying library when asked to map "[]" to a POJO may vary (return null, return an - * empty POJO, or throw); the only requirement enforced here is that the result is never - * an ArrayList. - */ - @Test - public void testParseObject_emptyArrayJson_nonCollectionType_doesNotReturnArrayList() { - assertEmptyArrayDoesNotProduceCollection(true); - assertEmptyArrayDoesNotProduceCollection(false); - } - - private void assertEmptyArrayDoesNotProduceCollection(boolean ignoreAutoType) { - Object result; - try { - result = jsonSerializer.parseObject("[]", TestObject.class, ignoreAutoType); - } catch (JsonParseException ignored) { - // throwing is also an acceptable outcome - return; - } - if (result != null) { - assertThat(result).isNotInstanceOf(java.util.Collection.class); - } - } - - public static class TestObject { - private String name; - private int value; - - public TestObject() {} - - public TestObject(String name, int value) { - this.name = name; - this.value = value; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public int getValue() { - return value; - } - - public void setValue(int value) { - this.value = value; - } - } -} diff --git a/json-common/json-common-core/src/test/java/org/apache/seata/common/json/GsonJsonSerializerTest.java b/json-common/json-common-core/src/test/java/org/apache/seata/common/json/GsonJsonSerializerTest.java deleted file mode 100644 index ed2419912bd..00000000000 --- a/json-common/json-common-core/src/test/java/org/apache/seata/common/json/GsonJsonSerializerTest.java +++ /dev/null @@ -1,265 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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.apache.seata.common.json; - -import org.apache.seata.common.exception.JsonParseException; -import org.apache.seata.common.json.impl.GsonJsonSerializer; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import java.util.ArrayList; -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -public class GsonJsonSerializerTest { - - private JsonSerializer jsonSerializer; - - @BeforeEach - void setUp() { - // Use factory to get Gson serializer by name - jsonSerializer = JsonSerializerFactory.getSerializer("gson"); - } - - @Test - public void testToJSONString_basicObject() { - TestObject obj = new TestObject("test", 123); - String json = jsonSerializer.toJSONString(obj); - - assertThat(json).contains("\"name\":\"test\""); - assertThat(json).contains("\"value\":123"); - assertThat(json).doesNotContain("@type"); - } - - @Test - public void testParseObject_basicObject() { - String json = "{\"name\":\"test\",\"value\":123}"; - TestObject obj = jsonSerializer.parseObject(json, TestObject.class); - - assertThat(obj).isNotNull(); - assertThat(obj.getName()).isEqualTo("test"); - assertThat(obj.getValue()).isEqualTo(123); - } - - @Test - public void testToJSONString_and_parseObject() { - TestObject original = new TestObject("school", 456); - String json = jsonSerializer.toJSONString(original); - TestObject restored = jsonSerializer.parseObject(json, TestObject.class); - - assertThat(restored.getName()).isEqualTo(original.getName()); - assertThat(restored.getValue()).isEqualTo(original.getValue()); - } - - @Test - public void testUseAutoType_withType() { - String json = "{\"@type\":\"some.type\",\"name\":\"test\"}"; - boolean hasAutoType = jsonSerializer.useAutoType(json); - assertThat(hasAutoType).isFalse(); - } - - @Test - public void testUseAutoType_withoutType() { - String json = "{\"name\":\"test\"}"; - boolean hasAutoType = jsonSerializer.useAutoType(json); - assertThat(hasAutoType).isFalse(); - } - - @Test - public void testToJSONString_prettyPrint() { - TestObject obj = new TestObject("pretty", 789); - String prettyJson = jsonSerializer.toJSONString(obj, true); - - assertThat(prettyJson).contains("\n"); - assertThat(prettyJson).contains(" \""); - } - - @Test - public void testToJSONString_ignoreAutoType() { - TestObject obj = new TestObject("noType", 111); - String jsonWithoutType = jsonSerializer.toJSONString(obj, true, true); - - assertThat(jsonWithoutType).doesNotContain("@type"); - } - - @Test - public void testParseObject_ignoreAutoType() { - TestObject obj = new TestObject("ignored", 222); - String json = jsonSerializer.toJSONString(obj); - - TestObject restored = jsonSerializer.parseObject(json, TestObject.class, true); - - assertThat(restored).isNotNull(); - assertThat(restored.getName()).isEqualTo("ignored"); - assertThat(restored.getValue()).isEqualTo(222); - } - - @Test - public void testEmptyList_serialization() { - List emptyList = new ArrayList<>(); - String json = jsonSerializer.toJSONString(emptyList, false, false); - assertThat(json).isEqualTo("[]"); - } - - @Test - public void testEmptyList_deserialization() { - String json = "[]"; - List list = jsonSerializer.parseObject(json, List.class, false); - assertThat(list).isEmpty(); - } - - @Test - public void testNullInput_toJSONString() { - String json = jsonSerializer.toJSONString(null); - assertThat(json).isEqualTo("null"); - } - - @Test - public void testNullInput_parseObject() { - TestObject obj = jsonSerializer.parseObject(null, TestObject.class); - assertThat(obj).isNull(); - - TestObject obj2 = jsonSerializer.parseObject("{}", null); - assertThat(obj2).isNull(); - } - - @Test - public void testParseObject_invalidJson() { - assertThatThrownBy(() -> jsonSerializer.parseObject("{invalid json}", TestObject.class)) - .isInstanceOf(JsonParseException.class) - .hasMessageContaining("Gson deserialize error"); - } - - @Test - public void testFactoryReturnsCorrectInstance() { - JsonSerializer serializer = JsonSerializerFactory.getSerializer("gson"); - assertThat(serializer).isNotNull(); - assertThat(serializer).isInstanceOf(GsonJsonSerializer.class); - } - - @Test - public void testParseObject_nullText() { - assertThat(jsonSerializer.parseObject(null, String.class)).isNull(); - } - - @Test - public void testToJSONString_emptyList() { - List emptyList = new ArrayList<>(); - String json = jsonSerializer.toJSONString(emptyList, false, false); - assertThat(json).isEqualTo("[]"); - } - - @Test - public void testToJSONString_withAutoType() { - TestObject obj = new TestObject("withType", 789); - String jsonWithAutoType = jsonSerializer.toJSONString(obj, false, false); - - assertThat(jsonWithAutoType).doesNotContain("@type"); - } - - @Test - public void testToJsonString_prettyPrint() { - TestObject obj = new TestObject("pretty", 789); - String prettyJson = jsonSerializer.toJSONString(obj, false, true); - - // Pretty JSON should contain newlines and indentation - assertThat(prettyJson).contains("\n"); - } - - @Test - public void testParseObject_nullJson() { - assertThat(jsonSerializer.parseObject(null, TestObject.class, false)).isNull(); - } - - @Test - public void testParseObject_emptyList() { - String json = "[]"; - List list = jsonSerializer.parseObject(json, List.class, false); - assertThat(list).isEmpty(); - } - - /** - * The "[]" shortcut in parseObject(String, Class, boolean) must only apply when the - * target type is a Collection. Otherwise it would silently return an empty ArrayList - * cast to T, causing a ClassCastException at the call site. The exact behaviour of the - * underlying library when asked to map "[]" to a POJO may vary (return null, return an - * empty POJO, or throw); the only requirement enforced here is that the result is never - * an ArrayList. - */ - @Test - public void testParseObject_emptyArrayJson_nonCollectionType_doesNotReturnArrayList() { - assertEmptyArrayDoesNotProduceCollection(true); - assertEmptyArrayDoesNotProduceCollection(false); - } - - private void assertEmptyArrayDoesNotProduceCollection(boolean ignoreAutoType) { - Object result; - try { - result = jsonSerializer.parseObject("[]", TestObject.class, ignoreAutoType); - } catch (JsonParseException ignored) { - // throwing is also an acceptable outcome - return; - } - if (result != null) { - assertThat(result).isNotInstanceOf(java.util.Collection.class); - } - } - - @Test - public void testParseObject_withAutoType() { - TestObject original = new TestObject("autoTypeTest", 999); - String jsonWithAutoType = jsonSerializer.toJSONString(original, false, false); - - TestObject restored = jsonSerializer.parseObject(jsonWithAutoType, TestObject.class, false); - - assertThat(restored).isNotNull(); - assertThat(restored.getName()).isEqualTo("autoTypeTest"); - assertThat(restored.getValue()).isEqualTo(999); - } - - public static class TestObject { - private String name; - private int value; - - public TestObject() { - // Default constructor required for Gson - } - - public TestObject(String name, int value) { - this.name = name; - this.value = value; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public int getValue() { - return value; - } - - public void setValue(int value) { - this.value = value; - } - } -} diff --git a/json-common/json-common-core/src/test/java/org/apache/seata/common/json/JacksonAllowlistTest.java b/json-common/json-common-core/src/test/java/org/apache/seata/common/json/JacksonAllowlistTest.java deleted file mode 100644 index 522b301a08c..00000000000 --- a/json-common/json-common-core/src/test/java/org/apache/seata/common/json/JacksonAllowlistTest.java +++ /dev/null @@ -1,249 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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.apache.seata.common.json; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -/** - * Tests for Jackson serializer with allowlist security check - */ -public class JacksonAllowlistTest { - - private JsonSerializer jsonSerializer; - - @BeforeEach - void setUp() { - jsonSerializer = JsonSerializerFactory.getSerializer("jackson"); - } - - @AfterEach - void tearDown() { - JsonAllowlistManager.getInstance().clearUserAllowlist(); - } - - @Test - public void testParseObject_allowedSeataClass() { - - String json = - "{\"@type\":\"org.apache.seata.common.json.JacksonAllowlistTest$AllowedTestClass\",\"name\":\"test\"}"; - - AllowedTestClass result = jsonSerializer.parseObject(json, AllowedTestClass.class, false); - - assertThat(result).isNotNull(); - assertThat(result.getName()).isEqualTo("test"); - } - - @Test - public void testParseObject_allowedJavaClass() { - - String json = "{\"@type\":\"java.util.HashMap\"}"; - - Object result = jsonSerializer.parseObject(json, Object.class, false); - - assertThat(result).isNotNull(); - } - - @Test - public void testParseObject_notAllowedClass() { - - String json = "{\"@type\":\"com.malicious.EvilClass\",\"command\":\"rm -rf /\"}"; - - assertThatThrownBy(() -> jsonSerializer.parseObject(json, Object.class, false)) - .isInstanceOf(SecurityException.class) - .hasMessageContaining("not in JSON deserialization allowlist") - .hasMessageContaining("com.malicious.EvilClass"); - } - - @Test - public void testParseObject_userAllowedClass() { - - JsonAllowlistManager.getInstance().addUserClass("com.example.UserClass"); - - String json = "{\"@type\":\"com.example.UserClass\",\"data\":\"test\"}"; - - try { - jsonSerializer.parseObject(json, Object.class, false); - } catch (SecurityException e) { - throw e; - } catch (Exception e) { - assertThat(e).isNotInstanceOf(SecurityException.class); - } - } - - @Test - public void testParseObject_userAllowedPrefix() { - JsonAllowlistManager.getInstance().addUserPrefix("com.mycompany.model."); - - String json = "{\"@type\":\"com.mycompany.model.User\",\"id\":1}"; - - try { - jsonSerializer.parseObject(json, Object.class, false); - } catch (SecurityException e) { - throw e; - } catch (Exception e) { - - assertThat(e).isNotInstanceOf(SecurityException.class); - } - } - - @Test - public void testParseObject_ignoreAutoType_bypasses_check() { - - String json = "{\"@type\":\"com.malicious.EvilClass\",\"command\":\"rm -rf /\"}"; - - try { - jsonSerializer.parseObject(json, Object.class, true); - } catch (SecurityException e) { - throw new AssertionError("Should not throw SecurityException when ignoreAutoType=true", e); - } catch (Exception e) { - - assertThat(e).isNotInstanceOf(SecurityException.class); - } - } - - @Test - public void testParseObject_noAutoType_bypasses_check() { - // Jackson's objectMapperWithAutoType requires @type for non-final types, - // so use the 2-arg parseObject (defaultObjectMapper, no AutoType) to verify - // that normal JSON without @type does not trigger any SecurityException. - String json = "{\"name\":\"test\",\"value\":123}"; - - TestObject result = jsonSerializer.parseObject(json, TestObject.class); - - assertThat(result).isNotNull(); - assertThat(result.getName()).isEqualTo("test"); - } - - @Test - public void testParseObject_multipleAutoTypes() { - - String json = "{\"@type\":\"org.apache.seata.common.json.JacksonAllowlistTest$ContainerClass\"," - + "\"inner\":{\"@type\":\"org.apache.seata.common.json.JacksonAllowlistTest$AllowedTestClass\",\"name\":\"nested\"}}"; - - ContainerClass result = jsonSerializer.parseObject(json, ContainerClass.class, false); - - assertThat(result).isNotNull(); - } - - @Test - public void testParseObject_multipleAutoTypes_oneNotAllowed() { - - String json = "{\"@type\":\"org.apache.seata.common.json.JacksonAllowlistTest$ContainerClass\"," - + "\"inner\":{\"@type\":\"com.malicious.EvilClass\",\"name\":\"evil\"}}"; - - assertThatThrownBy(() -> jsonSerializer.parseObject(json, ContainerClass.class, false)) - .isInstanceOf(SecurityException.class) - .hasMessageContaining("com.malicious.EvilClass"); - } - - @Test - public void testParseObject_atTypeInStringValue_notBlocked() { - // @type appearing inside a string value should not be treated as AutoType metadata. - // The JSON has a real @type for the root object (required by Jackson's DefaultTyping), - // and a fake @type inside a string value which should not be flagged. - String json = "{\"@type\":\"org.apache.seata.common.json.JacksonAllowlistTest$TestObject\"," - + "\"name\":\"the \\\"@type\\\" field is important\",\"value\":123}"; - - TestObject result = jsonSerializer.parseObject(json, TestObject.class, false); - - assertThat(result).isNotNull(); - assertThat(result.getName()).isEqualTo("the \"@type\" field is important"); - } - - @Test - public void testLoadUserAllowlist_thenParse() { - JsonAllowlistManager.getInstance().loadUserAllowlist("com.trusted.model.,com.trusted.dto.SpecificDTO"); - - String json1 = "{\"@type\":\"com.trusted.model.User\",\"id\":1}"; - try { - jsonSerializer.parseObject(json1, Object.class, false); - } catch (SecurityException e) { - throw e; - } catch (Exception e) { - - } - - String json2 = "{\"@type\":\"com.trusted.dto.SpecificDTO\",\"data\":\"test\"}"; - try { - jsonSerializer.parseObject(json2, Object.class, false); - } catch (SecurityException e) { - throw e; - } catch (Exception e) { - - } - - String json3 = "{\"@type\":\"com.untrusted.EvilClass\",\"data\":\"evil\"}"; - assertThatThrownBy(() -> jsonSerializer.parseObject(json3, Object.class, false)) - .isInstanceOf(SecurityException.class); - } - - public static class TestObject { - private String name; - private int value; - - public TestObject() {} - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public int getValue() { - return value; - } - - public void setValue(int value) { - this.value = value; - } - } - - public static class AllowedTestClass { - private String name; - - public AllowedTestClass() {} - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - } - - public static class ContainerClass { - private AllowedTestClass inner; - - public ContainerClass() {} - - public AllowedTestClass getInner() { - return inner; - } - - public void setInner(AllowedTestClass inner) { - this.inner = inner; - } - } -} diff --git a/json-common/json-common-core/src/test/java/org/apache/seata/common/json/JacksonJsonSerializerTest.java b/json-common/json-common-core/src/test/java/org/apache/seata/common/json/JacksonJsonSerializerTest.java deleted file mode 100644 index 1433fb0ca2d..00000000000 --- a/json-common/json-common-core/src/test/java/org/apache/seata/common/json/JacksonJsonSerializerTest.java +++ /dev/null @@ -1,380 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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.apache.seata.common.json; - -import com.alibaba.fastjson.TypeReference; -import org.apache.seata.common.exception.JsonParseException; -import org.apache.seata.common.json.impl.JacksonJsonSerializer; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import java.lang.reflect.Type; -import java.util.ArrayList; -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -public class JacksonJsonSerializerTest { - - private JsonSerializer jsonSerializer; - - @BeforeEach - void setUp() { - // Use factory to get Jackson serializer by name - jsonSerializer = JsonSerializerFactory.getSerializer("jackson"); - } - - @Test - public void testToJSONString_basicObject() { - TestObject obj = new TestObject("test", 123); - String json = jsonSerializer.toJSONString(obj); - - assertThat(json).contains("\"name\":\"test\""); - assertThat(json).contains("\"value\":123"); - } - - @Test - public void testParseObject_basicObject() { - String json = - "{\"@class\":\"org.apache.seata.common.json.JacksonJsonSerializerTest$TestObject\",\"name\":\"test\",\"value\":123}"; - TestObject obj = jsonSerializer.parseObject(json, TestObject.class); - - assertThat(obj).isNotNull(); - assertThat(obj.getName()).isEqualTo("test"); - assertThat(obj.getValue()).isEqualTo(123); - } - - @Test - public void testToJSONString_and_parseObject() { - TestObject original = new TestObject("school", 456); - String json = jsonSerializer.toJSONString(original); - TestObject restored = jsonSerializer.parseObject(json, TestObject.class); - - assertThat(restored.getName()).isEqualTo(original.getName()); - assertThat(restored.getValue()).isEqualTo(original.getValue()); - } - - @Test - public void testUseAutoType_withType() { - String json = "{\"@type\":\"some.type\",\"name\":\"test\"}"; - boolean hasAutoType = jsonSerializer.useAutoType(json); - assertThat(hasAutoType).isTrue(); - } - - @Test - public void testUseAutoType_withoutType() { - String json = "{\"name\":\"test\"}"; - boolean hasAutoType = jsonSerializer.useAutoType(json); - assertThat(hasAutoType).isFalse(); - } - - @Test - public void testUseAutoType_typeInValue() { - String json = "{\"comment\":\"this has @type in it\"}"; - boolean hasAutoType = jsonSerializer.useAutoType(json); - assertThat(hasAutoType).isFalse(); - } - - @Test - public void testToJSONString_withAutoType() { - TestObject obj = new TestObject("withType", 789); - String jsonWithAutoType = jsonSerializer.toJSONString(obj, false, false); - - assertThat(jsonWithAutoType).isNotNull(); - assertThat(jsonWithAutoType).contains("@type"); - assertThat(jsonWithAutoType).contains("\"name\":\"withType\""); - assertThat(jsonWithAutoType).contains("\"value\":789"); - } - - @Test - public void testParseObject_withAutoType() { - TestObject original = new TestObject("autoTypeTest", 999); - // Serialize with autoType enabled - String jsonWithAutoType = jsonSerializer.toJSONString(original, false, false); - - // Deserialize with autoType enabled - TestObject restored = jsonSerializer.parseObject(jsonWithAutoType, TestObject.class, false); - - assertThat(restored).isNotNull(); - assertThat(restored.getName()).isEqualTo("autoTypeTest"); - assertThat(restored.getValue()).isEqualTo(999); - } - - @Test - public void testEmptyList_serialization() { - List emptyList = new ArrayList<>(); - String json = jsonSerializer.toJSONString(emptyList, false, false); - assertThat(json).isEqualTo("[]"); - } - - @Test - public void testEmptyList_deserialization() { - String json = "[]"; - List list = jsonSerializer.parseObject(json, List.class, false); - assertThat(list).isEmpty(); - } - - @Test - public void testNullInput_toJSONString() { - String json = jsonSerializer.toJSONString(null); - assertThat(json).isEqualTo("null"); - } - - @Test - public void testNullInput_parseObject() { - TestObject obj = jsonSerializer.parseObject(null, TestObject.class); - assertThat(obj).isNull(); - - TestObject obj2 = jsonSerializer.parseObject("{}", null); - assertThat(obj2).isNull(); - } - - @Test - public void testParseObject_invalidJson() { - assertThatThrownBy(() -> jsonSerializer.parseObject("{invalid json}", TestObject.class)) - .isInstanceOf(JsonParseException.class) - .hasMessageContaining("Jackson deserialize error"); - } - - @Test - public void testFactoryReturnsCorrectInstance() { - JsonSerializer serializer = JsonSerializerFactory.getSerializer("jackson"); - assertThat(serializer).isNotNull(); - assertThat(serializer).isInstanceOf(JacksonJsonSerializer.class); - } - - @Test - public void testFactoryReturnsDefaultInstance() { - JsonSerializer serializer = JsonSerializerFactory.getSerializer(null); - assertThat(serializer).isNotNull(); - } - - @Test - public void testToJSONString_throwsException() { - Object unserializable = new Object() { - private final java.io.InputStream stream = System.in; - }; - - assertThatThrownBy(() -> jsonSerializer.toJSONString(unserializable)) - .isInstanceOf(JsonParseException.class) - .hasMessageContaining("Jackson serialize error"); - } - - @Test - public void testParseObject_nullText() { - assertThat(jsonSerializer.parseObject(null, String.class)).isNull(); - } - - @Test - public void testToJSONString_prettyPrint() { - TestObject obj = new TestObject("pretty", 789); - String prettyJson = jsonSerializer.toJSONString(obj, true); - - // Pretty JSON should contain newlines and indentation - assertThat(prettyJson).contains("\n"); - } - - @Test - public void testParseObject_nullJson() { - assertThat(jsonSerializer.parseObject(null, TestObject.class, false)).isNull(); - } - - @Test - public void testParseObject_emptyList() { - String json = "[]"; - List list = jsonSerializer.parseObject(json, List.class, false); - assertThat(list).isEmpty(); - } - - @Test - public void testParseObject_ignoreAutoType() { - TestObject obj = new TestObject("ignored", 222); - String json = jsonSerializer.toJSONString(obj); - - TestObject restored = jsonSerializer.parseObject(json, TestObject.class, true); - - assertThat(restored).isNotNull(); - assertThat(restored.getName()).isEqualTo("ignored"); - assertThat(restored.getValue()).isEqualTo(222); - } - - @Test - public void testParseObject_withType() { - String json = - "{\"@type\":\"org.apache.seata.common.json.JacksonJsonSerializerTest$TestObject\",\"name\":\"test\",\"value\":123}"; - Type type = new TypeReference() {}.getType(); - - TestObject obj = jsonSerializer.parseObjectWithType(json, type); - assertThat(obj).isNotNull(); - assertThat(obj.getName()).isEqualTo("test"); - assertThat(obj.getValue()).isEqualTo(123); - - assertThatThrownBy(() -> jsonSerializer.parseObjectWithType("{invalid json}", type)) - .isInstanceOf(JsonParseException.class) - .hasMessageContaining("Jackson deserialize error"); - } - - @Test - public void testUseAutoType() { - String jsonWithAutoType = "{\"@type\":\"some.type\",\"name\":\"test\"}"; - boolean hasAutoType = jsonSerializer.useAutoType(jsonWithAutoType); - assertThat(hasAutoType).isTrue(); - - String jsonWithoutAutoType = "{\"name\":\"test\"}"; - boolean noAutoType = jsonSerializer.useAutoType(jsonWithoutAutoType); - assertThat(noAutoType).isFalse(); - - String jsonWithTypeInValue = "{\"comment\":\"this has @type in it\"}"; - boolean typeInValue = jsonSerializer.useAutoType(jsonWithTypeInValue); - assertThat(typeInValue).isFalse(); - - boolean nullAutoType = jsonSerializer.useAutoType(null); - assertThat(nullAutoType).isFalse(); - - boolean emptyAutoType = jsonSerializer.useAutoType(""); - assertThat(emptyAutoType).isFalse(); - } - - @Test - public void testToJSONString_withPrettyPrint() { - TestObject obj = new TestObject("pretty", 789); - - String prettyJson = jsonSerializer.toJSONString(obj, true); - assertThat(prettyJson).contains("\n"); - assertThat(prettyJson).contains("@type"); - - String normalJson = jsonSerializer.toJSONString(obj, false); - assertThat(normalJson).doesNotContain("\n"); - assertThat(normalJson).contains("@type"); - } - - @Test - public void testToJSONString_withIgnoreAutoTypeAndPrettyPrint() { - TestObject obj = new TestObject("noType", 111); - - String jsonIgnorePretty = jsonSerializer.toJSONString(obj, true, true); - assertThat(jsonIgnorePretty).contains("\n"); - assertThat(jsonIgnorePretty).doesNotContain("@type"); - - String jsonIgnoreNormal = jsonSerializer.toJSONString(obj, true, false); - assertThat(jsonIgnoreNormal).doesNotContain("\n"); - assertThat(jsonIgnoreNormal).doesNotContain("@type"); - - String jsonNoIgnorePretty = jsonSerializer.toJSONString(obj, false, true); - assertThat(jsonNoIgnorePretty).contains("\n"); - assertThat(jsonNoIgnorePretty).contains("@type"); - - String jsonNoIgnoreNormal = jsonSerializer.toJSONString(obj, false, false); - assertThat(jsonNoIgnoreNormal).doesNotContain("\n"); - assertThat(jsonNoIgnoreNormal).contains("@type"); - - List emptyList = new ArrayList<>(); - String emptyListJson = jsonSerializer.toJSONString(emptyList, false, false); - assertThat(emptyListJson).isEqualTo("[]"); - - Object invalidObject = new Object() { - private final java.io.InputStream stream = System.in; - }; - - assertThatThrownBy(() -> jsonSerializer.toJSONString(invalidObject, false, false)) - .isInstanceOf(JsonParseException.class) - .hasMessageContaining("Jackson serialize error"); - } - - /** - * The "[]" shortcut in parseObject(String, Class, boolean) must only apply when the - * target type is a Collection. Otherwise it would silently return an empty ArrayList - * cast to T, causing a ClassCastException at the call site. The exact behaviour of the - * underlying library when asked to map "[]" to a POJO may vary (return null, return an - * empty POJO, or throw); the only requirement enforced here is that the result is never - * an ArrayList. - */ - @Test - public void testParseObject_emptyArrayJson_nonCollectionType_doesNotReturnArrayList() { - assertEmptyArrayDoesNotProduceCollection(true); - assertEmptyArrayDoesNotProduceCollection(false); - } - - private void assertEmptyArrayDoesNotProduceCollection(boolean ignoreAutoType) { - Object result; - try { - result = jsonSerializer.parseObject("[]", TestObject.class, ignoreAutoType); - } catch (JsonParseException ignored) { - // throwing is also an acceptable outcome - return; - } - if (result != null) { - assertThat(result).isNotInstanceOf(java.util.Collection.class); - } - } - - @Test - public void testParseObject_withIgnoreAutoType() { - String jsonWithAutoType = jsonSerializer.toJSONString(new TestObject("ignored", 222), false, false); - - TestObject objIgnore = jsonSerializer.parseObject(jsonWithAutoType, TestObject.class, true); - assertThat(objIgnore).isNotNull(); - assertThat(objIgnore.getName()).isEqualTo("ignored"); - assertThat(objIgnore.getValue()).isEqualTo(222); - - TestObject objNoIgnore = jsonSerializer.parseObject(jsonWithAutoType, TestObject.class, false); - assertThat(objNoIgnore).isNotNull(); - assertThat(objNoIgnore.getName()).isEqualTo("ignored"); - assertThat(objNoIgnore.getValue()).isEqualTo(222); - - List emptyList = jsonSerializer.parseObject("[]", List.class, false); - assertThat(emptyList).isEmpty(); - - List emptyListIgnore = jsonSerializer.parseObject("[]", List.class, true); - assertThat(emptyListIgnore).isEmpty(); - - assertThat(jsonSerializer.parseObject(null, TestObject.class, false)).isNull(); - - assertThatThrownBy(() -> jsonSerializer.parseObject("{invalid json}", TestObject.class, false)) - .isInstanceOf(JsonParseException.class) - .hasMessageContaining("Jackson deserialize error"); - } - - public static class TestObject { - private String name; - private int value; - - public TestObject() {} - - public TestObject(String name, int value) { - this.name = name; - this.value = value; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public int getValue() { - return value; - } - - public void setValue(int value) { - this.value = value; - } - } -} diff --git a/json-common/json-common-core/src/test/java/org/apache/seata/common/json/JsonAllowlistManagerTest.java b/json-common/json-common-core/src/test/java/org/apache/seata/common/json/JsonAllowlistManagerTest.java deleted file mode 100644 index c33425308ba..00000000000 --- a/json-common/json-common-core/src/test/java/org/apache/seata/common/json/JsonAllowlistManagerTest.java +++ /dev/null @@ -1,340 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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.apache.seata.common.json; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -/** - * Tests for {@link JsonAllowlistManager} - */ -public class JsonAllowlistManagerTest { - - @AfterEach - void tearDown() { - - JsonAllowlistManager.getInstance().clearUserAllowlist(); - } - - @Test - public void testBuiltinAllowlist_primitiveWrappers() { - JsonAllowlistManager manager = JsonAllowlistManager.getInstance(); - - assertThat(manager.isAllowed("java.lang.String")).isTrue(); - assertThat(manager.isAllowed("java.lang.Integer")).isTrue(); - assertThat(manager.isAllowed("java.lang.Long")).isTrue(); - assertThat(manager.isAllowed("java.lang.Boolean")).isTrue(); - assertThat(manager.isAllowed("java.lang.Double")).isTrue(); - assertThat(manager.isAllowed("java.lang.Float")).isTrue(); - assertThat(manager.isAllowed("java.lang.Byte")).isTrue(); - assertThat(manager.isAllowed("java.lang.Short")).isTrue(); - assertThat(manager.isAllowed("java.lang.Character")).isTrue(); - assertThat(manager.isAllowed("java.lang.Number")).isTrue(); - assertThat(manager.isAllowed("java.math.BigDecimal")).isTrue(); - assertThat(manager.isAllowed("java.math.BigInteger")).isTrue(); - } - - @Test - public void testBuiltinAllowlist_arrays() { - JsonAllowlistManager manager = JsonAllowlistManager.getInstance(); - - // 1D primitive arrays - assertThat(manager.isAllowed("[B")).isTrue(); - assertThat(manager.isAllowed("[I")).isTrue(); - assertThat(manager.isAllowed("[J")).isTrue(); - assertThat(manager.isAllowed("[Z")).isTrue(); - assertThat(manager.isAllowed("[C")).isTrue(); - assertThat(manager.isAllowed("[S")).isTrue(); - assertThat(manager.isAllowed("[F")).isTrue(); - assertThat(manager.isAllowed("[D")).isTrue(); - - // Multi-dimensional primitive arrays - assertThat(manager.isAllowed("[[I")).isTrue(); - assertThat(manager.isAllowed("[[Z")).isTrue(); - assertThat(manager.isAllowed("[[B")).isTrue(); - assertThat(manager.isAllowed("[[J")).isTrue(); - assertThat(manager.isAllowed("[[[D")).isTrue(); - assertThat(manager.isAllowed("[[[F")).isTrue(); - - // Object arrays - assertThat(manager.isAllowed("[Ljava.lang.String;")).isTrue(); - assertThat(manager.isAllowed("[Ljava.lang.Object;")).isTrue(); - } - - @Test - public void testBuiltinAllowlist_dateTime() { - JsonAllowlistManager manager = JsonAllowlistManager.getInstance(); - - assertThat(manager.isAllowed("java.util.Date")).isTrue(); - assertThat(manager.isAllowed("java.sql.Timestamp")).isTrue(); - assertThat(manager.isAllowed("java.time.LocalDateTime")).isTrue(); - assertThat(manager.isAllowed("java.time.LocalDate")).isTrue(); - assertThat(manager.isAllowed("java.time.Instant")).isTrue(); - assertThat(manager.isAllowed("java.time.ZonedDateTime")).isTrue(); - } - - @Test - public void testBuiltinAllowlist_collections() { - JsonAllowlistManager manager = JsonAllowlistManager.getInstance(); - - assertThat(manager.isAllowed("java.util.ArrayList")).isTrue(); - assertThat(manager.isAllowed("java.util.LinkedList")).isTrue(); - assertThat(manager.isAllowed("java.util.HashMap")).isTrue(); - assertThat(manager.isAllowed("java.util.LinkedHashMap")).isTrue(); - assertThat(manager.isAllowed("java.util.HashSet")).isTrue(); - assertThat(manager.isAllowed("java.util.TreeMap")).isTrue(); - assertThat(manager.isAllowed("java.util.concurrent.ConcurrentHashMap")).isTrue(); - } - - @Test - public void testBuiltinAllowlist_seataPrefix() { - JsonAllowlistManager manager = JsonAllowlistManager.getInstance(); - - assertThat(manager.isAllowed("org.apache.seata.core.model.BranchType")).isTrue(); - assertThat(manager.isAllowed("io.seata.saga.engine.mock.DemoService$People")) - .isTrue(); - assertThat(manager.isAllowed("org.apache.seata.rm.datasource.undo.UndoLogParser")) - .isTrue(); - assertThat(manager.isAllowed("org.apache.seata.common.json.JsonAllowlistManager")) - .isTrue(); - } - - @Test - public void testBuiltinAllowlist_objectArrayDescriptorsByPrefix() { - JsonAllowlistManager manager = JsonAllowlistManager.getInstance(); - - assertThat(manager.isAllowed("[Lorg.apache.seata.common.json.JsonAllowlistManager;")) - .isTrue(); - assertThat(manager.isAllowed("[Lio.seata.saga.engine.mock.DemoService$People;")) - .isTrue(); - assertThat(manager.isAllowed("[[Lio.seata.saga.engine.mock.DemoService$People;")) - .isTrue(); - assertThat(manager.isAllowed("[Lcom.malicious.EvilClass;")).isFalse(); - } - - @Test - public void testBuiltinAllowlist_objectArrayCanonicalNameByPrefix() { - JsonAllowlistManager manager = JsonAllowlistManager.getInstance(); - - assertThat(manager.isAllowed("io.seata.saga.engine.mock.DemoService$People[]")) - .isTrue(); - assertThat(manager.isAllowed("io.seata.saga.engine.mock.DemoService$People[][]")) - .isTrue(); - assertThat(manager.isAllowed("com.malicious.EvilClass[]")).isFalse(); - } - - @Test - public void testBuiltinAllowlist_notAllowed() { - JsonAllowlistManager manager = JsonAllowlistManager.getInstance(); - - assertThat(manager.isAllowed("com.sun.rowset.JdbcRowSetImpl")).isFalse(); - assertThat(manager.isAllowed("java.lang.Runtime")).isFalse(); - assertThat(manager.isAllowed("java.lang.ProcessBuilder")).isFalse(); - assertThat(manager.isAllowed("javax.naming.InitialContext")).isFalse(); - assertThat(manager.isAllowed("com.example.MaliciousClass")).isFalse(); - } - - @Test - public void testLoadUserAllowlist_exactMatch() { - JsonAllowlistManager manager = JsonAllowlistManager.getInstance(); - - manager.loadUserAllowlist("com.example.MyClass,com.example.AnotherClass"); - - assertThat(manager.isAllowed("com.example.MyClass")).isTrue(); - assertThat(manager.isAllowed("com.example.AnotherClass")).isTrue(); - assertThat(manager.isAllowed("com.example.NotInList")).isFalse(); - } - - @Test - public void testLoadUserAllowlist_prefixMatch() { - JsonAllowlistManager manager = JsonAllowlistManager.getInstance(); - - manager.loadUserAllowlist("com.company.model.,com.company.dto."); - - assertThat(manager.isAllowed("com.company.model.User")).isTrue(); - assertThat(manager.isAllowed("com.company.model.Order")).isTrue(); - assertThat(manager.isAllowed("com.company.dto.UserDTO")).isTrue(); - assertThat(manager.isAllowed("com.company.service.UserService")).isFalse(); - } - - @Test - public void testLoadUserAllowlist_mixedMatch() { - JsonAllowlistManager manager = JsonAllowlistManager.getInstance(); - - manager.loadUserAllowlist("com.company.model.,com.thirdparty.SpecificClass"); - - assertThat(manager.isAllowed("com.company.model.User")).isTrue(); - assertThat(manager.isAllowed("com.company.model.sub.NestedClass")).isTrue(); - assertThat(manager.isAllowed("com.thirdparty.SpecificClass")).isTrue(); - assertThat(manager.isAllowed("com.thirdparty.OtherClass")).isFalse(); - } - - @Test - public void testLoadUserAllowlist_withSpaces() { - JsonAllowlistManager manager = JsonAllowlistManager.getInstance(); - - manager.loadUserAllowlist(" com.example.Class1 , com.example.Class2 "); - - assertThat(manager.isAllowed("com.example.Class1")).isTrue(); - assertThat(manager.isAllowed("com.example.Class2")).isTrue(); - } - - @Test - public void testLoadUserAllowlist_emptyEntries() { - JsonAllowlistManager manager = JsonAllowlistManager.getInstance(); - - manager.loadUserAllowlist("com.example.Class1,,com.example.Class2,"); - - assertThat(manager.isAllowed("com.example.Class1")).isTrue(); - assertThat(manager.isAllowed("com.example.Class2")).isTrue(); - } - - @Test - public void testLoadUserAllowlist_nullOrEmpty() { - JsonAllowlistManager manager = JsonAllowlistManager.getInstance(); - - manager.loadUserAllowlist(null); - manager.loadUserAllowlist(""); - - assertThat(manager.isAllowed("java.lang.String")).isTrue(); - } - - @Test - public void testAddUserClass() { - JsonAllowlistManager manager = JsonAllowlistManager.getInstance(); - - manager.addUserClass("com.example.DynamicClass"); - - assertThat(manager.isAllowed("com.example.DynamicClass")).isTrue(); - } - - @Test - public void testAddUserPrefix() { - JsonAllowlistManager manager = JsonAllowlistManager.getInstance(); - - manager.addUserPrefix("com.dynamic.pkg."); - - assertThat(manager.isAllowed("com.dynamic.pkg.Class1")).isTrue(); - assertThat(manager.isAllowed("com.dynamic.pkg.sub.Class2")).isTrue(); - } - - @Test - public void testClearUserAllowlist() { - JsonAllowlistManager manager = JsonAllowlistManager.getInstance(); - - manager.loadUserAllowlist("com.example.ToBeCleared"); - assertThat(manager.isAllowed("com.example.ToBeCleared")).isTrue(); - - manager.clearUserAllowlist(); - assertThat(manager.isAllowed("com.example.ToBeCleared")).isFalse(); - - assertThat(manager.isAllowed("java.lang.String")).isTrue(); - } - - @Test - public void testCheckClass_allowed() { - JsonAllowlistManager manager = JsonAllowlistManager.getInstance(); - - manager.checkClass("java.lang.String"); - manager.checkClass("java.util.ArrayList"); - manager.checkClass("org.apache.seata.core.model.BranchType"); - } - - @Test - public void testCheckClass_notAllowed() { - JsonAllowlistManager manager = JsonAllowlistManager.getInstance(); - - assertThatThrownBy(() -> manager.checkClass("com.malicious.EvilClass")) - .isInstanceOf(SecurityException.class) - .hasMessageContaining("not in JSON deserialization allowlist") - .hasMessageContaining("com.malicious.EvilClass") - .hasMessageContaining("seata.json.allowlist"); - } - - @Test - public void testCheckClass_userAllowed() { - JsonAllowlistManager manager = JsonAllowlistManager.getInstance(); - - manager.loadUserAllowlist("com.myapp.model."); - - manager.checkClass("com.myapp.model.User"); - } - - @Test - public void testIsAllowed_null() { - JsonAllowlistManager manager = JsonAllowlistManager.getInstance(); - - assertThat(manager.isAllowed(null)).isFalse(); - } - - @Test - public void testCheckClass_null() { - JsonAllowlistManager manager = JsonAllowlistManager.getInstance(); - - assertThatThrownBy(() -> manager.checkClass(null)).isInstanceOf(SecurityException.class); - } - - @Test - public void testAddUserClass_nullOrEmpty() { - JsonAllowlistManager manager = JsonAllowlistManager.getInstance(); - - manager.addUserClass(null); - manager.addUserClass(""); - } - - @Test - public void testAddUserPrefix_nullOrEmpty() { - JsonAllowlistManager manager = JsonAllowlistManager.getInstance(); - - manager.addUserPrefix(null); - manager.addUserPrefix(""); - } - - @Test - public void testSingleton() { - JsonAllowlistManager instance1 = JsonAllowlistManager.getInstance(); - JsonAllowlistManager instance2 = JsonAllowlistManager.getInstance(); - - assertThat(instance1).isSameAs(instance2); - } - - @Test - public void testCacheWorks() { - JsonAllowlistManager manager = JsonAllowlistManager.getInstance(); - - boolean result1 = manager.isAllowed("java.lang.String"); - - boolean result2 = manager.isAllowed("java.lang.String"); - - assertThat(result1).isTrue(); - assertThat(result2).isTrue(); - } - - @Test - public void testCacheClearedOnLoadUserAllowlist() { - JsonAllowlistManager manager = JsonAllowlistManager.getInstance(); - - manager.isAllowed("com.example.CacheTest"); - - manager.loadUserAllowlist("com.example.CacheTest"); - - assertThat(manager.isAllowed("com.example.CacheTest")).isTrue(); - } -} diff --git a/json-common/json-common-core/src/test/java/org/apache/seata/common/json/JsonSerializerFactoryTest.java b/json-common/json-common-core/src/test/java/org/apache/seata/common/json/JsonSerializerFactoryTest.java deleted file mode 100644 index 99a66966f3e..00000000000 --- a/json-common/json-common-core/src/test/java/org/apache/seata/common/json/JsonSerializerFactoryTest.java +++ /dev/null @@ -1,145 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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.apache.seata.common.json; - -import org.apache.seata.common.json.impl.Fastjson2JsonSerializer; -import org.apache.seata.common.json.impl.FastjsonJsonSerializer; -import org.apache.seata.common.json.impl.GsonJsonSerializer; -import org.apache.seata.common.json.impl.JacksonJsonSerializer; -import org.apache.seata.common.loader.EnhancedServiceNotFoundException; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import java.lang.reflect.Field; -import java.util.Map; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -public class JsonSerializerFactoryTest { - - @BeforeEach - @SuppressWarnings("unchecked") - void clearInstancesCache() throws Exception { - Field field = JsonSerializerFactory.class.getDeclaredField("INSTANCES"); - field.setAccessible(true); - ((Map) field.get(null)).clear(); - } - - @Test - public void testGetSerializer_jackson() { - JsonSerializer serializer = JsonSerializerFactory.getSerializer("jackson"); - assertThat(serializer).isNotNull().isInstanceOf(JacksonJsonSerializer.class); - } - - @Test - public void testGetSerializer_fastjson() { - JsonSerializer serializer = JsonSerializerFactory.getSerializer("fastjson"); - assertThat(serializer).isNotNull().isInstanceOf(FastjsonJsonSerializer.class); - } - - @Test - public void testGetSerializer_fastjson2() { - JsonSerializer serializer = JsonSerializerFactory.getSerializer("fastjson2"); - assertThat(serializer).isNotNull().isInstanceOf(Fastjson2JsonSerializer.class); - } - - @Test - public void testGetSerializer_gson() { - JsonSerializer serializer = JsonSerializerFactory.getSerializer("gson"); - assertThat(serializer).isNotNull().isInstanceOf(GsonJsonSerializer.class); - } - - @Test - public void testGetSerializer_nullName_returnsDefaultJackson() { - JsonSerializer serializer = JsonSerializerFactory.getSerializer(null); - assertThat(serializer).isNotNull().isInstanceOf(JacksonJsonSerializer.class); - } - - @Test - public void testGetSerializer_unknownName_throwsException() { - assertThatThrownBy(() -> JsonSerializerFactory.getSerializer("definitely-not-a-real-serializer")) - .isInstanceOf(EnhancedServiceNotFoundException.class); - } - - @Test - public void testGetSerializer_sameNameReturnsCachedInstance() { - JsonSerializer first = JsonSerializerFactory.getSerializer("jackson"); - JsonSerializer second = JsonSerializerFactory.getSerializer("jackson"); - assertThat(second).isSameAs(first); - } - - /** - * In json-common-core tests, the jackson3 SPI lives in the separate json-common-jackson3 - * module which is NOT on the test classpath. The factory must catch the - * EnhancedServiceNotFoundException for "jackson3" and fall back to the default "jackson" - * serializer. - */ - @Test - public void testGetSerializer_jackson3_fallsBackToJacksonWhenUnavailable() { - JsonSerializer serializer = JsonSerializerFactory.getSerializer("jackson3"); - assertThat(serializer).isNotNull().isInstanceOf(JacksonJsonSerializer.class); - } - - @Test - public void testGetSerializer_jackson3FallbackIsCached() { - JsonSerializer first = JsonSerializerFactory.getSerializer("jackson3"); - JsonSerializer second = JsonSerializerFactory.getSerializer("jackson3"); - assertThat(second).isSameAs(first); - assertThat(first).isInstanceOf(JacksonJsonSerializer.class); - } - - @Test - public void testGetSerializer_jackson3FallbackIsUsableForSerialization() { - JsonSerializer serializer = JsonSerializerFactory.getSerializer("jackson3"); - String json = serializer.toJSONString(new SimplePojo("hello", 7)); - assertThat(json).contains("\"name\":\"hello\"").contains("\"value\":7"); - - SimplePojo restored = serializer.parseObject(json, SimplePojo.class); - assertThat(restored).isNotNull(); - assertThat(restored.getName()).isEqualTo("hello"); - assertThat(restored.getValue()).isEqualTo(7); - } - - public static class SimplePojo { - private String name; - private int value; - - public SimplePojo() {} - - public SimplePojo(String name, int value) { - this.name = name; - this.value = value; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public int getValue() { - return value; - } - - public void setValue(int value) { - this.value = value; - } - } -} diff --git a/json-common/json-common-core/src/test/java/org/apache/seata/common/json/JsonUtilTest.java b/json-common/json-common-core/src/test/java/org/apache/seata/common/json/JsonUtilTest.java deleted file mode 100644 index 2dab9ba4ecc..00000000000 --- a/json-common/json-common-core/src/test/java/org/apache/seata/common/json/JsonUtilTest.java +++ /dev/null @@ -1,202 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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.apache.seata.common.json; - -import org.apache.seata.common.ConfigurationKeys; -import org.apache.seata.common.DefaultValues; -import org.apache.seata.config.Configuration; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.mockito.Mockito.mock; -import static org.mockito.Mockito.when; - -public class JsonUtilTest { - - @BeforeEach - void setUp() {} - - @Test - public void testToJSONStringBasicObject() { - TestObject obj = new TestObject("test", 123); - String json = JsonUtil.toJSONString(obj); - - assertThat(json).isNotNull(); - assertThat(json).contains("\"name\":\"test\""); - assertThat(json).contains("\"value\":123"); - } - - @Test - public void testParseObjectBasicObject() { - String json = "{\"name\":\"test\",\"value\":123}"; - TestObject obj = JsonUtil.parseObject(json, TestObject.class); - - assertThat(obj).isNotNull(); - assertThat(obj.getName()).isEqualTo("test"); - assertThat(obj.getValue()).isEqualTo(123); - } - - @Test - public void testToJSONStringAndParseObjectApple() { - TestObject original = new TestObject("apple", 456); - String json = JsonUtil.toJSONString(original); - TestObject restored = JsonUtil.parseObject(json, TestObject.class); - - assertThat(restored.getName()).isEqualTo(original.getName()); - assertThat(restored.getValue()).isEqualTo(original.getValue()); - } - - @Test - public void testParseObjectNullInputs() { - TestObject obj1 = JsonUtil.parseObject(null, TestObject.class); - assertThat(obj1).isNull(); - - TestObject obj2 = JsonUtil.parseObject("{\"name\":\"test\"}", null); - assertThat(obj2).isNull(); - } - - @Test - public void testParseObjectPrefixLogic() { - String normalJson = "{\"name\":\"normalTest\",\"value\":888}"; - TestObject obj = JsonUtil.parseObject(normalJson, TestObject.class); - assertThat(obj).isNotNull(); - assertThat(obj.getName()).isEqualTo("normalTest"); - assertThat(obj.getValue()).isEqualTo(888); - } - - @Test - public void testToJSONStringNullObject() { - String json = JsonUtil.toJSONString(null); - assertThat(json).isEqualTo("null"); - } - - @Test - public void testParseObjectComplexObject() { - ComplexTestObject complexObj = new ComplexTestObject(); - complexObj.setName("complex"); - complexObj.setValue(789); - complexObj.setNested(new TestObject("nested", 1)); - - String json = JsonUtil.toJSONString(complexObj); - ComplexTestObject restored = JsonUtil.parseObject(json, ComplexTestObject.class); - - assertThat(restored).isNotNull(); - assertThat(restored.getName()).isEqualTo("complex"); - assertThat(restored.getValue()).isEqualTo(789); - assertThat(restored.getNested()).isNotNull(); - assertThat(restored.getNested().getName()).isEqualTo("nested"); - assertThat(restored.getNested().getValue()).isEqualTo(1); - } - - @Test - public void testResolveJsonSerializerNamePrefersNewConfig() { - Configuration configuration = mock(Configuration.class); - when(configuration.getConfig(ConfigurationKeys.JSON_SERIALIZER_TYPE)).thenReturn("gson"); - - assertThat(JsonUtil.resolveJsonSerializerName(configuration)).isEqualTo("gson"); - } - - @Test - public void testResolveJsonSerializerNameFallsBackToDeprecatedConfig() { - Configuration configuration = mock(Configuration.class); - when(configuration.getConfig(ConfigurationKeys.JSON_SERIALIZER_TYPE)).thenReturn(null); - when(configuration.getConfig(ConfigurationKeys.TCC_BUSINESS_ACTION_CONTEXT_JSON_PARSER_NAME)) - .thenReturn("fastjson2"); - - assertThat(JsonUtil.resolveJsonSerializerName(configuration)).isEqualTo("fastjson2"); - } - - @Test - public void testResolveJsonSerializerNameReturnsDefaultWhenConfigMissing() { - Configuration configuration = mock(Configuration.class); - when(configuration.getConfig(ConfigurationKeys.JSON_SERIALIZER_TYPE)).thenReturn(null); - when(configuration.getConfig(ConfigurationKeys.TCC_BUSINESS_ACTION_CONTEXT_JSON_PARSER_NAME)) - .thenReturn(null); - - assertThat(JsonUtil.resolveJsonSerializerName(configuration)) - .isEqualTo(DefaultValues.BUSINESS_ACTION_CONTEXT_JSON_PARSER); - } - - @Test - public void testResolveJsonSerializerNameIgnoresBlankNewConfigAndFallsBackToDeprecatedConfig() { - Configuration configuration = mock(Configuration.class); - when(configuration.getConfig(ConfigurationKeys.JSON_SERIALIZER_TYPE)).thenReturn(" "); - when(configuration.getConfig(ConfigurationKeys.TCC_BUSINESS_ACTION_CONTEXT_JSON_PARSER_NAME)) - .thenReturn("fastjson2"); - - assertThat(JsonUtil.resolveJsonSerializerName(configuration)).isEqualTo("fastjson2"); - } - - public static class TestObject { - private String name; - private int value; - - public TestObject() {} - - public TestObject(String name, int value) { - this.name = name; - this.value = value; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public int getValue() { - return value; - } - - public void setValue(int value) { - this.value = value; - } - } - - public static class ComplexTestObject { - private String name; - private int value; - private TestObject nested; - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public int getValue() { - return value; - } - - public void setValue(int value) { - this.value = value; - } - - public TestObject getNested() { - return nested; - } - - public void setNested(TestObject nested) { - this.nested = nested; - } - } -} diff --git a/json-common/json-common-core/src/test/resources/file.conf b/json-common/json-common-core/src/test/resources/file.conf deleted file mode 100644 index 46c3e0401cc..00000000000 --- a/json-common/json-common-core/src/test/resources/file.conf +++ /dev/null @@ -1,25 +0,0 @@ -# -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You 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. -# - -service { - #transaction service group mapping - vgroupMapping.default_tx_group = "default" - #only support when registry.type=file, please don't set multiple addresses - default.grouplist = "127.0.0.1:8091" - #disable seata - disableGlobalTransaction = false -} \ No newline at end of file diff --git a/json-common/json-common-core/src/test/resources/registry.conf b/json-common/json-common-core/src/test/resources/registry.conf deleted file mode 100644 index 5ad014bf55a..00000000000 --- a/json-common/json-common-core/src/test/resources/registry.conf +++ /dev/null @@ -1,34 +0,0 @@ -# -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You 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. -# - -registry { - # file 、nacos 、eureka、redis、zk、consul、etcd3、sofa - type = "file" - - file { - name = "file.conf" - } -} - -config { - # file、nacos 、apollo、zk、consul、etcd3 - type = "file" - - file { - name = "file.conf" - } -} \ No newline at end of file diff --git a/json-common/json-common-jackson3/pom.xml b/json-common/json-common-jackson3/pom.xml deleted file mode 100644 index da4d5c48329..00000000000 --- a/json-common/json-common-jackson3/pom.xml +++ /dev/null @@ -1,53 +0,0 @@ - - - - - org.apache.seata - json-common - ${revision} - - 4.0.0 - json-common-jackson3 - jar - json-common-jackson3 ${project.version} - Jackson 3 JSON serializer for Seata (requires JDK 17+) - - - 17 - 17 - 17 - - - - - ${project.groupId} - json-common-core - ${project.version} - - - tools.jackson.core - jackson-databind - provided - - - - diff --git a/json-common/json-common-jackson3/src/main/java/org/apache/seata/common/json/impl/Jackson3JsonSerializer.java b/json-common/json-common-jackson3/src/main/java/org/apache/seata/common/json/impl/Jackson3JsonSerializer.java deleted file mode 100644 index 86c361a3b9f..00000000000 --- a/json-common/json-common-jackson3/src/main/java/org/apache/seata/common/json/impl/Jackson3JsonSerializer.java +++ /dev/null @@ -1,185 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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.apache.seata.common.json.impl; - -import org.apache.seata.common.exception.JsonParseException; -import org.apache.seata.common.json.JsonAllowlistManager; -import org.apache.seata.common.json.JsonSerializer; -import org.apache.seata.common.loader.LoadLevel; -import tools.jackson.core.JacksonException; -import tools.jackson.databind.DatabindContext; -import tools.jackson.databind.DefaultTyping; -import tools.jackson.databind.DeserializationFeature; -import tools.jackson.databind.JavaType; -import tools.jackson.databind.ObjectMapper; -import tools.jackson.databind.json.JsonMapper; -import tools.jackson.databind.jsontype.PolymorphicTypeValidator; - -import java.lang.reflect.Type; -import java.util.ArrayList; -import java.util.List; -import java.util.regex.Pattern; - -/** - * Jackson 3.x implementation of JsonSerializer - */ -@LoadLevel(name = Jackson3JsonSerializer.NAME) -public class Jackson3JsonSerializer implements JsonSerializer { - - public static final String NAME = "jackson3"; - - private static final Pattern AUTOTYPE_PATTERN = Pattern.compile("\"@type\"\\s*:"); - - private final ObjectMapper defaultObjectMapper; - - private final ObjectMapper objectMapperWithAutoType; - - public Jackson3JsonSerializer() { - this.defaultObjectMapper = JsonMapper.builder() - .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) - .build(); - - AllowlistTypeValidator validator = new AllowlistTypeValidator(); - this.objectMapperWithAutoType = JsonMapper.builder() - .disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES) - .activateDefaultTypingAsProperty(validator, DefaultTyping.NON_FINAL, "@type") - .build(); - } - - @Override - public String toJSONString(Object object) { - try { - if (object instanceof List && ((List) object).isEmpty()) { - return "[]"; - } - return objectMapperWithAutoType.writeValueAsString(object); - } catch (JacksonException e) { - throw new JsonParseException("Jackson3 serialize error", e); - } - } - - @Override - public T parseObject(String text, Class clazz) { - if (text == null || clazz == null) { - return null; - } - try { - return defaultObjectMapper.readValue(text, clazz); - } catch (JacksonException e) { - throw new JsonParseException("Jackson3 deserialize error", e); - } - } - - @Override - public T parseObjectWithType(String text, Type type) { - if (text == null || type == null) { - return null; - } - try { - return objectMapperWithAutoType.readValue(text, objectMapperWithAutoType.constructType(type)); - } catch (SecurityException e) { - throw e; - } catch (JacksonException e) { - rethrowIfSecurityException(e); - throw new JsonParseException("Jackson3 deserialize error", e); - } - } - - @Override - public boolean useAutoType(String json) { - return json != null && AUTOTYPE_PATTERN.matcher(json).find(); - } - - @Override - public String toJSONString(Object o, boolean prettyPrint) { - return toJSONString(o, false, prettyPrint); - } - - @Override - public String toJSONString(Object o, boolean ignoreAutoType, boolean prettyPrint) { - try { - if (o instanceof List && ((List) o).isEmpty()) { - return "[]"; - } - ObjectMapper mapper = ignoreAutoType ? defaultObjectMapper : objectMapperWithAutoType; - if (prettyPrint) { - return mapper.writerWithDefaultPrettyPrinter().writeValueAsString(o); - } else { - return mapper.writeValueAsString(o); - } - } catch (JacksonException e) { - throw new JsonParseException("Jackson3 serialize error", e); - } - } - - @Override - public T parseObject(String json, Class type, boolean ignoreAutoType) { - if (json == null || type == null) { - return null; - } - try { - if ("[]".equals(json) && (java.util.Collection.class.isAssignableFrom(type) || type == Object.class)) { - return (T) new ArrayList<>(0); - } - if (ignoreAutoType) { - return defaultObjectMapper.readValue(json, type); - } else { - return objectMapperWithAutoType.readValue(json, type); - } - } catch (SecurityException e) { - throw e; - } catch (JacksonException e) { - rethrowIfSecurityException(e); - throw new JsonParseException("Jackson3 deserialize error", e); - } - } - - private static void rethrowIfSecurityException(Throwable e) { - Throwable cause = e.getCause(); - while (cause != null) { - if (cause instanceof SecurityException) { - throw (SecurityException) cause; - } - cause = cause.getCause(); - } - } - - /** - * Jackson 3.x native PolymorphicTypeValidator that delegates to JsonAllowlistManager - */ - private static class AllowlistTypeValidator extends PolymorphicTypeValidator.Base { - private static final long serialVersionUID = 1L; - - @Override - public Validity validateBaseType(DatabindContext ctxt, JavaType baseType) { - return Validity.INDETERMINATE; - } - - @Override - public Validity validateSubClassName(DatabindContext ctxt, JavaType baseType, String subClassName) { - // Throws SecurityException if not allowed - JsonAllowlistManager.getInstance().checkClass(subClassName); - return Validity.ALLOWED; - } - - @Override - public Validity validateSubType(DatabindContext ctxt, JavaType baseType, JavaType subType) { - JsonAllowlistManager.getInstance().checkClass(subType.getRawClass().getName()); - return Validity.ALLOWED; - } - } -} diff --git a/json-common/json-common-jackson3/src/main/resources/META-INF/services/org.apache.seata.common.json.JsonSerializer b/json-common/json-common-jackson3/src/main/resources/META-INF/services/org.apache.seata.common.json.JsonSerializer deleted file mode 100644 index f396fca1218..00000000000 --- a/json-common/json-common-jackson3/src/main/resources/META-INF/services/org.apache.seata.common.json.JsonSerializer +++ /dev/null @@ -1,17 +0,0 @@ -# -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You 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. -# -org.apache.seata.common.json.impl.Jackson3JsonSerializer diff --git a/json-common/json-common-jackson3/src/test/java/org/apache/seata/common/json/Jackson3AllowlistTest.java b/json-common/json-common-jackson3/src/test/java/org/apache/seata/common/json/Jackson3AllowlistTest.java deleted file mode 100644 index 418829c2cfd..00000000000 --- a/json-common/json-common-jackson3/src/test/java/org/apache/seata/common/json/Jackson3AllowlistTest.java +++ /dev/null @@ -1,243 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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.apache.seata.common.json; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -/** - * Tests for Jackson3 serializer with allowlist security check - */ -public class Jackson3AllowlistTest { - - private JsonSerializer jsonSerializer; - - @BeforeEach - void setUp() { - jsonSerializer = JsonSerializerFactory.getSerializer("jackson3"); - } - - @AfterEach - void tearDown() { - JsonAllowlistManager.getInstance().clearUserAllowlist(); - } - - @Test - public void testParseObject_allowedSeataClass() { - - String json = - "{\"@type\":\"org.apache.seata.common.json.Jackson3AllowlistTest$AllowedTestClass\",\"name\":\"test\"}"; - - AllowedTestClass result = jsonSerializer.parseObject(json, AllowedTestClass.class, false); - - assertThat(result).isNotNull(); - assertThat(result.getName()).isEqualTo("test"); - } - - @Test - public void testParseObject_allowedJavaClass() { - - String json = "{\"@type\":\"java.util.HashMap\"}"; - - Object result = jsonSerializer.parseObject(json, Object.class, false); - - assertThat(result).isNotNull(); - } - - @Test - public void testParseObject_notAllowedClass() { - - String json = "{\"@type\":\"com.malicious.EvilClass\",\"command\":\"rm -rf /\"}"; - - assertThatThrownBy(() -> jsonSerializer.parseObject(json, Object.class, false)) - .isInstanceOf(SecurityException.class) - .hasMessageContaining("not in JSON deserialization allowlist") - .hasMessageContaining("com.malicious.EvilClass"); - } - - @Test - public void testParseObject_userAllowedClass() { - - JsonAllowlistManager.getInstance().addUserClass("com.example.UserClass"); - - String json = "{\"@type\":\"com.example.UserClass\",\"data\":\"test\"}"; - - try { - jsonSerializer.parseObject(json, Object.class, false); - } catch (SecurityException e) { - throw e; - } catch (Exception e) { - assertThat(e).isNotInstanceOf(SecurityException.class); - } - } - - @Test - public void testParseObject_userAllowedPrefix() { - JsonAllowlistManager.getInstance().addUserPrefix("com.mycompany.model."); - - String json = "{\"@type\":\"com.mycompany.model.User\",\"id\":1}"; - - try { - jsonSerializer.parseObject(json, Object.class, false); - } catch (SecurityException e) { - throw e; - } catch (Exception e) { - - assertThat(e).isNotInstanceOf(SecurityException.class); - } - } - - @Test - public void testParseObject_ignoreAutoType_bypasses_check() { - - String json = "{\"@type\":\"com.malicious.EvilClass\",\"command\":\"rm -rf /\"}"; - - try { - jsonSerializer.parseObject(json, Object.class, true); - } catch (SecurityException e) { - throw new AssertionError("Should not throw SecurityException when ignoreAutoType=true", e); - } catch (Exception e) { - - assertThat(e).isNotInstanceOf(SecurityException.class); - } - } - - @Test - public void testParseObject_noAutoType_bypasses_check() { - String json = "{\"name\":\"test\",\"value\":123}"; - - TestObject result = jsonSerializer.parseObject(json, TestObject.class); - - assertThat(result).isNotNull(); - assertThat(result.getName()).isEqualTo("test"); - } - - @Test - public void testParseObject_multipleAutoTypes() { - - String json = "{\"@type\":\"org.apache.seata.common.json.Jackson3AllowlistTest$ContainerClass\"," - + "\"inner\":{\"@type\":\"org.apache.seata.common.json.Jackson3AllowlistTest$AllowedTestClass\",\"name\":\"nested\"}}"; - - ContainerClass result = jsonSerializer.parseObject(json, ContainerClass.class, false); - - assertThat(result).isNotNull(); - } - - @Test - public void testParseObject_multipleAutoTypes_oneNotAllowed() { - - String json = "{\"@type\":\"org.apache.seata.common.json.Jackson3AllowlistTest$ContainerClass\"," - + "\"inner\":{\"@type\":\"com.malicious.EvilClass\",\"name\":\"evil\"}}"; - - assertThatThrownBy(() -> jsonSerializer.parseObject(json, ContainerClass.class, false)) - .isInstanceOf(SecurityException.class) - .hasMessageContaining("com.malicious.EvilClass"); - } - - @Test - public void testParseObject_atTypeInStringValue_notBlocked() { - String json = "{\"@type\":\"org.apache.seata.common.json.Jackson3AllowlistTest$TestObject\"," - + "\"name\":\"the \\\"@type\\\" field is important\",\"value\":123}"; - - TestObject result = jsonSerializer.parseObject(json, TestObject.class, false); - - assertThat(result).isNotNull(); - assertThat(result.getName()).isEqualTo("the \"@type\" field is important"); - } - - @Test - public void testLoadUserAllowlist_thenParse() { - JsonAllowlistManager.getInstance().loadUserAllowlist("com.trusted.model.,com.trusted.dto.SpecificDTO"); - - String json1 = "{\"@type\":\"com.trusted.model.User\",\"id\":1}"; - try { - jsonSerializer.parseObject(json1, Object.class, false); - } catch (SecurityException e) { - throw e; - } catch (Exception e) { - - } - - String json2 = "{\"@type\":\"com.trusted.dto.SpecificDTO\",\"data\":\"test\"}"; - try { - jsonSerializer.parseObject(json2, Object.class, false); - } catch (SecurityException e) { - throw e; - } catch (Exception e) { - - } - - String json3 = "{\"@type\":\"com.untrusted.EvilClass\",\"data\":\"evil\"}"; - assertThatThrownBy(() -> jsonSerializer.parseObject(json3, Object.class, false)) - .isInstanceOf(SecurityException.class); - } - - public static class TestObject { - private String name; - private int value; - - public TestObject() {} - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public int getValue() { - return value; - } - - public void setValue(int value) { - this.value = value; - } - } - - public static class AllowedTestClass { - private String name; - - public AllowedTestClass() {} - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - } - - public static class ContainerClass { - private AllowedTestClass inner; - - public ContainerClass() {} - - public AllowedTestClass getInner() { - return inner; - } - - public void setInner(AllowedTestClass inner) { - this.inner = inner; - } - } -} diff --git a/json-common/json-common-jackson3/src/test/java/org/apache/seata/common/json/Jackson3JsonSerializerTest.java b/json-common/json-common-jackson3/src/test/java/org/apache/seata/common/json/Jackson3JsonSerializerTest.java deleted file mode 100644 index 89c7e6f183a..00000000000 --- a/json-common/json-common-jackson3/src/test/java/org/apache/seata/common/json/Jackson3JsonSerializerTest.java +++ /dev/null @@ -1,350 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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.apache.seata.common.json; - -import org.apache.seata.common.exception.JsonParseException; -import org.apache.seata.common.json.impl.Jackson3JsonSerializer; -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import tools.jackson.core.type.TypeReference; - -import java.lang.reflect.Type; -import java.util.ArrayList; -import java.util.List; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -public class Jackson3JsonSerializerTest { - - private JsonSerializer jsonSerializer; - - @BeforeEach - void setUp() { - jsonSerializer = JsonSerializerFactory.getSerializer("jackson3"); - } - - @Test - public void testToJSONString_basicObject() { - TestObject obj = new TestObject("test", 123); - String json = jsonSerializer.toJSONString(obj); - - assertThat(json).contains("\"name\":\"test\""); - assertThat(json).contains("\"value\":123"); - } - - @Test - public void testParseObject_basicObject() { - String json = - "{\"@type\":\"org.apache.seata.common.json.Jackson3JsonSerializerTest$TestObject\",\"name\":\"test\",\"value\":123}"; - TestObject obj = jsonSerializer.parseObject(json, TestObject.class); - - assertThat(obj).isNotNull(); - assertThat(obj.getName()).isEqualTo("test"); - assertThat(obj.getValue()).isEqualTo(123); - } - - @Test - public void testToJSONString_and_parseObject() { - TestObject original = new TestObject("school", 456); - String json = jsonSerializer.toJSONString(original); - TestObject restored = jsonSerializer.parseObject(json, TestObject.class); - - assertThat(restored.getName()).isEqualTo(original.getName()); - assertThat(restored.getValue()).isEqualTo(original.getValue()); - } - - @Test - public void testUseAutoType_withType() { - String json = "{\"@type\":\"some.type\",\"name\":\"test\"}"; - boolean hasAutoType = jsonSerializer.useAutoType(json); - assertThat(hasAutoType).isTrue(); - } - - @Test - public void testUseAutoType_withoutType() { - String json = "{\"name\":\"test\"}"; - boolean hasAutoType = jsonSerializer.useAutoType(json); - assertThat(hasAutoType).isFalse(); - } - - @Test - public void testToJSONString_withAutoType() { - TestObject obj = new TestObject("withType", 789); - String jsonWithAutoType = jsonSerializer.toJSONString(obj, false, false); - - assertThat(jsonWithAutoType).isNotNull(); - assertThat(jsonWithAutoType).contains("@type"); - assertThat(jsonWithAutoType).contains("\"name\":\"withType\""); - assertThat(jsonWithAutoType).contains("\"value\":789"); - } - - @Test - public void testToJSONString_singleArg_containsAutoType() { - TestObject obj = new TestObject("singleArg", 321); - String json = jsonSerializer.toJSONString(obj); - - assertThat(json).contains("@type"); - assertThat(json).contains("\"name\":\"singleArg\""); - assertThat(json).contains("\"value\":321"); - } - - @Test - public void testEmptyList_toJSONString_singleArg() { - List emptyList = new ArrayList<>(); - assertThat(jsonSerializer.toJSONString(emptyList)).isEqualTo("[]"); - } - - @Test - public void testParseObject_withAutoType() { - TestObject original = new TestObject("autoTypeTest", 999); - String jsonWithAutoType = jsonSerializer.toJSONString(original, false, false); - - TestObject restored = jsonSerializer.parseObject(jsonWithAutoType, TestObject.class, false); - - assertThat(restored).isNotNull(); - assertThat(restored.getName()).isEqualTo("autoTypeTest"); - assertThat(restored.getValue()).isEqualTo(999); - } - - @Test - public void testEmptyList_serialization() { - List emptyList = new ArrayList<>(); - String json = jsonSerializer.toJSONString(emptyList, false, false); - assertThat(json).isEqualTo("[]"); - } - - @Test - public void testEmptyList_deserialization() { - String json = "[]"; - List list = jsonSerializer.parseObject(json, List.class, false); - assertThat(list).isEmpty(); - } - - @Test - public void testNullInput_toJSONString() { - String json = jsonSerializer.toJSONString(null); - assertThat(json).isEqualTo("null"); - } - - @Test - public void testNullInput_parseObject() { - TestObject obj = jsonSerializer.parseObject(null, TestObject.class); - assertThat(obj).isNull(); - - TestObject obj2 = jsonSerializer.parseObject("{}", null); - assertThat(obj2).isNull(); - } - - @Test - public void testParseObject_invalidJson() { - assertThatThrownBy(() -> jsonSerializer.parseObject("{invalid json}", TestObject.class)) - .isInstanceOf(JsonParseException.class) - .hasMessageContaining("Jackson3 deserialize error"); - } - - @Test - public void testFactoryReturnsCorrectInstance() { - JsonSerializer serializer = JsonSerializerFactory.getSerializer("jackson3"); - assertThat(serializer).isNotNull(); - assertThat(serializer).isInstanceOf(Jackson3JsonSerializer.class); - } - - @Test - public void testToJSONString_prettyPrint() { - TestObject obj = new TestObject("pretty", 789); - String prettyJson = jsonSerializer.toJSONString(obj, true); - - assertThat(prettyJson).contains("\n"); - } - - @Test - public void testParseObject_ignoreAutoType() { - TestObject obj = new TestObject("ignored", 222); - String json = jsonSerializer.toJSONString(obj); - - TestObject restored = jsonSerializer.parseObject(json, TestObject.class, true); - - assertThat(restored).isNotNull(); - assertThat(restored.getName()).isEqualTo("ignored"); - assertThat(restored.getValue()).isEqualTo(222); - } - - @Test - public void testParseObject_nullJson() { - assertThat(jsonSerializer.parseObject(null, TestObject.class, false)).isNull(); - } - - @Test - public void testParseObject_emptyList() { - String json = "[]"; - List list = jsonSerializer.parseObject(json, List.class, false); - assertThat(list).isEmpty(); - } - - @Test - public void testParseObject_withType() { - String json = - "{\"@type\":\"org.apache.seata.common.json.Jackson3JsonSerializerTest$TestObject\",\"name\":\"test\",\"value\":123}"; - Type type = new TypeReference() {}.getType(); - - TestObject obj = jsonSerializer.parseObjectWithType(json, type); - assertThat(obj).isNotNull(); - assertThat(obj.getName()).isEqualTo("test"); - assertThat(obj.getValue()).isEqualTo(123); - - assertThatThrownBy(() -> jsonSerializer.parseObjectWithType("{invalid json}", type)) - .isInstanceOf(JsonParseException.class) - .hasMessageContaining("Jackson3 deserialize error"); - } - - @Test - public void testUseAutoType() { - String jsonWithAutoType = "{\"@type\":\"some.type\",\"name\":\"test\"}"; - assertThat(jsonSerializer.useAutoType(jsonWithAutoType)).isTrue(); - - String jsonWithoutAutoType = "{\"name\":\"test\"}"; - assertThat(jsonSerializer.useAutoType(jsonWithoutAutoType)).isFalse(); - - String jsonWithTypeInValue = "{\"comment\":\"this has @type in it\"}"; - assertThat(jsonSerializer.useAutoType(jsonWithTypeInValue)).isFalse(); - - assertThat(jsonSerializer.useAutoType(null)).isFalse(); - - assertThat(jsonSerializer.useAutoType("")).isFalse(); - } - - @Test - public void testToJSONString_withPrettyPrint() { - TestObject obj = new TestObject("pretty", 789); - - String prettyJson = jsonSerializer.toJSONString(obj, true); - assertThat(prettyJson).contains("\n"); - assertThat(prettyJson).contains("@type"); - - String normalJson = jsonSerializer.toJSONString(obj, false); - assertThat(normalJson).doesNotContain("\n"); - assertThat(normalJson).contains("@type"); - } - - @Test - public void testToJSONString_withIgnoreAutoTypeAndPrettyPrint() { - TestObject obj = new TestObject("noType", 111); - - String jsonIgnorePretty = jsonSerializer.toJSONString(obj, true, true); - assertThat(jsonIgnorePretty).contains("\n"); - assertThat(jsonIgnorePretty).doesNotContain("@type"); - - String jsonIgnoreNormal = jsonSerializer.toJSONString(obj, true, false); - assertThat(jsonIgnoreNormal).doesNotContain("\n"); - assertThat(jsonIgnoreNormal).doesNotContain("@type"); - - String jsonNoIgnorePretty = jsonSerializer.toJSONString(obj, false, true); - assertThat(jsonNoIgnorePretty).contains("\n"); - assertThat(jsonNoIgnorePretty).contains("@type"); - - String jsonNoIgnoreNormal = jsonSerializer.toJSONString(obj, false, false); - assertThat(jsonNoIgnoreNormal).doesNotContain("\n"); - assertThat(jsonNoIgnoreNormal).contains("@type"); - - List emptyList = new ArrayList<>(); - String emptyListJson = jsonSerializer.toJSONString(emptyList, false, false); - assertThat(emptyListJson).isEqualTo("[]"); - } - - @Test - public void testParseObject_withIgnoreAutoType() { - String jsonWithAutoType = jsonSerializer.toJSONString(new TestObject("ignored", 222), false, false); - - TestObject objIgnore = jsonSerializer.parseObject(jsonWithAutoType, TestObject.class, true); - assertThat(objIgnore).isNotNull(); - assertThat(objIgnore.getName()).isEqualTo("ignored"); - assertThat(objIgnore.getValue()).isEqualTo(222); - - TestObject objNoIgnore = jsonSerializer.parseObject(jsonWithAutoType, TestObject.class, false); - assertThat(objNoIgnore).isNotNull(); - assertThat(objNoIgnore.getName()).isEqualTo("ignored"); - assertThat(objNoIgnore.getValue()).isEqualTo(222); - - List emptyList = jsonSerializer.parseObject("[]", List.class, false); - assertThat(emptyList).isEmpty(); - - List emptyListIgnore = jsonSerializer.parseObject("[]", List.class, true); - assertThat(emptyListIgnore).isEmpty(); - - assertThat(jsonSerializer.parseObject(null, TestObject.class, false)).isNull(); - - assertThatThrownBy(() -> jsonSerializer.parseObject("{invalid json}", TestObject.class, false)) - .isInstanceOf(JsonParseException.class) - .hasMessageContaining("Jackson3 deserialize error"); - } - - /** - * The "[]" shortcut in parseObject(String, Class, boolean) must only apply when the - * target type is a Collection. Otherwise it would silently return an empty ArrayList - * cast to T, causing a ClassCastException at the call site. The exact behaviour of the - * underlying library when asked to map "[]" to a POJO may vary (return null, return an - * empty POJO, or throw); the only requirement enforced here is that the result is never - * an ArrayList. - */ - @Test - public void testParseObject_emptyArrayJson_nonCollectionType_doesNotReturnArrayList() { - assertEmptyArrayDoesNotProduceCollection(true); - assertEmptyArrayDoesNotProduceCollection(false); - } - - private void assertEmptyArrayDoesNotProduceCollection(boolean ignoreAutoType) { - Object result; - try { - result = jsonSerializer.parseObject("[]", TestObject.class, ignoreAutoType); - } catch (JsonParseException ignored) { - // throwing is also an acceptable outcome - return; - } - if (result != null) { - assertThat(result).isNotInstanceOf(java.util.Collection.class); - } - } - - public static class TestObject { - private String name; - private int value; - - public TestObject() {} - - public TestObject(String name, int value) { - this.name = name; - this.value = value; - } - - public String getName() { - return name; - } - - public void setName(String name) { - this.name = name; - } - - public int getValue() { - return value; - } - - public void setValue(int value) { - this.value = value; - } - } -} diff --git a/json-common/pom.xml b/json-common/pom.xml deleted file mode 100644 index 1c0d9a5b5ff..00000000000 --- a/json-common/pom.xml +++ /dev/null @@ -1,51 +0,0 @@ - - - - - org.apache.seata - seata-parent - ${revision} - - 4.0.0 - json-common - pom - json-common ${project.version} - json-common top parent for Seata built with Maven - - - json-common-core - - - - - - JDK17Plus - - [17,) - - - json-common-jackson3 - - - - - diff --git a/pom.xml b/pom.xml index 5eef17fa251..b2fb282f29f 100644 --- a/pom.xml +++ b/pom.xml @@ -134,7 +134,7 @@ [17,) - jdk17 + spi-jdk17 @@ -144,7 +144,7 @@ [21,) - jdk21 + spi-jdk21 diff --git a/saga/seata-saga-statelang/src/main/java/org/apache/seata/saga/statelang/parser/JsonParser.java b/saga/seata-saga-statelang/src/main/java/org/apache/seata/saga/statelang/parser/JsonParser.java index ccc2342ef48..6b390b83120 100644 --- a/saga/seata-saga-statelang/src/main/java/org/apache/seata/saga/statelang/parser/JsonParser.java +++ b/saga/seata-saga-statelang/src/main/java/org/apache/seata/saga/statelang/parser/JsonParser.java @@ -19,7 +19,7 @@ /** * Json Parser * - * @deprecated use {@link org.apache.seata.common.json.JsonSerializer} in json-common-core module instead. + * @deprecated use {@link org.apache.seata.common.json.JsonSerializer} in seata-common module instead. */ @Deprecated public interface JsonParser { diff --git a/saga/seata-saga-statelang/src/main/java/org/apache/seata/saga/statelang/parser/JsonParserFactory.java b/saga/seata-saga-statelang/src/main/java/org/apache/seata/saga/statelang/parser/JsonParserFactory.java index a8b4ab9e493..c4b4bd97476 100644 --- a/saga/seata-saga-statelang/src/main/java/org/apache/seata/saga/statelang/parser/JsonParserFactory.java +++ b/saga/seata-saga-statelang/src/main/java/org/apache/seata/saga/statelang/parser/JsonParserFactory.java @@ -25,7 +25,7 @@ /** * JsonParserFactory * - * @deprecated use {@link org.apache.seata.common.json.JsonSerializerFactory} in json-common-core module instead. + * @deprecated use {@link org.apache.seata.common.json.JsonSerializerFactory} in seata-common module instead. */ @Deprecated public class JsonParserFactory { diff --git a/saga/seata-saga-statelang/src/main/java/org/apache/seata/saga/statelang/parser/impl/FastjsonParser.java b/saga/seata-saga-statelang/src/main/java/org/apache/seata/saga/statelang/parser/impl/FastjsonParser.java index 851e4d1bd69..ad49e4462df 100644 --- a/saga/seata-saga-statelang/src/main/java/org/apache/seata/saga/statelang/parser/impl/FastjsonParser.java +++ b/saga/seata-saga-statelang/src/main/java/org/apache/seata/saga/statelang/parser/impl/FastjsonParser.java @@ -25,7 +25,7 @@ /** * JsonParser implement by Fastjson * - * @deprecated use {@link org.apache.seata.common.json.impl.FastjsonJsonSerializer} in json-common-core module instead. + * @deprecated use {@link org.apache.seata.common.json.impl.FastjsonJsonSerializer} in seata-common module instead. */ @Deprecated @LoadLevel(name = FastjsonParser.NAME) diff --git a/saga/seata-saga-statelang/src/main/java/org/apache/seata/saga/statelang/parser/impl/JacksonJsonParser.java b/saga/seata-saga-statelang/src/main/java/org/apache/seata/saga/statelang/parser/impl/JacksonJsonParser.java index f79abf45669..4e4a238ba16 100644 --- a/saga/seata-saga-statelang/src/main/java/org/apache/seata/saga/statelang/parser/impl/JacksonJsonParser.java +++ b/saga/seata-saga-statelang/src/main/java/org/apache/seata/saga/statelang/parser/impl/JacksonJsonParser.java @@ -32,7 +32,7 @@ /** * JsonParser implement by Jackson * - * @deprecated use {@link org.apache.seata.common.json.impl.JacksonJsonSerializer} in json-common-core module instead. + * @deprecated use {@link org.apache.seata.common.json.impl.JacksonJsonSerializer} in seata-common module instead. */ @Deprecated @LoadLevel(name = JacksonJsonParser.NAME) diff --git a/server/pom.xml b/server/pom.xml index 851a4585939..31287410998 100644 --- a/server/pom.xml +++ b/server/pom.xml @@ -486,7 +486,7 @@ ${project.groupId} - seata-jdk21 + seata-spi-jdk21 ${project.version} diff --git a/jdk17/pom.xml b/spi-jdk17/pom.xml similarity index 91% rename from jdk17/pom.xml rename to spi-jdk17/pom.xml index a451c5f9911..64c520ef5a7 100644 --- a/jdk17/pom.xml +++ b/spi-jdk17/pom.xml @@ -26,10 +26,10 @@ ${revision} 4.0.0 - seata-jdk17 + seata-spi-jdk17 jar - seata-jdk17 ${project.version} - JDK 17+ extensions for Seata (Jackson 3 JSON serializer) + seata-spi-jdk17 ${project.version} + JDK 17+ SPI extensions for Seata (Jackson 3 JSON serializer) 17 diff --git a/jdk17/src/main/java/org/apache/seata/common/json/impl/Jackson3JsonSerializer.java b/spi-jdk17/src/main/java/org/apache/seata/common/json/impl/Jackson3JsonSerializer.java similarity index 100% rename from jdk17/src/main/java/org/apache/seata/common/json/impl/Jackson3JsonSerializer.java rename to spi-jdk17/src/main/java/org/apache/seata/common/json/impl/Jackson3JsonSerializer.java diff --git a/jdk17/src/main/resources/META-INF/services/org.apache.seata.common.json.JsonSerializer b/spi-jdk17/src/main/resources/META-INF/services/org.apache.seata.common.json.JsonSerializer similarity index 100% rename from jdk17/src/main/resources/META-INF/services/org.apache.seata.common.json.JsonSerializer rename to spi-jdk17/src/main/resources/META-INF/services/org.apache.seata.common.json.JsonSerializer diff --git a/jdk17/src/test/java/org/apache/seata/common/json/Jackson3AllowlistTest.java b/spi-jdk17/src/test/java/org/apache/seata/common/json/Jackson3AllowlistTest.java similarity index 100% rename from jdk17/src/test/java/org/apache/seata/common/json/Jackson3AllowlistTest.java rename to spi-jdk17/src/test/java/org/apache/seata/common/json/Jackson3AllowlistTest.java diff --git a/jdk17/src/test/java/org/apache/seata/common/json/Jackson3JsonSerializerTest.java b/spi-jdk17/src/test/java/org/apache/seata/common/json/Jackson3JsonSerializerTest.java similarity index 100% rename from jdk17/src/test/java/org/apache/seata/common/json/Jackson3JsonSerializerTest.java rename to spi-jdk17/src/test/java/org/apache/seata/common/json/Jackson3JsonSerializerTest.java diff --git a/jdk21/pom.xml b/spi-jdk21/pom.xml similarity index 90% rename from jdk21/pom.xml rename to spi-jdk21/pom.xml index 859b53663f0..5618cb320da 100644 --- a/jdk21/pom.xml +++ b/spi-jdk21/pom.xml @@ -26,10 +26,10 @@ ${revision} 4.0.0 - seata-jdk21 + seata-spi-jdk21 jar - seata-jdk21 ${project.version} - JDK 21+ extensions for Seata (virtual thread pool provider) + seata-spi-jdk21 ${project.version} + JDK 21+ SPI extensions for Seata (virtual thread pool provider) 21 diff --git a/jdk21/src/main/java/org/apache/seata/common/thread/VirtualScheduledThreadPoolExecutor.java b/spi-jdk21/src/main/java/org/apache/seata/common/thread/VirtualScheduledThreadPoolExecutor.java similarity index 100% rename from jdk21/src/main/java/org/apache/seata/common/thread/VirtualScheduledThreadPoolExecutor.java rename to spi-jdk21/src/main/java/org/apache/seata/common/thread/VirtualScheduledThreadPoolExecutor.java diff --git a/jdk21/src/main/java/org/apache/seata/common/thread/VirtualThreadFactoryHelper.java b/spi-jdk21/src/main/java/org/apache/seata/common/thread/VirtualThreadFactoryHelper.java similarity index 100% rename from jdk21/src/main/java/org/apache/seata/common/thread/VirtualThreadFactoryHelper.java rename to spi-jdk21/src/main/java/org/apache/seata/common/thread/VirtualThreadFactoryHelper.java diff --git a/jdk21/src/main/java/org/apache/seata/common/thread/VirtualThreadPoolExecutor.java b/spi-jdk21/src/main/java/org/apache/seata/common/thread/VirtualThreadPoolExecutor.java similarity index 100% rename from jdk21/src/main/java/org/apache/seata/common/thread/VirtualThreadPoolExecutor.java rename to spi-jdk21/src/main/java/org/apache/seata/common/thread/VirtualThreadPoolExecutor.java diff --git a/jdk21/src/main/java/org/apache/seata/common/thread/VirtualThreadPoolProvider.java b/spi-jdk21/src/main/java/org/apache/seata/common/thread/VirtualThreadPoolProvider.java similarity index 100% rename from jdk21/src/main/java/org/apache/seata/common/thread/VirtualThreadPoolProvider.java rename to spi-jdk21/src/main/java/org/apache/seata/common/thread/VirtualThreadPoolProvider.java diff --git a/jdk21/src/main/resources/META-INF/services/org.apache.seata.common.thread.ThreadPoolProvider b/spi-jdk21/src/main/resources/META-INF/services/org.apache.seata.common.thread.ThreadPoolProvider similarity index 100% rename from jdk21/src/main/resources/META-INF/services/org.apache.seata.common.thread.ThreadPoolProvider rename to spi-jdk21/src/main/resources/META-INF/services/org.apache.seata.common.thread.ThreadPoolProvider diff --git a/jdk21/src/test/java/org/apache/seata/common/thread/VirtualThreadPoolProviderTest.java b/spi-jdk21/src/test/java/org/apache/seata/common/thread/VirtualThreadPoolProviderTest.java similarity index 100% rename from jdk21/src/test/java/org/apache/seata/common/thread/VirtualThreadPoolProviderTest.java rename to spi-jdk21/src/test/java/org/apache/seata/common/thread/VirtualThreadPoolProviderTest.java diff --git a/tcc/src/main/java/org/apache/seata/rm/tcc/json/FastJsonParser.java b/tcc/src/main/java/org/apache/seata/rm/tcc/json/FastJsonParser.java index 120e986f360..11442b86b06 100644 --- a/tcc/src/main/java/org/apache/seata/rm/tcc/json/FastJsonParser.java +++ b/tcc/src/main/java/org/apache/seata/rm/tcc/json/FastJsonParser.java @@ -22,7 +22,7 @@ import org.apache.seata.integration.tx.api.json.JsonParser; /** - * @deprecated use {@link org.apache.seata.common.json.impl.FastjsonJsonSerializer} in json-common-core module instead. + * @deprecated use {@link org.apache.seata.common.json.impl.FastjsonJsonSerializer} in seata-common module instead. */ @Deprecated @LoadLevel(name = Constants.FASTJSON_JSON_PARSER_NAME) diff --git a/tcc/src/main/java/org/apache/seata/rm/tcc/json/GsonJsonParser.java b/tcc/src/main/java/org/apache/seata/rm/tcc/json/GsonJsonParser.java index 26caf1e678e..efe27b3f902 100644 --- a/tcc/src/main/java/org/apache/seata/rm/tcc/json/GsonJsonParser.java +++ b/tcc/src/main/java/org/apache/seata/rm/tcc/json/GsonJsonParser.java @@ -25,7 +25,7 @@ import java.lang.reflect.Modifier; /** - * @deprecated use {@link org.apache.seata.common.json.impl.GsonJsonSerializer} in json-common-core module instead. + * @deprecated use {@link org.apache.seata.common.json.impl.GsonJsonSerializer} in seata-common module instead. */ @Deprecated @LoadLevel(name = Constants.GSON_JSON_PARSER_NAME) diff --git a/tcc/src/main/java/org/apache/seata/rm/tcc/json/JacksonJsonParser.java b/tcc/src/main/java/org/apache/seata/rm/tcc/json/JacksonJsonParser.java index ea33594c244..a51f9cff88b 100644 --- a/tcc/src/main/java/org/apache/seata/rm/tcc/json/JacksonJsonParser.java +++ b/tcc/src/main/java/org/apache/seata/rm/tcc/json/JacksonJsonParser.java @@ -28,7 +28,7 @@ import java.io.IOException; /** - * @deprecated use {@link org.apache.seata.common.json.impl.JacksonJsonSerializer} in json-common-core module instead. + * @deprecated use {@link org.apache.seata.common.json.impl.JacksonJsonSerializer} in seata-common module instead. */ @Deprecated @LoadLevel(name = Constants.JACKSON_JSON_PARSER_NAME) diff --git a/threadpool-loom/pom.xml b/threadpool-loom/pom.xml deleted file mode 100644 index 1717daf07dd..00000000000 --- a/threadpool-loom/pom.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - - - org.apache.seata - seata-parent - ${revision} - - 4.0.0 - seata-threadpool-loom - jar - seata-threadpool-loom ${project.version} - Loom-backed thread pool provider for Seata managed thread pools - - - 21 - 21 - - - - - org.apache.seata - seata-threadpool - ${project.version} - - - diff --git a/threadpool-loom/src/main/java/org/apache/seata/common/thread/VirtualScheduledThreadPoolExecutor.java b/threadpool-loom/src/main/java/org/apache/seata/common/thread/VirtualScheduledThreadPoolExecutor.java deleted file mode 100644 index e7740c93dae..00000000000 --- a/threadpool-loom/src/main/java/org/apache/seata/common/thread/VirtualScheduledThreadPoolExecutor.java +++ /dev/null @@ -1,31 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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.apache.seata.common.thread; - -import java.util.concurrent.RejectedExecutionHandler; -import java.util.concurrent.ScheduledThreadPoolExecutor; - -/** - * Virtual-thread-backed scheduled thread pool implementation. - */ -public class VirtualScheduledThreadPoolExecutor extends ScheduledThreadPoolExecutor { - - public VirtualScheduledThreadPoolExecutor( - String threadPrefix, int corePoolSize, boolean daemon, RejectedExecutionHandler rejectedHandler) { - super(corePoolSize, VirtualThreadFactoryHelper.newThreadFactory(threadPrefix, daemon), rejectedHandler); - } -} diff --git a/threadpool-loom/src/main/java/org/apache/seata/common/thread/VirtualThreadFactoryHelper.java b/threadpool-loom/src/main/java/org/apache/seata/common/thread/VirtualThreadFactoryHelper.java deleted file mode 100644 index a0ca608fc8f..00000000000 --- a/threadpool-loom/src/main/java/org/apache/seata/common/thread/VirtualThreadFactoryHelper.java +++ /dev/null @@ -1,39 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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.apache.seata.common.thread; - -import java.util.concurrent.ThreadFactory; - -/** - * Helper for creating consistently named virtual-thread factories. - */ -final class VirtualThreadFactoryHelper { - - private VirtualThreadFactoryHelper() {} - - static ThreadFactory newThreadFactory(String threadPrefix, boolean daemon) { - if (!daemon) { - throw new IllegalArgumentException( - "Virtual threads are always daemon threads; daemon=false is not supported"); - } - return Thread.ofVirtual().name(normalizePrefix(threadPrefix), 1).factory(); - } - - private static String normalizePrefix(String threadPrefix) { - return threadPrefix.endsWith("-") ? threadPrefix : threadPrefix + "-"; - } -} diff --git a/threadpool-loom/src/main/java/org/apache/seata/common/thread/VirtualThreadPoolExecutor.java b/threadpool-loom/src/main/java/org/apache/seata/common/thread/VirtualThreadPoolExecutor.java deleted file mode 100644 index e0979228155..00000000000 --- a/threadpool-loom/src/main/java/org/apache/seata/common/thread/VirtualThreadPoolExecutor.java +++ /dev/null @@ -1,47 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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.apache.seata.common.thread; - -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.RejectedExecutionHandler; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; - -/** - * Virtual-thread-backed thread pool implementation. - */ -public class VirtualThreadPoolExecutor extends ThreadPoolExecutor { - - public VirtualThreadPoolExecutor( - String threadPrefix, - int corePoolSize, - int maximumPoolSize, - long keepAliveTime, - TimeUnit unit, - BlockingQueue workQueue, - boolean daemon, - RejectedExecutionHandler rejectedHandler) { - super( - corePoolSize, - maximumPoolSize, - keepAliveTime, - unit, - workQueue, - VirtualThreadFactoryHelper.newThreadFactory(threadPrefix, daemon), - rejectedHandler); - } -} diff --git a/threadpool-loom/src/main/java/org/apache/seata/common/thread/VirtualThreadPoolProvider.java b/threadpool-loom/src/main/java/org/apache/seata/common/thread/VirtualThreadPoolProvider.java deleted file mode 100644 index 5a0c2b10ba4..00000000000 --- a/threadpool-loom/src/main/java/org/apache/seata/common/thread/VirtualThreadPoolProvider.java +++ /dev/null @@ -1,53 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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.apache.seata.common.thread; - -import org.apache.seata.common.loader.LoadLevel; - -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.RejectedExecutionHandler; -import java.util.concurrent.ScheduledThreadPoolExecutor; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; - -/** - * JDK 21+ SPI implementation that creates virtual-thread-backed business pools. - * Virtual threads are always daemon threads, so {@code daemon=false} is rejected. - */ -@LoadLevel(name = "virtual", order = Integer.MIN_VALUE) -public class VirtualThreadPoolProvider implements ThreadPoolProvider { - - @Override - public ThreadPoolExecutor newThreadPoolExecutor( - String threadPrefix, - int corePoolSize, - int maximumPoolSize, - long keepAliveTime, - TimeUnit unit, - BlockingQueue workQueue, - boolean daemon, - RejectedExecutionHandler rejectedHandler) { - return new VirtualThreadPoolExecutor( - threadPrefix, corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, daemon, rejectedHandler); - } - - @Override - public ScheduledThreadPoolExecutor newScheduledThreadPoolExecutor( - String threadPrefix, int corePoolSize, boolean daemon, RejectedExecutionHandler rejectedHandler) { - return new VirtualScheduledThreadPoolExecutor(threadPrefix, corePoolSize, daemon, rejectedHandler); - } -} diff --git a/threadpool-loom/src/main/resources/META-INF/services/org.apache.seata.common.thread.ThreadPoolProvider b/threadpool-loom/src/main/resources/META-INF/services/org.apache.seata.common.thread.ThreadPoolProvider deleted file mode 100644 index 602fa94509c..00000000000 --- a/threadpool-loom/src/main/resources/META-INF/services/org.apache.seata.common.thread.ThreadPoolProvider +++ /dev/null @@ -1,17 +0,0 @@ -# -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You 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. -# -org.apache.seata.common.thread.VirtualThreadPoolProvider diff --git a/threadpool-loom/src/test/java/org/apache/seata/common/thread/VirtualThreadPoolProviderTest.java b/threadpool-loom/src/test/java/org/apache/seata/common/thread/VirtualThreadPoolProviderTest.java deleted file mode 100644 index b283f246faf..00000000000 --- a/threadpool-loom/src/test/java/org/apache/seata/common/thread/VirtualThreadPoolProviderTest.java +++ /dev/null @@ -1,99 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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.apache.seata.common.thread; - -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.api.condition.EnabledIfSystemProperty; - -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ScheduledThreadPoolExecutor; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; - -import static org.assertj.core.api.Assertions.assertThat; -import static org.assertj.core.api.Assertions.assertThatThrownBy; - -/** - * Tests for loom-backed thread pool providers. - */ -@EnabledIfSystemProperty(named = "java.specification.version", matches = "(21|2[2-9]|[3-9][0-9])") -public class VirtualThreadPoolProviderTest { - - @AfterEach - public void tearDown() { - ThreadPoolRuntimeEnvironment.reset(); - } - - @Test - public void testVirtualThreadPoolExecutorKeepsConfiguredBounds() { - ThreadPoolRuntimeEnvironment.setThreadPoolTypeSupplier(() -> "virtual"); - ThreadPoolRuntimeEnvironment.setJdkFeatureSupplier(() -> 21); - LinkedBlockingQueue workQueue = new LinkedBlockingQueue<>(); - - ThreadPoolExecutor executor = - ThreadPoolExecutorFactory.newThreadPoolExecutor("virtualPool", 1, 2, 60, TimeUnit.SECONDS, workQueue); - try { - Thread thread = executor.getThreadFactory().newThread(() -> {}); - - assertThat(executor).isInstanceOf(VirtualThreadPoolExecutor.class); - assertThat(executor.getMaximumPoolSize()).isEqualTo(2); - assertThat(executor.getKeepAliveTime(TimeUnit.SECONDS)).isEqualTo(60); - assertThat(executor.getQueue()).isSameAs(workQueue); - assertThat(thread.isVirtual()).isTrue(); - assertThat(thread.isDaemon()).isTrue(); - assertThat(thread.getName()).startsWith("virtualPool-"); - } finally { - executor.shutdownNow(); - } - } - - @Test - public void testVirtualScheduledThreadPoolExecutorUsesVirtualThreads() { - ThreadPoolRuntimeEnvironment.setThreadPoolTypeSupplier(() -> "virtual"); - ThreadPoolRuntimeEnvironment.setJdkFeatureSupplier(() -> 21); - - ScheduledThreadPoolExecutor executor = - ThreadPoolExecutorFactory.newScheduledThreadPoolExecutor("virtualSchedule", 1, true); - try { - Thread thread = executor.getThreadFactory().newThread(() -> {}); - - assertThat(executor).isInstanceOf(VirtualScheduledThreadPoolExecutor.class); - assertThat(thread.isVirtual()).isTrue(); - assertThat(thread.isDaemon()).isTrue(); - assertThat(thread.getName()).startsWith("virtualSchedule-"); - } finally { - executor.shutdownNow(); - } - } - - @Test - public void testVirtualThreadPoolsRejectNonDaemonRequests() { - ThreadPoolRuntimeEnvironment.setThreadPoolTypeSupplier(() -> "virtual"); - ThreadPoolRuntimeEnvironment.setJdkFeatureSupplier(() -> 21); - - assertThatThrownBy(() -> ThreadPoolExecutorFactory.newThreadPoolExecutor( - "virtualReject", 1, 1, 0, TimeUnit.MILLISECONDS, new LinkedBlockingQueue(), false)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("always daemon"); - - assertThatThrownBy(() -> - ThreadPoolExecutorFactory.newScheduledThreadPoolExecutor("virtualRejectSchedule", 1, false)) - .isInstanceOf(IllegalArgumentException.class) - .hasMessageContaining("always daemon"); - } -} diff --git a/threadpool/pom.xml b/threadpool/pom.xml deleted file mode 100644 index e0ec17f0e49..00000000000 --- a/threadpool/pom.xml +++ /dev/null @@ -1,46 +0,0 @@ - - - - - org.apache.seata - seata-parent - ${revision} - - 4.0.0 - seata-threadpool - jar - seata-threadpool ${project.version} - Managed thread pool factory for Seata - - - - org.apache.seata - seata-common - ${project.version} - - - org.apache.seata - seata-config-core - ${project.version} - - - diff --git a/threadpool/src/main/java/org/apache/seata/common/thread/PlatformThreadPoolExecutor.java b/threadpool/src/main/java/org/apache/seata/common/thread/PlatformThreadPoolExecutor.java deleted file mode 100644 index 6ed58894494..00000000000 --- a/threadpool/src/main/java/org/apache/seata/common/thread/PlatformThreadPoolExecutor.java +++ /dev/null @@ -1,40 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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.apache.seata.common.thread; - -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.RejectedExecutionHandler; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; - -/** - * Historical platform-thread-backed thread pool implementation. - */ -public class PlatformThreadPoolExecutor extends ThreadPoolExecutor { - - public PlatformThreadPoolExecutor( - int corePoolSize, - int maximumPoolSize, - long keepAliveTime, - TimeUnit unit, - BlockingQueue workQueue, - ThreadFactory threadFactory, - RejectedExecutionHandler rejectedHandler) { - super(corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, threadFactory, rejectedHandler); - } -} diff --git a/threadpool/src/main/java/org/apache/seata/common/thread/PlatformThreadPoolProvider.java b/threadpool/src/main/java/org/apache/seata/common/thread/PlatformThreadPoolProvider.java deleted file mode 100644 index 961eca35e68..00000000000 --- a/threadpool/src/main/java/org/apache/seata/common/thread/PlatformThreadPoolProvider.java +++ /dev/null @@ -1,59 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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.apache.seata.common.thread; - -import org.apache.seata.common.loader.LoadLevel; - -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.RejectedExecutionHandler; -import java.util.concurrent.ScheduledThreadPoolExecutor; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; - -/** - * Default SPI implementation that preserves the current Seata thread pool behavior. - */ -@LoadLevel(name = "platform", order = ThreadPoolProviderOrders.DEFAULT_PROVIDER_ORDER) -public class PlatformThreadPoolProvider implements ThreadPoolProvider { - - @Override - public ThreadPoolExecutor newThreadPoolExecutor( - String threadPrefix, - int corePoolSize, - int maximumPoolSize, - long keepAliveTime, - TimeUnit unit, - BlockingQueue workQueue, - boolean daemon, - RejectedExecutionHandler rejectedHandler) { - return new PlatformThreadPoolExecutor( - corePoolSize, - maximumPoolSize, - keepAliveTime, - unit, - workQueue, - new NamedThreadFactory(threadPrefix, maximumPoolSize, daemon), - rejectedHandler); - } - - @Override - public ScheduledThreadPoolExecutor newScheduledThreadPoolExecutor( - String threadPrefix, int corePoolSize, boolean daemon, RejectedExecutionHandler rejectedHandler) { - return new ScheduledThreadPoolExecutor( - corePoolSize, new NamedThreadFactory(threadPrefix, corePoolSize, daemon), rejectedHandler); - } -} diff --git a/threadpool/src/main/java/org/apache/seata/common/thread/ThreadPoolExecutorFactory.java b/threadpool/src/main/java/org/apache/seata/common/thread/ThreadPoolExecutorFactory.java deleted file mode 100644 index e1b3090adf2..00000000000 --- a/threadpool/src/main/java/org/apache/seata/common/thread/ThreadPoolExecutorFactory.java +++ /dev/null @@ -1,195 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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.apache.seata.common.thread; - -import org.apache.seata.common.loader.EnhancedServiceLoader; -import org.apache.seata.common.loader.EnhancedServiceNotFoundException; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.Objects; -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.RejectedExecutionHandler; -import java.util.concurrent.ScheduledThreadPoolExecutor; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; - -/** - * Central factory used by Seata managed thread pools. - */ -public final class ThreadPoolExecutorFactory { - - private static final Logger LOGGER = LoggerFactory.getLogger(ThreadPoolExecutorFactory.class); - - private ThreadPoolExecutorFactory() {} - - public static ThreadFactory newThreadFactory(String threadPrefix, int totalSize) { - return newThreadFactory(threadPrefix, totalSize, true); - } - - public static ThreadFactory newThreadFactory(String threadPrefix, int totalSize, boolean daemon) { - Objects.requireNonNull(threadPrefix, "threadPrefix must not be null"); - return new NamedThreadFactory(threadPrefix, totalSize, daemon); - } - - public static ThreadPoolExecutor newThreadPoolExecutor( - String threadPrefix, - int corePoolSize, - int maximumPoolSize, - long keepAliveTime, - TimeUnit unit, - BlockingQueue workQueue) { - return newThreadPoolExecutor(threadPrefix, corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, true); - } - - public static ThreadPoolExecutor newThreadPoolExecutor( - String threadPrefix, - int corePoolSize, - int maximumPoolSize, - long keepAliveTime, - TimeUnit unit, - BlockingQueue workQueue, - boolean daemon) { - return newThreadPoolExecutor( - threadPrefix, - corePoolSize, - maximumPoolSize, - keepAliveTime, - unit, - workQueue, - daemon, - new ThreadPoolExecutor.AbortPolicy()); - } - - public static ThreadPoolExecutor newThreadPoolExecutor( - String threadPrefix, - int corePoolSize, - int maximumPoolSize, - long keepAliveTime, - TimeUnit unit, - BlockingQueue workQueue, - RejectedExecutionHandler rejectedHandler) { - return newThreadPoolExecutor( - threadPrefix, corePoolSize, maximumPoolSize, keepAliveTime, unit, workQueue, true, rejectedHandler); - } - - public static ThreadPoolExecutor newThreadPoolExecutor( - String threadPrefix, - int corePoolSize, - int maximumPoolSize, - long keepAliveTime, - TimeUnit unit, - BlockingQueue workQueue, - boolean daemon, - RejectedExecutionHandler rejectedHandler) { - validateThreadPoolArguments(threadPrefix, corePoolSize, maximumPoolSize, keepAliveTime, unit); - return resolveThreadPoolProvider() - .newThreadPoolExecutor( - threadPrefix, - corePoolSize, - maximumPoolSize, - keepAliveTime, - unit, - Objects.requireNonNull(workQueue, "workQueue must not be null"), - daemon, - Objects.requireNonNull(rejectedHandler, "rejectedHandler must not be null")); - } - - public static ScheduledThreadPoolExecutor newScheduledThreadPoolExecutor(String threadPrefix, int corePoolSize) { - return newScheduledThreadPoolExecutor(threadPrefix, corePoolSize, true); - } - - public static ScheduledThreadPoolExecutor newScheduledThreadPoolExecutor( - String threadPrefix, int corePoolSize, boolean daemon) { - return newScheduledThreadPoolExecutor(threadPrefix, corePoolSize, daemon, new ThreadPoolExecutor.AbortPolicy()); - } - - public static ScheduledThreadPoolExecutor newScheduledThreadPoolExecutor( - String threadPrefix, int corePoolSize, boolean daemon, RejectedExecutionHandler rejectedHandler) { - validateScheduledThreadPoolArguments(threadPrefix, corePoolSize); - return resolveThreadPoolProvider() - .newScheduledThreadPoolExecutor( - threadPrefix, - corePoolSize, - daemon, - Objects.requireNonNull(rejectedHandler, "rejectedHandler must not be null")); - } - - private static void validateThreadPoolArguments( - String threadPrefix, int corePoolSize, int maximumPoolSize, long keepAliveTime, TimeUnit unit) { - Objects.requireNonNull(threadPrefix, "threadPrefix must not be null"); - Objects.requireNonNull(unit, "timeUnit must not be null"); - if (corePoolSize < 0) { - throw new IllegalArgumentException("corePoolSize must not be negative"); - } - if (maximumPoolSize <= 0) { - throw new IllegalArgumentException("maximumPoolSize must be greater than zero"); - } - if (maximumPoolSize < corePoolSize) { - throw new IllegalArgumentException("maximumPoolSize must be greater than or equal to corePoolSize"); - } - if (keepAliveTime < 0) { - throw new IllegalArgumentException("keepAliveTime must not be negative"); - } - } - - private static void validateScheduledThreadPoolArguments(String threadPrefix, int corePoolSize) { - Objects.requireNonNull(threadPrefix, "threadPrefix must not be null"); - if (corePoolSize < 0) { - throw new IllegalArgumentException("corePoolSize must not be negative"); - } - } - - private static ThreadPoolProvider resolveThreadPoolProvider() { - ThreadPoolType threadPoolType = ThreadPoolRuntimeEnvironment.resolveThreadPoolType(); - if (threadPoolType == ThreadPoolType.VIRTUAL && ThreadPoolProviderHolder.VIRTUAL_THREAD_POOL_PROVIDER != null) { - return ThreadPoolProviderHolder.VIRTUAL_THREAD_POOL_PROVIDER; - } - if (threadPoolType == ThreadPoolType.VIRTUAL) { - // loadOptional() already warned that the provider is absent at initialisation time. - // This second warning is intentionally kept to surface the per-request fallback - // decision so that operators can correlate the missing-provider startup message - // with the actual thread-pool mode that is in effect. - LOGGER.warn( - "Virtual thread pool was selected but the virtual-thread SPI provider (seata-threadpool-virtual) " - + "is not present on the classpath. Falling back to platform threads."); - } - return ThreadPoolProviderHolder.PLATFORM_THREAD_POOL_PROVIDER; - } - - private static final class ThreadPoolProviderHolder { - private static final ThreadPoolProvider PLATFORM_THREAD_POOL_PROVIDER = - EnhancedServiceLoader.load(ThreadPoolProvider.class, ThreadPoolType.PLATFORM.getCode()); - private static final ThreadPoolProvider VIRTUAL_THREAD_POOL_PROVIDER = - loadOptional(ThreadPoolType.VIRTUAL.getCode()); - - private ThreadPoolProviderHolder() {} - } - - private static ThreadPoolProvider loadOptional(String activateName) { - try { - return EnhancedServiceLoader.load(ThreadPoolProvider.class, activateName); - } catch (EnhancedServiceNotFoundException ignored) { - LOGGER.warn( - "Virtual thread pool SPI provider '{}' is not available on the classpath. " - + "Virtual threads cannot be used.", - activateName); - return null; - } - } -} diff --git a/threadpool/src/main/java/org/apache/seata/common/thread/ThreadPoolProvider.java b/threadpool/src/main/java/org/apache/seata/common/thread/ThreadPoolProvider.java deleted file mode 100644 index b788f2c10da..00000000000 --- a/threadpool/src/main/java/org/apache/seata/common/thread/ThreadPoolProvider.java +++ /dev/null @@ -1,42 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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.apache.seata.common.thread; - -import java.util.concurrent.BlockingQueue; -import java.util.concurrent.RejectedExecutionHandler; -import java.util.concurrent.ScheduledThreadPoolExecutor; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; - -/** - * SPI abstraction used by Seata managed thread pools. - */ -public interface ThreadPoolProvider { - - ThreadPoolExecutor newThreadPoolExecutor( - String threadPrefix, - int corePoolSize, - int maximumPoolSize, - long keepAliveTime, - TimeUnit unit, - BlockingQueue workQueue, - boolean daemon, - RejectedExecutionHandler rejectedHandler); - - ScheduledThreadPoolExecutor newScheduledThreadPoolExecutor( - String threadPrefix, int corePoolSize, boolean daemon, RejectedExecutionHandler rejectedHandler); -} diff --git a/threadpool/src/main/java/org/apache/seata/common/thread/ThreadPoolProviderOrders.java b/threadpool/src/main/java/org/apache/seata/common/thread/ThreadPoolProviderOrders.java deleted file mode 100644 index d8e1c32ae7a..00000000000 --- a/threadpool/src/main/java/org/apache/seata/common/thread/ThreadPoolProviderOrders.java +++ /dev/null @@ -1,27 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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.apache.seata.common.thread; - -/** - * Shared order constants for {@link ThreadPoolProvider} SPI implementations. - */ -public final class ThreadPoolProviderOrders { - - public static final int DEFAULT_PROVIDER_ORDER = 0; - - private ThreadPoolProviderOrders() {} -} diff --git a/threadpool/src/main/java/org/apache/seata/common/thread/ThreadPoolRuntimeEnvironment.java b/threadpool/src/main/java/org/apache/seata/common/thread/ThreadPoolRuntimeEnvironment.java deleted file mode 100644 index bfcfce94f35..00000000000 --- a/threadpool/src/main/java/org/apache/seata/common/thread/ThreadPoolRuntimeEnvironment.java +++ /dev/null @@ -1,108 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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.apache.seata.common.thread; - -import org.apache.seata.common.ConfigurationKeys; -import org.apache.seata.common.DefaultValues; -import org.apache.seata.config.Configuration; -import org.apache.seata.config.ConfigurationFactory; -import org.slf4j.Logger; -import org.slf4j.LoggerFactory; - -import java.util.function.IntSupplier; -import java.util.function.Supplier; - -/** - * Runtime helper used to resolve the thread pool mode. - */ -final class ThreadPoolRuntimeEnvironment { - - private static final Logger LOGGER = LoggerFactory.getLogger(ThreadPoolRuntimeEnvironment.class); - - private static final Supplier DEFAULT_THREAD_POOL_TYPE_SUPPLIER = - ThreadPoolRuntimeEnvironment::loadConfiguredThreadPoolType; - private static final IntSupplier DEFAULT_JDK_FEATURE_SUPPLIER = ThreadPoolRuntimeEnvironment::javaFeatureVersion; - - private static volatile Supplier threadPoolTypeSupplier = DEFAULT_THREAD_POOL_TYPE_SUPPLIER; - private static volatile IntSupplier jdkFeatureSupplier = DEFAULT_JDK_FEATURE_SUPPLIER; - - private ThreadPoolRuntimeEnvironment() {} - - static ThreadPoolType resolveThreadPoolType() { - ThreadPoolType configuredType = ThreadPoolType.from(threadPoolTypeSupplier.get()); - if (configuredType == ThreadPoolType.PLATFORM) { - return ThreadPoolType.PLATFORM; - } - int jdkFeature = jdkFeatureSupplier.getAsInt(); - if (configuredType == ThreadPoolType.VIRTUAL) { - if (jdkFeature < 21) { - LOGGER.warn( - "transport.threadpool=virtual is configured but the current JDK feature version is {} (<21). " - + "Virtual threads are not supported; falling back to platform threads.", - jdkFeature); - } - return jdkFeature >= 21 ? ThreadPoolType.VIRTUAL : ThreadPoolType.PLATFORM; - } - return jdkFeature >= 25 ? ThreadPoolType.VIRTUAL : ThreadPoolType.PLATFORM; - } - - static void setThreadPoolTypeSupplier(Supplier supplier) { - threadPoolTypeSupplier = supplier == null ? DEFAULT_THREAD_POOL_TYPE_SUPPLIER : supplier; - } - - static void setJdkFeatureSupplier(IntSupplier supplier) { - jdkFeatureSupplier = supplier == null ? DEFAULT_JDK_FEATURE_SUPPLIER : supplier; - } - - static void reset() { - threadPoolTypeSupplier = DEFAULT_THREAD_POOL_TYPE_SUPPLIER; - jdkFeatureSupplier = DEFAULT_JDK_FEATURE_SUPPLIER; - } - - private static String loadConfiguredThreadPoolType() { - Configuration configuration = ConfigurationFactory.getInstance(); - return configuration.getConfig( - ConfigurationKeys.TRANSPORT_THREADPOOL, DefaultValues.DEFAULT_TRANSPORT_THREADPOOL); - } - - static int javaFeatureVersion() { - String specVersion = System.getProperty("java.specification.version", "1.8"); - if (specVersion != null) { - specVersion = specVersion.trim(); - } - if (specVersion.startsWith("1.")) { - // Java 8 and earlier: "1.8", "1.7", etc. - // Be tolerant of values like "1.8 ", "1.8.0", or "1.x". - String legacyPart = specVersion.substring(2); - int dotIndex = legacyPart.indexOf('.'); - if (dotIndex >= 0) { - legacyPart = legacyPart.substring(0, dotIndex); - } - try { - return Integer.parseInt(legacyPart); - } catch (NumberFormatException e) { - return 8; - } - } - // Java 9+: "9", "11", "17", "21", "25", etc. - try { - return Integer.parseInt(specVersion); - } catch (NumberFormatException e) { - return 8; - } - } -} diff --git a/threadpool/src/main/java/org/apache/seata/common/thread/ThreadPoolType.java b/threadpool/src/main/java/org/apache/seata/common/thread/ThreadPoolType.java deleted file mode 100644 index 4bc05c7fedd..00000000000 --- a/threadpool/src/main/java/org/apache/seata/common/thread/ThreadPoolType.java +++ /dev/null @@ -1,61 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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.apache.seata.common.thread; - -import org.apache.seata.common.util.StringUtils; - -/** - * Supported Seata thread pool modes. - */ -public enum ThreadPoolType { - /** - * Automatic selection: uses virtual threads when running on JDK 25 or later (with the loom extension - * present), otherwise falls back to platform threads. - */ - AUTO("auto"), - /** - * Always uses platform (OS) threads regardless of the JDK version. - */ - PLATFORM("platform"), - /** - * Prefers virtual threads when running on JDK 21 or later (with the loom extension present), - * otherwise falls back to platform threads. - */ - VIRTUAL("virtual"); - - private final String code; - - ThreadPoolType(String code) { - this.code = code; - } - - public String getCode() { - return code; - } - - public static ThreadPoolType from(String code) { - if (StringUtils.isBlank(code)) { - return AUTO; - } - for (ThreadPoolType threadPoolType : values()) { - if (threadPoolType.code.equalsIgnoreCase(code)) { - return threadPoolType; - } - } - return AUTO; - } -} diff --git a/threadpool/src/main/resources/META-INF/services/org.apache.seata.common.thread.ThreadPoolProvider b/threadpool/src/main/resources/META-INF/services/org.apache.seata.common.thread.ThreadPoolProvider deleted file mode 100644 index 7c56610bd3e..00000000000 --- a/threadpool/src/main/resources/META-INF/services/org.apache.seata.common.thread.ThreadPoolProvider +++ /dev/null @@ -1,17 +0,0 @@ -# -# Licensed to the Apache Software Foundation (ASF) under one or more -# contributor license agreements. See the NOTICE file distributed with -# this work for additional information regarding copyright ownership. -# The ASF licenses this file to You 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. -# -org.apache.seata.common.thread.PlatformThreadPoolProvider diff --git a/threadpool/src/test/java/org/apache/seata/common/thread/ThreadPoolExecutorFactoryTest.java b/threadpool/src/test/java/org/apache/seata/common/thread/ThreadPoolExecutorFactoryTest.java deleted file mode 100644 index d72c957d218..00000000000 --- a/threadpool/src/test/java/org/apache/seata/common/thread/ThreadPoolExecutorFactoryTest.java +++ /dev/null @@ -1,181 +0,0 @@ -/* - * Licensed to the Apache Software Foundation (ASF) under one or more - * contributor license agreements. See the NOTICE file distributed with - * this work for additional information regarding copyright ownership. - * The ASF licenses this file to You 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.apache.seata.common.thread; - -import org.apache.seata.common.DefaultValues; -import org.junit.jupiter.api.AfterEach; -import org.junit.jupiter.api.Test; -import org.junit.jupiter.params.ParameterizedTest; -import org.junit.jupiter.params.provider.CsvSource; - -import java.util.concurrent.LinkedBlockingQueue; -import java.util.concurrent.ScheduledThreadPoolExecutor; -import java.util.concurrent.ThreadFactory; -import java.util.concurrent.ThreadPoolExecutor; -import java.util.concurrent.TimeUnit; - -import static org.assertj.core.api.Assertions.assertThat; - -/** - * Tests for {@link ThreadPoolExecutorFactory}. - */ -public class ThreadPoolExecutorFactoryTest { - - @AfterEach - public void tearDown() { - ThreadPoolRuntimeEnvironment.reset(); - } - - @Test - public void testNewThreadFactoryCreatesThreadsWithExpectedName() { - ThreadFactory threadFactory = ThreadPoolExecutorFactory.newThreadFactory("factoryTest", 2, true); - - Thread thread = threadFactory.newThread(() -> {}); - - assertThat(thread.getName()).startsWith("factoryTest"); - assertThat(thread.isDaemon()).isTrue(); - } - - @Test - public void testNewThreadPoolExecutor() { - ThreadPoolRuntimeEnvironment.setThreadPoolTypeSupplier(() -> "platform"); - ThreadPoolExecutor executor = ThreadPoolExecutorFactory.newThreadPoolExecutor( - "poolTest", 1, 2, 60, TimeUnit.SECONDS, new LinkedBlockingQueue()); - try { - Thread thread = executor.getThreadFactory().newThread(() -> {}); - - assertThat(executor).isInstanceOf(PlatformThreadPoolExecutor.class); - assertThat(executor.getCorePoolSize()).isEqualTo(1); - assertThat(executor.getMaximumPoolSize()).isEqualTo(2); - assertThat(thread.getName()).startsWith("poolTest"); - assertThat(thread.isDaemon()).isTrue(); - } finally { - executor.shutdownNow(); - } - } - - @Test - public void testNewScheduledThreadPoolExecutor() { - ThreadPoolRuntimeEnvironment.setThreadPoolTypeSupplier(() -> "platform"); - ScheduledThreadPoolExecutor executor = - ThreadPoolExecutorFactory.newScheduledThreadPoolExecutor("scheduleTest", 1, true); - try { - Thread thread = executor.getThreadFactory().newThread(() -> {}); - - assertThat(executor.getCorePoolSize()).isEqualTo(1); - assertThat(thread.getName()).startsWith("scheduleTest"); - assertThat(thread.isDaemon()).isTrue(); - } finally { - executor.shutdownNow(); - } - } - - @Test - public void testNewThreadPoolExecutorAllowsZeroCorePoolSize() { - ThreadPoolRuntimeEnvironment.setThreadPoolTypeSupplier(() -> "platform"); - ThreadPoolExecutor executor = ThreadPoolExecutorFactory.newThreadPoolExecutor( - "zeroCorePool", 0, 1, 60, TimeUnit.SECONDS, new LinkedBlockingQueue()); - try { - assertThat(executor.getCorePoolSize()).isZero(); - assertThat(executor.getMaximumPoolSize()).isEqualTo(1); - } finally { - executor.shutdownNow(); - } - } - - @Test - public void testNewScheduledThreadPoolExecutorAllowsZeroCorePoolSize() { - ThreadPoolRuntimeEnvironment.setThreadPoolTypeSupplier(() -> "platform"); - ScheduledThreadPoolExecutor executor = - ThreadPoolExecutorFactory.newScheduledThreadPoolExecutor("zeroSchedule", 0, true); - try { - assertThat(executor.getCorePoolSize()).isZero(); - } finally { - executor.shutdownNow(); - } - } - - @Test - public void testVirtualThreadPoolFallsBackToPlatformWithoutLoomProvider() { - ThreadPoolRuntimeEnvironment.setThreadPoolTypeSupplier(() -> "virtual"); - ThreadPoolRuntimeEnvironment.setJdkFeatureSupplier(() -> 21); - - ThreadPoolExecutor executor = ThreadPoolExecutorFactory.newThreadPoolExecutor( - "virtualFallback", 1, 2, 60, TimeUnit.SECONDS, new LinkedBlockingQueue()); - try { - assertThat(executor).isInstanceOf(PlatformThreadPoolExecutor.class); - assertThat(executor.getMaximumPoolSize()).isEqualTo(2); - } finally { - executor.shutdownNow(); - } - } - - @Test - public void testAutoThreadPoolFallsBackToPlatformBeforeLoomAvailable() { - ThreadPoolRuntimeEnvironment.setThreadPoolTypeSupplier(() -> "auto"); - ThreadPoolRuntimeEnvironment.setJdkFeatureSupplier(() -> 25); - - ThreadPoolExecutor executor = ThreadPoolExecutorFactory.newThreadPoolExecutor( - "autoFallback", 1, 2, 60, TimeUnit.SECONDS, new LinkedBlockingQueue()); - try { - assertThat(executor).isInstanceOf(PlatformThreadPoolExecutor.class); - } finally { - executor.shutdownNow(); - } - } - - @Test - public void testVirtualScheduledThreadPoolFallsBackToPlatformWithoutLoomProvider() { - ThreadPoolRuntimeEnvironment.setThreadPoolTypeSupplier(() -> "virtual"); - ThreadPoolRuntimeEnvironment.setJdkFeatureSupplier(() -> 21); - - ScheduledThreadPoolExecutor executor = - ThreadPoolExecutorFactory.newScheduledThreadPoolExecutor("virtualScheduleFallback", 1, true); - try { - Thread thread = executor.getThreadFactory().newThread(() -> {}); - - assertThat(executor.getCorePoolSize()).isEqualTo(1); - assertThat(thread.getName()).startsWith("virtualScheduleFallback"); - assertThat(thread.isDaemon()).isTrue(); - } finally { - executor.shutdownNow(); - } - } - - @ParameterizedTest - @CsvSource({"1.8, 8", "1.7, 7", "9, 9", "11, 11", "17, 17", "21, 21", "25, 25"}) - public void testJavaFeatureVersionParsing(String specVersion, int expectedFeature) { - String previousVersion = System.getProperty("java.specification.version"); - try { - System.setProperty("java.specification.version", specVersion); - assertThat(ThreadPoolRuntimeEnvironment.javaFeatureVersion()).isEqualTo(expectedFeature); - } finally { - if (previousVersion != null) { - System.setProperty("java.specification.version", previousVersion); - } else { - System.clearProperty("java.specification.version"); - } - } - } - - @Test - public void testThreadPoolTypeAutoCodeIsStable() { - assertThat(ThreadPoolType.AUTO.getCode()).isEqualTo("auto"); - assertThat(ThreadPoolType.from(DefaultValues.DEFAULT_TRANSPORT_THREADPOOL)) - .isEqualTo(ThreadPoolType.AUTO); - } -}