Add the hexagonal module: Hexagonal Architecture (Ports and Adapters) in Spring Boot 4
This commit is contained in:
@@ -0,0 +1,39 @@
|
||||
package com.ankurm.hexagonal.app;
|
||||
|
||||
import com.ankurm.hexagonal.WalletService;
|
||||
import com.ankurm.hexagonal.ports.WalletRepository;
|
||||
|
||||
import org.springframework.boot.SpringApplication;
|
||||
import org.springframework.boot.autoconfigure.SpringBootApplication;
|
||||
import org.springframework.boot.persistence.autoconfigure.EntityScan;
|
||||
import org.springframework.context.annotation.Bean;
|
||||
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
|
||||
|
||||
/**
|
||||
* The composition root. Everything above this point in the article -- the domain, the ports,
|
||||
* both adapters -- is wired together in exactly one place, and this is it.
|
||||
* {@code WalletService} carries no {@code @Service} annotation, so without the {@code @Bean}
|
||||
* method below, Spring would never construct it; the wiring is explicit, not discovered.
|
||||
*
|
||||
* <p>{@code scanBasePackages} covers {@code @Component}-style scanning, but Spring Data JPA's
|
||||
* repository scan and JPA's entity scan each use their own default -- the package of this
|
||||
* class, {@code com.ankurm.hexagonal.app} -- not {@code scanBasePackages}. That default is a
|
||||
* sibling of {@code com.ankurm.hexagonal.persistence}, not an ancestor of it, so without the
|
||||
* two annotations below Spring Data JPA silently finds zero repositories and the
|
||||
* {@code JpaWalletRepositoryAdapter} bean fails to wire: see HexagonalApplicationTest's build
|
||||
* log for the "No qualifying bean of type SpringDataWalletRepository" failure this caused.
|
||||
*/
|
||||
@SpringBootApplication(scanBasePackages = "com.ankurm.hexagonal")
|
||||
@EnableJpaRepositories(basePackages = "com.ankurm.hexagonal.persistence")
|
||||
@EntityScan(basePackages = "com.ankurm.hexagonal.persistence")
|
||||
public class HexagonalApplication {
|
||||
|
||||
public static void main(String[] args) {
|
||||
SpringApplication.run(HexagonalApplication.class, args);
|
||||
}
|
||||
|
||||
@Bean
|
||||
public WalletService walletService(WalletRepository walletRepository) {
|
||||
return new WalletService(walletRepository);
|
||||
}
|
||||
}
|
||||
@@ -0,0 +1,3 @@
|
||||
spring.application.name=hexagonal
|
||||
spring.datasource.url=jdbc:h2:mem:hexagonaldb;DB_CLOSE_DELAY=-1
|
||||
spring.jpa.hibernate.ddl-auto=update
|
||||
@@ -0,0 +1,80 @@
|
||||
package com.ankurm.hexagonal.app;
|
||||
|
||||
import java.util.UUID;
|
||||
|
||||
import com.ankurm.hexagonal.domain.Wallet;
|
||||
import com.ankurm.hexagonal.ports.WalletRepository;
|
||||
import com.ankurm.hexagonal.web.WalletController.BalanceResponse;
|
||||
|
||||
import org.junit.jupiter.api.Test;
|
||||
import org.springframework.beans.factory.annotation.Autowired;
|
||||
import org.springframework.boot.test.context.SpringBootTest;
|
||||
import org.springframework.boot.test.context.SpringBootTest.WebEnvironment;
|
||||
import org.springframework.boot.resttestclient.TestRestTemplate;
|
||||
import org.springframework.boot.resttestclient.autoconfigure.AutoConfigureTestRestTemplate;
|
||||
import org.springframework.http.HttpStatus;
|
||||
import org.springframework.http.ResponseEntity;
|
||||
import org.springframework.test.context.DynamicPropertyRegistry;
|
||||
import org.springframework.test.context.DynamicPropertySource;
|
||||
|
||||
import static org.assertj.core.api.Assertions.assertThat;
|
||||
|
||||
/**
|
||||
* The whole stack, really wired, really running: a real embedded web server, a real H2
|
||||
* database through the real persistence adapter, and HTTP requests that cross every
|
||||
* boundary this article draws. If any of the wiring in {@link HexagonalApplication} were
|
||||
* wrong, this is the test that would fail, not a unit test of any single class.
|
||||
*
|
||||
* <p>{@code @AutoConfigureTestRestTemplate} is required explicitly in Spring Boot 4 -- unlike
|
||||
* Boot 3, a {@code @SpringBootTest(webEnvironment = RANDOM_PORT)} no longer auto-configures a
|
||||
* {@code TestRestTemplate} bean on its own.
|
||||
*/
|
||||
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
|
||||
@AutoConfigureTestRestTemplate
|
||||
class HexagonalApplicationTest {
|
||||
|
||||
@Autowired
|
||||
TestRestTemplate rest;
|
||||
|
||||
@Autowired
|
||||
WalletRepository walletRepository;
|
||||
|
||||
@DynamicPropertySource
|
||||
static void datasource(DynamicPropertyRegistry registry) {
|
||||
registry.add("spring.datasource.url",
|
||||
() -> "jdbc:h2:mem:" + UUID.randomUUID() + ";DB_CLOSE_DELAY=-1");
|
||||
}
|
||||
|
||||
@Test
|
||||
void depositThenWithdrawOverHttpEndsAtTheRightBalanceThroughTheRealDatabase() {
|
||||
walletRepository.save(new Wallet("w1", 1000));
|
||||
|
||||
ResponseEntity<BalanceResponse> afterDeposit = rest.postForEntity(
|
||||
"/wallets/w1/deposit?amountCents=500", null, BalanceResponse.class);
|
||||
System.out.println("After deposit: " + afterDeposit.getStatusCode() + " " + afterDeposit.getBody());
|
||||
|
||||
ResponseEntity<BalanceResponse> afterWithdraw = rest.postForEntity(
|
||||
"/wallets/w1/withdraw?amountCents=200", null, BalanceResponse.class);
|
||||
System.out.println("After withdraw: " + afterWithdraw.getStatusCode() + " " + afterWithdraw.getBody());
|
||||
|
||||
assertThat(afterDeposit.getBody().balanceCents()).isEqualTo(1500);
|
||||
assertThat(afterWithdraw.getBody().balanceCents()).isEqualTo(1300);
|
||||
|
||||
// Reload straight from the real repository bean -- not from the HTTP response -- to
|
||||
// prove the number actually landed in H2, not just in the controller's return value.
|
||||
long persisted = walletRepository.findById("w1").orElseThrow().balanceCents();
|
||||
System.out.println("Balance reloaded directly from WalletRepository: " + persisted);
|
||||
assertThat(persisted).isEqualTo(1300);
|
||||
}
|
||||
|
||||
@Test
|
||||
void withdrawingTooMuchReturns409() {
|
||||
walletRepository.save(new Wallet("w2", 100));
|
||||
|
||||
ResponseEntity<String> response = rest.postForEntity(
|
||||
"/wallets/w2/withdraw?amountCents=101", null, String.class);
|
||||
System.out.println("Overdraft attempt: " + response.getStatusCode());
|
||||
|
||||
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.CONFLICT);
|
||||
}
|
||||
}
|
||||
Reference in New Issue
Block a user