Skip to content

Commit 3dd8b63

Browse files
artembilangaryrussell
authored andcommitted
INT-4569: Disallow beans override in DSL (#2664)
* INT-4569: Disallow beans override in DSL JIRA: https://jira.spring.io/browse/INT-4569 * Thorw `BeanDefinitionOverrideException` from the `IntegrationFlowBeanPostProcessor` when it detects existing bean and it is not the same object we try to register from the DSL * Document limitations about `prototype` beans * Some polishing in the DSL chapter of the docs * * Fix algorithm in the `IntegrationFlowBeanPostProcessor.noBeanPresentForComponent()` * * Polishing dsl.adoc * Call `BeanFactory.initializeBean()` for existing beans if they are `prototype` * * Code formatting in the `ManualFlowTests`
1 parent 65df35b commit 3dd8b63

File tree

3 files changed

+86
-23
lines changed

3 files changed

+86
-23
lines changed

spring-integration-core/src/main/java/org/springframework/integration/dsl/context/IntegrationFlowBeanPostProcessor.java

+39-12
Original file line numberDiff line numberDiff line change
@@ -16,7 +16,6 @@
1616

1717
package org.springframework.integration.dsl.context;
1818

19-
import java.util.Collection;
2019
import java.util.LinkedHashMap;
2120
import java.util.Map;
2221

@@ -34,10 +33,12 @@
3433
import org.springframework.beans.factory.config.BeanDefinition;
3534
import org.springframework.beans.factory.config.BeanDefinitionCustomizer;
3635
import org.springframework.beans.factory.config.BeanPostProcessor;
36+
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
3737
import org.springframework.beans.factory.config.ConfigurableListableBeanFactory;
3838
import org.springframework.beans.factory.config.EmbeddedValueResolver;
3939
import org.springframework.beans.factory.support.AbstractBeanDefinition;
4040
import org.springframework.beans.factory.support.BeanDefinitionBuilder;
41+
import org.springframework.beans.factory.support.BeanDefinitionOverrideException;
4142
import org.springframework.beans.factory.support.BeanDefinitionRegistry;
4243
import org.springframework.context.ApplicationContext;
4344
import org.springframework.context.ApplicationContextAware;
@@ -163,7 +164,7 @@ else if (useFlowIdAsPrefix) {
163164
id = flowNamePrefix + id;
164165
}
165166

166-
if (noBeanPresentForComponent(messageHandler)) {
167+
if (noBeanPresentForComponent(messageHandler, flowBeanName)) {
167168
String handlerBeanName = generateBeanName(messageHandler, flowNamePrefix);
168169

169170
registerComponent(messageHandler, handlerBeanName, flowBeanName);
@@ -174,7 +175,7 @@ else if (useFlowIdAsPrefix) {
174175
targetIntegrationComponents.put(endpoint, id);
175176
}
176177
else {
177-
if (noBeanPresentForComponent(component)) {
178+
if (noBeanPresentForComponent(component, flowBeanName)) {
178179
if (component instanceof AbstractMessageChannel) {
179180
String channelBeanName = ((AbstractMessageChannel) component).getComponentName();
180181
if (channelBeanName == null) {
@@ -211,7 +212,7 @@ else if (component instanceof SourcePollingChannelAdapterSpec) {
211212
if (!CollectionUtils.isEmpty(componentsToRegister)) {
212213
componentsToRegister.entrySet()
213214
.stream()
214-
.filter(o -> noBeanPresentForComponent(o.getKey()))
215+
.filter(o -> noBeanPresentForComponent(o.getKey(), flowBeanName))
215216
.forEach(o ->
216217
registerComponent(o.getKey(),
217218
generateBeanName(o.getKey(), flowNamePrefix, o.getValue(),
@@ -231,7 +232,7 @@ else if (useFlowIdAsPrefix) {
231232
targetIntegrationComponents.put(pollingChannelAdapterFactoryBean, id);
232233

233234
MessageSource<?> messageSource = spec.get().getT2();
234-
if (noBeanPresentForComponent(messageSource)) {
235+
if (noBeanPresentForComponent(messageSource, flowBeanName)) {
235236
String messageSourceId = id + ".source";
236237
if (messageSource instanceof NamedComponent
237238
&& ((NamedComponent) messageSource).getComponentName() != null) {
@@ -277,7 +278,14 @@ else if (component instanceof AnnotationGatewayProxyFactoryBean) {
277278
}
278279
}
279280
else {
280-
targetIntegrationComponents.put(entry.getKey(), entry.getValue());
281+
Object componentToUse = entry.getKey();
282+
String beanNameToUse = entry.getValue();
283+
if (StringUtils.hasText(beanNameToUse) &&
284+
ConfigurableBeanFactory.SCOPE_PROTOTYPE.equals(
285+
this.beanFactory.getBeanDefinition(beanNameToUse).getScope())) {
286+
this.beanFactory.initializeBean(componentToUse, beanNameToUse);
287+
}
288+
targetIntegrationComponents.put(component, beanNameToUse);
281289
}
282290
}
283291
}
@@ -337,7 +345,7 @@ private void processIntegrationComponentSpec(String beanName, IntegrationCompone
337345

338346
componentsToRegister.entrySet()
339347
.stream()
340-
.filter(component -> noBeanPresentForComponent(component.getKey()))
348+
.filter(component -> noBeanPresentForComponent(component.getKey(), beanName))
341349
.forEach(component ->
342350
registerComponent(component.getKey(),
343351
generateBeanName(component.getKey(), component.getValue())));
@@ -378,17 +386,36 @@ private void invokeBeanInitializationHooks(final String beanName, final Object b
378386
}
379387
}
380388

381-
private boolean noBeanPresentForComponent(Object instance) {
389+
@SuppressWarnings("unchecked")
390+
private boolean noBeanPresentForComponent(Object instance, String parentBeanName) {
382391
if (instance instanceof NamedComponent) {
383392
String beanName = ((NamedComponent) instance).getComponentName();
384393
if (beanName != null) {
385-
return !this.beanFactory.containsBean(beanName);
394+
if (this.beanFactory.containsBean(beanName)) {
395+
BeanDefinition existingBeanDefinition = this.beanFactory.getBeanDefinition(beanName);
396+
if (!ConfigurableBeanFactory.SCOPE_PROTOTYPE.equals(existingBeanDefinition.getScope())
397+
&& !instance.equals(this.beanFactory.getBean(beanName))) {
398+
399+
AbstractBeanDefinition beanDefinition =
400+
BeanDefinitionBuilder.genericBeanDefinition((Class<Object>) instance.getClass(),
401+
() -> instance)
402+
.getBeanDefinition();
403+
beanDefinition.setResourceDescription("the '" + parentBeanName + "' bean definition");
404+
throw new BeanDefinitionOverrideException(beanName, beanDefinition, existingBeanDefinition);
405+
}
406+
else {
407+
return false;
408+
}
409+
}
410+
else {
411+
return true;
412+
}
386413
}
387414
}
388415

389-
Collection<?> beans = this.beanFactory.getBeansOfType(instance.getClass(), false, false).values();
390-
391-
return !beans.contains(instance);
416+
return !this.beanFactory.getBeansOfType(instance.getClass(), false, false)
417+
.values()
418+
.contains(instance);
392419
}
393420

394421
private void registerComponent(Object component, String beanName) {

spring-integration-core/src/test/java/org/springframework/integration/dsl/manualflow/ManualFlowTests.java

+25-2
Original file line numberDiff line numberDiff line change
@@ -16,6 +16,7 @@
1616

1717
package org.springframework.integration.dsl.manualflow;
1818

19+
import static org.assertj.core.api.Assertions.assertThatThrownBy;
1920
import static org.hamcrest.Matchers.containsString;
2021
import static org.hamcrest.Matchers.instanceOf;
2122
import static org.hamcrest.Matchers.lessThan;
@@ -46,18 +47,21 @@
4647
import org.junit.Test;
4748
import org.junit.runner.RunWith;
4849

50+
import org.springframework.beans.factory.BeanCreationException;
4951
import org.springframework.beans.factory.BeanCreationNotAllowedException;
5052
import org.springframework.beans.factory.BeanFactory;
5153
import org.springframework.beans.factory.DisposableBean;
5254
import org.springframework.beans.factory.ListableBeanFactory;
5355
import org.springframework.beans.factory.annotation.Autowired;
5456
import org.springframework.beans.factory.config.BeanFactoryPostProcessor;
5557
import org.springframework.beans.factory.config.ConfigurableBeanFactory;
58+
import org.springframework.beans.factory.support.BeanDefinitionOverrideException;
5659
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
5760
import org.springframework.context.annotation.AnnotationConfigApplicationContext;
5861
import org.springframework.context.annotation.Bean;
5962
import org.springframework.context.annotation.Configuration;
6063
import org.springframework.context.annotation.Scope;
64+
import org.springframework.integration.channel.DirectChannel;
6165
import org.springframework.integration.channel.QueueChannel;
6266
import org.springframework.integration.config.EnableIntegration;
6367
import org.springframework.integration.config.EnableIntegrationManagement;
@@ -241,7 +245,8 @@ public void testManualFlowRegistration() throws InterruptedException {
241245
assertThat(e.getMessage(), containsString("The 'receive()/receiveAndConvert()' isn't supported"));
242246
}
243247

244-
assertThat(this.beanFactory.getBeanNamesForType(MessageTransformingHandler.class)[0], startsWith(flowId + "."));
248+
assertThat(this.beanFactory.getBeanNamesForType(MessageTransformingHandler.class)[0],
249+
startsWith(flowId + "."));
245250

246251
flowRegistration.destroy();
247252

@@ -516,6 +521,18 @@ public void testConcurrentRegistration() throws InterruptedException {
516521
flowRegistrations.forEach(IntegrationFlowRegistration::destroy);
517522
}
518523

524+
525+
@Test
526+
public void testDisabledBeansOverride() {
527+
assertThatThrownBy(
528+
() -> this.integrationFlowContext
529+
.registration(f -> f.channel(c -> c.direct("doNotOverrideChannel")))
530+
.register())
531+
.isExactlyInstanceOf(BeanCreationException.class)
532+
.hasCauseExactlyInstanceOf(BeanDefinitionOverrideException.class)
533+
.hasMessageContaining("Invalid bean definition with name 'doNotOverrideChannel'");
534+
}
535+
519536
@Configuration
520537
@EnableIntegration
521538
@EnableMessageHistory
@@ -533,6 +550,12 @@ public Date foo() {
533550
return new Date();
534551
}
535552

553+
554+
@Bean
555+
public MessageChannel doNotOverrideChannel() {
556+
return new DirectChannel();
557+
}
558+
536559
}
537560

538561
private static class MyFlowAdapter extends IntegrationFlowAdapter {
@@ -582,7 +605,7 @@ protected void doInit() {
582605
}
583606

584607
@Override
585-
public void destroy() throws Exception {
608+
public void destroy() {
586609
this.destroyed = true;
587610
}
588611

src/reference/asciidoc/dsl.adoc

+22-9
Original file line numberDiff line numberDiff line change
@@ -115,28 +115,38 @@ The endpoints are automatically wired together by using direct channels.
115115

116116
[[java-dsl-class-cast]]
117117
.Lambdas And `Message<?>` Arguments
118-
IMPORTANT: When using lambdas in EIP methods, the "input" argument is generally the message payload.
118+
[IMPORTANT]
119+
====
120+
When using lambdas in EIP methods, the "input" argument is generally the message payload.
119121
If you wish to access the entire message, use one of the overloaded methods that take a `Class<?>` as the first parameter.
120122
For example, this won't work:
121123
122-
====
123124
[source, java]
124125
----
125126
.<Message<?>, Foo>transform(m -> newFooFromMessage(m))
126127
----
127-
====
128128
129129
This will fail at runtime with a `ClassCastException` because the lambda doesn't retain the argument type and the framework will attempt to cast the payload to a `Message<?>`.
130130
131131
Instead, use:
132132
133-
====
134133
[source, java]
135134
----
136135
.(Message.class, m -> newFooFromMessage(m))
137136
----
138137
====
139138

139+
[[bean-definitions-override]]
140+
.Bean Definitions override
141+
[IMPORTANT]
142+
====
143+
The Java DSL can register beans for the object defined in-line in the flow definition, as well as can reuse existing, injected beans.
144+
In case of the same bean name defined for in-line object and existing bean definition, a `BeanDefinitionOverrideException` is thrown indicating that such a configuration is wrong.
145+
However when you deal with `prototype` beans, there is no way to detect from the integration flow processor an existing bean definition because every time we call a `prototype` bean from the `BeanFactory` we get a new instance.
146+
This way a provided instance is used in the `IntegrationFlow` as is without any bean registration and any possible check against existing `prototype` bean definition.
147+
However `BeanFactory.initializeBean()` is called for this object if it has an explicit `id` and bean definition for this name is in `prototype` scope.
148+
====
149+
140150
[[java-dsl-channels]]
141151
=== Message Channels
142152

@@ -316,7 +326,7 @@ It avoids inconvenient coding using setters and makes the flow definition more s
316326
Note that you can use `Transformers` to declare target `Transformer` instances as `@Bean` instances and, again, use them from `IntegrationFlow` definition as bean methods.
317327
Nevertheless, the DSL parser takes care of bean declarations for inline objects, if they are not yet defined as beans.
318328

319-
See [https://docs.spring.io/spring-integration/api/org/springframework/integration/dsl/Transformers.html] in the Javadoc for more information and supported factory methods.
329+
See https://docs.spring.io/spring-integration/api/org/springframework/integration/dsl/Transformers.html[Transformers] in the Javadoc for more information and supported factory methods.
320330

321331
Also see <<java-dsl-class-cast>>.
322332

@@ -789,15 +799,18 @@ public IntegrationFlow evenFlow() {
789799
}
790800
----
791801
802+
{empty} +
792803
In this case, when you need to receive a reply from such a sub-flow and continue the main flow, this `IntegrationFlow` bean reference (or its input channel) has to be wrapped with a `.gateway()` as shown in the preceding example.
793804
The `oddFlow()` reference in the preceding example is not wrapped to the `.gateway()`.
794805
Therefore, we do not expect a reply from this routing branch.
795806
Otherwise, you end up with an exception similar to the following:
796807
797-
[source]
798-
----
799-
Caused by: org.springframework.beans.factory.BeanCreationException: The 'currentComponent' (org.springframework.integration.router.MethodInvokingRouter@7965a51c) is a one-way 'MessageHandler' and it isn't appropriate to configure 'outputChannel'. This is the end of the integration flow.
800-
----
808+
....
809+
Caused by: org.springframework.beans.factory.BeanCreationException:
810+
The 'currentComponent' (org.springframework.integration.router.MethodInvokingRouter@7965a51c)
811+
is a one-way 'MessageHandler' and it isn't appropriate to configure 'outputChannel'.
812+
This is the end of the integration flow.
813+
....
801814
802815
When you configure a sub-flow as a lambda, the framework handles the request-reply interaction with the sub-flow and a gateway is not needed.
803816
====

0 commit comments

Comments
 (0)