├── .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 | Swagger 22 | 23 | #### Grafana UI: http://localhost:3000 24 | Grafana 25 | 26 | #### Zipkin UI: http://localhost:9411 27 | Zipkin 28 | 29 | #### Prometheus UI: http://localhost:9090 30 | Prometheus 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 | --------------------------------------------------------------------------------