From ba208d7b80e16248c8d4b6290758eeccb042651a Mon Sep 17 00:00:00 2001 From: laynexiong Date: Mon, 2 Nov 2026 14:30:01 +0800 Subject: [PATCH] =?UTF-8?q?feat:=20=E6=8A=8A=20tRPC=20=E7=9A=84=E5=90=AF?= =?UTF-8?q?=E5=8A=A8=E5=92=8C=E5=81=9C=E6=AD=A2=E6=8B=86=E6=88=90=E4=B8=A4?= =?UTF-8?q?=E4=B8=AA=E7=9B=91=E5=90=AC=E5=99=A8=EF=BC=9A=E5=90=AF=E5=8A=A8?= =?UTF-8?q?=E6=8E=A8=E8=BF=9F=E5=88=B0=20LOWEST=20-=2020=EF=BC=8C=E7=AD=89?= =?UTF-8?q?=20RefreshScope=20=E7=AD=89=20refresh=20=E7=9B=91=E5=90=AC?= =?UTF-8?q?=E5=99=A8=E6=89=A7=E8=A1=8C=E5=AE=8C=EF=BC=9B=E5=81=9C=E6=AD=A2?= =?UTF-8?q?=E4=BB=8D=E5=9C=A8=20HIGHEST=20+=2020000=20=E6=9C=80=E5=85=88?= =?UTF-8?q?=E6=89=A7=E8=A1=8C=E3=80=82springmvc=20=E8=B7=AF=E7=94=B1?= =?UTF-8?q?=E5=AF=BC=E5=87=BA=E4=B9=9F=E6=94=B9=E6=88=90=20tRPC=20?= =?UTF-8?q?=E5=90=AF=E5=8A=A8=E5=90=8E=E6=89=8D=E5=81=9A=EF=BC=9B=E5=90=8C?= =?UTF-8?q?=E6=97=B6=E5=8F=AA=E5=A4=84=E7=90=86=E6=89=80=E5=B1=9E=E4=B8=8A?= =?UTF-8?q?=E4=B8=8B=E6=96=87=E7=9A=84=E4=BA=8B=E4=BB=B6=EF=BC=8C=E5=90=AF?= =?UTF-8?q?=E5=8A=A8=E5=A4=B1=E8=B4=A5=EF=BC=88=E5=90=AB=20Error=EF=BC=89?= =?UTF-8?q?=E6=97=B6=E5=9B=9E=E6=BB=9A=E7=8A=B6=E6=80=81=EF=BC=8C=E7=94=A8?= =?UTF-8?q?=20destroy()=20=E5=85=9C=E5=BA=95=E5=81=9C=E6=AD=A2=EF=BC=8C?= =?UTF-8?q?=E5=90=AF=E5=8A=A8=E6=A0=87=E8=AE=B0=E6=94=B9=E4=B8=BA=E5=AE=9E?= =?UTF-8?q?=E4=BE=8B=E7=BA=A7=E3=80=82?= MIME-Version: 1.0 Content-Type: text/plain; charset=UTF-8 Content-Transfer-Encoding: 8bit --- .../TRpcApplicationFailedListener.java | 37 ++- .../TRpcApplicationFailedListenerTest.java | 75 +++++ .../spring/context/TRpcConfiguration.java | 11 + .../spring/context/TRpcLifecycleManager.java | 109 +++++--- .../spring/context/TRpcShutdownListener.java | 50 ++++ .../context/TRpcLifecycleManagerTest.java | 257 ++++++++++++++++++ .../context/TRpcShutdownListenerTest.java | 78 ++++++ .../springmvc/TRpcServiceExportListener.java | 74 +++++ .../springmvc/TRpcServiceHandlerMapping.java | 20 +- .../TRpcSpringMvcAutoConfiguration.java | 9 + .../TRpcServiceExportListenerTest.java | 138 ++++++++++ .../TRpcServiceHandlerMappingTest.java | 140 ++++++++++ .../TRpcSpringMvcAutoConfigurationTest.java | 44 +++ 13 files changed, 982 insertions(+), 60 deletions(-) create mode 100644 trpc-spring-boot-starters/trpc-spring-boot-starter/src/test/java/com/tencent/trpc/spring/boot/starters/listener/TRpcApplicationFailedListenerTest.java create mode 100644 trpc-spring-support/trpc-spring/src/main/java/com/tencent/trpc/spring/context/TRpcShutdownListener.java create mode 100644 trpc-spring-support/trpc-spring/src/test/java/com/tencent/trpc/spring/context/TRpcLifecycleManagerTest.java create mode 100644 trpc-spring-support/trpc-spring/src/test/java/com/tencent/trpc/spring/context/TRpcShutdownListenerTest.java create mode 100644 trpc-spring-support/trpc-springmvc/src/main/java/com/tencent/trpc/springmvc/TRpcServiceExportListener.java create mode 100644 trpc-spring-support/trpc-springmvc/src/test/java/com/tencent/trpc/springmvc/TRpcServiceExportListenerTest.java create mode 100644 trpc-spring-support/trpc-springmvc/src/test/java/com/tencent/trpc/springmvc/TRpcServiceHandlerMappingTest.java create mode 100644 trpc-spring-support/trpc-springmvc/src/test/java/com/tencent/trpc/springmvc/TRpcSpringMvcAutoConfigurationTest.java 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 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); + } +}