Статьи

Служба сокращения URL-адресов в 42 строках кода в… Java (?!) Spring Boot + Redis

По-видимому, написание службы сокращения URL-адресов — это новый « Привет, мир! ”В мире Интернета вещей / микросервиса / эры. Все началось с сервиса сокращения URL в 45 строках Scala — аккуратный кусок Scala, приправленный Spray и Redis для хранения. За этим быстро последовал сервис сокращения URL в 35 строках Clojure и даже URL Shortener в 43 строках Haskell . Так что мой внутренний анти-хипстер спросил: как долго это будет на Яве? Но не ради простой Java, ради бога. Spring Boot с Spring Data Redis — хорошая отправная точка. Все, что нам нужно, это простой контроллер, обрабатывающий GET и 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);
            return new ResponseEntity<>("http://mydomain.com/" + id, HttpStatus.OK);
        } else
            return new ResponseEntity<>(HttpStatus.BAD_REQUEST);
    }
}

Код хорошо нагляден и функционально эквивалентен версии в Scala. Я не пытался сжать его слишком сильно, чтобы счетчик строк был как можно короче, приведенный выше код довольно типичен, с некоторыми подробностями:

  • Я обычно не использую символы подстановки
  • Я не использую полностью определенные имена классов (я хочу сохранить одну строку import , я признаю)
  • Я окружаю if / else блоки скобками
  • Я почти никогда не использую полевую инъекцию, самый уродливый брат в инверсии семейства контроля. Вместо этого я бы выбрал конструктор, чтобы разрешить тестирование с использованием мошеннического Redis
1
2
3
4
5
6
@Autowired
private final StringRedisTemplate redis;
  
public UrlShortener(StringRedisTemplate redis) {
    this.redis = redis;
}

Больше всего я боролся за то, чтобы получить оригинальный полный URL. В основном мне нужно было все после .com или порта. Никакого кровавого пути (ни сервлетов, ни Spring MVC), отсюда и неудобное getQueryString() . Вы можете использовать сервис следующим образом — создать более короткий 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

Перенаправление через более короткий 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
<

Для полноты, вот файл сборки в Gradle (также будет работать maven), пропущенный во всех предыдущих решениях:

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()
        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'
}

На самом деле также 42 строки … Вот и все приложение, без XML, без дескрипторов, без установки.

Я не отношусь к этому упражнению как к фиктивному коду для кратчайшего, наиболее запутанного рабочего кода. Веб-сервис сокращения URL с бэкэндом Redis — интересная демонстрация синтаксиса и возможностей данного языка и экосистемы. Гораздо интереснее, чем куча алгоритмических проблем, например, в коде Розетты . Также это хороший минимальный шаблон для написания REST-сервиса.

Одна важная особенность оригинальной реализации Scala , о которой как-то молча забыли во всех реализациях, включая эту, заключается в том, что она не блокируется. Доступ как к HTTP, так и к Redis основан на событиях ( реактивный , хорошо, я так сказал), поэтому я полагаю, что он может одновременно обрабатывать десятки тысяч клиентов. Этого нельзя достичь с помощью блокирующих контроллеров, поддерживаемых Tomcat. Но все же вы должны признать, что такой сервис, написанный на Java (даже не на Java 8!), Удивительно лаконичен, прост в использовании и понятен — ни одно из других решений не является настолько читабельным (это, конечно, субъективно).

В ожидании других!