Check out the full series of Questions Management tutorial here.
Composite Service will connect to the Core Services to retrieve data, combine or transform those data according to the needs of the user and return them. So, before you build the API to get all the questions for the Composite Question Service, there are a few things we have to do first:
Because the Composite Question Service will deal with categories, questions, and options, we will add model objects for each type to contain their information as follows:
Category:
1 2 3 4 5 6 7 8 9 10 11 12 |
package com.huongdanjava.questionservice.dto; import lombok.Data; @Data public class Category { private String id; private String name; } |
Question:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 |
package com.huongdanjava.questionservice.dto; import lombok.Data; @Data public class Question { private String id; private String description; private String categoryId; } |
Option:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 |
package com.huongdanjava.questionservice.dto; import lombok.Data; @Data public class Option { private String id; private String description; private String note; private Boolean isCorrect; private String questionId; } |
Here, we will have an object containing all the information about a question, the category that this question belongs to and its options, so we will also add a new CompositeQuestion object like this:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
package com.huongdanjava.questionservice.dto; import java.util.List; import lombok.AllArgsConstructor; import lombok.Data; @Data @AllArgsConstructor public class CompositeQuestion { private String id; private String description; private Category category; private List<Option> options; } |
Because the Composite Question Service will call to the Core Category Service for the category information, to the Core Question Service for the question information, to the Core Option Service for the option, so I will add their information in the application.properties file as follows:
1 2 3 |
corecategoryservice.url=http://localhost:8082 corequestionservice.url=http://localhost:8081 coreoptionservice.url=http://localhost:8083 |
To handle the call to the Core Category Service, I will create an interface called CoreCategoryService:
1 2 3 4 5 6 7 |
package com.huongdanjava.questionservice.service; public interface CoreCategoryService { String getServiceUrl(); } |
with the implementation is CoreCategoryServiceImpl:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
package com.huongdanjava.questionservice.service.impl; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import com.huongdanjava.questionservice.service.CoreCategoryService; @Service public class CoreCategoryServiceImpl implements CoreCategoryService { @Value("${corecategoryservice.url}") private String coreCategoryServiceUrl; @Override public String getServiceUrl() { return coreCategoryServiceUrl; } } |
To handle the call to the Core Question Service, we will create an interface called CoreQuestionService:
1 2 3 4 5 6 7 |
package com.huongdanjava.questionservice.service; public interface CoreQuestionService { String getServiceUrl(); } |
with the implementation of CoreQuestionServiceImpl:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
package com.huongdanjava.questionservice.service.impl; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import com.huongdanjava.questionservice.service.CoreQuestionService; @Service public class CoreQuestionServiceImpl implements CoreQuestionService { @Value("${corequestionservice.url}") private String coreQuestionServiceUrl; @Override public String getServiceUrl() { return coreQuestionServiceUrl; } } |
To handle the call to the Core Option Service, I will create an interface called CoreOptionService:
1 2 3 4 5 6 7 |
package com.huongdanjava.questionservice.service; public interface CoreOptionService { String getServiceUrl(); } |
with the implementation is CoreOptionServiceImpl:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 |
package com.huongdanjava.questionservice.service.impl; import org.springframework.beans.factory.annotation.Value; import org.springframework.stereotype.Service; import com.huongdanjava.questionservice.service.CoreOptionService; @Service public class CoreOptionServiceImpl implements CoreOptionService { @Value("${coreoptionservice.url}") private String coreOptionServiceUrl; @Override public String getServiceUrl() { return coreOptionServiceUrl; } } |
I will run this service using port 8181 so I will also add the server.port property in the application.properties file as follows:
1 |
server.port=8181 |
OK, everything is ready, now I will go to the main part of this tutorial!
First, I will create a controller named CompositeQuestionController with the following content:
1 2 3 4 5 6 7 8 9 10 |
package com.huongdanjava.questionservice; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RestController; @RestController @RequestMapping("/question") public class CompositeQuestionController { } |
With this declaration, I also expose APIs for the Composite Question Service with the request URL starting with “/question”.
Next I will create a CompositeQuestionService to handle all the operations related to the Composite Question Service.
1 2 3 4 5 |
package com.huongdanjava.questionservice.service; public interface CompositeQuestionService { } |
The first action will be to find and combine information of all the questions that are in the system.
This information includes all questions, the category that the questions belong to, and the options for that questions. We will use the CompositeQuestion object we have declared above to contain all of this information. Specifically, I will add an abstract method to the CompositeQuestionService class as follows:
1 2 3 4 5 6 7 8 9 10 |
package com.huongdanjava.questionservice.service; import com.huongdanjava.questionservice.dto.CompositeQuestion; import reactor.core.publisher.Flux; public interface CompositeQuestionService { Flux<CompositeQuestion> findAllQuestions(); } |
With the findAllQuestions() method, we first need to call the Core Question Service to get all the questions that are in the system. Because the information from the Core Question Service only has a Category ID with the content of the question, so we need to call the Core Category Service to find the category information by ID and call to the Core Option Service to get all the options of that question by Question ID. That’s all we need to do for the findAllQuestions() method.
In order to implement the findAllQuestions() method, we first need to call the Core Question Service to get all the questions that are in the system by adding a new method in the CoreQuestionService:
1 |
Flux<Question> findAllQuestions(); |
with the implementation of the CoreQuestionServiceImpl class is as follows:
1 2 3 4 5 6 7 8 9 10 11 12 |
@Override public Flux<Question> findAllQuestions() { WebClient client = WebClient.builder() .baseUrl(getServiceUrl()) .build(); WebClient.ResponseSpec responseSpec = client.get() .uri("/question/all") .retrieve(); return responseSpec.bodyToFlux(Question.class); } |
Next we will call the Core Category Service to find the category information by ID by adding a new method in the CoreCategoryService class:
1 |
Mono<Category> findById(String categoryId); |
with the implementation of the CoreCategoryServiceImpl class is as follows:
1 2 3 4 5 6 7 8 9 10 11 12 |
@Override public Mono<Category> findById(String categoryId) { WebClient client = WebClient.builder() .baseUrl(getServiceUrl()) .build(); WebClient.ResponseSpec responseSpec = client.get() .uri("/category/" + categoryId) .retrieve(); return responseSpec.bodyToMono(Category.class); } |
Finally, we will call the Core Option Service to get all options of this question by Question ID by adding a new method in the CoreOptionService class:
1 |
Flux<Option> getOptions(String questionId); |
with the implementation of the CoreOptionServiceImpl class is as follows:
1 2 3 4 5 6 7 8 9 10 11 12 |
@Override public Flux<Option> getOptions(String questionId) { WebClient client = WebClient.builder() .baseUrl(getServiceUrl()) .build(); WebClient.ResponseSpec responseSpec = client.get() .uri("/option?questionId=" + questionId) .retrieve(); return responseSpec.bodyToFlux(Option.class); } |
OK, now we will implement the findAllQuestions() method.
We will inject 3 core service classes CoreCategoryService, CoreQuestionService, and CoreOptionService first:
1 2 3 4 5 6 7 8 |
@Autowired private CoreQuestionService coreQuestionService; @Autowired private CoreOptionService coreOptionService; @Autowired private CoreCategoryService coreCategoryService; |
In the findAllQuestions() method, we will first call the Core Question Service:
1 |
Flux<Question> questionsFromCoreQuestionService = coreQuestionService.findAllQuestions(); |
For each item that the Core Question Service returns with this method, we will use that item to search for category information and options using the flatMap() method:
1 2 3 4 5 6 7 8 9 10 11 |
@Override public Flux<CompositeQuestion> findAllQuestions() { Flux<Question> questionsFromCoreQuestionService = coreQuestionService.findAllQuestions(); return questionsFromCoreQuestionService.flatMap(question -> coreCategoryService.findById(question.getCategoryId()) .flatMap(category -> coreOptionService.getOptions(question.getId()) .collectList() .map(options -> new CompositeQuestion(question.getId(), question.getDescription(), category, options))) ).subscribeOn(Schedulers.elastic()); } |
Here, we use the collectList() method to return a list of options, map() method to build a Composite object that we need. Another thing to keep in mind is that since calling the Core Category Service and the Core Option Service takes time to process, we have run this process on another thread using the subscribeOn() method.
The last thing we need to do is to add a new method to the CompositeQuestionController class to expose a GET request “/all”:
1 2 3 |
@GetMapping("/all") public Flux<CompositeQuestion> findAllQuestions() { } |
then declare autowire for the CompositeQuestionService object:
1 2 |
@Autowired private CompositeQuestionService compositeQuestionService; |
to be able to use the findAllQuestions() method we created above:
1 2 3 4 |
@GetMapping("/all") public Flux<CompositeQuestion> findAllQuestions() { return compositeQuestionService.findAllQuestions(); } |
OK, here we have finished building the API to get all the questions for the Composite Question Service. Let’s test it.