Example application for structuring a Spring Boot app

This commit is contained in:
Tom Hombergs
2018-05-27 20:55:24 +02:00
parent f2b83425ac
commit a4d70d5835
30 changed files with 750 additions and 77 deletions

View File

@@ -0,0 +1,13 @@
package io.reflectoring;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}

View File

@@ -1,18 +1,32 @@
package io.reflectoring.booking;
import io.reflectoring.booking.business.BookingService;
import org.springframework.context.annotation.Bean;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import io.reflectoring.booking.data.BookingRepository;
import io.reflectoring.customer.CustomerRepository;
import io.reflectoring.flight.FlightRepository;
import io.reflectoring.customer.CustomerConfiguration;
import io.reflectoring.customer.data.CustomerRepository;
import io.reflectoring.flight.FlightConfiguration;
import io.reflectoring.flight.data.FlightService;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Import;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.web.servlet.config.annotation.EnableWebMvc;
@EnableJpaRepositories("re")
@SpringBootConfiguration
@Import({CustomerConfiguration.class, FlightConfiguration.class})
@EnableAutoConfiguration
@ComponentScan
public class BookingConfiguration {
@Bean
public BookingService bookingService(BookingRepository bookingRepository, CustomerRepository customerRepository, FlightRepository flightRepository) {
return new BookingService(bookingRepository, customerRepository, flightRepository);
public BookingService bookingService(
BookingRepository bookingRepository,
CustomerRepository customerRepository,
FlightService flightService) {
return new BookingService(bookingRepository, customerRepository, flightService);
}
}

View File

@@ -4,10 +4,10 @@ import java.util.Optional;
import io.reflectoring.booking.data.Booking;
import io.reflectoring.booking.data.BookingRepository;
import io.reflectoring.customer.Customer;
import io.reflectoring.customer.CustomerRepository;
import io.reflectoring.flight.Flight;
import io.reflectoring.flight.FlightRepository;
import io.reflectoring.customer.data.Customer;
import io.reflectoring.customer.data.CustomerRepository;
import io.reflectoring.flight.data.Flight;
import io.reflectoring.flight.data.FlightService;
public class BookingService {
@@ -15,35 +15,35 @@ public class BookingService {
private CustomerRepository customerRepository;
private FlightRepository flightRepository;
FlightService flightService;
public BookingService(
BookingRepository bookingRepository,
CustomerRepository customerRepository,
FlightRepository flightRepository) {
FlightService flightService) {
this.bookingRepository = bookingRepository;
this.customerRepository = customerRepository;
this.flightRepository = flightRepository;
this.flightService = flightService;
}
/**
* Books the given flight for the given customer.
*/
public Booking bookFlight(Long customerId, Long flightId) {
public Booking bookFlight(Long customerId, String flightNumber) {
Optional<Customer> customer = customerRepository.findById(customerId);
if (!customer.isPresent()) {
throw new CustomerDoesNotExistException(customerId);
}
Optional<Flight> flight = flightRepository.findById(flightId);
Optional<Flight> flight = flightService.findFlight(flightNumber);
if (!flight.isPresent()) {
throw new FlightDoesNotExistException(flightId);
throw new FlightDoesNotExistException(flightNumber);
}
Booking booking = Booking.builder()
.customer(customer.get())
.flight(flight.get())
.flightNumber(flight.get().getFlightNumber())
.build();
return this.bookingRepository.save(booking);

View File

@@ -2,8 +2,8 @@ package io.reflectoring.booking.business;
class FlightDoesNotExistException extends RuntimeException {
FlightDoesNotExistException(Long flightId) {
super(String.format("A flight with ID '%d' doesn't exist!", flightId));
FlightDoesNotExistException(String flightNumber) {
super(String.format("A flight with ID '%d' doesn't exist!", flightNumber));
}
}

View File

@@ -1,12 +1,9 @@
package io.reflectoring.booking.data;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.ManyToOne;
import javax.persistence.*;
import io.reflectoring.customer.Customer;
import io.reflectoring.flight.Flight;
import io.reflectoring.customer.data.Customer;
import io.reflectoring.flight.data.Flight;
import lombok.Builder;
import lombok.Data;
@@ -22,7 +19,7 @@ public class Booking {
@ManyToOne
private Customer customer;
@ManyToOne
private Flight flight;
@Column
private String flightNumber;
}

View File

@@ -1,11 +1,11 @@
package io.reflectoring.booking.web;
import io.reflectoring.booking.business.BookingService;
import io.reflectoring.booking.data.Booking;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import io.reflectoring.booking.data.Booking;
@RestController
public class BookingController {
@@ -19,19 +19,12 @@ public class BookingController {
@PostMapping("/booking")
public ResponseEntity<BookingResultResource> bookFlight(
@RequestParam("customerId") Long customerId,
@RequestParam("flightId") Long flightId) {
try {
Booking booking = bookingService.bookFlight(customerId, flightId);
BookingResultResource bookingResult = BookingResultResource.builder()
.success(true)
.build();
return ResponseEntity.ok(bookingResult);
} catch (Exception e) {
BookingResultResource bookingResult = BookingResultResource.builder()
.success(false)
.build();
return ResponseEntity.badRequest().body(bookingResult);
}
@RequestParam("flightNumber") String flightNumber) {
Booking booking = bookingService.bookFlight(customerId, flightNumber);
BookingResultResource bookingResult = BookingResultResource.builder()
.success(true)
.build();
return ResponseEntity.ok(bookingResult);
}
}

View File

@@ -0,0 +1,14 @@
package io.reflectoring.customer;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan
public class CustomerConfiguration {
}

View File

@@ -1,15 +1,16 @@
package io.reflectoring.customer;
package io.reflectoring.customer.data;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import lombok.Builder;
import lombok.Data;
import lombok.*;
@Entity
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Customer {
@Id

View File

@@ -1,6 +1,11 @@
package io.reflectoring.customer;
package io.reflectoring.customer.data;
import java.util.List;
import org.springframework.data.repository.CrudRepository;
public interface CustomerRepository extends CrudRepository<Customer, Long> {
List<Customer> findByName(String name);
}

View File

@@ -1,23 +0,0 @@
package io.reflectoring.flight;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import lombok.Builder;
import lombok.Data;
@Entity
@Data
@Builder
public class Flight {
@Id
@GeneratedValue
private Long id;
private String flightNumber;
private String airline;
}

View File

@@ -0,0 +1,23 @@
package io.reflectoring.flight;
import io.reflectoring.flight.data.FlightService;
import org.springframework.boot.SpringBootConfiguration;
import org.springframework.boot.autoconfigure.EnableAutoConfiguration;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.jpa.repository.config.EnableJpaRepositories;
import org.springframework.scheduling.annotation.EnableScheduling;
@SpringBootConfiguration
@EnableAutoConfiguration
@EnableScheduling
@ComponentScan
public class FlightConfiguration {
@Bean
public FlightService flightService(){
return new FlightService();
}
}

View File

@@ -1,6 +0,0 @@
package io.reflectoring.flight;
import org.springframework.data.repository.CrudRepository;
public interface FlightRepository extends CrudRepository<Flight, Long> {
}

View File

@@ -0,0 +1,14 @@
package io.reflectoring.flight.data;
import lombok.Builder;
import lombok.Data;
@Data
@Builder
public class Flight {
private String flightNumber;
private String airline;
}

View File

@@ -0,0 +1,14 @@
package io.reflectoring.flight.data;
import java.util.Optional;
public class FlightService {
public Optional<Flight> findFlight(String flightNumber) {
return Optional.of(Flight.builder()
.airline("Oceanic")
.flightNumber("815")
.build());
}
}

View File

@@ -0,0 +1,16 @@
package io.reflectoring;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
@ExtendWith(SpringExtension.class)
@SpringBootTest
class ApplicationTests {
@Test
void applicationContextLoads() {
}
}

View File

@@ -0,0 +1,32 @@
package io.reflectoring.booking;
import java.util.Arrays;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.context.ApplicationContext;
import org.springframework.test.context.junit.jupiter.SpringExtension;
@ExtendWith(SpringExtension.class)
@SpringBootTest(classes = BookingConfiguration.class)
class BookingConfigurationTest {
@Autowired
private ApplicationContext applicationContext;
@BeforeEach
void printApplicationContext() {
Arrays.stream(applicationContext.getBeanDefinitionNames())
.map(name -> applicationContext.getBean(name).getClass().getName())
.sorted()
.forEach(System.out::println);
}
@Test
void bookingConfigurationLoads() {
}
}

View File

@@ -0,0 +1,49 @@
package io.reflectoring.booking;
import io.reflectoring.customer.data.Customer;
import io.reflectoring.customer.data.CustomerRepository;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;
import org.springframework.web.context.WebApplicationContext;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@ExtendWith(SpringExtension.class)
@SpringBootTest
class BookingIntegrationTest {
@Autowired
private WebApplicationContext applicationContext;
@Autowired
private CustomerRepository customerRepository;
private MockMvc mockMvc;
@BeforeEach
void setup() {
this.mockMvc = MockMvcBuilders
.webAppContextSetup(applicationContext)
.build();
}
@Test
void bookFlightReturnsHttpStatusOk() throws Exception {
this.customerRepository.save(Customer.builder()
.name("Hurley")
.build());
this.mockMvc.perform(
post("/booking")
.param("customerId", "1")
.param("flightNumber", "Oceanic 815"))
.andExpect(status().isOk());
}
}

View File

@@ -0,0 +1,67 @@
package io.reflectoring.booking.business;
import java.util.Optional;
import io.reflectoring.booking.data.Booking;
import io.reflectoring.booking.data.BookingRepository;
import io.reflectoring.customer.data.Customer;
import io.reflectoring.customer.data.CustomerRepository;
import io.reflectoring.flight.data.Flight;
import io.reflectoring.flight.data.FlightService;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.mockito.Mockito;
import static org.assertj.core.api.Assertions.assertThat;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.verify;
import static org.mockito.Mockito.when;
class BookingServiceTest {
private CustomerRepository customerRepository = Mockito.mock(CustomerRepository.class);
private FlightService flightService = Mockito.mock(FlightService.class);
private BookingRepository bookingRepository = Mockito.mock(BookingRepository.class);
private BookingService bookingService;
@BeforeEach
void setup() {
this.bookingService = new BookingService(bookingRepository, customerRepository, flightService);
}
@Test
void bookFlightReturnsBooking() {
when(customerRepository.findById(42L)).thenReturn(customer());
when(flightService.findFlight("Oceanic 815")).thenReturn(flight());
when(bookingRepository.save(eq(booking()))).thenReturn(booking());
Booking booking = bookingService.bookFlight(42L, "Oceanic 815");
assertThat(booking).isNotNull();
verify(bookingRepository).save(eq(booking));
}
private Optional<Flight> flight() {
return Optional.of(Flight.builder()
.flightNumber("Oceanic 815")
.airline("Oceanic")
.build());
}
private Booking booking() {
return Booking.builder()
.flightNumber("Oceanic 815")
.customer(customer().get())
.build();
}
private Optional<Customer> customer() {
return Optional.of(Customer.builder()
.id(42L)
.name("Hurley")
.build());
}
}

View File

@@ -0,0 +1,67 @@
package io.reflectoring.booking.web;
import java.util.Arrays;
import io.reflectoring.booking.business.BookingService;
import io.reflectoring.booking.data.Booking;
import io.reflectoring.customer.data.Customer;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.web.servlet.WebMvcTest;
import org.springframework.boot.test.mock.mockito.MockBean;
import org.springframework.context.ApplicationContext;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import org.springframework.test.web.servlet.MockMvc;
import static org.mockito.ArgumentMatchers.eq;
import static org.mockito.Mockito.when;
import static org.springframework.test.web.servlet.request.MockMvcRequestBuilders.post;
import static org.springframework.test.web.servlet.result.MockMvcResultMatchers.status;
@ExtendWith(SpringExtension.class)
@WebMvcTest(controllers = BookingController.class)
class BookingControllerTest {
@Autowired
private MockMvc mockMvc;
@Autowired
private ApplicationContext applicationContext;
@MockBean
private BookingService bookingService;
@BeforeEach
void printApplicationContext() {
Arrays.stream(applicationContext.getBeanDefinitionNames())
.map(name -> applicationContext.getBean(name).getClass().getName())
.sorted()
.forEach(System.out::println);
}
@Test
void bookFlightReturnsHttpStatusOk() throws Exception {
when(bookingService.bookFlight(eq(42L), eq("Oceanic 815")))
.thenReturn(expectedBooking());
mockMvc.perform(
post("/booking")
.param("customerId", "42")
.param("flightNumber", "Oceanic 815"))
.andExpect(status().isOk());
}
private Booking expectedBooking() {
return Booking.builder()
.customer(Customer.builder()
.id(42L)
.name("Zaphod")
.build())
.flightNumber("Oceanic 815")
.build();
}
}

View File

@@ -0,0 +1,16 @@
package io.reflectoring.customer;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.context.junit.jupiter.SpringExtension;
@ExtendWith(SpringExtension.class)
@SpringBootTest(classes = CustomerConfiguration.class)
class CustomerConfigurationTest {
@Test
void bookingConfigurationLoads() {
}
}

View File

@@ -0,0 +1,44 @@
package io.reflectoring.customer.data;
import java.util.Arrays;
import java.util.List;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.extension.ExtendWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.autoconfigure.orm.jpa.DataJpaTest;
import org.springframework.context.ApplicationContext;
import org.springframework.test.context.junit.jupiter.SpringExtension;
import static org.assertj.core.api.Assertions.assertThat;
@ExtendWith(SpringExtension.class)
@DataJpaTest
class CustomerRepositoryTest {
@Autowired
private CustomerRepository repository;
@Autowired
private ApplicationContext applicationContext;
@BeforeEach
void printApplicationContext() {
Arrays.stream(applicationContext.getBeanDefinitionNames())
.map(name -> applicationContext.getBean(name).getClass().getName())
.sorted()
.forEach(System.out::println);
}
@Test
void findsByName() {
Customer customer = Customer.builder()
.name("Hurley")
.build();
repository.save(customer);
List<Customer> foundCustomers = repository.findByName("Hurley");
assertThat(foundCustomers).hasSize(1);
}
}

View File

@@ -0,0 +1,4 @@
package io.reflectoring.flight;
public class FlightMockConfiguration {
}