diff --git a/trpc-spring-boot-starters/trpc-spring-boot-starter/src/main/java/com/tencent/trpc/spring/boot/starters/listener/TRpcApplicationFailedListener.java b/trpc-spring-boot-starters/trpc-spring-boot-starter/src/main/java/com/tencent/trpc/spring/boot/starters/listener/TRpcApplicationFailedListener.java
index 7b47c1c5e..93a4737f5 100644
--- a/trpc-spring-boot-starters/trpc-spring-boot-starter/src/main/java/com/tencent/trpc/spring/boot/starters/listener/TRpcApplicationFailedListener.java
+++ b/trpc-spring-boot-starters/trpc-spring-boot-starter/src/main/java/com/tencent/trpc/spring/boot/starters/listener/TRpcApplicationFailedListener.java
@@ -11,28 +11,43 @@
package com.tencent.trpc.spring.boot.starters.listener;
-import com.tencent.trpc.core.common.ConfigManager;
+import com.tencent.trpc.core.logger.Logger;
+import com.tencent.trpc.core.logger.LoggerFactory;
+import com.tencent.trpc.spring.context.TRpcLifecycleManager;
+import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
import org.springframework.boot.context.event.ApplicationFailedEvent;
import org.springframework.context.ApplicationListener;
+import org.springframework.context.ConfigurableApplicationContext;
import org.springframework.core.annotation.Order;
-import java.util.concurrent.atomic.AtomicBoolean;
-
/**
* Handle the application failed event, the reason was TRpcLifecycleManager called before the application failed.
+ *
Only stop tRPC through the existing {@link TRpcLifecycleManager} singletons of the failed context,
+ * never create new beans, since the context may have been partly destroyed.
*/
@Order
public class TRpcApplicationFailedListener implements ApplicationListener {
- private static final AtomicBoolean PROCESSED = new AtomicBoolean(false);
+
+ private static final Logger logger = LoggerFactory.getLogger(TRpcApplicationFailedListener.class);
@Override
public void onApplicationEvent(ApplicationFailedEvent event) {
- if (PROCESSED.compareAndSet(false, true)) {
- stopTRPC();
+ ConfigurableApplicationContext context = event.getApplicationContext();
+ if (context == null) {
+ return;
+ }
+ ConfigurableListableBeanFactory beanFactory;
+ try {
+ beanFactory = context.getBeanFactory();
+ } catch (IllegalStateException e) {
+ logger.debug("bean factory of the failed application context is not available", e);
+ return;
+ }
+ for (String beanName : beanFactory.getBeanNamesForType(TRpcLifecycleManager.class, false, false)) {
+ Object lifecycleManager = beanFactory.getSingleton(beanName);
+ if (lifecycleManager instanceof TRpcLifecycleManager) {
+ ((TRpcLifecycleManager) lifecycleManager).stopIfStarted();
+ }
}
}
-
- private void stopTRPC() {
- ConfigManager.getInstance().stop();
- }
-}
\ No newline at end of file
+}
diff --git a/trpc-spring-boot-starters/trpc-spring-boot-starter/src/test/java/com/tencent/trpc/spring/boot/starters/listener/TRpcApplicationFailedListenerTest.java b/trpc-spring-boot-starters/trpc-spring-boot-starter/src/test/java/com/tencent/trpc/spring/boot/starters/listener/TRpcApplicationFailedListenerTest.java
new file mode 100644
index 000000000..b8247b0ed
--- /dev/null
+++ b/trpc-spring-boot-starters/trpc-spring-boot-starter/src/test/java/com/tencent/trpc/spring/boot/starters/listener/TRpcApplicationFailedListenerTest.java
@@ -0,0 +1,75 @@
+/*
+ * Tencent is pleased to support the open source community by making tRPC available.
+ *
+ * Copyright (C) 2023 Tencent.
+ * All rights reserved.
+ *
+ * If you have downloaded a copy of the tRPC source code from Tencent,
+ * please note that tRPC source code is licensed under the Apache 2.0 License,
+ * A copy of the Apache 2.0 License can be found in the LICENSE file.
+ */
+
+package com.tencent.trpc.spring.boot.starters.listener;
+
+import static org.junit.jupiter.api.Assertions.assertDoesNotThrow;
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertFalse;
+
+import com.tencent.trpc.spring.context.TRpcLifecycleManager;
+import org.junit.jupiter.api.Test;
+import org.springframework.boot.SpringApplication;
+import org.springframework.boot.context.event.ApplicationFailedEvent;
+import org.springframework.context.ConfigurableApplicationContext;
+import org.springframework.context.support.ClassPathXmlApplicationContext;
+import org.springframework.context.support.GenericApplicationContext;
+
+public class TRpcApplicationFailedListenerTest {
+
+ private final TRpcApplicationFailedListener listener = new TRpcApplicationFailedListener();
+
+ @Test
+ public void testNullContext() {
+ assertDoesNotThrow(() -> listener.onApplicationEvent(newEvent(null)));
+ }
+
+ @Test
+ public void testBeanFactoryNotAvailable() {
+ // bean factory of a not refreshed refreshable context is not available
+ ConfigurableApplicationContext context = new ClassPathXmlApplicationContext();
+ assertDoesNotThrow(() -> listener.onApplicationEvent(newEvent(context)));
+ }
+
+ @Test
+ public void testStopExistingLifecycleManager() {
+ CountingLifecycleManager lifecycleManager = new CountingLifecycleManager();
+ GenericApplicationContext context = new GenericApplicationContext();
+ context.getBeanFactory().registerSingleton("tRpcLifecycleManager", lifecycleManager);
+
+ listener.onApplicationEvent(newEvent(context));
+ assertEquals(1, lifecycleManager.stopCount);
+ }
+
+ @Test
+ public void testNotCreateLifecycleManager() {
+ GenericApplicationContext context = new GenericApplicationContext();
+ context.registerBean("tRpcLifecycleManager", TRpcLifecycleManager.class);
+
+ listener.onApplicationEvent(newEvent(context));
+ assertFalse(context.getBeanFactory().containsSingleton("tRpcLifecycleManager"));
+ }
+
+ private static ApplicationFailedEvent newEvent(ConfigurableApplicationContext context) {
+ return new ApplicationFailedEvent(new SpringApplication(), new String[0], context,
+ new RuntimeException("startup failed"));
+ }
+
+ private static class CountingLifecycleManager extends TRpcLifecycleManager {
+
+ private int stopCount;
+
+ @Override
+ public void stopIfStarted() {
+ stopCount++;
+ }
+ }
+}
diff --git a/trpc-spring-support/trpc-spring/src/main/java/com/tencent/trpc/spring/context/TRpcConfiguration.java b/trpc-spring-support/trpc-spring/src/main/java/com/tencent/trpc/spring/context/TRpcConfiguration.java
index 63dda30c7..6238cd802 100644
--- a/trpc-spring-support/trpc-spring/src/main/java/com/tencent/trpc/spring/context/TRpcConfiguration.java
+++ b/trpc-spring-support/trpc-spring/src/main/java/com/tencent/trpc/spring/context/TRpcConfiguration.java
@@ -37,6 +37,17 @@ public TRpcLifecycleManager tRpcLifecycleManager() {
return new TRpcLifecycleManager();
}
+ /**
+ * Build {@link TRpcShutdownListener}
+ *
+ * @param tRpcLifecycleManager the {@link TRpcLifecycleManager} which started tRPC
+ * @return TRpcShutdownListener
+ */
+ @Bean
+ public TRpcShutdownListener tRpcShutdownListener(TRpcLifecycleManager tRpcLifecycleManager) {
+ return new TRpcShutdownListener(tRpcLifecycleManager);
+ }
+
/**
* Build {@link TRpcConfigManagerInitializer}.
* Users could implement {@link TRpcConfigManagerCustomizer} and
diff --git a/trpc-spring-support/trpc-spring/src/main/java/com/tencent/trpc/spring/context/TRpcLifecycleManager.java b/trpc-spring-support/trpc-spring/src/main/java/com/tencent/trpc/spring/context/TRpcLifecycleManager.java
index a26087fc1..15ebfaea8 100644
--- a/trpc-spring-support/trpc-spring/src/main/java/com/tencent/trpc/spring/context/TRpcLifecycleManager.java
+++ b/trpc-spring-support/trpc-spring/src/main/java/com/tencent/trpc/spring/context/TRpcLifecycleManager.java
@@ -15,65 +15,106 @@
import com.tencent.trpc.core.common.config.ProviderConfig;
import com.tencent.trpc.spring.util.TRpcSpringUtils;
import java.util.concurrent.atomic.AtomicBoolean;
+import org.springframework.beans.factory.DisposableBean;
import org.springframework.context.ApplicationContext;
-import org.springframework.context.ApplicationEvent;
-import org.springframework.context.event.ContextClosedEvent;
+import org.springframework.context.ApplicationContextAware;
+import org.springframework.context.ApplicationListener;
import org.springframework.context.event.ContextRefreshedEvent;
-import org.springframework.context.event.SmartApplicationListener;
import org.springframework.core.Ordered;
import org.springframework.lang.NonNull;
/**
- * Response on {@link ContextRefreshedEvent} and {@link ContextClosedEvent}
- * to perform necessary tRPC operations.
+ * Start tRPC on {@link ContextRefreshedEvent}.
+ *
Runs late so that other {@link ContextRefreshedEvent} listeners (e.g. Spring Cloud RefreshScope eager
+ * initialization) have finished before tRPC begins to accept traffic. Shutdown is handled by
+ * {@link TRpcShutdownListener}, which runs first on {@link org.springframework.context.event.ContextClosedEvent}.
+ * Only events of the application context which this manager belongs to are handled, events propagated from
+ * child contexts (e.g. actuator management context, whose id is {@code {parentId}:management}) are ignored.
*/
-public class TRpcLifecycleManager implements SmartApplicationListener {
+public class TRpcLifecycleManager implements ApplicationListener, ApplicationContextAware,
+ DisposableBean, Ordered {
/**
- * tRPC 容器启动标记
+ * Order of starting tRPC on {@link ContextRefreshedEvent}.
+ */
+ public static final int START_ORDER = Ordered.LOWEST_PRECEDENCE - 20;
+
+ /**
+ * Whether tRPC has been started by this manager.
*/
private final AtomicBoolean started = new AtomicBoolean(Boolean.FALSE);
+ /**
+ * The application context which this manager belongs to.
+ */
+ private ApplicationContext applicationContext;
+
@Override
- public boolean supportsEventType(@NonNull Class extends ApplicationEvent> eventType) {
- return ContextRefreshedEvent.class == eventType
- || ContextClosedEvent.class == eventType;
+ public void setApplicationContext(@NonNull ApplicationContext applicationContext) {
+ this.applicationContext = applicationContext;
}
- @Override
- public void onApplicationEvent(@NonNull ApplicationEvent event) {
- if (event instanceof ContextRefreshedEvent) {
- onContextRefreshedEvent((ContextRefreshedEvent) event);
- } else {
- onContextClosedEvent((ContextClosedEvent) event);
- }
+ /**
+ * Whether tRPC has been started by this manager.
+ *
+ * @return true if started
+ */
+ public boolean isStarted() {
+ return started.get();
}
/**
- * Start tRPC services on {@link ContextRefreshedEvent}
+ * Whether the context is managed by this manager, i.e. it is the context this manager belongs to
+ * and requires aware of tRPC.
+ *
+ * @param context the context of the event
+ * @return true if managed
*/
- private void onContextRefreshedEvent(ContextRefreshedEvent event) {
- if (!TRpcSpringUtils.isAwareContext(event.getApplicationContext())) {
- return;
+ public boolean isManagedContext(ApplicationContext context) {
+ if (context == null) {
+ return false;
}
- if (started.compareAndSet(Boolean.FALSE, Boolean.TRUE)) {
- replaceProvidersRef(event.getApplicationContext());
- startTRPC();
+ if (applicationContext != null && applicationContext != context) {
+ return false;
}
+ return TRpcSpringUtils.isAwareContext(context);
}
/**
- * Shutdown tRPC services on {@link ContextClosedEvent}
+ * Stop tRPC only if it has been started by this manager, e.g. on context close or application startup failure.
*/
- private void onContextClosedEvent(ContextClosedEvent event) {
- if (!TRpcSpringUtils.isAwareContext(event.getApplicationContext())) {
+ public void stopIfStarted() {
+ if (started.compareAndSet(Boolean.TRUE, Boolean.FALSE)) {
+ ConfigManager.getInstance().stop();
+ }
+ }
+
+ @Override
+ public void onApplicationEvent(@NonNull ContextRefreshedEvent event) {
+ if (!isManagedContext(event.getApplicationContext())) {
return;
}
- if (started.compareAndSet(Boolean.TRUE, Boolean.FALSE)) {
- stopTRPC();
+ if (started.compareAndSet(Boolean.FALSE, Boolean.TRUE)) {
+ try {
+ replaceProvidersRef(event.getApplicationContext());
+ ConfigManager.getInstance().start(false);
+ } catch (Throwable t) {
+ // tRPC lifecycle has already stopped itself on start failure, avoid stopping it again
+ started.set(Boolean.FALSE);
+ throw t;
+ }
}
}
+ /**
+ * Fallback of stopping tRPC when the context refresh fails after tRPC has started, in which case
+ * beans are destroyed without publishing {@link org.springframework.context.event.ContextClosedEvent}.
+ */
+ @Override
+ public void destroy() {
+ stopIfStarted();
+ }
+
/**
* Replace tRPC provider ref to related spring beans.
*/
@@ -92,16 +133,8 @@ private void doReplaceProviderRef(ApplicationContext applicationContext, Provide
}
}
- private void startTRPC() {
- ConfigManager.getInstance().start(false);
- }
-
- private void stopTRPC() {
- ConfigManager.getInstance().stop();
- }
-
@Override
public int getOrder() {
- return Ordered.HIGHEST_PRECEDENCE + 20000;
+ return START_ORDER;
}
}
diff --git a/trpc-spring-support/trpc-spring/src/main/java/com/tencent/trpc/spring/context/TRpcShutdownListener.java b/trpc-spring-support/trpc-spring/src/main/java/com/tencent/trpc/spring/context/TRpcShutdownListener.java
new file mode 100644
index 000000000..46818ca65
--- /dev/null
+++ b/trpc-spring-support/trpc-spring/src/main/java/com/tencent/trpc/spring/context/TRpcShutdownListener.java
@@ -0,0 +1,50 @@
+/*
+ * Tencent is pleased to support the open source community by making tRPC available.
+ *
+ * Copyright (C) 2023 Tencent.
+ * All rights reserved.
+ *
+ * If you have downloaded a copy of the tRPC source code from Tencent,
+ * please note that tRPC source code is licensed under the Apache 2.0 License,
+ * A copy of the Apache 2.0 License can be found in the LICENSE file.
+ */
+
+package com.tencent.trpc.spring.context;
+
+import java.util.Objects;
+import org.springframework.context.ApplicationListener;
+import org.springframework.context.event.ContextClosedEvent;
+import org.springframework.core.Ordered;
+import org.springframework.lang.NonNull;
+
+/**
+ * Stop tRPC on {@link ContextClosedEvent}.
+ * Runs early so that tRPC unregisters and drains in-flight requests before other
+ * {@link ContextClosedEvent} listeners release resources.
+ */
+public class TRpcShutdownListener implements ApplicationListener, Ordered {
+
+ /**
+ * Order of stopping tRPC on {@link ContextClosedEvent}.
+ */
+ public static final int STOP_ORDER = Ordered.HIGHEST_PRECEDENCE + 20000;
+
+ private final TRpcLifecycleManager lifecycleManager;
+
+ public TRpcShutdownListener(TRpcLifecycleManager lifecycleManager) {
+ this.lifecycleManager = Objects.requireNonNull(lifecycleManager, "lifecycleManager");
+ }
+
+ @Override
+ public void onApplicationEvent(@NonNull ContextClosedEvent event) {
+ if (!lifecycleManager.isManagedContext(event.getApplicationContext())) {
+ return;
+ }
+ lifecycleManager.stopIfStarted();
+ }
+
+ @Override
+ public int getOrder() {
+ return STOP_ORDER;
+ }
+}
diff --git a/trpc-spring-support/trpc-spring/src/test/java/com/tencent/trpc/spring/context/TRpcLifecycleManagerTest.java b/trpc-spring-support/trpc-spring/src/test/java/com/tencent/trpc/spring/context/TRpcLifecycleManagerTest.java
new file mode 100644
index 000000000..dc52645ed
--- /dev/null
+++ b/trpc-spring-support/trpc-spring/src/test/java/com/tencent/trpc/spring/context/TRpcLifecycleManagerTest.java
@@ -0,0 +1,257 @@
+/*
+ * Tencent is pleased to support the open source community by making tRPC available.
+ *
+ * Copyright (C) 2023 Tencent.
+ * All rights reserved.
+ *
+ * If you have downloaded a copy of the tRPC source code from Tencent,
+ * please note that tRPC source code is licensed under the Apache 2.0 License,
+ * A copy of the Apache 2.0 License can be found in the LICENSE file.
+ */
+
+package com.tencent.trpc.spring.context;
+
+import com.tencent.trpc.core.common.ConfigManager;
+import com.tencent.trpc.core.common.config.ProviderConfig;
+import com.tencent.trpc.core.common.config.ServerConfig;
+import com.tencent.trpc.core.common.config.ServiceConfig;
+import com.tencent.trpc.core.rpc.RpcContext;
+import com.tencent.trpc.core.rpc.anno.TRpcMethod;
+import com.tencent.trpc.core.rpc.anno.TRpcService;
+import com.tencent.trpc.spring.test.TRpcConfigManagerTestUtils;
+import java.lang.reflect.Field;
+import org.junit.After;
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.mockito.Mockito;
+import org.springframework.context.event.ContextRefreshedEvent;
+import org.springframework.context.support.GenericApplicationContext;
+import org.springframework.core.Ordered;
+
+public class TRpcLifecycleManagerTest {
+
+ private static final String AWARE_CONTEXT_ID = "application";
+
+ private static final String SERVICE_NAME = "TRpcLifecycleManagerTest.service";
+
+ private ConfigManager configManager;
+
+ private ServerConfig serverConfig;
+
+ private GenericApplicationContext context;
+
+ private TRpcLifecycleManager manager;
+
+ @Before
+ public void setUp() {
+ configManager = Mockito.mock(ConfigManager.class);
+ serverConfig = new ServerConfig();
+ Mockito.when(configManager.getServerConfig()).thenReturn(serverConfig);
+ setConfigManager(configManager);
+
+ context = new GenericApplicationContext();
+ context.setId(AWARE_CONTEXT_ID);
+ context.registerBean(TestServiceImpl.class);
+ context.refresh();
+
+ manager = new TRpcLifecycleManager();
+ manager.setApplicationContext(context);
+ }
+
+ @After
+ public void tearDown() {
+ context.close();
+ TRpcConfigManagerTestUtils.resetConfigManager();
+ }
+
+ @Test
+ public void testGetOrder() {
+ Assert.assertEquals(TRpcLifecycleManager.START_ORDER, manager.getOrder());
+ Assert.assertEquals(Ordered.LOWEST_PRECEDENCE - 20, TRpcLifecycleManager.START_ORDER);
+ }
+
+ @Test
+ public void testStartAndStop() {
+ ProviderConfig> providerConfig = addProvider(TestServiceImpl.class.getName());
+
+ manager.onApplicationEvent(new ContextRefreshedEvent(context));
+ Assert.assertTrue(manager.isStarted());
+ Assert.assertTrue(providerConfig.getRef() instanceof TestServiceImpl);
+ Assert.assertEquals(TestService.class, providerConfig.getServiceInterface());
+ Mockito.verify(configManager, Mockito.times(1)).start(false);
+
+ // start only once
+ manager.onApplicationEvent(new ContextRefreshedEvent(context));
+ Mockito.verify(configManager, Mockito.times(1)).start(false);
+
+ manager.stopIfStarted();
+ Assert.assertFalse(manager.isStarted());
+ Mockito.verify(configManager, Mockito.times(1)).stop();
+
+ // stop only once
+ manager.stopIfStarted();
+ manager.destroy();
+ Mockito.verify(configManager, Mockito.times(1)).stop();
+ }
+
+ @Test
+ public void testStateIsPerInstance() {
+ manager.onApplicationEvent(new ContextRefreshedEvent(context));
+ Assert.assertTrue(manager.isStarted());
+
+ TRpcLifecycleManager another = new TRpcLifecycleManager();
+ Assert.assertFalse(another.isStarted());
+ another.stopIfStarted();
+ Mockito.verify(configManager, Mockito.never()).stop();
+ Assert.assertTrue(manager.isStarted());
+ }
+
+ @Test
+ public void testDestroyStopsStartedTRpc() {
+ manager.onApplicationEvent(new ContextRefreshedEvent(context));
+ Assert.assertTrue(manager.isStarted());
+
+ manager.destroy();
+ Assert.assertFalse(manager.isStarted());
+ Mockito.verify(configManager, Mockito.times(1)).stop();
+ }
+
+ @Test
+ public void testDestroyWithoutStart() {
+ manager.destroy();
+ Mockito.verify(configManager, Mockito.never()).stop();
+ }
+
+ @Test
+ public void testIgnoreChildContextEvent() {
+ GenericApplicationContext child = new GenericApplicationContext(context);
+ child.setId(AWARE_CONTEXT_ID + ":management");
+
+ Assert.assertTrue(manager.isManagedContext(context));
+ Assert.assertFalse(manager.isManagedContext(child));
+ Assert.assertFalse(manager.isManagedContext(null));
+
+ manager.onApplicationEvent(new ContextRefreshedEvent(child));
+ Assert.assertFalse(manager.isStarted());
+ Mockito.verify(configManager, Mockito.never()).start(false);
+ }
+
+ @Test
+ public void testIgnoreNotAwareContextEvent() {
+ context.setId("not-aware");
+ Assert.assertFalse(manager.isManagedContext(context));
+
+ manager.onApplicationEvent(new ContextRefreshedEvent(context));
+ Assert.assertFalse(manager.isStarted());
+ Mockito.verify(configManager, Mockito.never()).start(false);
+ }
+
+ @Test
+ public void testWithoutApplicationContextAware() {
+ TRpcLifecycleManager notAwareManager = new TRpcLifecycleManager();
+ Assert.assertTrue(notAwareManager.isManagedContext(context));
+
+ context.setId("not-aware");
+ Assert.assertFalse(notAwareManager.isManagedContext(context));
+ }
+
+ @Test
+ public void testStartFailedWithRuntimeException() {
+ IllegalStateException startException = new IllegalStateException("start failed");
+ Mockito.doThrow(startException).when(configManager).start(false);
+ try {
+ manager.onApplicationEvent(new ContextRefreshedEvent(context));
+ Assert.fail("expected exception");
+ } catch (IllegalStateException e) {
+ Assert.assertSame(startException, e);
+ }
+ Assert.assertFalse(manager.isStarted());
+
+ // stop is not required since tRPC lifecycle has stopped itself on start failure
+ manager.stopIfStarted();
+ Mockito.verify(configManager, Mockito.never()).stop();
+
+ // could start again
+ Mockito.doNothing().when(configManager).start(false);
+ manager.onApplicationEvent(new ContextRefreshedEvent(context));
+ Assert.assertTrue(manager.isStarted());
+ }
+
+ @Test
+ public void testStartFailedWithError() {
+ NoClassDefFoundError startError = new NoClassDefFoundError("start failed");
+ Mockito.doThrow(startError).when(configManager).start(false);
+ try {
+ manager.onApplicationEvent(new ContextRefreshedEvent(context));
+ Assert.fail("expected error");
+ } catch (NoClassDefFoundError e) {
+ Assert.assertSame(startError, e);
+ }
+ Assert.assertFalse(manager.isStarted());
+
+ manager.destroy();
+ Mockito.verify(configManager, Mockito.never()).stop();
+ }
+
+ @Test
+ public void testReplaceProviderRefFailed() {
+ addProvider("com.tencent.trpc.spring.context.NotExistServiceImpl");
+ try {
+ manager.onApplicationEvent(new ContextRefreshedEvent(context));
+ Assert.fail("expected exception");
+ } catch (RuntimeException e) {
+ Assert.assertTrue(e.getCause() instanceof ClassNotFoundException);
+ }
+ Assert.assertFalse(manager.isStarted());
+ Mockito.verify(configManager, Mockito.never()).start(false);
+ }
+
+ @Test
+ public void testStartAndStopTRpcContainer() {
+ TRpcConfigManagerTestUtils.resetConfigManager();
+ ConfigManager.getInstance().getServerConfig().setWaitTimeout(1);
+ ConfigManager.getInstance().getServerConfig().setCloseTimeout(1);
+ TRpcLifecycleManager realManager = new TRpcLifecycleManager();
+ realManager.setApplicationContext(context);
+ realManager.onApplicationEvent(new ContextRefreshedEvent(context));
+ Assert.assertTrue(realManager.isStarted());
+
+ realManager.destroy();
+ Assert.assertFalse(realManager.isStarted());
+ }
+
+ private ProviderConfig> addProvider(String refClazz) {
+ ProviderConfig> providerConfig = new ProviderConfig<>();
+ providerConfig.setRefClazz(refClazz);
+ ServiceConfig serviceConfig = new ServiceConfig();
+ serviceConfig.addProviderConfig(providerConfig);
+ serverConfig.getServiceMap().put(SERVICE_NAME, serviceConfig);
+ return providerConfig;
+ }
+
+ private static void setConfigManager(ConfigManager configManager) {
+ try {
+ Field instanceField = ConfigManager.class.getDeclaredField("instance");
+ instanceField.setAccessible(true);
+ instanceField.set(null, configManager);
+ } catch (Exception e) {
+ throw new IllegalStateException("set ConfigManager instance error", e);
+ }
+ }
+
+ @TRpcService(name = SERVICE_NAME)
+ public interface TestService {
+
+ @TRpcMethod(name = "call")
+ Object call(RpcContext context, Object req);
+ }
+
+ public static class TestServiceImpl implements TestService {
+
+ @Override
+ public Object call(RpcContext context, Object req) {
+ return null;
+ }
+ }
+}
diff --git a/trpc-spring-support/trpc-spring/src/test/java/com/tencent/trpc/spring/context/TRpcShutdownListenerTest.java b/trpc-spring-support/trpc-spring/src/test/java/com/tencent/trpc/spring/context/TRpcShutdownListenerTest.java
new file mode 100644
index 000000000..f032abbd5
--- /dev/null
+++ b/trpc-spring-support/trpc-spring/src/test/java/com/tencent/trpc/spring/context/TRpcShutdownListenerTest.java
@@ -0,0 +1,78 @@
+/*
+ * Tencent is pleased to support the open source community by making tRPC available.
+ *
+ * Copyright (C) 2023 Tencent.
+ * All rights reserved.
+ *
+ * If you have downloaded a copy of the tRPC source code from Tencent,
+ * please note that tRPC source code is licensed under the Apache 2.0 License,
+ * A copy of the Apache 2.0 License can be found in the LICENSE file.
+ */
+
+package com.tencent.trpc.spring.context;
+
+import org.junit.Assert;
+import org.junit.Before;
+import org.junit.Test;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.event.ContextClosedEvent;
+import org.springframework.context.support.GenericApplicationContext;
+import org.springframework.core.Ordered;
+
+public class TRpcShutdownListenerTest {
+
+ private StubLifecycleManager lifecycleManager;
+
+ private TRpcShutdownListener listener;
+
+ private GenericApplicationContext context;
+
+ @Before
+ public void setUp() {
+ lifecycleManager = new StubLifecycleManager();
+ listener = new TRpcShutdownListener(lifecycleManager);
+ context = new GenericApplicationContext();
+ }
+
+ @Test(expected = NullPointerException.class)
+ public void testNullLifecycleManager() {
+ new TRpcShutdownListener(null);
+ }
+
+ @Test
+ public void testGetOrder() {
+ Assert.assertEquals(TRpcShutdownListener.STOP_ORDER, listener.getOrder());
+ Assert.assertEquals(Ordered.HIGHEST_PRECEDENCE + 20000, TRpcShutdownListener.STOP_ORDER);
+ }
+
+ @Test
+ public void testStopOnManagedContextClosed() {
+ lifecycleManager.managed = true;
+ listener.onApplicationEvent(new ContextClosedEvent(context));
+ Assert.assertEquals(1, lifecycleManager.stopCount);
+ }
+
+ @Test
+ public void testIgnoreNotManagedContextClosed() {
+ lifecycleManager.managed = false;
+ listener.onApplicationEvent(new ContextClosedEvent(context));
+ Assert.assertEquals(0, lifecycleManager.stopCount);
+ }
+
+ private static class StubLifecycleManager extends TRpcLifecycleManager {
+
+ private boolean managed;
+
+ private int stopCount;
+
+ @Override
+ public boolean isManagedContext(ApplicationContext context) {
+ return managed;
+ }
+
+ @Override
+ public void stopIfStarted() {
+ stopCount++;
+ }
+ }
+}
diff --git a/trpc-spring-support/trpc-springmvc/src/main/java/com/tencent/trpc/springmvc/TRpcServiceExportListener.java b/trpc-spring-support/trpc-springmvc/src/main/java/com/tencent/trpc/springmvc/TRpcServiceExportListener.java
new file mode 100644
index 000000000..9bbdf2ac4
--- /dev/null
+++ b/trpc-spring-support/trpc-springmvc/src/main/java/com/tencent/trpc/springmvc/TRpcServiceExportListener.java
@@ -0,0 +1,74 @@
+/*
+ * Tencent is pleased to support the open source community by making tRPC available.
+ *
+ * Copyright (C) 2023 Tencent.
+ * All rights reserved.
+ *
+ * If you have downloaded a copy of the tRPC source code from Tencent,
+ * please note that tRPC source code is licensed under the Apache 2.0 License,
+ * A copy of the Apache 2.0 License can be found in the LICENSE file.
+ */
+
+package com.tencent.trpc.springmvc;
+
+import com.tencent.trpc.core.logger.Logger;
+import com.tencent.trpc.core.logger.LoggerFactory;
+import com.tencent.trpc.spring.context.TRpcLifecycleManager;
+import java.util.Objects;
+import org.springframework.context.ApplicationListener;
+import org.springframework.context.event.ContextRefreshedEvent;
+import org.springframework.core.Ordered;
+import org.springframework.lang.NonNull;
+import org.springframework.lang.Nullable;
+
+/**
+ * Export tRPC rest services to {@link TRpcServiceHandlerMapping} on {@link ContextRefreshedEvent},
+ * after tRPC has been started by {@link TRpcLifecycleManager}.
+ */
+public class TRpcServiceExportListener implements ApplicationListener, Ordered {
+
+ /**
+ * Must be after {@link TRpcLifecycleManager#START_ORDER}.
+ */
+ public static final int EXPORT_ORDER = Ordered.LOWEST_PRECEDENCE - 10;
+
+ private static final Logger logger = LoggerFactory.getLogger(TRpcServiceExportListener.class);
+
+ private final TRpcServiceHandlerMapping handlerMapping;
+
+ private final TRpcLifecycleManager lifecycleManager;
+
+ /**
+ * Construct {@link TRpcServiceExportListener}.
+ *
+ * @param handlerMapping the handler mapping to export tRPC services to
+ * @param lifecycleManager the {@link TRpcLifecycleManager} which starts tRPC, null if absent
+ */
+ public TRpcServiceExportListener(TRpcServiceHandlerMapping handlerMapping,
+ @Nullable TRpcLifecycleManager lifecycleManager) {
+ this.handlerMapping = Objects.requireNonNull(handlerMapping, "handlerMapping");
+ this.lifecycleManager = lifecycleManager;
+ }
+
+ @Override
+ public void onApplicationEvent(@NonNull ContextRefreshedEvent event) {
+ if (lifecycleManager == null) {
+ logger.warn("TRpcLifecycleManager not found, skip exporting trpc backends to springmvc");
+ return;
+ }
+ if (!lifecycleManager.isManagedContext(event.getApplicationContext())) {
+ logger.debug("the event is not aware");
+ return;
+ }
+ if (!lifecycleManager.isStarted()) {
+ logger.warn("tRPC is not started, skip exporting trpc backends to springmvc");
+ return;
+ }
+ handlerMapping.exportTRpcServices();
+ }
+
+ @Override
+ public int getOrder() {
+ return EXPORT_ORDER;
+ }
+}
diff --git a/trpc-spring-support/trpc-springmvc/src/main/java/com/tencent/trpc/springmvc/TRpcServiceHandlerMapping.java b/trpc-spring-support/trpc-springmvc/src/main/java/com/tencent/trpc/springmvc/TRpcServiceHandlerMapping.java
index 807516f5e..ea9df9dd7 100644
--- a/trpc-spring-support/trpc-springmvc/src/main/java/com/tencent/trpc/springmvc/TRpcServiceHandlerMapping.java
+++ b/trpc-spring-support/trpc-springmvc/src/main/java/com/tencent/trpc/springmvc/TRpcServiceHandlerMapping.java
@@ -21,21 +21,20 @@
import com.tencent.trpc.core.rpc.common.RpcMethodInfoAndInvoker;
import com.tencent.trpc.core.rpc.def.DefMethodInfoRegister;
import com.tencent.trpc.core.rpc.def.DefProviderInvoker;
-import com.tencent.trpc.spring.util.TRpcSpringUtils;
import java.util.Map;
import java.util.Map.Entry;
import java.util.concurrent.atomic.AtomicBoolean;
import javax.servlet.http.HttpServletRequest;
-import org.springframework.context.ApplicationListener;
-import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.core.Ordered;
import org.springframework.web.servlet.handler.AbstractHandlerMapping;
/**
* TRPC service routing management class.
+ * tRPC services are exported by {@link TRpcServiceExportListener} after tRPC has started. This class
+ * does not listen to context events itself, because its {@link #getOrder()} is the handler mapping priority and
+ * must not be coupled with the event listener order.
*/
-public class TRpcServiceHandlerMapping extends AbstractHandlerMapping implements
- ApplicationListener {
+public class TRpcServiceHandlerMapping extends AbstractHandlerMapping {
private static final Logger logger = LoggerFactory.getLogger(TRpcServiceHandlerMapping.class);
@@ -49,13 +48,12 @@ public TRpcServiceHandlerMapping() {
setOrder(Ordered.HIGHEST_PRECEDENCE + 50000);
}
- @Override
+ /**
+ * Export tRPC rest services to spring mvc. Must be called after tRPC has started,
+ * so that {@link ProviderConfig} refs are replaced by spring beans and initialized.
+ */
@SuppressWarnings({"unchecked", "rawtypes"})
- public void onApplicationEvent(ContextRefreshedEvent event) {
- if (!TRpcSpringUtils.isAwareContext(event.getApplicationContext())) {
- logger.debug("the event is not aware");
- return;
- }
+ public void exportTRpcServices() {
if (!registered.compareAndSet(Boolean.FALSE, Boolean.TRUE)) {
logger.info("already exported trpc backends to springmvc");
return;
diff --git a/trpc-spring-support/trpc-springmvc/src/main/java/com/tencent/trpc/springmvc/TRpcSpringMvcAutoConfiguration.java b/trpc-spring-support/trpc-springmvc/src/main/java/com/tencent/trpc/springmvc/TRpcSpringMvcAutoConfiguration.java
index 311f8dfaa..b1c9aba91 100644
--- a/trpc-spring-support/trpc-springmvc/src/main/java/com/tencent/trpc/springmvc/TRpcSpringMvcAutoConfiguration.java
+++ b/trpc-spring-support/trpc-springmvc/src/main/java/com/tencent/trpc/springmvc/TRpcSpringMvcAutoConfiguration.java
@@ -12,8 +12,10 @@
package com.tencent.trpc.springmvc;
import com.tencent.trpc.spring.context.TRpcConfiguration;
+import com.tencent.trpc.spring.context.TRpcLifecycleManager;
import com.tencent.trpc.springmvc.proto.ProtoJsonHttpMessageConverter;
import java.util.List;
+import org.springframework.beans.factory.ObjectProvider;
import org.springframework.boot.autoconfigure.AutoConfiguration;
import org.springframework.boot.autoconfigure.condition.ConditionalOnClass;
import org.springframework.boot.autoconfigure.condition.ConditionalOnMissingBean;
@@ -43,6 +45,13 @@ public TRpcServiceHandlerMapping tRpcServiceHandlerMapping() {
return new TRpcServiceHandlerMapping();
}
+ @Bean
+ @ConditionalOnMissingBean
+ public TRpcServiceExportListener tRpcServiceExportListener(TRpcServiceHandlerMapping tRpcServiceHandlerMapping,
+ ObjectProvider tRpcLifecycleManager) {
+ return new TRpcServiceExportListener(tRpcServiceHandlerMapping, tRpcLifecycleManager.getIfAvailable());
+ }
+
@Bean
@ConditionalOnMissingBean
public TRpcHandlerAdapter tRpcHandlerAdapter() {
diff --git a/trpc-spring-support/trpc-springmvc/src/test/java/com/tencent/trpc/springmvc/TRpcServiceExportListenerTest.java b/trpc-spring-support/trpc-springmvc/src/test/java/com/tencent/trpc/springmvc/TRpcServiceExportListenerTest.java
new file mode 100644
index 000000000..d16d864ec
--- /dev/null
+++ b/trpc-spring-support/trpc-springmvc/src/test/java/com/tencent/trpc/springmvc/TRpcServiceExportListenerTest.java
@@ -0,0 +1,138 @@
+/*
+ * Tencent is pleased to support the open source community by making tRPC available.
+ *
+ * Copyright (C) 2023 Tencent.
+ * All rights reserved.
+ *
+ * If you have downloaded a copy of the tRPC source code from Tencent,
+ * please note that tRPC source code is licensed under the Apache 2.0 License,
+ * A copy of the Apache 2.0 License can be found in the LICENSE file.
+ */
+
+package com.tencent.trpc.springmvc;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.tencent.trpc.spring.context.TRpcLifecycleManager;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.beans.factory.support.DefaultListableBeanFactory;
+import org.springframework.context.ApplicationContext;
+import org.springframework.context.event.ContextRefreshedEvent;
+import org.springframework.context.support.GenericApplicationContext;
+
+public class TRpcServiceExportListenerTest {
+
+ private StubHandlerMapping handlerMapping;
+
+ private StubLifecycleManager lifecycleManager;
+
+ private GenericApplicationContext context;
+
+ @BeforeEach
+ public void setUp() {
+ handlerMapping = new StubHandlerMapping();
+ lifecycleManager = new StubLifecycleManager();
+ context = new GenericApplicationContext();
+ }
+
+ @Test
+ public void testNullHandlerMapping() {
+ assertThrows(NullPointerException.class, () -> new TRpcServiceExportListener(null, lifecycleManager));
+ }
+
+ @Test
+ public void testGetOrder() {
+ TRpcServiceExportListener listener = new TRpcServiceExportListener(handlerMapping, lifecycleManager);
+ assertEquals(TRpcServiceExportListener.EXPORT_ORDER, listener.getOrder());
+ assertTrue(TRpcServiceExportListener.EXPORT_ORDER > TRpcLifecycleManager.START_ORDER,
+ "must export after tRPC started");
+ }
+
+ @Test
+ public void testExportAfterStarted() {
+ lifecycleManager.managed = true;
+ lifecycleManager.started = true;
+ new TRpcServiceExportListener(handlerMapping, lifecycleManager)
+ .onApplicationEvent(new ContextRefreshedEvent(context));
+ assertEquals(1, handlerMapping.exportCount);
+ }
+
+ @Test
+ public void testSkipWithoutLifecycleManager() {
+ new TRpcServiceExportListener(handlerMapping, null)
+ .onApplicationEvent(new ContextRefreshedEvent(context));
+ assertEquals(0, handlerMapping.exportCount);
+ }
+
+ @Test
+ public void testSkipNotManagedContext() {
+ lifecycleManager.managed = false;
+ lifecycleManager.started = true;
+ new TRpcServiceExportListener(handlerMapping, lifecycleManager)
+ .onApplicationEvent(new ContextRefreshedEvent(context));
+ assertEquals(0, handlerMapping.exportCount);
+ }
+
+ @Test
+ public void testSkipNotStarted() {
+ lifecycleManager.managed = true;
+ lifecycleManager.started = false;
+ new TRpcServiceExportListener(handlerMapping, lifecycleManager)
+ .onApplicationEvent(new ContextRefreshedEvent(context));
+ assertEquals(0, handlerMapping.exportCount);
+ }
+
+ @Test
+ public void testAutoConfiguration() {
+ DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
+ beanFactory.registerSingleton("tRpcLifecycleManager", lifecycleManager);
+ TRpcServiceExportListener listener = new TRpcSpringMvcAutoConfiguration()
+ .tRpcServiceExportListener(handlerMapping, beanFactory.getBeanProvider(TRpcLifecycleManager.class));
+ assertNotNull(listener);
+
+ lifecycleManager.managed = true;
+ lifecycleManager.started = true;
+ listener.onApplicationEvent(new ContextRefreshedEvent(context));
+ assertEquals(1, handlerMapping.exportCount);
+ }
+
+ @Test
+ public void testAutoConfigurationWithoutLifecycleManager() {
+ DefaultListableBeanFactory beanFactory = new DefaultListableBeanFactory();
+ TRpcServiceExportListener listener = new TRpcSpringMvcAutoConfiguration()
+ .tRpcServiceExportListener(handlerMapping, beanFactory.getBeanProvider(TRpcLifecycleManager.class));
+ listener.onApplicationEvent(new ContextRefreshedEvent(context));
+ assertEquals(0, handlerMapping.exportCount);
+ }
+
+ private static class StubHandlerMapping extends TRpcServiceHandlerMapping {
+
+ private int exportCount;
+
+ @Override
+ public void exportTRpcServices() {
+ exportCount++;
+ }
+ }
+
+ private static class StubLifecycleManager extends TRpcLifecycleManager {
+
+ private boolean managed;
+
+ private boolean started;
+
+ @Override
+ public boolean isManagedContext(ApplicationContext context) {
+ return managed;
+ }
+
+ @Override
+ public boolean isStarted() {
+ return started;
+ }
+ }
+}
diff --git a/trpc-spring-support/trpc-springmvc/src/test/java/com/tencent/trpc/springmvc/TRpcServiceHandlerMappingTest.java b/trpc-spring-support/trpc-springmvc/src/test/java/com/tencent/trpc/springmvc/TRpcServiceHandlerMappingTest.java
new file mode 100644
index 000000000..5e41d247a
--- /dev/null
+++ b/trpc-spring-support/trpc-springmvc/src/test/java/com/tencent/trpc/springmvc/TRpcServiceHandlerMappingTest.java
@@ -0,0 +1,140 @@
+/*
+ * Tencent is pleased to support the open source community by making tRPC available.
+ *
+ * Copyright (C) 2023 Tencent.
+ * All rights reserved.
+ *
+ * If you have downloaded a copy of the tRPC source code from Tencent,
+ * please note that tRPC source code is licensed under the Apache 2.0 License,
+ * A copy of the Apache 2.0 License can be found in the LICENSE file.
+ */
+
+package com.tencent.trpc.springmvc;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertNull;
+
+import com.tencent.trpc.core.common.ConfigManager;
+import com.tencent.trpc.core.common.config.ProviderConfig;
+import com.tencent.trpc.core.common.config.ServiceConfig;
+import com.tencent.trpc.core.rpc.RpcContext;
+import com.tencent.trpc.core.rpc.anno.TRpcMethod;
+import com.tencent.trpc.core.rpc.anno.TRpcService;
+import com.tencent.trpc.core.rpc.common.RpcMethodInfoAndInvoker;
+import java.util.Map;
+import org.junit.jupiter.api.AfterEach;
+import org.junit.jupiter.api.BeforeEach;
+import org.junit.jupiter.api.Test;
+import org.springframework.mock.web.MockHttpServletRequest;
+
+public class TRpcServiceHandlerMappingTest {
+
+ private static final String RPC_SERVICE_NAME = "trpc.HandlerMappingTest.Server.Greeter";
+
+ private static final String RPC_METHOD_NAME = "hello";
+
+ private static final String TRPC_SERVICE = "TRpcServiceHandlerMappingTest.trpc";
+
+ private static final String REST_SERVICE = "TRpcServiceHandlerMappingTest.rest";
+
+ private static final String BROKEN_REST_SERVICE = "TRpcServiceHandlerMappingTest.broken";
+
+ private Map serviceMap;
+
+ private TRpcServiceHandlerMapping handlerMapping;
+
+ @BeforeEach
+ public void setUp() {
+ serviceMap = ConfigManager.getInstance().getServerConfig().getServiceMap();
+
+ // non-rest service is ignored
+ ServiceConfig trpcService = new ServiceConfig();
+ trpcService.setName(TRPC_SERVICE);
+ trpcService.setProtocol("trpc");
+ serviceMap.put(TRPC_SERVICE, trpcService);
+
+ ServiceConfig restService = new ServiceConfig();
+ restService.setName(REST_SERVICE);
+ restService.setProtocol("rest");
+ ProviderConfig providerConfig = new ProviderConfig<>();
+ providerConfig.setServiceInterface(GreeterService.class);
+ providerConfig.setRef(new GreeterServiceImpl());
+ providerConfig.setDisableDefaultFilter(Boolean.FALSE);
+ providerConfig.setServiceConfig(restService);
+ restService.addProviderConfig(providerConfig);
+ serviceMap.put(REST_SERVICE, restService);
+
+ // provider without service interface, export fails and is ignored
+ ServiceConfig brokenRestService = new ServiceConfig();
+ brokenRestService.setName(BROKEN_REST_SERVICE);
+ brokenRestService.setProtocol("rest");
+ brokenRestService.addProviderConfig(new ProviderConfig<>());
+ serviceMap.put(BROKEN_REST_SERVICE, brokenRestService);
+
+ handlerMapping = new TRpcServiceHandlerMapping();
+ handlerMapping.exportTRpcServices();
+ }
+
+ @AfterEach
+ public void tearDown() {
+ serviceMap.remove(TRPC_SERVICE);
+ serviceMap.remove(REST_SERVICE);
+ serviceMap.remove(BROKEN_REST_SERVICE);
+ }
+
+ @Test
+ public void testExportOnlyOnce() {
+ // the second export is ignored, otherwise duplicate router key exception is logged
+ handlerMapping.exportTRpcServices();
+ assertNotNull(handlerMapping.getHandlerInternal(
+ new MockHttpServletRequest("POST", "/" + RPC_SERVICE_NAME + "/" + RPC_METHOD_NAME)));
+ }
+
+ @Test
+ public void testRouteByPath() {
+ MockHttpServletRequest request = new MockHttpServletRequest("POST",
+ "/" + RPC_SERVICE_NAME + "/" + RPC_METHOD_NAME);
+ RpcMethodInfoAndInvoker route = handlerMapping.getHandlerInternal(request);
+ assertNotNull(route);
+ assertEquals(RPC_SERVICE_NAME, request.getAttribute(TRpcHttpConstants.REQUEST_ATTRIBUTE_TRPC_SERVICE));
+ assertEquals(RPC_METHOD_NAME, request.getAttribute(TRpcHttpConstants.REQUEST_ATTRIBUTE_TRPC_METHOD));
+ }
+
+ @Test
+ public void testRouteByParameter() {
+ MockHttpServletRequest request = new MockHttpServletRequest("GET", "/not/exist");
+ request.setParameter(TRpcHttpConstants.TRPC_PARAM_SERVICE, RPC_SERVICE_NAME);
+ request.setParameter(TRpcHttpConstants.TRPC_PARAM_METHOD, RPC_METHOD_NAME);
+ assertNotNull(handlerMapping.getHandlerInternal(request));
+ }
+
+ @Test
+ public void testRouteNotFound() {
+ MockHttpServletRequest request = new MockHttpServletRequest("GET", "/not/exist");
+ assertNull(handlerMapping.getHandlerInternal(request));
+ assertNull(request.getAttribute(TRpcHttpConstants.REQUEST_ATTRIBUTE_TRPC_SERVICE));
+ }
+
+ @Test
+ public void testUnsupportedHttpMethod() {
+ MockHttpServletRequest request = new MockHttpServletRequest("PUT",
+ "/" + RPC_SERVICE_NAME + "/" + RPC_METHOD_NAME);
+ assertNull(handlerMapping.getHandlerInternal(request));
+ }
+
+ @TRpcService(name = RPC_SERVICE_NAME)
+ public interface GreeterService {
+
+ @TRpcMethod(name = RPC_METHOD_NAME)
+ String hello(RpcContext context, String request);
+ }
+
+ public static class GreeterServiceImpl implements GreeterService {
+
+ @Override
+ public String hello(RpcContext context, String request) {
+ return request;
+ }
+ }
+}
diff --git a/trpc-spring-support/trpc-springmvc/src/test/java/com/tencent/trpc/springmvc/TRpcSpringMvcAutoConfigurationTest.java b/trpc-spring-support/trpc-springmvc/src/test/java/com/tencent/trpc/springmvc/TRpcSpringMvcAutoConfigurationTest.java
new file mode 100644
index 000000000..46a718554
--- /dev/null
+++ b/trpc-spring-support/trpc-springmvc/src/test/java/com/tencent/trpc/springmvc/TRpcSpringMvcAutoConfigurationTest.java
@@ -0,0 +1,44 @@
+/*
+ * Tencent is pleased to support the open source community by making tRPC available.
+ *
+ * Copyright (C) 2023 Tencent.
+ * All rights reserved.
+ *
+ * If you have downloaded a copy of the tRPC source code from Tencent,
+ * please note that tRPC source code is licensed under the Apache 2.0 License,
+ * A copy of the Apache 2.0 License can be found in the LICENSE file.
+ */
+
+package com.tencent.trpc.springmvc;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertNotNull;
+import static org.junit.jupiter.api.Assertions.assertTrue;
+
+import com.tencent.trpc.springmvc.proto.ProtoJsonHttpMessageConverter;
+import java.util.ArrayList;
+import java.util.List;
+import org.junit.jupiter.api.Test;
+import org.springframework.http.converter.HttpMessageConverter;
+import org.springframework.http.converter.StringHttpMessageConverter;
+
+public class TRpcSpringMvcAutoConfigurationTest {
+
+ private final TRpcSpringMvcAutoConfiguration configuration = new TRpcSpringMvcAutoConfiguration();
+
+ @Test
+ public void testBeans() {
+ assertNotNull(configuration.tRpcWebServerFactoryCustomizer());
+ assertNotNull(configuration.tRpcServiceHandlerMapping());
+ assertNotNull(configuration.tRpcHandlerAdapter());
+ }
+
+ @Test
+ public void testExtendMessageConverters() {
+ List> converters = new ArrayList<>();
+ converters.add(new StringHttpMessageConverter());
+ configuration.extendMessageConverters(converters);
+ assertEquals(2, converters.size());
+ assertTrue(converters.get(0) instanceof ProtoJsonHttpMessageConverter);
+ }
+}