{"id":17248762,"url":"https://github.com/nomisrev/spring-server-events","last_synced_at":"2025-03-26T06:13:59.267Z","repository":{"id":219121271,"uuid":"748210784","full_name":"nomisRev/spring-server-events","owner":"nomisRev","description":"Spring MVC example of server sent events and Spring Security","archived":false,"fork":false,"pushed_at":"2024-02-01T10:00:05.000Z","size":22,"stargazers_count":0,"open_issues_count":0,"forks_count":0,"subscribers_count":2,"default_branch":"main","last_synced_at":"2025-01-31T07:41:38.235Z","etag":null,"topics":[],"latest_commit_sha":null,"homepage":null,"language":"Kotlin","has_issues":true,"has_wiki":null,"has_pages":null,"mirror_url":null,"source_name":null,"license":"apache-2.0","status":null,"scm":"git","pull_requests_enabled":true,"icon_url":"https://github.com/nomisRev.png","metadata":{"files":{"readme":"README.md","changelog":null,"contributing":null,"funding":null,"license":"LICENSE","code_of_conduct":null,"threat_model":null,"audit":null,"citation":null,"codeowners":null,"security":null,"support":null,"governance":null,"roadmap":null,"authors":null,"dei":null}},"created_at":"2024-01-25T14:05:38.000Z","updated_at":"2024-01-25T14:13:28.000Z","dependencies_parsed_at":"2024-01-25T16:07:46.399Z","dependency_job_id":null,"html_url":"https://github.com/nomisRev/spring-server-events","commit_stats":null,"previous_names":["nomisrev/spring-server-events"],"tags_count":0,"template":false,"template_full_name":null,"repository_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nomisRev%2Fspring-server-events","tags_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nomisRev%2Fspring-server-events/tags","releases_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nomisRev%2Fspring-server-events/releases","manifests_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories/nomisRev%2Fspring-server-events/manifests","owner_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners/nomisRev","download_url":"https://codeload.github.com/nomisRev/spring-server-events/tar.gz/refs/heads/main","host":{"name":"GitHub","url":"https://github.com","kind":"github","repositories_count":245598315,"owners_count":20641884,"icon_url":"https://github.com/github.png","version":null,"created_at":"2022-05-30T11:31:42.601Z","updated_at":"2022-07-04T15:15:14.044Z","host_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub","repositories_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repositories","repository_names_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/repository_names","owners_url":"https://repos.ecosyste.ms/api/v1/hosts/GitHub/owners"}},"keywords":[],"created_at":"2024-10-15T06:42:05.356Z","updated_at":"2025-03-26T06:13:59.240Z","avatar_url":"https://github.com/nomisRev.png","language":"Kotlin","funding_links":[],"categories":[],"sub_categories":[],"readme":"# spring-server-events\n\nSpring MVC example of server sent events and Spring Security\n\n## Requirements\n\n1. Spring dependencies (Web, Security \u0026 Jackson)\n2. JWT dependencies (jsonwebtoken api, impl \u0026 jackson)\n3. KotlinX Integrations (Coroutines \u0026 SLF4J)\n4. Optional: KotlinX Reactor \u0026 Reactive Streams\n\n## Locally running the server\n\n```console\n./gradlew bootRun\n```\n\nIf the Spring server is started, you can curl the endpoints.\nFirst we need to retrieve the token:\n\n```console\ncurl --location --no-buffer --request GET 'localhost:8080/token'\n```\n\nAnd then we need replace `{token}` with the result of the previous `curl` command.\n\n```console\ncurl --location --no-buffer --request GET 'localhost:8080/events' \\\n--header 'Authorization: Bearer eyJhbGciOiJIUzUxMiJ9.eyJzdWIiOiJhZG1pbiIsImlhdCI6MTcwNjE5OTQ4MywiZXhwIjoxNzA2MjE3NDgzfQ.xt6v-N38fkDvuIAsA-FF785BygNw2ifXn8ZGSlYjTdbw7Pu2gjpkIPBginb0O_6R6_jtAdVUsPaJfHYeUSDiag'\n```\n\nYou can also try `event2`, and `events3` to respectively test `SseEmitter` and `Flow`.\n\n## Security\n\nOnly the bare minimum is implemented here in terms of Security to keep the example simple.\nWe use a hardcoded user with `username = admin` and `password = admin`,\nwe gave him `ADMIN` role for example use cases.\n\nYou can find all the relevant code in `com.example.streamingdemo.auth`, and is set up as usual:\n\nConfiguring Spring is done using `@EnableWebSecurity`, `SecurityFilterChain`,\n`PasswordEncoder`, `AuthenticationProvider` \u0026 `AuthenticationManager`. Nothing special needs to be configured here.\n\n**Important:** for Spring to be able to complete request processing after the server sent all its events we need to\nset `shouldNotFilterAsyncDispatch` to `true` in `OncePerRequestFilter`.\n\nSee [JWTRequestFilter](https://github.com/nomisRev/spring-server-events/blob/dd671b3dd8a750707451d171d9fd0c10ded1aaaf/src/main/kotlin/com/example/streamingdemo/auth/JWTRequestFilter.kt#L27)\nfor practical details.\n\n### SecurityHolderContext \u0026 MDC\n\n`SecurityHolderContext` and `MDC` are `ThreadLocal` constructs,\nand thus they're not properly propagated between _dispatched_ coroutines.\n\nWe want both to be properly managed throughout KotlinX Coroutines,\nand therefore we use `ThreadContextElement`. This gives us the opportunity to `updateThreadContext`,\nand `restoreThreadContext` whenever we enter or exit a coroutine. Such that the state is properly maintained.\n\nLuckily KotlinX already implements one for `MDC` out-of-the-box, but not for `SecurityHolderContext`.\n[The SecurityCoroutineContext implementation can be found here](https://github.com/nomisRev/spring-server-events/blob/main/src/main/kotlin/com/example/streamingdemo/coroutines/SecurityCoroutineContext.kt).\n\n### SpringScope\n\nIn order to _launch_ a coroutine, we need a KotlinX `CoroutineScope`,\nthis is important such that the lifecycle of the coroutines is properly maintained to the Spring application lifecycle.\nThe easiest way to do this is to implement `DestroyableBean`, and make our implementing class a `@Component`.\n\nBy backing the `CoroutineScope` with a `SupervisorJob` a child doesn't fail and cancel the parent.\nThis means that **all** children have to handle their own errors, but luckily all uncaught errors are properly logged\nthanks to `CoroutineExceptionHandler`.\n\nWe run these coroutines on Spring's `AsyncTaskExecutor`, which we convert into a `CoroutineDispather`.\n\n[The SpringScope implementation can be found here](https://github.com/nomisRev/spring-server-events/blob/main/src/main/kotlin/com/example/streamingdemo/coroutines/SpringScope.kt).\n\n### Server Sent events\n\nWe have 3 options of sending server side events:\n\n1. ResponseBodyEmitter\n2. SseEmitter\n3. KotlinX Flow\n\n##### ResponseBodyEmitter\n\n`ResponseBodyEmitter` allows us to `send` messages and `complete` or `completeWithError` the emitter.\nThis can easily be done by combining `SpringScope`, `SecurityCoroutineContext`, and `MDCContext` explained above.\n\nAs you can see in the snippet below:\n\n1. we construct a `ResponseBodyEmitter`\n2. Launch a coroutine on a managed `SpringScope`, setting up the proper contexts\n3. We `try/catch` collecting our `Flow`, and if finished we `complete` the emitter.\n   If something went wrong we `completeWithError` the emitter.\n\n```kotlin\n@GetMapping(\"/events\")\nfun responseBodyEmitter(): ResponseBodyEmitter =\n  ResponseBodyEmitter().apply {\n    scope.launch(SecurityCoroutineContext() + MDCContext()) {\n      try {\n        mockStream.collect(::send)\n        complete()\n      } catch (e: Throwable) {\n        completeWithError(e)\n      }\n    }\n  }\n```\n\nThis is really neat, and powerful since `send` allows us to send different kind of messages with different `MediaType`.\n\n##### SseEmitter\n\n`SseEmitter` add some convenience methods on top of `ResponseBodyEmitter`,\nbut some might be undesired for example it prefixes all send data with `data:`.\n\nAs you can see, the resulting code is identical.\n\n```kotlin\n@GetMapping(\"/events2\")\nfun sseEmitter(): SseEmitter = SseEmitter().apply {\n    scope.launch(SecurityCoroutineContext() + MDCContext()) {\n      try {\n        mockStream.collect(::send)\n        complete()\n      } catch (e: Throwable) {\n        completeWithError(e)\n      }\n    }\n  }\n```\n\n##### Flow\n\nDirectly returning `Flow` to Spring MVC is possible, and Spring will\nuse [ReactiveAdapterRegistry](https://docs.spring.io/spring-framework/docs/6.1.3/javadoc-api/org/springframework/core/ReactiveAdapterRegistry.html).\n\nBe careful since this requires Reactive Streams, and Reactor to be on the classpath even though it is not used by us\ndirectly.\n\n```kotlin\n// Required for Flow -\u003e SseEmitter\nimplementation(\"org.reactivestreams:reactive-streams:1.0.4\")\nimplementation(\"org.jetbrains.kotlinx:kotlinx-coroutines-reactor:1.7.3\")\n```\n\nThis solution looks simplest from the `Controller` point-of-view,\nbut some care is required because it might become \"blocking\" depending on the type.\n\n`Flow\u003cString` streams correctly over the network, but `Flow\u003cInt\u003e` becomes blocking which is not the case\nfor `ResponseBodyEmitter` or `SseEmitter` although the might internally convert to `toString()`.\n","project_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnomisrev%2Fspring-server-events","html_url":"https://awesome.ecosyste.ms/projects/github.com%2Fnomisrev%2Fspring-server-events","lists_url":"https://awesome.ecosyste.ms/api/v1/projects/github.com%2Fnomisrev%2Fspring-server-events/lists"}