Creating a reactive file controller

With our reactive image service in place, we can start to work on the reactive file controller.

For starters, let's create a HomeController as shown here:

    @Controller 
    public class HomeController { 
 
      private static final String BASE_PATH = "/images"; 
      private static final String FILENAME = "{filename:.+}"; 
 
      private final ImageService imageService; 
 
      public HomeController(ImageService imageService) { 
        this.imageService = imageService; 
      } 

The preceding code can be described as follows:

  • @Controller: This indicates that it is a web controller, and will be registered by Spring Boot to handle web requests.
  • BASE_PATH: This is a static string used to define the base of many routes.
  • FILENAME: This is a pattern for filenames where the "." is included. Otherwise, Spring WebFlux will use the suffix as part of content negotiation (for example, .json would try to fetch a JSON response, while .xml would try to fetch an XML response).
  • ImageService: This is injected via constructor injection so that we can tap our reactive image handling code we just wrote.

With this in place, we can code the handler for displaying a single image on the web page like this:

    @GetMapping(value = BASE_PATH + "/" + FILENAME + "/raw", 
     produces = MediaType.IMAGE_JPEG_VALUE) 
    @ResponseBody 
    public Mono<ResponseEntity<?>> oneRawImage( 
      @PathVariable String filename) { 
        return imageService.findOneImage(filename) 
         .map(resource -> { 
           try { 
             return ResponseEntity.ok() 
              .contentLength(resource.contentLength()) 
              .body(new InputStreamResource( 
                resource.getInputStream())); 
           } catch (IOException e) { 
               return ResponseEntity.badRequest() 
                .body("Couldn't find " + filename + 
                 " => " + e.getMessage()); 
           } 
        }); 
    } 

The last code can be explained as follows:

  • @GetMapping defines a route mapping for GET BASE_PATH + "/" + FILENAME + "/raw". It also sets the Content-Type header to properly render it as an image.
  • @ResponseBody indicates that this method's response will be written directly into the HTTP response body.
  • @PathVariable flags that the input filename will be extracted from the route's {filename} attribute.
  • Mono<ResponseEntity<?>> shows that we are returning a single response, reactively. ResponseEntity<?> describes a generic HTTP response.
  • The code taps our image service's findOneImage() using filename.
It's possible to have incoming arguments wrapped in Reactor types such as Mono<String>. Since this argument comes from the route and not the request body, there is nothing gained in this situation.
  • Since findOneImage returns a Mono<Resource>, we map over it, transforming this Spring Resource into a ResponseEntity including a Content-Length response header as well as the data embedded in the body.
  • In the event of an exception, it will return an HTTP Bad Response.

This one controller handler method demonstrates many features provided by Reactive Spring. We see route handling, delegating to a separate service, converting the response into a suitable format for clients, and error handling.

This code also shows it being done reactively. Generating the HTTP OK / HTTP BAD REQUEST response doesn't happen until map() is executed. This is chained to the image service fetching the file from disk. And none of that happens until the client subscribes. In this case, subscribing is handled by the framework when a request comes in.

I thought you said to keep controllers light! That is true. Maybe this looks not so light? To take the ResponseEntity wrapping and move it into the ImageService would be wrong, because that service doesn't know anything about the web layer. This controller's focus is to make the data presentable to web clients, which is exactly what we've coded.

The next controller method we can add to HomeController is the handler for uploading new files, as shown here:

    @PostMapping(value = BASE_PATH) 
    public Mono<String> createFile(@RequestPart(name = "file") 
     Flux<FilePart> files) { 
       return imageService.createImage(files) 
        .then(Mono.just("redirect:/")); 
    } 

The preceding method is described as follows:

  • A collection of incoming FilePart objects is represented as a Flux
  • The flux of files is handed directly to the image service to be processed
  • .then() indicates that once the method is complete, it will then return a redirect:/ directive (wrapped in a Mono), issuing an HTML redirect to /

It's important to remember that we aren't issuing .then() against the flux of files. Instead, the image service hands us back a Mono<Void> that signals when it has completed processing all the files. It is that Mono which we are chaining an additional call to return back the redirect.

The next thing we need to add to our HomeController is the ability to handle requests for deleting images. This is done as follows:

    @DeleteMapping(BASE_PATH + "/" + FILENAME) 
    public Mono<String> deleteFile(@PathVariable String filename) { 
      return imageService.deleteImage(filename) 
       .then(Mono.just("redirect:/")); 
    } 

The previous code can be described like this:

  • Using Spring's @DeleteMapping annotation, this method is ready for HTTP DELETE operations
  • It's keyed to the same BASE_PATH + "/" + FILENAME pattern
  • It taps the image service's deleteImage() method
  • It uses then() to wait until the delete is done before returning back a mono-wrapped redirect:/ directive

The last bit to add to our HomeController is the call to serve up a list of images in a template. For that, we need this general GET handler for the root:

    @GetMapping("/") 
    public Mono<String> index(Model model) { 
      model.addAttribute("images", imageService.findAllImages()); 
      return Mono.just("index"); 
    } 

The preceding handler can be described as follows:

  • @GetMapping is used to explicitly map the "/" route.
  • It accepts a Model object, giving us a place to load data reactively.
  • addAttribute() lets us assign the image service's findAllImages() Flux to the template model's images attribute.
  • The method returns "index" wrapped in a Mono, ensuring the whole thing is chained together, top to bottom, to kick off when Spring WebFlux subscribes to render the template.

It's important to understand that we don't assign a list of images to the template model's images attribute. We assign a lazy Flux of images, which means that the model won't be populated with real data until Reactive Spring subscribes for the data. Only then will the code actually start fetching image data.

Perhaps, at this stage, you're wondering amidst all the lambdas, Fluxes, Monos, and subscriptions, exactly what is happening from a threading perspective. Project Reactor is concurrency agnostic. It doesn't enforce a certain concurrency model, but leaves you in command instead. Reactor has several schedulers that support a multitude of options. This includes running in the current thread, running in a single worker thread, running on a per-call dedicated thread, an elastic pool of threads, a fixed pool of worker threads tuned for parallel work, and a time-aware scheduler capable of scheduling tasks in the future. Additionally, Reactor allows creating a scheduler out of any ExecutorService. We aren't going to delve into that in this work, but it's definitely something to investigate when you build a real application and want to govern how things scale.
..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.141.7.240