Skip to content
Merged
Show file tree
Hide file tree
Changes from all commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
Original file line number Diff line number Diff line change
Expand Up @@ -42,9 +42,9 @@
* value is the fully-qualified class name of a client class implementing {@link JavaDelegate}. The
* class is resolved through the currently-installed {@link ClientClassLoader} (managed by
* {@link ClientClassLoaderHolder}), instantiated afresh on every execution - through the client
* bean container when it declares an injection point (constructor or {@code @Inject} field), else
* via its public no-arg constructor - and invoked with the engine-provided
* {@link DelegateExecution}.
* bean container when it declares an injection point (constructor, {@code @Inject} field or
* {@code @PostConstruct} method), else via its public no-arg constructor - and invoked with the
* engine-provided {@link DelegateExecution}.
*
* <p>
* For the alternative {@code flowable:class="..."} approach, see the classloader configured on the
Expand Down Expand Up @@ -205,8 +205,8 @@ private static JavaDelegate instantiate(Class<?> handlerClass, String fqn) {
return (JavaDelegate) handlerClass.getDeclaredConstructor()
.newInstance();
} catch (ReflectiveOperationException e) {
throw new BpmnRuntimeException("Failed to instantiate client Java class [" + fqn
+ "]. A public no-arg constructor is required, or declare the collaborators it injects as @Component.", e);
throw new BpmnRuntimeException(
"Failed to instantiate client Java class [" + fqn + "]. A public no-arg constructor is required.", e);
}
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -43,8 +43,8 @@
* eagerly instantiates singletons with recursive <em>constructor injection</em> (plus
* {@code @Inject} field injection and {@code @PostConstruct} callbacks), detecting construction
* cycles. The behaviour consumers ({@code @Controller}, {@code @Scheduled}, {@code @Listener},
* {@code @Websocket}, {@code @Extension}) then fetch the ready instances via
* {@link #instanceOf(Class)} rather than instantiating client classes themselves.
* {@code @Websocket}) then fetch the ready instances via {@link #instanceOf(Class)} rather than
* instantiating client classes themselves.
*
* <p>
* Implements {@link ClientBeanFactory} and publishes itself into {@link ClientBeansHolder} so the
Expand Down Expand Up @@ -435,6 +435,11 @@ public <T> List<T> getAll(Class<T> type) {
*/
@Override
public <T> Optional<T> createUnmanaged(Class<T> type) {
if (isBean(type)) {
LOGGER.warn(
"[{}] is a JavaDelegate annotated @Component. A JavaDelegate must NOT be a @Component: Flowable instantiates the delegate itself, so the annotation additionally builds a container-managed singleton the engine never runs — a stray candidate for every List<JavaDelegate> injection. Remove @Component from the delegate.",
type.getName());
}
BeanDefinition definition = new BeanDefinition(type.getName(), type);
if (!declaresInjectionPoint(definition)) {
// Nothing to wire: the caller's own no-arg instantiation is equivalent, so it stays on it
Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -236,7 +236,7 @@ private Set<String> applyGeneration(Map<String, LoadedClass> nextGeneration, Cli

// Build the client bean container for the new generation BEFORE the load pass: every
// @Component (and the meta-annotated @Controller / @Repository / @Scheduled / @Listener /
// @Websocket / @Extension) is instantiated here with constructor + field injection, so the
// @Websocket) is instantiated here with constructor + field injection, so the
// behaviour consumers below just fetch ready instances via ComponentContainer#instanceOf.
componentContainer.rebuild(nextGeneration.values());

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,12 @@
import org.eclipse.dirigible.sdk.component.Component;
import org.eclipse.dirigible.sdk.component.Inject;
import org.junit.jupiter.api.Test;
import org.slf4j.LoggerFactory;

import ch.qos.logback.classic.Level;
import ch.qos.logback.classic.Logger;
import ch.qos.logback.classic.spi.ILoggingEvent;
import ch.qos.logback.core.read.ListAppender;
import jakarta.annotation.PostConstruct;
import jakarta.annotation.PreDestroy;

Expand Down Expand Up @@ -135,8 +140,11 @@ void a_post_construct_only_class_is_still_wired_so_its_callback_is_not_silently_

@Test
void an_unsatisfied_dependency_is_refused_and_is_not_a_rebuild_error() {
ComponentContainer container = TestComponentContainers.of();
// A real rebuild that registered a bean, so wiringErrors() reflects an actual generation and
// the "not a rebuild error" assertion below is load-bearing rather than trivially empty.
ComponentContainer container = TestComponentContainers.of(EnglishGreeter.class);

// The delegate needs RateProvider, which this container does not know: an unsatisfied dependency.
BeanContainerException exception =
assertThrows(BeanContainerException.class, () -> container.createUnmanaged(ConstructorDelegate.class));

Expand Down Expand Up @@ -173,6 +181,29 @@ void a_field_name_matching_a_bean_name_disambiguates() {
assertEquals(GermanGreeter.class, delegate.germanGreeter.getClass());
}

@Test
void a_delegate_annotated_component_is_warned_about_because_the_rule_is_otherwise_unobservable() {
Logger logger = (Logger) LoggerFactory.getLogger(ComponentContainer.class);
ListAppender<ILoggingEvent> appender = new ListAppender<>();
appender.start();
logger.addAppender(appender);
try {
ComponentContainer container = TestComponentContainers.of(RateProvider.class, ComponentDelegate.class);

container.createUnmanaged(ComponentDelegate.class)
.orElseThrow();

assertTrue(appender.list.stream()
.anyMatch(event -> event.getLevel() == Level.WARN && event.getFormattedMessage()
.contains(ComponentDelegate.class.getName())
&& event.getFormattedMessage()
.contains("must NOT be a @Component")),
() -> "expected a WARN naming the delegate and the rule, got: " + appender.list);
} finally {
logger.detachAppender(appender);
}
}

// --- fixtures (not @Component: a delegate is never a bean) ------------------------------------

@Component
Expand Down Expand Up @@ -241,4 +272,14 @@ static class NameHintedDelegate {
@Inject
Greeter germanGreeter;
}

/** The mistake the rule forbids: a delegate annotated {@code @Component}. */
@Component
static class ComponentDelegate {
final RateProvider rates;

ComponentDelegate(RateProvider rates) {
this.rates = rates;
}
}
}
Loading