Skip to content

Commit

Permalink
Removed dependency to bean validation for message types
Browse files Browse the repository at this point in the history
  • Loading branch information
JohT committed Dec 30, 2021
1 parent cf335a3 commit 8713711
Show file tree
Hide file tree
Showing 10 changed files with 158 additions and 32 deletions.
22 changes: 19 additions & 3 deletions showcase-quarkus-eventsourcing/WALKTHROUGH.md
Original file line number Diff line number Diff line change
Expand Up @@ -8,6 +8,7 @@ The following topics are meant to lead through the code and highlight most impor
* [Connecting CDI to AxonFramework](#Connecting-CDI-to-AxonFramework)
* [Connecting JTA Transactions to AxonFramework](#Connecting-JTA-Transactions-to-AxonFramework)
* [Connecting JSON Binding to AxonFramework](#Connecting-JSON-Binding-to-AxonFramework)
* [Connecting Bean Validation to AxonFramework](#Connecting-Bean-Validation-to-AxonFramework)
* [Mitigate Core API dependencies](#Mitigate-Core-API-dependencies)
* [AxonFramework behind the boundary](#AxonFramework-behind-the-boundary)
* [ArchUnit in action](#ArchUnit-in-action)
Expand Down Expand Up @@ -72,6 +73,19 @@ This is pretty similar to [AxonFramework/cdi JtaTransactionManager.java](https:/
AxonFramework has build-in support for Jackson JSON serializer. For [JSON Binding][JSONBinding], some of Axon's internal serializable data types need to be adapted.
These are registered in [JsonbAxonAdapterRegister.java](./src/main/java/io/github/joht/showcase/quarkuseventsourcing/messaging/infrastructure/axon/serializer/jsonb/axon/adapter/JsonbAxonAdapterRegister.java). Except for [JsonbMetaDataAdapter.java](./src/main/java/io/github/joht/showcase/quarkuseventsourcing/messaging/infrastructure/axon/serializer/jsonb/axon/adapter/JsonbMetaDataAdapter.java) and [JsonbReplayTokenAdapter.java](./src/main/java/io/github/joht/showcase/quarkuseventsourcing/messaging/infrastructure/axon/serializer/jsonb/axon/adapter/JsonbReplayTokenAdapter.java) the remaining ones shouldn't be needed any more since [PullRequest #1163](https://github.com/AxonFramework/AxonFramework/pull/1163).

## Connecting Bean Validation to AxonFramework

[Jakarta Bean Validation][JakartaBeanValidation] lets you express constraints on object models via annotations. As described in [Axon Framework Message Intercepting][AxonFrameworkMessageIntercepting], there is a build-in dispatch interceptor to validate messages using Bean Validation.
The following code snippet shows how to attach it to the command bus:

```java
configuration.commandBus().registerDispatchInterceptor(new BeanValidationInterceptor<>(validatorFactory));
```

Bean Validation had been removed from this showcase in favor to dependency free message types.
It still can be found in version V1.4 and earlier e.g. in [AxonConfiguration.java](https://github.com/JohT/showcase-quarkus-eventsourcing/blob/6ebae39aba2e0828f5d1e93d82cd3caeedcb7541/showcase-quarkus-eventsourcing/src/main/java/io/github/joht/showcase/quarkuseventsourcing/messaging/infrastructure/axon/AxonConfiguration.java#L342) and [CreateAccountCommand.java](https://github.com/JohT/showcase-quarkus-eventsourcing/blob/6ebae39aba2e0828f5d1e93d82cd3caeedcb7541/showcase-quarkus-eventsourcing/src/main/java/io/github/joht/showcase/quarkuseventsourcing/message/command/account/CreateAccountCommand.java#L11)


## Mitigate Core API dependencies

The core API contains value objects of all messages (events, commands, queries). These can be found in the package [message](./src/main/java/io/github/joht/showcase/quarkuseventsourcing/message).
Expand All @@ -80,8 +94,6 @@ As a reference [AxonFramework Giftcard Example][AxonFrameworkGiftcardExample] sh

The core API might become a separate module and will be shared upon command- and query-side implementations. Shared libraries need to be treated with special care. Changes might introduce changes in dependent modules (coupling). They may also introduce version conflicts, if the dependent module needs a library in a different version. To mitigate that it is advantageous when the shared API is self contained and only depends on java itself.

**&#8505;** Currently (2021) there is a dependency to the bean validation API. This could also be replaced.

### Command message types without axon dependency

Command messages need a property that matches the id of the aggregate they belong to.
Expand Down Expand Up @@ -274,11 +286,13 @@ If all use cases of the application are covered by integration tests or in other
* [Axon Framework CDI Support][AxonFrameworkCDI]
* [Axon Framework Giftcard Example][AxonFrameworkGiftcardExample]
* [Axon Framework Parameter Resolver][AxonFrameworkParameterResolver]
* [Axon Framework Message Intercepting][AxonFrameworkMessageIntercepting]
* [Building a native executable][QuarkusNativeExecutable]
* [CDI Portable Extension][CDIExtension]
* [Eclipse MicroProfile][MicroProfile]
* [Flyway Version control for your database][Flyway]
* [Introducing Subscription Queries][AxonSubscriptionQueries]
* [Jakarta Bean Validation][JakartaBeanValidation]
* [Jakarta JSON Binding][JSONBinding]
* [Jakarta Transactions (JTA)][JakartaTransaction]
* [Java Beans ConstructorProperties Annotation][ConstructorProperties]
Expand All @@ -300,12 +314,14 @@ If all use cases of the application are covered by integration tests or in other
[ArchUnit]: https://www.archunit.org
[NativeImageAssistedConfiguration]: https://www.graalvm.org/reference-manual/native-image/Agent
[AxonFrameworkCDI]: https://github.com/AxonFramework/extension-cdi
[AxonFrameworkParameterResolver]: https://axoniq.io/blog-overview/parameter-resolvers-axon
[AxonFrameworkGiftcardExample]: https://github.com/AxonFramework/extension-springcloud-sample
[AxonFrameworkParameterResolver]: https://axoniq.io/blog-overview/parameter-resolvers-axon
[AxonFrameworkMessageIntercepting]: https://docs.axoniq.io/reference-guide/v/master/axon-framework/messaging-concepts/message-intercepting#structural-validation
[AxonSubscriptionQueries]: https://axoniq.io/blog-overview/introducing-subscription-queries
[CDIExtension]: https://docs.jboss.org/weld/reference/latest/en-US/html/extend.html
[ConstructorProperties]: https://docs.oracle.com/javase/8/docs/api/java/beans/ConstructorProperties.html
[Flyway]: https://flywaydb.org
[JakartaBeanValidation]: https://beanvalidation.org
[JakartaTransaction]: https://jakarta.ee/specifications/transactions/
[Jasmine]: https://jasmine.github.io
[jasmine-maven-plugin]: https://searls.github.io/jasmine-maven-plugin/
Expand Down
4 changes: 0 additions & 4 deletions showcase-quarkus-eventsourcing/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -148,10 +148,6 @@
<groupId>io.quarkus</groupId>
<artifactId>quarkus-smallrye-openapi</artifactId>
</dependency>
<dependency>
<groupId>io.quarkus</groupId>
<artifactId>quarkus-hibernate-validator</artifactId>
</dependency>

<!-- Integration Testing -->
<dependency>
Expand Down
10 changes: 10 additions & 0 deletions showcase-quarkus-eventsourcing/reflection-config.json
Original file line number Diff line number Diff line change
Expand Up @@ -540,5 +540,15 @@
"allDeclaredFields": true,
"allDeclaredMethods": true,
"allDeclaredConstructors": true
},
{
"name":"io.github.joht.showcase.quarkuseventsourcing.message.command.account.ChangeNicknameCommand",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true
},
{
"name":"io.github.joht.showcase.quarkuseventsourcing.message.command.account.CreateAccountCommand",
"allDeclaredFields":true,
"queryAllDeclaredMethods":true
}
]
Original file line number Diff line number Diff line change
@@ -1,25 +1,23 @@
package io.github.joht.showcase.quarkuseventsourcing.message.command.account;

import java.beans.ConstructorProperties;

import javax.validation.constraints.NotNull;
import java.util.Objects;
import java.util.function.Supplier;

import io.github.joht.showcase.quarkuseventsourcing.message.command.CommandTargetAggregateIdentifier;
import io.github.joht.showcase.quarkuseventsourcing.message.common.Nickname;

public class ChangeNicknameCommand {

@NotNull
@CommandTargetAggregateIdentifier
private final String accountId;

@NotNull
private final Nickname nickname;

@ConstructorProperties({ "accountId", "nickname" })
public ChangeNicknameCommand(String accountId, Nickname nickname) {
this.accountId = accountId;
this.nickname = nickname;
this.accountId = requireNonNull(accountId, () -> "accountId missing");;
this.nickname = requireNonNull(nickname, () -> "nickname missing");;
}

public String getAccountId() {
Expand All @@ -34,4 +32,25 @@ public Nickname getNickname() {
public String toString() {
return "ChangeNicknameCommand [accountId=" + accountId + ", nickname=" + nickname + "]";
}

@Override
public int hashCode() {
return Objects.hash(accountId, nickname);
}

@Override
public boolean equals(Object obj) {
if ((obj == null) || (getClass() != obj.getClass())) {
return false;
}
ChangeNicknameCommand other = (ChangeNicknameCommand) obj;
return Objects.equals(accountId, other.accountId) && Objects.equals(nickname, other.nickname);
}

private static <T> T requireNonNull(T obj, Supplier<String> messageSupplier) {
if (obj == null) {
throw new IllegalArgumentException(messageSupplier.get());
}
return obj;
}
}
Original file line number Diff line number Diff line change
@@ -1,20 +1,19 @@
package io.github.joht.showcase.quarkuseventsourcing.message.command.account;

import java.beans.ConstructorProperties;

import javax.validation.constraints.NotNull;
import java.util.Objects;
import java.util.function.Supplier;

import io.github.joht.showcase.quarkuseventsourcing.message.command.CommandTargetAggregateIdentifier;

public class CreateAccountCommand {

@NotNull
@CommandTargetAggregateIdentifier
private final String accountId;

@ConstructorProperties({ "accountId" })
public CreateAccountCommand(String accountId) {
this.accountId = accountId;
this.accountId = requireNonNull(accountId, () -> "accountId missing");
}

public String getAccountId() {
Expand All @@ -25,4 +24,25 @@ public String getAccountId() {
public String toString() {
return "CreateAccountCommand [accountId=" + accountId + "]";
}

@Override
public int hashCode() {
return Objects.hash(accountId);
}

@Override
public boolean equals(Object obj) {
if ((obj == null) || (getClass() != obj.getClass())) {
return false;
}
CreateAccountCommand other = (CreateAccountCommand) obj;
return Objects.equals(accountId, other.accountId);
}

private static <T> T requireNonNull(T obj, Supplier<String> messageSupplier) {
if (obj == null) {
throw new IllegalArgumentException(messageSupplier.get());
}
return obj;
}
}
Original file line number Diff line number Diff line change
Expand Up @@ -21,7 +21,6 @@
import javax.sql.DataSource;
import javax.transaction.TransactionSynchronizationRegistry;
import javax.transaction.UserTransaction;
import javax.validation.ValidatorFactory;

import org.axonframework.common.Priority;
import org.axonframework.common.transaction.TransactionManager;
Expand All @@ -38,7 +37,6 @@
import org.axonframework.eventsourcing.eventstore.jdbc.JdbcEventStorageEngine;
import org.axonframework.messaging.annotation.MultiParameterResolverFactory;
import org.axonframework.messaging.annotation.ParameterResolverFactory;
import org.axonframework.messaging.interceptors.BeanValidationInterceptor;
import org.axonframework.serialization.RevisionResolver;
import org.axonframework.serialization.Serializer;

Expand Down Expand Up @@ -87,9 +85,6 @@ public class AxonConfiguration {
@Named("messaging")
DataSource dataSource;

@Inject
ValidatorFactory validatorFactory;

@Inject
UserTransaction userTransaction;

Expand All @@ -116,7 +111,6 @@ protected void startUp() {
.configureTransactionManager(c -> transactionManager())
.buildConfiguration();
configurer.registerComponent(ParameterResolverFactory.class, this::parameterResolvers);
enableBeanValidationForCommandMessages();
configuration.start();
}

Expand Down Expand Up @@ -336,9 +330,4 @@ private Class<?> postgreSqlObjectType() {
throw new UnsupportedOperationException("Cannot load PostgreSql specific data type class " + className, e);
}
}

// Note Command-Side
private void enableBeanValidationForCommandMessages() {
configuration.commandBus().registerDispatchInterceptor(new BeanValidationInterceptor<>(validatorFactory));
}
}
Original file line number Diff line number Diff line change
@@ -1,5 +1,8 @@
package io.github.joht.showcase.quarkuseventsourcing.service.infrastructure;

import java.util.logging.Level;
import java.util.logging.Logger;

import javax.ws.rs.core.Response;
import javax.ws.rs.core.Response.Status;
import javax.ws.rs.ext.ExceptionMapper;
Expand All @@ -8,11 +11,14 @@
@Provider
public class IllegalArgumentExceptionMapper implements ExceptionMapper<IllegalArgumentException> {

private static final Logger LOGGER = Logger.getLogger(IllegalArgumentExceptionMapper.class.getName());

/**
* {@inheritDoc}
*/
@Override
public Response toResponse(IllegalArgumentException exception) {
LOGGER.log(Level.WARNING, exception, () -> "Will return status code " + Status.BAD_REQUEST);
return Response.status(Status.BAD_REQUEST).entity(exception.getMessage()).build();
}

Expand Down
Original file line number Diff line number Diff line change
Expand Up @@ -74,10 +74,10 @@ void boundaryShouldNotUseAxonDirectly() {
}

@Test
@DisplayName("messages should not depend on javax")
void messagesShouldNotDependOnJavax() {
classes().that().resideInAPackage("..messages..")
.should().onlyDependOnClassesThat().resideOutsideOfPackage("..javax..")
@DisplayName("message types should only depend on java and classes of the package they belong to")
void messageTypesShouldOnlyDependOnJava() {
classes().that().resideInAPackage("..message..")
.should().onlyDependOnClassesThat().resideInAnyPackage("java..", "..message..")
.check(classes);
}

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,46 @@
package io.github.joht.showcase.quarkuseventsourcing.message.command.account;

import static org.junit.jupiter.api.Assertions.*;

import org.junit.jupiter.api.Test;

import io.github.joht.showcase.quarkuseventsourcing.message.common.Nickname;

class ChangeNicknameCommandTest {

private static final Nickname NICKNAME = Nickname.of("Nick");

private ChangeNicknameCommand commandUnderTest;

@Test
void containsAccountId() {
String expectedValue = "1234";
commandUnderTest = createChangeNicknameCommand(expectedValue, NICKNAME);
assertEquals(expectedValue, commandUnderTest.getAccountId());
}

@Test
void containsNickname() {
Nickname expectedValue = NICKNAME;
commandUnderTest = createChangeNicknameCommand("", expectedValue);
assertEquals(expectedValue, commandUnderTest.getNickname());
}

@Test
void failsOnMissingAccountId() {
RuntimeException exception = assertThrows(RuntimeException.class,
() -> createChangeNicknameCommand(null, NICKNAME));
assertEquals("accountId missing", exception.getMessage());
}

@Test
void failsOnMissingNickname() {
RuntimeException exception = assertThrows(RuntimeException.class,
() -> createChangeNicknameCommand("", null));
assertEquals("nickname missing", exception.getMessage());
}

private ChangeNicknameCommand createChangeNicknameCommand(String accountId, Nickname nickname) {
return new ChangeNicknameCommand(accountId, nickname);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package io.github.joht.showcase.quarkuseventsourcing.message.command.account;

import static org.junit.jupiter.api.Assertions.*;

import org.junit.jupiter.api.Test;

class CreateAccountCommandTest {

private CreateAccountCommand commandUnderTest;

@Test
void containsAccountId() {
String expectedValue = "12345";
commandUnderTest = new CreateAccountCommand(expectedValue);
assertEquals(expectedValue, commandUnderTest.getAccountId());
}

@Test
void failsOnMissingAccountId() {
RuntimeException exception = assertThrows(RuntimeException.class,
() -> new CreateAccountCommand(null));
assertEquals("accountId missing", exception.getMessage());
}
}

0 comments on commit 8713711

Please sign in to comment.