admin管理员组文章数量:1410737
I am trying to understand how to externalize Spring Modulith application events via Kafka, but I can't get it to work properly. The problem is that the application events are not being sent via Kafka. Nothing is happening at the moment, and I am not getting any exceptions either, which makes it hard to debug since there are no clear hints on what is going wrong.
I have provided the complete Spring Boot application on GitHub:
In src/main/resources/environment, you will find two Docker Compose scripts — one for Kafka and one for MariaDB. The application starts correctly, and via REST API, you can create an account and perform financial transactions. However, no events are being published to Kafka.
Has anyone encountered a similar issue, or does anyone have any hints or advice on what I might be missing? Any help would be greatly appreciated!
I added this ApplicationModuleTest to make clear what should happen in the workflow.
package de.homebrewed.financemanager.workflow;
import static .assertj.core.api.Assertions.assertThat;
import de.homebrewed.financemanager.events.AccountCreationEvent;
import de.homebrewed.financemanager.events.FinancialTransactionCreationEvent;
import de.homebrewed.financemanager.external.persistance.entity.AccountEntity;
import de.homebrewed.financemanager.external.persistance.entity.FinancialTransactionEntity;
import de.homebrewed.financemanager.external.persistance.repository.AccountRepository;
import de.homebrewed.financemanager.external.persistance.repository.FinancialTransactionRepository;
import de.homebrewed.financemanager.sharedmands.CreateAccountCommand;
import de.homebrewed.financemanager.sharedmands.CreateTransactionCommand;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import .junit.jupiter.api.Test;
import .springframework.boot.test.autoconfigure.web.servlet.AutoConfigureWebMvc;
import .springframework.modulith.test.ApplicationModuleTest;
import .springframework.modulith.test.Scenario;
import .springframework.test.context.ActiveProfiles;
@ActiveProfiles("test")
@ApplicationModuleTest(ApplicationModuleTest.BootstrapMode.DIRECT_DEPENDENCIES)
@AutoConfigureWebMvc
@RequiredArgsConstructor
class TransactionWorkflowModuleTest {
private final AccountRepository accountRepository;
private final FinancialTransactionRepository financialTransactionRepository;
@Test
void createAccount(Scenario scenario) {
// 1st Creating an account (usually via REST)
var uuidAccount = UUID.randomUUID().toString();
BigDecimal initialBalance = new BigDecimal("10000");
CreateAccountCommand createAccountCommand =
new CreateAccountCommand("test checking account", initialBalance);
scenario
.publish(() -> new AccountCreationEvent(uuidAccount, createAccountCommand))
.andWaitForStateChange(accountRepository::findAll)
.andVerify(
result -> {
assertThat(result).hasSize(1);
AccountEntity actualAccountEntity = result.getFirst();
assertThat(actualAccountEntity.getBalance()).isEqualTo(initialBalance);
assertThat(actualAccountEntity.getId()).isNotNull();
});
// 2nd Creating a transaction to change account (subtract or add) balance
var uuidTransaction = UUID.randomUUID().toString();
BigDecimal amountToPay = new BigDecimal("100");
CreateTransactionCommand createTransactionCommand =
new CreateTransactionCommand(
"PAYMENT", amountToPay, "USD", LocalDateTime.now(), 1L, 1L, Boolean.FALSE);
scenario
.publish(
() -> new FinancialTransactionCreationEvent(uuidTransaction, createTransactionCommand))
.andWaitForStateChange(financialTransactionRepository::findAll)
.andVerify(
result -> {
assertThat(result).hasSize(1);
FinancialTransactionEntity financialTransactionEntity = result.getFirst();
assertThat(financialTransactionEntity.getId()).isNotNull();
assertThat(financialTransactionEntity.getCleared()).isTrue();
});
// If transaction was cleared successfully, then the amount of balance has changed in the created account
assertThat(accountRepository.findAll().getFirst().getBalance())
.isEqualTo(initialBalance.subtract(amountToPay));
}
}
I am trying to understand how to externalize Spring Modulith application events via Kafka, but I can't get it to work properly. The problem is that the application events are not being sent via Kafka. Nothing is happening at the moment, and I am not getting any exceptions either, which makes it hard to debug since there are no clear hints on what is going wrong.
I have provided the complete Spring Boot application on GitHub: https://github/xFakEdx/finance-manager
In src/main/resources/environment, you will find two Docker Compose scripts — one for Kafka and one for MariaDB. The application starts correctly, and via REST API, you can create an account and perform financial transactions. However, no events are being published to Kafka.
Has anyone encountered a similar issue, or does anyone have any hints or advice on what I might be missing? Any help would be greatly appreciated!
I added this ApplicationModuleTest to make clear what should happen in the workflow.
package de.homebrewed.financemanager.workflow;
import static .assertj.core.api.Assertions.assertThat;
import de.homebrewed.financemanager.events.AccountCreationEvent;
import de.homebrewed.financemanager.events.FinancialTransactionCreationEvent;
import de.homebrewed.financemanager.external.persistance.entity.AccountEntity;
import de.homebrewed.financemanager.external.persistance.entity.FinancialTransactionEntity;
import de.homebrewed.financemanager.external.persistance.repository.AccountRepository;
import de.homebrewed.financemanager.external.persistance.repository.FinancialTransactionRepository;
import de.homebrewed.financemanager.sharedmands.CreateAccountCommand;
import de.homebrewed.financemanager.sharedmands.CreateTransactionCommand;
import java.math.BigDecimal;
import java.time.LocalDateTime;
import java.util.UUID;
import lombok.RequiredArgsConstructor;
import .junit.jupiter.api.Test;
import .springframework.boot.test.autoconfigure.web.servlet.AutoConfigureWebMvc;
import .springframework.modulith.test.ApplicationModuleTest;
import .springframework.modulith.test.Scenario;
import .springframework.test.context.ActiveProfiles;
@ActiveProfiles("test")
@ApplicationModuleTest(ApplicationModuleTest.BootstrapMode.DIRECT_DEPENDENCIES)
@AutoConfigureWebMvc
@RequiredArgsConstructor
class TransactionWorkflowModuleTest {
private final AccountRepository accountRepository;
private final FinancialTransactionRepository financialTransactionRepository;
@Test
void createAccount(Scenario scenario) {
// 1st Creating an account (usually via REST)
var uuidAccount = UUID.randomUUID().toString();
BigDecimal initialBalance = new BigDecimal("10000");
CreateAccountCommand createAccountCommand =
new CreateAccountCommand("test checking account", initialBalance);
scenario
.publish(() -> new AccountCreationEvent(uuidAccount, createAccountCommand))
.andWaitForStateChange(accountRepository::findAll)
.andVerify(
result -> {
assertThat(result).hasSize(1);
AccountEntity actualAccountEntity = result.getFirst();
assertThat(actualAccountEntity.getBalance()).isEqualTo(initialBalance);
assertThat(actualAccountEntity.getId()).isNotNull();
});
// 2nd Creating a transaction to change account (subtract or add) balance
var uuidTransaction = UUID.randomUUID().toString();
BigDecimal amountToPay = new BigDecimal("100");
CreateTransactionCommand createTransactionCommand =
new CreateTransactionCommand(
"PAYMENT", amountToPay, "USD", LocalDateTime.now(), 1L, 1L, Boolean.FALSE);
scenario
.publish(
() -> new FinancialTransactionCreationEvent(uuidTransaction, createTransactionCommand))
.andWaitForStateChange(financialTransactionRepository::findAll)
.andVerify(
result -> {
assertThat(result).hasSize(1);
FinancialTransactionEntity financialTransactionEntity = result.getFirst();
assertThat(financialTransactionEntity.getId()).isNotNull();
assertThat(financialTransactionEntity.getCleared()).isTrue();
});
// If transaction was cleared successfully, then the amount of balance has changed in the created account
assertThat(accountRepository.findAll().getFirst().getBalance())
.isEqualTo(initialBalance.subtract(amountToPay));
}
}
Share
Improve this question
edited Mar 29 at 18:04
marc_s
756k184 gold badges1.4k silver badges1.5k bronze badges
asked Mar 23 at 11:20
F4k3dF4k3d
7251 gold badge12 silver badges29 bronze badges
3
|
1 Answer
Reset to default 2The test of the project fail for the following reasons:
- You have an
@ComponentScan
on your application class which disables all automatic component scanning set up for the base application package. Thus only code infinancemanager.external
is bootstrapped, nothing else. This explains why the event listeners in yourworkflow
package are not even getting triggered. - Event externalization is only triggered for the auto-configuration packages currently, configured. For
@ApplicationModuleTest
executions, this means that only events from modules included in the bootstrap are externalized. As you have all events grouped in a dedicated module not included in the test run, they do not get externalized. ConfiguringextraIncludes = "events"
does the trick here. I've filed a ticket to improve the logging at bootstrap a bit so that cases like this are easier to detect.
General recommendations
You're kind of abusing module base packages for technical decomposition. Spring Modulith does not work that way by default. Try to use business module packages (accounting
, transactions
?) on the top level. Further decompose into nested packages if necessary (hint: in 90% of the cases, it's not, just leads to scattered “one class per package” hard to understand codebases).
Your domains are intermingled. AccountService
and FinancialTransactionService
cross-reference each other's lower-level abstractions, which basically turns them into one big blob logically. I'm obviously not in the details but I wonder if we're actually talking about one domain here, not two.
The command to event listener translation feels like technical cargoculting. For the account creation you receive a request, you forward that to a service, that publishes an event, that an event listener receives, which in turn calls the actual AccountService
. IMO that's way too much technical machinery and boilerplate code required before anything useful happens. Just think about how much code you would have to add to implement a new use case that starts with receiving an HTTP request. Instead, receive the call in the controller (primary adapter), delegate to the AccountService (PrimaryPort / Application), publish events to notify others about the outcome, let them react to that. Leave all the eventually-consistent event machinery to the integration between business modules.
TransactionalEventListeners should really be executed asynchronously as they otherwise block the original thread and prevent the connection of the original transaction from being released.
Finally, the ….andWaitForStateChange(accountRepository::findAll)
would need to become .andWaitForStateChange(accountRepository::findAll, it -> !it.isEmpty())
as it's currently just checking for a non-null result by default and would immediately fail afterwards instead of waiting for the event-induced proceessing to finish. I filed a ticket to improve that.
Original analysis
The events published by the controllers are not emitted within a transaction. The event listener considering the @Externalized
annotations on the event types is a @TransactionalEventListener
, which means that it's only triggered on a transaction commit. In other words, you should get this to work by annotating the controllers with @Transactional
or by introducing a transactional service you delegate the command handling to.
Events are not a separate module. They're usually the API of a module (some might even stay internal).
I've filed a ticket to potentially detect that problem and inform users more proactively about it.
本文标签: Why is event externalization not triggered in a Spring Modulith applicationStack Overflow
版权声明:本文标题:Why is event externalization not triggered in a Spring Modulith application? - Stack Overflow 内容由网友自发贡献,该文观点仅代表作者本人, 转载请联系作者并注明出处:http://www.betaflare.com/web/1744288872a2599017.html, 本站仅提供信息存储空间服务,不拥有所有权,不承担相关法律责任。如发现本站有涉嫌抄袭侵权/违法违规的内容,一经查实,本站将立刻删除。
./mvnw spring-boot:run
fails with infrastructure missing. Ideally, you add a test case that triggers functionality ending in an event externalization using theScenario
API documented here. Setting the log level for Modulith toDEBUG
should at least show the event externalization getting activated. – Oliver Drotbohm Commented Mar 23 at 16:48