├── .gitignore
├── .mvn
└── wrapper
│ ├── maven-wrapper.jar
│ └── maven-wrapper.properties
├── LICENSE
├── README.md
├── api-gateway
├── .gitignore
├── .mvn
│ └── wrapper
│ │ ├── MavenWrapperDownloader.java
│ │ ├── maven-wrapper.jar
│ │ └── maven-wrapper.properties
├── Dockerfile
├── mvnw
├── mvnw.cmd
├── pom.xml
└── src
│ ├── main
│ ├── java
│ │ └── com
│ │ │ └── mz
│ │ │ └── apigateway
│ │ │ ├── ApiGatewayApplication.java
│ │ │ └── RouteConfiguration.java
│ └── resources
│ │ └── application.yml
│ └── test
│ └── java
│ └── com
│ └── mz
│ └── apigateway
│ └── ApiGatewayApplicationTests.java
├── common-api
├── pom.xml
└── src
│ └── main
│ └── java
│ └── com.mz.reactivedemo.common.api
│ └── events
│ ├── Command.java
│ ├── Document.java
│ ├── DomainEvent.java
│ └── Event.java
├── common-persistence
├── pom.xml
└── src
│ ├── main
│ └── java
│ │ └── com
│ │ └── mz
│ │ └── reactivedemo
│ │ └── adapter
│ │ └── persistance
│ │ ├── AggregateFactory.java
│ │ ├── AggregatePersistenceConfiguration.java
│ │ ├── AggregateRepository.java
│ │ ├── AggregateService.java
│ │ ├── actor
│ │ ├── AggregatePersistenceActor.java
│ │ ├── RecoveryActor.java
│ │ └── RepositoryActor.java
│ │ ├── document
│ │ ├── DocumentReadOnlyRepository.java
│ │ └── impl
│ │ │ └── DocumentReadOnlyRepositoryImpl.java
│ │ └── impl
│ │ ├── AggregateFactoryImpl.java
│ │ ├── AggregateRepositoryImpl.java
│ │ └── AggregateServiceImpl.java
│ └── test
│ └── java
│ └── com
│ └── mz
│ └── reactivedemo
│ └── adapter
│ └── persistance
│ └── actor
│ └── RecoveryActorTest.java
├── common
├── pom.xml
└── src
│ ├── main
│ └── java
│ │ └── com
│ │ └── mz
│ │ └── reactivedemo
│ │ └── common
│ │ ├── CommandResult.java
│ │ ├── CommandResultState.java
│ │ ├── KafkaMapper.java
│ │ ├── ValidateResult.java
│ │ ├── aggregate
│ │ ├── AbstractRootAggregate.java
│ │ ├── Aggregate.java
│ │ ├── AggregateStatus.java
│ │ ├── Id.java
│ │ └── StringValue.java
│ │ ├── http
│ │ ├── ErrorMessage.java
│ │ ├── HttpErrorHandler.java
│ │ └── HttpHandler.java
│ │ └── util
│ │ ├── AbstractMatch.java
│ │ ├── CaseMatch.java
│ │ ├── Logger.java
│ │ ├── Match.java
│ │ └── Try.java
│ └── test
│ └── java
│ └── com
│ └── mz
│ └── reactivedemo
│ └── common
│ └── util
│ ├── CaseMatchTest.java
│ ├── MatchTest.java
│ └── TryTest.java
├── docker
├── create-kafka-topics.sh
├── docker-compose-api-gateway.yml
├── docker-compose-shortener-service.yml
├── docker-compose-statistic-service.yml
├── docker-compose-user-service.yml
├── docker-compose.sh
└── docker-compose.yml
├── mvnw
├── mvnw.cmd
├── pom.xml
├── shortener-api
├── pom.xml
└── src
│ └── main
│ └── java
│ └── com
│ └── mz
│ └── reactivedemo
│ └── shortener
│ └── api
│ ├── command
│ ├── CreateShortener.java
│ └── UpdateShortener.java
│ ├── dto
│ └── ShortenerDto.java
│ ├── event
│ ├── ShortenerChangedEvent.java
│ ├── ShortenerEvent.java
│ ├── ShortenerEventType.java
│ ├── ShortenerPayload.java
│ └── ShortenerViewed.java
│ └── topics
│ └── ShortenerTopics.java
├── shortener-impl
├── Dockerfile
├── pom.xml
└── src
│ ├── main
│ ├── java
│ │ └── com
│ │ │ └── mz
│ │ │ └── reactivedemo
│ │ │ └── shortener
│ │ │ ├── ShortenerApplication.java
│ │ │ ├── ShortenerConfiguration.java
│ │ │ ├── ShortenerHandler.java
│ │ │ ├── ShortenerMapper.java
│ │ │ ├── ShortenerRepository.java
│ │ │ ├── ShortenerService.java
│ │ │ ├── adapter
│ │ │ └── user
│ │ │ │ └── UserAdapterImpl.java
│ │ │ ├── domain
│ │ │ ├── aggregate
│ │ │ │ ├── ShortUrl.java
│ │ │ │ ├── ShortenerAggregate.java
│ │ │ │ └── Url.java
│ │ │ └── event
│ │ │ │ ├── ShortenerChanged.java
│ │ │ │ ├── ShortenerCreated.java
│ │ │ │ └── ShortenerUpdated.java
│ │ │ ├── impl
│ │ │ ├── ShortenerApplicationServiceImpl.java
│ │ │ └── ShortenerFunctions.java
│ │ │ ├── port
│ │ │ └── kafka
│ │ │ │ └── ShortenerProcessor.java
│ │ │ ├── streams
│ │ │ ├── ApplicationMessageBus.java
│ │ │ └── ApplicationMessageBusImpl.java
│ │ │ └── view
│ │ │ ├── ShortenerDocument.java
│ │ │ ├── ShortenerQuery.java
│ │ │ └── impl
│ │ │ └── ShortenerQueryImpl.java
│ └── resources
│ │ ├── application.conf
│ │ └── application.yaml
│ └── test
│ └── java
│ └── com
│ └── mz
│ └── reactivedemo
│ └── shortener
│ ├── ShortenerHandlerTest.java
│ ├── ShortenerMapperTest.java
│ ├── aggregate
│ └── IdValueTest.java
│ ├── domain
│ └── aggregate
│ │ └── ShortenerAggregateTest.java
│ └── impl
│ ├── ShortenerQueryImplTest.java
│ └── ShortenerServiceImplTest.java
├── statistic-impl
├── Dockerfile
├── pom.xml
└── src
│ ├── main
│ ├── java
│ │ └── com
│ │ │ └── mz
│ │ │ └── statistic
│ │ │ ├── StatisticApplication.java
│ │ │ ├── StatisticConfiguration.java
│ │ │ ├── StatisticHandler.java
│ │ │ ├── StatisticRepository.java
│ │ │ ├── StatisticService.java
│ │ │ ├── adapters
│ │ │ ├── shortener
│ │ │ │ ├── ShortenerSubscriber.java
│ │ │ │ └── impl
│ │ │ │ │ └── ShortenerSubscriberImpl.java
│ │ │ └── user
│ │ │ │ ├── UserSubscriber.java
│ │ │ │ └── impl
│ │ │ │ └── UserSubscriberImpl.java
│ │ │ ├── impl
│ │ │ └── StatisticServiceImpl.java
│ │ │ └── model
│ │ │ ├── EventType.java
│ │ │ └── StatisticDocument.java
│ └── resources
│ │ └── application.yaml
│ └── test
│ └── java
│ └── com
│ └── mz
│ └── statistic
│ ├── StatisticHandlerTest.java
│ └── impl
│ └── StatisticServiceImplTest.java
├── user-api
├── pom.xml
└── src
│ └── main
│ └── java
│ └── com
│ └── mz
│ └── user
│ ├── dto
│ ├── BasicDto.java
│ ├── ContactInfoDto.java
│ └── UserDto.java
│ ├── message
│ ├── ContactInfoPayload.java
│ ├── UserPayload.java
│ ├── command
│ │ ├── CreateContactInfo.java
│ │ └── CreateUser.java
│ └── event
│ │ ├── UserChangedEvent.java
│ │ └── UserEventType.java
│ └── topics
│ └── UserTopics.java
└── user-impl
├── Dockerfile
├── pom.xml
└── src
├── main
├── java
│ └── com
│ │ └── mz
│ │ └── user
│ │ ├── UserApplication.java
│ │ ├── UserApplicationConfiguration.java
│ │ ├── UserApplicationMessageBus.java
│ │ ├── UserApplicationService.java
│ │ ├── UserHandler.java
│ │ ├── UserMapper.java
│ │ ├── adapter
│ │ └── shortener
│ │ │ ├── ShortenerAdapter.java
│ │ │ └── impl
│ │ │ └── ShortenerAdapterImpl.java
│ │ ├── domain
│ │ ├── aggregate
│ │ │ ├── ContactInfo.java
│ │ │ ├── Email.java
│ │ │ ├── FirstName.java
│ │ │ ├── LastName.java
│ │ │ ├── PhoneNumber.java
│ │ │ └── UserAggregate.java
│ │ ├── command
│ │ │ └── AddShortener.java
│ │ └── event
│ │ │ ├── ContactInfoCreated.java
│ │ │ ├── ShortenerAdded.java
│ │ │ └── UserCreated.java
│ │ ├── impl
│ │ ├── UserApplicationMessageBusImpl.java
│ │ ├── UserApplicationServiceImpl.java
│ │ └── UserFunctions.java
│ │ ├── port
│ │ ├── kafka
│ │ │ └── UserProcessor.java
│ │ └── rest
│ │ │ └── CreateContactInfoRequest.java
│ │ └── view
│ │ ├── ContactInfoDocument.java
│ │ ├── UserDocument.java
│ │ ├── UserQuery.java
│ │ ├── UserRepository.java
│ │ └── impl
│ │ └── UserQueryImpl.java
└── resources
│ ├── application.conf
│ └── application.yaml
└── test
└── java
└── com
└── mz
└── user
├── UserHandlerTest.java
├── domain
└── aggregate
│ ├── EmailTest.java
│ ├── PhoneNumberTest.java
│ └── UserAggregateTest.java
├── impl
└── UserAggregateServiceImplTest.java
└── port
└── kafka
└── UserProcessorTest.java
/.gitignore:
--------------------------------------------------------------------------------
1 | /target/
2 | !.mvn/wrapper/maven-wrapper.jar
3 | **/target/
4 |
5 | ### STS ###
6 | .apt_generated
7 | .classpath
8 | .factorypath
9 | .project
10 | .settings
11 | .springBeans
12 | .sts4-cache
13 |
14 | ### IntelliJ IDEA ###
15 | .idea
16 | *.iws
17 | *.iml
18 | *.ipr
19 |
20 | ### NetBeans ###
21 | /nbproject/private/
22 | /build/
23 | /nbbuild/
24 | /dist/
25 | /nbdist/
26 | /.nb-gradle/
--------------------------------------------------------------------------------
/.mvn/wrapper/maven-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michalzeman/spring-reactive-microservices/3cefbfe631b295b010a881ae8e2a4a045fc08067/.mvn/wrapper/maven-wrapper.jar
--------------------------------------------------------------------------------
/.mvn/wrapper/maven-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionUrl=https://repo1.maven.org/maven2/org/apache/maven/apache-maven/3.5.3/apache-maven-3.5.3-bin.zip
2 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | Copyright 2023 Michal Zeman, zeman.michal@yahoo.com
2 |
3 | Licensed under the Creative Commons Attribution (CC BY) license. You are free to share, copy, distribute,
4 | and adapt this work, provided you give appropriate credit to the original author Michal Zeman, zeman.michal@yahoo.com.
5 |
6 | To view a copy of the license, visit https://creativecommons.org/licenses/by/4.0/
7 |
8 | # spring-reactive-microservices
9 |
10 | This demo is based on:
11 | - Spring boot
12 | - Spring Webflux
13 | - Apache Kafka
14 | - Akka persistence for event sourcing support
15 |
16 | There are services:
17 |
18 | - Api-gateway
19 | - Statistic MS
20 | - Shortener MS
21 | - User MS
22 |
23 | ## Shortener MS
24 | - responsible by creating of short url
25 | - when user is loading some url based on some key representing shortened url, MS is generating event ShortenerViewed and publishing it into the Kafka topic "shortener-viewed"
26 | - this MS is designed or trying to be designed in DDD style and is representing Shortener Bounded Context.
27 | - Changes of Shortener aggregate like it was created, updated ... are captured by ShortenerChangedEvent and published into the Kafka topic "shortener-changed"
28 | - Result of changes done on aggregate is also published as a document event into the Kafka topic "shortener-document".
29 | This document event representing current state of Shortener aggregate, and it could be used for others services for
30 | building of local views in order to avoid direct communication between service. This document event is also possible to use for creating of views e.g. consumed by ElasticSearch. Maybe it would be implemented later like CQRS.
31 |
32 | ## Statistic MS
33 | - this MS is downstream of Shortener MS
34 | - responsible by calculation of some static related with Shortener MS
35 | - consumer of "shortener-viewed" Kafka topic
36 | - providing of API for VIEWS like how many times was displayed particular URL
37 |
38 | ## User MS
39 | - TBD not ready
40 |
41 | # How to run microservices locally
42 | ## Requirements
43 | - installed Docker
44 | - Java SDK 11
45 | - Insomnia for the REST API collection
46 | - CURL
47 | ## Build and run locally
48 | ### Run it as Dockerized services
49 | 1) From to root directory, execute the build with skipped tests
50 | ```
51 | ./mvnw clean install -DskipTests
52 | ```
53 | - this will start all services as docker containers
54 | 2) From to `./docker` dir. execute
55 | ```
56 | docker-compose -f docker-compose.yml -f docker-compose-api-gateway.yml -f docker-compose-shortener-service.yml -f docker-compose-statistic-service.yml -f docker-compose-user-service.yml up -d
57 | ```
58 | 3) For the verification of running services, use CURL commands
59 | - Create user:
60 | ```
61 | curl --request POST \
62 | --url http://localhost:8080/users \
63 | --header 'Content-Type: application/json' \
64 | --data '{
65 | "firstName": "FirstNameTest",
66 | "lastName": "LastNameTest"
67 | }'
68 | ```
69 | - this will verify the running User MS
70 | - List all users
71 | ```
72 | curl --request GET \
73 | --url http://localhost:8080/users/
74 | ```
75 | - Add contact information to the created user
76 | ```
77 | curl --request PUT \
78 | --url http://localhost:8080/users/[user_id]/contactinformation \
79 | --header 'Content-Type: application/json' \
80 | --data '{
81 | "email": "test@email.com",
82 | "phoneNumber": "+421999009001"
83 | }'
84 | ```
85 | - Get details of the created user
86 | ```
87 | curl --request GET \
88 | --url http://localhost:8080/users/[user_id]
89 | ```
90 | - List all events published by User MS into the Kafka topic and consumed by Statistic MS
91 | ```
92 | curl --request GET \
93 | --url http://localhost:8080/statistics
94 | ```
95 | - Create a shortener, this will test Shortener MS
96 | ```
97 | curl --request POST \
98 | --url http://localhost:8080/shorteners/ \
99 | --header 'Content-Type: application/json' \
100 | --data '{
101 | "url":"https://www.reactivemanifesto.org",
102 | "userId": "a6ed70a3-8f55-4513-acd0-96768a0e0e0c"
103 | }
104 | '
105 | ```
106 | - where "userId": "a6ed70a3-8f55-4513-acd0-96768a0e0e0c" is user id of some created user, it will be different from case to case
107 | - Test created shortener link
108 | - into the browser, enter the link `http://localhost:8080/shorteners/map/${shortener-key}`
109 | - placeholder `${shortener-key}` is an attribute you have received as a response body from the creation e.g. `"key": "2be71aba-4ff5-45c5-a3bb-7287a69e7e9d",`
110 |
111 | You can check all events related to any action or changes done in the microservices ` user MS` and `Shortener MS` via `Statistic MS` with the following command:
112 | ```
113 | curl --request GET \
114 | --url http://localhost:8080/statistics
115 | ```
116 |
117 | ### Run necessary Docker infrastructure for the local development
118 | - from the `docker` directory, execute the following command to run Apache Kafka and other services:
119 | ```
120 | docker compose -f docker-compose.yml up -d
121 | ```
122 | - Then, you can build the project with unit tests:
123 | ```
124 | ./mvnw clean install
125 | ```
--------------------------------------------------------------------------------
/api-gateway/.gitignore:
--------------------------------------------------------------------------------
1 | HELP.md
2 | /target/
3 | !.mvn/wrapper/maven-wrapper.jar
4 |
5 | ### STS ###
6 | .apt_generated
7 | .classpath
8 | .factorypath
9 | .project
10 | .settings
11 | .springBeans
12 | .sts4-cache
13 |
14 | ### IntelliJ IDEA ###
15 | .idea
16 | *.iws
17 | *.iml
18 | *.ipr
19 |
20 | ### NetBeans ###
21 | /nbproject/private/
22 | /nbbuild/
23 | /dist/
24 | /nbdist/
25 | /.nb-gradle/
26 | /build/
27 |
--------------------------------------------------------------------------------
/api-gateway/.mvn/wrapper/MavenWrapperDownloader.java:
--------------------------------------------------------------------------------
1 | /*
2 | Licensed to the Apache Software Foundation (ASF) under one
3 | or more contributor license agreements. See the NOTICE file
4 | distributed with this work for additional information
5 | regarding copyright ownership. The ASF licenses this file
6 | to you under the Apache License, Version 2.0 (the
7 | "License"); you may not use this file except in compliance
8 | with the License. You may obtain a copy of the License at
9 |
10 | http://www.apache.org/licenses/LICENSE-2.0
11 |
12 | Unless required by applicable law or agreed to in writing,
13 | software distributed under the License is distributed on an
14 | "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 | KIND, either express or implied. See the License for the
16 | specific language governing permissions and limitations
17 | under the License.
18 | */
19 |
20 | import java.io.File;
21 | import java.io.FileInputStream;
22 | import java.io.FileOutputStream;
23 | import java.io.IOException;
24 | import java.net.URL;
25 | import java.nio.channels.Channels;
26 | import java.nio.channels.ReadableByteChannel;
27 | import java.util.Properties;
28 |
29 | public class MavenWrapperDownloader {
30 |
31 | /**
32 | * Default URL to download the maven-wrapper.jar from, if no 'downloadUrl' is provided.
33 | */
34 | private static final String DEFAULT_DOWNLOAD_URL =
35 | "https://repo.maven.apache.org/maven2/io/takari/maven-wrapper/0.4.2/maven-wrapper-0.4.2.jar";
36 |
37 | /**
38 | * Path to the maven-wrapper.properties file, which might contain a downloadUrl property to
39 | * use instead of the default one.
40 | */
41 | private static final String MAVEN_WRAPPER_PROPERTIES_PATH =
42 | ".mvn/wrapper/maven-wrapper.properties";
43 |
44 | /**
45 | * Path where the maven-wrapper.jar will be saved to.
46 | */
47 | private static final String MAVEN_WRAPPER_JAR_PATH =
48 | ".mvn/wrapper/maven-wrapper.jar";
49 |
50 | /**
51 | * Name of the property which should be used to override the default download url for the wrapper.
52 | */
53 | private static final String PROPERTY_NAME_WRAPPER_URL = "wrapperUrl";
54 |
55 | public static void main(String args[]) {
56 | System.out.println("- Downloader started");
57 | File baseDirectory = new File(args[0]);
58 | System.out.println("- Using base directory: " + baseDirectory.getAbsolutePath());
59 |
60 | // If the maven-wrapper.properties exists, read it and check if it contains a custom
61 | // wrapperUrl parameter.
62 | File mavenWrapperPropertyFile = new File(baseDirectory, MAVEN_WRAPPER_PROPERTIES_PATH);
63 | String url = DEFAULT_DOWNLOAD_URL;
64 | if(mavenWrapperPropertyFile.exists()) {
65 | FileInputStream mavenWrapperPropertyFileInputStream = null;
66 | try {
67 | mavenWrapperPropertyFileInputStream = new FileInputStream(mavenWrapperPropertyFile);
68 | Properties mavenWrapperProperties = new Properties();
69 | mavenWrapperProperties.load(mavenWrapperPropertyFileInputStream);
70 | url = mavenWrapperProperties.getProperty(PROPERTY_NAME_WRAPPER_URL, url);
71 | } catch (IOException e) {
72 | System.out.println("- ERROR loading '" + MAVEN_WRAPPER_PROPERTIES_PATH + "'");
73 | } finally {
74 | try {
75 | if(mavenWrapperPropertyFileInputStream != null) {
76 | mavenWrapperPropertyFileInputStream.close();
77 | }
78 | } catch (IOException e) {
79 | // Ignore ...
80 | }
81 | }
82 | }
83 | System.out.println("- Downloading from: : " + url);
84 |
85 | File outputFile = new File(baseDirectory.getAbsolutePath(), MAVEN_WRAPPER_JAR_PATH);
86 | if(!outputFile.getParentFile().exists()) {
87 | if(!outputFile.getParentFile().mkdirs()) {
88 | System.out.println(
89 | "- ERROR creating output direcrory '" + outputFile.getParentFile().getAbsolutePath() + "'");
90 | }
91 | }
92 | System.out.println("- Downloading to: " + outputFile.getAbsolutePath());
93 | try {
94 | downloadFileFromURL(url, outputFile);
95 | System.out.println("Done");
96 | System.exit(0);
97 | } catch (Throwable e) {
98 | System.out.println("- Error downloading");
99 | e.printStackTrace();
100 | System.exit(1);
101 | }
102 | }
103 |
104 | private static void downloadFileFromURL(String urlString, File destination) throws Exception {
105 | URL website = new URL(urlString);
106 | ReadableByteChannel rbc;
107 | rbc = Channels.newChannel(website.openStream());
108 | FileOutputStream fos = new FileOutputStream(destination);
109 | fos.getChannel().transferFrom(rbc, 0, Long.MAX_VALUE);
110 | fos.close();
111 | rbc.close();
112 | }
113 |
114 | }
115 |
--------------------------------------------------------------------------------
/api-gateway/.mvn/wrapper/maven-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/michalzeman/spring-reactive-microservices/3cefbfe631b295b010a881ae8e2a4a045fc08067/api-gateway/.mvn/wrapper/maven-wrapper.jar
--------------------------------------------------------------------------------
/api-gateway/.mvn/wrapper/maven-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.6.0/apache-maven-3.6.0-bin.zip
2 |
--------------------------------------------------------------------------------
/api-gateway/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM openjdk:11-jdk
2 | VOLUME /tmp
3 |
4 | ENV STATISTIC_SERVICE_DOMAIN=localhost
5 | ENV SHORTENER_SERVICE_DOMAIN=localhost
6 | ENV USER_SERVICE_DOMAIN=localhost
7 |
8 | COPY api-gateway/target/*SNAPSHOT.jar app.jar
9 |
10 | ENTRYPOINT ["java","-jar","/app.jar",""]
--------------------------------------------------------------------------------
/api-gateway/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | 4.0.0
5 |
6 |
7 | spring-reactive-microservices
8 | com.mz
9 | 0.0.1-SNAPSHOT
10 |
11 |
12 | com.mz
13 | api-gateway
14 | 0.0.1-SNAPSHOT
15 | api-gateway
16 | Demo project for Spring Boot
17 |
18 |
19 |
20 | org.springframework.cloud
21 | spring-cloud-starter-gateway
22 |
23 |
24 |
25 | org.springframework.boot
26 | spring-boot-starter-test
27 | test
28 |
29 |
30 |
31 |
32 |
33 |
34 | org.springframework.boot
35 | spring-boot-maven-plugin
36 |
37 |
38 |
39 |
40 |
41 |
--------------------------------------------------------------------------------
/api-gateway/src/main/java/com/mz/apigateway/ApiGatewayApplication.java:
--------------------------------------------------------------------------------
1 | package com.mz.apigateway;
2 |
3 | import org.springframework.boot.SpringApplication;
4 | import org.springframework.boot.autoconfigure.SpringBootApplication;
5 |
6 | @SpringBootApplication
7 | public class ApiGatewayApplication {
8 |
9 | public static void main(String[] args) {
10 | SpringApplication.run(ApiGatewayApplication.class, args);
11 | }
12 |
13 | }
14 |
--------------------------------------------------------------------------------
/api-gateway/src/main/java/com/mz/apigateway/RouteConfiguration.java:
--------------------------------------------------------------------------------
1 | package com.mz.apigateway;
2 |
3 | import org.springframework.beans.factory.annotation.Value;
4 | import org.springframework.cloud.gateway.route.RouteLocator;
5 | import org.springframework.cloud.gateway.route.builder.RouteLocatorBuilder;
6 | import org.springframework.context.annotation.Bean;
7 | import org.springframework.context.annotation.Configuration;
8 |
9 |
10 | /**
11 | * Created by zemi on 31/10/2018.
12 | */
13 | @Configuration
14 | public class RouteConfiguration {
15 |
16 | final String statisticUri;
17 |
18 | final String shortenerUri;
19 |
20 | final String userUri;
21 |
22 | public RouteConfiguration(@Value("${service.statistic.uri}") String statisticUri,
23 | @Value("${service.shortener.uri}") String shortenerUri,
24 | @Value("${service.user.uri}") String userUri) {
25 | this.statisticUri = statisticUri;
26 | this.shortenerUri = shortenerUri;
27 | this.userUri = userUri;
28 | }
29 |
30 | @Bean
31 | public RouteLocator customRouteLocator(RouteLocatorBuilder builder) {
32 | return builder.routes()
33 | .route("statistic_id", r -> r.path("/statistics/**")
34 | .uri(statisticUri)
35 | )
36 | .route("shortener_id", r -> r.path("/shorteners/**")
37 | .uri(shortenerUri)
38 | )
39 | .route("user_id", r -> r.path("/users/**")
40 | .uri(userUri)
41 | )
42 | .build();
43 | }
44 |
45 | }
46 |
--------------------------------------------------------------------------------
/api-gateway/src/main/resources/application.yml:
--------------------------------------------------------------------------------
1 | service:
2 | statistic:
3 | uri: http://${STATISTIC_SERVICE_DOMAIN:localhost}:8091
4 | shortener:
5 | uri: http://${SHORTENER_SERVICE_DOMAIN:localhost}:8092
6 | user:
7 | uri: http://${USER_SERVICE_DOMAIN:localhost}:8093
8 |
9 | spring:
10 | cloud:
11 | gateway:
12 | # routes:
13 | # - id: statistic_id
14 | # url: http://localhost:8081
15 | # predicates:
16 | # - Path: /statistics/**
17 |
--------------------------------------------------------------------------------
/api-gateway/src/test/java/com/mz/apigateway/ApiGatewayApplicationTests.java:
--------------------------------------------------------------------------------
1 | package com.mz.apigateway;
2 |
3 | import org.junit.Test;
4 | import org.junit.runner.RunWith;
5 | import org.springframework.boot.test.context.SpringBootTest;
6 | import org.springframework.test.context.junit4.SpringRunner;
7 |
8 | @RunWith(SpringRunner.class)
9 | @SpringBootTest
10 | public class ApiGatewayApplicationTests {
11 |
12 | @Test
13 | public void contextLoads() {
14 | }
15 |
16 | }
17 |
--------------------------------------------------------------------------------
/common-api/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 | spring-reactive-microservices
7 | com.mz
8 | 0.0.1-SNAPSHOT
9 |
10 | 4.0.0
11 |
12 | common-api
13 |
14 |
15 |
16 |
17 |
18 | org.immutables
19 | value
20 | provided
21 |
22 |
23 |
24 |
25 | org.junit.jupiter
26 | junit-jupiter-api
27 | test
28 |
29 |
30 |
31 | org.junit.jupiter
32 | junit-jupiter-engine
33 | test
34 |
35 |
36 |
37 | org.springframework.boot
38 | spring-boot-starter-test
39 | test
40 |
42 |
43 |
44 | junit
45 | junit
46 |
47 |
48 | org.mockito
49 | mockito-core
50 |
51 |
52 |
53 |
54 |
55 |
56 |
57 |
58 |
59 |
60 |
61 |
62 |
63 |
64 |
65 |
66 |
67 |
68 |
69 |
70 |
71 |
72 |
--------------------------------------------------------------------------------
/common-api/src/main/java/com.mz.reactivedemo.common.api/events/Command.java:
--------------------------------------------------------------------------------
1 | package com.mz.reactivedemo.common.api.events;
2 |
3 | /**
4 | * Created by zemi on 29/09/2018.
5 | */
6 | public interface Command {
7 | }
8 |
--------------------------------------------------------------------------------
/common-api/src/main/java/com.mz.reactivedemo.common.api/events/Document.java:
--------------------------------------------------------------------------------
1 | package com.mz.reactivedemo.common.api.events;
2 |
3 | /**
4 | * Created by zemi on 29/09/2018.
5 | */
6 | public interface Document {
7 | }
8 |
--------------------------------------------------------------------------------
/common-api/src/main/java/com.mz.reactivedemo.common.api/events/DomainEvent.java:
--------------------------------------------------------------------------------
1 | package com.mz.reactivedemo.common.api.events;
2 |
3 | public interface DomainEvent extends Event {
4 | String aggregateId();
5 | }
6 |
--------------------------------------------------------------------------------
/common-api/src/main/java/com.mz.reactivedemo.common.api/events/Event.java:
--------------------------------------------------------------------------------
1 | package com.mz.reactivedemo.common.api.events;
2 |
3 |
4 | import org.immutables.value.Value;
5 |
6 | import java.io.Serializable;
7 | import java.time.Instant;
8 | import java.util.UUID;
9 |
10 | /**
11 | * Created by zemi on 29/09/2018.
12 | */
13 | public interface Event extends Serializable {
14 |
15 | @Value.Default
16 | default String eventId() {
17 | return UUID.randomUUID().toString();
18 | }
19 |
20 | @Value.Default
21 | default Instant eventCreatedAt() {
22 | return Instant.now();
23 | }
24 | }
25 |
--------------------------------------------------------------------------------
/common-persistence/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 | spring-reactive-microservices
7 | com.mz
8 | 0.0.1-SNAPSHOT
9 |
10 | 4.0.0
11 |
12 | common-persistence
13 |
14 |
15 |
16 |
17 | org.springframework.cloud
18 | spring-cloud-stream-binder-kafka-streams
19 |
20 |
21 |
22 | org.springframework.cloud
23 | spring-cloud-stream-binder-kafka
24 |
25 |
26 |
27 | org.apache.kafka
28 | kafka-streams
29 |
30 |
31 |
32 |
33 | com.typesafe.akka
34 | akka-persistence-query_2.12
35 |
36 |
37 | com.typesafe.akka
38 | akka-persistence_2.12
39 |
40 |
41 |
42 | com.typesafe.akka
43 | akka-cluster_2.12
44 |
45 |
46 |
47 | com.typesafe.akka
48 | akka-testkit_2.12
49 | 2.5.21
50 |
51 |
52 |
53 |
54 | com.github.scullxbones
55 | akka-persistence-mongo-common_2.12
56 |
57 |
58 |
59 |
60 | com.github.scullxbones
61 | akka-persistence-mongo-casbah_2.12
62 |
63 |
64 |
65 |
66 | org.mongodb
67 | casbah_2.12
68 | pom
69 |
70 |
71 |
72 |
73 | org.mongodb.scala
74 | mongo-scala-driver_2.12
75 |
76 |
77 |
78 | org.mongodb.scala
79 | mongo-scala-bson_2.12
80 |
81 |
82 |
83 |
84 | common
85 | com.mz
86 |
87 |
88 |
89 | com.mz
90 | common-api
91 |
92 |
93 | com.mz
94 | common-api
95 | 0.0.1-SNAPSHOT
96 |
97 |
98 |
99 |
100 | org.junit.jupiter
101 | junit-jupiter-api
102 | test
103 |
104 |
105 |
106 | org.junit.jupiter
107 | junit-jupiter-engine
108 | test
109 |
110 |
111 |
112 |
113 |
114 |
--------------------------------------------------------------------------------
/common-persistence/src/main/java/com/mz/reactivedemo/adapter/persistance/AggregateFactory.java:
--------------------------------------------------------------------------------
1 | package com.mz.reactivedemo.adapter.persistance;
2 |
3 | import com.mz.reactivedemo.adapter.persistance.impl.AggregateFactoryImpl;
4 | import com.mz.reactivedemo.common.aggregate.Aggregate;
5 |
6 | import java.util.function.Function;
7 |
8 | public interface AggregateFactory {
9 |
10 | Aggregate of(String id);
11 |
12 | Aggregate of(S state);
13 |
14 | static AggregateFactory build(Function> createAggregateById,
15 | Function> createAggregateByState) {
16 | return new AggregateFactoryImpl(createAggregateById, createAggregateByState);
17 | }
18 |
19 | }
20 |
--------------------------------------------------------------------------------
/common-persistence/src/main/java/com/mz/reactivedemo/adapter/persistance/AggregatePersistenceConfiguration.java:
--------------------------------------------------------------------------------
1 | package com.mz.reactivedemo.adapter.persistance;
2 |
3 | import akka.actor.ActorSystem;
4 | import com.mz.reactivedemo.adapter.persistance.impl.AggregateRepositoryImpl;
5 | import org.springframework.beans.factory.annotation.Value;
6 | import org.springframework.beans.factory.config.BeanDefinition;
7 | import org.springframework.context.annotation.Bean;
8 | import org.springframework.context.annotation.Configuration;
9 | import org.springframework.context.annotation.Scope;
10 |
11 | @Configuration
12 | public class AggregatePersistenceConfiguration {
13 |
14 | @Bean
15 | @Scope(BeanDefinition.SCOPE_SINGLETON)
16 | public ActorSystem actorSystem(@Value("${kafka.consumer.group-id}") String actorySystemName) {
17 | return ActorSystem.create(actorySystemName);
18 | }
19 |
20 | @Bean
21 | @Scope(BeanDefinition.SCOPE_PROTOTYPE)
22 | public AggregateRepository persistenceRepository(ActorSystem actorSystem) {
23 | return new AggregateRepositoryImpl(actorSystem);
24 | }
25 | }
26 |
--------------------------------------------------------------------------------
/common-persistence/src/main/java/com/mz/reactivedemo/adapter/persistance/AggregateRepository.java:
--------------------------------------------------------------------------------
1 | package com.mz.reactivedemo.adapter.persistance;
2 |
3 | import com.mz.reactivedemo.common.CommandResult;
4 | import com.mz.reactivedemo.common.api.events.Command;
5 | import reactor.core.publisher.Mono;
6 |
7 | public interface AggregateRepository {
8 |
9 | Mono> execute(String aggregateId, Command cmd, AggregateFactory aggregateFactory);
10 |
11 | }
12 |
--------------------------------------------------------------------------------
/common-persistence/src/main/java/com/mz/reactivedemo/adapter/persistance/AggregateService.java:
--------------------------------------------------------------------------------
1 | package com.mz.reactivedemo.adapter.persistance;
2 |
3 | import com.mz.reactivedemo.adapter.persistance.impl.AggregateServiceImpl;
4 | import com.mz.reactivedemo.common.api.events.Command;
5 | import com.mz.reactivedemo.common.api.events.DomainEvent;
6 | import reactor.core.publisher.Mono;
7 |
8 | import java.util.function.Consumer;
9 | import java.util.function.Function;
10 |
11 | public interface AggregateService {
12 |
13 | Mono execute(String aggregateId, Command cmd);
14 |
15 | static AggregateService of(AggregateRepository repository,
16 | AggregateFactory aggregateFactory,
17 | Function> updateView,
18 | Consumer publishChangedEvent,
19 | Consumer publishDocumentMessage) {
20 | return new AggregateServiceImpl<>(repository, aggregateFactory, updateView, publishChangedEvent,
21 | publishDocumentMessage);
22 | }
23 | }
24 |
--------------------------------------------------------------------------------
/common-persistence/src/main/java/com/mz/reactivedemo/adapter/persistance/actor/AggregatePersistenceActor.java:
--------------------------------------------------------------------------------
1 | package com.mz.reactivedemo.adapter.persistance.actor;
2 |
3 | import akka.actor.Props;
4 | import akka.event.Logging;
5 | import akka.event.LoggingAdapter;
6 | import akka.persistence.AbstractPersistentActor;
7 | import akka.persistence.PersistentRepr;
8 | import akka.persistence.RecoveryCompleted;
9 | import com.mz.reactivedemo.adapter.persistance.AggregateFactory;
10 | import com.mz.reactivedemo.common.CommandResult;
11 | import com.mz.reactivedemo.common.ValidateResult;
12 | import com.mz.reactivedemo.common.aggregate.Aggregate;
13 | import com.mz.reactivedemo.common.api.events.Command;
14 | import com.mz.reactivedemo.common.api.events.DomainEvent;
15 | import org.eclipse.collections.api.list.ImmutableList;
16 |
17 | import java.util.Optional;
18 |
19 | public class AggregatePersistenceActor extends AbstractPersistentActor {
20 |
21 | public static Props props(String id, AggregateFactory aggregateFactory) {
22 | return Props.create(AggregatePersistenceActor.class, () -> new AggregatePersistenceActor(id, aggregateFactory));
23 | }
24 |
25 | private final LoggingAdapter log = Logging.getLogger(this);
26 |
27 | private final String id;
28 |
29 | private final AggregateFactory aggregateFactory;
30 |
31 | private Optional> aggregate;
32 |
33 | private AggregatePersistenceActor(String id, AggregateFactory aggregateFactory) {
34 | this.aggregateFactory = aggregateFactory;
35 | this.id = id;
36 | this.aggregate = Optional.of(aggregateFactory.of(id));
37 | }
38 |
39 | @Override
40 | public Receive createReceiveRecover() {
41 | return receiveBuilder()
42 | .match(PersistentRepr.class, m -> log.info(m.toString()))
43 | .match(DomainEvent.class, event -> {
44 | log.info("Event to apply in recovery -> ", event);
45 | this.aggregate = Optional.of
46 | (this.aggregate.orElseGet(() -> aggregateFactory.of(this.id)).apply(event));
47 | })
48 | .match(RecoveryCompleted.class, evt -> log.info("Recovery completed. Current sequence: {}", lastSequenceNr()))
49 | .build();
50 | }
51 |
52 | @Override
53 | public Receive createReceive() {
54 | return receiveBuilder()
55 | .match(Command.class, this::processUpdate)
56 | .build();
57 | }
58 |
59 | @Override
60 | public String persistenceId() {
61 | return this.id;
62 | }
63 |
64 | private void processUpdate(Command cmd) {
65 | aggregate.ifPresentOrElse(
66 | a -> a.validate(cmd)
67 | .map(ValidateResult::events)
68 | .onFailure(this::onFailure)
69 | .toOptional()
70 | .filter(domainEvents -> !domainEvents.isEmpty())
71 | .ifPresentOrElse(
72 | domainEvents -> onSuccess(a, domainEvents),
73 | () -> {
74 | log.debug("No changes on {} aggregate", a.getClass());
75 | sender().tell(CommandResult.none(), self());
76 | }),
77 | () -> sender().tell(CommandResult.error(new RuntimeException("Wrong actor state")), self())
78 | );
79 | }
80 |
81 | private void onSuccess(Aggregate aggregate, ImmutableList events) {
82 | persistAll(events, (evt) -> {
83 | log.debug("persistAllAsync for event: {}", evt);
84 | aggregate.apply(evt);
85 | });
86 |
87 | deferAsync(events, (evt) -> {
88 | log.debug("Defer for event: {}", evt);
89 | this.aggregate.ifPresent(a -> {
90 | CommandResult commandResult = CommandResult.of(a.state(), events);
91 | sender().tell(commandResult, self());
92 | });
93 | });
94 | }
95 |
96 | private void onFailure(Throwable error) {
97 | CommandResult commandResult = CommandResult.error(error);
98 | sender().tell(commandResult, self());
99 | }
100 | }
101 |
--------------------------------------------------------------------------------
/common-persistence/src/main/java/com/mz/reactivedemo/adapter/persistance/actor/RecoveryActor.java:
--------------------------------------------------------------------------------
1 | package com.mz.reactivedemo.adapter.persistance.actor;
2 |
3 | import akka.actor.*;
4 |
5 | import java.util.UUID;
6 | import java.util.function.Supplier;
7 |
8 | public class RecoveryActor extends AbstractActor {
9 |
10 | public static Props props() {
11 | return Props.create(RecoveryActor.class);
12 | }
13 |
14 | public static class RecoverActor {
15 | public final ActorPath actorPath;
16 |
17 | public final Supplier createActor;
18 |
19 | public RecoverActor(ActorPath actorPath, Supplier createActor) {
20 | this.createActor = createActor;
21 | this.actorPath = actorPath;
22 | }
23 | }
24 |
25 | @Override
26 | public Receive createReceive() {
27 | return receiveBuilder()
28 | .match(RecoverActor.class, r -> {
29 | getContext().getSystem()
30 | .actorSelection(r.actorPath)
31 | .tell(new Identify(UUID.randomUUID().toString()), getSelf());
32 | getContext().become(actorIdentity(getSender(), r));
33 | })
34 | .build();
35 | }
36 |
37 | private Receive actorIdentity(ActorRef orgRequester, RecoverActor recoverActor) {
38 | return receiveBuilder()
39 | .match(ActorIdentity.class, actorIdentity -> {
40 | orgRequester.tell(actorIdentity.getActorRef().orElseGet(() -> recoverActor.createActor.get()), getSelf());
41 | getContext().getSystem().stop(getSelf());
42 | })
43 | .build();
44 | }
45 | }
46 |
--------------------------------------------------------------------------------
/common-persistence/src/main/java/com/mz/reactivedemo/adapter/persistance/actor/RepositoryActor.java:
--------------------------------------------------------------------------------
1 | package com.mz.reactivedemo.adapter.persistance.actor;
2 |
3 | import akka.actor.AbstractActor;
4 | import akka.actor.Props;
5 | import com.mz.reactivedemo.adapter.persistance.AggregateFactory;
6 | import com.mz.reactivedemo.common.api.events.Command;
7 |
8 | public class RepositoryActor extends AbstractActor {
9 |
10 | public static Props props() {
11 | return Props.create(RepositoryActor.class);
12 | }
13 |
14 | public static class CommandMsg {
15 |
16 | final Command cmd;
17 |
18 | final String aggregateId;
19 |
20 | final AggregateFactory aggregateFactory;
21 |
22 | public CommandMsg(Command cmd, String aggregateId, AggregateFactory aggregateFactory) {
23 | this.cmd = cmd;
24 | this.aggregateId = aggregateId;
25 | this.aggregateFactory = aggregateFactory;
26 | }
27 | }
28 |
29 | @Override
30 | public Receive createReceive() {
31 | return receiveBuilder()
32 | .match(CommandMsg.class, c -> getContext().findChild(c.aggregateId)
33 | .orElseGet(() -> getContext().actorOf(AggregatePersistenceActor.props(c.aggregateId, c.aggregateFactory),
34 | c.aggregateId))
35 | .tell(c.cmd, sender()))
36 | .build();
37 | }
38 | }
39 |
--------------------------------------------------------------------------------
/common-persistence/src/main/java/com/mz/reactivedemo/adapter/persistance/document/DocumentReadOnlyRepository.java:
--------------------------------------------------------------------------------
1 | package com.mz.reactivedemo.adapter.persistance.document;
2 |
3 | import reactor.core.publisher.Mono;
4 |
5 | public interface DocumentReadOnlyRepository {
6 |
7 | Mono get(K key);
8 |
9 | }
10 |
--------------------------------------------------------------------------------
/common-persistence/src/main/java/com/mz/reactivedemo/adapter/persistance/document/impl/DocumentReadOnlyRepositoryImpl.java:
--------------------------------------------------------------------------------
1 | package com.mz.reactivedemo.adapter.persistance.document.impl;
2 |
3 | import com.mz.reactivedemo.adapter.persistance.document.DocumentReadOnlyRepository;
4 | import org.apache.kafka.streams.state.QueryableStoreTypes;
5 | import org.apache.kafka.streams.state.ReadOnlyKeyValueStore;
6 | import org.springframework.cloud.stream.binder.kafka.streams.InteractiveQueryService;
7 | import reactor.core.publisher.Mono;
8 | import reactor.core.scheduler.Scheduler;
9 |
10 | import java.util.Optional;
11 | import java.util.function.Function;
12 |
13 | import static java.util.Objects.requireNonNull;
14 |
15 | public class DocumentReadOnlyRepositoryImpl implements DocumentReadOnlyRepository {
16 |
17 | private final InteractiveQueryService queryService;
18 |
19 | private final Scheduler scheduler;
20 |
21 | private final String documentStorageName;
22 |
23 | public DocumentReadOnlyRepositoryImpl(InteractiveQueryService queryService, Scheduler scheduler, String documentStorageName) {
24 | this.queryService = requireNonNull(queryService, "queryService is required");
25 | this.scheduler = requireNonNull(scheduler, "scheduler is required");
26 | this.documentStorageName = requireNonNull(documentStorageName, "documentStorageName is required");
27 | }
28 |
29 | @Override
30 | public Mono get(K key) {
31 | return Mono.fromCallable(this::getReadOnlyKeyValueStore)
32 | .flatMap(getValue(key))
33 | .publishOn(scheduler);
34 | }
35 |
36 | private Function, Mono> getValue(K key) {
37 | return storage -> Mono.justOrEmpty(Optional.ofNullable(key).map(storage::get));
38 | }
39 |
40 | private ReadOnlyKeyValueStore getReadOnlyKeyValueStore() {
41 | return queryService.getQueryableStore(documentStorageName, QueryableStoreTypes.keyValueStore());
42 | }
43 | }
44 |
--------------------------------------------------------------------------------
/common-persistence/src/main/java/com/mz/reactivedemo/adapter/persistance/impl/AggregateFactoryImpl.java:
--------------------------------------------------------------------------------
1 | package com.mz.reactivedemo.adapter.persistance.impl;
2 |
3 | import com.mz.reactivedemo.adapter.persistance.AggregateFactory;
4 | import com.mz.reactivedemo.common.aggregate.Aggregate;
5 |
6 | import java.util.function.Function;
7 |
8 | public class AggregateFactoryImpl implements AggregateFactory {
9 |
10 | private final Function> createAggregateById;
11 |
12 | private final Function> createAggregateByState;
13 |
14 | public AggregateFactoryImpl(Function> createAggregateById,
15 | Function> createAggregateByState) {
16 | this.createAggregateById = createAggregateById;
17 | this.createAggregateByState = createAggregateByState;
18 | }
19 |
20 | @Override
21 | public Aggregate of(String id) {
22 | return createAggregateById.apply(id);
23 | }
24 |
25 | @Override
26 | public Aggregate of(S state) {
27 | return createAggregateByState.apply(state);
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/common-persistence/src/main/java/com/mz/reactivedemo/adapter/persistance/impl/AggregateRepositoryImpl.java:
--------------------------------------------------------------------------------
1 | package com.mz.reactivedemo.adapter.persistance.impl;
2 |
3 | import akka.actor.ActorRef;
4 | import akka.actor.ActorSystem;
5 | import com.mz.reactivedemo.adapter.persistance.AggregateFactory;
6 | import com.mz.reactivedemo.adapter.persistance.AggregateRepository;
7 | import com.mz.reactivedemo.adapter.persistance.actor.RecoveryActor;
8 | import com.mz.reactivedemo.adapter.persistance.actor.RepositoryActor;
9 | import com.mz.reactivedemo.common.CommandResult;
10 | import com.mz.reactivedemo.common.api.events.Command;
11 | import org.apache.commons.logging.Log;
12 | import org.apache.commons.logging.LogFactory;
13 | import reactor.core.publisher.Mono;
14 | import reactor.core.scheduler.Schedulers;
15 |
16 | import java.time.Duration;
17 |
18 | import static akka.pattern.Patterns.ask;
19 |
20 | public class AggregateRepositoryImpl implements AggregateRepository {
21 |
22 | private static String REPOSITORY_NAME = "persistence_repository";
23 |
24 | private static final Log log = LogFactory.getLog(AggregateRepositoryImpl.class);
25 |
26 | private final ActorSystem system;
27 |
28 | private ActorRef repositoryActor;
29 |
30 | public AggregateRepositoryImpl(ActorSystem system) {
31 | this.system = system;
32 | this.repositoryActor = getRepositoryActor(system);
33 | }
34 |
35 | private ActorRef getRepositoryActor(ActorSystem system) {
36 | return system.actorOf(RepositoryActor.props(), String.format("%s_%s", system.name(),
37 | REPOSITORY_NAME));
38 | }
39 |
40 | private Mono recoverRepositoryActor() {
41 | log.info("recoverRepositoryActor() -> is going to recover RepositoryActor");
42 | return Mono.fromCompletionStage(ask(system.actorOf(RecoveryActor.props()),
43 | new RecoveryActor.RecoverActor(this.repositoryActor.path(),
44 | () -> getRepositoryActor(system)),
45 | Duration.ofMillis(5000)))
46 | .cast(ActorRef.class);
47 | }
48 |
49 | private Mono> sendCommand(ActorRef repActor, String aggregateId, Command cmd,
50 | AggregateFactory aggregateFactory) {
51 | return Mono.fromCompletionStage(ask(repActor, new RepositoryActor.CommandMsg(cmd, aggregateId,
52 | aggregateFactory), Duration.ofMillis(5000)))
53 | .publishOn(Schedulers.elastic())
54 | .map(r -> (CommandResult) r);
55 | }
56 |
57 | @Override
58 | public Mono> execute(String aggregateId, Command cmd,
59 | AggregateFactory aggregateFactory) {
60 | return sendCommand(this.repositoryActor, aggregateId, cmd, aggregateFactory)
61 | .onErrorResume(error -> recoverRepositoryActor()
62 | .flatMap(actorRef -> {
63 | this.repositoryActor = actorRef;
64 | return sendCommand(actorRef, aggregateId, cmd, aggregateFactory);
65 | })
66 | );
67 |
68 | }
69 |
70 | }
71 |
--------------------------------------------------------------------------------
/common-persistence/src/main/java/com/mz/reactivedemo/adapter/persistance/impl/AggregateServiceImpl.java:
--------------------------------------------------------------------------------
1 | package com.mz.reactivedemo.adapter.persistance.impl;
2 |
3 | import com.mz.reactivedemo.adapter.persistance.AggregateFactory;
4 | import com.mz.reactivedemo.adapter.persistance.AggregateRepository;
5 | import com.mz.reactivedemo.adapter.persistance.AggregateService;
6 | import com.mz.reactivedemo.common.CommandResult;
7 | import com.mz.reactivedemo.common.api.events.Command;
8 | import com.mz.reactivedemo.common.api.events.DomainEvent;
9 | import reactor.core.publisher.Mono;
10 |
11 | import java.util.function.Consumer;
12 | import java.util.function.Function;
13 |
14 | public class AggregateServiceImpl implements AggregateService {
15 |
16 | protected final Function> updateView;
17 |
18 | protected final Consumer publishChangedEvent;
19 |
20 | protected final Consumer publishDocumentMessage;
21 |
22 | protected final AggregateRepository repository;
23 |
24 | protected final AggregateFactory aggregateFactory;
25 |
26 | public AggregateServiceImpl(
27 | AggregateRepository repository,
28 | AggregateFactory aggregateFactory,
29 | Function> updateView,
30 | Consumer publishChangedEvent,
31 | Consumer publishDocumentMessage
32 | ) {
33 | this.updateView = updateView;
34 | this.publishChangedEvent = publishChangedEvent;
35 | this.publishDocumentMessage = publishDocumentMessage;
36 | this.repository = repository;
37 | this.aggregateFactory = aggregateFactory;
38 | }
39 |
40 | @Override
41 | public Mono execute(String aggregateId, Command cmd) {
42 | return repository.execute(aggregateId, cmd, aggregateFactory)
43 | .flatMap(this::processResult);
44 | }
45 |
46 | private Mono processResult(CommandResult result) {
47 | switch (result.status()) {
48 | case MODIFIED:
49 | if (result.state().isPresent()) {
50 | return updateView.apply(result.state().get())
51 | .doOnSuccess(s -> result.domainEvents().forEach(publishChangedEvent))
52 | .doOnSuccess(publishDocumentMessage);
53 | } else {
54 | return Mono.empty();
55 | }
56 | case ERROR:
57 | return Mono.error(result.error().orElseGet(() -> new RuntimeException("Generic error")));
58 | case NOT_MODIFIED:
59 | default:
60 | return Mono.empty();
61 | }
62 | }
63 |
64 | }
65 |
--------------------------------------------------------------------------------
/common-persistence/src/test/java/com/mz/reactivedemo/adapter/persistance/actor/RecoveryActorTest.java:
--------------------------------------------------------------------------------
1 | package com.mz.reactivedemo.adapter.persistance.actor;
2 |
3 | import akka.actor.ActorPath;
4 | import akka.actor.ActorRef;
5 | import akka.actor.ActorSystem;
6 | import akka.testkit.javadsl.TestKit;
7 | import org.junit.jupiter.api.AfterAll;
8 | import org.junit.jupiter.api.BeforeAll;
9 | import org.junit.jupiter.api.Test;
10 |
11 | import static org.junit.jupiter.api.Assertions.assertEquals;
12 |
13 | public class RecoveryActorTest {
14 |
15 | static ActorSystem system;
16 |
17 | @BeforeAll
18 | public static void setup() {
19 | system = ActorSystem.create();
20 | }
21 |
22 | @Test
23 | void recoverActorNotEquals() {
24 | final var testMsg = "test";
25 | final var testProbe = new TestKit(system);
26 | final var repositoryActor = new TestKit(system);
27 | var repoActorPath = ActorPath.fromString("akka://default/user/testNotEquals");
28 | var repoActorNew = new TestKit(system);
29 | final var recoveryActor = system.actorOf(RecoveryActor.props());
30 | recoveryActor.tell(new RecoveryActor.RecoverActor(repoActorPath, () -> repoActorNew.getRef()), testProbe.getRef());
31 |
32 | ActorRef result = testProbe.expectMsgClass(ActorRef.class);
33 | result.tell(testMsg, testProbe.getRef());
34 |
35 | repoActorNew.expectMsg(testMsg);
36 | }
37 |
38 | @Test
39 | void recoverActorEquals() {
40 | final var testProbe = new TestKit(system);
41 |
42 | final var repositoryActor = system.actorOf(RepositoryActor.props());
43 |
44 | var repoActorPath = ActorPath.fromString("akka://default/user/testEquals");
45 | final var recoveryActor = system.actorOf(RecoveryActor.props());
46 |
47 | recoveryActor.tell(new RecoveryActor.RecoverActor(repoActorPath, () -> repositoryActor), testProbe.getRef());
48 | var repoActorNew = testProbe.expectMsgClass(ActorRef.class);
49 | assertEquals(repoActorNew, repositoryActor);
50 | }
51 |
52 | @AfterAll
53 | public static void teardown() {
54 | TestKit.shutdownActorSystem(system);
55 | system = null;
56 | }
57 | }
58 |
--------------------------------------------------------------------------------
/common/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
5 |
6 | spring-reactive-microservices
7 | com.mz
8 | 0.0.1-SNAPSHOT
9 |
10 |
11 | jar
12 |
13 | 4.0.0
14 |
15 | common
16 | com.mz
17 |
18 |
19 |
20 | com.mz
21 | common-api
22 |
23 |
24 |
25 | org.eclipse.collections
26 | eclipse-collections-api
27 |
28 |
29 |
30 | org.eclipse.collections
31 | eclipse-collections
32 |
33 |
34 |
35 |
36 | org.immutables
37 | value
38 | provided
39 |
40 |
41 |
42 | org.springframework
43 | spring-web
44 | compile
45 |
46 |
47 | org.springframework
48 | spring-webflux
49 | compile
50 |
51 |
52 |
53 | org.springframework.boot
54 | spring-boot-starter-data-mongodb-reactive
55 |
56 |
57 |
58 | com.fasterxml.jackson.core
59 | jackson-databind
60 | compile
61 |
62 |
63 |
64 | javax.annotation
65 | javax.annotation-api
66 |
67 |
68 |
69 | io.projectreactor.kafka
70 | reactor-kafka
71 | compile
72 |
73 |
74 |
75 |
76 | org.junit.jupiter
77 | junit-jupiter-api
78 | test
79 |
80 |
81 |
82 | org.junit.jupiter
83 | junit-jupiter-engine
84 | test
85 |
86 |
87 |
88 |
89 |
90 |
91 |
92 |
93 | org.apache.maven.plugins
94 | maven-failsafe-plugin
95 |
96 |
97 |
98 |
99 |
100 |
--------------------------------------------------------------------------------
/common/src/main/java/com/mz/reactivedemo/common/CommandResult.java:
--------------------------------------------------------------------------------
1 | package com.mz.reactivedemo.common;
2 |
3 | import com.mz.reactivedemo.common.api.events.DomainEvent;
4 | import org.eclipse.collections.api.list.ImmutableList;
5 | import org.eclipse.collections.impl.factory.Lists;
6 | import org.immutables.value.Value;
7 |
8 | import java.util.Optional;
9 |
10 | @Value.Immutable
11 | public interface CommandResult {
12 |
13 | enum StatusCode {
14 | NOT_MODIFIED,
15 | MODIFIED,
16 | BAD_COMMAND,
17 | ERROR
18 | }
19 |
20 | StatusCode status();
21 |
22 | Optional error();
23 |
24 | Optional state();
25 |
26 | ImmutableList domainEvents();
27 |
28 | static CommandResult of(S state, ImmutableList domainEvents) {
29 | return ImmutableCommandResult.builder()
30 | .state(state)
31 | .status(domainEvents.size() > 0 ? StatusCode.MODIFIED : StatusCode.NOT_MODIFIED)
32 | .domainEvents(domainEvents)
33 | .build();
34 | }
35 |
36 | static CommandResult error(Throwable error) {
37 | return ImmutableCommandResult.builder()
38 | .status(StatusCode.ERROR)
39 | .domainEvents(Lists.immutable.empty())
40 | .error(error)
41 | .build();
42 | }
43 |
44 | static CommandResult none() {
45 | return ImmutableCommandResult.builder()
46 | .status(StatusCode.NOT_MODIFIED)
47 | .domainEvents(Lists.immutable.empty())
48 | .build();
49 | }
50 | }
51 |
--------------------------------------------------------------------------------
/common/src/main/java/com/mz/reactivedemo/common/CommandResultState.java:
--------------------------------------------------------------------------------
1 | package com.mz.reactivedemo.common;
2 |
3 | public enum CommandResultState {
4 | CHANGED, NONE, ERROR
5 | }
6 |
--------------------------------------------------------------------------------
/common/src/main/java/com/mz/reactivedemo/common/KafkaMapper.java:
--------------------------------------------------------------------------------
1 | package com.mz.reactivedemo.common;
2 |
3 | import com.fasterxml.jackson.core.JsonProcessingException;
4 | import com.fasterxml.jackson.databind.ObjectMapper;
5 | import org.apache.kafka.clients.producer.ProducerRecord;
6 | import reactor.kafka.sender.SenderRecord;
7 |
8 | import java.util.function.Function;
9 |
10 | public enum KafkaMapper {
11 |
12 | FN;
13 |
14 | public Function mapFromJson(ObjectMapper mapper, Class clazz) {
15 | return json -> {
16 | try {
17 | return mapper.readValue(json, clazz);
18 | } catch (JsonProcessingException e) {
19 | throw new RuntimeException(e);
20 | }
21 | };
22 | }
23 |
24 | public Function> mapToRecord(
25 | String topic,
26 | ObjectMapper mapper,
27 | Function idMapper
28 | ) {
29 | return value -> {
30 | try {
31 | final var producerRecord = new ProducerRecord<>(
32 | topic, idMapper.apply(value),
33 | mapper.writeValueAsString(value)
34 | );
35 | return SenderRecord.create(producerRecord, value);
36 | } catch (JsonProcessingException e) {
37 | throw new RuntimeException(e);
38 | }
39 | };
40 | }
41 | }
42 |
--------------------------------------------------------------------------------
/common/src/main/java/com/mz/reactivedemo/common/ValidateResult.java:
--------------------------------------------------------------------------------
1 | package com.mz.reactivedemo.common;
2 |
3 | import com.mz.reactivedemo.common.api.events.DomainEvent;
4 | import org.eclipse.collections.api.list.ImmutableList;
5 | import org.immutables.value.Value;
6 |
7 | @Value.Immutable
8 | public interface ValidateResult {
9 |
10 | ImmutableList events();
11 |
12 | static ImmutableValidateResult.Builder builder() {
13 | return ImmutableValidateResult.builder();
14 | }
15 | }
16 |
--------------------------------------------------------------------------------
/common/src/main/java/com/mz/reactivedemo/common/aggregate/AbstractRootAggregate.java:
--------------------------------------------------------------------------------
1 | package com.mz.reactivedemo.common.aggregate;
2 |
3 | import com.mz.reactivedemo.common.ValidateResult;
4 | import com.mz.reactivedemo.common.api.events.Command;
5 | import com.mz.reactivedemo.common.api.events.DomainEvent;
6 | import com.mz.reactivedemo.common.util.Try;
7 | import org.eclipse.collections.api.list.ImmutableList;
8 |
9 | /**
10 | * Created by zemi on 04/01/2019.
11 | */
12 | public abstract class AbstractRootAggregate implements Aggregate {
13 |
14 | protected AggregateStatus status;
15 |
16 | protected abstract ImmutableList behavior(Command cmd);
17 |
18 | protected abstract String getRootEntityId();
19 |
20 | public abstract Aggregate apply(DomainEvent event);
21 |
22 | @Override
23 | public Try validate(Command cmd) {
24 | return Try.of(() -> behavior(cmd))
25 | .map(events -> ValidateResult.builder()
26 | .events(events)
27 | .build());
28 | }
29 | }
30 |
--------------------------------------------------------------------------------
/common/src/main/java/com/mz/reactivedemo/common/aggregate/Aggregate.java:
--------------------------------------------------------------------------------
1 | package com.mz.reactivedemo.common.aggregate;
2 |
3 | import com.mz.reactivedemo.common.ValidateResult;
4 | import com.mz.reactivedemo.common.api.events.Command;
5 | import com.mz.reactivedemo.common.api.events.DomainEvent;
6 | import com.mz.reactivedemo.common.util.Try;
7 |
8 | public interface Aggregate {
9 |
10 | Aggregate apply(DomainEvent event);
11 |
12 | Try validate(Command cmd);
13 |
14 | S state();
15 | }
16 |
--------------------------------------------------------------------------------
/common/src/main/java/com/mz/reactivedemo/common/aggregate/AggregateStatus.java:
--------------------------------------------------------------------------------
1 | package com.mz.reactivedemo.common.aggregate;
2 |
3 | public enum AggregateStatus {
4 | NEW, EXISTING
5 | }
6 |
--------------------------------------------------------------------------------
/common/src/main/java/com/mz/reactivedemo/common/aggregate/Id.java:
--------------------------------------------------------------------------------
1 | package com.mz.reactivedemo.common.aggregate;
2 |
3 | /**
4 | * Created by zemi on 30/09/2018.
5 | */
6 | public class Id extends StringValue {
7 |
8 | public Id(java.lang.String value) {
9 | super(value);
10 | }
11 |
12 | }
13 |
--------------------------------------------------------------------------------
/common/src/main/java/com/mz/reactivedemo/common/aggregate/StringValue.java:
--------------------------------------------------------------------------------
1 | package com.mz.reactivedemo.common.aggregate;
2 |
3 | import java.util.Objects;
4 |
5 | /**
6 | * Created by zemi on 30/09/2018.
7 | */
8 | public class StringValue {
9 | public final java.lang.String value;
10 |
11 | public StringValue(java.lang.String value) {
12 | if (Objects.isNull(value) || value.isEmpty()) {
13 | throw new RuntimeException("String value is null or empty!");
14 | }
15 | this.value = value;
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/common/src/main/java/com/mz/reactivedemo/common/http/ErrorMessage.java:
--------------------------------------------------------------------------------
1 | package com.mz.reactivedemo.common.http;
2 |
3 | import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
4 | import com.fasterxml.jackson.databind.annotation.JsonSerialize;
5 | import org.immutables.value.Value;
6 |
7 | /**
8 | * Created by zemi on 02/10/2018.
9 | */
10 | @Value.Immutable
11 | @JsonSerialize(as = ImmutableErrorMessage.class)
12 | @JsonDeserialize(as = ImmutableErrorMessage.class)
13 | public interface ErrorMessage {
14 |
15 | String error();
16 |
17 | static ImmutableErrorMessage.Builder builder() {
18 | return ImmutableErrorMessage.builder();
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/common/src/main/java/com/mz/reactivedemo/common/http/HttpErrorHandler.java:
--------------------------------------------------------------------------------
1 | package com.mz.reactivedemo.common.http;
2 |
3 | import org.springframework.http.HttpStatus;
4 | import org.springframework.http.MediaType;
5 | import org.springframework.web.reactive.function.server.ServerRequest;
6 | import org.springframework.web.reactive.function.server.ServerResponse;
7 | import reactor.core.publisher.Mono;
8 |
9 | import static org.springframework.web.reactive.function.BodyInserters.fromValue;
10 |
11 | public enum HttpErrorHandler {
12 | FN;
13 |
14 | public Mono onError(E e, ServerRequest req) {
15 | return Mono.just(ErrorMessage.builder().error(e.getMessage()).build())
16 | .flatMap(error -> ServerResponse.status(HttpStatus.BAD_REQUEST)
17 | .contentType(MediaType.APPLICATION_JSON)
18 | .body(fromValue(error)));
19 | }
20 | }
21 |
--------------------------------------------------------------------------------
/common/src/main/java/com/mz/reactivedemo/common/http/HttpHandler.java:
--------------------------------------------------------------------------------
1 | package com.mz.reactivedemo.common.http;
2 |
3 | import org.springframework.http.MediaType;
4 | import org.springframework.web.reactive.function.server.RouterFunction;
5 | import org.springframework.web.reactive.function.server.ServerResponse;
6 | import reactor.core.publisher.Mono;
7 |
8 | import static org.springframework.web.reactive.function.BodyInserters.fromValue;
9 |
10 | /**
11 | * Created by zemi on 02/10/2018.
12 | */
13 | public interface HttpHandler {
14 |
15 | default Mono mapToResponse(T result) {
16 | return ServerResponse.ok()
17 | .contentType(MediaType.APPLICATION_JSON).body(fromValue(result));
18 | }
19 |
20 | RouterFunction route();
21 | }
22 |
--------------------------------------------------------------------------------
/common/src/main/java/com/mz/reactivedemo/common/util/AbstractMatch.java:
--------------------------------------------------------------------------------
1 | package com.mz.reactivedemo.common.util;
2 |
3 | import java.util.Optional;
4 |
5 | abstract class AbstractMatch {
6 |
7 | protected final Object o;
8 |
9 | protected AbstractMatch(Object o) {
10 | this.o = o;
11 | }
12 |
13 | protected boolean casePattern(Object obj, Class type) {
14 | return Optional.ofNullable(type)
15 | .flatMap(t -> Optional.ofNullable(obj).map(t::isInstance)).orElse(false);
16 | }
17 | }
18 |
--------------------------------------------------------------------------------
/common/src/main/java/com/mz/reactivedemo/common/util/CaseMatch.java:
--------------------------------------------------------------------------------
1 | package com.mz.reactivedemo.common.util;
2 |
3 | import java.util.Objects;
4 | import java.util.function.Consumer;
5 |
6 | public class CaseMatch extends AbstractMatch {
7 |
8 | private boolean executed = false;
9 |
10 | private CaseMatch(Object o) {
11 | super(o);
12 | }
13 |
14 | public CaseMatch when(Class type, Runnable statement) {
15 | Objects.requireNonNull(type);
16 | Objects.requireNonNull(statement);
17 | if (casePattern(o, type) && !executed) {
18 | statement.run();
19 | this.executed = true;
20 | }
21 | return this;
22 | }
23 |
24 | public CaseMatch when(Class type, Consumer statement) {
25 | Objects.requireNonNull(type);
26 | Objects.requireNonNull(statement);
27 | if (casePattern(o, type) && !executed) {
28 | statement.accept(type.cast(o));
29 | this.executed = true;
30 | }
31 | return this;
32 | }
33 |
34 | public CaseMatch orElse(Runnable statement) {
35 | Objects.requireNonNull(statement);
36 | if (!executed) {
37 | statement.run();
38 | this.executed = true;
39 | }
40 | return this;
41 | }
42 |
43 | static public CaseMatch match(Object o) {
44 | return new CaseMatch(o);
45 | }
46 |
47 | }
48 |
--------------------------------------------------------------------------------
/common/src/main/java/com/mz/reactivedemo/common/util/Logger.java:
--------------------------------------------------------------------------------
1 | package com.mz.reactivedemo.common.util;
2 |
3 | import org.apache.commons.logging.Log;
4 |
5 | import java.util.function.Supplier;
6 |
7 | /**
8 | * Created by zemi on 22/06/2018.
9 | */
10 | public class Logger {
11 |
12 | private final Log logger;
13 |
14 | public Logger(Log logger) {
15 | this.logger = logger;
16 | }
17 |
18 | public void debug(Supplier