Skip to content

Commit

Permalink
Better example code for Spring Boot
Browse files Browse the repository at this point in the history
  • Loading branch information
badgerwithagun committed Dec 25, 2023
1 parent 386a751 commit 7e6a0cb
Show file tree
Hide file tree
Showing 20 changed files with 259 additions and 174 deletions.
12 changes: 12 additions & 0 deletions transactionoutbox-spring/pom.xml
Original file line number Diff line number Diff line change
Expand Up @@ -54,6 +54,12 @@
<artifactId>lombok</artifactId>
</dependency>
<!-- Test -->
<dependency>
<groupId>com.gruelbox</groupId>
<artifactId>transactionoutbox-jackson</artifactId>
<scope>test</scope>
<version>${project.version}</version>
</dependency>
<dependency>
<groupId>ch.qos.logback</groupId>
<artifactId>logback-classic</artifactId>
Expand Down Expand Up @@ -100,5 +106,11 @@
<groupId>org.junit.jupiter</groupId>
<artifactId>junit-jupiter-params</artifactId>
</dependency>
<dependency>
<groupId>org.awaitility</groupId>
<artifactId>awaitility</artifactId>
<version>4.2.0</version>
<scope>test</scope>
</dependency>
</dependencies>
</project>
Original file line number Diff line number Diff line change
Expand Up @@ -3,6 +3,10 @@
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

/**
* @deprecated Just {@code @Import} the components you need.
*/
@Configuration
@Deprecated(forRemoval = true)
@Import({SpringTransactionManager.class, SpringInstantiator.class})
public class SpringTransactionOutboxConfiguration {}

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

This file was deleted.

Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.gruelbox.transactionoutbox.spring.acceptance;
package com.gruelbox.transactionoutbox.spring.example;

import jakarta.persistence.Column;
import jakarta.persistence.Entity;
Expand Down
Original file line number Diff line number Diff line change
@@ -1,4 +1,4 @@
package com.gruelbox.transactionoutbox.spring.acceptance;
package com.gruelbox.transactionoutbox.spring.example;

import org.springframework.data.repository.CrudRepository;
import org.springframework.stereotype.Repository;
Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,37 @@
package com.gruelbox.transactionoutbox.spring.example;

import static org.springframework.http.HttpStatus.NOT_FOUND;

import com.gruelbox.transactionoutbox.TransactionOutbox;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.server.ResponseStatusException;

@SuppressWarnings("unused")
@RestController
class EventuallyConsistentController {

private static final Logger LOGGER =
LoggerFactory.getLogger(EventuallyConsistentController.class);

@Autowired private CustomerRepository customerRepository;
@Autowired private TransactionOutbox outbox;

@SuppressWarnings("SameReturnValue")
@PostMapping(path = "/customer")
@Transactional
public void createCustomer(@RequestBody Customer customer) {
customerRepository.save(customer);
outbox.schedule(ExternalQueueService.class).sendCustomerCreatedEvent(customer);
}

@GetMapping("/customer/{id}")
public Customer getCustomer(@PathVariable long id) {
return customerRepository
.findById(id)
.orElseThrow(() -> new ResponseStatusException(NOT_FOUND));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,64 @@
package com.gruelbox.transactionoutbox.spring.example;

import static java.util.concurrent.TimeUnit.SECONDS;
import static org.assertj.core.api.Assertions.assertThat;
import static org.awaitility.Awaitility.await;
import static org.junit.jupiter.api.Assertions.assertTrue;

import java.net.URL;
import javax.inject.Inject;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.boot.test.web.client.TestRestTemplate;
import org.springframework.boot.test.web.server.LocalServerPort;

@SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
class EventuallyConsistentControllerTest {

@SuppressWarnings("unused")
@LocalServerPort
private int port;

private URL base;

@SuppressWarnings("unused")
@Inject
private TestRestTemplate template;

@Inject private ExternalQueueService externalQueueService;

@BeforeEach
void setUp() throws Exception {
this.base = new URL("http://localhost:" + port + "/");
}

@Test
void testCheck() throws Exception {

var joe = new Customer(1L, "Joe", "Strummer");
var dave = new Customer(2L, "Dave", "Grohl");
var neil = new Customer(3L, "Neil", "Diamond");
var tupac = new Customer(4L, "Tupac", "Shakur");
var jeff = new Customer(5L, "Jeff", "Mills");
var customer = base.toString() + "/customer";

assertTrue(template.postForEntity(customer, joe, Void.class).getStatusCode().is2xxSuccessful());
assertTrue(
template.postForEntity(customer, dave, Void.class).getStatusCode().is2xxSuccessful());
assertTrue(
template.postForEntity(customer, neil, Void.class).getStatusCode().is2xxSuccessful());
assertTrue(
template.postForEntity(customer, tupac, Void.class).getStatusCode().is2xxSuccessful());
assertTrue(
template.postForEntity(customer, jeff, Void.class).getStatusCode().is2xxSuccessful());

await()
.atMost(10, SECONDS)
.pollDelay(1, SECONDS)
.untilAsserted(
() ->
assertThat(externalQueueService.getSent())
.containsExactlyInAnyOrder(joe, dave, neil, tupac, jeff));
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,24 @@
package com.gruelbox.transactionoutbox.spring.example;

import lombok.Getter;
import org.springframework.stereotype.Service;

import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.CopyOnWriteArrayList;

@Getter
@Service
class ExternalQueueService {

private final Set<Long> attempted = new HashSet<>();
private final List<Customer> sent = new CopyOnWriteArrayList<>();

void sendCustomerCreatedEvent(Customer customer) {
if (attempted.add(customer.getId())) {
throw new RuntimeException("Temporary failure, try again");
}
sent.add(customer);
}
}
Original file line number Diff line number Diff line change
@@ -0,0 +1,10 @@
package com.gruelbox.transactionoutbox.spring.example;

import com.gruelbox.transactionoutbox.spring.SpringInstantiator;
import com.gruelbox.transactionoutbox.spring.SpringTransactionManager;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;

@Configuration
@Import({SpringInstantiator.class, SpringTransactionManager.class})
class ExternalsConfiguration {}
Original file line number Diff line number Diff line change
@@ -0,0 +1,31 @@
package com.gruelbox.transactionoutbox.spring.example;

import com.gruelbox.transactionoutbox.TransactionOutbox;
import lombok.RequiredArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

/**
* Simple implementation of a background processor for {@link TransactionOutbox}. You don't need to
* use this if you need different semantics, but this is a good start for most purposes.
*/
@Component
@Slf4j
@RequiredArgsConstructor(onConstructor_ = {@Autowired})
class TransactionOutboxBackgroundProcessor {

private final TransactionOutbox outbox;

@Scheduled(fixedRateString = "${outbox.repeatEvery}")
void poll() {
try {
do {
log.info("Flushing");
} while (outbox.flush());
} catch (Exception e) {
log.error("Error flushing transaction outbox. Pausing", e);
}
}
}
Loading

0 comments on commit 7e6a0cb

Please sign in to comment.