├── .gitignore ├── pom.xml ├── spring-boot-webflux.md ├── spring-boot-webflux_wordpress.md └── src └── main ├── java └── com │ └── lankydanblog │ └── tutorial │ ├── Application.java │ ├── client │ └── Client.java │ └── person │ ├── Person.java │ ├── PersonManager.java │ ├── PersonMapper.java │ ├── repository │ ├── PersonByCountryRepository.java │ ├── PersonRepository.java │ └── entity │ │ ├── PersonByCountryEntity.java │ │ ├── PersonByCountryKey.java │ │ └── PersonEntity.java │ └── web │ ├── PersonHandler.java │ └── PersonRouter.java └── resources └── application.properties /.gitignore: -------------------------------------------------------------------------------- 1 | # Compiled class file 2 | *.class 3 | 4 | # Log file 5 | *.log 6 | 7 | # BlueJ files 8 | *.ctxt 9 | 10 | # Mobile Tools for Java (J2ME) 11 | .mtj.tmp/ 12 | 13 | # Package Files # 14 | *.jar 15 | *.war 16 | *.ear 17 | *.zip 18 | *.tar.gz 19 | *.rar 20 | 21 | # virtual machine crash logs, see http://www.java.com/en/download/help/error_hotspot.xml 22 | hs_err_pid* 23 | 24 | **.idea 25 | **.settings 26 | **target 27 | *.project 28 | *.classpath 29 | **.iml 30 | -------------------------------------------------------------------------------- /pom.xml: -------------------------------------------------------------------------------- 1 | 2 | 5 | 4.0.0 6 | 7 | spring-boot-webflux 8 | spring-boot-webflux 9 | 1.0.0 10 | 11 | 12 | org.springframework.boot 13 | spring-boot-starter-parent 14 | 2.0.0.RELEASE 15 | 16 | 17 | 18 | 19 | org.springframework.boot 20 | spring-boot-starter-webflux 21 | 22 | 23 | 24 | org.springframework.boot 25 | spring-boot-starter-data-cassandra-reactive 26 | 2.0.0.RELEASE 27 | 28 | 29 | 40 | 41 | 42 | 43 | 1.8 44 | 45 | 46 | -------------------------------------------------------------------------------- /spring-boot-webflux.md: -------------------------------------------------------------------------------- 1 | So Spring Boot 2.0 when GA recently, so I decided to write my first post about Spring for quite a while. Since the release I have been seeing more and more mentions of Spring WebFlux along with tutorials on how to use it. But after reading through them and trying to get it working myself, I found it a bit hard to make the jump from the code included in the posts and tutorials I read to writing code that actually does something a tiny bit more interesting than returning a string from the back-end. Now, I'm hoping I'm not shooting myself in the foot by saying that as you could probably make the same criticism of the code I use in this post, but here is my attempt to give a tutorial of Spring WebFlux that actually resembles something that you might use in the wild. 2 | 3 | Before I continue, and after all this mentioning of WebFlux, what actually is it? Spring WebFlux is a fully non-blocking reactive alternative to Spring MVC. It allows better vertical scaling without increasing your hardware resources. Being reactive it now makes use of Reactive Streams to allow asynchronous processing of data returned from calls to the server. This means we are going to see a lot less `List`s, `Collection`s or even single objects and instead their reactive equivalents such as `Flux` and `Mono` (from Reactor). I'm not going to go to in depth on what Reactive Streams are, as honestly I need to look into it even more myself before I try to explain it to anyone. Instead lets get back to focusing on WebFlux. 4 | 5 | I used Spring Boot to write the code in this tutorial as usual. 6 | 7 | Below are the dependencies that I used in this post. 8 | ```xml 9 | 10 | 11 | 12 | org.springframework.boot 13 | spring-boot-starter-webflux 14 | 15 | 16 | 17 | org.springframework.boot 18 | spring-boot-starter-data-cassandra-reactive 19 | 2.0.0.RELEASE 20 | 21 | 22 | 23 | ``` 24 | Although I didn't include it in the dependency snippet above, the `spring-boot-starter-parent` is used, which can finally be upped to version `2.0.0.RELEASE`. Being this tutorial is about WebFlux, including the `spring-boot-starter-webflux` is obviously a good idea. `spring-boot-starter-data-cassandra-reactive` has also been included as we will be using this as the database for the example application as it is one of the few databases that have reactive support (at the time of writing). By using these dependencies together our application can be fully reactive from front to back. 25 | 26 | WebFlux introduces a different way to handle requests instead of using the `@Controller` or `@RestController` programming model that is used in Spring MVC. But, it does not replace it. Instead it has been updated to allow reactive types to be used. This allows you to keep the same format that you are used to writing with Spring but with a few changes to the return types so `Flux`s or `Mono`s are returned instead. Below is a very contrived example. 27 | ```java 28 | @RestController 29 | public class PersonController { 30 | 31 | private final PersonRepository personRepository; 32 | 33 | public PersonController(PersonRepository personRepository) { 34 | this.personRepository = personRepository; 35 | } 36 | 37 | @GetMapping("/people") 38 | public Flux all() { 39 | return personRepository.findAll(); 40 | } 41 | 42 | @GetMapping("/people/{id}") 43 | Mono findById(@PathVariable String id) { 44 | return personRepository.findOne(id); 45 | } 46 | } 47 | ``` 48 | To me this looks very familiar and from a quick glance it doesn't really look any different from your standard Spring MVC controller, but after reading through the methods we can see the different return types from what we would normally expect. In this example `PersonRepository` must be a reactive repository as we have been able to directly return the results of their search queries, for reference, reactive repositories will return a `Flux` for collections and a `Mono` for singular entities. 49 | 50 | The annotation method is not what I want to focus on in this post though. It's not cool and hip enough for us. There isn't enough use of lambdas to satisfy our thirst for writing Java in a more functional way. But Spring WebFlux has our backs. It provides an alternative method to route and handle requests to our servers that lightly uses lambdas to write router functions. Let's take a look at an example. 51 | ```java 52 | @Configuration 53 | public class PersonRouter { 54 | 55 | @Bean 56 | public RouterFunction route(PersonHandler personHandler) { 57 | return RouterFunctions.route(GET("/people/{id}").and(accept(APPLICATION_JSON)), personHandler::get) 58 | .andRoute(GET("/people").and(accept(APPLICATION_JSON)), personHandler::all) 59 | .andRoute(POST("/people").and(accept(APPLICATION_JSON)).and(contentType(APPLICATION_JSON)), personHandler::post) 60 | .andRoute(PUT("/people/{id}").and(accept(APPLICATION_JSON)).and(contentType(APPLICATION_JSON)), personHandler::put) 61 | .andRoute(DELETE("/people/{id}"), personHandler::delete) 62 | .andRoute(GET("/people/country/{country}").and(accept(APPLICATION_JSON)), personHandler::getByCountry); 63 | } 64 | } 65 | ``` 66 | These are all the routes to methods in the `PersonHandler` which we will look at later on. We have created a bean that will handle our routing. To setup the routing functions we use the well named `RouterFunctions` class providing us with a load of static methods, but for now we are only interested with it's `route` method. Below is the signature of the `route` method. 67 | ```java 68 | public static RouterFunction route( 69 | RequestPredicate predicate, HandlerFunction handlerFunction) { 70 | // stuff 71 | } 72 | ``` 73 | The method shows that it takes in a `RequestPredicate` along with a `HandlerFunction` and outputs a `RouterFunction`. 74 | 75 | The `RequestPredicate` is what we use to specify behavior of the route, such as the path to our handler function, what type of request it is and the type of input it can accept. Due to my use of static imports to make everything read a bit clearer, some important information has been hidden from you. To create a `RequestPredicate` we should use the `RequestPredicates` (plural), a static helper class providing us with all the methods we need. Personally I do recommend statically importing `RequestPredicates` otherwise you code will be a mess due to the amount of times you might need to make use of `RequestPredicates` static methods. In the above example, `GET`, `POST`, `PUT`, `DELETE`, `accept` and `contentType` are all static `RequestPredicates` methods. 76 | 77 | The next parameter is a `HandlerFunction`, which is a Functional Interface. There are three pieces of important information here, it has a generic type of ``, it's `handle` method returns a `Mono` and it takes in a `ServerRequest`. Using these we can determine that we need to pass in a function that returns a `Mono` (or one of it's subtypes). This obviously places a heavy constraint onto what is returned from our handler functions as they must meet this requirement or they will not be suitable for use in this format. 78 | 79 | Finally the output is a `RouterFunction`. This can then be returned and will be used to route to whatever function we specified. But normally we would want to route lots of different requests to various handlers at once, which WebFlux caters for. Due to `route` returning a `RouterFunction` and the fact that `RouterFunction` also has its own routing method available, `andRoute`, we can chain the calls together and keep adding all the extra routes that we require. 80 | 81 | If we take another look back at the `PersonRouter` example above, we can see that the methods are named after the REST verbs such as `GET` and `POST` that define the path and type of requests that a handler will take. If we take the first `GET` request for example, it is routing to `/people` with a path variable name `id` (path variable denoted by `{id}`) and the type of the returned content, specifically `APPLICATION_JSON` (static field from `MediaType`) is defined using the `accept` method. If a different path is used, it will not be handled. If the path is correct but the Accept header is not one of the accepted types, then the request will fail. 82 | 83 | Before we continue I want to go over the `accept` and `contentType` methods. Both of these set request headers, `accept` matches to the Accept header and `contentType` to Content-Type. The Accept header defines what Media Types are acceptable for the response, as we were returning JSON representations of the `Person` object setting it to `APPLICATION_JSON` (`application/json` in the actual header) makes sense. The Content-Type has the same idea but instead describes what Media Type is inside the body of the sent request. That is why only the `POST` and `PUT` verbs have `contentType` included as the others do not have anything contained in their bodies. `DELETE` does not include `accept` and `contentType` so we can conclude that it is neither expecting anything to be returned nor including anything in its request body. 84 | 85 | Now that we know how to setup the routes, lets look at writing the handler methods that deal with the incoming requests. Below is the code that handles all the requests from the routes that were defined in the earlier example. 86 | ```java 87 | @Component 88 | public class PersonHandler { 89 | 90 | private final PersonManager personManager; 91 | 92 | public PersonHandler(PersonManager personManager) { 93 | this.personManager = personManager; 94 | } 95 | 96 | public Mono get(ServerRequest request) { 97 | final UUID id = UUID.fromString(request.pathVariable("id")); 98 | final Mono person = personManager.findById(id); 99 | return person 100 | .flatMap(p -> ok().contentType(APPLICATION_JSON).body(fromPublisher(person, Person.class))) 101 | .switchIfEmpty(notFound().build()); 102 | } 103 | 104 | public Mono all(ServerRequest request) { 105 | return ok().contentType(APPLICATION_JSON) 106 | .body(fromPublisher(personManager.findAll(), Person.class)); 107 | } 108 | 109 | public Mono put(ServerRequest request) { 110 | final UUID id = UUID.fromString(request.pathVariable("id")); 111 | final Mono person = request.bodyToMono(Person.class); 112 | return personManager 113 | .findById(id) 114 | .flatMap( 115 | old -> 116 | ok().contentType(APPLICATION_JSON) 117 | .body( 118 | fromPublisher( 119 | person 120 | .map(p -> new Person(p, id)) 121 | .flatMap(p -> personManager.update(old, p)), 122 | Person.class))) 123 | .switchIfEmpty(notFound().build()); 124 | } 125 | 126 | public Mono post(ServerRequest request) { 127 | final Mono person = request.bodyToMono(Person.class); 128 | final UUID id = UUID.randomUUID(); 129 | return created(UriComponentsBuilder.fromPath("people/" + id).build().toUri()) 130 | .contentType(APPLICATION_JSON) 131 | .body( 132 | fromPublisher( 133 | person.map(p -> new Person(p, id)).flatMap(personManager::save), Person.class)); 134 | } 135 | 136 | public Mono delete(ServerRequest request) { 137 | final UUID id = UUID.fromString(request.pathVariable("id")); 138 | return personManager 139 | .findById(id) 140 | .flatMap(p -> noContent().build(personManager.delete(p))) 141 | .switchIfEmpty(notFound().build()); 142 | } 143 | 144 | public Mono getByCountry(ServerRequest serverRequest) { 145 | final String country = serverRequest.pathVariable("country"); 146 | return ok().contentType(APPLICATION_JSON) 147 | .body(fromPublisher(personManager.findAllByCountry(country), Person.class)); 148 | } 149 | } 150 | ``` 151 | One thing that is quite noticeable, is the lack of annotations. Bar the `@Component` annotation to auto create a `PersonHandler` bean there are no other Spring annotations. 152 | 153 | I have tried to keep most of the repository logic out of this class and have hidden any references to the entity objects by going via the `PersonManager` that delegates to the `PersonRepository` it contains. If you are interested in the code within `PersonManager` then it can be seen here on my [GitHub](https://github.com/lankydan/spring-boot-webflux/blob/master/src/main/java/com/lankydanblog/tutorial/person/PersonManager.java), further explanations about it will be excluded for this post so we can focus on WebFlux itself. 154 | 155 | Ok, back to the code at hand. Let's take a closer look at the `get` and `post` methods to figure out what is going on. 156 | ```java 157 | public Mono get(ServerRequest request) { 158 | final UUID id = UUID.fromString(request.pathVariable("id")); 159 | final Mono person = personManager.findById(id); 160 | return person 161 | .flatMap(p -> ok().contentType(APPLICATION_JSON).body(fromPublisher(person, Person.class))) 162 | .switchIfEmpty(notFound().build()); 163 | } 164 | ``` 165 | This method is for retrieving a single record from the database that backs this example application. Due to Cassandra being the database of choice I have decided to use an `UUID` for the primary key of each record, this has the unfortunate effect of making testing the example more annoying but nothing that some copy and pasting can't solve. 166 | 167 | Remember that a path variable was included in the path for this `GET` request. Using the `pathVariable` method on the `ServerRequest` passed into the method we are able to extract it's value by providing the name of the variable, in this case `id`. The ID is then converted into a `UUID`, which will throw an exception if the string is not in the correct format, I decided to ignore this problem so the example code doesn't get messier. 168 | 169 | Once we have the ID, we can query the database for the existence of a matching record. A `Mono` is returned which either contains the existing record mapped to a `Person` or it left as an empty `Mono`. 170 | 171 | Using the returned `Mono` we can output different responses depending on it's existence. This means we can return useful status codes to the client to go along with the contents of the body. If the record exists then `flatMap` returns a `ServerResponse` with the `OK` status. Along with this status we want to output the record, to do this we specify the content type of the body, in this case `APPLICATION_JSON`, and add the record into it. `fromPublisher` takes our `Mono` (which is a `Publisher`) along with the `Person` class so it knows what it is mapping into the body. `fromPublisher` is a static method from the `BodyInserters` class. 172 | 173 | If the record does not exist, then the flow will move into the `switchIfEmpty` block and return a `NOT FOUND` status. As nothing is found, the body can be left empty so we just create the `ServerResponse` there are then. 174 | 175 | Now onto the `post` handler. 176 | ```java 177 | public Mono post(ServerRequest request) { 178 | final Mono person = request.bodyToMono(Person.class); 179 | final UUID id = UUID.randomUUID(); 180 | return created(UriComponentsBuilder.fromPath("people/" + id).build().toUri()) 181 | .contentType(APPLICATION_JSON) 182 | .body( 183 | fromPublisher( 184 | person.map(p -> new Person(p, id)).flatMap(personManager::save), Person.class)); 185 | } 186 | ``` 187 | Even just from the first line we can see that it is already different to how the `get` method was working. As this is a `POST` request it needs to accept the object that we want to persist from the body of the request. As we are trying to insert a single record we will use the request's `bodyToMono` method to retrieve the `Person` from the body. If you were dealing with multiple records you would probably want to use `bodyToFlux` instead. 188 | 189 | We will return a `CREATED` status using the `created` method that takes in a `URI` to determine the path to the inserted record. It then follows a similar setup as the `get` method by using the `fromPublisher` method to add the new record to the body of the response. The code that forms the `Publisher` is slightly different but the output is still a `Mono` which is what matters. Just for further explanation about how the inserting is done, the `Person` passed in from the request is mapped to a new `Person` using the `UUID` we generated and is then passed to `save` by calling `flatMap`. By creating a new `Person` we only insert values into Cassandra that we allow, in this case we do not want the `UUID` passed in from the request body. 190 | 191 | So, that's about it when it comes to the handlers. Obviously there other methods that we didn't go through. They all work differently but all follow the same concept of returning a `ServerResponse` that contains a suitable status code and record(s) in the body if required. 192 | 193 | We have now written all the code we need to get a basic Spring WebFlux back-end up a running. All that is left is to tie all the configuration together, which is easy with Spring Boot. 194 | ```java 195 | @SpringBootApplication 196 | public class Application { 197 | public static void main(String args[]) { 198 | SpringApplication.run(Application.class); 199 | } 200 | } 201 | ``` 202 | Rather than ending the post here we should probably look into how to actually make use of the code. 203 | 204 | Spring provides the `WebClient` class to handle requests without blocking. We can make use of this now as a way to test the application, although there is also a `WebTestClient` which we could use here instead. The `WebClient` is what you would use instead of the blocking `RestTemplate` when creating a reactive application. 205 | 206 | Below is some code that calls the handlers that were defined in the `PersonHandler`. 207 | ```java 208 | public class Client { 209 | 210 | private WebClient client = WebClient.create("http://localhost:8080"); 211 | 212 | public void doStuff() { 213 | 214 | // POST 215 | final Person record = new Person(UUID.randomUUID(), "John", "Doe", "UK", 50); 216 | final Mono postResponse = 217 | client 218 | .post() 219 | .uri("/people") 220 | .body(Mono.just(record), Person.class) 221 | .accept(APPLICATION_JSON) 222 | .exchange(); 223 | postResponse 224 | .map(ClientResponse::statusCode) 225 | .subscribe(status -> System.out.println("POST: " + status.getReasonPhrase())); 226 | 227 | // GET 228 | client 229 | .get() 230 | .uri("/people/{id}", "a4f66fe5-7c1b-4bcf-89b4-93d8fcbc52a4") 231 | .accept(APPLICATION_JSON) 232 | .exchange() 233 | .flatMap(response -> response.bodyToMono(Person.class)) 234 | .subscribe(person -> System.out.println("GET: " + person)); 235 | 236 | // ALL 237 | client 238 | .get() 239 | .uri("/people") 240 | .accept(APPLICATION_JSON) 241 | .exchange() 242 | .flatMapMany(response -> response.bodyToFlux(Person.class)) 243 | .subscribe(person -> System.out.println("ALL: " + person)); 244 | 245 | // PUT 246 | final Person updated = new Person(UUID.randomUUID(), "Peter", "Parker", "US", 18); 247 | client 248 | .put() 249 | .uri("/people/{id}", "ec2212fc-669e-42ff-9c51-69782679c9fc") 250 | .body(Mono.just(updated), Person.class) 251 | .accept(APPLICATION_JSON) 252 | .exchange() 253 | .map(ClientResponse::statusCode) 254 | .subscribe(response -> System.out.println("PUT: " + response.getReasonPhrase())); 255 | 256 | // DELETE 257 | client 258 | .delete() 259 | .uri("/people/{id}", "ec2212fc-669e-42ff-9c51-69782679c9fc") 260 | .exchange() 261 | .map(ClientResponse::statusCode) 262 | .subscribe(status -> System.out.println("DELETE: " + status)); 263 | } 264 | } 265 | 266 | ``` 267 | Don't forget to instantiate the `Client` somewhere, below is a nice lazy way to do it! 268 | ```java 269 | @SpringBootApplication 270 | public class Application { 271 | 272 | public static void main(String args[]) { 273 | SpringApplication.run(Application.class); 274 | Client client = new Client(); 275 | client.doStuff(); 276 | } 277 | } 278 | ``` 279 | First we create the `WebClient`. 280 | ```java 281 | private final WebClient client = WebClient.create("http://localhost:8080"); 282 | ``` 283 | Once created we can start doing stuff with it, hence the `doStuff` method. 284 | 285 | Let's break down the `POST` request that is being send to the back-end. 286 | ```java 287 | final Mono postResponse = 288 | client 289 | .post() 290 | .uri("/people") 291 | .body(Mono.just(record), Person.class) 292 | .accept(APPLICATION_JSON) 293 | .exchange(); 294 | postResponse 295 | .map(ClientResponse::statusCode) 296 | .subscribe(status -> System.out.println("POST: " + status.getReasonPhrase())); 297 | ``` 298 | I wrote this one down slightly differently so you can see that a `Mono` is returned from sending a request. The `exchange` method fires the HTTP request over to the server. The response will then be dealt with whenever the response arrives, if it ever does. 299 | 300 | Using the `WebClient` we specify that we want to send a `POST` request using the `post` method of course. The `URI` is then added with the `uri` method (overloaded method, this one takes in a `String` but another accepts a `URI`). Im tired of saying this method does what the method is called so, the contents of the body are then added along with the Accept header. Finally we send the request by calling `exchange`. 301 | 302 | Note that the Media Type of `APPLICATION_JSON` matches up with the type defined in the `POST` router function. If we were to send a different type, say `TEXT_PLAIN` we would get a `404` error as no handler exists that matches to what the request is expecting to be returned. 303 | 304 | Using the `Mono` returned by calling `exchange` we can map it's contents to our desired output. In the case of the example above, the status code is printed to the console. If we think back to the `post` method in `PersonHandler`, remember that it can only return the "Created" status, but if the sent request does not match up correctly then "Not Found" will be printed out. 305 | 306 | Let's look at one of the other requests. 307 | ```java 308 | client 309 | .get() 310 | .uri("/people/{id}", "a4f66fe5-7c1b-4bcf-89b4-93d8fcbc52a4") 311 | .accept(APPLICATION_JSON) 312 | .exchange() 313 | .flatMap(response -> response.bodyToMono(Person.class)) 314 | .subscribe(person -> System.out.println("GET: " + person)); 315 | ``` 316 | This is our typical `GET` request. It looks pretty similar to the `POST` request we just went through. The main differences are that `uri` takes in both the path of the request and the `UUID` (as a `String` in this case) as a parameter to that will replace the path variable `{id}` and that the body is left empty. How the response is handled is also different. In this example it extracts the body of the response and maps it to a `Mono` and prints it out. This could have been done with the previous `POST` example but the status code of the response was more useful for it's scenario. 317 | 318 | For a slightly different perspective, we could use cURL to make requests and see what the response looks like. 319 | ``` 320 | CURL -H "Accept:application/json" -i localhost:8080/people 321 | ```json 322 | HTTP/1.1 200 OK 323 | transfer-encoding: chunked 324 | Content-Type: application/json 325 | 326 | [ 327 | { 328 | "id": "13c403a2-6770-4174-8b76-7ba7b75ef73d", 329 | "firstName": "John", 330 | "lastName": "Doe", 331 | "country": "UK", 332 | "age": 50 333 | }, 334 | { 335 | "id": "fbd53e55-7313-4759-ad74-6fc1c5df0986", 336 | "firstName": "Peter", 337 | "lastName": "Parker", 338 | "country": "US", 339 | "age": 50 340 | } 341 | ] 342 | ``` 343 | The response will look something like this, obviously it will differ depending on the data you have stored. 344 | 345 | Note the response headers. 346 | ``` 347 | transfer-encoding: chunked 348 | Content-Type: application/json 349 | ``` 350 | The `transfer-encoding` here represents data that is transferred in chunks that can be used to stream data. This is what we need so the client can act reactively to the data that is returned to it. 351 | 352 | I think that this should be a good place to stop. We have covered quite a lot of material here which has hopefully helped you understand Spring WebFlux better. There are a few other topics I want to cover about WebFlux but I will do those in separate posts as I think this one is long enough as it is. 353 | 354 | In conclusion, in this post we very briefly discussed why you would want to use Spring WebFlux over a typical Spring MVC back-end. We then looked at how to setup routes and handlers to process the incoming requests. The handlers implemented methods that could deal with most of the REST verbs and returned the correct data and status codes in their responses. Finally we looked at two ways to make requests to the back-end, one using a `WebClient` to process the output directly on the client side and another via cURL to see what the returned JSON looks like. 355 | 356 | If you are interested in looking at the rest of the code I used to create the example application for this post, it can be found on my [GitHub](https://github.com/lankydan/spring-boot-webflux). 357 | 358 | As always if you found this post helpful, please share it and if you want to keep up with my latest posts then you can follow me on Twitter at [@LankyDanDev](https://twitter.com/LankyDanDev). -------------------------------------------------------------------------------- /spring-boot-webflux_wordpress.md: -------------------------------------------------------------------------------- 1 | So Spring Boot 2.0 when GA recently, so I decided to write my first post about Spring for quite a while. Since the release I have been seeing more and more mentions of Spring WebFlux along with tutorials on how to use it. But after reading through them and trying to get it working myself, I found it a bit hard to make the jump from the code included in the posts and tutorials I read to writing code that actually does something a tiny bit more interesting than returning a string from the back-end. Now, I'm hoping I'm not shooting myself in the foot by saying that as you could probably make the same criticism of the code I use in this post, but here is my attempt to give a tutorial of Spring WebFlux that actually resembles something that you might use in the wild. 2 | 3 | Before I continue, and after all this mentioning of WebFlux, what actually is it? Spring WebFlux is a fully non-blocking reactive alternative to Spring MVC. It allows better vertical scaling without increasing your hardware resources. Being reactive it now makes use of Reactive Streams to allow asynchronous processing of data returned from calls to the server. This means we are going to see a lot less Lists, Collections or even single objects and instead their reactive equivalents such as Flux and Mono (from Reactor). I'm not going to go to in depth on what Reactive Streams are, as honestly I need to look into it even more myself before I try to explain it to anyone. Instead lets get back to focusing on WebFlux. 4 | 5 | I used Spring Boot to write the code in this tutorial as usual. 6 | 7 | Below are the dependencies that I used in this post. 8 | 9 | [gist https://gist.github.com/lankydan/756297e0ce725b4652ccb012503c96af /] 10 | 11 | Although I didn't include it in the dependency snippet above, the spring-boot-starter-parent is used, which can finally be upped to version 2.0.0.RELEASE. Being this tutorial is about WebFlux, including the spring-boot-starter-webflux is obviously a good idea. spring-boot-starter-data-cassandra-reactive has also been included as we will be using this as the database for the example application as it is one of the few databases that have reactive support (at the time of writing). By using these dependencies together our application can be fully reactive from front to back. 12 | 13 | WebFlux introduces a different way to handle requests instead of using the @Controller or @RestController programming model that is used in Spring MVC. But, it does not replace it. Instead it has been updated to allow reactive types to be used. This allows you to keep the same format that you are used to writing with Spring but with a few changes to the return types so Fluxs or Monos are returned instead. Below is a very contrived example. 14 | 15 | [gist https://gist.github.com/lankydan/622339d4438afe4ba1015635aed149c9 /] 16 | 17 | To me this looks very familiar and from a quick glance it doesn't really look any different from your standard Spring MVC controller, but after reading through the methods we can see the different return types from what we would normally expect. In this example PersonRepository must be a reactive repository as we have been able to directly return the results of their search queries, for reference, reactive repositories will return a Flux for collections and a Mono for singular entities. 18 | 19 | The annotation method is not what I want to focus on in this post though. It's not cool and hip enough for us. There isn't enough use of lambdas to satisfy our thirst for writing Java in a more functional way. But Spring WebFlux has our backs. It provides an alternative method to route and handle requests to our servers that lightly uses lambdas to write router functions. Let's take a look at an example. 20 | 21 | [gist https://gist.github.com/lankydan/fb84804c2d7dda73c611bbbcdc6f7d08 /] 22 | 23 | These are all the routes to methods in the PersonHandler which we will look at later on. We have created a bean that will handle our routing. To setup the routing functions we use the well named RouterFunctions class providing us with a load of static methods, but for now we are only interested with it's route method. Below is the signature of the route method. 24 | 25 | [gist https://gist.github.com/lankydan/faa6c8dfdbc8900cb372cb366cba03d0 /] 26 | 27 | The method shows that it takes in a RequestPredicate along with a HandlerFunction and outputs a RouterFunction. 28 | 29 | The RequestPredicate is what we use to specify behavior of the route, such as the path to our handler function, what type of request it is and the type of input it can accept. Due to my use of static imports to make everything read a bit clearer, some important information has been hidden from you. To create a RequestPredicate we should use the RequestPredicates (plural), a static helper class providing us with all the methods we need. Personally I do recommend statically importing RequestPredicates otherwise you code will be a mess due to the amount of times you might need to make use of RequestPredicates static methods. In the above example, GET, POST, PUT, DELETE, accept and contentType are all static RequestPredicates methods. 30 | 31 | The next parameter is a HandlerFunction, which is a Functional Interface. There are three pieces of important information here, it has a generic type of <T extends ServerResponse>, it's handle method returns a Mono<T> and it takes in a ServerRequest. Using these we can determine that we need to pass in a function that returns a Mono<ServerResponse> (or one of it's subtypes). This obviously places a heavy constraint onto what is returned from our handler functions as they must meet this requirement or they will not be suitable for use in this format. 32 | 33 | Finally the output is a RouterFunction. This can then be returned and will be used to route to whatever function we specified. But normally we would want to route lots of different requests to various handlers at once, which WebFlux caters for. Due to route returning a RouterFunction and the fact that RouterFunction also has its own routing method available, andRoute, we can chain the calls together and keep adding all the extra routes that we require. 34 | 35 | If we take another look back at the PersonRouter example above, we can see that the methods are named after the REST verbs such as GET and POST that define the path and type of requests that a handler will take. If we take the first GET request for example, it is routing to /people with a path variable name id (path variable denoted by {id}) and the type of the returned content, specifically APPLICATION_JSON (static field from MediaType) is defined using the accept method. If a different path is used, it will not be handled. If the path is correct but the Accept header is not one of the accepted types, then the request will fail. 36 | 37 | Before we continue I want to go over the accept and contentType methods. Both of these set request headers, accept matches to the Accept header and contentType to Content-Type. The Accept header defines what Media Types are acceptable for the response, as we were returning JSON representations of the Person object setting it to APPLICATION_JSON (application/json in the actual header) makes sense. The Content-Type has the same idea but instead describes what Media Type is inside the body of the sent request. That is why only the POST and PUT verbs have contentType included as the others do not have anything contained in their bodies. DELETE does not include accept and contentType so we can conclude that it is neither expecting anything to be returned nor including anything in its request body. 38 | 39 | Now that we know how to setup the routes, lets look at writing the handler methods that deal with the incoming requests. Below is the code that handles all the requests from the routes that were defined in the earlier example. 40 | 41 | [gist https://gist.github.com/lankydan/2558d7a040f647d89d619f554be85a73 /] 42 | 43 | One thing that is quite noticeable, is the lack of annotations. Bar the @Component annotation to auto create a PersonHandler bean there are no other Spring annotations. 44 | 45 | I have tried to keep most of the repository logic out of this class and have hidden any references to the entity objects by going via the PersonManager that delegates to the PersonRepository it contains. If you are interested in the code within PersonManager then it can be seen here on my GitHub, further explanations about it will be excluded for this post so we can focus on WebFlux itself. 46 | 47 | Ok, back to the code at hand. Let's take a closer look at the get and post methods to figure out what is going on. 48 | 49 | [gist https://gist.github.com/lankydan/f5811875a4d03a6c95c108b0fb44b9b0 /] 50 | 51 | This method is for retrieving a single record from the database that backs this example application. Due to Cassandra being the database of choice I have decided to use an UUID for the primary key of each record, this has the unfortunate effect of making testing the example more annoying but nothing that some copy and pasting can't solve. 52 | 53 | Remember that a path variable was included in the path for this GET request. Using the pathVariable method on the ServerRequest passed into the method we are able to extract it's value by providing the name of the variable, in this case id. The ID is then converted into a UUID, which will throw an exception if the string is not in the correct format, I decided to ignore this problem so the example code doesn't get messier. 54 | 55 | Once we have the ID, we can query the database for the existence of a matching record. A Mono<Person> is returned which either contains the existing record mapped to a Person or it left as an empty Mono. 56 | 57 | Using the returned Mono we can output different responses depending on it's existence. This means we can return useful status codes to the client to go along with the contents of the body. If the record exists then flatMap returns a ServerResponse with the OK status. Along with this status we want to output the record, to do this we specify the content type of the body, in this case APPLICATION_JSON, and add the record into it. fromPublisher takes our Mono<Person> (which is a Publisher) along with the Person class so it knows what it is mapping into the body. fromPublisher is a static method from the BodyInserters class. 58 | 59 | If the record does not exist, then the flow will move into the switchIfEmpty block and return a NOT FOUND status. As nothing is found, the body can be left empty so we just create the ServerResponse there are then. 60 | 61 | Now onto the post handler. 62 | 63 | [gist https://gist.github.com/lankydan/5406fd6316a4050e72287d1ffd3e21a9 /] 64 | 65 | Even just from the first line we can see that it is already different to how the get method was working. As this is a POST request it needs to accept the object that we want to persist from the body of the request. As we are trying to insert a single record we will use the request's bodyToMono method to retrieve the Person from the body. If you were dealing with multiple records you would probably want to use bodyToFlux instead. 66 | 67 | We will return a CREATED status using the created method that takes in a URI to determine the path to the inserted record. It then follows a similar setup as the get method by using the fromPublisher method to add the new record to the body of the response. The code that forms the Publisher is slightly different but the output is still a Mono<Person> which is what matters. Just for further explanation about how the inserting is done, the Person passed in from the request is mapped to a new Person using the UUID we generated and is then passed to save by calling flatMap. By creating a new Person we only insert values into Cassandra that we allow, in this case we do not want the UUID passed in from the request body. 68 | 69 | So, that's about it when it comes to the handlers. Obviously there other methods that we didn't go through. They all work differently but all follow the same concept of returning a ServerResponse that contains a suitable status code and record(s) in the body if required. 70 | 71 | We have now written all the code we need to get a basic Spring WebFlux back-end up a running. All that is left is to tie all the configuration together, which is easy with Spring Boot. 72 | 73 | [gist https://gist.github.com/lankydan/8e69212fd861e628cf016d7067350057 /] 74 | 75 | Rather than ending the post here we should probably look into how to actually make use of the code. 76 | 77 | Spring provides the WebClient class to handle requests without blocking. We can make use of this now as a way to test the application, although there is also a WebTestClient which we could use here instead. The WebClient is what you would use instead of the blocking RestTemplate when creating a reactive application. 78 | 79 | Below is some code that calls the handlers that were defined in the PersonHandler. 80 | 81 | [gist https://gist.github.com/lankydan/56808561fd389885c4ba9077b80e0215 /] 82 | 83 | Don't forget to instantiate the Client somewhere, below is a nice lazy way to do it! 84 | 85 | [gist https://gist.github.com/lankydan/d28bf38b7c54e8687cee7329f56ef284 /] 86 | 87 | First we create the WebClient. 88 | 89 | [gist https://gist.github.com/lankydan/7b44b1ef4e1768f6095f6dfc2d11a27b /] 90 | 91 | Once created we can start doing stuff with it, hence the doStuff method. 92 | 93 | Let's break down the POST request that is being send to the back-end. 94 | 95 | [gist https://gist.github.com/lankydan/73c157dbf260ffeeee28e787f530958d /] 96 | 97 | I wrote this one down slightly differently so you can see that a Mono<ClientResponse> is returned from sending a request. The exchange method fires the HTTP request over to the server. The response will then be dealt with whenever the response arrives, if it ever does. 98 | 99 | Using the WebClient we specify that we want to send a POST request using the post method of course. The URI is then added with the uri method (overloaded method, this one takes in a String but another accepts a URI). Im tired of saying this method does what the method is called so, the contents of the body are then added along with the Accept header. Finally we send the request by calling exchange. 100 | 101 | Note that the Media Type of APPLICATION_JSON matches up with the type defined in the POST router function. If we were to send a different type, say TEXT_PLAIN we would get a 404 error as no handler exists that matches to what the request is expecting to be returned. 102 | 103 | Using the Mono<ClientResponse> returned by calling exchange we can map it's contents to our desired output. In the case of the example above, the status code is printed to the console. If we think back to the post method in PersonHandler, remember that it can only return the "Created" status, but if the sent request does not match up correctly then "Not Found" will be printed out. 104 | 105 | Let's look at one of the other requests. 106 | 107 | [gist https://gist.github.com/lankydan/f5e6f5bfeccad877498e6cd98639c33d /] 108 | 109 | This is our typical GET request. It looks pretty similar to the POST request we just went through. The main differences are that uri takes in both the path of the request and the UUID (as a String in this case) as a parameter to that will replace the path variable {id} and that the body is left empty. How the response is handled is also different. In this example it extracts the body of the response and maps it to a Mono<Person> and prints it out. This could have been done with the previous POST example but the status code of the response was more useful for it's scenario. 110 | 111 | For a slightly different perspective, we could use cURL to make requests and see what the response looks like. 112 |
113 | CURL -H "Accept:application/json" -i localhost:8080/people
114 | 
115 | 116 |
117 | HTTP/1.1 200 OK
118 | transfer-encoding: chunked
119 | Content-Type: application/json
120 | 
121 | [
122 |   {
123 |       "id": "13c403a2-6770-4174-8b76-7ba7b75ef73d",
124 |       "firstName": "John",
125 |       "lastName": "Doe",
126 |       "country": "UK",
127 |       "age": 50
128 |   },
129 |   {
130 |       "id": "fbd53e55-7313-4759-ad74-6fc1c5df0986",
131 |       "firstName": "Peter",
132 |       "lastName": "Parker",
133 |       "country": "US",
134 |       "age": 50
135 |   }
136 | ]
137 | 
138 | The response will look something like this, obviously it will differ depending on the data you have stored. 139 | 140 | Note the response headers. 141 |
142 | transfer-encoding: chunked
143 | Content-Type: application/json
144 | 
145 | The transfer-encoding here represents data that is transferred in chunks that can be used to stream data. This is what we need so the client can act reactively to the data that is returned to it. 146 | 147 | I think that this should be a good place to stop. We have covered quite a lot of material here which has hopefully helped you understand Spring WebFlux better. There are a few other topics I want to cover about WebFlux but I will do those in separate posts as I think this one is long enough as it is. 148 | 149 | In conclusion, in this post we very briefly discussed why you would want to use Spring WebFlux over a typical Spring MVC back-end. We then looked at how to setup routes and handlers to process the incoming requests. The handlers implemented methods that could deal with most of the REST verbs and returned the correct data and status codes in their responses. Finally we looked at two ways to make requests to the back-end, one using a WebClient to process the output directly on the client side and another via cURL to see what the returned JSON looks like. 150 | 151 | If you are interested in looking at the rest of the code I used to create the example application for this post, it can be found on my GitHub. 152 | 153 | As always if you found this post helpful, please share it and if you want to keep up with my latest posts then you can follow me on Twitter at @LankyDanDev. -------------------------------------------------------------------------------- /src/main/java/com/lankydanblog/tutorial/Application.java: -------------------------------------------------------------------------------- 1 | package com.lankydanblog.tutorial; 2 | 3 | import com.lankydanblog.tutorial.client.Client; 4 | import org.springframework.boot.SpringApplication; 5 | import org.springframework.boot.autoconfigure.SpringBootApplication; 6 | 7 | @SpringBootApplication 8 | public class Application { 9 | 10 | public static void main(String args[]) { 11 | SpringApplication.run(Application.class); 12 | Client client = new Client(); 13 | client.doStuff(); 14 | } 15 | } 16 | -------------------------------------------------------------------------------- /src/main/java/com/lankydanblog/tutorial/client/Client.java: -------------------------------------------------------------------------------- 1 | package com.lankydanblog.tutorial.client; 2 | 3 | import com.lankydanblog.tutorial.person.Person; 4 | import org.springframework.web.reactive.function.client.ClientResponse; 5 | import org.springframework.web.reactive.function.client.WebClient; 6 | import reactor.core.publisher.Mono; 7 | 8 | import java.util.UUID; 9 | 10 | import static org.springframework.http.MediaType.APPLICATION_JSON; 11 | 12 | public class Client { 13 | 14 | private WebClient client = WebClient.create("http://localhost:8080"); 15 | 16 | public void doStuff() { 17 | 18 | // POST 19 | final Person record = new Person(UUID.randomUUID(), "John", "Doe", "UK", 50); 20 | final Mono postResponse = 21 | client 22 | .post() 23 | .uri("/people") 24 | .body(Mono.just(record), Person.class) 25 | .accept(APPLICATION_JSON) 26 | .exchange(); 27 | postResponse 28 | .map(ClientResponse::statusCode) 29 | .subscribe(status -> System.out.println("POST: " + status.getReasonPhrase())); 30 | 31 | // GET 32 | client 33 | .get() 34 | .uri("/people/{id}", "a4f66fe5-7c1b-4bcf-89b4-93d8fcbc52a4") 35 | .accept(APPLICATION_JSON) 36 | .exchange() 37 | .flatMap(response -> response.bodyToMono(Person.class)) 38 | .subscribe(person -> System.out.println("GET: " + person)); 39 | 40 | // ALL 41 | client 42 | .get() 43 | .uri("/people") 44 | .accept(APPLICATION_JSON) 45 | .exchange() 46 | .flatMapMany(response -> response.bodyToFlux(Person.class)) 47 | .subscribe(person -> System.out.println("ALL: " + person)); 48 | 49 | // PUT 50 | final Person updated = new Person(UUID.randomUUID(), "Peter", "Parker", "US", 18); 51 | client 52 | .put() 53 | .uri("/people/{id}", "ec2212fc-669e-42ff-9c51-69782679c9fc") 54 | .body(Mono.just(updated), Person.class) 55 | .accept(APPLICATION_JSON) 56 | .exchange() 57 | .map(ClientResponse::statusCode) 58 | .subscribe(response -> System.out.println("PUT: " + response.getReasonPhrase())); 59 | 60 | // DELETE 61 | client 62 | .delete() 63 | .uri("/people/{id}", "ec2212fc-669e-42ff-9c51-69782679c9fc") 64 | .exchange() 65 | .map(ClientResponse::statusCode) 66 | .subscribe(status -> System.out.println("DELETE: " + status)); 67 | } 68 | } 69 | -------------------------------------------------------------------------------- /src/main/java/com/lankydanblog/tutorial/person/Person.java: -------------------------------------------------------------------------------- 1 | package com.lankydanblog.tutorial.person; 2 | 3 | import java.util.Objects; 4 | import java.util.UUID; 5 | 6 | public class Person { 7 | 8 | private UUID id; 9 | private String firstName; 10 | private String lastName; 11 | private String country; 12 | private int age; 13 | 14 | public Person() {} 15 | 16 | public Person(UUID id, String firstName, String lastName, String country, int age) { 17 | this.id = id; 18 | this.firstName = firstName; 19 | this.lastName = lastName; 20 | this.country = country; 21 | this.age = age; 22 | } 23 | 24 | public Person(Person person, UUID id) { 25 | this.id = id; 26 | this.firstName = person.firstName; 27 | this.lastName = person.lastName; 28 | this.country = person.country; 29 | this.age = person.age; 30 | } 31 | 32 | public UUID getId() { 33 | return id; 34 | } 35 | 36 | public void setId(UUID id) { 37 | this.id = id; 38 | } 39 | 40 | public String getFirstName() { 41 | return firstName; 42 | } 43 | 44 | public void setFirstName(String firstName) { 45 | this.firstName = firstName; 46 | } 47 | 48 | public String getLastName() { 49 | return lastName; 50 | } 51 | 52 | public void setLastName(String lastName) { 53 | this.lastName = lastName; 54 | } 55 | 56 | public String getCountry() { 57 | return country; 58 | } 59 | 60 | public void setCountry(String country) { 61 | this.country = country; 62 | } 63 | 64 | public int getAge() { 65 | return age; 66 | } 67 | 68 | public void setAge(int age) { 69 | this.age = age; 70 | } 71 | 72 | @Override 73 | public boolean equals(Object o) { 74 | if (this == o) return true; 75 | if (o == null || getClass() != o.getClass()) return false; 76 | Person person = (Person) o; 77 | return age == person.age 78 | && Objects.equals(id, person.id) 79 | && Objects.equals(firstName, person.firstName) 80 | && Objects.equals(lastName, person.lastName) 81 | && Objects.equals(country, person.country); 82 | } 83 | 84 | @Override 85 | public int hashCode() { 86 | return Objects.hash(id, firstName, lastName, country, age); 87 | } 88 | 89 | @Override 90 | public String toString() { 91 | return "Person{" 92 | + "id=" 93 | + id 94 | + ", firstName='" 95 | + firstName 96 | + '\'' 97 | + ", lastName='" 98 | + lastName 99 | + '\'' 100 | + ", country='" 101 | + country 102 | + '\'' 103 | + ", age=" 104 | + age 105 | + '}'; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/main/java/com/lankydanblog/tutorial/person/PersonManager.java: -------------------------------------------------------------------------------- 1 | package com.lankydanblog.tutorial.person; 2 | 3 | import com.lankydanblog.tutorial.person.repository.PersonByCountryRepository; 4 | import com.lankydanblog.tutorial.person.repository.PersonRepository; 5 | import org.springframework.stereotype.Component; 6 | import reactor.core.publisher.Flux; 7 | import reactor.core.publisher.Mono; 8 | 9 | import java.util.UUID; 10 | 11 | import static com.lankydanblog.tutorial.person.PersonMapper.toPersonByCountryEntity; 12 | import static com.lankydanblog.tutorial.person.PersonMapper.toPersonEntity; 13 | 14 | @Component 15 | public class PersonManager { 16 | 17 | private final PersonRepository personRepository; 18 | private final PersonByCountryRepository personByCountryRepository; 19 | 20 | public PersonManager( 21 | PersonRepository personRepository, PersonByCountryRepository personByCountryRepository) { 22 | this.personRepository = personRepository; 23 | this.personByCountryRepository = personByCountryRepository; 24 | } 25 | 26 | public Flux findAll() { 27 | return personByCountryRepository.findAll().map(PersonMapper::toPerson); 28 | } 29 | 30 | public Flux findAllByCountry(String country) { 31 | return personByCountryRepository.findAllByKeyCountry(country).map(PersonMapper::toPerson); 32 | } 33 | 34 | public Mono findById(final UUID id) { 35 | return personRepository.findById(id).map(PersonMapper::toPerson).switchIfEmpty(Mono.empty()); 36 | } 37 | 38 | public Mono save(Person person) { 39 | return Mono.fromSupplier( 40 | () -> { 41 | personRepository 42 | .save(toPersonEntity(person)) 43 | .and(personByCountryRepository.save(toPersonByCountryEntity(person))) 44 | .subscribe(); 45 | return person; 46 | }); 47 | } 48 | 49 | public Mono update(Person old, Person updated) { 50 | return Mono.fromSupplier( 51 | () -> { 52 | personRepository 53 | .save(toPersonEntity(updated)) 54 | .and(personByCountryRepository.delete(toPersonByCountryEntity(old))) 55 | .and(personByCountryRepository.save(toPersonByCountryEntity(updated))) 56 | .subscribe(); 57 | return updated; 58 | }); 59 | } 60 | 61 | public Mono delete(Person person) { 62 | return Mono.fromSupplier( 63 | () -> { 64 | personRepository 65 | .delete(toPersonEntity(person)) 66 | .and(personByCountryRepository.delete(toPersonByCountryEntity(person))) 67 | .subscribe(); 68 | return null; 69 | }); 70 | } 71 | } 72 | -------------------------------------------------------------------------------- /src/main/java/com/lankydanblog/tutorial/person/PersonMapper.java: -------------------------------------------------------------------------------- 1 | package com.lankydanblog.tutorial.person; 2 | 3 | import com.lankydanblog.tutorial.person.repository.entity.PersonByCountryEntity; 4 | import com.lankydanblog.tutorial.person.repository.entity.PersonByCountryKey; 5 | import com.lankydanblog.tutorial.person.repository.entity.PersonEntity; 6 | 7 | class PersonMapper { 8 | 9 | private PersonMapper() {} 10 | 11 | static Person toPerson(PersonByCountryEntity personByCountryEntity) { 12 | return new Person( 13 | personByCountryEntity.getKey().getId(), 14 | personByCountryEntity.getKey().getFirstName(), 15 | personByCountryEntity.getKey().getLastName(), 16 | personByCountryEntity.getKey().getCountry(), 17 | personByCountryEntity.getAge()); 18 | } 19 | 20 | static Person toPerson(PersonEntity personEntity) { 21 | return new Person( 22 | personEntity.getId(), 23 | personEntity.getFirstName(), 24 | personEntity.getLastName(), 25 | personEntity.getCountry(), 26 | personEntity.getAge()); 27 | } 28 | 29 | static PersonEntity toPersonEntity(Person person) { 30 | return new PersonEntity( 31 | person.getId(), 32 | person.getFirstName(), 33 | person.getLastName(), 34 | person.getCountry(), 35 | person.getAge()); 36 | } 37 | 38 | static PersonByCountryEntity toPersonByCountryEntity(Person person) { 39 | return new PersonByCountryEntity( 40 | new PersonByCountryKey( 41 | person.getCountry(), person.getFirstName(), person.getLastName(), person.getId()), 42 | person.getAge()); 43 | } 44 | } 45 | -------------------------------------------------------------------------------- /src/main/java/com/lankydanblog/tutorial/person/repository/PersonByCountryRepository.java: -------------------------------------------------------------------------------- 1 | package com.lankydanblog.tutorial.person.repository; 2 | 3 | import com.lankydanblog.tutorial.person.repository.entity.PersonByCountryEntity; 4 | import com.lankydanblog.tutorial.person.repository.entity.PersonByCountryKey; 5 | import com.lankydanblog.tutorial.person.repository.entity.PersonEntity; 6 | import org.springframework.data.cassandra.repository.ReactiveCassandraRepository; 7 | import org.springframework.stereotype.Repository; 8 | import reactor.core.publisher.Flux; 9 | 10 | @Repository 11 | public interface PersonByCountryRepository extends ReactiveCassandraRepository { 12 | 13 | Flux findAllByKeyCountry(final String country); 14 | } 15 | -------------------------------------------------------------------------------- /src/main/java/com/lankydanblog/tutorial/person/repository/PersonRepository.java: -------------------------------------------------------------------------------- 1 | package com.lankydanblog.tutorial.person.repository; 2 | 3 | import java.util.UUID; 4 | 5 | import com.lankydanblog.tutorial.person.repository.entity.PersonEntity; 6 | import org.springframework.data.cassandra.repository.ReactiveCassandraRepository; 7 | import org.springframework.stereotype.Repository; 8 | 9 | @Repository 10 | public interface PersonRepository extends ReactiveCassandraRepository { 11 | } 12 | -------------------------------------------------------------------------------- /src/main/java/com/lankydanblog/tutorial/person/repository/entity/PersonByCountryEntity.java: -------------------------------------------------------------------------------- 1 | package com.lankydanblog.tutorial.person.repository.entity; 2 | 3 | import java.util.Objects; 4 | 5 | import org.springframework.data.cassandra.core.mapping.PrimaryKey; 6 | import org.springframework.data.cassandra.core.mapping.Table; 7 | 8 | @Table("people_by_country") 9 | public class PersonByCountryEntity { 10 | 11 | @PrimaryKey private PersonByCountryKey key; 12 | private int age; 13 | 14 | public PersonByCountryEntity(PersonByCountryKey key, int age) { 15 | this.key = key; 16 | this.age = age; 17 | } 18 | 19 | public PersonByCountryKey getKey() { 20 | return key; 21 | } 22 | 23 | public void setKey(PersonByCountryKey key) { 24 | this.key = key; 25 | } 26 | 27 | public int getAge() { 28 | return age; 29 | } 30 | 31 | public void setAge(int age) { 32 | this.age = age; 33 | } 34 | 35 | @Override 36 | public boolean equals(Object o) { 37 | if (this == o) return true; 38 | if (o == null || getClass() != o.getClass()) return false; 39 | PersonByCountryEntity that = (PersonByCountryEntity) o; 40 | return age == that.age && Objects.equals(key, that.key); 41 | } 42 | 43 | @Override 44 | public int hashCode() { 45 | 46 | return Objects.hash(key, age); 47 | } 48 | 49 | @Override 50 | public String toString() { 51 | return "PersonByCountryEntity{" + "key=" + key + ", age=" + age + '}'; 52 | } 53 | } 54 | -------------------------------------------------------------------------------- /src/main/java/com/lankydanblog/tutorial/person/repository/entity/PersonByCountryKey.java: -------------------------------------------------------------------------------- 1 | package com.lankydanblog.tutorial.person.repository.entity; 2 | 3 | import java.io.Serializable; 4 | import java.util.Objects; 5 | import java.util.UUID; 6 | 7 | import org.springframework.data.cassandra.core.mapping.PrimaryKeyClass; 8 | import org.springframework.data.cassandra.core.mapping.PrimaryKeyColumn; 9 | 10 | import static org.springframework.data.cassandra.core.cql.PrimaryKeyType.*; 11 | 12 | @PrimaryKeyClass 13 | public class PersonByCountryKey implements Serializable { 14 | 15 | @PrimaryKeyColumn(type = PARTITIONED) 16 | private String country; 17 | 18 | @PrimaryKeyColumn(name = "first_name", type = CLUSTERED, ordinal = 0) 19 | private String firstName; 20 | 21 | @PrimaryKeyColumn(name = "last_name", type = CLUSTERED, ordinal = 1) 22 | private String lastName; 23 | 24 | @PrimaryKeyColumn(name = "person_id", type = CLUSTERED, ordinal = 2) 25 | private UUID id; 26 | 27 | public PersonByCountryKey(String country, String firstName, String lastName, UUID id) { 28 | this.country = country; 29 | this.firstName = firstName; 30 | this.lastName = lastName; 31 | this.id = id; 32 | } 33 | 34 | public String getCountry() { 35 | return country; 36 | } 37 | 38 | public void setCountry(String country) { 39 | this.country = country; 40 | } 41 | 42 | public String getFirstName() { 43 | return firstName; 44 | } 45 | 46 | public void setFirstName(String firstName) { 47 | this.firstName = firstName; 48 | } 49 | 50 | public String getLastName() { 51 | return lastName; 52 | } 53 | 54 | public void setLastName(String lastName) { 55 | this.lastName = lastName; 56 | } 57 | 58 | public UUID getId() { 59 | return id; 60 | } 61 | 62 | public void setId(UUID id) { 63 | this.id = id; 64 | } 65 | 66 | @Override 67 | public boolean equals(Object o) { 68 | if (this == o) return true; 69 | if (o == null || getClass() != o.getClass()) return false; 70 | PersonByCountryKey that = (PersonByCountryKey) o; 71 | return Objects.equals(country, that.country) 72 | && Objects.equals(firstName, that.firstName) 73 | && Objects.equals(lastName, that.lastName) 74 | && Objects.equals(id, that.id); 75 | } 76 | 77 | @Override 78 | public int hashCode() { 79 | 80 | return Objects.hash(country, firstName, lastName, id); 81 | } 82 | 83 | @Override 84 | public String toString() { 85 | return "PersonByCountryKey{" 86 | + "country='" 87 | + country 88 | + '\'' 89 | + ", firstName='" 90 | + firstName 91 | + '\'' 92 | + ", lastName='" 93 | + lastName 94 | + '\'' 95 | + ", id=" 96 | + id 97 | + '}'; 98 | } 99 | } 100 | -------------------------------------------------------------------------------- /src/main/java/com/lankydanblog/tutorial/person/repository/entity/PersonEntity.java: -------------------------------------------------------------------------------- 1 | package com.lankydanblog.tutorial.person.repository.entity; 2 | 3 | import java.util.Objects; 4 | import java.util.UUID; 5 | 6 | import org.springframework.data.cassandra.core.mapping.PrimaryKey; 7 | import org.springframework.data.cassandra.core.mapping.PrimaryKeyColumn; 8 | import org.springframework.data.cassandra.core.mapping.Table; 9 | 10 | import static org.springframework.data.cassandra.core.cql.PrimaryKeyType.PARTITIONED; 11 | 12 | @Table("people") 13 | public class PersonEntity { 14 | 15 | @PrimaryKey("person_id") 16 | private UUID id; 17 | 18 | private String firstName; 19 | private String lastName; 20 | private String country; 21 | private int age; 22 | 23 | public PersonEntity(UUID id, String firstName, String lastName, String country, int age) { 24 | this.id = id; 25 | this.firstName = firstName; 26 | this.lastName = lastName; 27 | this.country = country; 28 | this.age = age; 29 | } 30 | 31 | public UUID getId() { 32 | return id; 33 | } 34 | 35 | public void setId(UUID id) { 36 | this.id = id; 37 | } 38 | 39 | public String getFirstName() { 40 | return firstName; 41 | } 42 | 43 | public void setFirstName(String firstName) { 44 | this.firstName = firstName; 45 | } 46 | 47 | public String getLastName() { 48 | return lastName; 49 | } 50 | 51 | public void setLastName(String lastName) { 52 | this.lastName = lastName; 53 | } 54 | 55 | public String getCountry() { 56 | return country; 57 | } 58 | 59 | public void setCountry(String country) { 60 | this.country = country; 61 | } 62 | 63 | public int getAge() { 64 | return age; 65 | } 66 | 67 | public void setAge(int age) { 68 | this.age = age; 69 | } 70 | 71 | @Override 72 | public boolean equals(Object o) { 73 | if (this == o) return true; 74 | if (o == null || getClass() != o.getClass()) return false; 75 | PersonEntity that = (PersonEntity) o; 76 | return age == that.age 77 | && Objects.equals(id, that.id) 78 | && Objects.equals(firstName, that.firstName) 79 | && Objects.equals(lastName, that.lastName) 80 | && Objects.equals(country, that.country); 81 | } 82 | 83 | @Override 84 | public int hashCode() { 85 | 86 | return Objects.hash(id, firstName, lastName, country, age); 87 | } 88 | 89 | @Override 90 | public String toString() { 91 | return "PersonEntity{" 92 | + "id=" 93 | + id 94 | + ", firstName='" 95 | + firstName 96 | + '\'' 97 | + ", lastName='" 98 | + lastName 99 | + '\'' 100 | + ", country='" 101 | + country 102 | + '\'' 103 | + ", age=" 104 | + age 105 | + '}'; 106 | } 107 | } 108 | -------------------------------------------------------------------------------- /src/main/java/com/lankydanblog/tutorial/person/web/PersonHandler.java: -------------------------------------------------------------------------------- 1 | package com.lankydanblog.tutorial.person.web; 2 | 3 | import com.lankydanblog.tutorial.person.Person; 4 | import com.lankydanblog.tutorial.person.PersonManager; 5 | import org.springframework.stereotype.Component; 6 | import org.springframework.web.reactive.function.server.ServerRequest; 7 | import org.springframework.web.reactive.function.server.ServerResponse; 8 | import org.springframework.web.util.UriComponentsBuilder; 9 | import reactor.core.publisher.Mono; 10 | 11 | import java.util.UUID; 12 | 13 | import static org.springframework.http.MediaType.APPLICATION_JSON; 14 | import static org.springframework.web.reactive.function.BodyInserters.fromPublisher; 15 | import static org.springframework.web.reactive.function.server.ServerResponse.*; 16 | 17 | @Component 18 | public class PersonHandler { 19 | 20 | private final PersonManager personManager; 21 | 22 | public PersonHandler(PersonManager personManager) { 23 | this.personManager = personManager; 24 | } 25 | 26 | public Mono get(ServerRequest request) { 27 | final UUID id = UUID.fromString(request.pathVariable("id")); 28 | final Mono person = personManager.findById(id); 29 | return person 30 | .flatMap(p -> ok().contentType(APPLICATION_JSON).body(fromPublisher(person, Person.class))) 31 | .switchIfEmpty(notFound().build()); 32 | } 33 | 34 | public Mono all(ServerRequest request) { 35 | return ok().contentType(APPLICATION_JSON) 36 | .body(fromPublisher(personManager.findAll(), Person.class)); 37 | } 38 | 39 | public Mono put(ServerRequest request) { 40 | final UUID id = UUID.fromString(request.pathVariable("id")); 41 | final Mono person = request.bodyToMono(Person.class); 42 | return personManager 43 | .findById(id) 44 | .flatMap( 45 | old -> 46 | ok().contentType(APPLICATION_JSON) 47 | .body( 48 | fromPublisher( 49 | person 50 | .map(p -> new Person(p, id)) 51 | .flatMap(p -> personManager.update(old, p)), 52 | Person.class))) 53 | .switchIfEmpty(notFound().build()); 54 | } 55 | 56 | public Mono post(ServerRequest request) { 57 | final Mono person = request.bodyToMono(Person.class); 58 | final UUID id = UUID.randomUUID(); 59 | return created(UriComponentsBuilder.fromPath("people/" + id).build().toUri()) 60 | .contentType(APPLICATION_JSON) 61 | .body( 62 | fromPublisher( 63 | person.map(p -> new Person(p, id)).flatMap(personManager::save), Person.class)); 64 | } 65 | 66 | public Mono delete(ServerRequest request) { 67 | final UUID id = UUID.fromString(request.pathVariable("id")); 68 | return personManager 69 | .findById(id) 70 | .flatMap(p -> noContent().build(personManager.delete(p))) 71 | .switchIfEmpty(notFound().build()); 72 | } 73 | 74 | public Mono getByCountry(ServerRequest serverRequest) { 75 | final String country = serverRequest.pathVariable("country"); 76 | return ok().contentType(APPLICATION_JSON) 77 | .body(fromPublisher(personManager.findAllByCountry(country), Person.class)); 78 | } 79 | } 80 | -------------------------------------------------------------------------------- /src/main/java/com/lankydanblog/tutorial/person/web/PersonRouter.java: -------------------------------------------------------------------------------- 1 | package com.lankydanblog.tutorial.person.web; 2 | 3 | import org.springframework.context.annotation.Bean; 4 | import org.springframework.context.annotation.Configuration; 5 | import org.springframework.web.reactive.function.server.RouterFunction; 6 | import org.springframework.web.reactive.function.server.RouterFunctions; 7 | import org.springframework.web.reactive.function.server.ServerResponse; 8 | 9 | import static org.springframework.http.MediaType.APPLICATION_JSON; 10 | import static org.springframework.web.reactive.function.server.RequestPredicates.*; 11 | 12 | @Configuration 13 | public class PersonRouter { 14 | 15 | @Bean 16 | public RouterFunction route(PersonHandler personHandler) { 17 | return RouterFunctions.route(GET("/people/{id}").and(accept(APPLICATION_JSON)), personHandler::get) 18 | .andRoute(GET("/people").and(accept(APPLICATION_JSON)), personHandler::all) 19 | .andRoute(POST("/people").and(accept(APPLICATION_JSON)).and(contentType(APPLICATION_JSON)), personHandler::post) 20 | .andRoute(PUT("/people/{id}").and(accept(APPLICATION_JSON)).and(contentType(APPLICATION_JSON)), personHandler::put) 21 | .andRoute(DELETE("/people/{id}"), personHandler::delete) 22 | .andRoute(GET("/people/country/{country}").and(accept(APPLICATION_JSON)), personHandler::getByCountry); 23 | } 24 | } 25 | -------------------------------------------------------------------------------- /src/main/resources/application.properties: -------------------------------------------------------------------------------- 1 | spring.data.cassandra.keyspace-name=mykeyspace 2 | spring.data.cassandra.schema-action=CREATE_IF_NOT_EXISTS --------------------------------------------------------------------------------