diff --git a/.github/workflows/ci.yml b/.github/workflows/ci.yml index 82be6786b..445a41707 100644 --- a/.github/workflows/ci.yml +++ b/.github/workflows/ci.yml @@ -32,6 +32,8 @@ jobs: cache: maven - name: Test with Maven run: mvn clean install -Dmaven.test.skip=true -B -U --file pom.xml + - name: Test HTTP transport with Spring Boot BOM first + run: python3 trpc-transport/trpc-transport-http/src/test/it/boot-bom-first/run.py JDK21: runs-on: ubuntu-latest steps: @@ -48,6 +50,8 @@ jobs: cache: maven - name: Test with Maven run: mvn clean install -Dmaven.test.skip=true -B -U --file pom.xml + - name: Test HTTP transport with Spring Boot BOM first + run: python3 trpc-transport/trpc-transport-http/src/test/it/boot-bom-first/run.py JDK17-master: runs-on: ubuntu-latest steps: diff --git a/trpc-dependencies/trpc-dependencies-bom/pom.xml b/trpc-dependencies/trpc-dependencies-bom/pom.xml index 9dca91b50..f36efab38 100644 --- a/trpc-dependencies/trpc-dependencies-bom/pom.xml +++ b/trpc-dependencies/trpc-dependencies-bom/pom.xml @@ -96,8 +96,7 @@ 4.0.2 2.1.1 2.0.1.Final - 8.1.13.v20181017 - 11.0.25 + 12.1.14 2.7 2.4.14 5.12.2 @@ -577,85 +576,18 @@ ${javax.servlet-api.version} - jetty-server - org.eclipse.jetty - ${jetty.version} - - - jetty-servlet - org.eclipse.jetty - ${jetty.version} - - - jetty-io - org.eclipse.jetty - ${jetty.version} - - - jetty-util - org.eclipse.jetty - ${jetty.version} - - - jetty-util-ajax - org.eclipse.jetty - ${jetty.version} - - - jetty-security - org.eclipse.jetty - ${jetty.version} - - - jetty-http - org.eclipse.jetty - ${jetty.version} - - - http2-server - org.eclipse.jetty.http2 - ${jetty.version} - - - http2-common - org.eclipse.jetty.http2 - ${jetty.version} - - - http2-hpack - org.eclipse.jetty.http2 - ${jetty.version} - - - jetty-alpn-server - org.eclipse.jetty - ${jetty.version} - - - jetty-alpn-conscrypt-server - org.eclipse.jetty - ${jetty.version} - - - jetty-alpn-java-server - org.eclipse.jetty - ${jetty.version} - - - jetty-alpn-openjdk8-server org.eclipse.jetty + jetty-bom ${jetty.version} + pom + import - org.eclipse.jetty - jetty-webapp + org.eclipse.jetty.ee10 + jetty-ee10-bom ${jetty.version} - - - alpn-boot - org.mortbay.jetty.alpn - runtime - ${jetty.alpn.boot.version} + pom + import diff --git a/trpc-proto/trpc-proto-http/src/test/java/com/tencent/trpc/proto/http/server/DefaultHttpExecutorTest.java b/trpc-proto/trpc-proto-http/src/test/java/com/tencent/trpc/proto/http/server/DefaultHttpExecutorTest.java index 6df61e884..5d0a7fe1d 100644 --- a/trpc-proto/trpc-proto-http/src/test/java/com/tencent/trpc/proto/http/server/DefaultHttpExecutorTest.java +++ b/trpc-proto/trpc-proto-http/src/test/java/com/tencent/trpc/proto/http/server/DefaultHttpExecutorTest.java @@ -21,10 +21,9 @@ import com.tencent.trpc.core.rpc.def.DefProviderInvoker; import com.tencent.trpc.core.utils.NetUtils; import com.tencent.trpc.proto.standard.common.TRPCProtocol; +import jakarta.servlet.http.HttpServletRequest; +import jakarta.servlet.http.HttpServletResponse; import javax.ws.rs.HttpMethod; -import org.eclipse.jetty.server.HttpChannel; -import org.eclipse.jetty.server.Request; -import org.eclipse.jetty.server.Response; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; import org.eclipse.jetty.util.thread.QueuedThreadPool; @@ -69,9 +68,8 @@ public void testRegist() { connector.setAcceptQueueSize(60); server.addConnector(connector); - HttpChannel httpChannel = org.mockito.Mockito.mock(HttpChannel.class); - Request request = org.mockito.Mockito.mock(Request.class); - Response response = org.mockito.Mockito.mock(Response.class); + HttpServletRequest request = org.mockito.Mockito.mock(HttpServletRequest.class); + HttpServletResponse response = org.mockito.Mockito.mock(HttpServletResponse.class); org.mockito.Mockito.when(request.getMethod()).thenReturn(HttpMethod.GET); org.mockito.Mockito.when(request.getPathInfo()).thenReturn("/trpc.test.rpc.Hello/sayHello"); diff --git a/trpc-spring-support/trpc-spring-cloud-gateway/pom.xml b/trpc-spring-support/trpc-spring-cloud-gateway/pom.xml index b47d8bbfc..33827dfc5 100644 --- a/trpc-spring-support/trpc-spring-cloud-gateway/pom.xml +++ b/trpc-spring-support/trpc-spring-cloud-gateway/pom.xml @@ -45,8 +45,8 @@ trpc-rpc-support - org.eclipse.jetty - jetty-webapp + org.eclipse.jetty.ee10 + jetty-ee10-webapp com.tencent.trpc @@ -64,4 +64,4 @@ - \ No newline at end of file + diff --git a/trpc-transport/trpc-transport-http/pom.xml b/trpc-transport/trpc-transport-http/pom.xml index 72c0c34f1..f70fa1b0d 100644 --- a/trpc-transport/trpc-transport-http/pom.xml +++ b/trpc-transport/trpc-transport-http/pom.xml @@ -25,22 +25,17 @@ jetty-server - org.eclipse.jetty - jetty-servlet + org.eclipse.jetty.ee10 + jetty-ee10-servlet org.eclipse.jetty.http2 - http2-server + jetty-http2-server org.eclipse.jetty jetty-alpn-server - - org.mortbay.jetty.alpn - alpn-boot - runtime - org.eclipse.jetty jetty-alpn-conscrypt-server diff --git a/trpc-transport/trpc-transport-http/src/main/java/com/tencent/trpc/transport/http/support/jetty/JettyHttpServer.java b/trpc-transport/trpc-transport-http/src/main/java/com/tencent/trpc/transport/http/support/jetty/JettyHttpServer.java index abbc6093b..a0cc63b88 100755 --- a/trpc-transport/trpc-transport-http/src/main/java/com/tencent/trpc/transport/http/support/jetty/JettyHttpServer.java +++ b/trpc-transport/trpc-transport-http/src/main/java/com/tencent/trpc/transport/http/support/jetty/JettyHttpServer.java @@ -20,11 +20,11 @@ import com.tencent.trpc.transport.http.ExecutorDispatcher; import com.tencent.trpc.transport.http.HttpExecutor; import com.tencent.trpc.transport.http.common.ServletManager; +import org.eclipse.jetty.ee10.servlet.ServletContextHandler; +import org.eclipse.jetty.ee10.servlet.ServletHandler; +import org.eclipse.jetty.ee10.servlet.ServletHolder; import org.eclipse.jetty.server.Server; import org.eclipse.jetty.server.ServerConnector; -import org.eclipse.jetty.servlet.ServletContextHandler; -import org.eclipse.jetty.servlet.ServletHandler; -import org.eclipse.jetty.servlet.ServletHolder; import org.eclipse.jetty.util.thread.QueuedThreadPool; /** @@ -120,9 +120,10 @@ private ServletContextHandler getServletContextHandler(Server server) { servletHandler.addServletWithMapping(ExecutorDispatcher.class, "/*"); servletHolder.setAsyncSupported(true); servletHolder.setInitOrder(1); - ServletContextHandler context = - new ServletContextHandler(server, "/", ServletContextHandler.SESSIONS); + ServletContextHandler context = new ServletContextHandler(ServletContextHandler.SESSIONS); + context.setContextPath("/"); context.setServletHandler(servletHandler); + server.setHandler(context); return context; } diff --git a/trpc-transport/trpc-transport-http/src/test/it/boot-bom-first/README.md b/trpc-transport/trpc-transport-http/src/test/it/boot-bom-first/README.md new file mode 100644 index 000000000..05379070f --- /dev/null +++ b/trpc-transport/trpc-transport-http/src/test/it/boot-bom-first/README.md @@ -0,0 +1,52 @@ +# Spring Boot BOM compatibility regression + +This consumer reproduces the dependency-management order from issue +[#152](https://github.com/trpc-group/trpc-java/issues/152): Spring Boot's BOM precedes +tRPC's BOM. It reuses the transport tests against the installed tRPC artifacts, +covering HTTP/1.1, HTTPS, h2c, h2 with ALPN, request bodies and asynchronous Servlet +responses. All server sockets bind explicitly to `127.0.0.1`. + +Run from the repository root with JDK 17 or 21: + +```sh +mvn -B -ntp -pl trpc-dependencies/trpc-dependencies-bom,trpc-dependencies/trpc-bom,trpc-transport/trpc-transport-http -am -Dmaven.test.skip=true install +mvn -B -ntp -f trpc-transport/trpc-transport-http/src/test/it/boot-bom-first/pom.xml test +``` + +To test the released implementation as a negative control, add +`-Dtrpc.version=2.0.5` to the second command. With Spring Boot 3.5.16 this mixes +Jetty 12 core artifacts with Jetty 11 Servlet/HTTP2 artifacts and fails during +server initialization. The fixed snapshot uses Jetty 12 core and EE10 BOMs, +EE10 Servlet coordinates, and the renamed HTTP/2 artifacts. + +The JDK 17 and JDK 21 Maven install CI jobs run the compatibility matrix after +installing the reactor artifacts: + +```sh +python3 trpc-transport/trpc-transport-http/src/test/it/boot-bom-first/run.py +``` + +The runner reads the current `springboot.version` from tRPC's dependency BOM and +Maven Central's release metadata. It selects every published stable patch of +that baseline's minor line and all later stable versions, plus the latest patch +of the two preceding minor lines within the same major. For the 3.5.0 baseline, +this covers every 3.5.x patch and +later stable releases, with 3.4.x and 3.3.x as backward compatibility samples. +Milestones, release candidates and snapshots are excluded. Boot 3.1 and Jetty 11 +are outside this support range; their incompatibility is not a release blocker. +New stable versions are picked up on subsequent CI runs, rather than assumed +compatible in advance. + +Jetty core and EE10 BOMs default to the 12.1 line. Boot 3.5's BOM manages both +core and EE10 artifacts, selecting its aligned 12.0.x versions. Boot 4 manages +EE11 instead of EE10, so tRPC must provide a 12.1-compatible EE10 default to +avoid mixing EE10 12.0 with a 12.1 core. The Servlet-based tRPC executor contract +is unchanged. These tests cover the HTTP transport under each BOM, not the full +Spring Boot starter, application auto-configuration or gateway application. + +Each version runs in a separate Maven invocation. Results and dependency trees +are saved under the repository's `target/jetty-boot-compatibility/` directory. +The normal reactor `test` command does not invoke this fixture. Additional +arguments to `run.py` are passed to Maven, so isolated settings and repository +options can be reused. For a single version, use the standalone POM command +above with `-Dspring-boot.version=...`. diff --git a/trpc-transport/trpc-transport-http/src/test/it/boot-bom-first/pom.xml b/trpc-transport/trpc-transport-http/src/test/it/boot-bom-first/pom.xml new file mode 100644 index 000000000..e25a5e836 --- /dev/null +++ b/trpc-transport/trpc-transport-http/src/test/it/boot-bom-first/pom.xml @@ -0,0 +1,97 @@ + + + 4.0.0 + + com.tencent.trpc.tests + jetty-boot-bom-first + 1.0-SNAPSHOT + + + 2.0.5-SNAPSHOT + 3.5.16 + 17 + UTF-8 + + + + + + + org.springframework.boot + spring-boot-dependencies + ${spring-boot.version} + pom + import + + + com.tencent.trpc + trpc-dependencies-bom + ${trpc.version} + pom + import + + + + + + + com.tencent.trpc + trpc-transport-http + ${trpc.version} + + + org.apache.httpcomponents.core5 + httpcore5-h2 + test + + + org.apache.httpcomponents.client5 + httpclient5 + test + + + org.apache.httpcomponents + httpcore + test + + + org.apache.httpcomponents + httpclient + test + + + org.junit.jupiter + junit-jupiter + test + + + org.mockito + mockito-core + test + + + + + + ${project.basedir}/../../java + + + ${project.basedir}/../../resources + + + + + org.apache.maven.plugins + maven-compiler-plugin + 3.11.0 + + + org.apache.maven.plugins + maven-surefire-plugin + 3.0.0-M9 + + + + diff --git a/trpc-transport/trpc-transport-http/src/test/it/boot-bom-first/run.py b/trpc-transport/trpc-transport-http/src/test/it/boot-bom-first/run.py new file mode 100644 index 000000000..b1247e06f --- /dev/null +++ b/trpc-transport/trpc-transport-http/src/test/it/boot-bom-first/run.py @@ -0,0 +1,87 @@ +#!/usr/bin/env python3 +"""Run the HTTP transport contract against the supported Boot BOM range.""" + +import json +from pathlib import Path +import re +import subprocess +import sys +import time +import urllib.request +import xml.etree.ElementTree as ET + + +METADATA = ( + "https://repo.maven.apache.org/maven2/org/springframework/boot/" + "spring-boot-dependencies/maven-metadata.xml" +) + + +def select_versions(metadata, baseline): + versions = sorted({ + tuple(map(int, item.text.split("."))) + for item in metadata.findall("versioning/versions/version") + if item.text and re.fullmatch(r"\d+\.\d+\.\d+", item.text) + }) + if baseline not in versions: + raise ValueError("The framework's Boot baseline is absent from Maven Central") + previous_lines = sorted({ + version[:2] for version in versions + if version[0] == baseline[0] and version[:2] < baseline[:2] + })[-2:] + previous = [max(v for v in versions if v[:2] == line) for line in previous_lines] + supported = previous + [v for v in versions if v[:2] >= baseline[:2]] + return [".".join(map(str, v)) for v in supported] + + +def main(): + fixture = Path(__file__).resolve().parent + repository = fixture.parents[5] + ns = {"m": "http://maven.apache.org/POM/4.0.0"} + bom = ET.parse(repository / "trpc-dependencies/trpc-dependencies-bom/pom.xml") + baseline_text = bom.findtext("m:properties/m:springboot.version", namespaces=ns) + baseline = tuple(map(int, baseline_text.split("."))) + with urllib.request.urlopen(METADATA, timeout=30) as response: + metadata = ET.fromstring(response.read()) + versions = select_versions(metadata, baseline) + output = repository / "target/jetty-boot-compatibility" + output.mkdir(parents=True, exist_ok=True) + (output / "summary.json").write_text("[]\n") + print("HTTP transport Boot BOM versions: " + ", ".join(versions), flush=True) + results = [] + for version in versions: + log = output / (version + ".log") + command = [ + "mvn", "-B", "-ntp", "-f", str(fixture / "pom.xml"), *sys.argv[1:], + "-Dspring-boot.version=" + version, + "org.apache.maven.plugins:maven-dependency-plugin:3.8.1:tree", + "-Dincludes=org.eclipse.jetty*:*,jakarta.servlet:*", "clean", "test", + ] + started = time.time() + with log.open("w") as stream: + process = subprocess.run(command, stdout=stream, stderr=subprocess.STDOUT) + counts = dict.fromkeys(("tests", "failures", "errors", "skipped"), 0) + for report in (fixture / "target/surefire-reports").glob("TEST-*.xml"): + if report.stat().st_mtime < started: + continue + suite = ET.parse(report).getroot() + for key in counts: + counts[key] += int(suite.get(key, "0")) + passed = process.returncode == 0 and counts["tests"] > 0 and not any( + counts[key] for key in ("failures", "errors", "skipped") + ) + text = log.read_text() + jetty = re.search(r"org.eclipse.jetty:jetty-server:jar:([^:]+):", text) + result = {"boot": version, "passed": passed, "exit_code": process.returncode, + "jetty": jetty.group(1) if jetty else None, **counts, "log": str(log)} + results.append(result) + (output / "summary.json").write_text(json.dumps(results, indent=2) + "\n") + print(json.dumps(result), flush=True) + if not passed: + errors = [line for line in text.splitlines() if "[ERROR]" in line or "Caused by:" in line] + print("\n".join(errors[:15]), flush=True) + return 0 if all(result["passed"] for result in results) else 1 + + +if __name__ == "__main__": + sys.exit(main()) diff --git a/trpc-transport/trpc-transport-http/src/test/java/com/tencent/trpc/transport/http/JettyHttp2ServerTest.java b/trpc-transport/trpc-transport-http/src/test/java/com/tencent/trpc/transport/http/JettyHttp2ServerTest.java index 0b2e39a2c..42305a713 100644 --- a/trpc-transport/trpc-transport-http/src/test/java/com/tencent/trpc/transport/http/JettyHttp2ServerTest.java +++ b/trpc-transport/trpc-transport-http/src/test/java/com/tencent/trpc/transport/http/JettyHttp2ServerTest.java @@ -62,7 +62,7 @@ public class JettyHttp2ServerTest { public static void beforeClass() throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException, KeyManagementException, InterruptedException { ProtocolConfig protocolConfig = ProtocolConfig.newInstance(); - protocolConfig.setIp("localhost"); + protocolConfig.setIp("127.0.0.1"); protocolConfig.setPort(18082); protocolConfig.setProtocol(HTTP2_SCHEME); protocolConfig.setTransporter("jetty"); diff --git a/trpc-transport/trpc-transport-http/src/test/java/com/tencent/trpc/transport/http/JettyHttp2cServerTest.java b/trpc-transport/trpc-transport-http/src/test/java/com/tencent/trpc/transport/http/JettyHttp2cServerTest.java index af653d798..edfc97b7c 100644 --- a/trpc-transport/trpc-transport-http/src/test/java/com/tencent/trpc/transport/http/JettyHttp2cServerTest.java +++ b/trpc-transport/trpc-transport-http/src/test/java/com/tencent/trpc/transport/http/JettyHttp2cServerTest.java @@ -52,7 +52,7 @@ public class JettyHttp2cServerTest { @BeforeAll public static void beforeClass() throws InterruptedException { ProtocolConfig protocolConfig = ProtocolConfig.newInstance(); - protocolConfig.setIp("localhost"); + protocolConfig.setIp("127.0.0.1"); protocolConfig.setPort(18081); protocolConfig.setProtocol(HTTP2_SCHEME); protocolConfig.setTransporter("jetty"); diff --git a/trpc-transport/trpc-transport-http/src/test/java/com/tencent/trpc/transport/http/JettyHttpsServerTest.java b/trpc-transport/trpc-transport-http/src/test/java/com/tencent/trpc/transport/http/JettyHttpsServerTest.java index 597d40d00..8535b7491 100644 --- a/trpc-transport/trpc-transport-http/src/test/java/com/tencent/trpc/transport/http/JettyHttpsServerTest.java +++ b/trpc-transport/trpc-transport-http/src/test/java/com/tencent/trpc/transport/http/JettyHttpsServerTest.java @@ -62,7 +62,7 @@ public class JettyHttpsServerTest { public static void beforeClass() throws CertificateException, NoSuchAlgorithmException, KeyStoreException, IOException, KeyManagementException, InterruptedException { ProtocolConfig protocolConfig = ProtocolConfig.newInstance(); - protocolConfig.setIp("localhost"); + protocolConfig.setIp("127.0.0.1"); protocolConfig.setPort(18083); protocolConfig.setProtocol(HTTP_SCHEME); protocolConfig.setTransporter("jetty"); diff --git a/trpc-transport/trpc-transport-http/src/test/java/com/tencent/trpc/transport/http/JettyServerTest.java b/trpc-transport/trpc-transport-http/src/test/java/com/tencent/trpc/transport/http/JettyServerTest.java index 3cd114064..c56b6ef86 100644 --- a/trpc-transport/trpc-transport-http/src/test/java/com/tencent/trpc/transport/http/JettyServerTest.java +++ b/trpc-transport/trpc-transport-http/src/test/java/com/tencent/trpc/transport/http/JettyServerTest.java @@ -19,8 +19,11 @@ import com.tencent.trpc.core.logger.LoggerFactory; import com.tencent.trpc.transport.http.common.ServletManager; import com.tencent.trpc.transport.http.spi.HttpServerFactory; +import jakarta.servlet.AsyncContext; +import java.io.IOException; import java.net.HttpURLConnection; import java.net.URL; +import java.nio.charset.StandardCharsets; import org.eclipse.jetty.http.HttpStatus; import org.junit.jupiter.api.AfterAll; import org.junit.jupiter.api.Assertions; @@ -36,7 +39,7 @@ public class JettyServerTest { @BeforeAll public static void beforeClass() { ProtocolConfig protocolConfig = ProtocolConfig.newInstance(); - protocolConfig.setIp("localhost"); + protocolConfig.setIp("127.0.0.1"); protocolConfig.setPort(18080); protocolConfig.setProtocol("http"); protocolConfig.setTransporter("jetty"); @@ -49,6 +52,20 @@ public static void beforeClass() { String pathInfo = request.getPathInfo(); if ("/".equals(pathInfo)) { response.setStatus(HttpStatus.OK_200); + } else if ("/echo".equals(pathInfo)) { + response.getWriter().write(request.getMethod() + ":" + pathInfo + ":" + + request.getParameter("id") + ":" + request.getReader().readLine()); + } else if ("/async".equals(pathInfo)) { + AsyncContext asyncContext = request.startAsync(); + asyncContext.start(() -> { + try { + asyncContext.getResponse().getWriter().write("async response"); + } catch (IOException e) { + throw new IllegalStateException(e); + } finally { + asyncContext.complete(); + } + }); } else { response.sendError(HttpStatus.NOT_FOUND_404); } @@ -122,4 +139,44 @@ public void testNotExistRequest() throws Exception { } } + @Test + public void testPostRequestBody() throws Exception { + HttpURLConnection connection = (HttpURLConnection) + new URL("http://127.0.0.1:18080/echo?id=42").openConnection(); + try { + connection.setRequestMethod("POST"); + connection.setRequestProperty("Content-Type", "text/plain; charset=UTF-8"); + connection.setConnectTimeout(1000); + connection.setReadTimeout(5000); + connection.setDoOutput(true); + try (var output = connection.getOutputStream()) { + output.write("request body".getBytes(StandardCharsets.UTF_8)); + } + Assertions.assertEquals(200, connection.getResponseCode()); + try (var input = connection.getInputStream()) { + Assertions.assertEquals("POST:/echo:42:request body", + new String(input.readAllBytes(), StandardCharsets.UTF_8)); + } + } finally { + connection.disconnect(); + } + } + + @Test + public void testAsyncRequest() throws Exception { + HttpURLConnection connection = (HttpURLConnection) + new URL("http://127.0.0.1:18080/async").openConnection(); + try { + connection.setConnectTimeout(1000); + connection.setReadTimeout(5000); + Assertions.assertEquals(200, connection.getResponseCode()); + try (var input = connection.getInputStream()) { + Assertions.assertEquals("async response", + new String(input.readAllBytes(), StandardCharsets.UTF_8)); + } + } finally { + connection.disconnect(); + } + } + }