Skip to content

Commit dffc738

Browse files
committed
Improve formatting and headings in the Spring Boot streaming guide for better readability
1 parent e434273 commit dffc738

File tree

1 file changed

+90
-43
lines changed

1 file changed

+90
-43
lines changed

spring/docs/GUIDE.md

Lines changed: 90 additions & 43 deletions
Original file line numberDiff line numberDiff line change
@@ -1,34 +1,42 @@
11
# Streaming real-time data with Spring Boot
22

3-
### Goals
3+
## Goals
44

5-
This guide will show you how to use the Caplin platform and [Spring Boot](https://spring.io/projects/spring-boot) to rapidly build an application that can deliver on-demand, real time data to a browser or mobile application.
5+
This guide will show you how to use the Caplin platform and [Spring Boot](https://spring.io/projects/spring-boot) to
6+
rapidly build an application that can deliver on-demand, real time data to a browser or mobile application.
67

7-
### Pre-requisites
8+
## Pre-requisites
89

9-
This guide assumes that you are familiar with Spring Boot, else it would be beneficial to follow the [Building an Application with Spring Boot](https://spring.io/guides/gs/spring-boot/) guide before returning.
10+
This guide assumes that you are familiar with Spring Boot, else it would be beneficial to follow
11+
the [Building an Application with Spring Boot](https://spring.io/guides/gs/spring-boot/) guide before returning.
1012

11-
#### Software requirements
13+
### Software requirements
1214

1315
* Java JDK 17 or later
1416
* Docker, or a similar container runtime that supports compose files.
1517
* A Java or Kotlin IDE
1618

17-
### Project setup
19+
## Project setup
1820

1921
Now let's create a simple application.
2022

2123
* Navigate to [Spring Initializr](https://start.spring.io/)
2224

23-
* It's recommended to choose _Gradle - Kotlin_ for the Project, and _Kotlin_ for the Language, though you may of course use _Java_.
25+
* It's recommended to choose _Gradle - Kotlin_ for the Project, and _Kotlin_ for the Language, though you may of course
26+
use _Java_.
2427

2528
* Choose the latest Spring Boot release version, at the time of writing this is 3.5.3.
2629

2730
* Generate the project, unzip it, and then import it into your IDE.
2831

29-
* Now we need to add our DataSource Starter dependency, so open up `build.gradle.kts` and add `implementation("com.caplin.integration.datasourcex:spring-boot-starter-datasource:1.0.0")` to the `dependencies` block.
32+
* Now we need to add our DataSource Starter dependency, so open up `build.gradle.kts` and add
33+
`implementation("com.caplin.integration.datasourcex:spring-boot-starter-datasource:1.0.0")` to the `dependencies`
34+
block.
3035

31-
* You will also need to add the Caplin repository to be able to access the Caplin DataSource libraries. To do so, add the following to the `repositories` block. Note the credentials here should be retrieved from your Caplin Account Manager. These are best passed in from the command line, via environment variable or via your global `gradle.properties` file to ensure they are not inadvertently exposed:
36+
* You will also need to add the Caplin repository to be able to access the Caplin DataSource libraries. To do so, add
37+
the following to the `repositories` block. Note the credentials here should be retrieved from your Caplin Account
38+
Manager. These are best passed in from the command line, via environment variable or via your global
39+
`gradle.properties` file to ensure they are not inadvertently exposed:
3240
```kotlin
3341
maven {
3442
url = uri("https://repository.caplin.com/repository/caplin-release")
@@ -39,26 +47,38 @@ Now let's create a simple application.
3947
}
4048
```
4149

42-
* Lastly, we'll need to configure the Liberator host that DataSource will connect to, so open `src/main/resources/application.properties` and add the line `caplin.datasource.managed.peer.outgoing=ws://localhost:19000`
50+
* Lastly, we'll need to configure the Liberator host that DataSource will connect to, so open
51+
`src/main/resources/application.properties` and add the line
52+
`caplin.datasource.managed.peer.outgoing=ws://localhost:19000`
4353

44-
### Running the Caplin platform
54+
## Running the Caplin platform
4555

46-
To launch the Caplin platform you can use the [example Docker Compose file](https://github.com/caplin/DataSource-Extensions/tree/main/examples) from the repository examples. Please refer to the brief readme for instructions. This will launch a container running a preconfigured Liberator and expose two ports; `18080` for inbound front end application connections and `19000` for the inbound WebSocket connection from our new server application.
56+
To launch the Caplin platform you can use
57+
the [example Docker Compose file](https://github.com/caplin/DataSource-Extensions/tree/main/examples) from the
58+
repository examples. Please refer to the brief readme for instructions. This will launch a container running a
59+
preconfigured Liberator and expose two ports; `18080` for inbound front end application connections and `19000` for the
60+
inbound WebSocket connection from our new server application.
4761

48-
### Creating a simple browser client
62+
## Creating a simple browser client
4963

50-
We'll want to be able to request and display some data from our server, so let us create a basic browser client application to do so. Add the following to your project as `./index.html`. This code sets up a connection to the platform with the StreamLink library (In this case, hosted by our Liberator container at `http://localhost:18080/sljs/streamlink.js`) and enables the library's support for handling streaming JSON patches behind the scenes.
64+
We'll want to be able to request and display some data from our server, so let us create a basic browser client
65+
application to do so. Add the following to your project as `./index.html`. This code sets up a connection to the
66+
platform with the StreamLink library (In this case, hosted by our Liberator container at
67+
`http://localhost:18080/sljs/streamlink.js`) and enables the library's support for handling streaming JSON patches
68+
behind the scenes.
5169

5270
> For the sake of clarity, we are omitting most error handling code.
5371
5472
```html
73+
5574
<html lang="en">
5675
<head>
5776
<title>Streaming Demo</title>
5877
<script src="https://cdn.jsdelivr.net/npm/@tailwindcss/browser@4"></script>
5978
<script src="http://localhost:18080/sljs/streamlink.js"></script>
6079
<script type="module">
61-
import * as jsonpatch from 'https://cdnjs.cloudflare.com/ajax/libs/fast-json-patch/3.1.1/fast-json-patch.min.js';
80+
import * as jsonpatch
81+
from 'https://cdnjs.cloudflare.com/ajax/libs/fast-json-patch/3.1.1/fast-json-patch.min.js';
6282
6383
export let streamLink = caplin.streamlink.StreamLinkFactory.create({
6484
liberator_urls: "rttp://localhost:18080",
@@ -80,12 +100,12 @@ We'll want to be able to request and display some data from our server, so let u
80100
});
81101
82102
streamLink.addConnectionListener({
83-
onConnectionStatusChange: function(connectionStatusEvent) {
103+
onConnectionStatusChange: function (connectionStatusEvent) {
84104
document.getElementById("connection-status").innerHTML = `<pre>${connectionStatusEvent}</pre>`
85105
},
86106
});
87107
88-
window.onbeforeunload = function(event) {
108+
window.onbeforeunload = function (event) {
89109
streamLink.disconnect()
90110
}
91111
@@ -106,9 +126,10 @@ If you open this in your browser, you should see that we have successfully conne
106126
ConnectionStatusEventImpl [LiberatorURL=ws://localhost:18080, connectionState=LOGGEDIN]
107127
```
108128

109-
### Providing static data
129+
## Providing static data
110130

111-
Now let's add some data! In this case our client wants to retrieve the local time and time zone of the server. To handle this we'll create a new `@Controller` class providing an aptly named `/serverTime` endpoint.
131+
Now let's add some data! In this case our client wants to retrieve the local time and time zone of the server. To handle
132+
this we'll create a new `@Controller` class providing an aptly named `/serverTime` endpoint.
112133

113134
```kotlin
114135
@Controller
@@ -138,7 +159,9 @@ and we should see log line indicating our subject has been bound correctly
138159
Registering [/serverTime] as Static
139160
```
140161

141-
If we now edit our `index.html` to subscribe to this, adding the code provided below in place of the existing `//TODO subscriptions` placeholder, and refresh our browser, we should see the server's time and time zone data being displayed.
162+
If we now edit our `index.html` to subscribe to this, adding the code provided below in place of the existing
163+
`//TODO subscriptions` placeholder, and refresh our browser, we should see the server's time and time zone data being
164+
displayed.
142165

143166
```javascript
144167
document.body.innerHTML += `<div class="text-xl p-4 m-4 bg-gray-100 rounded-lg" id="serverTime"></div>`
@@ -151,44 +174,52 @@ streamLink.subscribe(timeSubject, {
151174
})
152175
```
153176

154-
Note that serialization to JSON is handled for us automatically by way of Spring's [Jackson](https://github.com/FasterXML/jackson) integration.
177+
Note that serialization to JSON is handled for us automatically by way of
178+
Spring's [Jackson](https://github.com/FasterXML/jackson) integration.
155179

156-
### Providing streaming data
180+
## Providing streaming data
157181

158-
Now as nice as that is, we'd like more than a single response, so let's modify our endpoint to provide a stream of events, rather than just the initial response. For Kotlin we'll be returning a [Flow](https://kotlinlang.org/docs/flow.html). For Java you can instead make use of Reactor's [Flux](https://projectreactor.io/docs/core/release/reference/#flux). Both are powerful abstractions over a stream of data.
182+
Now as nice as that is, we'd like more than a single response, so let's modify our endpoint to provide a stream of
183+
events, rather than just the initial response. For Kotlin we'll be returning
184+
a [Flow](https://kotlinlang.org/docs/flow.html). For Java you can instead make use of
185+
Reactor's [Flux](https://projectreactor.io/docs/core/release/reference/#flux). Both are powerful abstractions over a
186+
stream of data.
159187

160188
Modify the `StreamingController` class to replace our previous function with the following:
161189

162190
```kotlin
163191
@MessageMapping("/serverTime")
164192
fun serverTime(): Flow<TimeEvent> = flow {
165-
while (true) {
166-
emit(TimeEvent(LocalTime.now(), ZoneId.systemDefault()))
167-
delay(100)
193+
while (true) {
194+
emit(TimeEvent(LocalTime.now(), ZoneId.systemDefault()))
195+
delay(100)
196+
}
168197
}
169-
}
170198
```
171199

172200
One brief restart of the server application later, and you should see the client updating in real time!
173201

174-
### Request parameters
202+
## Request parameters
175203

176-
Now, imagine that the browser client needs to fetch the local time in a specific time zone. To achieve this we can fairly simply add a new endpoint to our controller, this time named `zonedTime` and taking a `@DestinationVariable` that is extracted from the requested subject.
204+
Now, imagine that the browser client needs to fetch the local time in a specific time zone. To achieve this we can
205+
fairly simply add a new endpoint to our controller, this time named `zonedTime` and taking a `@DestinationVariable` that
206+
is extracted from the requested subject.
177207

178208
```kotlin
179209
@MessageMapping("/zonedTime/{zoneId}")
180210
fun zonedTime(@DestinationVariable zoneId: ZoneId): Flow<TimeEvent> = flow {
181-
while (true) {
182-
val now = ZonedDateTime.now(zoneId)
183-
emit(TimeEvent(now.toLocalTime(), zoneId))
184-
delay(100)
211+
while (true) {
212+
val now = ZonedDateTime.now(zoneId)
213+
emit(TimeEvent(now.toLocalTime(), zoneId))
214+
delay(100)
215+
}
185216
}
186-
}
187217
```
188218

189219
To test this we can add to our client code to specify a time zone on a second request.
190220

191-
> As our parameter contains a `/` character, the zone ID must be URL encoded in order to match our subject defined in the `@MessageMapping`:
221+
> As our parameter contains a `/` character, the zone ID must be URL encoded in order to match our subject defined in
222+
> the `@MessageMapping`:
192223
193224
```javascript
194225
document.body.innerHTML += `<div class="text-xl p-4 m-4 bg-gray-100 rounded-lg" id="zonedTime"></div>`
@@ -202,9 +233,13 @@ streamLink.subscribe(zonedTimeSubject, {
202233

203234
After another quick restart of our server, and a refresh of our browser, we now have two time streams being displayed.
204235

205-
### Request payloads
236+
## Request payloads
206237

207-
But what if our stream request becomes a bit more complicated, perhaps containing optional or arrays of parameters? At this point it's more natural to represent our request as a payload object. Let's assume our client now wishes to make a single subscription to the time in various user specified zones, again we can support this with a few minor additions to our server. Create a new endpoint named `/times` in your controller, this time receiving single non-annotated method parameter which will be our payload from the client.
238+
But what if our stream request becomes a bit more complicated, perhaps containing optional or arrays of parameters? At
239+
this point it's more natural to represent our request as a payload object. Let's assume our client now wishes to make a
240+
single subscription to the time in various user specified zones, again we can support this with a few minor additions to
241+
our server. Create a new endpoint named `/times` in your controller, this time receiving single non-annotated method
242+
parameter which will be our payload from the client.
208243

209244
```kotlin
210245
data class TimesRequest(
@@ -222,7 +257,8 @@ fun times(timesRequest: TimesRequest): Flow<List<TimeEvent>> = flow {
222257
}
223258
```
224259

225-
Now for our client we need to do something a bit different for this case - we'll need to establish a channel rather than a plain subscription, and then send our request. This is quite simple:
260+
Now for our client we need to do something a bit different for this case - we'll need to establish a channel rather than
261+
a plain subscription, and then send our request. This is quite simple:
226262

227263
```javascript
228264
document.body.innerHTML += `<div class="text-xl p-4 m-4 bg-gray-100 rounded-lg" id="times"></div>`
@@ -240,11 +276,13 @@ timesChannel.send({
240276

241277
Running this we'll now see all the requested times being displayed and updating in sync.
242278

243-
### Two-way communication
279+
## Two-way communication
244280

245-
Lastly, say we now want our client to have the ability to add new zones to the stream in an ad-hoc manner. Fortunately, we can do this with a just few tweaks.
281+
Lastly, say we now want our client to have the ability to add new zones to the stream in an ad-hoc manner. Fortunately,
282+
we can do this with a just few tweaks.
246283

247-
On the client we'll add a simple text entry box and button, the clicking of which will send a message through the channel to let the server know to add a new zone.
284+
On the client we'll add a simple text entry box and button, the clicking of which will send a message through the
285+
channel to let the server know to add a new zone.
248286

249287
```javascript
250288
window.addZone = function () {
@@ -255,7 +293,8 @@ window.addZone = function () {
255293
document.body.innerHTML += `<div class="text-xl p-4 m-4 bg-gray-100 rounded-lg"><input type="text" id="zone" value="Chile/EasterIsland" class="border p-2 rounded mr-2"><button type="button" onclick="addZone()" class="bg-blue-500 text-white px-4 py-2 rounded">Add zone</button></div>`
256294
```
257295

258-
And on the server we can update our `/times` endpoint to accept a stream of data from the client by changing our parameter to be either a Flow or Flux accordingly, and then update our responses to include the newly requested zones:
296+
And on the server we can update our `/times` endpoint to accept a stream of data from the client by changing our
297+
parameter to be either a Flow or Flux accordingly, and then update our responses to include the newly requested zones:
259298

260299
```kotlin
261300
data class TimesRequest(
@@ -275,4 +314,12 @@ fun times(zoneRequests: Flow<TimesRequest>): Flow<List<TimeEvent>> = zoneRequest
275314
}
276315
```
277316

278-
One final restart of our application, and by clicking the button we can now see additional times being added to our stream each time we add a new zone.
317+
One final restart of our application, and by clicking the button we can now see additional times being added to our
318+
stream each time we add a new zone.
319+
320+
## What next?
321+
322+
Consider
323+
adding [per-user](https://github.com/caplin/DataSource-Extensions/blob/main/examples/spring-kotlin/src/main/kotlin/example/StreamsController.kt#L56)
324+
or [per-session](https://github.com/caplin/DataSource-Extensions/blob/main/examples/spring-kotlin/src/main/kotlin/example/StreamsController.kt#L87)
325+
subscriptions and channels, or interacting with a [stateful server](https://github.com/caplin/DataSource-Extensions/blob/main/examples/spring-kotlin-chat/src/main/kotlin/example/ChatController.kt#L77).

0 commit comments

Comments
 (0)