Add Java DSL for Camel module and docs (#3937)

* Add Java DSL for Camel module and docs

* Introduce a `LambdaRouteBuilder` option into the `CamelMessageHandler`
to easily provide the Camel route just in-place

* * Fix language in docs

Co-authored-by: Gary Russell <grussell@vmware.com>

Co-authored-by: Gary Russell <grussell@vmware.com>
This commit is contained in:
Artem Bilan
2022-11-03 12:18:35 -04:00
committed by GitHub
parent 978724e212
commit 0b9bab313b
12 changed files with 513 additions and 3 deletions

View File

@@ -496,9 +496,12 @@ project('spring-integration-camel') {
dependencies {
api project(':spring-integration-core')
api 'org.apache.camel:camel-api'
api 'org.apache.camel:camel-core-model'
testImplementation 'org.apache.camel:camel-test-junit5'
testImplementation ('org.apache.camel:camel-spring') {
exclude group: 'org.springframework'
}
}
}

View File

@@ -0,0 +1,93 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed 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
*
* https://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.springframework.integration.camel.dsl;
import org.apache.camel.ExchangePattern;
import org.apache.camel.ProducerTemplate;
import org.apache.camel.builder.LambdaRouteBuilder;
import org.springframework.lang.Nullable;
/**
* Factory class for Apache Camel components DSL.
*
* @author Artem Bilan
*
* @since 6.0
*/
public final class Camel {
/**
* Create an instance of {@link CamelMessageHandlerSpec} in a {@link ExchangePattern#InOnly} mode.
* @return the spec.
*/
public static CamelMessageHandlerSpec handler() {
return camelHandler(null, ExchangePattern.InOnly);
}
/**
* Create an instance of {@link CamelMessageHandlerSpec} for the provided {@link ProducerTemplate}
* in a {@link ExchangePattern#InOnly} mode.
* @param producerTemplate the {@link ProducerTemplate} to use.
* @return the spec.
*/
public static CamelMessageHandlerSpec handler(ProducerTemplate producerTemplate) {
return camelHandler(producerTemplate, ExchangePattern.InOnly);
}
/**
* Create an instance of {@link CamelMessageHandlerSpec} in a {@link ExchangePattern#InOut} mode.
* @return the spec.
*/
public static CamelMessageHandlerSpec gateway() {
return camelHandler(null, ExchangePattern.InOut);
}
/**
* Create an instance of {@link CamelMessageHandlerSpec} for the provided {@link ProducerTemplate}
* in a {@link ExchangePattern#InOut} mode.
* @param producerTemplate the {@link ProducerTemplate} to use.
* @return the spec.
*/
public static CamelMessageHandlerSpec gateway(ProducerTemplate producerTemplate) {
return camelHandler(producerTemplate, ExchangePattern.InOut);
}
/**
* Create an instance of {@link CamelMessageHandlerSpec} for the provided {@link LambdaRouteBuilder}
* in a {@link ExchangePattern#InOut} mode.
* The {@code CamelContext} is fetched as a bean from the application context.
* @param route the {@link LambdaRouteBuilder} to use.
* @return the spec.
*/
public static CamelMessageHandlerSpec route(LambdaRouteBuilder route) {
return camelHandler(null, ExchangePattern.InOut)
.route(route);
}
private static CamelMessageHandlerSpec camelHandler(@Nullable ProducerTemplate producerTemplate,
ExchangePattern exchangePattern) {
return new CamelMessageHandlerSpec(producerTemplate)
.exchangePattern(exchangePattern);
}
private Camel() {
}
}

View File

@@ -0,0 +1,134 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed 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
*
* https://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.springframework.integration.camel.dsl;
import java.util.Arrays;
import java.util.Map;
import java.util.function.Function;
import org.apache.camel.ExchangePattern;
import org.apache.camel.ProducerTemplate;
import org.apache.camel.builder.LambdaRouteBuilder;
import org.springframework.expression.Expression;
import org.springframework.integration.camel.outbound.CamelMessageHandler;
import org.springframework.integration.camel.support.CamelHeaderMapper;
import org.springframework.integration.dsl.MessageHandlerSpec;
import org.springframework.integration.expression.FunctionExpression;
import org.springframework.integration.mapping.HeaderMapper;
import org.springframework.lang.Nullable;
import org.springframework.messaging.Message;
import org.springframework.util.Assert;
/**
* The {@link MessageHandlerSpec} for {@link CamelMessageHandler}.
*
* @author Artem Bilan
*
* @since 6.0
*/
public class CamelMessageHandlerSpec extends
MessageHandlerSpec<CamelMessageHandlerSpec, CamelMessageHandler> {
private String[] inboundHeaderNames = { "*" };
private String[] outboundHeaderNames = { "*" };
protected CamelMessageHandlerSpec(@Nullable ProducerTemplate producerTemplate) {
this.target = producerTemplate == null ? new CamelMessageHandler() : new CamelMessageHandler(producerTemplate);
}
public CamelMessageHandlerSpec endpointUri(String endpointUri) {
this.target.setEndpointUri(endpointUri);
return this;
}
public CamelMessageHandlerSpec endpointUri(Function<Message<?>, String> endpointUriFunction) {
return endpointUriExpression(new FunctionExpression<>(endpointUriFunction));
}
public CamelMessageHandlerSpec endpointUriExpression(String endpointUriExpression) {
return endpointUriExpression(PARSER.parseExpression(endpointUriExpression));
}
public CamelMessageHandlerSpec endpointUriExpression(Expression endpointUriExpression) {
this.target.setEndpointUriExpression(endpointUriExpression);
return this;
}
protected CamelMessageHandlerSpec route(LambdaRouteBuilder route) {
this.target.setRoute(route);
return this;
}
public CamelMessageHandlerSpec exchangePattern(ExchangePattern exchangePattern) {
this.target.setExchangePattern(exchangePattern);
return this;
}
public CamelMessageHandlerSpec exchangePattern(Function<Message<?>, ExchangePattern> exchangePatternFunction) {
return exchangePatternExpression(new FunctionExpression<>(exchangePatternFunction));
}
public CamelMessageHandlerSpec exchangePatternExpression(String exchangePatternExpression) {
return exchangePatternExpression(PARSER.parseExpression(exchangePatternExpression));
}
public CamelMessageHandlerSpec exchangePatternExpression(Expression exchangePatternExpression) {
this.target.setExchangePatternExpression(exchangePatternExpression);
return this;
}
public CamelMessageHandlerSpec inboundHeaderNames(String... inboundHeaderNames) {
Assert.notEmpty(inboundHeaderNames, "'inboundHeaderNames' must not be empty");
this.inboundHeaderNames = Arrays.copyOf(inboundHeaderNames, inboundHeaderNames.length);
return addCamelHeaderMapper();
}
public CamelMessageHandlerSpec outboundHeaderNames(String... outboundHeaderNames) {
Assert.notEmpty(outboundHeaderNames, "'outboundHeaderNames' must not be empty");
this.outboundHeaderNames = Arrays.copyOf(outboundHeaderNames, outboundHeaderNames.length);
return addCamelHeaderMapper();
}
private CamelMessageHandlerSpec addCamelHeaderMapper() {
CamelHeaderMapper headerMapper = new CamelHeaderMapper();
headerMapper.setInboundHeaderNames(this.inboundHeaderNames);
headerMapper.setOutboundHeaderNames(this.outboundHeaderNames);
return headerMapper(headerMapper);
}
public CamelMessageHandlerSpec headerMapper(HeaderMapper<org.apache.camel.Message> headerMapper) {
this.target.setHeaderMapper(headerMapper);
return this;
}
public CamelMessageHandlerSpec exchangeProperties(Map<String, Object> exchangeProperties) {
this.target.setExchangeProperties(exchangeProperties);
return this;
}
public CamelMessageHandlerSpec exchangePropertiesExpression(String exchangePropertiesExpression) {
return exchangePropertiesExpression(PARSER.parseExpression(exchangePropertiesExpression));
}
public CamelMessageHandlerSpec exchangePropertiesExpression(Expression exchangePropertiesExpression) {
this.target.setExchangePropertiesExpression(exchangePropertiesExpression);
return this;
}
}

View File

@@ -0,0 +1,7 @@
/**
* Provides supporting classes for JavaDSL with Apache Camel components.
*/
@org.springframework.lang.NonNullApi
@org.springframework.lang.NonNullFields
package org.springframework.integration.camel.dsl;

View File

@@ -19,12 +19,18 @@ package org.springframework.integration.camel.outbound;
import java.util.Map;
import java.util.concurrent.CompletableFuture;
import org.apache.camel.CamelContext;
import org.apache.camel.CamelExecutionException;
import org.apache.camel.Endpoint;
import org.apache.camel.Exchange;
import org.apache.camel.ExchangePattern;
import org.apache.camel.ProducerTemplate;
import org.apache.camel.builder.LambdaRouteBuilder;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.model.RouteDefinition;
import org.springframework.beans.factory.BeanFactory;
import org.springframework.beans.factory.BeanInitializationException;
import org.springframework.expression.Expression;
import org.springframework.expression.common.LiteralExpression;
import org.springframework.expression.spel.support.StandardEvaluationContext;
@@ -62,13 +68,16 @@ import org.springframework.util.StringUtils;
*/
public class CamelMessageHandler extends AbstractReplyProducingMessageHandler {
private final ProducerTemplate producerTemplate;
private ProducerTemplate producerTemplate;
private Expression exchangePatternExpression = new ValueExpression<>(ExchangePattern.InOnly);
@Nullable
private Expression endpointUriExpression;
@Nullable
private LambdaRouteBuilder route;
private HeaderMapper<org.apache.camel.Message> headerMapper = new CamelHeaderMapper();
@Nullable
@@ -76,21 +85,47 @@ public class CamelMessageHandler extends AbstractReplyProducingMessageHandler {
private StandardEvaluationContext evaluationContext;
public CamelMessageHandler() {
}
public CamelMessageHandler(ProducerTemplate producerTemplate) {
Assert.notNull(producerTemplate, "'producerTemplate' must not be null");
this.producerTemplate = producerTemplate;
}
/**
* Set Camel route endpoint uri to send a message.
* Mutually exclusive with {@link #setEndpointUriExpression(Expression)} and {@link #setRoute(LambdaRouteBuilder)}.
* @param endpointUri the Camel route endpoint to send a message.
*/
public void setEndpointUri(String endpointUri) {
Assert.hasText(endpointUri, "'endpointUri' must not be empty");
setEndpointUriExpression(new LiteralExpression(endpointUri));
}
/**
* Set Camel route endpoint uri to send a message.
* Mutually exclusive with {@link #setEndpointUri(String)} and {@link #setRoute(LambdaRouteBuilder)}.
* @param endpointUriExpression the SpEL expression to determine a Camel route endpoint to send a message.
*/
public void setEndpointUriExpression(Expression endpointUriExpression) {
Assert.notNull(endpointUriExpression, "'endpointUriExpression' must not be null");
this.endpointUriExpression = endpointUriExpression;
}
/**
* Set a {@link LambdaRouteBuilder} to add an inline Camel route definition.
* Can be used as a lambda {@code rb -> rb.from("direct:inbound").bean(MyBean.class)}
* or reference to external instance.
* Mutually exclusive with {@link #setEndpointUri(String)} and {@link #setEndpointUriExpression(Expression)}.
* The endpoint to send a message is extracted from the target {@link RouteBuilder}.
* @param route the {@link LambdaRouteBuilder} to use.
*/
public void setRoute(LambdaRouteBuilder route) {
Assert.notNull(route, "'route' must not be null");
this.route = route;
}
public void setExchangePattern(ExchangePattern exchangePattern) {
Assert.notNull(exchangePattern, "'exchangePattern' must not be null");
setExchangePatternExpression(new ValueExpression<>(exchangePattern));
@@ -126,7 +161,38 @@ public class CamelMessageHandler extends AbstractReplyProducingMessageHandler {
@Override
protected final void doInit() {
this.evaluationContext = ExpressionUtils.createStandardEvaluationContext(getBeanFactory());
Assert.state(this.endpointUriExpression == null || this.route == null,
"The 'endpointUri' option is mutually exclusive with 'route'");
BeanFactory beanFactory = getBeanFactory();
if (this.producerTemplate == null) {
this.producerTemplate = beanFactory.getBean(CamelContext.class).createProducerTemplate();
}
if (this.route != null) {
CamelContext camelContext = this.producerTemplate.getCamelContext();
RouteBuilder routeBuilder =
new RouteBuilder(camelContext) {
@Override
public void configure() throws Exception {
CamelMessageHandler.this.route.accept(this);
}
};
try {
camelContext.addRoutes(routeBuilder);
}
catch (Exception ex) {
throw new BeanInitializationException("Cannot load Camel route", ex);
}
RouteDefinition routeDefinition = routeBuilder.getRouteCollection().getRoutes().get(0);
this.endpointUriExpression = new LiteralExpression(routeDefinition.getInput().getEndpointUri());
}
this.evaluationContext = ExpressionUtils.createStandardEvaluationContext(beanFactory);
}
@Override

View File

@@ -0,0 +1,90 @@
/*
* Copyright 2022 the original author or authors.
*
* Licensed 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
*
* https://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.springframework.integration.camel.dsl;
import static org.assertj.core.api.Assertions.assertThat;
import org.apache.camel.CamelContext;
import org.apache.camel.builder.RouteBuilder;
import org.apache.camel.spring.SpringCamelContext;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.event.ContextRefreshedEvent;
import org.springframework.context.event.EventListener;
import org.springframework.integration.config.EnableIntegration;
import org.springframework.integration.core.MessagingTemplate;
import org.springframework.integration.dsl.IntegrationFlow;
import org.springframework.messaging.MessageChannel;
import org.springframework.test.annotation.DirtiesContext;
import org.springframework.test.context.junit.jupiter.SpringJUnitConfig;
/**
* @author Artem Bilan
*
* @since 6.0
*/
@SpringJUnitConfig
@DirtiesContext
public class CamelDslTests {
@Autowired
@Qualifier("camelFlow.input")
MessageChannel input;
@Test
void sendAndReceiveCamelRoute() {
String result = new MessagingTemplate().convertSendAndReceive(this.input, "apache camel", String.class);
assertThat(result).isEqualTo("___APACHE CAMEL___");
}
@Configuration
@EnableIntegration
public static class Config {
@Bean
CamelContext springCamelContext() {
return new SpringCamelContext();
}
@EventListener(ContextRefreshedEvent.class)
public void simpleRoute() throws Exception {
RouteBuilder.addRoutes(springCamelContext(),
rb -> rb.from("direct:simple").bean("camelDslTests.Config", "transformPayload"));
}
@Bean
IntegrationFlow camelFlow() {
return f -> f
.handle(Camel.gateway().endpointUri("direct:simple"))
.handle(Camel.route(this::camelRoute));
}
private void camelRoute(RouteBuilder routeBuilder) {
routeBuilder.from("direct:inbound").transform(routeBuilder.simple("${body.toUpperCase()}"));
}
public String transformPayload(String payload) {
return "___" + payload + "___";
}
}
}

View File

@@ -183,6 +183,7 @@ public class CamelMessageHandlerTests extends CamelTestSupport {
public void configure() {
from("direct:simple").to("mock:result");
}
};
}

View File

@@ -0,0 +1,101 @@
[[camel]]
== Apache Camel Support
Spring Integration provides an API and configuration to communicate with https://camel.apache.org[Apache Camel] endpoints declared in the same application context.
You need to include this dependency into your project:
====
[source, xml, subs="normal", role="primary"]
.Maven
----
<dependency>
<groupId>org.springframework.integration</groupId>
<artifactId>spring-integration-camel</artifactId>
<version>{project-version}</version>
</dependency>
----
[source, groovy, subs="normal", role="secondary"]
.Gradle
----
compile "org.springframework.integration:spring-integration-camel:{project-version}"
----
====
Spring Integration and Apache Camel implement Enterprise Integration Patterns and provide a convenient way to compose them, but the projects use a different approach for their API and abstractions implementation.
Spring Integration fully relies on a dependency injection container from Spring Core.
It uses many other Spring projects (Spring Data, Spring AMQP, Spring for Apache Kafka etc.) for its channel adapter implementations.
It also uses the `MessageChannel` abstraction as a first class citizen of which developers need to be aware of, when composing their integration flows.
Apache Camel, on the other hand, does not provide a first class citizen abstraction of a message channel and proposes to compose its routes via internal exchanges, hidden from the API.
In addition, it requires some extra https://camel.apache.org/components/3.18.x/spring-summary.html[dependencies and configurations] for it to be used in a Spring application.
Even if it doesn't matter for the final enterprise integration solution how its parts are implemented, a developer experience and high productivity are taken into account.
Therefore, developers may choose one framework over another for many reasons, or both if there is a gap in some target systems support.
Spring Integration and Apache Camel applications can interact with each other through many external protocols for which they implement channel adapters.
For example, a Spring Integration flow may publish a record to an Apache Kafka topic which is consumed by an Apache Camel endpoint on the consumer side.
Or, an Apache Camel route may write data into an SFTP file the directory, which is polled by a SFTP Inbound Channel Adapter from Spring Integration.
Or, within the same Spring application context they can communicate via an `ApplicationEvent` https://camel.apache.org/components/3.18.x/spring-event-component.html[abstraction].
To make a development process easier, and to avoid unnecessary network hops, Apache Camel provides a https://camel.apache.org/components/3.18.x/spring-integration-component.html[module] to communicate with Spring Integration via message channels.
All that is needed is a reference to a `MessageChannel` from the application context, to send or consume messages.
This works well when Apache Camel routes are initiators of the message flow and Spring Integration plays only a supporting role as a part of the solution.
For a similar developer experience, Spring Integration now provides a channel adapter to call an Apache Camel endpoint and, optionally, wait for a reply.
There is no inbound channel adapter because subscribing to a `MessageChannel` for consuming Apache Camel messages is enough from the Spring Integration API and abstractions perspective.
[[camel-channel-adapter]]
=== Outbound Channel Adapter for Apache Camel
The `CamelMessageHandler` is an `AbstractReplyProducingMessageHandler` implementation and can work in both one-way (default) and request-reply modes.
It uses an `org.apache.camel.ProducerTemplate` to send (or send and receive) into an `org.apache.camel.Endpoint`.
An interaction mode can be controlled by the `ExchangePattern` option (which can be evaluated at runtime against the request message via a SpEL expression).
The target Apache Camel endpoint can be configured explicitly or as a SpEL expression to be evaluated at runtime.
Otherwise, it falls back to the `defaultEndpoint` provided on the `ProducerTemplate`.
Instead of specifying the endpoint, an in-line, explicit `LambdaRouteBuilder` can be provided, for example to make a call into an Apache Camel component for which there is no channel adapter support in Spring Integration.
In addition, a `HeaderMapper<org.apache.camel.Message>` (the `CamelHeaderMapper` is a default implementation) can be provided, to determine which headers to map between the Spring Integration and Apache Camel messages.
By default, all headers are mapped.
The `CamelMessageHandler` supports an `async` mode calling `ProducerTemplate.asyncSend()` and producing a `CompletableFuture` for reply processing (if any).
The `exchangeProperties` can be customized via a SpEL expression, which must evaluate to a `Map`.
If a `ProducerTemplate` is not provided, it is created via a `CamelContext` bean resolved from the application context.
====
[source, java]
----
@Bean
@ServiceActivator(inputChannel = "sendToCamel")
CamelMessageHandler camelService(ProducerTemplate producerTemplate) {
CamelHeaderMapper headerMapper = new CamelHeaderMapper();
headerMapper.setOutboundHeaderNames("");
headerMapper.setInboundHeaderNames("testHeader");
CamelMessageHandler camelMessageHandler = new CamelMessageHandler(producerTemplate);
camelMessageHandler.setEndpointUri("direct:simple");
camelMessageHandler.setExchangePatternExpression(spelExpressionParser.parseExpression("headers.exchangePattern"));
camelMessageHandler.setHeaderMapper(headerMapper);
return camelMessageHandler;
}
----
====
For Java DSL flow definitions this channel adapter can be configured with a few variants provided by the `Camel` factory:
====
[source, java]
----
@Bean
IntegrationFlow camelFlow() {
return f -> f
.handle(Camel.gateway().endpointUri("direct:simple"))
.handle(Camel.route(this::camelRoute))
.handle(Camel.handler().endpointUri("log:com.mycompany.order?level=WARN"));
}
private void camelRoute(RouteBuilder routeBuilder) {
routeBuilder.from("direct:inbound").transform(routeBuilder.simple("${body.toUpperCase()}"));
}
----
====

View File

@@ -54,6 +54,12 @@ The following table summarizes the various endpoints with quick links to the app
| <<./amqp.adoc#amqp-inbound-gateway,Inbound Gateway>>
| <<./amqp.adoc#amqp-outbound-gateway,Outbound Gateway>>
| *Apache Camel*
| N
| <<./camel.adoc#camel-channel-adapter,Outbound Channel Adapter>>
| N
| <<./camel.adoc#camel-channel-adapter,Outbound Gateway>>
| *Apache Cassandra*
| N
| <<./cassandra.adoc#cassandra-outbound,Outbound Channel Adapter>>

View File

@@ -45,6 +45,8 @@ include::./endpoint-summary.adoc[]
include::./amqp.adoc[]
include::./camel.adoc[]
include::./cassandra.adoc[]
include::./event.adoc[]

View File

@@ -35,6 +35,7 @@ Welcome to the Spring Integration reference documentation!
[horizontal]
<<./endpoint-summary.adoc#spring-integration-endpoints,Integration Endpoint Summary>> :: Protocol-specific channel adapters and gateways summary
<<./amqp.adoc#amqp,AMQP Support>> :: AMQP channels, adapters and gateways
<<./camel.adoc#camel,Apache Camel Support>> :: Apache Camel channel adapters and gateways
<<./cassandra.adoc#cassandra,Apache Cassandra Support>> :: Apache Cassandra channel adapters
<<./event.adoc#applicationevent,Spring `ApplicationEvent` Support>> :: Handling and consuming Spring application events with channel adapters
<<./feed.adoc#feed,Feed Adapter>> :: RSS and Atom channel adapters

View File

@@ -32,6 +32,12 @@ See <<./mqtt.adoc#mqtt-shared-client,Shared MQTT Client Support>> for more infor
The GraphQL support has been added.
See <<./graphql.adoc#graphql,GraphQL Support>> for more information.
[[x6.0-camel]]
==== Apache Camel Support
Support for Apache Camel routes has been introduced.
See <<./camel.adoc#camel,Apache Camel Support>> for more information.
[[x6.0-smb]]
==== SMB Support