├── .gitignore
├── .mvn
└── wrapper
│ ├── maven-wrapper.jar
│ └── maven-wrapper.properties
├── Dockerfile
├── Makefile
├── README.md
├── docker-compose.local.yaml
├── docker-compose.yaml
├── k8s
└── microservice
│ ├── .helmignore
│ ├── Chart.yaml
│ ├── templates
│ ├── microservice.yaml
│ ├── postgres.yaml
│ └── zipkin.yaml
│ └── values.yaml
├── monitoring
└── prometheus.yml
├── mvnw
├── mvnw.cmd
├── pom.xml
└── src
├── main
├── kotlin
│ └── com
│ │ └── example
│ │ └── alexbryksin
│ │ ├── KotlinSpringGrpcApplication.kt
│ │ ├── configuration
│ │ ├── DataLoaderConfig.kt
│ │ ├── FakerConfig.kt
│ │ ├── GrpcServerConfig.kt
│ │ └── SwaggerOpenAPIConfiguration.kt
│ │ ├── delivery
│ │ ├── grpc
│ │ │ ├── BankAccountGrpcService.kt
│ │ │ └── GrpcExceptionAdvice.kt
│ │ └── http
│ │ │ ├── BankAccountController.kt
│ │ │ └── GlobalControllerAdvice.kt
│ │ ├── domain
│ │ ├── BankAccount.kt
│ │ └── Currency.kt
│ │ ├── dto
│ │ ├── CreateBankAccountDto.kt
│ │ ├── DepositBalanceDto.kt
│ │ ├── ErrorHttpResponse.kt
│ │ ├── FindByBalanceRequestDto.kt
│ │ ├── SuccessBankAccountResponse.kt
│ │ └── WithdrawBalanceDto.kt
│ │ ├── exceptions
│ │ ├── BankAccountNotFoundException.kt
│ │ └── InvalidAmountException.kt
│ │ ├── interceptors
│ │ ├── GlobalInterceptorConfiguration.kt
│ │ └── LogGrpcInterceptor.kt
│ │ ├── repositories
│ │ ├── BankPostgresRepository.kt
│ │ ├── BankPostgresRepositoryImpl.kt
│ │ └── BankRepository.kt
│ │ ├── services
│ │ ├── BankAccountService.kt
│ │ └── BankAccountServiceImpl.kt
│ │ └── utils
│ │ └── TracingUtils.kt
├── proto
│ └── bank_account.proto
└── resources
│ ├── application.properties
│ └── db
│ └── migration
│ └── V1__initial_setup.sql
└── test
└── kotlin
└── com
└── example
└── alexbryksin
└── KotlinSpringGrpcApplicationTests.kt
/.gitignore:
--------------------------------------------------------------------------------
1 | HELP.md
2 | target/
3 | !.mvn/wrapper/maven-wrapper.jar
4 | !**/src/main/**/target/
5 | !**/src/test/**/target/
6 |
7 | ### STS ###
8 | .apt_generated
9 | .classpath
10 | .factorypath
11 | .project
12 | .settings
13 | .springBeans
14 | .sts4-cache
15 |
16 | ### IntelliJ IDEA ###
17 | .idea
18 | *.iws
19 | *.iml
20 | *.ipr
21 |
22 | ### NetBeans ###
23 | /nbproject/private/
24 | /nbbuild/
25 | /dist/
26 | /nbdist/
27 | /.nb-gradle/
28 | build/
29 | !**/src/main/**/build/
30 | !**/src/test/**/build/
31 |
32 | ### VS Code ###
33 | .vscode/
34 | docker_data
--------------------------------------------------------------------------------
/.mvn/wrapper/maven-wrapper.jar:
--------------------------------------------------------------------------------
https://raw.githubusercontent.com/AleksK1NG/Kotlin-Spring-gRPC-Microservice/e3a842ad7ceb9ba3c9b9b2485f78aa46ee76bcc6/.mvn/wrapper/maven-wrapper.jar
--------------------------------------------------------------------------------
/.mvn/wrapper/maven-wrapper.properties:
--------------------------------------------------------------------------------
1 | distributionUrl=https://repo.maven.apache.org/maven2/org/apache/maven/apache-maven/3.8.6/apache-maven-3.8.6-bin.zip
2 | wrapperUrl=https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar
3 |
--------------------------------------------------------------------------------
/Dockerfile:
--------------------------------------------------------------------------------
1 | FROM --platform=linux/arm64 azul/zulu-openjdk-alpine:17 as builder
2 | ARG JAR_FILE=target/KotlinSpringGrpc-0.0.1-SNAPSHOT.jar
3 | COPY ${JAR_FILE} application.jar
4 | RUN java -Djarmode=layertools -jar application.jar extract
5 |
6 | FROM azul/zulu-openjdk-alpine:17
7 | COPY --from=builder dependencies/ ./
8 | COPY --from=builder snapshot-dependencies/ ./
9 | COPY --from=builder spring-boot-loader/ ./
10 | COPY --from=builder application/ ./
11 | ENTRYPOINT ["java", "org.springframework.boot.loader.JarLauncher", "-XX:MaxRAMPercentage=75", "-XX:+UseG1GC"]
--------------------------------------------------------------------------------
/Makefile:
--------------------------------------------------------------------------------
1 | .PHONY:
2 |
3 | # ==============================================================================
4 | # Docker
5 |
6 | local:
7 | @echo Starting local docker compose
8 | docker-compose -f docker-compose.local.yaml up -d --build
9 |
10 | develop:
11 | mvn clean package -Dmaven.test.skip
12 | @echo Starting docker compose
13 | docker-compose -f docker-compose.yaml up -d --build
14 |
15 |
16 | # ==============================================================================
17 | # Docker and k8s support grafana - prom-operator
18 |
19 | FILES := $(shell docker ps -aq)
20 |
21 | down-local:
22 | docker stop $(FILES)
23 | docker rm $(FILES)
24 |
25 | clean:
26 | docker system prune -f
27 |
28 | logs-local:
29 | docker logs -f $(FILES)
30 |
31 |
32 | upload:
33 | mvn clean package -Dmaven.test.skip
34 | docker build -t alexanderbryksin/kotlin_spring_grpc_microservice:latest --platform=linux/arm64 -f ./Dockerfile .
35 | docker push alexanderbryksin/kotlin_spring_grpc_microservice:latest
36 |
37 |
38 | k8s_apply:
39 | kubectl apply -f k8s/microservice/templates
40 |
41 | k8s_delete:
42 | kubectl delete -f k8s/microservice/templates
43 |
44 | helm_install:
45 | kubens default
46 | helm install -f k8s/microservice/values.yaml microservices k8s/microservice
47 |
48 | helm_uninstall:
49 | kubens default
50 | helm uninstall microservices
51 |
52 | helm_install_all:
53 | helm repo add prometheus-community https://prometheus-community.github.io/helm-charts
54 | helm repo update
55 | kubectl create namespace monitoring
56 | helm install monitoring prometheus-community/kube-prometheus-stack -n monitoring
57 | kubens default
58 | helm install -f k8s/microservice/values.yaml microservices k8s/microservice
59 |
60 | helm_uninstall_all:
61 | kubens monitoring
62 | helm uninstall monitoring
63 | kubens default
64 | helm uninstall microservices
65 | kubectl delete namespace monitoring
66 |
--------------------------------------------------------------------------------
/README.md:
--------------------------------------------------------------------------------
1 | ### Kotlin, Spring WebFlux, gRPC microservice 👋✨💫
2 |
3 | #### 👨💻 Full list what has been used:
4 | [Spring](https://spring.io/) web framework
5 | [Spring WebFlux](https://docs.spring.io/spring-framework/docs/current/reference/html/web-reactive.html) Reactive REST Services
6 | [gRPC](https://grpc.io/docs/languages/kotlin/quickstart/) Kotlin gRPC
7 | [gRPC-Spring-Boot-Starter](https://yidongnan.github.io/grpc-spring-boot-starter/en/) gRPC Spring Boot Starter
8 | [Spring Data R2DBC](https://spring.io/projects/spring-data-r2dbc) a specification to integrate SQL databases using reactive drivers
9 | [Zipkin](https://zipkin.io/) open source, end-to-end distributed [tracing](https://opentracing.io/)
10 | [Spring Cloud Sleuth](https://docs.spring.io/spring-cloud-sleuth/docs/current-SNAPSHOT/reference/html/index.html) autoconfiguration for distributed tracing
11 | [Prometheus](https://prometheus.io/) monitoring and alerting
12 | [Grafana](https://grafana.com/) for to compose observability dashboards with everything from Prometheus
13 | [Kubernetes](https://kubernetes.io/) automating deployment, scaling, and management of containerized applications
14 | [Docker](https://www.docker.com/) and docker-compose
15 | [Helm](https://helm.sh/) The package manager for Kubernetes
16 | [Flywaydb](https://flywaydb.org/) for migrations
17 |
18 | All UI interfaces will be available on ports:
19 |
20 | #### Swagger UI: http://localhost:8000/webjars/swagger-ui/index.html
21 |
22 |
23 | #### Grafana UI: http://localhost:3000
24 |
25 |
26 | #### Zipkin UI: http://localhost:9411
27 |
28 |
29 | #### Prometheus UI: http://localhost:9090
30 |
31 |
32 |
33 | For local development 🙌👨💻🚀:
34 |
35 | ```
36 | make local // for run docker compose
37 | ```
38 | or
39 | ```
40 | make develop // run all in docker compose
41 | ```
--------------------------------------------------------------------------------
/docker-compose.local.yaml:
--------------------------------------------------------------------------------
1 | version: "3.9"
2 |
3 | services:
4 | microservices_postgresql:
5 | image: postgres:latest
6 | container_name: microservices_postgresql
7 | expose:
8 | - "5432"
9 | ports:
10 | - "5432:5432"
11 | restart: always
12 | environment:
13 | - POSTGRES_USER=postgres
14 | - POSTGRES_PASSWORD=postgres
15 | - POSTGRES_DB=bank_accounts
16 | - POSTGRES_HOST=5432
17 | command: -p 5432
18 | volumes:
19 | - ./docker_data/microservices_pgdata:/var/lib/postgresql/data
20 | networks: [ "microservices" ]
21 |
22 | prometheus:
23 | image: prom/prometheus:latest
24 | container_name: prometheus
25 | ports:
26 | - "9090:9090"
27 | command:
28 | - --config.file=/etc/prometheus/prometheus.yml
29 | volumes:
30 | - ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml:ro
31 | networks: [ "microservices" ]
32 |
33 | node_exporter:
34 | container_name: microservices_node_exporter
35 | restart: always
36 | image: prom/node-exporter
37 | ports:
38 | - '9101:9100'
39 | networks: [ "microservices" ]
40 |
41 | grafana:
42 | container_name: microservices_grafana
43 | restart: always
44 | image: grafana/grafana
45 | ports:
46 | - '3000:3000'
47 | networks: [ "microservices" ]
48 |
49 | zipkin:
50 | image: openzipkin/zipkin:latest
51 | restart: always
52 | container_name: microservices_zipkin
53 | ports:
54 | - "9411:9411"
55 | networks: [ "microservices" ]
56 |
57 | networks:
58 | microservices:
59 | name: microservices
--------------------------------------------------------------------------------
/docker-compose.yaml:
--------------------------------------------------------------------------------
1 | version: "3.9"
2 |
3 | services:
4 | microservice:
5 | platform: linux/arm64
6 | build:
7 | context: .
8 | dockerfile: Dockerfile
9 | container_name: microservice
10 | expose:
11 | - "8000"
12 | ports:
13 | - "8000:8000"
14 | - "8080:8080"
15 | environment:
16 | - SPRING_APPLICATION_NAME=microservice
17 | - SERVER_PORT=8080
18 | - SPRING_ZIPKIN_BASE_URL=http://host.docker.internal:9411
19 | - SPRING_R2DBC_URL=r2dbc:postgresql://host.docker.internal:5432/bank_accounts
20 | - SPRING_FLYWAY_URL=jdbc:postgresql://host.docker.internal:5432/bank_accounts
21 | - SPRING_REDIS_HOST=host.docker.internal
22 | - JAVA_OPTS="-XX:+UseG1GC -XX:MaxRAMPercentage=75"
23 | depends_on:
24 | - microservices_postgresql
25 | - prometheus
26 | - grafana
27 | - zipkin
28 | - node_exporter
29 | networks: [ "microservices" ]
30 |
31 | microservices_postgresql:
32 | image: postgres:latest
33 | container_name: microservices_postgresql
34 | expose:
35 | - "5432"
36 | ports:
37 | - "5432:5432"
38 | restart: always
39 | environment:
40 | - POSTGRES_USER=postgres
41 | - POSTGRES_PASSWORD=postgres
42 | - POSTGRES_DB=bank_accounts
43 | - POSTGRES_HOST=5432
44 | command: -p 5432
45 | volumes:
46 | - ./docker_data/microservices_pgdata:/var/lib/postgresql/data
47 | networks: [ "microservices" ]
48 |
49 | prometheus:
50 | image: prom/prometheus:latest
51 | container_name: prometheus
52 | ports:
53 | - "9090:9090"
54 | command:
55 | - --config.file=/etc/prometheus/prometheus.yml
56 | volumes:
57 | - ./monitoring/prometheus.yml:/etc/prometheus/prometheus.yml:ro
58 | networks: [ "microservices" ]
59 |
60 | node_exporter:
61 | container_name: microservices_node_exporter
62 | restart: always
63 | image: prom/node-exporter
64 | ports:
65 | - '9101:9100'
66 | networks: [ "microservices" ]
67 |
68 | grafana:
69 | container_name: microservices_grafana
70 | restart: always
71 | image: grafana/grafana
72 | ports:
73 | - '3000:3000'
74 | networks: [ "microservices" ]
75 |
76 | zipkin:
77 | image: openzipkin/zipkin:latest
78 | restart: always
79 | container_name: microservices_zipkin
80 | ports:
81 | - "9411:9411"
82 | networks: [ "microservices" ]
83 |
84 | networks:
85 | microservices:
86 | name: microservices
--------------------------------------------------------------------------------
/k8s/microservice/.helmignore:
--------------------------------------------------------------------------------
1 | # Patterns to ignore when building packages.
2 | # This supports shell glob matching, relative path matching, and
3 | # negation (prefixed with !). Only one pattern per line.
4 | .DS_Store
5 | # Common VCS dirs
6 | .git/
7 | .gitignore
8 | .bzr/
9 | .bzrignore
10 | .hg/
11 | .hgignore
12 | .svn/
13 | # Common backup files
14 | *.swp
15 | *.bak
16 | *.tmp
17 | *.orig
18 | *~
19 | # Various IDEs
20 | .project
21 | .idea/
22 | *.tmproj
23 | .vscode/
24 |
--------------------------------------------------------------------------------
/k8s/microservice/Chart.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: v2
2 | name: microservice
3 | description: A Helm chart for Kubernetes
4 |
5 | # A chart can be either an 'application' or a 'library' chart.
6 | #
7 | # Application charts are a collection of templates that can be packaged into versioned archives
8 | # to be deployed.
9 | #
10 | # Library charts provide useful utilities or functions for the chart developer. They're included as
11 | # a dependency of application charts to inject those utilities and functions into the rendering
12 | # pipeline. Library charts do not define any templates and therefore cannot be deployed.
13 | type: application
14 |
15 | # This is the chart version. This version number should be incremented each time you make changes
16 | # to the chart and its templates, including the app version.
17 | # Versions are expected to follow Semantic Versioning (https://semver.org/)
18 | version: 0.1.0
19 |
20 | # This is the version number of the application being deployed. This version number should be
21 | # incremented each time you make changes to the application. Versions are not expected to
22 | # follow Semantic Versioning. They should reflect the version the application is using.
23 | # It is recommended to use it with quotes.
24 | appVersion: "1.16.0"
25 |
--------------------------------------------------------------------------------
/k8s/microservice/templates/microservice.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: {{ .Values.microservice.name }}
5 | labels:
6 | app: {{ .Values.microservice.name }}
7 | spec:
8 | replicas: {{ .Values.microservice.replicas }}
9 | template:
10 | metadata:
11 | name: {{ .Values.microservice.name }}
12 | labels:
13 | app: {{ .Values.microservice.name }}
14 | spec:
15 | containers:
16 | - name: {{ .Values.microservice.name }}
17 | image: {{ .Values.microservice.image }}
18 | imagePullPolicy: Always
19 | resources:
20 | requests:
21 | memory: {{ .Values.microservice.resources.requests.memory }}
22 | cpu: {{ .Values.microservice.resources.requests.cpu }}
23 | limits:
24 | memory: {{ .Values.microservice.resources.limits.memory }}
25 | cpu: {{ .Values.microservice.resources.limits.cpu }}
26 | livenessProbe:
27 | httpGet:
28 | port: {{ .Values.microservice.livenessProbe.httpGet.port }}
29 | path: {{ .Values.microservice.livenessProbe.httpGet.path }}
30 | initialDelaySeconds: {{ .Values.microservice.livenessProbe.initialDelaySeconds }}
31 | periodSeconds: {{ .Values.microservice.livenessProbe.periodSeconds }}
32 | readinessProbe:
33 | httpGet:
34 | port: {{ .Values.microservice.readinessProbe.httpGet.port }}
35 | path: {{ .Values.microservice.readinessProbe.httpGet.path }}
36 | initialDelaySeconds: {{ .Values.microservice.readinessProbe.initialDelaySeconds }}
37 | periodSeconds: {{ .Values.microservice.readinessProbe.periodSeconds }}
38 | ports:
39 | - containerPort: {{ .Values.microservice.ports.http.containerPort }}
40 | name: {{ .Values.microservice.ports.http.name }}
41 | - containerPort: {{ .Values.microservice.ports.grpc.containerPort}}
42 | name: {{ .Values.microservice.ports.grpc.name }}
43 | env:
44 | - name: SPRING_APPLICATION_NAME
45 | value: microservice_k8s
46 | - name: JAVA_OPTS
47 | value: "-XX:+UseG1GC -XX:MaxRAMPercentage=75"
48 | - name: SERVER_PORT
49 | valueFrom:
50 | configMapKeyRef:
51 | key: server_port
52 | name: {{ .Values.microservice.name }}-config-map
53 | - name: GRPC_SERVER_PORT
54 | valueFrom:
55 | configMapKeyRef:
56 | key: grpc_server_port
57 | name: {{ .Values.microservice.name }}-config-map
58 | - name: SPRING_ZIPKIN_BASE_URL
59 | valueFrom:
60 | configMapKeyRef:
61 | key: zipkin_base_url
62 | name: {{ .Values.microservice.name }}-config-map
63 | - name: SPRING_R2DBC_URL
64 | valueFrom:
65 | configMapKeyRef:
66 | key: r2dbc_url
67 | name: {{ .Values.microservice.name }}-config-map
68 | - name: SPRING_FLYWAY_URL
69 | valueFrom:
70 | configMapKeyRef:
71 | key: flyway_url
72 | name: {{ .Values.microservice.name }}-config-map
73 | restartPolicy: Always
74 | terminationGracePeriodSeconds: {{ .Values.microservice.terminationGracePeriodSeconds }}
75 | selector:
76 | matchLabels:
77 | app: {{ .Values.microservice.name }}
78 |
79 | ---
80 |
81 | apiVersion: v1
82 | kind: Service
83 | metadata:
84 | name: {{ .Values.microservice.name }}-service
85 | labels:
86 | app: {{ .Values.microservice.name }}
87 | spec:
88 | selector:
89 | app: {{ .Values.microservice.name }}
90 | ports:
91 | - port: {{ .Values.microservice.service.httpPort }}
92 | name: http
93 | protocol: TCP
94 | targetPort: http
95 | - port: {{ .Values.microservice.service.grpcPort }}
96 | name: grpc
97 | protocol: TCP
98 | targetPort: grpc
99 | type: ClusterIP
100 |
101 | ---
102 |
103 | apiVersion: monitoring.coreos.com/v1
104 | kind: ServiceMonitor
105 | metadata:
106 | labels:
107 | release: monitoring
108 | name: {{ .Values.microservice.name }}-service-monitor
109 | namespace: default
110 | spec:
111 | selector:
112 | matchLabels:
113 | app: {{ .Values.microservice.name }}
114 | endpoints:
115 | - interval: 10s
116 | port: http
117 | path: /actuator/prometheus
118 | namespaceSelector:
119 | matchNames:
120 | - default
121 |
122 | ---
123 |
124 | apiVersion: v1
125 | kind: ConfigMap
126 | metadata:
127 | name: {{ .Values.microservice.name }}-config-map
128 | data:
129 | server_port: "8080"
130 | grpc_server_port: "8000"
131 | zipkin_base_url: zipkin:9411
132 | r2dbc_url: "r2dbc:postgresql://postgres:5432/bank_accounts"
133 | flyway_url: "jdbc:postgresql://postgres:5432/bank_accounts"
--------------------------------------------------------------------------------
/k8s/microservice/templates/postgres.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: postgres
5 | labels:
6 | app: postgres
7 | spec:
8 | replicas: 1
9 | template:
10 | metadata:
11 | name: postgres
12 | labels:
13 | app: postgres
14 | spec:
15 | containers:
16 | - name: postgres
17 | image: postgres:latest
18 | imagePullPolicy: IfNotPresent
19 | resources:
20 | requests:
21 | memory: "512Mi"
22 | cpu: "300m"
23 | limits:
24 | memory: "1500Mi"
25 | cpu: "800m"
26 | env:
27 | - name: POSTGRES_USER
28 | value: postgres
29 | - name: POSTGRES_PASSWORD
30 | value: postgres
31 | - name: POSTGRES_DB
32 | value: bank_accounts
33 | - name: POSTGRES_HOST
34 | value: "5432"
35 | restartPolicy: Always
36 | selector:
37 | matchLabels:
38 | app: postgres
39 | ---
40 | apiVersion: v1
41 | kind: Service
42 | metadata:
43 | name: postgres
44 | spec:
45 | selector:
46 | app: postgres
47 | ports:
48 | - port: 5432
49 | name: http
50 | type: NodePort
--------------------------------------------------------------------------------
/k8s/microservice/templates/zipkin.yaml:
--------------------------------------------------------------------------------
1 | apiVersion: apps/v1
2 | kind: Deployment
3 | metadata:
4 | name: zipkin
5 | labels:
6 | app: zipkin
7 | spec:
8 | replicas: 1
9 | template:
10 | metadata:
11 | name: zipkin
12 | labels:
13 | app: zipkin
14 | spec:
15 | containers:
16 | - name: zipkin
17 | image: openzipkin/zipkin:latest
18 | imagePullPolicy: IfNotPresent
19 | resources:
20 | requests:
21 | memory: "512Mi"
22 | cpu: "300m"
23 | limits:
24 | memory: "1000Mi"
25 | cpu: "800m"
26 | restartPolicy: Always
27 | selector:
28 | matchLabels:
29 | app: zipkin
30 | ---
31 | apiVersion: v1
32 | kind: Service
33 | metadata:
34 | name: zipkin
35 | spec:
36 | selector:
37 | app: zipkin
38 | ports:
39 | - port: 9411
40 | name: http
41 | type: NodePort
--------------------------------------------------------------------------------
/k8s/microservice/values.yaml:
--------------------------------------------------------------------------------
1 | microservice:
2 | name: kotlin-spring-microservice
3 | image: alexanderbryksin/kotlin_spring_grpc_microservice:latest
4 | replicas: 1
5 | livenessProbe:
6 | httpGet:
7 | port: 8080
8 | path: /actuator/health/liveness
9 | initialDelaySeconds: 60
10 | periodSeconds: 5
11 | readinessProbe:
12 | httpGet:
13 | port: 8080
14 | path: /actuator/health/readiness
15 | initialDelaySeconds: 60
16 | periodSeconds: 5
17 | ports:
18 | http:
19 | name: http
20 | containerPort: 8080
21 | grpc:
22 | name: grpc
23 | containerPort: 8000
24 | terminationGracePeriodSeconds: 20
25 | service:
26 | httpPort: 8080
27 | grpcPort: 8000
28 | resources:
29 | requests:
30 | memory: '6000Mi'
31 | cpu: "3000m"
32 | limits:
33 | memory: '6000Mi'
34 | cpu: "3000m"
--------------------------------------------------------------------------------
/monitoring/prometheus.yml:
--------------------------------------------------------------------------------
1 | global:
2 | scrape_interval: 10s
3 | evaluation_interval: 10s
4 |
5 | scrape_configs:
6 | - job_name: 'prometheus'
7 | static_configs:
8 | - targets: [ 'localhost:9090' ]
9 |
10 | - job_name: 'system'
11 | static_configs:
12 | - targets: [ 'host.docker.internal:9101' ]
13 |
14 | - job_name: 'search-microservice'
15 | metrics_path: '/actuator/prometheus'
16 | static_configs:
17 | - targets: [ 'host.docker.internal:8080' ]
--------------------------------------------------------------------------------
/mvnw:
--------------------------------------------------------------------------------
1 | #!/bin/sh
2 | # ----------------------------------------------------------------------------
3 | # Licensed to the Apache Software Foundation (ASF) under one
4 | # or more contributor license agreements. See the NOTICE file
5 | # distributed with this work for additional information
6 | # regarding copyright ownership. The ASF licenses this file
7 | # to you under the Apache License, Version 2.0 (the
8 | # "License"); you may not use this file except in compliance
9 | # with the License. You may obtain a copy of the License at
10 | #
11 | # https://www.apache.org/licenses/LICENSE-2.0
12 | #
13 | # Unless required by applicable law or agreed to in writing,
14 | # software distributed under the License is distributed on an
15 | # "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
16 | # KIND, either express or implied. See the License for the
17 | # specific language governing permissions and limitations
18 | # under the License.
19 | # ----------------------------------------------------------------------------
20 |
21 | # ----------------------------------------------------------------------------
22 | # Maven Start Up Batch script
23 | #
24 | # Required ENV vars:
25 | # ------------------
26 | # JAVA_HOME - location of a JDK home dir
27 | #
28 | # Optional ENV vars
29 | # -----------------
30 | # M2_HOME - location of maven2's installed home dir
31 | # MAVEN_OPTS - parameters passed to the Java VM when running Maven
32 | # e.g. to debug Maven itself, use
33 | # set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
34 | # MAVEN_SKIP_RC - flag to disable loading of mavenrc files
35 | # ----------------------------------------------------------------------------
36 |
37 | if [ -z "$MAVEN_SKIP_RC" ] ; then
38 |
39 | if [ -f /usr/local/etc/mavenrc ] ; then
40 | . /usr/local/etc/mavenrc
41 | fi
42 |
43 | if [ -f /etc/mavenrc ] ; then
44 | . /etc/mavenrc
45 | fi
46 |
47 | if [ -f "$HOME/.mavenrc" ] ; then
48 | . "$HOME/.mavenrc"
49 | fi
50 |
51 | fi
52 |
53 | # OS specific support. $var _must_ be set to either true or false.
54 | cygwin=false;
55 | darwin=false;
56 | mingw=false
57 | case "`uname`" in
58 | CYGWIN*) cygwin=true ;;
59 | MINGW*) mingw=true;;
60 | Darwin*) darwin=true
61 | # Use /usr/libexec/java_home if available, otherwise fall back to /Library/Java/Home
62 | # See https://developer.apple.com/library/mac/qa/qa1170/_index.html
63 | if [ -z "$JAVA_HOME" ]; then
64 | if [ -x "/usr/libexec/java_home" ]; then
65 | export JAVA_HOME="`/usr/libexec/java_home`"
66 | else
67 | export JAVA_HOME="/Library/Java/Home"
68 | fi
69 | fi
70 | ;;
71 | esac
72 |
73 | if [ -z "$JAVA_HOME" ] ; then
74 | if [ -r /etc/gentoo-release ] ; then
75 | JAVA_HOME=`java-config --jre-home`
76 | fi
77 | fi
78 |
79 | if [ -z "$M2_HOME" ] ; then
80 | ## resolve links - $0 may be a link to maven's home
81 | PRG="$0"
82 |
83 | # need this for relative symlinks
84 | while [ -h "$PRG" ] ; do
85 | ls=`ls -ld "$PRG"`
86 | link=`expr "$ls" : '.*-> \(.*\)$'`
87 | if expr "$link" : '/.*' > /dev/null; then
88 | PRG="$link"
89 | else
90 | PRG="`dirname "$PRG"`/$link"
91 | fi
92 | done
93 |
94 | saveddir=`pwd`
95 |
96 | M2_HOME=`dirname "$PRG"`/..
97 |
98 | # make it fully qualified
99 | M2_HOME=`cd "$M2_HOME" && pwd`
100 |
101 | cd "$saveddir"
102 | # echo Using m2 at $M2_HOME
103 | fi
104 |
105 | # For Cygwin, ensure paths are in UNIX format before anything is touched
106 | if $cygwin ; then
107 | [ -n "$M2_HOME" ] &&
108 | M2_HOME=`cygpath --unix "$M2_HOME"`
109 | [ -n "$JAVA_HOME" ] &&
110 | JAVA_HOME=`cygpath --unix "$JAVA_HOME"`
111 | [ -n "$CLASSPATH" ] &&
112 | CLASSPATH=`cygpath --path --unix "$CLASSPATH"`
113 | fi
114 |
115 | # For Mingw, ensure paths are in UNIX format before anything is touched
116 | if $mingw ; then
117 | [ -n "$M2_HOME" ] &&
118 | M2_HOME="`(cd "$M2_HOME"; pwd)`"
119 | [ -n "$JAVA_HOME" ] &&
120 | JAVA_HOME="`(cd "$JAVA_HOME"; pwd)`"
121 | fi
122 |
123 | if [ -z "$JAVA_HOME" ]; then
124 | javaExecutable="`which javac`"
125 | if [ -n "$javaExecutable" ] && ! [ "`expr \"$javaExecutable\" : '\([^ ]*\)'`" = "no" ]; then
126 | # readlink(1) is not available as standard on Solaris 10.
127 | readLink=`which readlink`
128 | if [ ! `expr "$readLink" : '\([^ ]*\)'` = "no" ]; then
129 | if $darwin ; then
130 | javaHome="`dirname \"$javaExecutable\"`"
131 | javaExecutable="`cd \"$javaHome\" && pwd -P`/javac"
132 | else
133 | javaExecutable="`readlink -f \"$javaExecutable\"`"
134 | fi
135 | javaHome="`dirname \"$javaExecutable\"`"
136 | javaHome=`expr "$javaHome" : '\(.*\)/bin'`
137 | JAVA_HOME="$javaHome"
138 | export JAVA_HOME
139 | fi
140 | fi
141 | fi
142 |
143 | if [ -z "$JAVACMD" ] ; then
144 | if [ -n "$JAVA_HOME" ] ; then
145 | if [ -x "$JAVA_HOME/jre/sh/java" ] ; then
146 | # IBM's JDK on AIX uses strange locations for the executables
147 | JAVACMD="$JAVA_HOME/jre/sh/java"
148 | else
149 | JAVACMD="$JAVA_HOME/bin/java"
150 | fi
151 | else
152 | JAVACMD="`\\unset -f command; \\command -v java`"
153 | fi
154 | fi
155 |
156 | if [ ! -x "$JAVACMD" ] ; then
157 | echo "Error: JAVA_HOME is not defined correctly." >&2
158 | echo " We cannot execute $JAVACMD" >&2
159 | exit 1
160 | fi
161 |
162 | if [ -z "$JAVA_HOME" ] ; then
163 | echo "Warning: JAVA_HOME environment variable is not set."
164 | fi
165 |
166 | CLASSWORLDS_LAUNCHER=org.codehaus.plexus.classworlds.launcher.Launcher
167 |
168 | # traverses directory structure from process work directory to filesystem root
169 | # first directory with .mvn subdirectory is considered project base directory
170 | find_maven_basedir() {
171 |
172 | if [ -z "$1" ]
173 | then
174 | echo "Path not specified to find_maven_basedir"
175 | return 1
176 | fi
177 |
178 | basedir="$1"
179 | wdir="$1"
180 | while [ "$wdir" != '/' ] ; do
181 | if [ -d "$wdir"/.mvn ] ; then
182 | basedir=$wdir
183 | break
184 | fi
185 | # workaround for JBEAP-8937 (on Solaris 10/Sparc)
186 | if [ -d "${wdir}" ]; then
187 | wdir=`cd "$wdir/.."; pwd`
188 | fi
189 | # end of workaround
190 | done
191 | echo "${basedir}"
192 | }
193 |
194 | # concatenates all lines of a file
195 | concat_lines() {
196 | if [ -f "$1" ]; then
197 | echo "$(tr -s '\n' ' ' < "$1")"
198 | fi
199 | }
200 |
201 | BASE_DIR=`find_maven_basedir "$(pwd)"`
202 | if [ -z "$BASE_DIR" ]; then
203 | exit 1;
204 | fi
205 |
206 | ##########################################################################################
207 | # Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
208 | # This allows using the maven wrapper in projects that prohibit checking in binary data.
209 | ##########################################################################################
210 | if [ -r "$BASE_DIR/.mvn/wrapper/maven-wrapper.jar" ]; then
211 | if [ "$MVNW_VERBOSE" = true ]; then
212 | echo "Found .mvn/wrapper/maven-wrapper.jar"
213 | fi
214 | else
215 | if [ "$MVNW_VERBOSE" = true ]; then
216 | echo "Couldn't find .mvn/wrapper/maven-wrapper.jar, downloading it ..."
217 | fi
218 | if [ -n "$MVNW_REPOURL" ]; then
219 | jarUrl="$MVNW_REPOURL/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
220 | else
221 | jarUrl="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
222 | fi
223 | while IFS="=" read key value; do
224 | case "$key" in (wrapperUrl) jarUrl="$value"; break ;;
225 | esac
226 | done < "$BASE_DIR/.mvn/wrapper/maven-wrapper.properties"
227 | if [ "$MVNW_VERBOSE" = true ]; then
228 | echo "Downloading from: $jarUrl"
229 | fi
230 | wrapperJarPath="$BASE_DIR/.mvn/wrapper/maven-wrapper.jar"
231 | if $cygwin; then
232 | wrapperJarPath=`cygpath --path --windows "$wrapperJarPath"`
233 | fi
234 |
235 | if command -v wget > /dev/null; then
236 | if [ "$MVNW_VERBOSE" = true ]; then
237 | echo "Found wget ... using wget"
238 | fi
239 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
240 | wget "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
241 | else
242 | wget --http-user=$MVNW_USERNAME --http-password=$MVNW_PASSWORD "$jarUrl" -O "$wrapperJarPath" || rm -f "$wrapperJarPath"
243 | fi
244 | elif command -v curl > /dev/null; then
245 | if [ "$MVNW_VERBOSE" = true ]; then
246 | echo "Found curl ... using curl"
247 | fi
248 | if [ -z "$MVNW_USERNAME" ] || [ -z "$MVNW_PASSWORD" ]; then
249 | curl -o "$wrapperJarPath" "$jarUrl" -f
250 | else
251 | curl --user $MVNW_USERNAME:$MVNW_PASSWORD -o "$wrapperJarPath" "$jarUrl" -f
252 | fi
253 |
254 | else
255 | if [ "$MVNW_VERBOSE" = true ]; then
256 | echo "Falling back to using Java to download"
257 | fi
258 | javaClass="$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.java"
259 | # For Cygwin, switch paths to Windows format before running javac
260 | if $cygwin; then
261 | javaClass=`cygpath --path --windows "$javaClass"`
262 | fi
263 | if [ -e "$javaClass" ]; then
264 | if [ ! -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
265 | if [ "$MVNW_VERBOSE" = true ]; then
266 | echo " - Compiling MavenWrapperDownloader.java ..."
267 | fi
268 | # Compiling the Java class
269 | ("$JAVA_HOME/bin/javac" "$javaClass")
270 | fi
271 | if [ -e "$BASE_DIR/.mvn/wrapper/MavenWrapperDownloader.class" ]; then
272 | # Running the downloader
273 | if [ "$MVNW_VERBOSE" = true ]; then
274 | echo " - Running MavenWrapperDownloader.java ..."
275 | fi
276 | ("$JAVA_HOME/bin/java" -cp .mvn/wrapper MavenWrapperDownloader "$MAVEN_PROJECTBASEDIR")
277 | fi
278 | fi
279 | fi
280 | fi
281 | ##########################################################################################
282 | # End of extension
283 | ##########################################################################################
284 |
285 | export MAVEN_PROJECTBASEDIR=${MAVEN_BASEDIR:-"$BASE_DIR"}
286 | if [ "$MVNW_VERBOSE" = true ]; then
287 | echo $MAVEN_PROJECTBASEDIR
288 | fi
289 | MAVEN_OPTS="$(concat_lines "$MAVEN_PROJECTBASEDIR/.mvn/jvm.config") $MAVEN_OPTS"
290 |
291 | # For Cygwin, switch paths to Windows format before running java
292 | if $cygwin; then
293 | [ -n "$M2_HOME" ] &&
294 | M2_HOME=`cygpath --path --windows "$M2_HOME"`
295 | [ -n "$JAVA_HOME" ] &&
296 | JAVA_HOME=`cygpath --path --windows "$JAVA_HOME"`
297 | [ -n "$CLASSPATH" ] &&
298 | CLASSPATH=`cygpath --path --windows "$CLASSPATH"`
299 | [ -n "$MAVEN_PROJECTBASEDIR" ] &&
300 | MAVEN_PROJECTBASEDIR=`cygpath --path --windows "$MAVEN_PROJECTBASEDIR"`
301 | fi
302 |
303 | # Provide a "standardized" way to retrieve the CLI args that will
304 | # work with both Windows and non-Windows executions.
305 | MAVEN_CMD_LINE_ARGS="$MAVEN_CONFIG $@"
306 | export MAVEN_CMD_LINE_ARGS
307 |
308 | WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
309 |
310 | exec "$JAVACMD" \
311 | $MAVEN_OPTS \
312 | $MAVEN_DEBUG_OPTS \
313 | -classpath "$MAVEN_PROJECTBASEDIR/.mvn/wrapper/maven-wrapper.jar" \
314 | "-Dmaven.home=${M2_HOME}" \
315 | "-Dmaven.multiModuleProjectDirectory=${MAVEN_PROJECTBASEDIR}" \
316 | ${WRAPPER_LAUNCHER} $MAVEN_CONFIG "$@"
317 |
--------------------------------------------------------------------------------
/mvnw.cmd:
--------------------------------------------------------------------------------
1 | @REM ----------------------------------------------------------------------------
2 | @REM Licensed to the Apache Software Foundation (ASF) under one
3 | @REM or more contributor license agreements. See the NOTICE file
4 | @REM distributed with this work for additional information
5 | @REM regarding copyright ownership. The ASF licenses this file
6 | @REM to you under the Apache License, Version 2.0 (the
7 | @REM "License"); you may not use this file except in compliance
8 | @REM with the License. You may obtain a copy of the License at
9 | @REM
10 | @REM https://www.apache.org/licenses/LICENSE-2.0
11 | @REM
12 | @REM Unless required by applicable law or agreed to in writing,
13 | @REM software distributed under the License is distributed on an
14 | @REM "AS IS" BASIS, WITHOUT WARRANTIES OR CONDITIONS OF ANY
15 | @REM KIND, either express or implied. See the License for the
16 | @REM specific language governing permissions and limitations
17 | @REM under the License.
18 | @REM ----------------------------------------------------------------------------
19 |
20 | @REM ----------------------------------------------------------------------------
21 | @REM Maven Start Up Batch script
22 | @REM
23 | @REM Required ENV vars:
24 | @REM JAVA_HOME - location of a JDK home dir
25 | @REM
26 | @REM Optional ENV vars
27 | @REM M2_HOME - location of maven2's installed home dir
28 | @REM MAVEN_BATCH_ECHO - set to 'on' to enable the echoing of the batch commands
29 | @REM MAVEN_BATCH_PAUSE - set to 'on' to wait for a keystroke before ending
30 | @REM MAVEN_OPTS - parameters passed to the Java VM when running Maven
31 | @REM e.g. to debug Maven itself, use
32 | @REM set MAVEN_OPTS=-Xdebug -Xrunjdwp:transport=dt_socket,server=y,suspend=y,address=8000
33 | @REM MAVEN_SKIP_RC - flag to disable loading of mavenrc files
34 | @REM ----------------------------------------------------------------------------
35 |
36 | @REM Begin all REM lines with '@' in case MAVEN_BATCH_ECHO is 'on'
37 | @echo off
38 | @REM set title of command window
39 | title %0
40 | @REM enable echoing by setting MAVEN_BATCH_ECHO to 'on'
41 | @if "%MAVEN_BATCH_ECHO%" == "on" echo %MAVEN_BATCH_ECHO%
42 |
43 | @REM set %HOME% to equivalent of $HOME
44 | if "%HOME%" == "" (set "HOME=%HOMEDRIVE%%HOMEPATH%")
45 |
46 | @REM Execute a user defined script before this one
47 | if not "%MAVEN_SKIP_RC%" == "" goto skipRcPre
48 | @REM check for pre script, once with legacy .bat ending and once with .cmd ending
49 | if exist "%USERPROFILE%\mavenrc_pre.bat" call "%USERPROFILE%\mavenrc_pre.bat" %*
50 | if exist "%USERPROFILE%\mavenrc_pre.cmd" call "%USERPROFILE%\mavenrc_pre.cmd" %*
51 | :skipRcPre
52 |
53 | @setlocal
54 |
55 | set ERROR_CODE=0
56 |
57 | @REM To isolate internal variables from possible post scripts, we use another setlocal
58 | @setlocal
59 |
60 | @REM ==== START VALIDATION ====
61 | if not "%JAVA_HOME%" == "" goto OkJHome
62 |
63 | echo.
64 | echo Error: JAVA_HOME not found in your environment. >&2
65 | echo Please set the JAVA_HOME variable in your environment to match the >&2
66 | echo location of your Java installation. >&2
67 | echo.
68 | goto error
69 |
70 | :OkJHome
71 | if exist "%JAVA_HOME%\bin\java.exe" goto init
72 |
73 | echo.
74 | echo Error: JAVA_HOME is set to an invalid directory. >&2
75 | echo JAVA_HOME = "%JAVA_HOME%" >&2
76 | echo Please set the JAVA_HOME variable in your environment to match the >&2
77 | echo location of your Java installation. >&2
78 | echo.
79 | goto error
80 |
81 | @REM ==== END VALIDATION ====
82 |
83 | :init
84 |
85 | @REM Find the project base dir, i.e. the directory that contains the folder ".mvn".
86 | @REM Fallback to current working directory if not found.
87 |
88 | set MAVEN_PROJECTBASEDIR=%MAVEN_BASEDIR%
89 | IF NOT "%MAVEN_PROJECTBASEDIR%"=="" goto endDetectBaseDir
90 |
91 | set EXEC_DIR=%CD%
92 | set WDIR=%EXEC_DIR%
93 | :findBaseDir
94 | IF EXIST "%WDIR%"\.mvn goto baseDirFound
95 | cd ..
96 | IF "%WDIR%"=="%CD%" goto baseDirNotFound
97 | set WDIR=%CD%
98 | goto findBaseDir
99 |
100 | :baseDirFound
101 | set MAVEN_PROJECTBASEDIR=%WDIR%
102 | cd "%EXEC_DIR%"
103 | goto endDetectBaseDir
104 |
105 | :baseDirNotFound
106 | set MAVEN_PROJECTBASEDIR=%EXEC_DIR%
107 | cd "%EXEC_DIR%"
108 |
109 | :endDetectBaseDir
110 |
111 | IF NOT EXIST "%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config" goto endReadAdditionalConfig
112 |
113 | @setlocal EnableExtensions EnableDelayedExpansion
114 | for /F "usebackq delims=" %%a in ("%MAVEN_PROJECTBASEDIR%\.mvn\jvm.config") do set JVM_CONFIG_MAVEN_PROPS=!JVM_CONFIG_MAVEN_PROPS! %%a
115 | @endlocal & set JVM_CONFIG_MAVEN_PROPS=%JVM_CONFIG_MAVEN_PROPS%
116 |
117 | :endReadAdditionalConfig
118 |
119 | SET MAVEN_JAVA_EXE="%JAVA_HOME%\bin\java.exe"
120 | set WRAPPER_JAR="%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.jar"
121 | set WRAPPER_LAUNCHER=org.apache.maven.wrapper.MavenWrapperMain
122 |
123 | set DOWNLOAD_URL="https://repo.maven.apache.org/maven2/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
124 |
125 | FOR /F "usebackq tokens=1,2 delims==" %%A IN ("%MAVEN_PROJECTBASEDIR%\.mvn\wrapper\maven-wrapper.properties") DO (
126 | IF "%%A"=="wrapperUrl" SET DOWNLOAD_URL=%%B
127 | )
128 |
129 | @REM Extension to allow automatically downloading the maven-wrapper.jar from Maven-central
130 | @REM This allows using the maven wrapper in projects that prohibit checking in binary data.
131 | if exist %WRAPPER_JAR% (
132 | if "%MVNW_VERBOSE%" == "true" (
133 | echo Found %WRAPPER_JAR%
134 | )
135 | ) else (
136 | if not "%MVNW_REPOURL%" == "" (
137 | SET DOWNLOAD_URL="%MVNW_REPOURL%/org/apache/maven/wrapper/maven-wrapper/3.1.0/maven-wrapper-3.1.0.jar"
138 | )
139 | if "%MVNW_VERBOSE%" == "true" (
140 | echo Couldn't find %WRAPPER_JAR%, downloading it ...
141 | echo Downloading from: %DOWNLOAD_URL%
142 | )
143 |
144 | powershell -Command "&{"^
145 | "$webclient = new-object System.Net.WebClient;"^
146 | "if (-not ([string]::IsNullOrEmpty('%MVNW_USERNAME%') -and [string]::IsNullOrEmpty('%MVNW_PASSWORD%'))) {"^
147 | "$webclient.Credentials = new-object System.Net.NetworkCredential('%MVNW_USERNAME%', '%MVNW_PASSWORD%');"^
148 | "}"^
149 | "[Net.ServicePointManager]::SecurityProtocol = [Net.SecurityProtocolType]::Tls12; $webclient.DownloadFile('%DOWNLOAD_URL%', '%WRAPPER_JAR%')"^
150 | "}"
151 | if "%MVNW_VERBOSE%" == "true" (
152 | echo Finished downloading %WRAPPER_JAR%
153 | )
154 | )
155 | @REM End of extension
156 |
157 | @REM Provide a "standardized" way to retrieve the CLI args that will
158 | @REM work with both Windows and non-Windows executions.
159 | set MAVEN_CMD_LINE_ARGS=%*
160 |
161 | %MAVEN_JAVA_EXE% ^
162 | %JVM_CONFIG_MAVEN_PROPS% ^
163 | %MAVEN_OPTS% ^
164 | %MAVEN_DEBUG_OPTS% ^
165 | -classpath %WRAPPER_JAR% ^
166 | "-Dmaven.multiModuleProjectDirectory=%MAVEN_PROJECTBASEDIR%" ^
167 | %WRAPPER_LAUNCHER% %MAVEN_CONFIG% %*
168 | if ERRORLEVEL 1 goto error
169 | goto end
170 |
171 | :error
172 | set ERROR_CODE=1
173 |
174 | :end
175 | @endlocal & set ERROR_CODE=%ERROR_CODE%
176 |
177 | if not "%MAVEN_SKIP_RC%"=="" goto skipRcPost
178 | @REM check for post script, once with legacy .bat ending and once with .cmd ending
179 | if exist "%USERPROFILE%\mavenrc_post.bat" call "%USERPROFILE%\mavenrc_post.bat"
180 | if exist "%USERPROFILE%\mavenrc_post.cmd" call "%USERPROFILE%\mavenrc_post.cmd"
181 | :skipRcPost
182 |
183 | @REM pause the script if MAVEN_BATCH_PAUSE is set to 'on'
184 | if "%MAVEN_BATCH_PAUSE%"=="on" pause
185 |
186 | if "%MAVEN_TERMINATE_CMD%"=="on" exit %ERROR_CODE%
187 |
188 | cmd /C exit /B %ERROR_CODE%
189 |
--------------------------------------------------------------------------------
/pom.xml:
--------------------------------------------------------------------------------
1 |
2 |
4 | 4.0.0
5 |
6 | org.springframework.boot
7 | spring-boot-starter-parent
8 | 2.7.4
9 |
10 |
11 | com.example.alexbryksin
12 | KotlinSpringGrpc
13 | 0.0.1-SNAPSHOT
14 | KotlinSpringGrpc
15 | Demo project for Spring Boot
16 |
17 | 17
18 | 1.6.21
19 | 1.3.0
20 | 1.43.0
21 | 3.20.1
22 | 3.21.7
23 |
24 |
25 |
26 | io.netty
27 | netty-all
28 | 4.1.82.Final
29 |
30 |
31 | org.springdoc
32 | springdoc-openapi-webflux-ui
33 | 1.6.12
34 |
35 |
36 | org.springframework.boot
37 | spring-boot-starter-validation
38 |
39 |
40 | org.springframework.cloud
41 | spring-cloud-sleuth-zipkin
42 | 3.1.4
43 |
44 |
45 | org.springframework.cloud
46 | spring-cloud-starter-sleuth
47 | 3.1.4
48 |
49 |
50 | io.micrometer
51 | micrometer-registry-prometheus
52 | 1.9.4
53 |
54 |
55 | com.github.javafaker
56 | javafaker
57 | 1.0.2
58 |
59 |
60 | org.flywaydb
61 | flyway-core
62 |
63 |
64 | org.postgresql
65 | postgresql
66 | runtime
67 |
68 |
69 | org.postgresql
70 | r2dbc-postgresql
71 | runtime
72 |
73 |
74 | org.springframework.boot
75 | spring-boot-starter-data-r2dbc
76 |
77 |
78 | org.springframework.boot
79 | spring-boot-starter-data-jdbc
80 |
81 |
82 | org.springframework.boot
83 | spring-boot-starter-actuator
84 |
85 |
86 | org.springframework.boot
87 | spring-boot-starter-webflux
88 |
89 |
90 | net.devh
91 | grpc-spring-boot-starter
92 | 2.13.1.RELEASE
93 |
94 |
95 |
96 |
97 |
98 |
99 |
100 |
101 | io.grpc
102 | grpc-kotlin-stub
103 | ${grpc.kotlin.version}
104 |
105 |
106 | io.grpc
107 | grpc-protobuf
108 | ${java.grpc.version}
109 |
110 |
111 | com.google.protobuf
112 | protobuf-kotlin
113 | ${protobuf.version}
114 |
115 |
116 | org.springframework.boot
117 | spring-boot-starter
118 |
119 |
120 | com.fasterxml.jackson.module
121 | jackson-module-kotlin
122 |
123 |
124 | io.projectreactor.kotlin
125 | reactor-kotlin-extensions
126 |
127 |
128 | org.jetbrains.kotlin
129 | kotlin-reflect
130 |
131 |
132 | org.jetbrains.kotlin
133 | kotlin-stdlib-jdk8
134 |
135 |
136 | org.jetbrains.kotlinx
137 | kotlinx-coroutines-reactor
138 |
139 |
140 | org.springframework.boot
141 | spring-boot-devtools
142 | runtime
143 | true
144 |
145 |
146 | org.springframework.boot
147 | spring-boot-configuration-processor
148 | true
149 |
150 |
151 | org.springframework.boot
152 | spring-boot-starter-test
153 | test
154 |
155 |
156 | io.projectreactor
157 | reactor-test
158 | test
159 |
160 |
161 |
162 |
163 | ${project.basedir}/src/main/kotlin
164 | ${project.basedir}/src/test/kotlin
165 |
166 |
167 | org.xolstice.maven.plugins
168 | protobuf-maven-plugin
169 | 0.6.1
170 |
171 |
172 | compile
173 |
174 | compile
175 | compile-custom
176 |
177 |
178 | com.google.protobuf:protoc:${protobuf.protoc.version}:exe:${os.detected.classifier}
179 | grpc-java
180 | io.grpc:protoc-gen-grpc-java:${java.grpc.version}:exe:${os.detected.classifier}
181 |
182 |
183 | grpc-kotlin
184 | io.grpc
185 | protoc-gen-grpc-kotlin
186 | ${grpc.kotlin.version}
187 | jdk8
188 | io.grpc.kotlin.generator.GeneratorRunner
189 |
190 |
191 |
192 |
193 |
194 | compile-kt
195 |
196 | compile-custom
197 |
198 |
199 | com.google.protobuf:protoc:${protobuf.protoc.version}:exe:${os.detected.classifier}
200 | ${project.build.directory}/generated-sources/protobuf/kotlin
201 | kotlin
202 |
203 |
204 |
205 |
206 |
207 | kr.motd.maven
208 | os-maven-plugin
209 | 1.7.0
210 |
211 |
212 | initialize
213 |
214 | detect
215 |
216 |
217 |
218 |
219 |
220 | org.springframework.boot
221 | spring-boot-maven-plugin
222 |
223 |
224 | org.jetbrains.kotlin
225 | kotlin-maven-plugin
226 |
227 |
228 | -Xjsr305=strict
229 |
230 |
231 | spring
232 |
233 |
234 |
235 |
236 | org.jetbrains.kotlin
237 | kotlin-maven-allopen
238 | ${kotlin.version}
239 |
240 |
241 |
242 |
243 |
244 |
245 |
246 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/example/alexbryksin/KotlinSpringGrpcApplication.kt:
--------------------------------------------------------------------------------
1 | package com.example.alexbryksin
2 |
3 | import org.springframework.boot.autoconfigure.SpringBootApplication
4 | import org.springframework.boot.runApplication
5 |
6 | @SpringBootApplication
7 | class KotlinSpringGrpcApplication
8 |
9 | fun main(args: Array) {
10 | runApplication(*args)
11 | }
12 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/example/alexbryksin/configuration/DataLoaderConfig.kt:
--------------------------------------------------------------------------------
1 | package com.example.alexbryksin.configuration
2 |
3 | import com.example.alexbryksin.domain.BankAccount
4 | import com.example.alexbryksin.domain.Currency
5 | import com.example.alexbryksin.services.BankAccountService
6 | import com.github.javafaker.Faker
7 | import kotlinx.coroutines.async
8 | import kotlinx.coroutines.runBlocking
9 | import org.slf4j.LoggerFactory
10 | import org.springframework.beans.factory.annotation.Value
11 | import org.springframework.boot.CommandLineRunner
12 | import org.springframework.boot.autoconfigure.condition.ConditionalOnProperty
13 | import org.springframework.context.annotation.Configuration
14 | import java.math.BigDecimal
15 | import java.time.LocalDateTime
16 |
17 |
18 | @Configuration
19 | @ConditionalOnProperty(prefix = "faker", name = ["enable"])
20 | class DataLoaderConfig(
21 | private val bankAccountService: BankAccountService,
22 | private val faker: Faker,
23 | ) : CommandLineRunner {
24 |
25 | @Value(value = "\${faker.count:300}")
26 | val count: Int = 300
27 |
28 | override fun run(vararg args: String?) = runBlocking {
29 |
30 | (0..count).map { _ ->
31 | async {
32 | try {
33 | bankAccountService.createBankAccount(
34 | BankAccount(
35 | id = null,
36 | email = faker.internet().emailAddress(),
37 | firstName = faker.name().firstName(),
38 | lastName = faker.name().lastName(),
39 | address = faker.address().fullAddress(),
40 | phone = faker.phoneNumber().cellPhone(),
41 | currency = Currency.USD,
42 | balance = BigDecimal.valueOf(faker.number().numberBetween(0, 500000).toDouble()),
43 | createdAt = LocalDateTime.now(),
44 | updatedAt = LocalDateTime.now()
45 | )
46 | )
47 | } catch (ex: Exception) {
48 | log.error("insert mock data error", ex)
49 | return@async null
50 | }
51 | }
52 | }
53 | .map { it.await() }
54 | .forEach { log.info("created bank account: $it") }
55 | .also { log.info("Mock data successfully inserted") }
56 | }
57 |
58 | companion object {
59 | private val log = LoggerFactory.getLogger(DataLoaderConfig::class.java)
60 | }
61 | }
--------------------------------------------------------------------------------
/src/main/kotlin/com/example/alexbryksin/configuration/FakerConfig.kt:
--------------------------------------------------------------------------------
1 | package com.example.alexbryksin.configuration
2 |
3 | import com.github.javafaker.Faker
4 | import org.springframework.beans.factory.annotation.Value
5 | import org.springframework.context.annotation.Bean
6 | import org.springframework.context.annotation.Configuration
7 | import java.util.*
8 |
9 |
10 | @Configuration
11 | class FakerConfig {
12 | @Value(value = "\${faker.locale:en}")
13 | val locale: String = "en"
14 |
15 | @Bean
16 | fun faker(): Faker = Faker(Locale(locale))
17 | }
--------------------------------------------------------------------------------
/src/main/kotlin/com/example/alexbryksin/configuration/GrpcServerConfig.kt:
--------------------------------------------------------------------------------
1 | package com.example.alexbryksin.configuration
2 |
3 | import net.devh.boot.grpc.server.event.GrpcServerStartedEvent
4 | import org.slf4j.LoggerFactory
5 | import org.springframework.context.annotation.Configuration
6 | import org.springframework.context.event.EventListener
7 |
8 |
9 | @Configuration
10 | class GrpcServerConfig {
11 | @EventListener
12 | fun onServerStarted(event: GrpcServerStartedEvent) {
13 | log.info("gRPC Server started, services: ${event.server.services[0].methods}")
14 | }
15 |
16 | companion object {
17 | private val log = LoggerFactory.getLogger(GrpcServerConfig::class.java)
18 | }
19 | }
--------------------------------------------------------------------------------
/src/main/kotlin/com/example/alexbryksin/configuration/SwaggerOpenAPIConfiguration.kt:
--------------------------------------------------------------------------------
1 | package com.example.alexbryksin.configuration
2 |
3 | import io.swagger.v3.oas.annotations.OpenAPIDefinition
4 | import io.swagger.v3.oas.annotations.info.Contact
5 | import io.swagger.v3.oas.annotations.info.Info
6 | import org.springframework.context.annotation.Configuration
7 |
8 |
9 | @OpenAPIDefinition(
10 | info = Info(
11 | title = "Kotlin, Spring WebFlux, gRPC, PostgreSQL Microservice",
12 | description = "Kotlin, Spring WebFlux, gRPC, PostgreSQL Microservice example",
13 | contact = Contact(
14 | name = "Alexander Bryksin",
15 | email = "alexander.bryksin@yandex.ru",
16 | url = "https://github.com/AleksK1NG"
17 | )
18 | )
19 | )
20 | @Configuration
21 | class SwaggerOpenAPIConfiguration
22 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/example/alexbryksin/delivery/grpc/BankAccountGrpcService.kt:
--------------------------------------------------------------------------------
1 | package com.example.alexbryksin.delivery.grpc
2 |
3 | import com.example.alexbryksin.domain.BankAccount
4 | import com.example.alexbryksin.domain.of
5 | import com.example.alexbryksin.domain.toProto
6 | import com.example.alexbryksin.dto.FindByBalanceRequestDto
7 | import com.example.alexbryksin.dto.of
8 | import com.example.alexbryksin.interceptors.LogGrpcInterceptor
9 | import com.example.alexbryksin.services.BankAccountService
10 | import com.example.alexbryksin.utils.runWithTracing
11 | import com.example.grpc.bank.service.BankAccount.*
12 | import com.example.grpc.bank.service.BankAccountServiceGrpcKt
13 | import kotlinx.coroutines.Dispatchers
14 | import kotlinx.coroutines.flow.Flow
15 | import kotlinx.coroutines.flow.flowOn
16 | import kotlinx.coroutines.flow.map
17 | import kotlinx.coroutines.withContext
18 | import kotlinx.coroutines.withTimeout
19 | import net.devh.boot.grpc.server.service.GrpcService
20 | import org.slf4j.LoggerFactory
21 | import org.springframework.cloud.sleuth.Tracer
22 | import org.springframework.cloud.sleuth.instrument.kotlin.asContextElement
23 | import org.springframework.data.domain.Page
24 | import java.math.BigDecimal
25 | import java.util.*
26 | import javax.validation.ConstraintViolationException
27 | import javax.validation.Validator
28 |
29 |
30 | @GrpcService(interceptors = [LogGrpcInterceptor::class])
31 | class BankAccountGrpcService(
32 | private val bankAccountService: BankAccountService,
33 | private val tracer: Tracer,
34 | private val validator: Validator
35 | ) : BankAccountServiceGrpcKt.BankAccountServiceCoroutineImplBase() {
36 |
37 |
38 | override suspend fun createBankAccount(request: CreateBankAccountRequest): CreateBankAccountResponse =
39 | withContext(tracer.asContextElement()) {
40 | withTimeout(timeOutMillis) {
41 | val span = tracer.startScopedSpan(CREATE_BANK_ACCOUNT)
42 |
43 | runWithTracing(span) {
44 | bankAccountService.createBankAccount(validate(BankAccount.of(request)))
45 | .let { CreateBankAccountResponse.newBuilder().setBankAccount(it.toProto()).build() }
46 | .also { it ->
47 | log.info("created bank account: $it").also { span.tag("account", it.toString()) }
48 | }
49 | }
50 | }
51 | }
52 |
53 | override suspend fun getBankAccountById(request: GetBankAccountByIdRequest): GetBankAccountByIdResponse =
54 | withContext(tracer.asContextElement()) {
55 | withTimeout(timeOutMillis) {
56 | val span = tracer.startScopedSpan(GET_BANK_ACCOUNT_BY_ID)
57 |
58 | runWithTracing(span) {
59 | bankAccountService.getBankAccountById(UUID.fromString(request.id))
60 | .let { GetBankAccountByIdResponse.newBuilder().setBankAccount(it.toProto()).build() }
61 | .also { it -> log.info("response: $it").also { span.tag("response", it.toString()) } }
62 | }
63 | }
64 | }
65 |
66 | override suspend fun depositBalance(request: DepositBalanceRequest): DepositBalanceResponse =
67 | withContext(tracer.asContextElement()) {
68 | withTimeout(timeOutMillis) {
69 | val span = tracer.startScopedSpan(DEPOSIT_BALANCE)
70 |
71 | runWithTracing(span) {
72 | bankAccountService.depositAmount(UUID.fromString(request.id), BigDecimal.valueOf(request.balance))
73 | .let { DepositBalanceResponse.newBuilder().setBankAccount(it.toProto()).build() }
74 | .also { it -> log.info("response: $it").also { span.tag("response", it.toString()) } }
75 | }
76 | }
77 | }
78 |
79 | override suspend fun withdrawBalance(request: WithdrawBalanceRequest): WithdrawBalanceResponse =
80 | withContext(tracer.asContextElement()) {
81 | withTimeout(timeOutMillis) {
82 | val span = tracer.startScopedSpan(WITHDRAW_BALANCE)
83 |
84 | runWithTracing(span) {
85 | bankAccountService.withdrawAmount(UUID.fromString(request.id), BigDecimal.valueOf(request.balance))
86 | .let { WithdrawBalanceResponse.newBuilder().setBankAccount(it.toProto()).build() }
87 | .also { it -> log.info("response: $it").also { span.tag("response", it.toString()) } }
88 | }
89 | }
90 | }
91 |
92 | override fun getAllByBalance(request: GetAllByBalanceRequest): Flow {
93 | runWithTracing(tracer, GET_ALL_BY_BALANCE) {
94 | return bankAccountService.findAllByBalanceBetween(validate(FindByBalanceRequestDto.of(request)))
95 | .map { GetAllByBalanceResponse.newBuilder().setBankAccount(it.toProto()).build() }
96 | .flowOn(Dispatchers.IO + tracer.asContextElement())
97 | }
98 | }
99 |
100 | override suspend fun getAllByBalanceWithPagination(request: GetAllByBalanceWithPaginationRequest): GetAllByBalanceWithPaginationResponse =
101 | withContext(tracer.asContextElement()) {
102 | withTimeout(timeOutMillis) {
103 | val span = tracer.startScopedSpan(GET_ALL_BY_BALANCE_WITH_PAGINATION)
104 |
105 | runWithTracing(span) {
106 | bankAccountService.findByBalanceAmount(validate(FindByBalanceRequestDto.of(request)))
107 | .toGetAllByBalanceWithPaginationResponse()
108 | .also { log.info("response: $it") }.also { span.tag("response", it.toString()) }
109 | }
110 | }
111 |
112 | }
113 |
114 |
115 | private fun validate(data: T): T {
116 | return data.run {
117 | val errors = validator.validate(data)
118 | if (errors.isNotEmpty()) throw ConstraintViolationException(errors).also { log.error("validation error: ${it.localizedMessage}") }
119 | data
120 | }
121 | }
122 |
123 |
124 | companion object {
125 | private val log = LoggerFactory.getLogger(BankAccountGrpcService::class.java)
126 | private const val timeOutMillis = 5000L
127 |
128 | private const val CREATE_BANK_ACCOUNT = "BankAccountGrpcService.createBankAccount"
129 | private const val GET_BANK_ACCOUNT_BY_ID = "BankAccountGrpcService.getBankAccountById"
130 | private const val DEPOSIT_BALANCE = "BankAccountGrpcService.depositBalance"
131 | private const val WITHDRAW_BALANCE = "BankAccountGrpcService.withdrawBalance"
132 | private const val GET_ALL_BY_BALANCE = "BankAccountGrpcService.getAllByBalance"
133 | private const val GET_ALL_BY_BALANCE_WITH_PAGINATION = "BankAccountGrpcService.getAllByBalanceWithPagination"
134 | }
135 | }
136 |
137 | fun Page.toGetAllByBalanceWithPaginationResponse(): GetAllByBalanceWithPaginationResponse {
138 | return GetAllByBalanceWithPaginationResponse
139 | .newBuilder()
140 | .setIsFirst(this.isFirst)
141 | .setIsLast(this.isLast)
142 | .setTotalElements(this.totalElements.toInt())
143 | .setTotalPages(this.totalPages)
144 | .setPage(this.pageable.pageNumber)
145 | .setSize(this.pageable.pageSize)
146 | .addAllBankAccount(this.content.map { it.toProto() })
147 | .build()
148 | }
149 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/example/alexbryksin/delivery/grpc/GrpcExceptionAdvice.kt:
--------------------------------------------------------------------------------
1 | package com.example.alexbryksin.delivery.grpc
2 |
3 | import com.example.alexbryksin.exceptions.BankAccountNotFoundException
4 | import io.grpc.Status
5 | import io.grpc.StatusException
6 | import net.devh.boot.grpc.server.advice.GrpcAdvice
7 | import net.devh.boot.grpc.server.advice.GrpcExceptionHandler
8 | import org.slf4j.LoggerFactory
9 | import org.springframework.web.bind.MethodArgumentNotValidException
10 | import javax.validation.ConstraintViolationException
11 |
12 |
13 | @GrpcAdvice
14 | class GrpcExceptionAdvice {
15 |
16 | @GrpcExceptionHandler(RuntimeException::class)
17 | fun handleRuntimeException(ex: RuntimeException): StatusException {
18 | val status = Status.INTERNAL.withDescription(ex.message).withCause(ex)
19 | return status.asException().also { log.error("status: $status") }
20 | }
21 |
22 | @GrpcExceptionHandler(BankAccountNotFoundException::class)
23 | fun handleBankAccountNotFoundException(ex: BankAccountNotFoundException): StatusException {
24 | val status = Status.INVALID_ARGUMENT.withDescription(ex.message).withCause(ex)
25 | return status.asException().also { log.error("status: $status") }
26 | }
27 |
28 | @GrpcExceptionHandler(MethodArgumentNotValidException::class)
29 | fun handleMethodArgumentNotValidException(ex: MethodArgumentNotValidException): StatusException {
30 | val errorMap: MutableMap = HashMap()
31 | ex.bindingResult.fieldErrors.forEach { error -> error.defaultMessage?.let { errorMap[error.field] = it } }
32 | val status = Status.INVALID_ARGUMENT.withDescription(errorMap.toString()).withCause(ex)
33 | return status.asException().also { log.error("status: $status") }
34 | }
35 |
36 | @GrpcExceptionHandler(ConstraintViolationException::class)
37 | fun handleConstraintViolationException(ex: ConstraintViolationException): StatusException {
38 | val status = Status.INVALID_ARGUMENT.withDescription(ex.toString()).withCause(ex)
39 | return status.asException().also { log.error("status: $status") }
40 | }
41 |
42 |
43 | companion object {
44 | private val log = LoggerFactory.getLogger(GrpcExceptionAdvice::class.java)
45 | }
46 | }
--------------------------------------------------------------------------------
/src/main/kotlin/com/example/alexbryksin/delivery/http/BankAccountController.kt:
--------------------------------------------------------------------------------
1 | package com.example.alexbryksin.delivery.http
2 |
3 | import com.example.alexbryksin.domain.BankAccount
4 | import com.example.alexbryksin.domain.of
5 | import com.example.alexbryksin.domain.toSuccessHttpResponse
6 | import com.example.alexbryksin.dto.*
7 | import com.example.alexbryksin.services.BankAccountService
8 | import io.swagger.v3.oas.annotations.Operation
9 | import io.swagger.v3.oas.annotations.tags.Tag
10 | import kotlinx.coroutines.flow.Flow
11 | import kotlinx.coroutines.flow.map
12 | import kotlinx.coroutines.withTimeout
13 | import org.slf4j.LoggerFactory
14 | import org.springframework.data.domain.PageRequest
15 | import org.springframework.http.HttpStatus
16 | import org.springframework.http.MediaType
17 | import org.springframework.http.ResponseEntity
18 | import org.springframework.web.bind.annotation.*
19 | import java.math.BigDecimal
20 | import java.util.*
21 | import javax.validation.Valid
22 |
23 |
24 | @Tag(name = "BankAccount", description = "Bank Account REST Controller")
25 | @RestController
26 | @RequestMapping(path = ["/api/v1/bank"])
27 | class BankAccountController(private val bankAccountService: BankAccountService) {
28 |
29 | @PostMapping(produces = [MediaType.APPLICATION_JSON_VALUE])
30 | @Operation(
31 | method = "createBankAccount",
32 | summary = "Create bew bank account",
33 | operationId = "createBankAccount",
34 | description = "Create new bank for account for user"
35 | )
36 | suspend fun createBankAccount(@Valid @RequestBody req: CreateBankAccountDto) =
37 | withTimeout(timeOutMillis) {
38 | ResponseEntity
39 | .status(HttpStatus.CREATED)
40 | .body(bankAccountService.createBankAccount(BankAccount.of(req)).toSuccessHttpResponse())
41 | .also { log.info("created bank account: $it") }
42 | }
43 |
44 | @PutMapping(path = ["/deposit/{id}"], produces = [MediaType.APPLICATION_JSON_VALUE])
45 | @Operation(
46 | method = "depositBalance",
47 | summary = "Deposit balance",
48 | operationId = "depositBalance",
49 | description = "Deposit given amount to the bank account balance"
50 | )
51 | suspend fun depositBalance(
52 | @PathVariable("id") id: UUID,
53 | @Valid @RequestBody depositBalanceDto: DepositBalanceDto
54 | ) = withTimeout(timeOutMillis) {
55 | ResponseEntity.ok(bankAccountService.depositAmount(id, depositBalanceDto.amount).toSuccessHttpResponse())
56 | .also { log.info("response: $it") }
57 | }
58 |
59 | @PutMapping(path = ["/withdraw/{id}"], produces = [MediaType.APPLICATION_JSON_VALUE])
60 | @Operation(
61 | method = "withdrawBalance",
62 | summary = "Withdraw balance",
63 | operationId = "withdrawBalance",
64 | description = "Withdraw given amount from the bank account balance"
65 | )
66 | suspend fun withdrawBalance(
67 | @PathVariable("id") id: UUID,
68 | @Valid @RequestBody withdrawBalanceDto: WithdrawBalanceDto
69 | ) = withTimeout(timeOutMillis) {
70 | ResponseEntity.ok(bankAccountService.depositAmount(id, withdrawBalanceDto.amount).toSuccessHttpResponse())
71 | .also { log.info("response: $it") }
72 | }
73 |
74 | @GetMapping(path = ["{id}"], produces = [MediaType.APPLICATION_JSON_VALUE])
75 | @Operation(
76 | method = "getBankAccountById",
77 | summary = "Get bank account by id",
78 | operationId = "getBankAccountById",
79 | description = "Get user bank account by given id"
80 | )
81 | suspend fun getBankAccountById(@PathVariable(required = true) id: UUID) = withTimeout(timeOutMillis) {
82 | ResponseEntity.ok(bankAccountService.getBankAccountById(id).toSuccessHttpResponse())
83 | .also { log.info("success get bank account: $it") }
84 | }
85 |
86 |
87 | @GetMapping(path = ["all/balance"], produces = [MediaType.APPLICATION_JSON_VALUE])
88 | @Operation(
89 | method = "findAllAccountsByBalance",
90 | summary = "Find all bank account with given amount range",
91 | operationId = "findAllAccounts",
92 | description = "Find all bank accounts for the given balance range with pagination"
93 | )
94 | suspend fun findAllAccountsByBalance(
95 | @RequestParam(name = "min", defaultValue = "0") min: BigDecimal,
96 | @RequestParam(name = "max", defaultValue = "500000000") max: BigDecimal,
97 | @RequestParam(name = "page", defaultValue = "0") page: Int,
98 | @RequestParam(name = "size", defaultValue = "10") size: Int,
99 | ) = withTimeout(timeOutMillis) {
100 | ResponseEntity.ok(bankAccountService.findByBalanceAmount(FindByBalanceRequestDto(min, max, PageRequest.of(page, size))))
101 | .also { log.info("response: $it") }
102 | }
103 |
104 | @GetMapping(path = ["all/balance/stream"])
105 | @Operation(
106 | method = "getAllByBalanceStream",
107 | summary = "Find all bank account with given amount range returns stream",
108 | operationId = "getAllByBalanceStream",
109 | description = "Find all bank accounts for the given balance range"
110 | )
111 | fun getAllByBalanceStream(
112 | @RequestParam(name = "min", defaultValue = "0") min: BigDecimal,
113 | @RequestParam(name = "max", defaultValue = "500000000") max: BigDecimal,
114 | @RequestParam(name = "page", defaultValue = "0") page: Int,
115 | @RequestParam(name = "size", defaultValue = "10") size: Int,
116 | ): Flow {
117 | return bankAccountService.findAllByBalanceBetween(FindByBalanceRequestDto(min, max, PageRequest.of(page, size)))
118 | .map { it -> it.toSuccessHttpResponse().also { log.info("response: $it") } }
119 | }
120 |
121 |
122 | companion object {
123 | private val log = LoggerFactory.getLogger(BankAccountController::class.java)
124 | private const val timeOutMillis = 5000L
125 | }
126 | }
--------------------------------------------------------------------------------
/src/main/kotlin/com/example/alexbryksin/delivery/http/GlobalControllerAdvice.kt:
--------------------------------------------------------------------------------
1 | package com.example.alexbryksin.delivery.http
2 |
3 | import com.example.alexbryksin.dto.ErrorHttpResponse
4 | import com.example.alexbryksin.exceptions.BankAccountNotFoundException
5 | import com.example.alexbryksin.exceptions.InvalidAmountException
6 | import org.slf4j.LoggerFactory
7 | import org.springframework.core.annotation.Order
8 | import org.springframework.http.HttpStatus
9 | import org.springframework.http.MediaType
10 | import org.springframework.http.ResponseEntity
11 | import org.springframework.http.server.reactive.ServerHttpRequest
12 | import org.springframework.web.bind.MethodArgumentNotValidException
13 | import org.springframework.web.bind.annotation.ControllerAdvice
14 | import org.springframework.web.bind.annotation.ExceptionHandler
15 | import org.springframework.web.bind.annotation.ResponseStatus
16 | import org.springframework.web.bind.support.WebExchangeBindException
17 | import java.time.LocalDateTime
18 |
19 |
20 | @Order(2)
21 | @ControllerAdvice
22 | class GlobalControllerAdvice {
23 |
24 | @ExceptionHandler(value = [RuntimeException::class])
25 | fun handleRuntimeException(ex: RuntimeException, request: ServerHttpRequest): ResponseEntity {
26 | val errorHttpResponse = ErrorHttpResponse(
27 | HttpStatus.INTERNAL_SERVER_ERROR.value(),
28 | ex.message ?: "",
29 | LocalDateTime.now().toString()
30 | )
31 | return ResponseEntity.status(HttpStatus.INTERNAL_SERVER_ERROR).contentType(MediaType.APPLICATION_JSON)
32 | .body(errorHttpResponse).also {
33 | log.error("(GlobalControllerAdvice) INTERNAL_SERVER_ERROR RuntimeException", ex)
34 | }
35 | }
36 |
37 | @ExceptionHandler(value = [BankAccountNotFoundException::class])
38 | fun handleBankAccountNotFoundException(
39 | ex: BankAccountNotFoundException,
40 | request: ServerHttpRequest
41 | ): ResponseEntity {
42 | val errorHttpResponse =
43 | ErrorHttpResponse(HttpStatus.NOT_FOUND.value(), ex.message ?: "", LocalDateTime.now().toString())
44 | return ResponseEntity.status(HttpStatus.NOT_FOUND).contentType(MediaType.APPLICATION_JSON)
45 | .body(errorHttpResponse)
46 | .also { log.error("(GlobalControllerAdvice) BankAccountNotFoundException NOT_FOUND", ex) }
47 | }
48 |
49 | @ExceptionHandler(value = [InvalidAmountException::class])
50 | fun handleInvalidAmountExceptionException(
51 | ex: InvalidAmountException,
52 | request: ServerHttpRequest
53 | ): ResponseEntity {
54 | val errorHttpResponse =
55 | ErrorHttpResponse(HttpStatus.BAD_REQUEST.value(), ex.message ?: "", LocalDateTime.now().toString())
56 | return ResponseEntity.status(HttpStatus.BAD_REQUEST).contentType(MediaType.APPLICATION_JSON)
57 | .body(errorHttpResponse)
58 | .also { log.error("(GlobalControllerAdvice) InvalidAmountException BAD_REQUEST", ex) }
59 | }
60 |
61 | @ResponseStatus(HttpStatus.BAD_REQUEST)
62 | @ExceptionHandler(value = [MethodArgumentNotValidException::class])
63 | fun handleInvalidArgument(ex: MethodArgumentNotValidException): ResponseEntity> {
64 | val errorMap: MutableMap = HashMap()
65 | ex.bindingResult.fieldErrors.forEach { error -> error.defaultMessage?.let { errorMap[error.field] = it } }
66 | return ResponseEntity.status(HttpStatus.BAD_REQUEST).contentType(MediaType.APPLICATION_JSON).body(errorMap)
67 | .also { log.error("(GlobalControllerAdvice) WebExchangeBindException BAD_REQUEST", ex) }
68 | }
69 |
70 | @ResponseStatus(HttpStatus.BAD_REQUEST)
71 | @ExceptionHandler(value = [WebExchangeBindException::class])
72 | fun handleWebExchangeInvalidArgument(ex: WebExchangeBindException): ResponseEntity> {
73 | val errorMap = mutableMapOf()
74 | ex.bindingResult.fieldErrors.forEach { error ->
75 | error.defaultMessage?.let {
76 | errorMap[error.field] = mapOf(
77 | "reason" to it,
78 | "rejectedValue" to error.rejectedValue,
79 | )
80 | }
81 | }
82 | return ResponseEntity.status(HttpStatus.BAD_REQUEST).contentType(MediaType.APPLICATION_JSON).body(errorMap)
83 | .also { log.error("(GlobalControllerAdvice) WebExchangeBindException BAD_REQUEST", ex) }
84 | }
85 |
86 |
87 | companion object {
88 | private val log = LoggerFactory.getLogger(GlobalControllerAdvice::class.java)
89 | }
90 | }
--------------------------------------------------------------------------------
/src/main/kotlin/com/example/alexbryksin/domain/BankAccount.kt:
--------------------------------------------------------------------------------
1 | package com.example.alexbryksin.domain
2 |
3 | import com.example.alexbryksin.dto.CreateBankAccountDto
4 | import com.example.alexbryksin.dto.SuccessBankAccountResponse
5 | import com.example.alexbryksin.exceptions.InvalidAmountException
6 | import com.example.grpc.bank.service.BankAccount.BankAccountData
7 | import org.springframework.data.annotation.Id
8 | import org.springframework.data.relational.core.mapping.Column
9 | import org.springframework.data.relational.core.mapping.Table
10 | import java.math.BigDecimal
11 | import java.time.LocalDateTime
12 | import java.util.*
13 | import javax.validation.constraints.DecimalMin
14 | import javax.validation.constraints.Email
15 | import javax.validation.constraints.Size
16 |
17 |
18 | @Table(schema = "microservices", name = "bank_accounts")
19 | data class BankAccount(
20 | @Column(BANK_ACCOUNT_ID) @Id var id: UUID?,
21 | @get:Email @Column(EMAIL) var email: String = "",
22 | @get:Size(min = 3, max = 60) @Column(FIRST_NAME) var firstName: String = "",
23 | @get:Size(min = 3, max = 60) @Column(LAST_NAME) var lastName: String = "",
24 | @get:Size(min = 3, max = 500) @Column(ADDRESS) var address: String = "",
25 | @get:Size(min = 6, max = 20) @Column(PHONE) var phone: String = "",
26 | @Column(CURRENCY) var currency: Currency = Currency.USD,
27 | @get:DecimalMin(value = "0.0") @Column(BALANCE) var balance: BigDecimal = BigDecimal.ZERO,
28 | @Column(CREATED_AT) var createdAt: LocalDateTime? = null,
29 | @Column(UPDATED_AT) var updatedAt: LocalDateTime? = null,
30 | ) {
31 |
32 | fun depositAmount(amount: BigDecimal): BankAccount {
33 | if (amount < BigDecimal.ZERO) throw InvalidAmountException(amount.toString())
34 | return this.apply {
35 | balance = balance.plus(amount)
36 | updatedAt = LocalDateTime.now()
37 | }
38 | }
39 |
40 | fun withdrawAmount(amount: BigDecimal): BankAccount {
41 | if (balance.minus(amount) < BigDecimal.ZERO) throw InvalidAmountException(amount.toString())
42 | return this.apply {
43 | balance = balance.minus(amount)
44 | updatedAt = LocalDateTime.now()
45 | }
46 | }
47 |
48 | companion object {
49 | const val BANK_ACCOUNT_ID = "bank_account_id"
50 | const val EMAIL = "email"
51 | const val FIRST_NAME = "first_name"
52 | const val LAST_NAME = "last_name"
53 | const val ADDRESS = "address"
54 | const val PHONE = "phone"
55 | const val BALANCE = "balance"
56 | const val CURRENCY = "currency"
57 | const val CREATED_AT = "created_at"
58 | const val UPDATED_AT = "updated_at"
59 | }
60 | }
61 |
62 | fun BankAccount.toProto(): BankAccountData {
63 | return BankAccountData.newBuilder()
64 | .setId(this.id.toString())
65 | .setEmail(this.email)
66 | .setFirstName(this.firstName)
67 | .setLastName(this.lastName)
68 | .setAddress(this.address)
69 | .setPhone(this.phone)
70 | .setBalance(this.balance.toDouble())
71 | .setCurrency(this.currency.name)
72 | .setUpdatedAt(this.updatedAt.toString())
73 | .setCreatedAt(this.createdAt.toString())
74 | .build()
75 | }
76 |
77 | fun BankAccount.toSuccessHttpResponse(): SuccessBankAccountResponse {
78 | return SuccessBankAccountResponse(
79 | id = this.id,
80 | email = this.email,
81 | firstName = this.firstName,
82 | lastName = this.lastName,
83 | address = this.address,
84 | phone = this.phone,
85 | currency = this.currency,
86 | balance = this.balance,
87 | createdAt = this.createdAt,
88 | updatedAt = this.updatedAt,
89 | )
90 | }
91 |
92 | fun BankAccount.Companion.of(request: com.example.grpc.bank.service.BankAccount.CreateBankAccountRequest): BankAccount {
93 | return BankAccount(
94 | id = null,
95 | email = request.email,
96 | firstName = request.firstName,
97 | lastName = request.lastName,
98 | address = request.address,
99 | phone = request.phone,
100 | currency = Currency.valueOf(request.currency),
101 | balance = BigDecimal.valueOf(request.balance),
102 | updatedAt = LocalDateTime.now(),
103 | createdAt = LocalDateTime.now(),
104 | )
105 | }
106 |
107 | fun BankAccount.Companion.of(request: CreateBankAccountDto): BankAccount {
108 | return BankAccount(
109 | id = null,
110 | email = request.email,
111 | firstName = request.firstName,
112 | lastName = request.lastName,
113 | address = request.address,
114 | phone = request.phone,
115 | currency = request.currency,
116 | balance = request.balance,
117 | updatedAt = LocalDateTime.now(),
118 | createdAt = LocalDateTime.now(),
119 | )
120 | }
--------------------------------------------------------------------------------
/src/main/kotlin/com/example/alexbryksin/domain/Currency.kt:
--------------------------------------------------------------------------------
1 | package com.example.alexbryksin.domain
2 |
3 | enum class Currency {
4 | EUR, USD
5 | }
--------------------------------------------------------------------------------
/src/main/kotlin/com/example/alexbryksin/dto/CreateBankAccountDto.kt:
--------------------------------------------------------------------------------
1 | package com.example.alexbryksin.dto
2 |
3 | import com.example.alexbryksin.domain.Currency
4 | import java.math.BigDecimal
5 | import javax.validation.constraints.DecimalMin
6 | import javax.validation.constraints.Email
7 | import javax.validation.constraints.Size
8 |
9 | data class CreateBankAccountDto(
10 | @get:Email @get:Size(min = 6, max = 60) var email: String,
11 | @get:Size(min = 3, max = 60) val firstName: String,
12 | @get:Size(min = 3, max = 60) val lastName: String,
13 | @get:Size(min = 3, max = 500) val address: String,
14 | @get:Size(min = 6, max = 20) val phone: String,
15 | var currency: Currency,
16 | @get:DecimalMin(value = "0.0") val balance: BigDecimal,
17 | )
18 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/example/alexbryksin/dto/DepositBalanceDto.kt:
--------------------------------------------------------------------------------
1 | package com.example.alexbryksin.dto
2 |
3 | import java.math.BigDecimal
4 | import javax.validation.constraints.DecimalMin
5 |
6 | class DepositBalanceDto(@get:DecimalMin(value = "0.0") val amount: BigDecimal)
--------------------------------------------------------------------------------
/src/main/kotlin/com/example/alexbryksin/dto/ErrorHttpResponse.kt:
--------------------------------------------------------------------------------
1 | package com.example.alexbryksin.dto
2 |
3 | data class ErrorHttpResponse(
4 | val status: Int,
5 | val message: String,
6 | val timestamp: String
7 | )
8 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/example/alexbryksin/dto/FindByBalanceRequestDto.kt:
--------------------------------------------------------------------------------
1 | package com.example.alexbryksin.dto
2 |
3 | import com.example.grpc.bank.service.BankAccount
4 | import org.springframework.data.domain.PageRequest
5 | import org.springframework.data.domain.Pageable
6 | import java.math.BigDecimal
7 | import javax.validation.constraints.DecimalMin
8 |
9 | data class FindByBalanceRequestDto(
10 | @get:DecimalMin(value = "0.0") val minBalance: BigDecimal,
11 | @get:DecimalMin(value = "1.0") val maxBalance: BigDecimal,
12 | val pageable: Pageable
13 | ) {
14 | companion object
15 | }
16 |
17 |
18 | fun FindByBalanceRequestDto.Companion.of(request: BankAccount.GetAllByBalanceWithPaginationRequest): FindByBalanceRequestDto =
19 | FindByBalanceRequestDto(request.min.toBigDecimal(), request.max.toBigDecimal(), PageRequest.of(request.page, request.size))
20 |
21 | fun FindByBalanceRequestDto.Companion.of(request: BankAccount.GetAllByBalanceRequest): FindByBalanceRequestDto =
22 | FindByBalanceRequestDto(request.min.toBigDecimal(), request.max.toBigDecimal(), PageRequest.of(request.page, request.size))
--------------------------------------------------------------------------------
/src/main/kotlin/com/example/alexbryksin/dto/SuccessBankAccountResponse.kt:
--------------------------------------------------------------------------------
1 | package com.example.alexbryksin.dto
2 |
3 | import com.example.alexbryksin.domain.Currency
4 | import java.math.BigDecimal
5 | import java.time.LocalDateTime
6 | import java.util.*
7 |
8 | data class SuccessBankAccountResponse(
9 | val id: UUID?,
10 | val email: String?,
11 | val firstName: String?,
12 | val lastName: String?,
13 | val address: String?,
14 | val phone: String?,
15 | val currency: Currency?,
16 | val balance: BigDecimal?,
17 | val createdAt: LocalDateTime?,
18 | val updatedAt: LocalDateTime?,
19 | )
20 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/example/alexbryksin/dto/WithdrawBalanceDto.kt:
--------------------------------------------------------------------------------
1 | package com.example.alexbryksin.dto
2 |
3 | import java.math.BigDecimal
4 | import javax.validation.constraints.DecimalMin
5 |
6 | data class WithdrawBalanceDto(@get:DecimalMin(value = "0.0") val amount: BigDecimal)
7 |
--------------------------------------------------------------------------------
/src/main/kotlin/com/example/alexbryksin/exceptions/BankAccountNotFoundException.kt:
--------------------------------------------------------------------------------
1 | package com.example.alexbryksin.exceptions
2 |
3 | class BankAccountNotFoundException : RuntimeException {
4 | constructor() : super()
5 | constructor(id: String?) : super("bank account with $id not found")
6 | constructor(id: String?, cause: Throwable?) : super("bank account with $id not found", cause)
7 | }
--------------------------------------------------------------------------------
/src/main/kotlin/com/example/alexbryksin/exceptions/InvalidAmountException.kt:
--------------------------------------------------------------------------------
1 | package com.example.alexbryksin.exceptions
2 |
3 | class InvalidAmountException : RuntimeException {
4 | constructor(amount: String?) : super("invalid amount $amount")
5 | constructor(amount: String?, cause: Throwable?) : super("invalid amount $amount", cause)
6 | constructor(cause: Throwable?) : super(cause)
7 | }
--------------------------------------------------------------------------------
/src/main/kotlin/com/example/alexbryksin/interceptors/GlobalInterceptorConfiguration.kt:
--------------------------------------------------------------------------------
1 | package com.example.alexbryksin.interceptors
2 |
3 | import net.devh.boot.grpc.server.interceptor.GrpcGlobalServerInterceptor
4 | import org.springframework.context.annotation.Configuration
5 |
6 |
7 | @Configuration(proxyBeanMethods = false)
8 | class GlobalInterceptorConfiguration {
9 |
10 | @GrpcGlobalServerInterceptor
11 | fun logServerInterceptor(): LogGrpcInterceptor? = LogGrpcInterceptor()
12 | }
--------------------------------------------------------------------------------
/src/main/kotlin/com/example/alexbryksin/interceptors/LogGrpcInterceptor.kt:
--------------------------------------------------------------------------------
1 | package com.example.alexbryksin.interceptors
2 |
3 | import io.grpc.Metadata
4 | import io.grpc.ServerCall
5 | import io.grpc.ServerCallHandler
6 | import io.grpc.ServerInterceptor
7 | import org.slf4j.LoggerFactory
8 |
9 | class LogGrpcInterceptor : ServerInterceptor {
10 |
11 | override fun interceptCall(
12 | call: ServerCall,
13 | headers: Metadata,
14 | next: ServerCallHandler
15 | ): ServerCall.Listener {
16 | log.info("Service: ${call.methodDescriptor.serviceName}, Method: ${call.methodDescriptor.bareMethodName}, Headers: $headers")
17 | return next.startCall(call, headers)
18 | }
19 |
20 | companion object {
21 | private val log = LoggerFactory.getLogger(LogGrpcInterceptor::class.java)
22 | }
23 | }
--------------------------------------------------------------------------------
/src/main/kotlin/com/example/alexbryksin/repositories/BankPostgresRepository.kt:
--------------------------------------------------------------------------------
1 | package com.example.alexbryksin.repositories
2 |
3 | import com.example.alexbryksin.domain.BankAccount
4 | import org.springframework.data.domain.Page
5 | import org.springframework.data.domain.Pageable
6 | import org.springframework.stereotype.Repository
7 | import java.math.BigDecimal
8 |
9 |
10 | @Repository
11 | interface BankPostgresRepository {
12 |
13 | suspend fun findByBalanceAmount(min: BigDecimal, max: BigDecimal, pageable: Pageable): Page
14 |
15 | }
--------------------------------------------------------------------------------
/src/main/kotlin/com/example/alexbryksin/repositories/BankPostgresRepositoryImpl.kt:
--------------------------------------------------------------------------------
1 | package com.example.alexbryksin.repositories
2 |
3 | import com.example.alexbryksin.domain.BankAccount
4 | import com.example.alexbryksin.domain.BankAccount.Companion.BALANCE
5 | import com.example.alexbryksin.utils.runWithTracing
6 | import kotlinx.coroutines.Dispatchers
7 | import kotlinx.coroutines.async
8 | import kotlinx.coroutines.flow.toList
9 | import kotlinx.coroutines.reactive.asFlow
10 | import kotlinx.coroutines.reactive.awaitFirst
11 | import kotlinx.coroutines.withContext
12 | import org.slf4j.LoggerFactory
13 | import org.springframework.cloud.sleuth.Tracer
14 | import org.springframework.cloud.sleuth.instrument.kotlin.asContextElement
15 | import org.springframework.data.domain.Page
16 | import org.springframework.data.domain.PageImpl
17 | import org.springframework.data.domain.Pageable
18 | import org.springframework.data.r2dbc.core.R2dbcEntityTemplate
19 | import org.springframework.data.relational.core.query.Criteria
20 | import org.springframework.data.relational.core.query.Query
21 | import org.springframework.r2dbc.core.DatabaseClient
22 | import org.springframework.stereotype.Repository
23 | import java.math.BigDecimal
24 |
25 |
26 | @Repository
27 | class BankPostgresRepositoryImpl(
28 | private val template: R2dbcEntityTemplate,
29 | private val databaseClient: DatabaseClient,
30 | private val tracer: Tracer,
31 | ) : BankPostgresRepository {
32 |
33 | override suspend fun findByBalanceAmount(min: BigDecimal, max: BigDecimal, pageable: Pageable): Page =
34 | withContext(Dispatchers.IO + tracer.asContextElement()) {
35 | val span = tracer.startScopedSpan(GET_ALL_BY_BALANCE_AMOUNT)
36 | val query = Query.query(Criteria.where(BALANCE).between(min, max))
37 |
38 | runWithTracing(span) {
39 | val accountsList = async {
40 | template.select(query.with(pageable), BankAccount::class.java)
41 | .asFlow()
42 | .toList()
43 | }
44 |
45 | val totalCount = async {
46 | databaseClient.sql("SELECT count(bank_account_id) as total FROM microservices.bank_accounts WHERE balance BETWEEN :min AND :max")
47 | .bind("min", min)
48 | .bind("max", max)
49 | .fetch()
50 | .one()
51 | .awaitFirst()
52 | }
53 |
54 | PageImpl(accountsList.await(), pageable, totalCount.await()["total"] as Long)
55 | .also { span.tag("pagination", it.toString()) }
56 | .also { log.debug("pagination: $it") }
57 | }
58 | }
59 |
60 | companion object {
61 | private val log = LoggerFactory.getLogger(BankPostgresRepositoryImpl::class.java)
62 | private const val GET_ALL_BY_BALANCE_AMOUNT = "BankPostgresRepository.findByBalanceAmount"
63 | }
64 | }
--------------------------------------------------------------------------------
/src/main/kotlin/com/example/alexbryksin/repositories/BankRepository.kt:
--------------------------------------------------------------------------------
1 | package com.example.alexbryksin.repositories
2 |
3 | import com.example.alexbryksin.domain.BankAccount
4 | import kotlinx.coroutines.flow.Flow
5 | import org.springframework.data.domain.Pageable
6 | import org.springframework.data.repository.kotlin.CoroutineSortingRepository
7 | import org.springframework.stereotype.Repository
8 | import java.math.BigDecimal
9 | import java.util.*
10 |
11 |
12 | @Repository
13 | interface BankRepository : CoroutineSortingRepository, BankPostgresRepository {
14 |
15 | fun findAllByBalanceBetween(min: BigDecimal, max: BigDecimal, pageable: Pageable): Flow
16 |
17 | }
--------------------------------------------------------------------------------
/src/main/kotlin/com/example/alexbryksin/services/BankAccountService.kt:
--------------------------------------------------------------------------------
1 | package com.example.alexbryksin.services
2 |
3 | import com.example.alexbryksin.domain.BankAccount
4 | import com.example.alexbryksin.dto.FindByBalanceRequestDto
5 | import kotlinx.coroutines.flow.Flow
6 | import org.springframework.data.domain.Page
7 | import org.springframework.stereotype.Service
8 | import java.math.BigDecimal
9 | import java.util.*
10 |
11 |
12 | @Service
13 | interface BankAccountService {
14 |
15 | suspend fun createBankAccount(bankAccount: BankAccount): BankAccount
16 |
17 | suspend fun getBankAccountById(id: UUID): BankAccount
18 |
19 | suspend fun depositAmount(id: UUID, amount: BigDecimal): BankAccount
20 |
21 | suspend fun withdrawAmount(id: UUID, amount: BigDecimal): BankAccount
22 |
23 | fun findAllByBalanceBetween(requestDto: FindByBalanceRequestDto): Flow
24 |
25 | suspend fun findByBalanceAmount(requestDto: FindByBalanceRequestDto): Page
26 | }
--------------------------------------------------------------------------------
/src/main/kotlin/com/example/alexbryksin/services/BankAccountServiceImpl.kt:
--------------------------------------------------------------------------------
1 | package com.example.alexbryksin.services
2 |
3 | import com.example.alexbryksin.domain.BankAccount
4 | import com.example.alexbryksin.dto.FindByBalanceRequestDto
5 | import com.example.alexbryksin.exceptions.BankAccountNotFoundException
6 | import com.example.alexbryksin.repositories.BankRepository
7 | import com.example.alexbryksin.utils.runWithTracing
8 | import kotlinx.coroutines.Dispatchers
9 | import kotlinx.coroutines.flow.Flow
10 | import kotlinx.coroutines.withContext
11 | import org.springframework.cloud.sleuth.Tracer
12 | import org.springframework.cloud.sleuth.instrument.kotlin.asContextElement
13 | import org.springframework.data.domain.Page
14 | import org.springframework.stereotype.Service
15 | import org.springframework.transaction.annotation.Transactional
16 | import java.math.BigDecimal
17 | import java.util.*
18 | import javax.validation.Valid
19 |
20 | @Service
21 | class BankAccountServiceImpl(
22 | private val bankRepository: BankRepository,
23 | private val tracer: Tracer
24 | ) : BankAccountService {
25 |
26 | @Transactional
27 | override suspend fun createBankAccount(@Valid bankAccount: BankAccount): BankAccount =
28 | withContext(Dispatchers.IO + tracer.asContextElement()) {
29 | val span = tracer.startScopedSpan(CREATE_BANK_ACCOUNT)
30 |
31 | runWithTracing(span) {
32 | bankRepository.save(bankAccount).also { span.tag("saved account", it.toString()) }
33 | }
34 | }
35 |
36 | @Transactional(readOnly = true)
37 | override suspend fun getBankAccountById(id: UUID): BankAccount =
38 | withContext(Dispatchers.IO + tracer.asContextElement()) {
39 | val span = tracer.startScopedSpan(GET_BANK_ACCOUNT_BY_ID)
40 |
41 | runWithTracing(span) {
42 | bankRepository.findById(id).also { span.tag("bank account", it.toString()) }
43 | ?: throw BankAccountNotFoundException(id.toString())
44 | }
45 | }
46 |
47 | @Transactional
48 | override suspend fun depositAmount(id: UUID, amount: BigDecimal): BankAccount =
49 | withContext(Dispatchers.IO + tracer.asContextElement()) {
50 | val span = tracer.startScopedSpan(DEPOSIT_AMOUNT)
51 |
52 | runWithTracing(span) {
53 | bankRepository.findById(id)
54 | ?.let { bankRepository.save(it.depositAmount(amount)) }
55 | .also { span.tag("bank account", it.toString()) }
56 | ?: throw BankAccountNotFoundException(id.toString())
57 | }
58 | }
59 |
60 | @Transactional
61 | override suspend fun withdrawAmount(id: UUID, amount: BigDecimal): BankAccount =
62 | withContext(Dispatchers.IO + tracer.asContextElement()) {
63 | val span = tracer.startScopedSpan(WITHDRAW_AMOUNT)
64 |
65 | runWithTracing(span) {
66 | bankRepository.findById(id)
67 | ?.let { bankRepository.save(it.withdrawAmount(amount)) }
68 | .also { span.tag("bank account", it.toString()) }
69 | ?: throw BankAccountNotFoundException(id.toString())
70 | }
71 | }
72 |
73 | @Transactional(readOnly = true)
74 | override fun findAllByBalanceBetween(requestDto: FindByBalanceRequestDto): Flow {
75 | val span = tracer.startScopedSpan(GET_ALL_BY_BALANCE)
76 |
77 | runWithTracing(span) {
78 | return bankRepository.findAllByBalanceBetween(
79 | requestDto.minBalance,
80 | requestDto.maxBalance,
81 | requestDto.pageable
82 | )
83 | }
84 | }
85 |
86 | @Transactional(readOnly = true)
87 | override suspend fun findByBalanceAmount(requestDto: FindByBalanceRequestDto): Page =
88 | withContext(Dispatchers.IO + tracer.asContextElement()) {
89 | val span = tracer.startScopedSpan(GET_ALL_BY_BALANCE_WITH_PAGINATION)
90 |
91 | runWithTracing(span) {
92 | bankRepository.findByBalanceAmount(requestDto.minBalance, requestDto.maxBalance, requestDto.pageable)
93 | .also { span.tag("pagination", it.toString()) }
94 | }
95 | }
96 |
97 |
98 | companion object {
99 | private const val CREATE_BANK_ACCOUNT = "BankAccountService.createBankAccount"
100 | private const val GET_BANK_ACCOUNT_BY_ID = "BankAccountService.getBankAccountById"
101 | private const val DEPOSIT_AMOUNT = "BankAccountService.depositAmount"
102 | private const val WITHDRAW_AMOUNT = "BankAccountService.withdrawAmount"
103 | private const val GET_ALL_BY_BALANCE = "BankAccountService.findAllByBalanceBetween"
104 | private const val GET_ALL_BY_BALANCE_WITH_PAGINATION = "BankAccountService.findByBalanceAmount"
105 | }
106 | }
--------------------------------------------------------------------------------
/src/main/kotlin/com/example/alexbryksin/utils/TracingUtils.kt:
--------------------------------------------------------------------------------
1 | package com.example.alexbryksin.utils
2 |
3 | import org.springframework.cloud.sleuth.ScopedSpan
4 | import org.springframework.cloud.sleuth.Tracer
5 |
6 |
7 | inline fun T.runWithTracing(span: ScopedSpan, block: T.() -> R): R {
8 | return try {
9 | block()
10 | } catch (ex: Exception) {
11 | span.error(ex)
12 | throw ex
13 | } finally {
14 | span.end()
15 | }
16 | }
17 |
18 | inline fun T.runWithTracing(tracer: Tracer, name: String, block: T.() -> R): R {
19 | val span = tracer.startScopedSpan(name)
20 |
21 | return try {
22 | block()
23 | } catch (ex: Exception) {
24 | span.error(ex)
25 | throw ex
26 | } finally {
27 | span.end()
28 | }
29 | }
--------------------------------------------------------------------------------
/src/main/proto/bank_account.proto:
--------------------------------------------------------------------------------
1 | syntax = "proto3";
2 |
3 | package com.example.grpc.bank.service;
4 |
5 | //import "google/protobuf/wrappers.proto";
6 | //import "google/protobuf/timestamp.proto";
7 |
8 | service BankAccountService {
9 | rpc createBankAccount (CreateBankAccountRequest) returns (CreateBankAccountResponse);
10 | rpc getBankAccountById (GetBankAccountByIdRequest) returns (GetBankAccountByIdResponse);
11 | rpc depositBalance (DepositBalanceRequest) returns (DepositBalanceResponse);
12 | rpc withdrawBalance (WithdrawBalanceRequest) returns (WithdrawBalanceResponse);
13 | rpc getAllByBalance (GetAllByBalanceRequest) returns (stream GetAllByBalanceResponse);
14 | rpc getAllByBalanceWithPagination(GetAllByBalanceWithPaginationRequest) returns (GetAllByBalanceWithPaginationResponse);
15 | }
16 |
17 | message BankAccountData {
18 | string id = 1;
19 | string firstName = 2;
20 | string lastName = 3;
21 | string email = 4;
22 | string address = 5;
23 | string currency = 6;
24 | string phone = 7;
25 | double balance = 8;
26 | string createdAt = 9;
27 | string updatedAt = 10;
28 | }
29 |
30 | message CreateBankAccountRequest {
31 | string email = 1;
32 | string firstName = 2;
33 | string lastName = 3;
34 | string address = 4;
35 | string currency = 5;
36 | string phone = 6;
37 | double balance = 7;
38 | }
39 |
40 | message CreateBankAccountResponse {
41 | BankAccountData bankAccount = 1;
42 | }
43 |
44 | message GetBankAccountByIdRequest {
45 | string id = 1;
46 | }
47 |
48 | message GetBankAccountByIdResponse {
49 | BankAccountData bankAccount = 1;
50 | }
51 |
52 | message DepositBalanceRequest {
53 | string id = 1;
54 | double balance = 2;
55 | }
56 |
57 | message DepositBalanceResponse {
58 | BankAccountData bankAccount = 1;
59 | }
60 |
61 | message WithdrawBalanceRequest {
62 | string id = 1;
63 | double balance = 2;
64 | }
65 |
66 | message WithdrawBalanceResponse {
67 | BankAccountData bankAccount = 1;
68 | }
69 |
70 | message GetAllByBalanceRequest {
71 | double min = 1;
72 | double max = 2;
73 | int32 page = 3;
74 | int32 size = 4;
75 | }
76 |
77 | message GetAllByBalanceResponse {
78 | BankAccountData bankAccount = 1;
79 | }
80 |
81 | message GetAllByBalanceWithPaginationRequest {
82 | double min = 1;
83 | double max = 2;
84 | int32 page = 3;
85 | int32 size = 4;
86 |
87 | }
88 |
89 | message GetAllByBalanceWithPaginationResponse {
90 | repeated BankAccountData bankAccount = 1;
91 | int32 page = 2;
92 | int32 size = 3;
93 | int32 totalElements = 4;
94 | int32 totalPages = 5;
95 | bool isFirst = 6;
96 | bool isLast = 7;
97 | }
--------------------------------------------------------------------------------
/src/main/resources/application.properties:
--------------------------------------------------------------------------------
1 | grpc.server.port=8000
2 | spring.application.name=microservice
3 | server.port=8080
4 |
5 | server.shutdown=graceful
6 | spring.lifecycle.timeout-per-shutdown-phase=30s
7 |
8 | grpc.server.reflection-service-enabled=true
9 |
10 | management.endpoints.web.exposure.include=*
11 | management.metrics.export.prometheus.enabled=true
12 | management.endpoint.health.probes.enabled=true
13 | management.health.livenessState.enabled=true
14 | management.health.readinessState.enabled=true
15 | management.endpoint.health.group.readiness.include=readinessState,customCheck
16 | management.endpoint.health.group.liveness.include=livenessState,customCheck
17 |
18 | management.endpoint.flyway.enabled=true
19 |
20 | spring.flyway.validate-on-migrate=true
21 | spring.flyway.user=postgres
22 | spring.flyway.password=postgres
23 | spring.flyway.url=jdbc:postgresql://localhost:5432/bank_accounts
24 | spring.flyway.schemas=["microservices"]
25 |
26 |
27 | spring.r2dbc.name=bank_accounts
28 | spring.r2dbc.password=postgres
29 | spring.r2dbc.username=postgres
30 | spring.r2dbc.url=r2dbc:postgresql://localhost:5432/bank_accounts
31 | spring.r2dbc.pool.max-size=30
32 | spring.data.r2dbc.repositories.enabled=true
33 | spring.r2dbc.pool.initial-size=20
34 |
35 | faker.enable=false
36 | faker.locale=en
37 | faker.count=1000
38 |
39 | springdoc.swagger-ui.path=/swagger-ui.html
40 | springdoc.swagger-ui.enabled=true
--------------------------------------------------------------------------------
/src/main/resources/db/migration/V1__initial_setup.sql:
--------------------------------------------------------------------------------
1 | CREATE EXTENSION IF NOT EXISTS citext;
2 | CREATE EXTENSION IF NOT EXISTS "uuid-ossp";
3 | CREATE SCHEMA microservices;
4 |
5 |
6 | CREATE TABLE IF NOT EXISTS microservices.bank_accounts
7 | (
8 | bank_account_id UUID DEFAULT uuid_generate_v4(),
9 | first_name VARCHAR(60) NOT NULL CHECK ( first_name <> '' ),
10 | last_name VARCHAR(60) NOT NULL CHECK ( last_name <> '' ),
11 | email VARCHAR(60) UNIQUE NOT NULL CHECK ( email <> '' ),
12 | address VARCHAR(500) NOT NULL CHECK ( address <> '' ),
13 | phone VARCHAR(20) UNIQUE NOT NULL CHECK ( phone <> '' ),
14 | balance DECIMAL(16, 2) NOT NULL DEFAULT 0.00,
15 | currency VARCHAR(3) NOT NULL DEFAULT 'USD',
16 | created_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP,
17 | updated_at TIMESTAMP WITH TIME ZONE DEFAULT CURRENT_TIMESTAMP
18 | );
19 |
20 | CREATE INDEX IF NOT EXISTS bank_account_email_idx ON microservices.bank_accounts (email);
--------------------------------------------------------------------------------
/src/test/kotlin/com/example/alexbryksin/KotlinSpringGrpcApplicationTests.kt:
--------------------------------------------------------------------------------
1 | package com.example.alexbryksin
2 |
3 | import com.example.grpc.bank.service.BankAccount
4 | import com.example.grpc.bank.service.BankAccount.CreateBankAccountRequest
5 | import com.example.grpc.bank.service.BankAccountServiceGrpcKt
6 | import io.grpc.ManagedChannelBuilder
7 | import kotlinx.coroutines.flow.collectIndexed
8 | import kotlinx.coroutines.runBlocking
9 | import org.junit.jupiter.api.Test
10 | import java.util.concurrent.TimeUnit
11 |
12 | //@SpringBootTest
13 | class KotlinSpringGrpcApplicationTests {
14 |
15 | @Test
16 | fun createBankAccount(): Unit = runBlocking {
17 | val channel = ManagedChannelBuilder.forAddress("localhost", 8000).usePlaintext().build()
18 |
19 | try {
20 | val client = BankAccountServiceGrpcKt.BankAccountServiceCoroutineStub(channel)
21 | val request = CreateBankAccountRequest.newBuilder()
22 | .setEmail("alexander.bryksin@yandex.ru")
23 | .build()
24 | val response = client.createBankAccount(request)
25 |
26 | println("response: $response")
27 | } catch (ex: Exception) {
28 | println("ex: $ex")
29 | } finally {
30 | channel.shutdown()
31 | channel.awaitTermination(5000, TimeUnit.MILLISECONDS)
32 | }
33 |
34 | }
35 |
36 | @Test
37 | fun findAllByAmount(): Unit = runBlocking {
38 | val channel = ManagedChannelBuilder.forAddress("localhost", 8000).usePlaintext().build()
39 |
40 | try {
41 | val client = BankAccountServiceGrpcKt.BankAccountServiceCoroutineStub(channel)
42 | val request = BankAccount.GetAllByBalanceRequest.newBuilder()
43 | .setMin(0.0)
44 | .setMax(500000.00)
45 | .setPage(1)
46 | .setSize(20)
47 | .build()
48 | val response = client.getAllByBalance(request)
49 |
50 | response.collectIndexed { index, value -> println("index: $index, value: $value") }
51 | } catch (ex: Exception) {
52 | println("ex: $ex")
53 | } finally {
54 | channel.shutdown()
55 | channel.awaitTermination(5000, TimeUnit.MILLISECONDS)
56 | }
57 | }
58 |
59 | }
60 |
--------------------------------------------------------------------------------