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.
Việc áp dụng Circuit Breaker cho các service trong ứng dụng Questions Management là một điều rất quan trọng nhằm đảm bảo các vấn đề xảy ra cần có giải pháp để giải quyết nhanh chóng và kịp thời. Mình sẽ sử dụng Hystrix để làm điều này.
Các bạn có thể tìm hiểu thêm về Hystrix và Hystrix dành cho Reactive application nếu muốn.
Trong ứng dụng Questions Management, các API và Composite service là những service sẽ gọi tới các service khác để lấy dữ liệu. Do đó, mình sẽ khai báo sử dụng Hystrix dependency trong các service này như sau:
1 2 3 4 |
<dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring-cloud-starter-netflix-hystrix</artifactId> </dependency> |
Tiếp theo mình sẽ đi từng service để enable Circuit Breaker và sửa lại phần gọi tới các service khác hỗ trợ Hystrix như sau:
API Category Service
ApiCategoryServiceApplication:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
package com.huongdanjava.categoryservice; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker; import org.springframework.cloud.netflix.eureka.EnableEurekaClient; @EnableEurekaClient @EnableCircuitBreaker @SpringBootApplication public class ApiCategoryServiceApplication { public static void main(String[] args) { SpringApplication.run(ApiCategoryServiceApplication.class, args); } } |
CoreCategoryServiceImpl:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 |
... @Service public class CoreCategoryServiceImpl implements CoreCategoryService { ... @Override public Flux<Category> findAll() { WebClient webClient = WebClient.builder() .baseUrl(getServiceUrl()) .build(); Flux<Category> findAll = webClient.get() .uri("/category/all") .retrieve() .bodyToFlux(Category.class); return HystrixCommands.from(findAll) .fallback(Flux.empty()) .commandName("ACS.CoreCategoryService.findAll") .toFlux(); } @Override public Mono<Category> save(Category category) { WebClient webClient = WebClient.builder() .baseUrl(getServiceUrl()) .build(); Mono<Category> save = webClient.post() .uri("/category/add") .body(BodyInserters.fromObject(category)) .retrieve() .bodyToMono(Category.class); return HystrixCommands.from(save) .fallback(Mono.empty()) .commandName("ACS.CoreCategoryService.save") .toMono(); } @Override public Mono<Category> findCategoryById(String categoryId) { WebClient webClient = WebClient.builder() .baseUrl(getServiceUrl()) .build(); Mono<Category> findCategoryById = webClient.get() .uri("/category/" + categoryId) .retrieve() .bodyToMono(Category.class); return HystrixCommands.from(findCategoryById) .fallback(Mono.empty()) .commandName("ACS.CoreCategoryService.findCategoryById") .toMono(); } @Override public Mono<Category> updateCategory(String categoryId, Category category) { WebClient webClient = WebClient.builder() .baseUrl(getServiceUrl()) .build(); Mono<Category> updateCategory = webClient.put() .uri("/category/" + categoryId) .body(BodyInserters.fromObject(category)) .retrieve() .bodyToMono(Category.class); return HystrixCommands.from(updateCategory) .fallback(Mono.empty()) .commandName("ACS.CoreCategoryService.updateCategory") .toMono(); } @Override public Mono<HttpStatus> deleteCategory(String categoryId) { WebClient webClient = WebClient.builder() .baseUrl(getServiceUrl()) .build(); Mono<HttpStatus> deleteCategory = webClient.delete() .uri("/category/" + categoryId) .exchange() .map(response -> response.statusCode()); return HystrixCommands.from(deleteCategory) .fallback(Mono.empty()) .commandName("coreCategoryService.deleteCategory") .toMono(); } } |
API Option Service
ApiOptionServiceApplication:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
package com.huongdanjava.optionservice; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker; import org.springframework.cloud.netflix.eureka.EnableEurekaClient; @EnableEurekaClient @EnableCircuitBreaker @SpringBootApplication public class ApiOptionServiceApplication { public static void main(String[] args) { SpringApplication.run(ApiOptionServiceApplication.class, args); } } |
CompositeOptionServiceImpl:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
... @Service public class CompositeOptionServiceImpl implements CompositeOptionService { ... @Override public Mono<Option> addNewOption(Option option) { WebClient client = WebClient.builder() .baseUrl(getServiceUrl()) .build(); WebClient.ResponseSpec responseSpec = client.post() .uri("/option/add") .body(BodyInserters.fromObject(option)) .retrieve(); Mono<Option> addNewOption = responseSpec.bodyToMono(Option.class); return HystrixCommands.from(addNewOption) .fallback(Mono.empty()) .commandName("AOS.CompositeOptionService.addNewOption") .toMono(); } } |
CoreOptionServiceImpl:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 |
... @Service public class CoreOptionServiceImpl implements CoreOptionService { ... @Override public Mono<Option> updateOption(String optionId, Option option) { WebClient webClient = WebClient.builder() .baseUrl(getServiceUrl()) .build(); Mono<Option> updateOption = webClient.put() .uri("/option/" + optionId) .body(BodyInserters.fromObject(option)) .retrieve() .bodyToMono(Option.class); return HystrixCommands.from(updateOption) .fallback(Mono.empty()) .commandName("AOS.CoreOptionService.updateOption") .toMono(); } @Override public Mono<HttpStatus> deleteOption(String optionId) { WebClient webClient = WebClient.builder() .baseUrl(getServiceUrl()) .build(); Mono<HttpStatus> deleteOption = webClient.delete() .uri("/option/" + optionId) .exchange() .map(response -> response.statusCode()); return HystrixCommands.from(deleteOption) .fallback(Mono.empty()) .commandName("AOS.CoreOptionService.deleteOption") .toMono(); } } |
API Question Service
ApiQuestionServiceApplication:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
package com.huongdanjava.questionservice; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker; import org.springframework.cloud.netflix.eureka.EnableEurekaClient; @EnableEurekaClient @EnableCircuitBreaker @SpringBootApplication public class ApiQuestionServiceApplication { public static void main(String[] args) { SpringApplication.run(ApiQuestionServiceApplication.class, args); } } |
CompositeQuestionServiceImpl:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 98 99 |
... @Service public class CompositeQuestionServiceImpl implements CompositeQuestionService { ... @Override public Flux<CompositeQuestion> findAllQuestions() { WebClient client = WebClient.builder() .baseUrl(getServiceUrl()) .build(); WebClient.ResponseSpec responseSpec = client.get() .uri("/question/all") .retrieve(); Flux<CompositeQuestion> findAllQuestions = responseSpec.bodyToFlux(CompositeQuestion.class); return HystrixCommands.from(findAllQuestions) .fallback(Flux.empty()) .commandName("AQS.CompositeQuestionService.findAllQuestions") .toFlux(); } @Override public Mono<CompositeQuestion> findQuestionById(String id) { WebClient client = WebClient.builder() .baseUrl(getServiceUrl()) .build(); WebClient.ResponseSpec responseSpec = client.get() .uri("/question/" + id) .retrieve(); Mono<CompositeQuestion> findQuestionById = responseSpec.bodyToMono(CompositeQuestion.class); return HystrixCommands.from(findQuestionById) .fallback(Mono.empty()) .commandName("AQS.CompositeQuestionService.findQuestionById") .toMono(); } @Override public Mono<CompositeQuestion> createNewQuestion(Question question) { WebClient client = WebClient.builder() .baseUrl(getServiceUrl()) .build(); WebClient.ResponseSpec responseSpec = client.post() .uri("/question/add") .body(BodyInserters.fromObject(question)) .retrieve(); Mono<CompositeQuestion> createNewQuestion = responseSpec.bodyToMono(CompositeQuestion.class); return HystrixCommands.from(createNewQuestion) .fallback(Mono.empty()) .commandName("AQS.CompositeQuestionService.createNewQuestion") .toMono(); } @Override public Mono<CompositeQuestion> updateQuestion(String questionId, Question question) { WebClient client = WebClient.builder() .baseUrl(getServiceUrl()) .build(); WebClient.ResponseSpec responseSpec = client.put() .uri("/question/" + questionId) .body(BodyInserters.fromObject(question)) .retrieve(); Mono<CompositeQuestion> updateQuestion = responseSpec.bodyToMono(CompositeQuestion.class); return HystrixCommands.from(updateQuestion) .fallback(Mono.empty()) .commandName("AQS.CompositeQuestionService.updateQuestion") .toMono(); } @Override public Mono<HttpStatus> deleteQuestion(String questionId) { WebClient webClient = WebClient.builder() .baseUrl(getServiceUrl()) .build(); Mono<HttpStatus> deleteQuestion = webClient.delete() .uri("/question/" + questionId) .exchange() .map(response -> response.statusCode()); return HystrixCommands.from(deleteQuestion) .fallback(Mono.empty()) .commandName("AQS.CompositeQuestionService.deleteQuestion") .toMono(); } } |
Composite Question Service
CompositeQuestionServiceApplication:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
package com.huongdanjava.questionservice; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker; import org.springframework.cloud.netflix.eureka.EnableEurekaClient; @EnableEurekaClient @EnableCircuitBreaker @SpringBootApplication public class CompositeQuestionServiceApplication { public static void main(String[] args) { SpringApplication.run(CompositeQuestionServiceApplication.class, args); } } |
CoreCategoryServiceImpl:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
... @Service public class CoreCategoryServiceImpl implements CoreCategoryService { ... @Override public Mono<Category> findById(String categoryId) { WebClient client = WebClient.builder() .baseUrl(getServiceUrl()) .build(); WebClient.ResponseSpec responseSpec = client.get() .uri("/category/" + categoryId) .retrieve(); Mono<Category> findById = responseSpec.bodyToMono(Category.class); return HystrixCommands.from(findById) .fallback(Mono.empty()) .commandName("CQS.CoreCategoryService.findById") .toMono(); } } |
CoreOptionServiceImpl:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 |
... @Service public class CoreOptionServiceImpl implements CoreOptionService { ... @Override public Flux<Option> getOptions(String questionId) { WebClient client = WebClient.builder() .baseUrl(getServiceUrl()) .build(); WebClient.ResponseSpec responseSpec = client.get() .uri("/option?questionId=" + questionId) .retrieve(); Flux<Option> getOptions = responseSpec.bodyToFlux(Option.class); return HystrixCommands.from(getOptions) .fallback(Flux.empty()) .commandName("CQS.CoreOptionService.getOptions") .toFlux(); } @Override public Mono<Void> deleteByQuestionId(String questionId) { WebClient client = WebClient.builder() .baseUrl(getServiceUrl()) .build(); WebClient.ResponseSpec responseSpec = client.delete() .uri("/option?questionId=" + questionId) .retrieve(); Mono<Void> deleteByQuestionId = responseSpec.bodyToMono(Void.class); return HystrixCommands.from(deleteByQuestionId) .fallback(Mono.empty()) .commandName("CQS.CoreOptionService.deleteByQuestionId") .toMono(); } } |
CoreQuestionServiceImpl:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 43 44 45 46 47 48 49 50 51 52 53 54 55 56 57 58 59 60 61 62 63 64 65 66 67 68 69 70 71 72 73 74 75 76 77 78 79 80 81 82 83 84 85 86 87 88 89 90 91 92 93 94 95 96 97 |
... @Service public class CoreQuestionServiceImpl implements CoreQuestionService { ... @Override public Flux<Question> findAllQuestions() { WebClient client = WebClient.builder() .baseUrl(getServiceUrl()) .build(); WebClient.ResponseSpec responseSpec = client.get() .uri("/question/all") .retrieve(); Flux<Question> findAllQuestions = responseSpec.bodyToFlux(Question.class); return HystrixCommands.from(findAllQuestions) .fallback(Flux.empty()) .commandName("CQS.CoreQuestionService.findAllQuestions") .toFlux(); } @Override public Mono<Question> findQuestionById(String id) { WebClient client = WebClient.builder() .baseUrl(getServiceUrl()) .build(); WebClient.ResponseSpec responseSpec = client.get() .uri("/question/" + id) .retrieve(); Mono<Question> findQuestionById = responseSpec.bodyToMono(Question.class); return HystrixCommands.from(findQuestionById) .fallback(Mono.empty()) .commandName("CQS.CoreQuestionService.findQuestionById") .toMono(); } @Override public Mono<Question> addNewQuestion(Question question) { WebClient webClient = WebClient.builder() .baseUrl(getServiceUrl()) .build(); Mono<Question> addNewQuestion = webClient.post() .uri("/question/add") .body(BodyInserters.fromObject(question)) .retrieve() .bodyToMono(Question.class); return HystrixCommands.from(addNewQuestion) .fallback(Mono.empty()) .commandName("CQS.CoreQuestionService.addNewQuestion") .toMono(); } @Override public Mono<Question> updateQuestion(String questionId, Question question) { WebClient webClient = WebClient.builder() .baseUrl(getServiceUrl()) .build(); Mono<Question> updateQuestion = webClient.put() .uri("/question/" + questionId) .body(BodyInserters.fromObject(question)) .retrieve() .bodyToMono(Question.class); return HystrixCommands.from(updateQuestion) .fallback(Mono.empty()) .commandName("CQS.CoreQuestionService.updateQuestion") .toMono(); } @Override public Mono<Void> deleteById(String questionId) { WebClient webClient = WebClient.builder() .baseUrl(getServiceUrl()) .build(); Mono<Void> deleteById = webClient.delete() .uri("/question/" + questionId) .retrieve() .bodyToMono(Void.class); return HystrixCommands.from(deleteById) .fallback(Mono.empty()) .commandName("CQS.CoreQuestionService.deleteById") .toMono(); } } |
Composite Option Service
CompositeOptionServiceApplication:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 |
package com.huongdanjava.optionservice; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.client.circuitbreaker.EnableCircuitBreaker; import org.springframework.cloud.netflix.eureka.EnableEurekaClient; @EnableEurekaClient @EnableCircuitBreaker @SpringBootApplication public class CompositeOptionServiceApplication { public static void main(String[] args) { SpringApplication.run(CompositeOptionServiceApplication.class, args); } } |
CoreOptionServiceImpl:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
... @Service public class CoreOptionServiceImpl implements CoreOptionService { ... @Override public Mono<Option> addNewOption(Option option) { WebClient webClient = WebClient.builder() .baseUrl(getServiceUrl()) .build(); Mono<Option> addNewOption = webClient.post() .uri("/option/add") .body(BodyInserters.fromObject(option)) .retrieve() .bodyToMono(Option.class); return HystrixCommands.from(addNewOption) .fallback(Mono.empty()) .commandName("COS.CoreOptionService.addNewOption") .toMono(); } } |
CoreQuestionServiceImpl:
1 2 3 4 5 6 7 8 9 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 |
... @Service public class CoreQuestionServiceImpl implements CoreQuestionService { ... @Override public Mono<Question> findById(String questionId) { WebClient client = WebClient.builder() .baseUrl(getServiceUrl()) .build(); WebClient.ResponseSpec responseSpec = client.get() .uri("/question/" + questionId) .retrieve(); Mono<Question> findById = responseSpec.bodyToMono(Question.class); return HystrixCommands.from(findById) .fallback(Mono.empty()) .commandName("COS.CoreQuestionService.findById") .toMono(); } } |
Xong rồi đó các bạn! Các bạn có thể test Circuit Breaker này như sau:
– Start tất cả các ứng dụng lên:
– Request lấy thông tin của 1 question:
– Tắt Core Option Service đi:
– Request lại question trên, các bạn sẽ thấy kết quả vẫn trả về bình thường mà không có thông tin option như sau: