INT-3500: Consider to add "error-channel" to the <enricher>

JIRA: https://jira.spring.io/browse/INT-3500

* add 'error-channel' to the <enricher> component
* delegate errorChannel to the internal gateway in ContentEnricher
* added tests for xml based and java based enrichers with error channels

INT-3500: Consider to add "error-channel" to the <enricher>

* Fixed typos and comments
* Reworked xml integration test to use the outputChannel and increased timeout
* Added overview in whats-new doc

INT-3500: Consider to add "error-channel" to the <enricher>

* Ensure requestChannel is set if an errorChannel is set
* Increased timeout to fix unit test
* Added details to content-enricher ref doc

Polishing
This commit is contained in:
Kris Jacyna
2014-09-15 14:46:13 +03:00
committed by Artem Bilan
parent 17b91592fb
commit 30b58836e5
8 changed files with 291 additions and 42 deletions

View File

@@ -41,6 +41,7 @@ import org.springframework.util.xml.DomUtils;
* @author Mark Fisher
* @author Artem Bilan
* @author Liujiong
* @author Kris Jacyna
* @since 2.1
*/
public class EnricherParser extends AbstractConsumerEndpointParser {
@@ -51,6 +52,7 @@ public class EnricherParser extends AbstractConsumerEndpointParser {
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "request-channel");
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "reply-channel");
IntegrationNamespaceUtils.setReferenceIfAttributeDefined(builder, element, "error-channel");
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "request-timeout");
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "reply-timeout");
IntegrationNamespaceUtils.setValueIfAttributeDefined(builder, element, "requires-reply");
@@ -69,19 +71,21 @@ public class EnricherParser extends AbstractConsumerEndpointParser {
boolean hasAttributeExpression = StringUtils.hasText(expression);
boolean hasAttributeNullResultExpression = StringUtils.hasText(nullResultExpression);
if (hasAttributeValue && hasAttributeExpression){
if (hasAttributeValue && hasAttributeExpression) {
parserContext.getReaderContext().error("Only one of 'value' or 'expression' is allowed", element);
}
if (!hasAttributeValue && !hasAttributeExpression && !hasAttributeNullResultExpression){
parserContext.getReaderContext().error("One of 'value' or 'expression' or 'null-result-expression' is required", element);
if (!hasAttributeValue && !hasAttributeExpression && !hasAttributeNullResultExpression) {
parserContext.getReaderContext()
.error("One of 'value' or 'expression' or 'null-result-expression' is required", element);
}
BeanDefinition expressionDef = null;
BeanDefinition nullResultExpressionExpressionDef;
if (hasAttributeValue) {
BeanDefinitionBuilder expressionBuilder = BeanDefinitionBuilder.genericBeanDefinition(ValueExpression.class);
BeanDefinitionBuilder expressionBuilder =
BeanDefinitionBuilder.genericBeanDefinition(ValueExpression.class);
if (StringUtils.hasText(type)) {
expressionBuilder.addConstructorArgValue(new TypedStringValue(value, type));
}
@@ -93,19 +97,20 @@ public class EnricherParser extends AbstractConsumerEndpointParser {
else if (hasAttributeExpression) {
if (StringUtils.hasText(type)) {
parserContext.getReaderContext().error("The 'type' attribute for '<property>' of '<enricher>' " +
"is not allowed with an 'expression' attribute.", element);
"is not allowed with an 'expression' attribute.", element);
}
expressionDef = BeanDefinitionBuilder
.genericBeanDefinition(ExpressionFactoryBean.class)
.addConstructorArgValue(expression)
.getBeanDefinition();
}
if (expressionDef != null){
if (expressionDef != null) {
expressions.put(name, expressionDef);
}
if (hasAttributeNullResultExpression) {
nullResultExpressionExpressionDef = BeanDefinitionBuilder.genericBeanDefinition(ExpressionFactoryBean.class)
.addConstructorArgValue(nullResultExpression).getBeanDefinition();
nullResultExpressionExpressionDef =
BeanDefinitionBuilder.genericBeanDefinition(ExpressionFactoryBean.class)
.addConstructorArgValue(nullResultExpression).getBeanDefinition();
nullResultExpressions.put(name, nullResultExpressionExpressionDef);
}
}
@@ -129,13 +134,14 @@ public class EnricherParser extends AbstractConsumerEndpointParser {
boolean hasAttributeValue = StringUtils.hasText(valueElementValue);
boolean hasAttributeExpression = StringUtils.hasText(expressionElementValue);
boolean hasAttributeNullResultExpression = StringUtils.hasText(nullResultHeaderExpression);
if (hasAttributeValue && hasAttributeExpression){
if (hasAttributeValue && hasAttributeExpression) {
parserContext.getReaderContext().error("Only one of '" + "value" + "' or '"
+ "expression" + "' is allowed", subElement);
+ "expression" + "' is allowed", subElement);
}
if (!hasAttributeValue && !hasAttributeExpression && !hasAttributeNullResultExpression){
parserContext.getReaderContext().error("One of 'value' or 'expression' or 'null-result-expression' is required", subElement);
if (!hasAttributeValue && !hasAttributeExpression && !hasAttributeNullResultExpression) {
parserContext.getReaderContext()
.error("One of 'value' or 'expression' or 'null-result-expression' is required", subElement);
}
BeanDefinition expressionDef = null;
if (hasAttributeValue) {
@@ -143,7 +149,8 @@ public class EnricherParser extends AbstractConsumerEndpointParser {
expressionDef.getConstructorArgumentValues().addGenericArgumentValue(valueElementValue);
}
else if (hasAttributeExpression) {
expressionDef = IntegrationNamespaceUtils.createExpressionDefIfAttributeDefined("expression", subElement);
expressionDef =
IntegrationNamespaceUtils.createExpressionDefIfAttributeDefined("expression", subElement);
}
if (StringUtils.hasText(subElement.getAttribute("expression"))
@@ -167,7 +174,8 @@ public class EnricherParser extends AbstractConsumerEndpointParser {
.genericBeanDefinition(ExpressionEvaluatingHeaderValueMessageProcessor.class)
.addConstructorArgValue(nullResultExpressionDefinition)
.addConstructorArgValue(subElement.getAttribute("type"));
IntegrationNamespaceUtils.setValueIfAttributeDefined(nullResultValueProcessorBuilder, subElement, "overwrite");
IntegrationNamespaceUtils.setValueIfAttributeDefined(nullResultValueProcessorBuilder, subElement,
"overwrite");
nullResultHeaderExpressions.put(name, nullResultValueProcessorBuilder.getBeanDefinition());
}
}
@@ -184,8 +192,9 @@ public class EnricherParser extends AbstractConsumerEndpointParser {
String requestPayloadExpression = element.getAttribute("request-payload-expression");
if (StringUtils.hasText(requestPayloadExpression)) {
BeanDefinitionBuilder expressionBuilder = BeanDefinitionBuilder.genericBeanDefinition(ExpressionFactoryBean.class);
expressionBuilder.addConstructorArgValue(requestPayloadExpression);
BeanDefinitionBuilder expressionBuilder =
BeanDefinitionBuilder.genericBeanDefinition(ExpressionFactoryBean.class)
.addConstructorArgValue(requestPayloadExpression);
builder.addPropertyValue("requestPayloadExpression", expressionBuilder.getBeanDefinition());
}

View File

@@ -49,6 +49,7 @@ import org.springframework.util.ReflectionUtils;
* @author Gary Russell
* @author Artem Bilan
* @author Liujiong
* @author Kris Jacyna
* @since 2.1
*/
public class ContentEnricher extends AbstractReplyProducingMessageHandler
@@ -82,6 +83,10 @@ public class ContentEnricher extends AbstractReplyProducingMessageHandler
private volatile String replyChannelName;
private volatile MessageChannel errorChannel;
private volatile String errorChannelName;
private volatile Gateway gateway = null;
private volatile Long requestTimeout;
@@ -167,6 +172,22 @@ public class ContentEnricher extends AbstractReplyProducingMessageHandler
this.replyChannelName = replyChannelName;
}
/**
* Set the content enricher's error channel to allow the error handling flow to return
* of an alternative object to use for enrichment if exceptions occur in the
* downstream flow.
* @param errorChannel The error channel.
* @since 4.1
*/
public void setErrorChannel(MessageChannel errorChannel) {
this.errorChannel = errorChannel;
}
public void setErrorChannelName(String errorChannelName) {
Assert.hasText(errorChannelName, "'errorChannelName' must not be empty");
this.errorChannelName = errorChannelName;
}
/**
* Set the timeout value for sending request messages. If not explicitly configured,
* the default is one second.
@@ -246,10 +267,17 @@ public class ContentEnricher extends AbstractReplyProducingMessageHandler
Assert.state(!(this.replyChannelName != null && this.replyChannel != null),
"'replyChannelName' and 'replyChannel' are mutually exclusive.");
Assert.state(!(this.errorChannelName != null && this.errorChannel != null),
"'errorChannelName' and 'errorChannel' are mutually exclusive.");
if (this.replyChannel != null || this.replyChannelName != null) {
Assert.state(this.requestChannel != null || this.requestChannelName != null,
"If the replyChannel is set, then the requestChannel must not be null");
}
if (this.errorChannel != null || this.errorChannelName != null) {
Assert.state(this.requestChannel != null || this.requestChannelName != null,
"If the errorChannel is set, then the requestChannel must not be null");
}
if (this.requestChannel != null || this.requestChannelName != null) {
this.gateway = new Gateway();
this.gateway.setRequestChannel(this.requestChannel);
@@ -269,6 +297,11 @@ public class ContentEnricher extends AbstractReplyProducingMessageHandler
this.gateway.setReplyChannelName(this.replyChannelName);
}
this.gateway.setErrorChannel(errorChannel);
if (this.errorChannelName != null) {
this.gateway.setErrorChannelName(this.errorChannelName);
}
if (this.getBeanFactory() != null) {
this.gateway.setBeanFactory(this.getBeanFactory());
}

View File

@@ -1387,6 +1387,21 @@
</xsd:appinfo>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="error-channel" type="xsd:string" use="optional">
<xsd:annotation>
<xsd:documentation>
Used when exceptions occur in a downstream flow and allows the error
handling flow to return an alternative object to use for enrichment.
If no "error-channel" reference is provided,
this enricher will propagate Exceptions to the caller.
</xsd:documentation>
<xsd:appinfo>
<tool:annotation kind="ref">
<tool:expected-type type="org.springframework.messaging.MessageChannel"/>
</tool:annotation>
</xsd:appinfo>
</xsd:annotation>
</xsd:attribute>
<xsd:attribute name="request-timeout" type="xsd:string">
<xsd:annotation>
<xsd:documentation><![CDATA[

View File

@@ -0,0 +1,26 @@
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/integration"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:beans="http://www.springframework.org/schema/beans"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/integration
http://www.springframework.org/schema/integration/spring-integration.xsd">
<channel id="inputChannel"/>
<channel id="requestChannel"/>
<channel id="errChannel"/>
<channel id="outputChannel">
<queue />
</channel>
<enricher id="enricher"
input-channel="inputChannel" request-channel="requestChannel"
output-channel="outputChannel" error-channel="errChannel">
<property name="name" expression="'Mr. ' + payload.name"/>
</enricher>
</beans:beans>

View File

@@ -0,0 +1,97 @@
/*
* Copyright 2014 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
*
* 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.springframework.integration.config.xml;
import static org.junit.Assert.assertEquals;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.ApplicationContext;
import org.springframework.integration.channel.DirectChannel;
import org.springframework.integration.handler.AbstractReplyProducingMessageHandler;
import org.springframework.integration.support.MessageBuilder;
import org.springframework.messaging.Message;
import org.springframework.messaging.PollableChannel;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
/**
* Tests the error-channel in an enricher to produce
* a default object in case of downstream failure.
*
* @author Kris Jacyna
* @since 4.1
*/
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration
public class EnricherParserTests5 {
@Autowired
private ApplicationContext context;
@Test
public void errorChannelTest() {
class ErrorThrower extends AbstractReplyProducingMessageHandler {
@Override
protected Object handleRequestMessage(Message<?> requestMessage) {
throw new RuntimeException();
}
}
class DefaultTargetProducer extends AbstractReplyProducingMessageHandler {
@Override
protected Object handleRequestMessage(Message<?> requestMessage) {
final Target defaultTarget = new Target();
defaultTarget.setName("Default");
return defaultTarget;
}
}
context.getBean("requestChannel", DirectChannel.class).subscribe(new ErrorThrower());
context.getBean("errChannel", DirectChannel.class).subscribe(new DefaultTargetProducer());
Target original = new Target();
original.setName("John");
Message<?> request = MessageBuilder.withPayload(original).build();
context.getBean("inputChannel", DirectChannel.class).send(request);
Message<?> reply = context.getBean("outputChannel", PollableChannel.class).receive(10000);
Target enriched = (Target) reply.getPayload();
assertEquals("Mr. Default", enriched.getName());
}
public static class Target {
private volatile String name;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
}

View File

@@ -57,6 +57,7 @@ import org.springframework.scheduling.support.PeriodicTrigger;
* @author Gunnar Hillert
* @author Artem Bilan
* @author Gary Russell
* @author Kris Jacyna
*
* @since 2.1
*/
@@ -85,9 +86,9 @@ public class ContentEnricherTests {
public void replyChannelReplyTimingOut() throws Exception {
final long requestTimeout = 500L;
final long replyTimeout = 700L;
final long replyTimeout = 700L;
final DirectChannel replyChannel = new DirectChannel();
final DirectChannel replyChannel = new DirectChannel();
final QueueChannel requestChannel = new QueueChannel(1);
final ContentEnricher enricher = new ContentEnricher();
@@ -144,10 +145,11 @@ public class ContentEnricherTests {
Message<?> requestMessage = MessageBuilder.withPayload(target).setReplyChannel(replyChannel).build();
try {
enricher.handleMessage(requestMessage);
enricher.handleMessage(requestMessage);
}
catch (ReplyRequiredException e) {
assertEquals("No reply produced by handler 'Enricher', and its 'requiresReply' property is set to true.", e.getMessage());
assertEquals("No reply produced by handler 'Enricher', and its 'requiresReply' property is set to true.",
e.getMessage());
return;
}
@@ -161,7 +163,7 @@ public class ContentEnricherTests {
final String requestChannelName = "Request_Channel";
final long requestTimeout = 200L;
QueueChannel replyChannel = new QueueChannel();
QueueChannel replyChannel = new QueueChannel();
QueueChannel requestChannel = new RendezvousChannel();
requestChannel.setBeanName(requestChannelName);
@@ -179,8 +181,7 @@ public class ContentEnricherTests {
}
catch (MessageDeliveryException e) {
assertEquals("failed to send message to channel '" + requestChannelName
+ "' within timeout: " + requestTimeout, e.getMessage());
return;
+ "' within timeout: " + requestTimeout, e.getMessage());
}
}
@@ -216,7 +217,7 @@ public class ContentEnricherTests {
@Test
public void setReplyChannelWithoutRequestChannel() {
QueueChannel replyChannel = new QueueChannel();
QueueChannel replyChannel = new QueueChannel();
ContentEnricher enricher = new ContentEnricher();
enricher.setReplyChannel(replyChannel);
@@ -287,8 +288,8 @@ public class ContentEnricherTests {
@Test
public void testContentEnricherWithNullRequestChannel() {
ContentEnricher enricher = new ContentEnricher();
enricher.setReplyChannel(new QueueChannel());
ContentEnricher enricher = new ContentEnricher();
enricher.setReplyChannel(new QueueChannel());
enricher.setBeanFactory(mock(BeanFactory.class));
try {
@@ -383,7 +384,7 @@ public class ContentEnricherTests {
enricher.afterPropertiesSet();
TargetUser target = new TargetUser();
target.setName("replace me");
target.setName("replace me");
Message<?> requestMessage = MessageBuilder.withPayload(target).setReplyChannel(replyChannel).build();
enricher.handleMessage(requestMessage);
@@ -416,12 +417,12 @@ public class ContentEnricherTests {
enricher.afterPropertiesSet();
UncloneableTargetUser target = new UncloneableTargetUser();
target.setName("replace me");
target.setName("replace me");
Message<?> requestMessage = MessageBuilder.withPayload(target).setReplyChannel(replyChannel).build();
try {
enricher.handleMessage(requestMessage);
enricher.handleMessage(requestMessage);
}
catch (MessageHandlingException e) {
assertThat(e.getMessage(), containsString("Failed to clone payload object"));
@@ -469,6 +470,56 @@ public class ContentEnricherTests {
assertTrue(enricher.isRunning());
}
/**
* In this test a {@link Target} message is passed into a {@link ContentEnricher}.
* The Enricher passes the message to a "request-channel" to a handler which throws
* an exception. The {@link ContentEnricher} then uses the error flow and consults
* the "error-channel" which returns a alternative {@link Target}.
*/
@Test
public void testErrorChannel() throws Exception {
final DirectChannel requestChannel = new DirectChannel();
requestChannel.subscribe(new AbstractReplyProducingMessageHandler() {
@Override
protected Object handleRequestMessage(Message<?> requestMessage) {
throw new RuntimeException();
}
});
final DirectChannel errorChannel = new DirectChannel();
errorChannel.subscribe(new AbstractReplyProducingMessageHandler() {
@Override
protected Object handleRequestMessage(Message<?> requestMessage) {
return new Target("failed");
}
});
final QueueChannel replyChannel = new QueueChannel();
final ContentEnricher enricher = new ContentEnricher();
enricher.setRequestChannel(requestChannel);
enricher.setErrorChannel(errorChannel);
SpelExpressionParser parser = new SpelExpressionParser();
Map<String, Expression> propertyExpressions = new HashMap<String, Expression>();
propertyExpressions.put("name", parser.parseExpression("payload.name + ' target'"));
enricher.setPropertyExpressions(propertyExpressions);
enricher.setBeanFactory(mock(BeanFactory.class));
enricher.afterPropertiesSet();
final Target target = new Target("replace me");
Message<?> requestMessage = MessageBuilder.withPayload(target).setReplyChannel(replyChannel).build();
enricher.handleMessage(requestMessage);
Message<?> reply = replyChannel.receive(10000);
Target result = (Target) reply.getPayload();
assertEquals("failed target", result.getName());
}
@SuppressWarnings("unused")
private static final class Source {

View File

@@ -173,7 +173,7 @@
with id <code>integrationHeaderChannelRegistry</code> and configure the required default delay using
a constructor argument (milliseconds).
</para>
<para>
Since <emphasis>version 4.1</emphasis>, you can set a property <code>removeOnGet</code>
to <code>true</code> on the <code>&lt;bean/&gt;</code> definition, and the mapping entry will be removed
@@ -212,7 +212,7 @@
</int:header-enricher>
<int:header-enricher input-channel="inputCustomTtl" output-channel="next">
<int:header-channels-to-string
<int:header-channels-to-string
time-to-live-expression="headers['channelTTL'] ?: 120000" />
</int:header-enricher>]]></programlisting>
@@ -295,12 +295,13 @@
output-channel="" ]]><co id="payload-enricher05-co" linkends="payload-enricher05" /><![CDATA[
request-payload-expression="" ]]><co id="payload-enricher06-co" linkends="payload-enricher06" /><![CDATA[
reply-channel="" ]]><co id="payload-enricher07-co" linkends="payload-enricher07" /><![CDATA[
send-timeout="" ]]><co id="payload-enricher08-co" linkends="payload-enricher08" /><![CDATA[
should-clone-payload="false"> ]]><co id="payload-enricher09-co" linkends="payload-enricher09" /><![CDATA[
<int:poller></int:poller> ]]><co id="payload-enricher10-co" linkends="payload-enricher10" /><![CDATA[
<int:property name="" expression="" null-result-expression="'Could not determine the name'"/> ]]><co id="payload-enricher11-co" linkends="payload-enricher11" /><![CDATA[
error-channel="" ]]><co id="payload-enricher08-co" linkends="payload-enricher08" /><![CDATA[
send-timeout="" ]]><co id="payload-enricher09-co" linkends="payload-enricher09" /><![CDATA[
should-clone-payload="false"> ]]><co id="payload-enricher10-co" linkends="payload-enricher10" /><![CDATA[
<int:poller></int:poller> ]]><co id="payload-enricher11-co" linkends="payload-enricher11" /><![CDATA[
<int:property name="" expression="" null-result-expression="'Could not determine the name'"/> ]]><co id="payload-enricher12-co" linkends="payload-enricher12" /><![CDATA[
<int:property name="" value="23" type="java.lang.Integer" null-result-expression="'0'"/>
<int:header name="" expression="" null-result-expression=""/> ]]><co id="payload-enricher12-co" linkends="payload-enricher12" /><![CDATA[
<int:header name="" expression="" null-result-expression=""/> ]]><co id="payload-enricher13-co" linkends="payload-enricher13" /><![CDATA[
<int:header name="" value="" overwrite="" type="" null-result-expression=""/>
</int:enricher>]]></programlisting>
@@ -381,7 +382,17 @@
<emphasis>Optional</emphasis>.
</para>
</callout>
<callout arearefs="payload-enricher08-co" id="payload-enricher08">
<callout arearefs="payload-enricher08-co" id="payload-enricher08">
<para>
Channel to which an <classname>ErrorMessage</classname> will be sent if an
<classname>Exception</classname> occurs downstream of the
<code>request-channel</code>. This enables you to return an alternative object to use
for enrichment. This is optional; if it is not set then <classname>Exception</classname>
is thrown to the caller.
<emphasis>Optional</emphasis>.
</para>
</callout>
<callout arearefs="payload-enricher09-co" id="payload-enricher09">
<para>
Maximum amount of time in milliseconds to wait when
sending a message to the channel, if such channel may block.
@@ -400,7 +411,7 @@
<emphasis>Optional</emphasis>.
</para>
</callout>
<callout arearefs="payload-enricher09-co" id="payload-enricher09">
<callout arearefs="payload-enricher10-co" id="payload-enricher10">
<para>
Boolean value indicating whether any payload that implements
<interfacename>Cloneable</interfacename> should be cloned
@@ -411,14 +422,14 @@
<emphasis>Optional</emphasis>.
</para>
</callout>
<callout arearefs="payload-enricher10-co" id="payload-enricher10">
<callout arearefs="payload-enricher11-co" id="payload-enricher11">
<para>
Allows you to configure a Message Poller if this endpoint
is a Polling Consumer.
<emphasis>Optional</emphasis>.
</para>
</callout>
<callout arearefs="payload-enricher11-co" id="payload-enricher11">
<callout arearefs="payload-enricher12-co" id="payload-enricher12">
<para>
Each <code>property</code> sub-element provides the
name of a property (via the mandatory <code>name</code>
@@ -452,7 +463,7 @@
returns null, it will be evaluated and the output of the evaluation will be returned instead.
</para>
</callout>
<callout arearefs="payload-enricher12-co" id="payload-enricher12">
<callout arearefs="payload-enricher13-co" id="payload-enricher13">
<para>
Each <code>header</code> sub-element provides the
name of a Message header (via the mandatory <code>name</code>

View File

@@ -131,10 +131,17 @@
<section id="4.1-content-enricher-improvement">
<title>Content Enricher Improvements</title>
<para>
Add null-result-expression attribute, which will be evaluated and returned if <code>&lt;enricher&gt;</code> returns null.
An <code>null-result-expression</code> attribute has been added, which is evaluated and returned if
<code>&lt;enricher&gt;</code> returns <code>null</code>.
It can be added in <code>&lt;header&gt;</code> and <code>&lt;property&gt;</code>.
See <xref linkend="content-enricher"/> for more information.
</para>
<para>
An <code>error-channel</code> attribute has been added, which is used to handle an error flow
if <classname>Exception</classname> occurs downstream of the <code>request-channel</code>. This enable
you to return an alternative object to use for enrichment.
See <xref linkend="content-enricher"/> for more information.
</para>
</section>
<section id="4.1-header-channel-registry">
<title>Header Channel Registry</title>