Add the hexagonal module: Hexagonal Architecture (Ports and Adapters) in Spring Boot 4
This commit is contained in:
@@ -0,0 +1,93 @@
|
||||
[INFO] Scanning for projects...
|
||||
[INFO]
|
||||
[INFO] --------------< com.ankurm:hexagonal-persistence-adapter >--------------
|
||||
[INFO] Building hexagonal-persistence-adapter 1.0
|
||||
[INFO] from pom.xml
|
||||
[INFO] --------------------------------[ jar ]---------------------------------
|
||||
[INFO]
|
||||
[INFO] --- clean:3.2.0:clean (default-clean) @ hexagonal-persistence-adapter ---
|
||||
[INFO] Deleting /home/claude/spring-boot-demo/hexagonal/persistence-adapter/target
|
||||
[INFO]
|
||||
[INFO] --- resources:3.3.1:resources (default-resources) @ hexagonal-persistence-adapter ---
|
||||
[INFO] skip non existing resourceDirectory /home/claude/spring-boot-demo/hexagonal/persistence-adapter/src/main/resources
|
||||
[INFO]
|
||||
[INFO] --- compiler:3.13.0:compile (default-compile) @ hexagonal-persistence-adapter ---
|
||||
[INFO] Recompiling the module because of changed source code.
|
||||
[INFO] Compiling 3 source files with javac [debug parameters target 25] to target/classes
|
||||
[INFO]
|
||||
[INFO] --- resources:3.3.1:testResources (default-testResources) @ hexagonal-persistence-adapter ---
|
||||
[INFO] skip non existing resourceDirectory /home/claude/spring-boot-demo/hexagonal/persistence-adapter/src/test/resources
|
||||
[INFO]
|
||||
[INFO] --- compiler:3.13.0:testCompile (default-testCompile) @ hexagonal-persistence-adapter ---
|
||||
[INFO] Recompiling the module because of changed dependency.
|
||||
[INFO] Compiling 2 source files with javac [debug parameters target 25] to target/test-classes
|
||||
[INFO]
|
||||
[INFO] --- surefire:3.2.5:test (default-test) @ hexagonal-persistence-adapter ---
|
||||
[INFO] Using auto detected provider org.apache.maven.surefire.junitplatform.JUnitPlatformProvider
|
||||
[INFO]
|
||||
[INFO] -------------------------------------------------------
|
||||
[INFO] T E S T S
|
||||
[INFO] -------------------------------------------------------
|
||||
[INFO] Running com.ankurm.hexagonal.persistence.JpaWalletRepositoryAdapterTest
|
||||
01:54:15.288 [main] INFO org.springframework.test.context.support.AnnotationConfigContextLoaderUtils -- Could not detect default configuration classes for test class [com.ankurm.hexagonal.persistence.JpaWalletRepositoryAdapterTest]: JpaWalletRepositoryAdapterTest does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
|
||||
01:54:15.428 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper -- Found @SpringBootConfiguration com.ankurm.hexagonal.persistence.TestApplication for test class com.ankurm.hexagonal.persistence.JpaWalletRepositoryAdapterTest
|
||||
01:54:15.430 [main] INFO org.springframework.test.context.support.AnnotationConfigContextLoaderUtils -- Could not detect default configuration classes for test class [com.ankurm.hexagonal.persistence.JpaWalletRepositoryAdapterTest]: JpaWalletRepositoryAdapterTest does not declare any static, non-private, non-final, nested classes annotated with @Configuration.
|
||||
01:54:15.445 [main] INFO org.springframework.boot.test.context.SpringBootTestContextBootstrapper -- Found @SpringBootConfiguration com.ankurm.hexagonal.persistence.TestApplication for test class com.ankurm.hexagonal.persistence.JpaWalletRepositoryAdapterTest
|
||||
|
||||
. ____ _ __ _ _
|
||||
/\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \
|
||||
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
|
||||
\\/ ___)| |_)| | | | | || (_| | ) ) ) )
|
||||
' |____| .__|_| |_|_| |_\__, | / / / /
|
||||
=========|_|==============|___/=/_/_/_/
|
||||
|
||||
:: Spring Boot :: (v4.1.1)
|
||||
|
||||
2026-10-04T01:54:15.739+05:30 INFO 13747 --- [ main] c.a.h.p.JpaWalletRepositoryAdapterTest : Starting JpaWalletRepositoryAdapterTest using Java 25.0.4.1 with PID 13747 (started by root in /home/claude/spring-boot-demo/hexagonal/persistence-adapter)
|
||||
2026-10-04T01:54:15.740+05:30 INFO 13747 --- [ main] c.a.h.p.JpaWalletRepositoryAdapterTest : No active profile set, falling back to 1 default profile: "default"
|
||||
2026-10-04T01:54:16.051+05:30 INFO 13747 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode.
|
||||
2026-10-04T01:54:16.089+05:30 INFO 13747 --- [ main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 31 ms. Found 1 JPA repository interface.
|
||||
2026-10-04T01:54:16.151+05:30 INFO 13747 --- [ main] beddedDataSourceBeanFactoryPostProcessor : Replacing 'dataSource' DataSource bean with embedded version
|
||||
2026-10-04T01:54:16.314+05:30 INFO 13747 --- [ main] o.s.j.d.e.EmbeddedDatabaseFactory : Starting embedded database: url='jdbc:h2:mem:586d3997-ff00-4f61-9011-c28e820e265a;DB_CLOSE_DELAY=-1;DB_CLOSE_ON_EXIT=false', username='sa'
|
||||
2026-10-04T01:54:16.618+05:30 INFO 13747 --- [ main] org.hibernate.orm.jpa : HHH008540: Processing PersistenceUnitInfo [name: default]
|
||||
2026-10-04T01:54:16.662+05:30 INFO 13747 --- [ main] org.hibernate.orm.core : HHH000001: Hibernate ORM core version 7.4.5.Final
|
||||
2026-10-04T01:54:16.967+05:30 INFO 13747 --- [ main] o.s.o.j.p.SpringPersistenceUnitInfo : No LoadTimeWeaver setup: ignoring JPA class transformer
|
||||
2026-10-04T01:54:17.033+05:30 INFO 13747 --- [ main] org.hibernate.orm.connections.pooling : HHH10001005: Database info:
|
||||
Database JDBC URL [jdbc:h2:mem:586d3997-ff00-4f61-9011-c28e820e265a]
|
||||
Database driver: H2 JDBC Driver
|
||||
Database dialect: H2Dialect
|
||||
Database version: 2.4.240
|
||||
Default catalog/schema: 586D3997-FF00-4F61-9011-C28E820E265A/PUBLIC
|
||||
Autocommit mode: undefined/unknown
|
||||
Isolation level: READ_COMMITTED [default READ_COMMITTED]
|
||||
JDBC fetch size: 100
|
||||
Pool: DataSourceConnectionProvider
|
||||
Minimum pool size: undefined/unknown
|
||||
Maximum pool size: undefined/unknown
|
||||
2026-10-04T01:54:17.583+05:30 INFO 13747 --- [ main] org.hibernate.orm.core : HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration)
|
||||
Hibernate: drop table if exists wallets cascade
|
||||
Hibernate: create table wallets (balance_cents bigint not null, id varchar(255) not null, primary key (id))
|
||||
2026-10-04T01:54:17.609+05:30 INFO 13747 --- [ main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
|
||||
2026-10-04T01:54:17.712+05:30 INFO 13747 --- [ main] o.s.d.j.r.query.QueryEnhancerFactories : Hibernate is in classpath; If applicable, HQL parser will be used.
|
||||
2026-10-04T01:54:17.819+05:30 INFO 13747 --- [ main] c.a.h.p.JpaWalletRepositoryAdapterTest : Started JpaWalletRepositoryAdapterTest in 2.324 seconds (process running for 3.492)
|
||||
Mockito is currently self-attaching to enable the inline-mock-maker. This will no longer work in future releases of the JDK. Please add Mockito as an agent to your build as described in Mockito's documentation: https://javadoc.io/doc/org.mockito/mockito-core/latest/org.mockito/org/mockito/Mockito.html#0.3
|
||||
OpenJDK 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended
|
||||
Hibernate: select we1_0.id,we1_0.balance_cents from wallets we1_0 where we1_0.id=?
|
||||
Hibernate: select we1_0.id,we1_0.balance_cents from wallets we1_0 where we1_0.id=?
|
||||
Reloaded wallet w1 balance from H2: 2500
|
||||
Hibernate: select we1_0.id,we1_0.balance_cents from wallets we1_0 where we1_0.id=?
|
||||
findById("ghost").isPresent(): false
|
||||
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0, Time elapsed: 3.284 s -- in com.ankurm.hexagonal.persistence.JpaWalletRepositoryAdapterTest
|
||||
2026-10-04T01:54:18.443+05:30 INFO 13747 --- [ionShutdownHook] j.LocalContainerEntityManagerFactoryBean : Closing JPA EntityManagerFactory for persistence unit 'default'
|
||||
Hibernate: drop table if exists wallets cascade
|
||||
[INFO]
|
||||
[INFO] Results:
|
||||
[INFO]
|
||||
[INFO] Tests run: 2, Failures: 0, Errors: 0, Skipped: 0
|
||||
[INFO]
|
||||
[INFO] ------------------------------------------------------------------------
|
||||
[INFO] BUILD SUCCESS
|
||||
[INFO] ------------------------------------------------------------------------
|
||||
[INFO] Total time: 6.595 s
|
||||
[INFO] Finished at: 2026-10-04T01:54:18+05:30
|
||||
[INFO] ------------------------------------------------------------------------
|
||||
@@ -0,0 +1,47 @@
|
||||
<project xmlns="http://maven.apache.org/POM/4.0.0">
|
||||
<modelVersion>4.0.0</modelVersion>
|
||||
|
||||
<parent>
|
||||
<groupId>com.ankurm</groupId>
|
||||
<artifactId>hexagonal-parent</artifactId>
|
||||
<version>1.0</version>
|
||||
</parent>
|
||||
|
||||
<artifactId>hexagonal-persistence-adapter</artifactId>
|
||||
<packaging>jar</packaging>
|
||||
|
||||
<!-- This is the only module allowed to know a database exists. It depends on hexagonal-core
|
||||
to implement WalletRepository, and on Spring Data JPA to do so for real. -->
|
||||
<dependencies>
|
||||
<dependency>
|
||||
<groupId>com.ankurm</groupId>
|
||||
<artifactId>hexagonal-core</artifactId>
|
||||
<version>1.0</version>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-data-jpa</artifactId>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>com.h2database</groupId>
|
||||
<artifactId>h2</artifactId>
|
||||
<scope>runtime</scope>
|
||||
</dependency>
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-starter-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
<!-- Spring Boot 4 split @DataJpaTest out of spring-boot-test-autoconfigure into its own
|
||||
artifact, alongside the rest of the data/jpa/web slices. Confirmed by unzipping
|
||||
spring-boot-test-autoconfigure-4.1.1.jar (no "jpa" class anywhere in it) and then
|
||||
spring-boot-data-jpa-test-4.1.1.jar, which has
|
||||
org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest - a new package too,
|
||||
not the old org.springframework.boot.test.autoconfigure.orm.jpa. -->
|
||||
<dependency>
|
||||
<groupId>org.springframework.boot</groupId>
|
||||
<artifactId>spring-boot-data-jpa-test</artifactId>
|
||||
<scope>test</scope>
|
||||
</dependency>
|
||||
</dependencies>
|
||||
</project>
|
||||
+38
@@ -0,0 +1,38 @@
|
||||
package com.ankurm.hexagonal.persistence;
|
||||
|
||||
import java.util.Optional;
|
||||
|
||||
import com.ankurm.hexagonal.domain.Wallet;
|
||||
import com.ankurm.hexagonal.ports.WalletRepository;
|
||||
|
||||
import org.springframework.stereotype.Repository;
|
||||
|
||||
/**
|
||||
* The real implementation of the core's outbound port -- the only class in this whole
|
||||
* application that translates between the domain's {@link Wallet} and JPA's
|
||||
* {@link WalletEntity}. If the persistence technology changed to MongoDB tomorrow, this is
|
||||
* the only class that would need to change; {@code WalletService} would not even recompile
|
||||
* differently, because it has never seen this class.
|
||||
*/
|
||||
@Repository
|
||||
public class JpaWalletRepositoryAdapter implements WalletRepository {
|
||||
|
||||
private final SpringDataWalletRepository jpa;
|
||||
|
||||
public JpaWalletRepositoryAdapter(SpringDataWalletRepository jpa) {
|
||||
this.jpa = jpa;
|
||||
}
|
||||
|
||||
@Override
|
||||
public Optional<Wallet> findById(String walletId) {
|
||||
return jpa.findById(walletId).map(entity -> new Wallet(entity.getId(), entity.getBalanceCents()));
|
||||
}
|
||||
|
||||
@Override
|
||||
public Wallet save(Wallet wallet) {
|
||||
WalletEntity entity = jpa.findById(wallet.id()).orElseGet(() -> new WalletEntity(wallet.id(), 0));
|
||||
entity.setBalanceCents(wallet.balanceCents());
|
||||
jpa.save(entity);
|
||||
return wallet;
|
||||
}
|
||||
}
|
||||
+6
@@ -0,0 +1,6 @@
|
||||
package com.ankurm.hexagonal.persistence;
|
||||
|
||||
import org.springframework.data.jpa.repository.JpaRepository;
|
||||
|
||||
interface SpringDataWalletRepository extends JpaRepository<WalletEntity, String> {
|
||||
}
|
||||
+39
@@ -0,0 +1,39 @@
|
||||
package com.ankurm.hexagonal.persistence;
|
||||
|
||||
import jakarta.persistence.Entity;
|
||||
import jakarta.persistence.Id;
|
||||
import jakarta.persistence.Table;
|
||||
|
||||
/** The JPA shape of a wallet. Deliberately a separate class from the domain's
|
||||
* {@code Wallet} -- this class exists to satisfy Hibernate (a no-arg constructor, mutable
|
||||
* fields), and the domain class exists to satisfy the business rules. Mixing the two would
|
||||
* mean every persistence concern leaks into the one class the core's tests are supposed to
|
||||
* be free of. */
|
||||
@Entity
|
||||
@Table(name = "wallets")
|
||||
public class WalletEntity {
|
||||
|
||||
@Id
|
||||
private String id;
|
||||
private long balanceCents;
|
||||
|
||||
protected WalletEntity() {
|
||||
}
|
||||
|
||||
public WalletEntity(String id, long balanceCents) {
|
||||
this.id = id;
|
||||
this.balanceCents = balanceCents;
|
||||
}
|
||||
|
||||
public String getId() {
|
||||
return id;
|
||||
}
|
||||
|
||||
public long getBalanceCents() {
|
||||
return balanceCents;
|
||||
}
|
||||
|
||||
public void setBalanceCents(long balanceCents) {
|
||||
this.balanceCents = balanceCents;
|
||||
}
|
||||
}
|
||||
+40
@@ -0,0 +1,40 @@
|
||||
package com.ankurm.hexagonal.persistence;
|
||||
|
||||
import com.ankurm.hexagonal.domain.Wallet;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.data.jpa.test.autoconfigure.DataJpaTest;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/** A real H2 database, a real JPA EntityManager, and the adapter under test -- proving the
|
||||
* port's contract against something that actually persists, not a guess at what Hibernate
|
||||
* does. */
|
||||
@DataJpaTest
|
||||
class JpaWalletRepositoryAdapterTest {
|
||||
|
||||
@Autowired
|
||||
SpringDataWalletRepository jpa;
|
||||
|
||||
@Test
|
||||
void savingThenFindingRoundTripsThroughTheRealDatabase() {
|
||||
JpaWalletRepositoryAdapter adapter = new JpaWalletRepositoryAdapter(jpa);
|
||||
|
||||
adapter.save(new Wallet("w1", 2500));
|
||||
Wallet reloaded = adapter.findById("w1").orElseThrow();
|
||||
|
||||
System.out.println("Reloaded wallet w1 balance from H2: " + reloaded.balanceCents());
|
||||
assertThat(reloaded.balanceCents()).isEqualTo(2500);
|
||||
}
|
||||
|
||||
@Test
|
||||
void findingAnUnknownIdReturnsEmpty() {
|
||||
JpaWalletRepositoryAdapter adapter = new JpaWalletRepositoryAdapter(jpa);
|
||||
|
||||
boolean present = adapter.findById("ghost").isPresent();
|
||||
|
||||
System.out.println("findById(\"ghost\").isPresent(): " + present);
|
||||
assertThat(present).isFalse();
|
||||
}
|
||||
}
|
||||
+10
@@ -0,0 +1,10 @@
|
||||
package com.ankurm.hexagonal.persistence;
|
||||
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
|
||||
/** Test-only bootstrap class so @DataJpaTest has a @SpringBootConfiguration to find in this
|
||||
* package. This module has no real @SpringBootApplication of its own -- that belongs to the
|
||||
* app module, which is the only place all the adapters actually get wired together. */
|
||||
@SpringBootApplication
|
||||
class TestApplication {
|
||||
}
|
||||
Reference in New Issue
Block a user