URL shortener service in 42 lines of code in… Java (?!) Spring Boot + Redis
Apparently writing a URL shortener service is the new “Hello, world!” in the IoT/microservice/era world. It all started with A URL shortener service in 45 lines of Scala – neat piece of Scala, flavoured with Spray and Redis for storage. This was quickly followed with A url shortener service in 35 lines of Clojure and even URL Shortener in 43 lines of Haskell. So my inner anti-hipster asked: how long would it be in Java? But not plain Java, for goodness’ sake. Spring Boot with Spring Data Redis are a good starting point. All we need is a simple controller handling GET and POST:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | import com.google.common.hash.Hashing; import org.apache.commons.validator.routines.UrlValidator; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.boot.SpringApplication; import org.springframework.data.redis.core.StringRedisTemplate; import org.springframework.http.*; import org.springframework.web.bind.annotation.*; import javax.servlet.http.*; import java.nio.charset.StandardCharsets; @org .springframework.boot.autoconfigure.EnableAutoConfiguration @org .springframework.stereotype.Controller public class UrlShortener { public static void main(String[] args) { SpringApplication.run(UrlShortener. class , args); } @Autowired private StringRedisTemplate redis; @RequestMapping (value = "/{id}" , method = RequestMethod.GET) public void redirect( @PathVariable String id, HttpServletResponse resp) throws Exception { final String url = redis.opsForValue().get(id); if (url != null ) resp.sendRedirect(url); else resp.sendError(HttpServletResponse.SC_NOT_FOUND); } @RequestMapping (method = RequestMethod.POST) public ResponseEntity<String> save(HttpServletRequest req) { final String queryParams = (req.getQueryString() != null ) ? "?" + req.getQueryString() : "" ; final String url = (req.getRequestURI() + queryParams).substring( 1 ); final UrlValidator urlValidator = new UrlValidator( new String[]{ "http" , "https" }); if (urlValidator.isValid(url)) { final String id = Hashing.murmur3_32().hashString(url, StandardCharsets.UTF_8).toString(); redis.opsForValue().set(id, url); } else return new ResponseEntity<>(HttpStatus.BAD_REQUEST); } } |
The code is nicely self-descriptive and is functionally equivalent to a version in Scala. I didn’t try to it squeeze too much to keep line count as short as possible, code above is quite typical with few details:
- I don’t normally use wildcard imports
- I don’t use fully qualified class names (I wanted to save one
import
line, I admit) - I surround
if
/else
blocks with braces - I almost never use field injection, ugliest brother in inversion of control family. Instead I would go for constructor to allow testing with mocked Redis:
1 2 3 4 5 6 | @Autowired private final StringRedisTemplate redis; public UrlShortener(StringRedisTemplate redis) { this .redis = redis; } |
The thing I struggled the most was… obtaining the original, full URL. Basically I needed everything after .com
or port. No bloody way (neither servlets, nor Spring MVC), hence the awkward getQueryString()
fiddling. You can use the service as follows – creating shorter URL:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 | $ curl -vX POST localhost:8080 /https : //www .google.pl /search ?q=tomasz+nurkiewicz > POST /https : //www .google.pl /search ?q=tomasz+nurkiewicz HTTP /1 .1 > User-Agent: curl /7 .30.0 > Host: localhost:8080 > Accept: */* > < HTTP /1 .1 200 OK < Server: Apache-Coyote /1 .1 < Content-Type: text /plain ;charset=ISO-8859-1 < Content-Length: 28 < Date: Sat, 23 Aug 2014 20:47:40 GMT < http: //mydomain .com /50784f51 |
Redirecting through shorter URL:
01 02 03 04 05 06 07 08 09 10 11 12 13 | $ curl - v localhost:8080 /50784f51 > GET /50784f51 HTTP /1 .1 > User-Agent: curl /7 .30.0 > Host: localhost:8080 > Accept: */* > < HTTP /1 .1 302 Found < Server: Apache-Coyote /1 .1 < Location: https: //www .google.pl /search ?q=tomasz+nurkiewicz < Content-Length: 0 < Date: Sat, 23 Aug 2014 20:48:00 GMT < |
For completeness, here is a build file in Gradle (maven would work as well), skipped in all previous solutions:
01 02 03 04 05 06 07 08 09 10 11 12 13 14 15 16 17 18 19 20 21 22 23 24 25 26 27 28 29 30 31 32 33 34 35 36 37 38 39 40 41 42 | buildscript { repositories { mavenLocal() mavenCentral() } dependencies { classpath 'org.springframework.boot:spring-boot-gradle-plugin:1.1.5.RELEASE' } } apply plugin: 'java' apply plugin: 'spring-boot' sourceCompatibility = '1.8' repositories { mavenLocal() mavenCentral() } dependencies { compile "org.springframework.boot:spring-boot-starter-web:1.1.5.RELEASE" compile "org.springframework.boot:spring-boot-starter-redis:1.1.5.RELEASE" compile 'com.google.guava:guava:17.0' compile 'org.apache.commons:commons-lang3:3.3.2' compile 'commons-validator:commons-validator:1.4.0' compile 'org.apache.tomcat.embed:tomcat-embed-el:8.0.9' compile "org.aspectj:aspectjrt:1.8.1" runtime "cglib:cglib-nodep:3.1" } tasks.withType(GroovyCompile) { groovyOptions.optimizationOptions.indy = true } task wrapper(type: Wrapper) { gradleVersion = '2.0' } |
Actually also 42 lines... That's the whole application, no XML, no descriptors, not setup.
I don't treat this exercise as just a dummy code golf for shortest, most obfuscated working code. URL shortener web service with Redis back-end is an interesting showcase of syntax and capabilities of a given language and ecosystem. Much more entertaining then a bunch of algorithmic problems, e.g. found in Rosetta code. Also it's a good bare minimum template for writing a REST service.
One important feature of original Scala implementation, that was somehow silently forgotten in all implementations, including this one, is that it's non-blocking. Both HTTP and Redis access is event-driven (reactive, all right, I said it), thus I suppose it can handle tens of thousands of clients simultaneously. This can't be achieved with blocking controllers backed by Tomcat. But still you have to admit such a service written in Java (not even Java 8!) is surprisingly concise, easy to follow and straightforward - none of the other solutions are that readable (this is of course subjective).
Waiting for others!
Reference: | URL shortener service in 42 lines of code in... Java (?!) Spring Boot + Redis from our JCG partner Tomasz Nurkiewicz at the Java and neighbourhood blog. |
Very nice and instructive example. I think the Java version is the most readable one out of all the implementations.
You can change the mappings to the new @GetMapping and @PostMapping.