This is an automated email from the ASF dual-hosted git repository. jamesnetherton pushed a commit to branch 3.8.x in repository https://gitbox.apache.org/repos/asf/camel-quarkus.git
commit e77cbd02a8cd6c947c12f1f93b5e2371bf42ad6e Author: James Netherton <jamesnether...@gmail.com> AuthorDate: Mon Mar 4 15:50:48 2024 +0000 Support loadOnStartup, async, forceAwait & executorRef configuration options Fixes #5834 --- .../ROOT/pages/reference/extensions/servlet.adoc | 48 ++++++++++++ .../servlet/deployment/ServletProcessor.java | 19 ++++- .../servlet/runtime/CamelServletConfig.java | 27 +++++++ .../quarkus/component/servlet/CamelRoute.java | 34 +++++++-- .../component/servlet/ServletProducers.java | 71 +++++++++++++++++ .../src/main/resources/application.properties | 50 ++++++++---- .../component/servlet/CamelServletTest.java | 89 ++++++++++++++++++---- 7 files changed, 304 insertions(+), 34 deletions(-) diff --git a/docs/modules/ROOT/pages/reference/extensions/servlet.adoc b/docs/modules/ROOT/pages/reference/extensions/servlet.adoc index ac9303ed92..848ac1299c 100644 --- a/docs/modules/ROOT/pages/reference/extensions/servlet.adoc +++ b/docs/modules/ROOT/pages/reference/extensions/servlet.adoc @@ -156,6 +156,30 @@ A servletName as it would be defined in a `web.xml` file or in the `jakarta.serv | `string` | `CamelServlet` +|icon:lock[title=Fixed at build time] [[quarkus.camel.servlet.load-on-startup]]`link:#quarkus.camel.servlet.load-on-startup[quarkus.camel.servlet.load-on-startup]` + +Sets the loadOnStartup priority on the Servlet. A loadOnStartup is a value greater than or equal to zero, indicates to the container the initialization priority of the Servlet. If loadOnStartup is a negative integer, the Servlet is initialized lazily. +| `java.lang.Integer` +| `-1` + +|icon:lock[title=Fixed at build time] [[quarkus.camel.servlet.async]]`link:#quarkus.camel.servlet.async[quarkus.camel.servlet.async]` + +Enables Camel to benefit from asynchronous Servlet support. +| `boolean` +| `false` + +|icon:lock[title=Fixed at build time] [[quarkus.camel.servlet.force-await]]`link:#quarkus.camel.servlet.force-await[quarkus.camel.servlet.force-await]` + +When set to `true` used in conjunction with `quarkus.camel.servlet.async = true`, this will force route processing to run synchronously. +| `boolean` +| `false` + +|icon:lock[title=Fixed at build time] [[quarkus.camel.servlet.executor-ref]]`link:#quarkus.camel.servlet.executor-ref[quarkus.camel.servlet.executor-ref]` + +The name of a bean to configure an optional custom thread pool for handling Camel Servlet processing. +| `string` +| + |icon:lock[title=Fixed at build time] [[quarkus.camel.servlet.multipart.location]]`link:#quarkus.camel.servlet.multipart.location[quarkus.camel.servlet.multipart.location]` An absolute path to a directory on the file system to store files temporarily while the parts are processed or when the size of the file exceeds the specified file-size-threshold configuration value. @@ -198,6 +222,30 @@ A servletName as it would be defined in a `web.xml` file or in the `jakarta.serv | `string` | `CamelServlet` +|icon:lock[title=Fixed at build time] [[quarkus.camel.servlet.-named-servlets-.load-on-startup]]`link:#quarkus.camel.servlet.-named-servlets-.load-on-startup[quarkus.camel.servlet."named-servlets".load-on-startup]` + +Sets the loadOnStartup priority on the Servlet. A loadOnStartup is a value greater than or equal to zero, indicates to the container the initialization priority of the Servlet. If loadOnStartup is a negative integer, the Servlet is initialized lazily. +| `java.lang.Integer` +| `-1` + +|icon:lock[title=Fixed at build time] [[quarkus.camel.servlet.-named-servlets-.async]]`link:#quarkus.camel.servlet.-named-servlets-.async[quarkus.camel.servlet."named-servlets".async]` + +Enables Camel to benefit from asynchronous Servlet support. +| `boolean` +| `false` + +|icon:lock[title=Fixed at build time] [[quarkus.camel.servlet.-named-servlets-.force-await]]`link:#quarkus.camel.servlet.-named-servlets-.force-await[quarkus.camel.servlet."named-servlets".force-await]` + +When set to `true` used in conjunction with `quarkus.camel.servlet.async = true`, this will force route processing to run synchronously. +| `boolean` +| `false` + +|icon:lock[title=Fixed at build time] [[quarkus.camel.servlet.-named-servlets-.executor-ref]]`link:#quarkus.camel.servlet.-named-servlets-.executor-ref[quarkus.camel.servlet."named-servlets".executor-ref]` + +The name of a bean to configure an optional custom thread pool for handling Camel Servlet processing. +| `string` +| + |icon:lock[title=Fixed at build time] [[quarkus.camel.servlet.-named-servlets-.multipart.location]]`link:#quarkus.camel.servlet.-named-servlets-.multipart.location[quarkus.camel.servlet."named-servlets".multipart.location]` An absolute path to a directory on the file system to store files temporarily while the parts are processed or when the size of the file exceeds the specified file-size-threshold configuration value. diff --git a/extensions/servlet/deployment/src/main/java/org/apache/camel/quarkus/component/servlet/deployment/ServletProcessor.java b/extensions/servlet/deployment/src/main/java/org/apache/camel/quarkus/component/servlet/deployment/ServletProcessor.java index 7e21a43adb..6afaf14202 100644 --- a/extensions/servlet/deployment/src/main/java/org/apache/camel/quarkus/component/servlet/deployment/ServletProcessor.java +++ b/extensions/servlet/deployment/src/main/java/org/apache/camel/quarkus/component/servlet/deployment/ServletProcessor.java @@ -63,7 +63,6 @@ class ServletProcessor { throw new IllegalStateException( "Map at least one servlet to a path using quarkus.camel.servlet.url-patterns or quarkus.camel.servlet.[your-servlet-name].url-patterns"); } - } static ServletBuildItem newServlet(String key, ServletConfig servletConfig) { @@ -80,6 +79,24 @@ class ServletProcessor { builder.addMapping(pattern); } + // NOTE: We only configure loadOnStartup, async & forceAwait if the default values were overridden + if (servletConfig.loadOnStartup > -1) { + builder.setLoadOnStartup(servletConfig.loadOnStartup); + } + + if (servletConfig.async) { + builder.setAsyncSupported(servletConfig.async); + builder.addInitParam("async", "true"); + } + + if (servletConfig.forceAwait) { + builder.addInitParam("forceAwait", "true"); + } + + servletConfig.executorRef.ifPresent(executorRef -> { + builder.addInitParam("executorRef", executorRef); + }); + MultipartConfig multipartConfig = servletConfig.multipart; if (multipartConfig != null) { builder.setMultipartConfig(new MultipartConfigElement( diff --git a/extensions/servlet/runtime/src/main/java/org/apache/camel/quarkus/servlet/runtime/CamelServletConfig.java b/extensions/servlet/runtime/src/main/java/org/apache/camel/quarkus/servlet/runtime/CamelServletConfig.java index 62ac5c7131..ff20269995 100644 --- a/extensions/servlet/runtime/src/main/java/org/apache/camel/quarkus/servlet/runtime/CamelServletConfig.java +++ b/extensions/servlet/runtime/src/main/java/org/apache/camel/quarkus/servlet/runtime/CamelServletConfig.java @@ -64,6 +64,33 @@ public final class CamelServletConfig { @ConfigItem(defaultValue = DEFAULT_SERVLET_NAME) public String servletName; + /** + * Sets the loadOnStartup priority on the Servlet. A loadOnStartup is a value greater than or equal to zero, + * indicates to the container the initialization priority of the Servlet. If loadOnStartup is a negative + * integer, the Servlet is initialized lazily. + */ + @ConfigItem(defaultValue = "-1") + public Integer loadOnStartup; + + /** + * Enables Camel to benefit from asynchronous Servlet support. + */ + @ConfigItem(defaultValue = "false") + public boolean async; + + /** + * When set to {@code true} used in conjunction with {@code quarkus.camel.servlet.async = true}, this will force + * route processing to run synchronously. + */ + @ConfigItem(defaultValue = "false") + public boolean forceAwait; + + /** + * The name of a bean to configure an optional custom thread pool for handling Camel Servlet processing. + */ + @ConfigItem + public Optional<String> executorRef; + /** * Servlet multipart request configuration. */ diff --git a/integration-tests/servlet/src/main/java/org/apache/camel/quarkus/component/servlet/CamelRoute.java b/integration-tests/servlet/src/main/java/org/apache/camel/quarkus/component/servlet/CamelRoute.java index aabc992357..6c8c117c35 100644 --- a/integration-tests/servlet/src/main/java/org/apache/camel/quarkus/component/servlet/CamelRoute.java +++ b/integration-tests/servlet/src/main/java/org/apache/camel/quarkus/component/servlet/CamelRoute.java @@ -18,6 +18,8 @@ package org.apache.camel.quarkus.component.servlet; import jakarta.enterprise.context.ApplicationScoped; import jakarta.inject.Inject; +import jakarta.inject.Named; +import org.apache.camel.Processor; import org.apache.camel.builder.RouteBuilder; @ApplicationScoped @@ -25,6 +27,10 @@ public class CamelRoute extends RouteBuilder { @Inject MultiPartProcessor multiPartProcessor; + @Inject + @Named("servletConfigInfoProcessor") + Processor servletConfigInfoProcessor; + @Override public void configure() { // by default the camel-quarkus-rest component sets platform-http @@ -43,11 +49,14 @@ public class CamelRoute extends RouteBuilder { from("servlet://hello?matchOnUriPrefix=true") .setBody(constant("GET: /hello")); - from("servlet://custom?servletName=my-named-servlet") + from("servlet://configuration") + .process(servletConfigInfoProcessor); + + from("servlet://custom?servletName=custom-servlet") .setBody(constant("GET: /custom")); - from("servlet://favorite?servletName=my-favorite-servlet") - .setBody(constant("GET: /favorite")); + from("servlet://named?servletName=my-named-servlet") + .setBody(constant("GET: /my-named-servlet")); from("direct:echoMethodPath") .setBody().simple("${header.CamelHttpMethod}: ${header.CamelServletContextPath}"); @@ -55,8 +64,23 @@ public class CamelRoute extends RouteBuilder { from("servlet://multipart/default?attachmentMultipartBinding=true") .process(multiPartProcessor); - from("servlet://multipart?servletName=my-named-servlet&attachmentMultipartBinding=true") + from("servlet://multipart?servletName=multipart-servlet&attachmentMultipartBinding=true") .process(multiPartProcessor); - } + from("servlet://eager-init?servletName=eager-init-servlet&matchOnUriPrefix=true") + .setHeader("servletName").constant("eager-init-servlet") + .process(servletConfigInfoProcessor); + + from("servlet://async?servletName=async-servlet&matchOnUriPrefix=true") + .setHeader("servletName").constant("async-servlet") + .process(servletConfigInfoProcessor); + + from("servlet://force-await?servletName=sync-async-servlet&matchOnUriPrefix=true") + .setHeader("servletName").constant("sync-async-servlet") + .process(servletConfigInfoProcessor); + + from("servlet://execute?servletName=custom-executor-servlet&matchOnUriPrefix=true") + .setHeader("servletName").constant("custom-executor-servlet") + .process(servletConfigInfoProcessor); + } } diff --git a/integration-tests/servlet/src/main/java/org/apache/camel/quarkus/component/servlet/ServletProducers.java b/integration-tests/servlet/src/main/java/org/apache/camel/quarkus/component/servlet/ServletProducers.java new file mode 100644 index 0000000000..292be43a1e --- /dev/null +++ b/integration-tests/servlet/src/main/java/org/apache/camel/quarkus/component/servlet/ServletProducers.java @@ -0,0 +1,71 @@ +/* + * Licensed to the Apache Software Foundation (ASF) under one or more + * contributor license agreements. See the NOTICE file distributed with + * this work for additional information regarding copyright ownership. + * The ASF licenses this file to You under the Apache License, Version 2.0 + * (the "License"); you may not use this file except in compliance with + * the License. You may obtain a copy of the License at + * + * http://www.apache.org/licenses/LICENSE-2.0 + * + * Unless required by applicable law or agreed to in writing, software + * distributed under the License is distributed on an "AS IS" BASIS, + * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied. + * See the License for the specific language governing permissions and + * limitations under the License. + */ +package org.apache.camel.quarkus.component.servlet; + +import java.util.concurrent.Executor; +import java.util.concurrent.Executors; + +import io.undertow.servlet.api.Deployment; +import io.undertow.servlet.api.ServletInfo; +import io.undertow.servlet.core.ManagedServlet; +import io.undertow.servlet.core.ManagedServlets; +import io.undertow.servlet.spec.ServletContextImpl; +import io.vertx.core.json.JsonObject; +import jakarta.enterprise.context.ApplicationScoped; +import jakarta.inject.Named; +import jakarta.inject.Singleton; +import jakarta.servlet.ServletContext; +import jakarta.servlet.http.HttpServletRequest; +import org.apache.camel.Exchange; +import org.apache.camel.Message; +import org.apache.camel.Processor; +import org.apache.camel.spi.MimeType; + +import static org.apache.camel.quarkus.servlet.runtime.CamelServletConfig.ServletConfig.DEFAULT_SERVLET_NAME; + +@ApplicationScoped +public class ServletProducers { + @Singleton + @Named("customServletExecutor") + public Executor customServletExecutor() { + return Executors.newSingleThreadExecutor(r -> new Thread(r, "custom-executor")); + } + + @Singleton + @Named("servletConfigInfoProcessor") + public Processor servletConfigInfoProcessor() { + return exchange -> { + JsonObject json = new JsonObject(); + Message message = exchange.getMessage(); + HttpServletRequest request = message.getHeader(Exchange.HTTP_SERVLET_REQUEST, HttpServletRequest.class); + String servletName = message.getHeader("servletName", DEFAULT_SERVLET_NAME, String.class); + ServletContext servletContext = request.getServletContext(); + Deployment deployment = ((ServletContextImpl) servletContext).getDeployment(); + ManagedServlets servlets = deployment.getServlets(); + ManagedServlet servlet = servlets.getManagedServlet(servletName); + ServletInfo servletInfo = servlet.getServletInfo(); + + json.put("isAsync", request.isAsyncSupported()); + json.put("threadName", Thread.currentThread().getName()); + json.put("loadOnStartup", servletInfo.getLoadOnStartup()); + json.put("initParams", servletInfo.getInitParams()); + + message.setHeader(Exchange.CONTENT_TYPE, MimeType.JSON.type()); + message.setBody(json.encode()); + }; + } +} diff --git a/integration-tests/servlet/src/main/resources/application.properties b/integration-tests/servlet/src/main/resources/application.properties index 0251262894..06920af73f 100644 --- a/integration-tests/servlet/src/main/resources/application.properties +++ b/integration-tests/servlet/src/main/resources/application.properties @@ -15,17 +15,39 @@ ## limitations under the License. ## --------------------------------------------------------------------------- -# -# Quarkus :: Camel :: Servlet -# -quarkus.camel.servlet.url-patterns=/folder-1/*,/folder-2/* - -quarkus.camel.servlet.my-named-servlet.url-patterns=/my-named-folder/* -quarkus.camel.servlet.my-named-servlet.servlet-class=org.apache.camel.quarkus.component.servlet.CustomServlet -quarkus.camel.servlet.my-named-servlet.multipart.location=${java.io.tmpdir}/my-named-servlet -quarkus.camel.servlet.my-named-servlet.multipart.max-file-size=11 -quarkus.camel.servlet.my-named-servlet.multipart.max-request-size=11 -quarkus.camel.servlet.my-named-servlet.multipart.file-size-threshold=5 - -quarkus.camel.servlet.ignored-key.servlet-name=my-favorite-servlet -quarkus.camel.servlet.ignored-key.url-patterns=/my-favorite-folder/* +# Default servlet configuration +quarkus.camel.servlet.url-patterns=/folder-1/*,/folder-2/*,/debug/* + +# Explicit definition of the servlet name +quarkus.camel.servlet.ignored-key.servlet-name=my-named-servlet +quarkus.camel.servlet.ignored-key.url-patterns=/my-named-folder/* + +# Custom servlet class +quarkus.camel.servlet.custom-servlet.url-patterns=/my-custom-folder/* +quarkus.camel.servlet.custom-servlet.servlet-class=org.apache.camel.quarkus.component.servlet.CustomServlet + +# Servlet configured with multipart support +quarkus.camel.servlet.multipart-servlet.url-patterns=/multipart-servlet/* +quarkus.camel.servlet.multipart-servlet.multipart.location=${java.io.tmpdir}/my-named-servlet +quarkus.camel.servlet.multipart-servlet.multipart.max-file-size=11 +quarkus.camel.servlet.multipart-servlet.multipart.max-request-size=11 +quarkus.camel.servlet.multipart-servlet.multipart.file-size-threshold=5 + +# Servlet configured for eager initialization +quarkus.camel.servlet.eager-init-servlet.url-patterns=/eager-init-servlet/* +quarkus.camel.servlet.eager-init-servlet.load-on-startup=1 + +# Servlet configured for async processing +quarkus.camel.servlet.async-servlet.url-patterns=/async-servlet/* +quarkus.camel.servlet.async-servlet.async=true + +# Servlet configured for async + sync processing +quarkus.camel.servlet.sync-async-servlet.url-patterns=/sync-async-servlet/* +quarkus.camel.servlet.sync-async-servlet.async=true +quarkus.camel.servlet.sync-async-servlet.force-await=true + +# Servlet with custom executor +quarkus.camel.servlet.custom-executor-servlet.url-patterns=/custom-executor/* +quarkus.camel.servlet.custom-executor-servlet.async=true +quarkus.camel.servlet.custom-executor-servlet.executor-ref=customServletExecutor + diff --git a/integration-tests/servlet/src/test/java/org/apache/camel/quarkus/component/servlet/CamelServletTest.java b/integration-tests/servlet/src/test/java/org/apache/camel/quarkus/component/servlet/CamelServletTest.java index 593e56cb9b..73bdd78dbe 100644 --- a/integration-tests/servlet/src/test/java/org/apache/camel/quarkus/component/servlet/CamelServletTest.java +++ b/integration-tests/servlet/src/test/java/org/apache/camel/quarkus/component/servlet/CamelServletTest.java @@ -20,36 +20,52 @@ import java.nio.charset.StandardCharsets; import io.quarkus.test.junit.QuarkusTest; import io.restassured.RestAssured; -import org.hamcrest.core.IsEqual; import org.junit.jupiter.api.Test; +import static org.hamcrest.Matchers.anEmptyMap; import static org.hamcrest.Matchers.is; +import static org.hamcrest.Matchers.nullValue; import static org.hamcrest.Matchers.oneOf; +import static org.hamcrest.Matchers.startsWith; +import static org.hamcrest.core.IsEqual.equalTo; @QuarkusTest public class CamelServletTest { + @Test + public void defaultConfiguration() { + RestAssured.get("/debug/configuration") + .then() + .body( + "isAsync", equalTo(false), + "threadName", startsWith("executor-thread"), + "initParams", anEmptyMap(), + "loadOnStartup", nullValue()); + } @Test public void multiplePaths() { - RestAssured.when().get("/folder-1/rest-get").then().body(IsEqual.equalTo("GET: /rest-get")); - RestAssured.when().get("/folder-2/rest-get").then().body(IsEqual.equalTo("GET: /rest-get")); - RestAssured.when().post("/folder-1/rest-post").then().body(IsEqual.equalTo("POST: /rest-post")); - RestAssured.when().post("/folder-2/rest-post").then().body(IsEqual.equalTo("POST: /rest-post")); - RestAssured.when().get("/folder-1/hello").then().body(IsEqual.equalTo("GET: /hello")); - RestAssured.when().get("/folder-2/hello").then().body(IsEqual.equalTo("GET: /hello")); + RestAssured.get("/folder-1/rest-get").then().body(equalTo("GET: /rest-get")); + RestAssured.get("/folder-2/rest-get").then().body(equalTo("GET: /rest-get")); + RestAssured.post("/folder-1/rest-post").then().body(equalTo("POST: /rest-post")); + RestAssured.post("/folder-2/rest-post").then().body(equalTo("POST: /rest-post")); + RestAssured.get("/folder-1/hello").then().body(equalTo("GET: /hello")); + RestAssured.get("/folder-2/hello").then().body(equalTo("GET: /hello")); } @Test - public void namedWithservletClass() { - RestAssured.when().get("/my-named-folder/custom").then() - .body(IsEqual.equalTo("GET: /custom")) - .and().header("x-servlet-class-name", CustomServlet.class.getName()); + public void namedWithServletClass() { + RestAssured.get("/my-custom-folder/custom") + .then() + .body(equalTo("GET: /custom")) + .and() + .header("x-servlet-class-name", CustomServlet.class.getName()); } @Test public void ignoredKey() { - RestAssured.when().get("/my-favorite-folder/favorite").then() - .body(IsEqual.equalTo("GET: /favorite")); + RestAssured.get("/my-named-folder/named") + .then() + .body(equalTo("GET: /my-named-servlet")); } @Test @@ -76,9 +92,54 @@ public class CamelServletTest { // Request body exceeding the limits defined on the multipart config RestAssured.given() .multiPart("test-multipart", "file", body.repeat(10).getBytes(StandardCharsets.UTF_8)) - .post("/my-named-folder/multipart") + .post("/multipart-servlet/multipart") .then() // TODO: Expect 413 only - https://github.com/apache/camel-quarkus/issues/5830 .statusCode(oneOf(413, 500)); } + + @Test + public void eagerInitServlet() { + RestAssured.get("/eager-init-servlet/eager-init") + .then() + .body( + "isAsync", equalTo(false), + "threadName", startsWith("executor-thread"), + "initParams", anEmptyMap(), + "loadOnStartup", equalTo(1)); + } + + @Test + public void asyncServlet() { + RestAssured.get("/async-servlet/async") + .then() + .body( + "isAsync", equalTo(true), + "threadName", startsWith("executor-thread"), + "initParams.async", equalTo("true"), + "loadOnStartup", nullValue()); + } + + @Test + public void asyncWithForceAwaitServlet() { + RestAssured.get("/sync-async-servlet/force-await") + .then() + .body( + "isAsync", equalTo(true), + "threadName", startsWith("executor-thread"), + "initParams.async", equalTo("true"), + "initParams.forceAwait", equalTo("true"), + "loadOnStartup", nullValue()); + } + + @Test + public void asyncWithCustomExecutor() { + RestAssured.get("/custom-executor/execute/get") + .then() + .body( + "isAsync", equalTo(true), + "threadName", equalTo("custom-executor"), + "initParams.async", equalTo("true"), + "loadOnStartup", nullValue()); + } }