Detta projekt är en mikrotjänstbaserad chattapplikation byggd med Java, Spring Boot, MySQL, RabbitMQ, gRPC, Docker och Kubernetes.
Applikationen låter en användare skapa konto, logga in, välja chattrum och skriva meddelanden. Om ett meddelande innehåller @bot publiceras ett event via outbox-mönstret till RabbitMQ, där botservice tar emot eventet och skapar ett botsvar i chatten.
Projektet har även stöd för native-images med GraalVM/Spring Boot AOT. Kubernetes-manifestet använder de native-images som byggs lokalt.
- Vad appen gör
- Tjänster
- Arkitektur
- Användarflöde
- Tekniskt meddelandeflöde
- Databaser och tabeller
- Säkerhet
- Bot och AI-läge
- Köra med Docker Compose
- Köra lokalt i IntelliJ
- Köra med Kubernetes
- Bygga native-images
- API-exempel
- Tester
- Felsökning
Appen består av en enkel webbchatt där användaren först måste skapa konto eller logga in. Efter inloggning visas chattvyn. Där kan användaren:
- välja rum:
general,supportellerrandom - läsa tidigare meddelanden i valt rum
- skriva nya meddelanden
- skicka med Enter
- använda Shift+Enter för ny rad
- skriva
@botför att trigga ett botsvar - logga ut
Webbgränssnittet finns i bff/src/main/resources/static och serveras av BFF-tjänsten.
| Tjänst | Port lokalt | Port i Kubernetes | Ansvar |
|---|---|---|---|
bff |
8080 |
internt 8080, exponeras via Traefik |
Webb-UI och API-gateway för frontend |
authservice |
9000 |
internt 9000 |
Registrering, login, JWT och JWKS |
userservice |
8083 |
internt 8083 |
Användarprofiler via REST och gRPC |
messageservice |
8081 |
internt 8081 |
Meddelanden, rum, outbox och RabbitMQ-publicering |
botservice |
8082 |
internt 8082 |
Lyssnar på RabbitMQ-event och skapar botsvar |
| MySQL | 3306 |
internt 3306 |
Databaser för auth, users och messages |
| RabbitMQ | 5672, 15672 |
internt 5672, 15672 |
Eventkö och management UI |
I normal användning ska webbläsaren bara prata med bff. De andra tjänsterna är interna.
flowchart LR
Browser["Webbläsare"] --> Traefik["Traefik<br/>Gateway API"]
Traefik --> BFF["bff<br/>UI + API gateway"]
BFF --> Auth["authservice<br/>login/register/JWT"]
BFF --> Messages["messageservice<br/>chat + outbox"]
BFF --> Users["userservice<br/>user REST"]
Auth --> Users
Messages --> UsersGrpc["userservice gRPC"]
Messages --> MySQL["MySQL"]
Auth --> MySQL
Users --> MySQL
Messages --> Rabbit["RabbitMQ"]
Rabbit --> Bot["botservice"]
Bot --> Messages
- Användaren öppnar webbappen.
- Om ingen giltig session finns visas loginvyn.
- Användaren kan skapa konto med användarnamn, visningsnamn och lösenord.
- BFF skickar registreringen till
authservice. authserviceskapar först en användarprofil iuserservice.authservicesparar sedan inloggningsuppgifterna iauth_db.authservicereturnerar en JWT access token.- BFF/frontenden sparar token i
localStorage. - Chatten visas.
- När användaren skickar meddelanden skickas token med som
Authorization: Bearer .... - BFF läser ut
userIdfrån JWT och skickar meddelandet vidare tillmessageservice. messageserviceslår upp användarnamn via gRPC motuserservice.- Meddelandet sparas i databasen och visas i chatten.
Sessionen ligger i webbläsarens localStorage och återställs automatiskt efter omladdning så länge token inte har gått ut. Token gäller i 1 timme.
När ett vanligt meddelande skickas:
- Frontend skickar
POST /api/messagestill BFF. - BFF kontrollerar JWT och skickar vidare till
messageservice. messageservicesparar meddelandet i tabellenmessages.- I samma transaktion skapas också ett event i tabellen
outbox_events. OutboxRelaykör schemalagt och letar efterPENDINGevent.- Eventet publiceras till RabbitMQ.
- RabbitMQ bekräftar publiceringen.
- Outbox-eventet markeras som
PROCESSED.
När meddelandet innehåller @bot:
botservicetar emotMessagePublishedEventfrån RabbitMQ.- Om avsändaren redan är botten ignoreras eventet för att undvika loop.
- Om texten inte innehåller
@botignoreras eventet. - Om texten innehåller
@botskapas ett botsvar. botserviceskickar botsvaret tillbaka tillmessageservice.messageservicesparar botmeddelandet i samma chattrum.- Frontendens auto-refresh hämtar svaret och visar det.
MySQL används med tre databaser:
| Databas | Ägare | Innehåll |
|---|---|---|
users_db |
userservice |
användarprofiler |
auth_db |
authservice |
loginuppgifter och koppling till user-id |
messages_db |
messageservice |
chattmeddelanden och outbox-events |
Databasscheman hanteras med Flyway i respektive modul. Hibernate körs med ddl-auto=validate, vilket betyder att Spring inte automatiskt skapar eller ändrar tabeller vid start. Om tabeller saknas ska Flyway-migrationerna eller Kubernetes-initjobbet skapa dem.
I Kubernetes finns dessutom ett mysql-init-databases Job i k8s/labb2.yaml. Det skapar databaserna idempotent så att Flyway i respektive tjänst kan skapa och migrera tabellerna även i ett tomt lokalt kluster.
Applikationen använder flera säkerhetslager:
- Användare loggar in via
authservice. - Lösenord sparas hashade.
authserviceutfärdar JWT med ES256-signering.- BFF validerar JWT mot
authserviceJWKS endpoint:/auth/jwks. - BFF skickar interna anrop vidare med headern
X-Internal-Api-Key. - Interna tjänster kräver samma interna API-nyckel.
- Authservice sparar sin signeringsnyckel på disk eller PVC så att token inte blir ogiltiga vid varje omstart.
Hemliga värden ska inte ligga i application.properties, docker-compose.yml eller k8s/labb2.yaml.
Projektet läser dem i stället från:
- Docker Compose secrets i
secrets/*.txt - miljövariabler när du kör tjänster direkt i IntelliJ
- Kubernetes
Secret-objekt i klustret
Som standard kör botten i regelbaserat läge. Då svarar den med fasta, men lite varierade, standardsvar beroende på meddelandet:
- hälsningar som
hej - frågor som
hur mår du - ord som
hjälp - ord som
test - tack eller hejdå
Botten svarar bara när meddelandet innehåller @bot.
Det finns även stöd för AI-svar via OpenRouter. Det är avstängt som standard. Om AI-läget är aktivt visas ett val i webbgränssnittet för Neutral eller Pirat. Valet skickas med varje meddelande och påverkar bara bottsvaret för det meddelandet. Om AI-läget är aktivt och API-anropet misslyckas faller botten tillbaka till regelbaserade svar.
botservice innehåller både den regelbaserade generatorn och OpenRouter-generatorn i samma native image. Valet mellan dem görs vid runtime med BOT_AI_ENABLED, så du kan slå på eller av AI-läget med Kubernetes ConfigMap/Secret och rollout utan att bygga om imagen. Om Java-koden för botten ändras måste imagen däremot byggas om.
När BOT_AI_ENABLED=true behöver botservice också en OpenRouter-nyckel via BOT_AI_API_KEY eller OPENROUTER_API_KEY. Saknas nyckeln startar inte botservice, vilket är avsiktligt för att undvika ett halvt aktiverat AI-läge.
PowerShell:
$env:BOT_AI_ENABLED="true"
$env:OPENROUTER_API_KEY="din-api-nyckel"
docker compose up --buildSkapa eller uppdatera först secreten med OpenRouter-nyckeln:
kubectl create secret generic bot-ai-secret `
-n labb2 `
--from-literal=OPENROUTER_API_KEY="din-api-nyckel" `
--dry-run=client -o yaml | kubectl apply -f -Slå sedan på AI-läget i ConfigMap:
$patch = @{
data = @{
BOT_AI_ENABLED = "true"
}
} | ConvertTo-Json -Compress
kubectl patch configmap bot-ai-config `
-n labb2 `
--type merge `
-p $patchStarta om botservice och bff så de läser miljövariablerna igen:
kubectl rollout restart deployment/botservice deployment/bff -n labb2
kubectl rollout status deployment/botservice -n labb2
kubectl rollout status deployment/bff -n labb2Kontrollera frontend-konfigurationen:
Invoke-RestMethod http://labb2.localhost/api/configKontrollera gärna också att botservice verkligen startade i AI-läge:
kubectl logs deployment/botservice -n labb2 --since=5mLoggen ska innehålla något i stil med:
AI bot response generation is enabled. Using OpenRouter model ...
$patch = @{
data = @{
BOT_AI_ENABLED = "false"
}
} | ConvertTo-Json -Compress
kubectl patch configmap bot-ai-config `
-n labb2 `
--type merge `
-p $patchkubectl rollout restart deployment/botservice deployment/bff -n labb2Docker Compose är enklaste sättet att köra hela systemet utan Kubernetes.
- Docker Desktop
- Lediga portar:
8080,8081,8082,8083,9000,3306,5672,15672
Första gången behöver du skapa lokala secret-filer. De ignoreras av git.
New-Item -ItemType Directory -Force secrets
[Convert]::ToBase64String([Security.Cryptography.RandomNumberGenerator]::GetBytes(32)) |
Set-Content -NoNewline secrets\mysql-root-password.txt
"labb2_rabbit" |
Set-Content -NoNewline secrets\rabbitmq-username.txt
[Convert]::ToBase64String([Security.Cryptography.RandomNumberGenerator]::GetBytes(24)) |
Set-Content -NoNewline secrets\rabbitmq-password.txt
[Convert]::ToBase64String([Security.Cryptography.RandomNumberGenerator]::GetBytes(32)) |
Set-Content -NoNewline secrets\internal-api-key.txt
[Convert]::ToBase64String([Security.Cryptography.RandomNumberGenerator]::GetBytes(32)) |
Set-Content -NoNewline secrets\auth-oauth-client-secret.txtStarta sedan från projektroten:
docker compose up --buildÖppna sedan:
http://localhost:8080
RabbitMQ Management finns här:
http://localhost:15672
Login:
användarnamn från secrets\rabbitmq-username.txt
lösenord från secrets\rabbitmq-password.txt
docker compose downOm du vill ta bort volymer och börja om med tom databas:
docker compose down -vDet här passar när du vill debugga tjänsterna en och en.
Starta bara MySQL och RabbitMQ:
docker compose up mysql rabbitmqEftersom appens hemligheter inte längre har dev-fallbacks behöver IntelliJ-konfigurationerna få miljövariabler. Det enklaste är att läsa samma lokala secret-filer och lägga värdena i varje Run Configuration som behöver dem.
Gemensamma värden:
$env:SPRING_DATASOURCE_USERNAME="root"
$env:SPRING_DATASOURCE_PASSWORD=(Get-Content secrets\mysql-root-password.txt -Raw)
$env:SPRING_RABBITMQ_USERNAME=(Get-Content secrets\rabbitmq-username.txt -Raw)
$env:SPRING_RABBITMQ_PASSWORD=(Get-Content secrets\rabbitmq-password.txt -Raw)
$env:INTERNAL_API_KEY=(Get-Content secrets\internal-api-key.txt -Raw)
$env:AUTH_OAUTH_CLIENT_SECRET=(Get-Content secrets\auth-oauth-client-secret.txt -Raw)I IntelliJ sätter du normalt:
userservice:SPRING_DATASOURCE_USERNAME,SPRING_DATASOURCE_PASSWORD,INTERNAL_API_KEYauthservice:SPRING_DATASOURCE_USERNAME,SPRING_DATASOURCE_PASSWORD,INTERNAL_API_KEY,AUTH_OAUTH_CLIENT_SECRETmessageservice:SPRING_DATASOURCE_USERNAME,SPRING_DATASOURCE_PASSWORD,SPRING_RABBITMQ_USERNAME,SPRING_RABBITMQ_PASSWORD,INTERNAL_API_KEYbotservice:SPRING_RABBITMQ_USERNAME,SPRING_RABBITMQ_PASSWORD,INTERNAL_API_KEYbff:INTERNAL_API_KEY
Starta modulerna i ungefär denna ordning:
userserviceauthservicemessageservicebotservicebff
Öppna sedan:
http://localhost:8080
| Modul | Port |
|---|---|
bff |
8080 |
messageservice |
8081 |
botservice |
8082 |
userservice |
8083 |
authservice |
9000 |
Om en port redan används måste den processen stoppas eller porten ändras i modulens application.properties.
Kubernetes-manifesten ligger i k8s/.
Huvudmanifest för själva applikationen:
k8s/labb2.yaml
Traefik installeras med Helm och denna values-fil:
k8s/traefik-values.yaml
Gateway API-manifestet för applikationens externa route:
k8s/labb2-gateway-traefik.yaml
- Docker Desktop
- Kubernetes aktiverat i Docker Desktop
kubectlfungerar mot Docker Desktop-klustret- Helm installerat
- Native-images finns lokalt, eller byggs enligt avsnittet Bygga native-images
Om helm version inte fungerar i PowerShell kan du installera Helm på Windows med:
winget install Helm.HelmStarta om terminalen efter installationen så att helm finns i PATH.
Kontrollera klustret:
kubectl get nodeskubectl create namespace labb2 --dry-run=client -o yaml | kubectl apply -f -
$mysqlPassword = [Convert]::ToBase64String([Security.Cryptography.RandomNumberGenerator]::GetBytes(32))
$rabbitPassword = [Convert]::ToBase64String([Security.Cryptography.RandomNumberGenerator]::GetBytes(24))
$internalApiKey = [Convert]::ToBase64String([Security.Cryptography.RandomNumberGenerator]::GetBytes(32))
$oauthClientSecret = [Convert]::ToBase64String([Security.Cryptography.RandomNumberGenerator]::GetBytes(32))
kubectl create secret generic labb2-secret `
-n labb2 `
--from-literal=mysql-root-password="$mysqlPassword" `
--from-literal=rabbitmq-username="labb2_rabbit" `
--from-literal=rabbitmq-password="$rabbitPassword" `
--from-literal=internal-api-key="$internalApiKey" `
--from-literal=auth-oauth-client-secret="$oauthClientSecret"Installera Gateway API CRDs och Traefik:
kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.5.1/standard-install.yaml
helm repo add traefik https://traefik.github.io/charts
helm repo update
helm upgrade --install traefik traefik/traefik `
--namespace traefik `
--create-namespace `
-f k8s\traefik-values.yaml `
--waitStarta applikationen och dess Gateway API route:
kubectl apply -f k8s\labb2.yaml
kubectl apply -f k8s\labb2-gateway-traefik.yamlVänta tills allt är redo:
kubectl get pods -n labb2
kubectl get gateway,httproute -n labb2Alla relevanta pods ska visa 1/1 Running, och initjobbet ska visa Completed.
Öppna appen:
http://labb2.localhost
Stoppa applikationen genom att ta bort namespacet:
kubectl delete namespace labb2Detta tar även bort lokala PVC:er i namespace labb2, så databasinnehåll försvinner i den lokala Kubernetes-miljön.
Ta bort Traefik separat om du även vill rensa ingress-/gatewaylagret:
helm uninstall traefik -n traefik
kubectl delete namespace traefikKubernetes-driften använder Traefik som extern entrypoint. Det innebär att BFF inte exponeras direkt med NodePort. I stället är BFF en intern ClusterIP-service, och Traefik routar extern HTTP-trafik till BFF via Gateway API.
Traefiks Kubernetes Service exponerar HTTP på port 80, medan Traefiks interna web entrypoint i Helm-chartet använder port 8000. Därför använder Gateway-listenern port 8000, men du surfar fortfarande till vanlig HTTP-port:
Flödet är:
http://labb2.localhost
-> Traefik LoadBalancer Service
-> Traefik web entrypoint 8000
-> Gateway labb2-gateway
-> HTTPRoute bff-route
-> bff Service
-> bff Pod
Om labb2.localhost inte fungerar i din miljö kan du testa med Host-header:
Invoke-WebRequest -Uri "http://localhost" -Headers @{ Host = "labb2.localhost" }Om port 80 redan används kan du temporärt port-forwarda Traefik:
kubectl -n traefik port-forward svc/traefik 8088:80Öppna då:
http://labb2.localhost:8088
Kubernetes-manifestet använder dessa images:
labb2-userservice:native
labb2-authservice:native
labb2-messageservice:native
labb2-botservice:native
labb2-bff:native
På grund av nuvarande builder-stöd används Java 25 vid native-build, även om projektets pom.xml anger Java 26.
Kör helst native-byggen när Kubernetes är nedskalat eller avstängt, eftersom GraalVM native-image kan använda mycket minne.
AI-läget i botservice är byggt för att vara runtime-styrt även i native image. Det betyder att ändring av BOT_AI_ENABLED, AI-modell, temperatur eller API-nyckel normalt bara kräver Kubernetes rollout av botservice och ibland bff, inte ny native-build. Ändringar i Java-kod, promptklasser, dependencies, application.properties eller frontendfiler kräver däremot ny image för berörd modul.
Om appen redan körs i Kubernetes:
kubectl scale deployment userservice authservice messageservice botservice bff -n labb2 --replicas=0
kubectl scale statefulset mysql rabbitmq -n labb2 --replicas=0Kör ett kommando i taget från respektive modulmapp.
userservice:
cd userservice
mvn clean -Pnative spring-boot:build-image -DskipTests "-Djava.version=25" "-Dspring-boot.build-image.imageName=labb2-userservice:native"
cd ..authservice:
cd authservice
mvn clean -Pnative spring-boot:build-image -DskipTests "-Djava.version=25" "-Dspring-boot.build-image.imageName=labb2-authservice:native"
cd ..messageservice:
cd messageservice
mvn clean -Pnative spring-boot:build-image -DskipTests "-Djava.version=25" "-Dspring-boot.build-image.imageName=labb2-messageservice:native"
cd ..botservice:
cd botservice
mvn clean -Pnative spring-boot:build-image -DskipTests "-Djava.version=25" "-Dspring-boot.build-image.imageName=labb2-botservice:native"
cd ..bff:
cd bff
mvn clean -Pnative spring-boot:build-image -DskipTests "-Djava.version=25" "-Dspring-boot.build-image.imageName=labb2-bff:native"
cd ..Ibland kan Paketo/Spring Boot build-image bli klar med själva native-binären men fastna i steget EXPORTING. Då brukar loggen visa något i stil med:
Finished generating 'org.example...Application'
===> EXPORTING
Om det händer finns Dockerfile.native-runtime i modulerna som fallback. Gör så här:
- Hitta buildercontainern:
docker ps- Kopiera ut native-runtime-lagret:
docker cp <builder-container-name>:/layers/paketo-buildpacks_native-image/native-image .\target\native-runtime- Bygg imagen manuellt:
docker build -f Dockerfile.native-runtime -t labb2-<modulnamn>:native .- Stoppa buildercontainern:
docker stop <builder-container-name>Exempel för BFF:
cd bff
docker cp great_stonebraker:/layers/paketo-buildpacks_native-image/native-image .\target\native-runtime
docker build -f Dockerfile.native-runtime -t labb2-bff:native .
docker stop great_stonebraker
cd ..Normalt används UI:t, men det går också att testa via PowerShell.
Använd port 8080:
$baseUrl = "http://localhost:8080"Använd Traefik-adressen:
$baseUrl = "http://labb2.localhost"$registerBody = @{
username = "martin"
displayName = "Martin"
password = "password"
} | ConvertTo-Json
$session = Invoke-RestMethod `
-Method Post `
-Uri "$baseUrl/api/register" `
-ContentType "application/json" `
-Body $registerBody$loginBody = @{
username = "martin"
password = "password"
} | ConvertTo-Json
$session = Invoke-RestMethod `
-Method Post `
-Uri "$baseUrl/api/login" `
-ContentType "application/json" `
-Body $loginBody$messageBody = @{
room = "general"
botPersonality = "neutral"
content = "Hej @bot"
} | ConvertTo-Json
Invoke-RestMethod `
-Method Post `
-Uri "$baseUrl/api/messages" `
-Headers @{ Authorization = "Bearer $($session.accessToken)" } `
-ContentType "application/json" `
-Body $messageBodyInvoke-RestMethod `
-Method Get `
-Uri "$baseUrl/api/messages?room=general" `
-Headers @{ Authorization = "Bearer $($session.accessToken)" }Kör tester per modul:
cd userservice
mvn test "-Djava.version=25"
cd ..
cd authservice
mvn test "-Djava.version=25"
cd ..
cd messageservice
mvn test "-Djava.version=25"
cd ..
cd botservice
mvn test "-Djava.version=25"
cd ..
cd bff
mvn test "-Djava.version=25"
cd ..Om en tjänst inte startar kan en port redan vara upptagen. Vanliga portar:
80, 443, 8080, 8081, 8082, 8083, 9000, 3306, 5672, 15672
Stoppa processen som använder porten, eller ändra server.port i berörd modul.
Docker Compose:
docker compose down -v
docker compose up --buildKubernetes:
kubectl delete namespace labb2
kubectl create namespace labb2 --dry-run=client -o yaml | kubectl apply -f -
# skapa labb2-secret igen enligt Kubernetes-avsnittet ovan
kubectl apply -f k8s\labb2.yaml
kubectl apply -f k8s\labb2-gateway-traefik.yamlRabbitMQ kan ta längre tid att starta i Docker Desktop, särskilt efter native-builds eller om Docker har lite minne. Kubernetes-manifestet använder TCP-probes och längre liveness-marginal för att undvika onödiga restarts.
Kontrollera status:
kubectl get pods -n labb2
kubectl logs rabbitmq-0 -n labb2Kontrollera:
- Innehåller meddelandet
@bot? - Är RabbitMQ redo?
- Kör
botservice? - Har outbox-eventet publicerats?
Kommandon:
kubectl logs deployment/messageservice -n labb2 --since=10m
kubectl logs deployment/botservice -n labb2 --since=10mKontrollera först att BFF ser AI-läget:
Invoke-RestMethod http://labb2.localhost/api/configKontrollera sedan att botservice startade med OpenRouter-generatorn:
kubectl logs deployment/botservice -n labb2 --since=5mDu vill se:
AI bot response generation is enabled. Using OpenRouter model ...
Om loggen i stället visar att AI är av, patcha ConfigMap och starta om:
$patch = @{
data = @{
BOT_AI_ENABLED = "true"
}
} | ConvertTo-Json -Compress
kubectl patch configmap bot-ai-config `
-n labb2 `
--type merge `
-p $patch
kubectl rollout restart deployment/botservice deployment/bff -n labb2Om AI är på men svaret ändå blir regelbaserat kan OpenRouter-anropet ha misslyckats. Då faller botten tillbaka till RuleBasedBotReplyGenerator. Kontrollera botservice-loggen efter varningar från OpenRouterBotResponseGenerator och verifiera att bot-ai-secret finns.
Om du ser fel om class file version eller Java 26/25, bygg med:
"-Djava.version=25"Exempel:
mvn clean -Pnative spring-boot:build-image -DskipTests "-Djava.version=25" "-Dspring-boot.build-image.imageName=labb2-bff:native"Det är normalt att native-build tar flera minuter per modul. Tjänster med JPA, Flyway, gRPC och security är tyngre. Docker Desktop bör ha gott om minne, gärna runt 8 GB eller mer.
Skala ner Kubernetes innan du bygger native-images om Docker Desktop känns segt:
kubectl scale deployment userservice authservice messageservice botservice bff -n labb2 --replicas=0
kubectl scale statefulset mysql rabbitmq -n labb2 --replicas=0docker images | findstr labb2kubectl get pods,svc,pvc,jobs -n labb2För vanlig lokal körning:
docker compose up --buildÖppna:
http://localhost:8080
För Kubernetes:
kubectl apply -f https://github.com/kubernetes-sigs/gateway-api/releases/download/v1.5.1/standard-install.yaml
helm upgrade --install traefik traefik/traefik --namespace traefik --create-namespace -f k8s\traefik-values.yaml --wait
kubectl apply -f k8s\labb2.yaml
kubectl apply -f k8s\labb2-gateway-traefik.yamlÖppna:
http://labb2.localhost