URL shortener service in 42 lines of code in... Java (?!)
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:
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); return new ResponseEntity<>("http://mydomain.com/" + id, HttpStatus.OK); } 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:
private final StringRedisTemplate redis; @Autowired public UrlShortener(StringRedisTemplate redis) { this.redis = redis; }
.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:
$ 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/50784f51Redirecting through shorter URL:
$ 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:
buildscript { repositories { mavenLocal() maven { url "http://repo.spring.io/libs-snapshot" } 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() maven { url 'http://repository.codehaus.org' } maven { url 'http://repo.spring.io/milestone' } 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!