diff --git a/README.md b/README.md index fd378e5d4..e2774f065 100644 --- a/README.md +++ b/README.md @@ -22,9 +22,9 @@ the URL for an event stream to monitor, for example the `api-gateway` service ru or running into docker: `http://api-gateway:8080/actuator/hystrix.stream` -You can tell Config Server to use your local Git repository by using `local` Spring profile and setting +You can tell Config Server to use your local Git repository by using `native` Spring profile and setting `GIT_REPO` environment variable, for example: -`-Dspring.profiles.active=local -DGIT_REPO=/projects/spring-petclinic-microservices-config` +`-Dspring.profiles.active=native -DGIT_REPO=/projects/spring-petclinic-microservices-config` ## Starting services locally with docker-compose In order to start entire infrastructure using Docker, you have to build images by executing `./mvnw clean install -PbuildDocker` diff --git a/pom.xml b/pom.xml index 38fe5a9fe..cd85814de 100644 --- a/pom.xml +++ b/pom.xml @@ -6,12 +6,12 @@ org.springframework.boot spring-boot-starter-parent - 2.1.4.RELEASE + 2.2.1.RELEASE org.springframework.samples spring-petclinic-microservices - 2.1.4 + 2.2.1 ${project.artifactId} pom @@ -30,8 +30,8 @@ 1.8 3.11.1 - 2.1.2.RELEASE - Greenwich.SR1 + 2.2.0.RELEASE + Hoxton.RC2 2.22.0 @@ -177,10 +177,24 @@ - repository.spring.milestone - Spring Milestone Repository - http://repo.spring.io/milestone + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + spring-milestones + Spring Milestones + https://repo.spring.io/milestone + + false + + + + diff --git a/spring-petclinic-admin-server/pom.xml b/spring-petclinic-admin-server/pom.xml index d27aa29af..0f9c746d2 100644 --- a/spring-petclinic-admin-server/pom.xml +++ b/spring-petclinic-admin-server/pom.xml @@ -12,7 +12,7 @@ org.springframework.samples spring-petclinic-microservices - 2.1.4 + 2.2.1 diff --git a/spring-petclinic-api-gateway/pom.xml b/spring-petclinic-api-gateway/pom.xml index be54a5828..7b66f6c21 100644 --- a/spring-petclinic-api-gateway/pom.xml +++ b/spring-petclinic-api-gateway/pom.xml @@ -11,7 +11,7 @@ org.springframework.samples spring-petclinic-microservices - 2.1.4 + 2.2.1 @@ -60,6 +60,20 @@ org.springframework.cloud spring-cloud-starter-netflix-eureka-client + + + org.springframework.cloud + spring-cloud-starter-netflix-ribbon + + + org.springframework.cloud + spring-cloud-netflix-ribbon + + + com.netflix.ribbon + ribbon-eureka + + org.springframework.cloud @@ -72,6 +86,12 @@ org.springframework.cloud spring-cloud-starter-netflix-hystrix + + + org.springframework.cloud + spring-cloud-netflix-ribbon + + @@ -125,6 +145,11 @@ junit-jupiter-engine test + + com.squareup.okhttp3 + mockwebserver + test + diff --git a/spring-petclinic-api-gateway/src/main/java/org/springframework/samples/petclinic/api/ApiGatewayApplication.java b/spring-petclinic-api-gateway/src/main/java/org/springframework/samples/petclinic/api/ApiGatewayApplication.java index b00c90ab1..1add5f1d8 100644 --- a/spring-petclinic-api-gateway/src/main/java/org/springframework/samples/petclinic/api/ApiGatewayApplication.java +++ b/spring-petclinic-api-gateway/src/main/java/org/springframework/samples/petclinic/api/ApiGatewayApplication.java @@ -26,6 +26,7 @@ import org.springframework.core.io.Resource; import org.springframework.http.MediaType; import org.springframework.web.client.RestTemplate; +import org.springframework.web.reactive.function.client.WebClient; import org.springframework.web.reactive.function.server.RequestPredicates; import org.springframework.web.reactive.function.server.RouterFunction; import org.springframework.web.reactive.function.server.RouterFunctions; @@ -50,6 +51,12 @@ RestTemplate loadBalancedRestTemplate() { return new RestTemplate(); } + @Bean + @LoadBalanced + public WebClient.Builder loadBalancedWebClientBuilder() { + return WebClient.builder(); + } + @Value("classpath:/static/index.html") private Resource indexHtml; @@ -61,7 +68,7 @@ RestTemplate loadBalancedRestTemplate() { RouterFunction routerFunction() { RouterFunction router = RouterFunctions.resources("/**", new ClassPathResource("static/")) .andRoute(RequestPredicates.GET("/"), - request -> ServerResponse.ok().contentType(MediaType.TEXT_HTML).syncBody(indexHtml)); + request -> ServerResponse.ok().contentType(MediaType.TEXT_HTML).bodyValue(indexHtml)); return router; } } diff --git a/spring-petclinic-api-gateway/src/main/java/org/springframework/samples/petclinic/api/application/CustomersServiceClient.java b/spring-petclinic-api-gateway/src/main/java/org/springframework/samples/petclinic/api/application/CustomersServiceClient.java index 2aaa6c674..0cb31c9b1 100644 --- a/spring-petclinic-api-gateway/src/main/java/org/springframework/samples/petclinic/api/application/CustomersServiceClient.java +++ b/spring-petclinic-api-gateway/src/main/java/org/springframework/samples/petclinic/api/application/CustomersServiceClient.java @@ -19,6 +19,8 @@ import org.springframework.samples.petclinic.api.dto.OwnerDetails; import org.springframework.stereotype.Component; import org.springframework.web.client.RestTemplate; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; /** * @author Maciej Szarlinski @@ -27,9 +29,12 @@ @RequiredArgsConstructor public class CustomersServiceClient { - private final RestTemplate loadBalancedRestTemplate; + private final WebClient.Builder webClientBuilder; - public OwnerDetails getOwner(final int ownerId) { - return loadBalancedRestTemplate.getForObject("http://customers-service/owners/{ownerId}", OwnerDetails.class, ownerId); + public Mono getOwner(final int ownerId) { + return webClientBuilder.build().get() + .uri("http://customers-service/owners/{ownerId}", ownerId) + .retrieve() + .bodyToMono(OwnerDetails.class); } } diff --git a/spring-petclinic-api-gateway/src/main/java/org/springframework/samples/petclinic/api/application/VisitsServiceClient.java b/spring-petclinic-api-gateway/src/main/java/org/springframework/samples/petclinic/api/application/VisitsServiceClient.java index 97a6af97d..8f9c392a9 100644 --- a/spring-petclinic-api-gateway/src/main/java/org/springframework/samples/petclinic/api/application/VisitsServiceClient.java +++ b/spring-petclinic-api-gateway/src/main/java/org/springframework/samples/petclinic/api/application/VisitsServiceClient.java @@ -15,22 +15,18 @@ */ package org.springframework.samples.petclinic.api.application; -import java.util.Collections; -import java.util.List; -import java.util.Map; - -import com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand; import lombok.RequiredArgsConstructor; import org.springframework.samples.petclinic.api.dto.VisitDetails; import org.springframework.samples.petclinic.api.dto.Visits; import org.springframework.stereotype.Component; -import org.springframework.web.client.RestTemplate; -import org.springframework.web.util.UriComponentsBuilder; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; +import java.util.Collections; +import java.util.List; +import java.util.Map; -import static java.util.stream.Collectors.groupingBy; import static java.util.stream.Collectors.joining; -import static org.springframework.web.util.UriComponentsBuilder.fromHttpUrl; /** * @author Maciej Szarlinski @@ -39,24 +35,29 @@ @RequiredArgsConstructor public class VisitsServiceClient { - private final RestTemplate loadBalancedRestTemplate; + // Could be changed for testing purpose + private String hostname = "http://visits-service/"; - @HystrixCommand(fallbackMethod = "emptyVisitsForPets") - public Map> getVisitsForPets(final List petIds) { - UriComponentsBuilder builder = fromHttpUrl("http://visits-service/pets/visits") - .queryParam("petId", joinIds(petIds)); + private final WebClient.Builder webClientBuilder; - return loadBalancedRestTemplate.getForObject(builder.toUriString(), Visits.class) - .getItems() - .stream() - .collect(groupingBy(VisitDetails::getPetId)); + // FIXME HYSTRIX @HystrixCommand(fallbackMethod = "emptyVisitsForPets") + public Mono getVisitsForPets(final List petIds) { + return webClientBuilder.build() + .get() + .uri(hostname + "pets/visits?petId={petId}", joinIds(petIds)) + .retrieve() + .bodyToMono(Visits.class); } private String joinIds(List petIds) { return petIds.stream().map(Object::toString).collect(joining(",")); } - private Map> emptyVisitsForPets(List petIds) { - return Collections.emptyMap(); + private Mono>> emptyVisitsForPets(Mono> petIds) { + return Mono.just(Collections.emptyMap()); + } + + void setHostname(String hostname) { + this.hostname = hostname; } } diff --git a/spring-petclinic-api-gateway/src/main/java/org/springframework/samples/petclinic/api/boundary/web/ApiGatewayController.java b/spring-petclinic-api-gateway/src/main/java/org/springframework/samples/petclinic/api/boundary/web/ApiGatewayController.java index 84232606a..6bc09a73b 100644 --- a/spring-petclinic-api-gateway/src/main/java/org/springframework/samples/petclinic/api/boundary/web/ApiGatewayController.java +++ b/spring-petclinic-api-gateway/src/main/java/org/springframework/samples/petclinic/api/boundary/web/ApiGatewayController.java @@ -15,21 +15,19 @@ */ package org.springframework.samples.petclinic.api.boundary.web; -import java.util.List; -import java.util.Map; -import java.util.Optional; import lombok.RequiredArgsConstructor; import org.springframework.samples.petclinic.api.application.CustomersServiceClient; -import org.springframework.samples.petclinic.api.dto.OwnerDetails; import org.springframework.samples.petclinic.api.application.VisitsServiceClient; -import org.springframework.samples.petclinic.api.dto.VisitDetails; +import org.springframework.samples.petclinic.api.dto.OwnerDetails; +import org.springframework.samples.petclinic.api.dto.Visits; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.PathVariable; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; +import reactor.core.publisher.Mono; - -import static java.util.Collections.emptyList; +import java.util.function.Function; +import java.util.stream.Collectors; /** * @author Maciej Szarlinski @@ -44,14 +42,24 @@ public class ApiGatewayController { private final VisitsServiceClient visitsServiceClient; @GetMapping(value = "owners/{ownerId}") - public OwnerDetails getOwnerDetails(final @PathVariable int ownerId) { - final OwnerDetails owner = customersServiceClient.getOwner(ownerId); - supplyVisits(owner, visitsServiceClient.getVisitsForPets(owner.getPetIds())); - return owner; + public Mono getOwnerDetails(final @PathVariable int ownerId) { + return customersServiceClient.getOwner(ownerId) + .flatMap(owner -> + visitsServiceClient.getVisitsForPets(owner.getPetIds()) + .map(addVisitsToOwner(owner)) + ); + } - private void supplyVisits(final OwnerDetails owner, final Map> visitsMapping) { - owner.getPets().forEach(pet -> - pet.getVisits().addAll(Optional.ofNullable(visitsMapping.get(pet.getId())).orElse(emptyList()))); + private Function addVisitsToOwner(OwnerDetails owner) { + return visits -> { + owner.getPets() + .forEach(pet -> pet.getVisits() + .addAll(visits.getItems().stream() + .filter(v -> v.getPetId() == pet.getId()) + .collect(Collectors.toList())) + ); + return owner; + }; } } diff --git a/spring-petclinic-api-gateway/src/main/resources/application.yml b/spring-petclinic-api-gateway/src/main/resources/application.yml index 44fb87003..74da17a05 100644 --- a/spring-petclinic-api-gateway/src/main/resources/application.yml +++ b/spring-petclinic-api-gateway/src/main/resources/application.yml @@ -1,5 +1,8 @@ spring: cloud: + loadbalancer: + ribbon: + enabled: false gateway: routes: - id: vets-service diff --git a/spring-petclinic-api-gateway/src/test/java/org/springframework/samples/petclinic/api/application/VisitsServiceClientIntegrationTest.java b/spring-petclinic-api-gateway/src/test/java/org/springframework/samples/petclinic/api/application/VisitsServiceClientIntegrationTest.java new file mode 100644 index 000000000..63387243d --- /dev/null +++ b/spring-petclinic-api-gateway/src/test/java/org/springframework/samples/petclinic/api/application/VisitsServiceClientIntegrationTest.java @@ -0,0 +1,78 @@ +package org.springframework.samples.petclinic.api.application; + +import okhttp3.mockwebserver.MockResponse; +import okhttp3.mockwebserver.MockWebServer; +import org.junit.jupiter.api.AfterEach; +import org.junit.jupiter.api.BeforeEach; +import org.junit.jupiter.api.Test; +import org.springframework.samples.petclinic.api.dto.Visits; +import org.springframework.web.reactive.function.client.WebClient; +import reactor.core.publisher.Mono; + +import java.io.IOException; +import java.util.Collections; +import java.util.function.Consumer; + +import static org.junit.jupiter.api.Assertions.assertEquals; +import static org.junit.jupiter.api.Assertions.assertNotNull; + +class VisitsServiceClientIntegrationTest { + + private static final Integer PET_ID = 1; + + private VisitsServiceClient visitsServiceClient; + + private MockWebServer server; + + @BeforeEach + void setUp() { + server = new MockWebServer(); + visitsServiceClient = new VisitsServiceClient(WebClient.builder()); + visitsServiceClient.setHostname(server.url("/").toString()); + } + + @AfterEach + void shutdown() throws IOException { + this.server.shutdown(); + } + + @Test + void getVisitsForPets_withAvailableVisitsService() { + prepareResponse(response -> response + .setHeader("Content-Type", "application/json") + .setBody("{\"items\":[{\"id\":5,\"date\":\"2018-11-15\",\"description\":\"test visit\",\"petId\":1}]}")); + + Mono visits = visitsServiceClient.getVisitsForPets(Collections.singletonList(1)); + + assertVisitDescriptionEquals(visits.block(), PET_ID,"test visit"); + } + + /** + * Test Hystrix fallback method + */ +// @Test +// public void getVisitsForPets_withServerError() { +// +// mockServer.expect(requestTo("http://visits-service/pets/visits?petId=1")) +// .andRespond(withServerError()); +// +// Map> visits = null; +// visitsServiceClient.getVisitsForPets(Mono.just(Collections.singletonList(1))); +// +// assertEquals(0, visits.size()); +// } + + private void assertVisitDescriptionEquals(Visits visits, int petId, String description) { + assertEquals(1, visits.getItems().size()); + assertNotNull(visits.getItems().get(0)); + assertEquals(petId, visits.getItems().get(0).getPetId()); + assertEquals(description, visits.getItems().get(0).getDescription()); + } + + private void prepareResponse(Consumer consumer) { + MockResponse response = new MockResponse(); + consumer.accept(response); + this.server.enqueue(response); + } + +} diff --git a/spring-petclinic-api-gateway/src/test/java/org/springframework/samples/petclinic/api/application/VisitsServiceClientTest.java b/spring-petclinic-api-gateway/src/test/java/org/springframework/samples/petclinic/api/application/VisitsServiceClientTest.java deleted file mode 100644 index 465ab7b74..000000000 --- a/spring-petclinic-api-gateway/src/test/java/org/springframework/samples/petclinic/api/application/VisitsServiceClientTest.java +++ /dev/null @@ -1,75 +0,0 @@ -package org.springframework.samples.petclinic.api.application; - -import org.junit.jupiter.api.BeforeEach; -import org.junit.jupiter.api.Test; -import org.springframework.beans.factory.annotation.Autowired; -import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker; -import org.springframework.context.annotation.EnableAspectJAutoProxy; -import org.springframework.http.MediaType; -import org.springframework.samples.petclinic.api.dto.VisitDetails; -import org.springframework.test.context.junit.jupiter.SpringJUnitConfig; -import org.springframework.test.web.client.MockRestServiceServer; -import org.springframework.web.client.RestTemplate; - -import java.util.Collections; -import java.util.List; -import java.util.Map; - -import static org.junit.jupiter.api.Assertions.assertEquals; -import static org.junit.jupiter.api.Assertions.assertNotNull; -import static org.springframework.test.web.client.match.MockRestRequestMatchers.requestTo; -import static org.springframework.test.web.client.response.MockRestResponseCreators.withServerError; -import static org.springframework.test.web.client.response.MockRestResponseCreators.withSuccess; - -@EnableCircuitBreaker -@EnableAspectJAutoProxy -@SpringJUnitConfig(classes = {VisitsServiceClient.class, RestTemplate.class}) -class VisitsServiceClientTest { - - private static final Integer PET_ID = 1; - - @Autowired - private VisitsServiceClient visitsServiceClient; - - @Autowired - private RestTemplate restTemplate; - - private MockRestServiceServer mockServer; - - @BeforeEach - public void setUp() { - mockServer = MockRestServiceServer.createServer(restTemplate); - } - - @Test - public void getVisitsForPets_withAvailableVisitsService() { - mockServer.expect(requestTo("http://visits-service/pets/visits?petId=1")) - .andRespond(withSuccess("{\"items\":[{\"id\":5,\"date\":\"2018-11-15\",\"description\":\"test visit\",\"petId\":1}]}", MediaType.APPLICATION_JSON)); - - Map> visits = visitsServiceClient.getVisitsForPets(Collections.singletonList(1)); - - assertVisitDescriptionEquals(visits, PET_ID,"test visit"); - } - - /** - * Test Hystrix fallback method - */ - @Test - public void getVisitsForPets_withServerError() { - - mockServer.expect(requestTo("http://visits-service/pets/visits?petId=1")) - .andRespond(withServerError()); - - Map> visits = visitsServiceClient.getVisitsForPets(Collections.singletonList(1)); - - assertEquals(0, visits.size()); - } - - private void assertVisitDescriptionEquals(Map> visits, int petId, String description) { - assertEquals(1, visits.size()); - assertNotNull(visits.get(1)); - assertEquals(1, visits.get(petId).size()); - assertEquals(description, visits.get(petId).get(0).getDescription()); - } - -} diff --git a/spring-petclinic-config-server/pom.xml b/spring-petclinic-config-server/pom.xml index 07fdca5ec..b8c5ab8cd 100644 --- a/spring-petclinic-config-server/pom.xml +++ b/spring-petclinic-config-server/pom.xml @@ -11,7 +11,7 @@ org.springframework.samples spring-petclinic-microservices - 2.1.4 + 2.2.1 diff --git a/spring-petclinic-config-server/src/main/resources/bootstrap.yml b/spring-petclinic-config-server/src/main/resources/bootstrap.yml index 6f4cfda69..43e3e9906 100644 --- a/spring-petclinic-config-server/src/main/resources/bootstrap.yml +++ b/spring-petclinic-config-server/src/main/resources/bootstrap.yml @@ -5,11 +5,7 @@ spring: server: git: uri: https://github.com/spring-petclinic/spring-petclinic-microservices-config ---- -spring: - profiles: local - cloud: - config: - server: - git: - uri: file:///${GIT_REPO} + # Use the File System Backend to avoid git pulling. Enable "native" profile in the Config Server. + native: + searchLocations: file:///${GIT_REPO} + diff --git a/spring-petclinic-customers-service/pom.xml b/spring-petclinic-customers-service/pom.xml index 2d2015492..b10c75e53 100644 --- a/spring-petclinic-customers-service/pom.xml +++ b/spring-petclinic-customers-service/pom.xml @@ -11,7 +11,7 @@ org.springframework.samples spring-petclinic-microservices - 2.1.4 + 2.2.1 diff --git a/spring-petclinic-customers-service/src/test/java/org/springframework/samples/petclinic/customers/web/PetResourceTest.java b/spring-petclinic-customers-service/src/test/java/org/springframework/samples/petclinic/customers/web/PetResourceTest.java index 04a6e0079..0bd2abb38 100644 --- a/spring-petclinic-customers-service/src/test/java/org/springframework/samples/petclinic/customers/web/PetResourceTest.java +++ b/spring-petclinic-customers-service/src/test/java/org/springframework/samples/petclinic/customers/web/PetResourceTest.java @@ -51,7 +51,7 @@ void shouldGetAPetInJSonFormat() throws Exception { mvc.perform(get("/owners/2/pets/2").accept(MediaType.APPLICATION_JSON)) .andExpect(status().isOk()) - .andExpect(content().contentType("application/json;charset=UTF-8")) + .andExpect(content().contentType("application/json")) .andExpect(jsonPath("$.id").value(2)) .andExpect(jsonPath("$.name").value("Basil")) .andExpect(jsonPath("$.type.id").value(6)); diff --git a/spring-petclinic-discovery-server/pom.xml b/spring-petclinic-discovery-server/pom.xml index 81e866a87..c4f69924a 100644 --- a/spring-petclinic-discovery-server/pom.xml +++ b/spring-petclinic-discovery-server/pom.xml @@ -11,7 +11,7 @@ org.springframework.samples spring-petclinic-microservices - 2.1.4 + 2.2.1 diff --git a/spring-petclinic-hystrix-dashboard/pom.xml b/spring-petclinic-hystrix-dashboard/pom.xml index c6c55f5da..c6a4eb486 100644 --- a/spring-petclinic-hystrix-dashboard/pom.xml +++ b/spring-petclinic-hystrix-dashboard/pom.xml @@ -5,7 +5,7 @@ spring-petclinic-microservices org.springframework.samples - 2.1.4 + 2.2.1 4.0.0 diff --git a/spring-petclinic-vets-service/pom.xml b/spring-petclinic-vets-service/pom.xml index cfa9ae945..fb3f6fd93 100644 --- a/spring-petclinic-vets-service/pom.xml +++ b/spring-petclinic-vets-service/pom.xml @@ -11,7 +11,7 @@ org.springframework.samples spring-petclinic-microservices - 2.1.4 + 2.2.1 diff --git a/spring-petclinic-visits-service/pom.xml b/spring-petclinic-visits-service/pom.xml index f9b213454..2316f8cbc 100644 --- a/spring-petclinic-visits-service/pom.xml +++ b/spring-petclinic-visits-service/pom.xml @@ -11,7 +11,7 @@ org.springframework.samples spring-petclinic-microservices - 2.1.4 + 2.2.1 diff --git a/spring-petclinic-visits-service/src/main/java/org/springframework/samples/petclinic/visits/model/VisitRepository.java b/spring-petclinic-visits-service/src/main/java/org/springframework/samples/petclinic/visits/model/VisitRepository.java index b4f8689be..063b3d270 100644 --- a/spring-petclinic-visits-service/src/main/java/org/springframework/samples/petclinic/visits/model/VisitRepository.java +++ b/spring-petclinic-visits-service/src/main/java/org/springframework/samples/petclinic/visits/model/VisitRepository.java @@ -15,6 +15,7 @@ */ package org.springframework.samples.petclinic.visits.model; +import java.util.Collection; import java.util.List; import org.springframework.data.jpa.repository.JpaRepository; @@ -33,5 +34,5 @@ public interface VisitRepository extends JpaRepository { List findByPetId(int petId); - List findByPetIdIn(Iterable petIds); + List findByPetIdIn(Collection petIds); }