Skip to content

Commit

Permalink
CAMEL-19182: camel-platform-http-starter - Async processing with Spri…
Browse files Browse the repository at this point in the history
…ng Boot

- Spring MVC automatically detects CompletableFuture return type of the handler method and executes asynchronously; @responsebody on the handler method to disable attempts to resolve mvc view from a response.
- testing async execution with MockMvc
- @EnableAutoConfiguration instead of @SpringBootApplication on test classes to not scan another tests' configurations
  • Loading branch information
kulagaIA committed Aug 20, 2024
1 parent b11a969 commit 43043e7
Show file tree
Hide file tree
Showing 7 changed files with 129 additions and 33 deletions.
Original file line number Diff line number Diff line change
Expand Up @@ -42,13 +42,14 @@
"matchOnUriPrefix": { "index": 9, "kind": "parameter", "displayName": "Match On Uri Prefix", "group": "consumer", "label": "consumer", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether or not the consumer should try to find a target consumer by matching the URI prefix if no exact match is found." },
"muteException": { "index": 10, "kind": "parameter", "displayName": "Mute Exception", "group": "consumer", "label": "consumer", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": true, "description": "If enabled and an Exchange failed processing on the consumer side the response's body won't contain the exception's stack trace." },
"produces": { "index": 11, "kind": "parameter", "displayName": "Produces", "group": "consumer", "label": "consumer", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "The content type this endpoint produces, such as application\/xml or application\/json." },
"useCookieHandler": { "index": 12, "kind": "parameter", "displayName": "Use Cookie Handler", "group": "consumer", "label": "advanced,consumer", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether to enable the Cookie Handler that allows Cookie addition, expiry, and retrieval (currently only supported by camel-platform-http-vertx)" },
"useStreaming": { "index": 13, "kind": "parameter", "displayName": "Use Streaming", "group": "consumer", "label": "advanced,consumer", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether to use streaming for large requests and responses (currently only supported by camel-platform-http-vertx)" },
"bridgeErrorHandler": { "index": 14, "kind": "parameter", "displayName": "Bridge Error Handler", "group": "consumer (advanced)", "label": "consumer,advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Allows for bridging the consumer to the Camel routing Error Handler, which mean any exceptions (if possible) occurred while the Camel consumer is trying to pickup incoming messages, or the likes, will now be processed as a message and handled by the routing Error Handler. Important: This is only possible if the 3rd party component allows Camel to be alerted if an exception was thrown. Some components handle this internally only, and therefore bridgeErrorHandler is not possible. In other situations we may improve the Camel component to hook into the 3rd party component and make this possible for future releases. By default the consumer will use the org.apache.camel.spi.ExceptionHandler to deal with exceptions, that will be logged at WARN or ERROR level and ignored." },
"exceptionHandler": { "index": 15, "kind": "parameter", "displayName": "Exception Handler", "group": "consumer (advanced)", "label": "consumer,advanced", "required": false, "type": "object", "javaType": "org.apache.camel.spi.ExceptionHandler", "optionalPrefix": "consumer.", "deprecated": false, "autowired": false, "secret": false, "description": "To let the consumer use a custom ExceptionHandler. Notice if the option bridgeErrorHandler is enabled then this option is not in use. By default the consumer will deal with exceptions, that will be logged at WARN or ERROR level and ignored." },
"exchangePattern": { "index": 16, "kind": "parameter", "displayName": "Exchange Pattern", "group": "consumer (advanced)", "label": "consumer,advanced", "required": false, "type": "object", "javaType": "org.apache.camel.ExchangePattern", "enum": [ "InOnly", "InOut" ], "deprecated": false, "autowired": false, "secret": false, "description": "Sets the exchange pattern when the consumer creates an exchange." },
"fileNameExtWhitelist": { "index": 17, "kind": "parameter", "displayName": "File Name Ext Whitelist", "group": "consumer (advanced)", "label": "consumer,advanced", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "A comma or whitespace separated list of file extensions. Uploads having these extensions will be stored locally. Null value or asterisk () will allow all files." },
"headerFilterStrategy": { "index": 18, "kind": "parameter", "displayName": "Header Filter Strategy", "group": "advanced", "label": "advanced", "required": false, "type": "object", "javaType": "org.apache.camel.spi.HeaderFilterStrategy", "deprecated": false, "autowired": false, "secret": false, "description": "To use a custom HeaderFilterStrategy to filter headers to and from Camel message." },
"platformHttpEngine": { "index": 19, "kind": "parameter", "displayName": "Platform Http Engine", "group": "advanced", "label": "advanced", "required": false, "type": "object", "javaType": "org.apache.camel.component.platform.http.spi.PlatformHttpEngine", "deprecated": false, "autowired": false, "secret": false, "description": "An HTTP Server engine implementation to serve the requests of this endpoint." }
"returnHttpRequestHeaders": { "index": 12, "kind": "parameter", "displayName": "Return Http Request Headers", "group": "consumer", "label": "advanced,consumer", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether to include HTTP request headers (Accept, User-Agent, etc.) into HTTP response produced by this endpoint." },
"useCookieHandler": { "index": 13, "kind": "parameter", "displayName": "Use Cookie Handler", "group": "consumer", "label": "advanced,consumer", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether to enable the Cookie Handler that allows Cookie addition, expiry, and retrieval (currently only supported by camel-platform-http-vertx)" },
"useStreaming": { "index": 14, "kind": "parameter", "displayName": "Use Streaming", "group": "consumer", "label": "advanced,consumer", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Whether to use streaming for large requests and responses (currently only supported by camel-platform-http-vertx)" },
"bridgeErrorHandler": { "index": 15, "kind": "parameter", "displayName": "Bridge Error Handler", "group": "consumer (advanced)", "label": "consumer,advanced", "required": false, "type": "boolean", "javaType": "boolean", "deprecated": false, "autowired": false, "secret": false, "defaultValue": false, "description": "Allows for bridging the consumer to the Camel routing Error Handler, which mean any exceptions (if possible) occurred while the Camel consumer is trying to pickup incoming messages, or the likes, will now be processed as a message and handled by the routing Error Handler. Important: This is only possible if the 3rd party component allows Camel to be alerted if an exception was thrown. Some components handle this internally only, and therefore bridgeErrorHandler is not possible. In other situations we may improve the Camel component to hook into the 3rd party component and make this possible for future releases. By default the consumer will use the org.apache.camel.spi.ExceptionHandler to deal with exceptions, that will be logged at WARN or ERROR level and ignored." },
"exceptionHandler": { "index": 16, "kind": "parameter", "displayName": "Exception Handler", "group": "consumer (advanced)", "label": "consumer,advanced", "required": false, "type": "object", "javaType": "org.apache.camel.spi.ExceptionHandler", "optionalPrefix": "consumer.", "deprecated": false, "autowired": false, "secret": false, "description": "To let the consumer use a custom ExceptionHandler. Notice if the option bridgeErrorHandler is enabled then this option is not in use. By default the consumer will deal with exceptions, that will be logged at WARN or ERROR level and ignored." },
"exchangePattern": { "index": 17, "kind": "parameter", "displayName": "Exchange Pattern", "group": "consumer (advanced)", "label": "consumer,advanced", "required": false, "type": "object", "javaType": "org.apache.camel.ExchangePattern", "enum": [ "InOnly", "InOut" ], "deprecated": false, "autowired": false, "secret": false, "description": "Sets the exchange pattern when the consumer creates an exchange." },
"fileNameExtWhitelist": { "index": 18, "kind": "parameter", "displayName": "File Name Ext Whitelist", "group": "consumer (advanced)", "label": "consumer,advanced", "required": false, "type": "string", "javaType": "java.lang.String", "deprecated": false, "autowired": false, "secret": false, "description": "A comma or whitespace separated list of file extensions. Uploads having these extensions will be stored locally. Null value or asterisk () will allow all files." },
"headerFilterStrategy": { "index": 19, "kind": "parameter", "displayName": "Header Filter Strategy", "group": "advanced", "label": "advanced", "required": false, "type": "object", "javaType": "org.apache.camel.spi.HeaderFilterStrategy", "deprecated": false, "autowired": false, "secret": false, "description": "To use a custom HeaderFilterStrategy to filter headers to and from Camel message." },
"platformHttpEngine": { "index": 20, "kind": "parameter", "displayName": "Platform Http Engine", "group": "advanced", "label": "advanced", "required": false, "type": "object", "javaType": "org.apache.camel.component.platform.http.spi.PlatformHttpEngine", "deprecated": false, "autowired": false, "secret": false, "description": "An HTTP Server engine implementation to serve the requests of this endpoint." }
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,6 +19,8 @@
import jakarta.servlet.ServletException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import java.io.IOException;
import java.util.concurrent.CompletableFuture;
import org.apache.camel.Exchange;
import org.apache.camel.ExchangePattern;
import org.apache.camel.Processor;
Expand All @@ -31,8 +33,7 @@
import org.apache.camel.support.DefaultConsumer;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;

import java.io.IOException;
import org.springframework.web.bind.annotation.ResponseBody;

public class SpringBootPlatformHttpConsumer extends DefaultConsumer implements PlatformHttpConsumer, Suspendable, SuspendableService {

Expand All @@ -56,21 +57,24 @@ public PlatformHttpEndpoint getEndpoint() {
/**
* This method is invoked by Spring Boot when invoking Camel via platform-http
*/
public void service(HttpServletRequest request, HttpServletResponse response) {
LOG.trace("Service: {}", request);
try {
handleService(request, response);
} catch (Exception e) {
// do not leak exception back to caller
LOG.warn("Error handling request due to: {}", e.getMessage(), e);
@ResponseBody
public CompletableFuture<Void> service(HttpServletRequest request, HttpServletResponse response) {
return CompletableFuture.runAsync(() -> {
LOG.trace("Service: {}", request);
try {
if (!response.isCommitted()) {
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
handleService(request, response);
} catch (Exception e) {
// do not leak exception back to caller
LOG.warn("Error handling request due to: {}", e.getMessage(), e);
try {
if (!response.isCommitted()) {
response.sendError(HttpServletResponse.SC_INTERNAL_SERVER_ERROR);
}
} catch (Exception e1) {
// ignore
}
} catch (Exception e1) {
// ignore
}
}
});
}

protected void handleService(HttpServletRequest request, HttpServletResponse response) throws Exception {
Expand All @@ -93,8 +97,6 @@ protected void handleService(HttpServletRequest request, HttpServletResponse res
exchange.getIn().setHeader(Exchange.HTTP_PATH, httpPath.substring(contextPath.length()));
}

// TODO: async with CompletionStage returned to spring boot?

// we want to handle the UoW
try {
createUoW(exchange);
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,93 @@
/*
* 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.component.platform.http.springboot;

import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.spring.boot.CamelAutoConfiguration;
import org.apache.camel.test.spring.junit5.CamelSpringBootTest;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.autoconfigure.web.servlet.AutoConfigureMockMvc;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.MvcResult;

import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.asyncDispatch;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.get;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.content;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.request;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;

@EnableAutoConfiguration
@DirtiesContext
@CamelSpringBootTest
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = { CamelAutoConfiguration.class,
SpringBootPlatformHttpRestDSLTest.class, PlatformHttpAsyncRequestHandlingTest.TestConfiguration.class,
PlatformHttpComponentAutoConfiguration.class, SpringBootPlatformHttpAutoConfiguration.class, })
@AutoConfigureMockMvc
public class PlatformHttpAsyncRequestHandlingTest {

@Autowired
private MockMvc mockMvc;

@Test
public void testGet() throws Exception {
MvcResult mvcResult = mockMvc.perform(get("/myget"))
.andExpect(status().isOk())
.andExpect(request().asyncStarted())
.andReturn();

mockMvc.perform(asyncDispatch(mvcResult))
.andExpect(status().isOk())
.andExpect(content().string("get"));
}

@Test
public void testPost() throws Exception {
MvcResult mvcResult = mockMvc.perform(post("/mypost").content("hello"))
.andExpect(status().isOk())
.andExpect(request().asyncStarted())
.andReturn();

mockMvc.perform(asyncDispatch(mvcResult))
.andExpect(status().isOk())
.andExpect(content().string("HELLO"));
}

// *************************************
// Config
// *************************************
@Configuration
public static class TestConfiguration {

@Bean
public RouteBuilder servletPlatformHttpRouteBuilder() {
return new RouteBuilder() {
@Override
public void configure() throws Exception {
from("platform-http:/myget").setBody().constant("get");
from("platform-http:/mypost").transform().body(String.class, b -> b.toUpperCase());
}
};
}
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -16,9 +16,8 @@
*/
package org.apache.camel.component.platform.http.springboot;

import org.junit.jupiter.api.Test;

import org.assertj.core.api.Assertions;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.web.client.TestRestTemplate;

Expand All @@ -33,7 +32,8 @@ public void testGet() {
}

@Test
public void testPost() {
public void testPost() throws InterruptedException {
Thread.sleep(500); //wait until http server is up
Assertions.assertThat(restTemplate.postForEntity("/mypost", "test", String.class).getBody()).isEqualTo("TEST");
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,13 @@
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.spring.boot.CamelAutoConfiguration;
import org.apache.camel.test.spring.junit5.CamelSpringBootTest;

import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.annotation.DirtiesContext;

@SpringBootApplication
@EnableAutoConfiguration
@DirtiesContext
@CamelSpringBootTest
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = { CamelAutoConfiguration.class,
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -19,14 +19,13 @@
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.spring.boot.CamelAutoConfiguration;
import org.apache.camel.test.spring.junit5.CamelSpringBootTest;

import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.annotation.DirtiesContext;

@SpringBootApplication
@EnableAutoConfiguration
@DirtiesContext
@CamelSpringBootTest
@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT, classes = { CamelAutoConfiguration.class,
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,2 @@
logging.level.org.springframework.web: DEBUG
logging.level.org.springframework.boot.web: DEBUG

0 comments on commit 43043e7

Please sign in to comment.