|
1 |
| -# Creating a CRUD application with Spring RSocket |
| 1 | +# Building a CRUD application with RSocket and Spring |
2 | 2 |
|
| 3 | +In [the last post](https://medium.com/@hantsy/using-rsocket-with-spring-boot-cfc67924d06a), we explored the basic RSocket support in Spring and Spring Boot. In this post, we will create a CRUD application which is more close to the real world applications. |
3 | 4 |
|
| 5 | +We will create a *client* and *server* applications to demonstrate the interactions between RSocket client and server side. |
4 | 6 |
|
| 7 | +Firstly let's create the server application. |
5 | 8 |
|
| 9 | +You can simply generate a project template from [Spring initialzr](https://start.spring.io), set the following properties. |
6 | 10 |
|
7 | 11 |
|
| 12 | +* Build: Maven |
| 13 | +* Java: 11 |
| 14 | +* Spring Boot version: 2.3.0.M3(I preferred the new version for practicing new techniques) |
| 15 | +* Dependencies: RSocket, Spring Data R2dbc, H2 Database, Lombok |
| 16 | + |
| 17 | +> If you are new to Spring Data R2dbc, check the post [ Accessing RDBMS with Spring Data R2dbc](https://medium.com/@hantsy/reactive-accessing-rdbms-with-spring-data-r2dbc-d6e453f2837e). |
| 18 | +
|
| 19 | +In the server application, we will use RSocket to serve a RSocket server via TCP protocol. |
| 20 | + |
| 21 | +Open the *src/main/resources/application.properties*. |
| 22 | + |
| 23 | +```properties |
| 24 | +spring.rsocket.server.port=7000 |
| 25 | +spring.rsocket.server.transport=tcp |
| 26 | +``` |
| 27 | + |
| 28 | +Like what I have done in the former posts, firstly create a simple POJO. |
| 29 | + |
| 30 | +```java |
| 31 | +@Data |
| 32 | +@ToString |
| 33 | +@Builder |
| 34 | +@NoArgsConstructor |
| 35 | +@AllArgsConstructor |
| 36 | +@Table("posts") |
| 37 | +class Post { |
| 38 | + |
| 39 | + @Id |
| 40 | + @Column("id") |
| 41 | + private Integer id; |
| 42 | + |
| 43 | + @Column("title") |
| 44 | + private String title; |
| 45 | + |
| 46 | + @Column("content") |
| 47 | + private String content; |
| 48 | + |
| 49 | +} |
| 50 | +``` |
| 51 | + |
| 52 | +And create a simple Repository for `Post`. |
| 53 | + |
| 54 | +```java |
| 55 | + |
| 56 | +interface PostRepository extends R2dbcRepository<Post, Integer> { |
| 57 | +} |
| 58 | +``` |
| 59 | + |
| 60 | +Create a `Controller` class to handle the request meassages. |
| 61 | + |
| 62 | +```java |
| 63 | +@Controller |
| 64 | +@RequiredArgsConstructor |
| 65 | +class PostController { |
| 66 | + |
| 67 | + private final PostRepository posts; |
| 68 | + |
| 69 | + @MessageMapping("posts.findAll") |
| 70 | + public Flux<Post> all() { |
| 71 | + return this.posts.findAll(); |
| 72 | + } |
| 73 | + |
| 74 | + @MessageMapping("posts.findById.{id}") |
| 75 | + public Mono<Post> get(@DestinationVariable("id") Integer id) { |
| 76 | + return this.posts.findById(id); |
| 77 | + } |
| 78 | + |
| 79 | + @MessageMapping("posts.save") |
| 80 | + public Mono<Post> create(@Payload Post post) { |
| 81 | + return this.posts.save(post); |
| 82 | + } |
| 83 | + |
| 84 | + @MessageMapping("posts.update.{id}") |
| 85 | + public Mono<Post> update(@DestinationVariable("id") Integer id, @Payload Post post) { |
| 86 | + return this.posts.findById(id) |
| 87 | + .map(p -> { |
| 88 | + p.setTitle(post.getTitle()); |
| 89 | + p.setContent(post.getContent()); |
| 90 | + |
| 91 | + return p; |
| 92 | + }) |
| 93 | + .flatMap(p -> this.posts.save(p)); |
| 94 | + } |
| 95 | + |
| 96 | + @MessageMapping("posts.deleteById.{id}") |
| 97 | + public Mono<Void> delete(@DestinationVariable("id") Integer id) { |
| 98 | + return this.posts.deleteById(id); |
| 99 | + } |
| 100 | + |
| 101 | +} |
| 102 | +``` |
| 103 | + |
| 104 | +Create a `schema.sql` and a `data.sql` to create tables and initialize the data. |
| 105 | + |
| 106 | +```sql |
| 107 | +-- schema.sql |
| 108 | +CREATE TABLE posts (id SERIAL PRIMARY KEY, title VARCHAR(255), content VARCHAR(255)); |
| 109 | +``` |
| 110 | + |
| 111 | +```sql |
| 112 | +-- data.sql |
| 113 | +DELETE FROM posts; |
| 114 | +INSERT INTO posts (title, content) VALUES ('post one in data.sql', 'content of post one in data.sql'); |
| 115 | +``` |
| 116 | +> Note: In the Spring 2.3.0.M3, Spring Data R2dbc is merged in the Spring Data release train. But unfornately, the `ConnectionFactoryInitializer` is NOT ported. |
| 117 | +
|
| 118 | +To make the the `schema.sql` and `data.sql` is loaded and executed at the application startup, declare a `ConnectionFactoryInitializer` bean. |
| 119 | + |
| 120 | +```java |
| 121 | +@Bean |
| 122 | +public ConnectionFactoryInitializer initializer(ConnectionFactory connectionFactory) { |
| 123 | + |
| 124 | + ConnectionFactoryInitializer initializer = new ConnectionFactoryInitializer(); |
| 125 | + initializer.setConnectionFactory(connectionFactory); |
| 126 | + |
| 127 | + CompositeDatabasePopulator populator = new CompositeDatabasePopulator(); |
| 128 | + populator.addPopulators(new ResourceDatabasePopulator(new ClassPathResource("schema.sql"))); |
| 129 | + populator.addPopulators(new ResourceDatabasePopulator(new ClassPathResource("data.sql"))); |
| 130 | + initializer.setDatabasePopulator(populator); |
| 131 | + |
| 132 | +return initializer; |
| 133 | +} |
| 134 | +``` |
| 135 | + |
| 136 | +Now, you can start the server application. |
| 137 | + |
| 138 | +```bash |
| 139 | +mvn spring-boot:run |
| 140 | +``` |
| 141 | + |
| 142 | +Next, let's move to the client application. |
| 143 | + |
| 144 | +Similarly, generate a project template from [Spring Initalizr](https://start.spring.io), in the *Dependencies* area, make sure you have choose *WebFlux*, *RSocket*, *Lombok*. |
| 145 | + |
| 146 | +The client application is a generic Webflux application, but use `RSocketRequester` to shake hands with the RSocket Servser. |
| 147 | + |
| 148 | +Declare a `RSocketRequester` bean. |
| 149 | + |
| 150 | +```java |
| 151 | +@Bean |
| 152 | +public RSocketRequester rSocketRequester(RSocketRequester.Builder b) { |
| 153 | + return b.dataMimeType(MimeTypeUtils.APPLICATION_JSON) |
| 154 | + .connectTcp("localhost", 7000) |
| 155 | + .block(); |
| 156 | +} |
| 157 | +``` |
| 158 | + |
| 159 | +Create a generic `RestController` and use the `RSocketRequester` to communicate with the RSocket server. |
| 160 | + |
| 161 | +```java |
| 162 | +@Slf4j |
| 163 | +@RequiredArgsConstructor |
| 164 | +@RestController() |
| 165 | +@RequestMapping("/posts") |
| 166 | +class PostClientController { |
| 167 | + |
| 168 | + private final RSocketRequester requester; |
| 169 | + |
| 170 | + @GetMapping("") |
| 171 | + Flux<Post> all() { |
| 172 | + return this.requester.route("posts.findAll") |
| 173 | + .retrieveFlux(Post.class); |
| 174 | + } |
| 175 | + |
| 176 | + @GetMapping("{id}") |
| 177 | + Mono<Post> findById(@PathVariable Integer id) { |
| 178 | + return this.requester.route("posts.findById." + id) |
| 179 | + .retrieveMono(Post.class); |
| 180 | + } |
| 181 | + |
| 182 | + @PostMapping("") |
| 183 | + Mono<Post> save(@RequestBody Post post) { |
| 184 | + return this.requester.route("posts.save") |
| 185 | + .data(post) |
| 186 | + .retrieveMono(Post.class); |
| 187 | + } |
| 188 | + |
| 189 | + @PutMapping("{id}") |
| 190 | + Mono<Post> update(@PathVariable Integer id, @RequestBody Post post) { |
| 191 | + return this.requester.route("posts.update."+ id) |
| 192 | + .data(post) |
| 193 | + .retrieveMono(Post.class); |
| 194 | + } |
| 195 | + |
| 196 | + @DeleteMapping("{id}") |
| 197 | + Mono<Void> delete(@PathVariable Integer id) { |
| 198 | + return this.requester.route("posts.deleteById."+ id).send(); |
| 199 | + } |
| 200 | + |
| 201 | +} |
| 202 | +``` |
| 203 | + |
| 204 | +Create a POJO `Post` to present the message payload transfered between client and server side. |
| 205 | + |
| 206 | +```java |
| 207 | +@Data |
| 208 | +@ToString |
| 209 | +@Builder |
| 210 | +@NoArgsConstructor |
| 211 | +@AllArgsConstructor |
| 212 | +class Post { |
| 213 | + private Integer id; |
| 214 | + private String title; |
| 215 | + private String content; |
| 216 | +} |
| 217 | +``` |
| 218 | +Start up the client application. |
| 219 | + |
| 220 | +Try to test the CRUD opertaions by curl. |
| 221 | + |
8 | 222 |
|
9 | 223 | ```bash
|
10 |
| -D:\hantsylabs\rsocket-sample\integration\server-boot>curl http://localhost:8080/posts |
| 224 | +# curl http://localhost:8080/posts |
11 | 225 | [{"id":1,"title":"post one in data.sql","content":"content of post one in data.sql"},{"id":2,"title":"Post one","content":"The content of post one"},{"id":3,"title":"Post tow","content":"The content of post tow"}]
|
12 |
| -D:\hantsylabs\rsocket-sample\integration\server-boot>curl http://localhost:8080/posts/1 |
13 |
| -{"id":1,"title":"post one in data.sql","content":"content of post one in data.sql"} |
14 |
| -D:\hantsylabs\rsocket-sample\integration\server-boot>curl http://localhost:8080/posts/3 |
15 |
| -{"id":3,"title":"Post tow","content":"The content of post tow"} |
16 |
| -D:\hantsylabs\rsocket-sample\integration\server-boot>curl http://localhost:8080/posts/2 |
17 |
| -{"id":2,"title":"Post one","content":"The content of post one"} |
18 |
| -D:\hantsylabs\rsocket-sample\integration\server-boot>curl http://localhost:8080/posts -d '{"title":"my save title", "content":"my content of my post"}' |
19 |
| -{"timestamp":"2020-03-13T11:09:52.653+0000","path":"/posts","status":500,"error":"Internal Server Error","message":"In a WebFlux application, form data is accessed via ServerWebExchange.getFormData().","requestId":"efbd0803-5"}curl: (3) [globbing] unmatched close brace/bracket in column 30 |
20 | 226 |
|
21 |
| -D:\hantsylabs\rsocket-sample\integration\server-boot>curl http://localhost:8080/posts -d '{"title":"my save title", "content":"my content of my post"}' -H "Content-Type:application/json" |
22 |
| -{"timestamp":"2020-03-13T11:10:18.814+0000","path":"/posts","status":400,"error":"Bad Request","message":"Failed to read HTTP message","requestId":"b02c8cb9-6"}curl: (3) [globbing] unmatched close brace/bracket in column 30 |
| 227 | +# curl http://localhost:8080/posts/1 |
| 228 | +{"id":1,"title":"post one in data.sql","content":"content of post one in data.sql"} |
23 | 229 |
|
24 |
| -D:\hantsylabs\rsocket-sample\integration\server-boot>curl http://localhost:8080/posts -d '{"title":"my save title", "content":"my content of my post"}' -H "Content-Type:application/json" -X POST |
25 |
| -{"timestamp":"2020-03-13T11:11:07.798+0000","path":"/posts","status":400,"error":"Bad Request","message":"Failed to read HTTP message","requestId":"96dca4ed-7"}curl: (3) [globbing] unmatched close brace/bracket in column 30 |
| 230 | +# curl http://localhost:8080/posts/3 |
| 231 | +{"id":3,"title":"Post tow","content":"The content of post tow"} |
26 | 232 |
|
27 |
| -D:\hantsylabs\rsocket-sample\integration\server-boot>curl http://localhost:8080/posts -d \"{\"title\":\"my save title\", \"content\":\"my content of my post\"}\" -H "Content-Type:application/json" -X POST |
28 |
| -{"timestamp":"2020-03-13T11:12:49.995+0000","path":"/posts","status":400,"error":"Bad Request","message":"Failed to read HTTP message","requestId":"52bb940b-8"}curl: (6) Could not resolve host: save |
29 |
| -curl: (6) Could not resolve host: title", |
30 |
| -curl: (3) Port number ended with '"' |
31 |
| -curl: (6) Could not resolve host: content |
32 |
| -curl: (6) Could not resolve host: of |
33 |
| -curl: (6) Could not resolve host: my |
34 |
| -curl: (3) [globbing] unmatched close brace/bracket in column 6 |
| 233 | +# curl http://localhost:8080/posts/2 |
| 234 | +{"id":2,"title":"Post one","content":"The content of post one"} |
35 | 235 |
|
36 |
| -D:\hantsylabs\rsocket-sample\integration\server-boot>curl http://localhost:8080/posts -d "{\"title\":\"my save title\", \"content\":\"my content of my post\"}" -H "Content-Type:application/json" -X POST |
| 236 | +# curl http://localhost:8080/posts -d "{\"title\":\"my save title\", \"content\":\"my content of my post\"}" -H "Content-Type:application/json" -X POST |
37 | 237 | {"id":4,"title":"my save title","content":"my content of my post"}
|
38 |
| -D:\hantsylabs\rsocket-sample\integration\server-boot>curl http://localhost:8080/posts/4 -d "{\"title\":\"my save title\", \"content\":\"update my content of my post\"}" -H "Content-Type:application/json" -X PUT |
39 |
| -{"id":4,"title":"my save title","content":"update my content of my post"} |
40 |
| -D:\hantsylabs\rsocket-sample\integration\server-boot>curl http://localhost:8080/posts |
| 238 | + |
| 239 | +# curl http://localhost:8080/posts |
41 | 240 | [{"id":1,"title":"post one in data.sql","content":"content of post one in data.sql"},{"id":2,"title":"Post one","content":"The content of post one"},{"id":3,"title":"Post tow","content":"The content of post tow"},{"id":4,"title":"my save title","content":"update my content of my post"}]
|
42 |
| -D:\hantsylabs\rsocket-sample\integration\server-boot>curl http://localhost:8080/posts/4 -X DELETE |
43 | 241 |
|
44 |
| -D:\hantsylabs\rsocket-sample\integration\server-boot>curl http://localhost:8080/posts |
| 242 | +# curl http://localhost:8080/posts/4 -X DELETE |
| 243 | + |
| 244 | +# curl http://localhost:8080/posts |
45 | 245 | [{"id":1,"title":"post one in data.sql","content":"content of post one in data.sql"},{"id":2,"title":"Post one","content":"The content of post one"},{"id":3,"title":"Post tow","content":"The content of post tow"}]
|
46 |
| -D:\hantsylabs\rsocket-sample\integration\server-boot>curl http://localhost:8080/posts |
| 246 | +``` |
| 247 | + |
| 248 | +As a bonus, try to add a filter to find the posts by keyword. |
| 249 | + |
| 250 | +In the server application, create a new method in the `PostRepository`. |
| 251 | + |
| 252 | +```java |
| 253 | +@Query("SELECT * FROM posts WHERE title like $1") |
| 254 | +Flux<Post> findByTitleContains(String name); |
| 255 | +``` |
47 | 256 |
|
| 257 | +And create a new route to handle this request from client. |
48 | 258 |
|
| 259 | +```java |
| 260 | +class PostController { |
| 261 | + //... |
| 262 | + @MessageMapping("posts.titleContains") |
| 263 | + public Flux<Post> titleContains(@Payload String title) { |
| 264 | + return this.posts.findByTitleContains(title); |
| 265 | + } |
| 266 | + //... |
| 267 | +} |
49 | 268 | ```
|
50 | 269 |
|
| 270 | +In the client side, change the `PostClientController` 's `all` method to the following: |
| 271 | + |
| 272 | +```java |
| 273 | +class PostClientController { |
| 274 | + //... |
| 275 | + Flux<Post> all(@RequestParam(name = "title", required = false) String title) { |
| 276 | + if (StringUtils.hasText(title)) { |
| 277 | + return this.requester.route("posts.titleContains") |
| 278 | + .data(title).retrieveFlux(Post.class); |
| 279 | + } else { |
| 280 | + return this.requester.route("posts.findAll") |
| 281 | + .retrieveFlux(Post.class); |
| 282 | + } |
| 283 | + } |
| 284 | + //... |
| 285 | +} |
| 286 | +``` |
| 287 | + |
| 288 | +Now, try to add a `title` request param to access http://localhost:8080/posts. |
| 289 | + |
| 290 | +Get the [source codes](https://github.com/hantsy/rsocket-sample/tree/master/crud) from my Github. |
0 commit comments