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);
         }
     }
 }

Reply via email to