tags | projects | |||||
---|---|---|---|---|---|---|
|
|
This guide walks you through the process of creating a RESTful image thumbnailing service using Spring Boot, Reactor, and Netty.
You’ll build a service that thumbnails an uploaded JPEG image and stores it for serving to a web browser. You’ll use Reactor’s TCP support (specifically, the Netty implementation) in order to provide excellent scalability in highly-concurrent deployments. Although similar systems could be built in a more traditional MVC style, it’s not very straightforward to harness the fully non-blocking capabilities that are only available in Servlet 3.1 and later containers.
You’ll need to download an image from Google Images or Flickr or find a JPEG image you have on your local hard drive. It should be larger than 250px on the long side.
To do the work of resizing the image, you’ll register a function on a Reactor
that will:
-
Accept a Path to an uploaded, full-size image.
-
Resize this image to 250px on the long side.
-
Return a
Path
to the resized (thumbnailed) image.
Note
|
There is another Getting Started Guide that discusses in more detail publishing and handling events in Reactor. |
Although the image quality will be quite poor, for simplicity’s sake you can use the JDK’s built-in Graphics2D engine to do the image thumbnailing. Specifically that means applying an AffineTransform
to a BufferedImage
.
src/main/java/hello/BufferedImageThumbnailer.java
link:complete/src/main/java/hello/BufferedImageThumbnailer.java[role=include]
The thumbnailer implements Reactor’s Function
interface by implementing the apply(Event<Path>)
method. It accepts an Event
whose payload is the Path
to the uploaded, full-size image. It then reads in the image using ImageIO
and determines its current dimensions. It has to calculate the scale it needs to use to shrink the image from its present dimensions to 250px on the longest side. If the image is square or horizontal, it uses the image’s width to determine that scale factor, otherwise it uses the image’s height.
After creating a Graphics2D
context, the thumbnailer applies a transformation to the image that scales it according to the scaling factor previously calculated to arrive at a dimension of 250px.
Note
|
If you have the GraphicsMagick package installed on your machine and want a higher-quality thumbnail, you can use the alternative implementation of the BufferedImageThumbnailer included in this guide: GraphicsMagickThumbnailer.java
|
Once the image is resized, it is written to disk and a Path
returned that points to that thumbnailed image.
To handle incoming HTTP requests using Reactor, you need to create a helper class. Create static methods that return stateless consumers which accept the Netty FullHttpRequest
object for the REST request. The helper methods should accept as parameters a Reactor NetChannel
to write a response to and an AtomicReference
to the shared thumbnail Path
.
src/main/java/hello/ImageThumbnailerRestApi.java
link:complete/src/main/java/hello/ImageThumbnailerRestApi.java[role=include]
Note
|
For simplicity’s sake, the upload handler expects raw binary data, not urlencoded data like in an upload form. This means you’ll need to use a command-line tool like curl to POST the image to the server in binary mode.
|
To bootstrap a Reactor NetServer
, you need to add the appropriate options that configure Netty’s pipeline to handle HTTP. Reactor has a special subclass of ServerSocketOptions
that expose the Netty ChannelPipeline
so you can add Netty built-in HTTP handlers.
Also required are some housekeeping objects like a CountDownLatch
to keep the main thread alive since Reactor’s TCP support is fully asynchronous and would not prevent the test app from exiting immediately after starting the server.
src/main/java/hello/ImageThumbnailerApp.java
link:complete/src/main/java/hello/ImageThumbnailerApp.java[role=include]
The Reactor NetServer
is configured in the restApi()
@Bean
method.
Note
|
The Environment parameter referenced in that method is coming from Reactor’s @EnableReactor bootstrap code. It’s not something you need to create yourself. The ServerSocketOptions , Reactor , and CountDownLatch parameters are all configured in other @Bean methods to keep the server configuration code clean from unnecessary clutter.
|
Reactor’s TCP support is channel-based. In order to respond to requests, you need to use the incoming Stream
of HTTP requests and attach the consumers you’re returning from the helper class you created in Create a REST API.
You can build a single executable JAR file that contains all the necessary dependencies, classes, and resources. This makes it easy to ship, version, and deploy the service as an application throughout the development lifecycle, across different environments, and so forth.
./gradlew build
Then you can run the JAR file:
java -jar build/libs/gs-reactor-thumbnailer-0.1.0.jar
If you are using Maven, you can run the application using mvn spring-boot:run
. Or you can build the JAR file with mvn clean package
and run the JAR by typing:
java -jar target/gs-reactor-thumbnailer-0.1.0.jar
The console should report the application starting.
. ____ _ __ _ _ /\\ / ___'_ __ _ _(_)_ __ __ _ \ \ \ \ ( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \ \\/ ___)| |_)| | | | | || (_| | ) ) ) ) ' |____| .__|_| |_|_| |_\__, | / / / / =========|_|==============|___/=/_/_/_/ :: Spring Boot :: (v1.0.1.RELEASE) 2014-04-14 18:10:55.456 INFO 64002 --- [ailerApp.main()] hello.ImageThumbnailerApp : Starting ImageThumbnailerApp on dev.home with PID 64002 (/Users/jbrisbin/Development/Projects/reactor/master/gs-reactor-thumbnailer/complete/target/classes started by jbrisbin) 2014-04-14 18:10:55.491 INFO 64002 --- [ailerApp.main()] s.c.a.AnnotationConfigApplicationContext : Refreshing org.springframework.context.annotation.AnnotationConfigApplicationContext@79ea1e26: startup date [Mon Apr 14 18:10:55 CDT 2014]; root of context hierarchy 2014-04-14 18:10:56.276 INFO 64002 --- [entExecutor-1-1] reactor.net.netty.tcp.NettyTcpServer : BIND /0:0:0:0:0:0:0:0:3000 2014-04-14 18:10:56.350 INFO 64002 --- [ailerApp.main()] o.s.j.e.a.AnnotationMBeanExporter : Registering beans for JMX exposure on startup 2014-04-14 18:10:56.365 INFO 64002 --- [ailerApp.main()] hello.ImageThumbnailerApp : Started ImageThumbnailerApp in 1.087 seconds (JVM running for 4.66)
When you see "Started ImageThumbnailerApp" in the console log, upload the image using a curl
command like the following:
curl -v -XPOST -H "Content-Type: image/jpeg" --data-binary @cute_kittens.jpg http://localhost:3000/thumbnail
You should get a 301 response back from the server with the Location
header pointing to the URI to pull up in your browser to see the thumbnailed image.
* About to connect() to localhost port 3000 (#0) * Trying ::1... * connected * Connected to localhost (::1) port 3000 (#0) > POST /thumbnail HTTP/1.1 > User-Agent: curl/7.24.0 (x86_64-apple-darwin12.0) libcurl/7.24.0 OpenSSL/0.9.8x zlib/1.2.5 > Host: localhost:3000 > Accept: */* > Content-Type: image/jpeg > Content-Length: 1002634 > Expect: 100-continue > < HTTP/1.1 100 Continue < HTTP/1.1 301 Moved Permanently < Content-Length: 0 < Location: /image/thumbnail.jpg
Although this is about as simple a multi-component setup as you can get, this guide demonstrates:
-
How to serve HTTP requests to power a REST API
-
How to distribute requests to a Reactor and use it as an event bus
-
How to send the response from an asynchronous task executed in a Reactor to an HTTP client that’s waiting on a result