Xem toàn bộ series bài viết hướng dẫn xây dựng ứng dụng Questions Management tại đây.
Composite Service sẽ có nhiệm vụ kết nối đến các Core Service để lấy dữ liệu, combine hoặc transform những dữ liệu đó tuỳ theo nhu cầu của người dùng rồi trả về cho họ. Do đó, trước khi xây dựng API lấy tất cả question cho Composite Question Service, có mấy việc chúng ta phải làm trước như sau:
Vì Composite Question Service sẽ thao tác liên quan đến các category, các question và cả những option nên mình sẽ thêm các đối tượng model cho từng loại để chứa thông tin của chúng như sau:
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; } |
Ở đây, chúng ta sẽ có 1 đối tượng chứa tất cả các thông tin về 1 question, category mà question này thuộc về và các lựa chọn của nó nên mình cũng sẽ thêm mới một đối tượng CompositeQuestion như sau:
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; } |
Vì Composite Question Service sẽ gọi đến Core Category Service để lấy thông tin về category, đến Core Question Service để lấy thông tin về question, đến Core Option Service để lấy option nên mình sẽ thêm mới trong tập tin application.properties thông tin về chúng như sau:
1 2 3 |
corecategoryservice.url=http://localhost:8082 corequestionservice.url=http://localhost:8081 coreoptionservice.url=http://localhost:8083 |
Để handle việc gọi tới Core Category Service, mình sẽ tạo một interface tên là CoreCategoryService:
1 2 3 4 5 6 7 |
package com.huongdanjava.questionservice.service; public interface CoreCategoryService { String getServiceUrl(); } |
với implementation là 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; } } |
Để handle việc gọi tới Core Question Service, mình sẽ tạo một interface tên là CoreQuestionService:
1 2 3 4 5 6 7 |
package com.huongdanjava.questionservice.service; public interface CoreQuestionService { String getServiceUrl(); } |
với implementation là 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; } } |
Để handle việc gọi tới Core Option Service, mình sẽ tạo một interface tên là CoreOptionService:
1 2 3 4 5 6 7 |
package com.huongdanjava.questionservice.service; public interface CoreOptionService { String getServiceUrl(); } |
với implementation là 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; } } |
Mình sẽ chạy service này sử dụng port 8181 nên mình cũng sẽ thêm property server.port trong tập tin application.properties như sau:
1 |
server.port=8181 |
OK, mọi thứ chuẩn bị đã xong, bây giờ mình sẽ đi vào phần chính của bài viết này các bạn nhé!
Đầu tiên, mình sẽ tạo mới một controller tên là CompositeQuestionController với nội dung như sau:
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 { } |
Với khai báo trên, mình cũng đã expose các API cho phần Composite Question Service với request URL bắt đầu là “/question”.
Tiếp theo mình sẽ tạo mới một CompositeQuestionService để handle tất cả các thao tác liên quan đến Composite Question Service.
1 2 3 4 5 |
package com.huongdanjava.questionservice.service; public interface CompositeQuestionService { } |
Thao tác đầu tiên sẽ là tìm và combine thông tin của tất cả các câu hỏi đang có trong hệ thống.
Thông tin này bao gồm tất cả các câu hỏi, category mà các câu hỏi đó thuộc về và những lựa chọn dành cho câu hỏi đó. Chúng ta sẽ sử dụng đối tượng CompositeQuestion mà chúng ta đã khai báo ở trên để chứa tất cả những thông tin này. Cụ thể mình sẽ thêm mới một abstract method vào class CompositeQuestionService như sau:
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(); } |
Với phương thức findAllQuestions(), đầu tiên chúng ta cần gọi tới Core Question Service để lấy tất cả các câu hỏi đang có trong hệ thống. Bởi vì thông tin từ Core Question Service chỉ có Category ID với nội dung của câu hỏi mà thôi nên chúng ta cần gọi tới Core Category Service để tìm thông tin về category theo ID và gọi tới Core Option Service để lấy tất cả các lựa chọn của câu hỏi đó theo Question ID. Đó là tất cả những thứ chúng ta cần làm đối với method findAllQuestions().
Để hiện thực được phương thức findAllQuestions() trên, đầu tiên chúng ta cần gọi tới Core Question Service để lấy tất cả các câu hỏi đang có trong hệ thống bằng cách thêm mới một method trong CoreQuestionService:
1 |
Flux<Question> findAllQuestions(); |
với phần implementation trong class CoreQuestionServiceImpl như sau:
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); } |
Tiếp theo chúng ta sẽ gọi tới Core Category Service để tìm thông tin về category theo ID bằng cách thêm mới một method trong class CoreCategoryService:
1 |
Mono<Category> findById(String categoryId); |
với phần implementation trong class CoreCategoryServiceImpl như sau:
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); } |
Cuối cùng thì chúng ta sẽ gọi tới Core Option Service để lấy tất cả các options của câu hỏi này theo Question ID bằng cách thêm mới một method trong class CoreOptionService:
1 |
Flux<Option> getOptions(String questionId); |
với phần implementation trong class CoreOptionServiceImpl như sau:
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, bây giờ chúng ta sẽ hiện thực phương thức findAllQuestions() các bạn nhé.
Chúng ta sẽ inject 3 class service CoreCategoryService, CoreQuestionService và CoreOptionService vào trước:
1 2 3 4 5 6 7 8 |
@Autowired private CoreQuestionService coreQuestionService; @Autowired private CoreOptionService coreOptionService; @Autowired private CoreCategoryService coreCategoryService; |
Trong phương thức findAllQuestion(), đầu tiên chúng ta sẽ gọi Core Question Service trước:
1 |
Flux<Question> questionsFromCoreQuestionService = coreQuestionService.findAllQuestions(); |
Ứng với mỗi item mà Core Question Service trả về với phương thức trên, chúng ta sẽ sử dụng item đó để tìm kiếm thông tin của category và các option sử dụng phương thức flatMap() như sau:
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()); } |
Ở đây, mình có sử dụng phương thức collectList() để trả về một danh sách các option, phương thức map để build đối tượng Composite mà chúng ta cần. Một điều lưu ý nữa, vì quá trình gọi qua Core Category Service và Core Option Service cần thời gian để xử lý nên mình đã cho chạy quá trình này trên một thread khác sử dụng phương thức subscribeOn().
Điều cuối cùng chúng ta cần làm đó là thêm mới một method vào class CompositeQuestionController để expose một GET request “/all”:
1 2 3 |
@GetMapping("/all") public Flux<CompositeQuestion> findAllQuestions() { } |
rồi khai báo autowire đối tượng CompositeQuestionService vào:
1 2 |
@Autowired private CompositeQuestionService compositeQuestionService; |
để có thể sử dụng phương thức findAllQuestions() mà chúng ta đã tạo ra ở trên:
1 2 3 4 |
@GetMapping("/all") public Flux<CompositeQuestion> findAllQuestions() { return compositeQuestionService.findAllQuestions(); } |
OK, đến đây là chúng ta đã hoàn thành việc xây dựng API lấy tất cả question cho Composite Question Service rồi đó các bạn. Test thử xem sao nhé.
Davitluit
Theo mình nghĩ ở đây là đối tượng chứa tất cả thông tin của 1 question, bao gồm category của nó, các option của nó như đúng đối tượng bạn tạo thì đúng hơn, đó lá suy nghĩ của mình nếu có gì chưa đúng bạn giải thích giúp thêm, cám ơn bạn.
Khanh Nguyen
Trong câu của mình cũng có đề cập về “câu hỏi là gì” đó bạn! Nhưng mà suy nghĩ lại có vẻ cách diễn đạt của bạn hay hơn nên mình sẽ sửa lại. Thanks bạn nhé! 🙂
Davitluit
Theo mình trong câu này “Ở đây, chúng ta sẽ có 1 đối tượng chứa tất cả các thông tin về category, câu hỏi là gì và các lựa chọn của nó nên mình cũng sẽ thêm mới một đối tượng CompositeQuestion như sau:”
Nên sửa “thông tin về category” thành “thông tin về question”
Khanh Nguyen
Sao bạn nghĩ mình nên sửa như vậy nhỉ?