Creating a reactive ImageService

The first rule of thumb when building web apps is to keep Spring controllers as light as possible. We can think of them as converters between HTTP traffic and our system.

To do that, we need to create a separate ImageService, as shown here, and let it do all the work:

    @Service 
    public class ImageService { 
 
      private static String UPLOAD_ROOT = "upload-dir"; 
 
      private final ResourceLoader resourceLoader; 
 
      public ImageService(ResourceLoader resourceLoader) { 
        this.resourceLoader = resourceLoader; 
      } 
      ... 
    } 

This last Spring service can be described as follows:

  • @Service: This indicates this is a Spring bean used as a service. Spring Boot will automatically scan this class and create an instance.
  • UPLOAD_ROOT: This is the base folder where images will be stored.
  • ResourceLoader: This is a Spring utility class used to manage files. It is created automatically by Spring Boot and injected to our service via constructor injection. This ensures our service starts off with a consistent state.

Now we can start creating various utility methods needed to service our application.

Let's kick things off by loading up some mock image files loaded with test data. To do that, we can add the following method to the bottom of our newly minted ImageService class:

    /** 
    * Pre-load some test images 
    * 
    * @return Spring Boot {@link CommandLineRunner} automatically 
    *         run after app context is loaded. 
    */ 
    @Bean 
    CommandLineRunner setUp() throws IOException { 
      return (args) -> { 
        FileSystemUtils.deleteRecursively(new File(UPLOAD_ROOT)); 
 
        Files.createDirectory(Paths.get(UPLOAD_ROOT)); 
 
        FileCopyUtils.copy("Test file", 
         new FileWriter(UPLOAD_ROOT + 
          "/learning-spring-boot-cover.jpg")); 
 
        FileCopyUtils.copy("Test file2", 
         new FileWriter(UPLOAD_ROOT + 
          "/learning-spring-boot-2nd-edition-cover.jpg")); 
 
        FileCopyUtils.copy("Test file3", 
         new FileWriter(UPLOAD_ROOT + "/bazinga.png")); 
      }; 
    } 

The preceding little nugget of initializing code is described as follows:

  • @Bean indicates that this method will return back an object to be registered as a Spring bean at the time that ImageService is created.
  • The bean returned is a CommandLineRunner. Spring Boot runs ALL CommandLineRunners after the application context is fully realized (but not in any particular order).
  • This method uses a Java 8 lambda, which gets automatically converted into a CommandLineRunner via Java 8 SAM (Single Abstract Method) rules.
  • The method deletes the UPLOAD_ROOT directory, creates a new one, then creates three new files with a little bit of text.

With test data in place, we can start interacting with it by fetching all the existing files in UPLOAD_ROOT reactively by adding the following method to our ImageService:

    public Flux<Image> findAllImages() { 
      try { 
        return Flux.fromIterable( 
          Files.newDirectoryStream(Paths.get(UPLOAD_ROOT))) 
           .map(path -> 
            new Image(path.hashCode(), 
                      path.getFileName().toString())); 
      } catch (IOException e) { 
          return Flux.empty(); 
      } 
    } 

Let's explore the preceding code:

  • This method returns Flux<Image>, a container of images that only gets created when the consumer subscribes.
  • The Java NIO APIs are used to create a Path from UPLOAD_ROOT, which is used to open a lazy DirectoryStream courtesy of Files.newDirectoryStream(). DirectoryStream is a lazy iterable, which means that nothing is fetched until next() is called, making it a perfect fit for our Reactor Flux.
  • Flux.fromIterable is used to wrap this lazy iterable, allowing us to only pull each item as demanded by a reactive streams client.
  • The Flux maps over the paths, converting each one to an Image.
  • In the event of an exception, an empty Flux is returned.

It's important to repeat that the stream of directory paths is lazy as well as the Flux itself. This means that nothing happens until the client subscribes, that is, starts pulling for images. At that point, the flow we just wrote will react, and start performing our data transformation. And it will only process each entry as each entry is pulled.

The next piece we need in our ImageService is the ability to fetch a single image so it can be displayed, and we can use this to do so:

    public Mono<Resource> findOneImage(String filename) { 
      return Mono.fromSupplier(() -> 
        resourceLoader.getResource( 
          "file:" + UPLOAD_ROOT + "/" + filename)); 
    } 

This last code can easily be described as follows:

  • Since this method only handles one image, it returns a Mono<Resource>. Remember, Mono is a container of one. Resource is Spring's abstract type for files.
  • resourceLoader.getResource() fetches the file based on filename and UPLOAD_ROOT.
  • To delay fetching the file until the client subscribes, we wrap it with Mono.fromSupplier(), and put getResource() inside a lambda.

Until now, we've seen Mono.just() used to illustrate Reactor's way of initializing single items. However, if we wrote Mono.just(resourceLoader.getResource(...​)), the resource fetching would happen immediately when the method is called. By putting it inside a Java 8 Supplier, that won't happen until the lambda is invoked. And because it's wrapped by a Mono, invocation won't happen until the client subscribes.

There is another Mono operation that is very similar to fromSupplier()--defer(). The difference is that Mono.defer() is invoked individually by every downstream subscriber. It's best used not for fetching resources like our situation but for something like polling status instead.

Having written code to fetch all images and a single image, it's time we introduce the ability to create new ones. The following code shows a reactive way to handle this:

    public Mono<Void> createImage(Flux<FilePart> files) { 
      return files.flatMap(file -> file.transferTo( 
        Paths.get(UPLOAD_ROOT, file.filename()).toFile())).then(); 
    } 

The last code can be described as follows:

  • This method returns a Mono<Void> indicating that it has no resulting value, but we still need a handle in order to subscribe for this operation to take place
  • The incoming Flux of FilePart objects are flatMapped over, so we can process each one
  • Each file is tested to ensure it's not empty
  • At the heart of our chunk of code, Spring Framework 5's FilePart transfers the content into a new file stored in UPLOAD_ROOT
  • then() lets us wait for the entire Flux to finish, yielding a Mono<Void>

Our last image-based operation to add to ImageService is to implement the means to delete images, as shown here:

    public Mono<Void> deleteImage(String filename) { 
      return Mono.fromRunnable(() -> { 
        try { 
          Files.deleteIfExists(Paths.get(UPLOAD_ROOT, filename)); 
        } catch (IOException e) { 
            throw new RuntimeException(e); 
        } 
      }); 
    } 

The preceding code can be described as follows:

  • Because this method doesn't care about return values, its return type is Mono<Void>.
  • To hold off until subscribe, we need to wrap our code with Mono.fromRunnable(), and use a lambda expression to coerce a Runnable. This lets us put our code off to the side until we're ready to run it.
  • Inside all of that, we can use Java NIO's handy Files.deleteIfExists().
If wrapping every return type in either a Flux or a Mono is starting to bend your brain, you are not alone. This style of programming may take a little getting used to but it's not that big of a leap. Once you get comfortable with it, I guarantee you'll spot blocking code all over the place. Then you can set out to make it reactive without descending into callback hell.
..................Content has been hidden....................

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