Статьи

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

По всей видимости, написание службы сокращения URL-адресов — это новый « Hello, world! » В мире IoT / microservice / era. Все началось с сервиса сокращения URL в 45 строках Scala — аккуратный кусок Scala, приправленный Spray и Redis для хранения. За этим быстро последовал сервис сокращения URL в 35 строках Clojure и даже URL Shortener в 43 строках Haskell . Так что мой внутренний анти-хипстер спросил: как долго это будет на Яве? Но не ради простой Java, ради бога. Spring Boot с Spring Data Redis — хорошая отправная точка. Все, что нам нужно, это простой контроллер, обрабатывающий GET и 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);
    }
}

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

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

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

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

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

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!), Удивительно лаконичен, прост и понятен — ни одно из других решений не является настолько читабельным (это, конечно, субъективно).

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