URL shortener service in 42 lines of code in... Java (?!) Spring Boot + Redis
import com.google.common.hash.Hashing;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:
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);
}
}
- 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+nurkiewiczRedirecting through shorter URL:
> 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
$ curl -v localhost:8080/50784f51For completeness, here is a build file in Gradle (maven would work as well), skipped in all previous solutions:
> 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
<
buildscript {Actually also 42 lines... That's the whole application, no XML, no descriptors, not setup.
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'
}
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!
Tags: gradle, microservices, redis, spring, spring boot