This is an automated email from the ASF dual-hosted git repository.
Aias00 pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/shenyu.git
The following commit(s) were added to refs/heads/master by this push:
new b7e940c612 fix(web): isolate beans by plugin identity (#7093)
b7e940c612 is described below
commit b7e940c612e5f1592eccca0a05124af14bf930dd
Author: Liming Deng <[email protected]>
AuthorDate: Sat Oct 17 20:48:53 2026 +0800
fix(web): isolate beans by plugin identity (#7093)
* fix(web): isolate beans by plugin identity
* fix(web): isolate plugin bean generations
---------
Co-authored-by: aias00 <[email protected]>
---
.../shenyu/plugin/api/utils/SpringBeanUtils.java | 60 +++++++++-
.../shenyu/web/loader/ShenyuPluginClassLoader.java | 33 +++--
.../ShenyuPluginClassLoaderBeanIsolationTest.java | 133 +++++++++++++++++++++
.../loader/ShenyuPluginClassLoaderHolderTest.java | 31 ++---
4 files changed, 230 insertions(+), 27 deletions(-)
diff --git
a/shenyu-plugin/shenyu-plugin-api/src/main/java/org/apache/shenyu/plugin/api/utils/SpringBeanUtils.java
b/shenyu-plugin/shenyu-plugin-api/src/main/java/org/apache/shenyu/plugin/api/utils/SpringBeanUtils.java
index 37f1d21a80..2d6ab33083 100644
---
a/shenyu-plugin/shenyu-plugin-api/src/main/java/org/apache/shenyu/plugin/api/utils/SpringBeanUtils.java
+++
b/shenyu-plugin/shenyu-plugin-api/src/main/java/org/apache/shenyu/plugin/api/utils/SpringBeanUtils.java
@@ -20,7 +20,9 @@ package org.apache.shenyu.plugin.api.utils;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.BeansException;
import org.springframework.beans.factory.config.BeanDefinition;
+import org.springframework.beans.factory.support.AbstractBeanDefinition;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
+import org.springframework.beans.factory.support.GenericBeanDefinition;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ConfigurableApplicationContext;
@@ -81,11 +83,54 @@ public final class SpringBeanUtils {
throw new NullPointerException("beanDefinition.beanClassName is
null");
}
String beanName = getBeanName(beanClassName);
- DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory)
applicationContext.getAutowireCapableBeanFactory();
- beanFactory.setBeanClassLoader(classLoader);
- beanFactory.registerBeanDefinition(beanName, beanDefinition);
+ registerBean(beanName, beanDefinition, classLoader);
return beanName;
}
+
+ /**
+ * Register a bean with an explicit name.
+ *
+ * @param beanName bean name
+ * @param beanDefinition bean definition
+ * @param classLoader class loader
+ */
+ public void registerBean(final String beanName, final BeanDefinition
beanDefinition, final ClassLoader classLoader) {
+ AbstractBeanDefinition definition = beanDefinition instanceof
AbstractBeanDefinition
+ ? (AbstractBeanDefinition) beanDefinition : new
GenericBeanDefinition(beanDefinition);
+ try {
+
definition.setBeanClass(Class.forName(beanDefinition.getBeanClassName(), false,
classLoader));
+ } catch (ClassNotFoundException ex) {
+ throw new IllegalArgumentException("Cannot load bean class " +
beanDefinition.getBeanClassName(), ex);
+ }
+ DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory)
applicationContext.getAutowireCapableBeanFactory();
+ beanFactory.registerBeanDefinition(beanName, definition);
+ }
+
+ /**
+ * Check whether a bean with the exact name exists.
+ *
+ * @param beanName bean name
+ * @return true when the bean exists
+ */
+ public boolean existBeanByName(final String beanName) {
+ return this.applicationContext.containsBean(beanName);
+ }
+
+ /**
+ * Get a bean by its exact name.
+ *
+ * @param beanName bean name
+ * @param <T> bean type
+ * @return bean or null when it cannot be obtained
+ */
+ @SuppressWarnings("all")
+ public <T> T getBeanByName(final String beanName) {
+ try {
+ return this.getBean(beanName);
+ } catch (BeansException e) {
+ return null;
+ }
+ }
/**
* Exist spring bean boolean.
@@ -123,6 +168,15 @@ public final class SpringBeanUtils {
*/
public void destroyBean(final String className) {
String beanName = getBeanName(className);
+ destroyBeanByName(beanName);
+ }
+
+ /**
+ * Destroy a bean by its exact name.
+ *
+ * @param beanName bean name
+ */
+ public void destroyBeanByName(final String beanName) {
DefaultListableBeanFactory beanFactory = getBeanFactory();
if (beanFactory.containsBean(beanName)) {
beanFactory.destroySingleton(beanName);
diff --git
a/shenyu-web/src/main/java/org/apache/shenyu/web/loader/ShenyuPluginClassLoader.java
b/shenyu-web/src/main/java/org/apache/shenyu/web/loader/ShenyuPluginClassLoader.java
index bf4e74b8bc..46916f4147 100644
---
a/shenyu-web/src/main/java/org/apache/shenyu/web/loader/ShenyuPluginClassLoader.java
+++
b/shenyu-web/src/main/java/org/apache/shenyu/web/loader/ShenyuPluginClassLoader.java
@@ -37,7 +37,9 @@ import java.util.Arrays;
import java.util.Map;
import java.util.Set;
import java.util.Objects;
+import java.util.Optional;
import java.util.concurrent.ConcurrentHashMap;
+import java.util.concurrent.atomic.AtomicLong;
import java.util.concurrent.locks.ReentrantLock;
/**
@@ -46,6 +48,8 @@ import java.util.concurrent.locks.ReentrantLock;
public final class ShenyuPluginClassLoader extends ClassLoader implements
Closeable {
private static final Logger LOG =
LoggerFactory.getLogger(ShenyuPluginClassLoader.class);
+ private static final AtomicLong GENERATION = new AtomicLong();
+
static {
registerAsParallelCapable();
}
@@ -56,6 +60,10 @@ public final class ShenyuPluginClassLoader extends
ClassLoader implements Closea
private final Map<String, byte[]> resourceCache = new
ConcurrentHashMap<>();
+ private final Set<String> registeredBeanNames =
ConcurrentHashMap.newKeySet();
+
+ private final long generation = GENERATION.incrementAndGet();
+
private final Set<String> loadedPluginNames =
ConcurrentHashMap.newKeySet();
private final PluginJarParser.PluginJar pluginJar;
@@ -146,23 +154,24 @@ public final class ShenyuPluginClassLoader extends
ClassLoader implements Closea
@Override
public void close() {
- Set<String> clazzNames = pluginJar.getClazzMap().keySet();
- for (String clazzName : clazzNames) {
- SpringBeanUtils.getInstance().destroyBean(clazzName);
+ for (String beanName : registeredBeanNames) {
+ SpringBeanUtils.getInstance().destroyBeanByName(beanName);
}
+ registeredBeanNames.clear();
}
private <T> T getOrCreateSpringBean(final String className) throws
ClassNotFoundException, IllegalAccessException, InstantiationException {
- if (SpringBeanUtils.getInstance().existBean(className)) {
- T inst =
SpringBeanUtils.getInstance().getBeanByClassName(className);
+ String beanName = getPluginBeanName(className);
+ if (SpringBeanUtils.getInstance().existBeanByName(beanName)) {
+ T inst = SpringBeanUtils.getInstance().getBeanByName(beanName);
// if the class is loaded by other classloader, then reload it
- if (!isLoadedByOtherClassLoader(inst)) {
+ if (Objects.nonNull(inst) && !isLoadedByOtherClassLoader(inst)) {
return inst;
}
}
lock.lock();
try {
- T inst =
SpringBeanUtils.getInstance().getBeanByClassName(className);
+ T inst = SpringBeanUtils.getInstance().getBeanByName(beanName);
if (Objects.isNull(inst) || isLoadedByOtherClassLoader(inst)) {
Class<?> clazz = Class.forName(className, false, this);
//Exclude ShenyuPlugin subclass and PluginDataHandler subclass
@@ -179,8 +188,9 @@ public final class ShenyuPluginClassLoader extends
ClassLoader implements Closea
beanDefinition.setBeanClassName(className);
beanDefinition.setAutowireCandidate(true);
beanDefinition.setRole(BeanDefinition.ROLE_INFRASTRUCTURE);
- String beanName =
SpringBeanUtils.getInstance().registerBean(beanDefinition, this);
- inst =
SpringBeanUtils.getInstance().getBeanByClassName(beanName);
+ SpringBeanUtils.getInstance().registerBean(beanName,
beanDefinition, this);
+ registeredBeanNames.add(beanName);
+ inst =
SpringBeanUtils.getInstance().getBeanByName(beanName);
}
}
return inst;
@@ -200,6 +210,11 @@ public final class ShenyuPluginClassLoader extends
ClassLoader implements Closea
return !inst.getClass().getClassLoader().equals(this);
}
+ String getPluginBeanName(final String className) {
+ String pluginKey =
Optional.ofNullable(pluginJar.getAbsolutePath()).orElse(pluginJar.getJarKey());
+ return pluginKey + "#" + generation + "#" + className;
+ }
+
private ShenyuLoaderResult buildResult(final Object instance) {
ShenyuLoaderResult result = new ShenyuLoaderResult();
if (instance instanceof ShenyuPlugin) {
diff --git
a/shenyu-web/src/test/java/org/apache/shenyu/web/loader/ShenyuPluginClassLoaderBeanIsolationTest.java
b/shenyu-web/src/test/java/org/apache/shenyu/web/loader/ShenyuPluginClassLoaderBeanIsolationTest.java
new file mode 100644
index 0000000000..677cd2940e
--- /dev/null
+++
b/shenyu-web/src/test/java/org/apache/shenyu/web/loader/ShenyuPluginClassLoaderBeanIsolationTest.java
@@ -0,0 +1,133 @@
+/*
+ * 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.shenyu.web.loader;
+
+import net.bytebuddy.ByteBuddy;
+import net.bytebuddy.description.annotation.AnnotationDescription;
+import org.apache.shenyu.plugin.api.utils.SpringBeanUtils;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.support.RootBeanDefinition;
+import org.springframework.context.support.GenericApplicationContext;
+import org.springframework.stereotype.Component;
+
+import java.util.Collections;
+
+import static org.junit.jupiter.api.Assertions.assertFalse;
+import static org.junit.jupiter.api.Assertions.assertSame;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+class ShenyuPluginClassLoaderBeanIsolationTest {
+
+ @Test
+ void closingOnePluginShouldNotDestroySameSimpleNameFromAnotherPlugin() {
+ GenericApplicationContext context = createContext();
+ String firstClassName = "fixture.first.SharedComponent";
+ String secondClassName = "fixture.second.SharedComponent";
+ ShenyuPluginClassLoader firstLoader = createLoader("plugin-one",
firstClassName);
+ ShenyuPluginClassLoader secondLoader = createLoader("plugin-two",
secondClassName);
+ try {
+ firstLoader.loadUploadedJarPlugins();
+ secondLoader.loadUploadedJarPlugins();
+ String firstBeanName =
firstLoader.getPluginBeanName(firstClassName);
+ String secondBeanName =
secondLoader.getPluginBeanName(secondClassName);
+ assertSame(firstLoader,
context.getBean(firstBeanName).getClass().getClassLoader());
+ assertSame(secondLoader,
context.getBean(secondBeanName).getClass().getClassLoader());
+
+ firstLoader.close();
+
+ assertFalse(context.containsBean(firstBeanName));
+ assertTrue(context.containsBean(secondBeanName));
+ } finally {
+ firstLoader.close();
+ secondLoader.close();
+ context.close();
+ }
+ }
+
+ @Test
+ void closingPreviousGenerationShouldNotDestroyReplacementBean() {
+ GenericApplicationContext context = createContext();
+ ClassLoader factoryClassLoader =
context.getDefaultListableBeanFactory().getBeanClassLoader();
+ String className = "fixture.reload.SharedComponent";
+ ShenyuPluginClassLoader previousLoader = createLoader("same-plugin",
className);
+ ShenyuPluginClassLoader replacementLoader =
createLoader("same-plugin", className);
+ try {
+ previousLoader.loadUploadedJarPlugins();
+ replacementLoader.loadUploadedJarPlugins();
+ assertSame(factoryClassLoader,
context.getDefaultListableBeanFactory().getBeanClassLoader());
+ String previousBeanName =
previousLoader.getPluginBeanName(className);
+ String replacementBeanName =
replacementLoader.getPluginBeanName(className);
+
+ previousLoader.close();
+
+ assertFalse(context.containsBean(previousBeanName));
+ assertTrue(context.containsBean(replacementBeanName));
+ assertSame(replacementLoader,
context.getBean(replacementBeanName).getClass().getClassLoader());
+ assertSame(factoryClassLoader,
context.getDefaultListableBeanFactory().getBeanClassLoader());
+ } finally {
+ previousLoader.close();
+ replacementLoader.close();
+ context.close();
+ }
+ }
+
+ @Test
+ void failedExistingBeanCreationShouldNotAbortPluginLoading() {
+ GenericApplicationContext context = createContext();
+ String className = "fixture.recovery.RecoveringComponent";
+ ShenyuPluginClassLoader loader = createLoader("recovery-plugin",
className);
+ String beanName = loader.getPluginBeanName(className);
+ context.registerBeanDefinition(beanName, new
RootBeanDefinition(FailingBean.class));
+ try {
+ loader.loadUploadedJarPlugins();
+
+ assertTrue(context.containsBean(beanName));
+ assertSame(loader,
context.getBean(beanName).getClass().getClassLoader());
+ } finally {
+ loader.close();
+ context.close();
+ }
+ }
+
+ private GenericApplicationContext createContext() {
+ GenericApplicationContext context = new GenericApplicationContext();
+ context.refresh();
+ SpringBeanUtils.getInstance().setApplicationContext(context);
+ return context;
+ }
+
+ private ShenyuPluginClassLoader createLoader(final String path, final
String className) {
+ byte[] classBytes = new ByteBuddy()
+ .subclass(Object.class)
+ .name(className)
+
.annotateType(AnnotationDescription.Builder.ofType(Component.class).build())
+ .make()
+ .getBytes();
+ PluginJarParser.PluginJar pluginJar = new PluginJarParser.PluginJar();
+ pluginJar.setAbsolutePath(path);
+ pluginJar.setClazzMap(Collections.singletonMap(className, classBytes));
+ return new ShenyuPluginClassLoader(pluginJar);
+ }
+
+ public static final class FailingBean {
+
+ private FailingBean() {
+ throw new IllegalStateException("bean creation failed");
+ }
+ }
+}
diff --git
a/shenyu-web/src/test/java/org/apache/shenyu/web/loader/ShenyuPluginClassLoaderHolderTest.java
b/shenyu-web/src/test/java/org/apache/shenyu/web/loader/ShenyuPluginClassLoaderHolderTest.java
index f0d7226923..c25a6e769a 100644
---
a/shenyu-web/src/test/java/org/apache/shenyu/web/loader/ShenyuPluginClassLoaderHolderTest.java
+++
b/shenyu-web/src/test/java/org/apache/shenyu/web/loader/ShenyuPluginClassLoaderHolderTest.java
@@ -17,11 +17,14 @@
package org.apache.shenyu.web.loader;
+import net.bytebuddy.ByteBuddy;
+import net.bytebuddy.description.annotation.AnnotationDescription;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.apache.shenyu.plugin.api.utils.SpringBeanUtils;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
-import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.context.support.GenericApplicationContext;
+import org.springframework.stereotype.Component;
import java.util.ArrayList;
import java.util.Collections;
@@ -71,15 +74,17 @@ public final class ShenyuPluginClassLoaderHolderTest {
@Test
public void
replacePluginClassLoaderSerializesLoadingAndClosesEveryDisplacedLoader() throws
Exception {
- int threadCount = 32;
+ final int threadCount = 32;
String jarKey = "concurrent-test-key";
CountingBeanFactory beanFactory = new CountingBeanFactory();
- ConfigurableApplicationContext context =
mock(ConfigurableApplicationContext.class);
- when(context.getBeanFactory()).thenReturn(beanFactory);
+ GenericApplicationContext context = new
GenericApplicationContext(beanFactory);
+ context.refresh();
SpringBeanUtils.getInstance().setApplicationContext(context);
PluginJarParser.PluginJar concurrentPluginJar =
mock(PluginJarParser.PluginJar.class);
when(concurrentPluginJar.getAbsolutePath()).thenReturn(jarKey);
-
when(concurrentPluginJar.getClazzMap()).thenReturn(Collections.singletonMap("sample.Plugin",
new byte[0]));
+ byte[] classBytes = new
ByteBuddy().subclass(Object.class).name("sample.Plugin")
+
.annotateType(AnnotationDescription.Builder.ofType(Component.class).build()).make().getBytes();
+
when(concurrentPluginJar.getClazzMap()).thenReturn(Collections.singletonMap("sample.Plugin",
classBytes));
ExecutorService executor = Executors.newFixedThreadPool(threadCount);
CountDownLatch ready = new CountDownLatch(threadCount);
CountDownLatch start = new CountDownLatch(1);
@@ -95,6 +100,7 @@ public final class ShenyuPluginClassLoaderHolderTest {
ShenyuPluginClassLoaderHolder.getSingleton().replacePluginClassLoader(concurrentPluginJar,
classLoader -> {
int current = activeLoads.incrementAndGet();
maximumActiveLoads.accumulateAndGet(current,
Math::max);
+ classLoader.loadUploadedJarPlugins();
activeLoads.decrementAndGet();
});
} catch (InterruptedException ex) {
@@ -116,6 +122,7 @@ public final class ShenyuPluginClassLoaderHolderTest {
executor.shutdownNow();
assertTrue(executor.awaitTermination(5, TimeUnit.SECONDS));
ShenyuPluginClassLoaderHolder.getSingleton().removePluginClassLoader(jarKey);
+ context.close();
}
}
@@ -123,18 +130,12 @@ public final class ShenyuPluginClassLoaderHolderTest {
private final AtomicInteger destroyCount = new AtomicInteger();
- @Override
- public boolean containsBean(final String name) {
- return true;
- }
-
@Override
public void destroySingleton(final String beanName) {
- destroyCount.incrementAndGet();
- }
-
- @Override
- public void removeBeanDefinition(final String beanName) {
+ if (containsSingleton(beanName)) {
+ destroyCount.incrementAndGet();
+ }
+ super.destroySingleton(beanName);
}
}
}