Как сократить URL-адреса: пошаговое руководство по Java и Spring

Как сократить URL-адреса: пошаговое руководство по Java и Spring

6 июня 2022 г.

Внедрение службы сокращения URL-адресов не является сложной задачей и часто является частью интервью по проектированию системы. В этом посте я постараюсь объяснить процесс внедрения сервиса. Сокращатель URL-адресов – это сервис, который используется для создания коротких ссылок из очень длинных URL-адресов.

Обычно короткие ссылки имеют размер одной трети или даже одной четверти исходного URL-адреса, что облегчает их ввод, представление или твит. Нажав на короткую ссылку, пользователь будет автоматически перенаправлен на исходный URL. В Интернете доступно множество сервисов сокращения URL-адресов, таких как tiny.cc, bitly.com, cutt.ly и т. д.

Теория

Перед внедрением всегда полезно записать, что необходимо сделать, в виде функциональных и нефункциональных требований.

Функциональные требования

  • Пользователи должны иметь возможность вводить длинный URL. Наша служба должна сохранить этот URL и сгенерировать короткую ссылку.
  • Нажатие на короткую ссылку должно перенаправлять пользователя на исходный длинный URL.
  • Пользователи должны иметь возможность ввести дату истечения срока действия. По истечении этой даты короткая ссылка станет недействительной.
  • Пользователи должны создать учетную запись, чтобы использовать службу. У служб может быть ограничение на использование для каждого пользователя (необязательно)
  • Пользователь может создать свою собственную короткую ссылку. Служба должна иметь показатели, например наиболее посещаемые ссылки (необязательно)

Нефункциональные требования

  • Служба должна работать 100% времени
  • Переадресация не должна длиться более двух секунд.

Преобразование URL

Допустим, нам нужна короткая ссылка максимальной длины 7. Самое главное в сокращателе URL — это алгоритм преобразования. Преобразование URL-адресов можно реализовать несколькими способами, и у каждого из них есть свои плюсы и минусы.

Одним из способов создания коротких ссылок может быть хеширование исходного URL с помощью некоторой хэш-функции (например, MD5 или SHA-2). При использовании хэш-функции совершенно очевидно, что разные входные данные приведут к разным выходным данным. Результат хеша длиннее семи символов, поэтому нам нужно будет взять первые семь символов. Но в этом случае может возникнуть коллизия, поскольку первые семь символов уже могут использоваться в качестве короткой ссылки. Затем мы берем следующие семь символов, пока не найдем короткую ссылку, которая не используется.

Второй способ создания короткой ссылки — использование UUID. Вероятность дублирования UUID не равна нулю, но достаточно близка к нулю, чтобы ею можно было пренебречь. Поскольку UUID имеет 36 символов, это означает, что у нас та же проблема, что и выше. Мы должны взять первые семь символов и проверить, не используется ли уже эта комбинация.

Третьим вариантом будет преобразование чисел из базы 10 в базу 62. База — это количество цифр или символов, которые можно использовать для представления определенного числа. База 10 — это цифры [0-9], которые мы используем в повседневной жизни, а база 62 — это [0-9][a-z][A-Z]. Это означает, что, например, число с основанием 10, состоящее из четырех цифр, будет таким же числом с основанием 62, но с двумя символами.

Использование базы 62 в преобразовании URL с максимальной длиной семи символов позволяет нам иметь 62^7 уникальных значений для коротких ссылок.

Как работают конверсии с основанием 62?

У нас есть число с основанием 10, которое мы хотим преобразовать в число с основанием 62. Мы собираемся использовать следующий алгоритм:

    while(number > 0)
    remainder = number % 62
    number = number / 62
    attach remainder to start of result collection

После этого нам просто нужно сопоставить числа из набора результатов с основанием 62 Alphabet = [0,1,2,…,a,b,c…,A,B,C,…].

Давайте посмотрим, как это работает на реальном примере. В этом примере давайте преобразуем 1000 из числа 10 в основание 62.

    1st iteration:
        number = 1000
        remainder = 1000 % 62 = 8
        number = 1000 / 62 = 16
        result list = [8]
    2nd iteration:
        number = 16
        remainder = 16 % 62 = 16
        number = 16 / 62 = 0
        result list = [16,8]
        There is no more iterations since number = 0 after 2nd iteration

Сопоставление [16,8] с основанием 62 будет g8. Это означает, что 1000base10 = g8base62.

Преобразование с основанием 62 в основание 10 также просто:

    i = 0
    while(i < inputString lenght)
        counter = i + 1
        mapped = base62alphabet.indexOf(inputString[i]) // map character to number based on its index in alphabet
        result = result + mapped * 62^(inputString lenght - counter)
        i++

Реальный пример:

    inputString = g8
    inputString length = 2
    i = 0
    result = 0
    1st iteration
        counter = 1
        mapped = 16 // index of g in base62alphabet is 16
        result = 0 + 16 * 62^1 = 992
    2nd iteration
        counter = 2
        mapped = 8 // index of 8 in base62alphabet is 8
        result = 992 + 8 * 62^1 = 1000

Реализация

Примечание. Полное решение находится на моем Github. Я реализовал этот сервис с помощью Spring Boot и MySQL.

Мы собираемся использовать функцию автоинкремента нашей базы данных. Автоинкрементное число будет использоваться для преобразования базы 62. Вы можете использовать любую другую базу данных с функцией автоинкремента.

Сначала посетите Spring initializr и выберите драйвер Spring Web и MySql. После этого нажмите кнопку «Создать» и загрузите zip-файл. Разархивируйте файл и откройте проект в вашей любимой IDE. Каждый раз, когда я начинаю новый проект, мне нравится создавать несколько папок, чтобы логически разделить мой код. Моими папками в данном случае являются контроллер, объект, служба, репозиторий, dto и config.

Внутри папки объектов создадим Url.java с четырьмя атрибутами: id, longUrl, createdDate, expiresDate.

Обратите внимание, что здесь нет атрибута короткой ссылки. Мы не будем сохранять короткие ссылки. Мы собираемся преобразовать атрибут id из базы 10 в базу 62 каждый раз, когда есть запрос GET. Таким образом, мы экономим место в нашей базе данных.

Атрибут LongUrl — это URL-адрес, на который мы должны перенаправить, как только пользователь перейдет по короткой ссылке. Дата создания предназначена только для того, чтобы видеть, когда сохраняется longUrl (это не важно), а expiresDate присутствует, если пользователь хочет сделать короткую ссылку недоступной через некоторое время.

Затем создадим BaseService.java в сервисной папке. BaseService содержит методы для преобразования базы 10 в базу 62 и наоборот.

    private static final String allowedString = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
    private char[] allowedCharacters = allowedString.toCharArray();
    private int base = allowedCharacters.length;

Как я упоминал ранее, если мы хотим использовать преобразование с основанием 62, нам нужен алфавит с основанием 62, который в данном случае называется allowCharacters. Кроме того, значение базовой переменной вычисляется из длины разрешенных символов на случай, если мы захотим изменить разрешенные символы.

Метод encode принимает число в качестве входных данных и возвращает короткую ссылку. Метод декодирования принимает строку (короткую ссылку) в качестве входных данных и возвращает число. Алгоритмы должны быть реализованы, как описано выше.

После этого внутри папки репозитория создадим UrlRepository. java, который является просто расширением JpaRepository и дает нам множество методов, таких как 'findById', 'save' и т. д. Нам не нужно ничего добавлять к этому.

Затем давайте создадим файл UrlController.java в папке контроллера. Контроллер должен иметь один метод POST для создания коротких ссылок и один метод GET для перенаправления на исходный URL.

    @PostMapping("create-short")
    public String convertToShortUrl(@RequestBody UrlLongRequest request) {
        return urlService.convertToShortUrl(request);
    }

    @GetMapping(value = "{shortUrl}")
    public ResponseEntity<Void> getAndRedirect(@PathVariable String shortUrl) {
        var url = urlService.getOriginalUrl(shortUrl);
        return ResponseEntity.status(HttpStatus.FOUND)
        .location(URI.create(url))
        .build();
    }

Метод POST имеет UrlLongRequest в качестве тела запроса. Это просто класс с атрибутами longUrl и expiresDate.

Метод GET принимает короткий URL-адрес в качестве переменной пути, а затем получает и перенаправляет на исходный URL-адрес. В верхней части контроллера вводится UrlService как зависимость, которая будет объяснена далее.

UrlService.java — это место, где находится большая часть логики. и является службой, используемой контроллером.

ConvertToShortUrl используется методом POST из контроллера. Он просто создает новую запись в базе данных и получает идентификатор. Затем идентификатор преобразуется в короткую ссылку с основанием 62 и возвращается контроллеру.

GetOriginalUrl — это метод, используемый методом GET из контроллера. Сначала он преобразует строку в основание 10, и результатом этого является идентификатор. Затем он получает запись из базы данных с этим идентификатором и выдает исключение, если он не существует. После этого он возвращает исходный URL контроллеру.

«Дополнительные» темы

В этой части я расскажу о документации swagger, докеризации приложения, кеше приложения и запланированном событии MySql.

Интерфейс Swagger

Каждый раз, когда вы разрабатываете API, полезно каким-либо образом документировать его. Документация упрощает понимание и использование API. API для этого проекта задокументирован с использованием пользовательского интерфейса Swagger.

Пользовательский интерфейс Swagger позволяет любому визуализировать ресурсы API и взаимодействовать с ними без какой-либо логики реализации.

Он генерируется автоматически, а визуальная документация упрощает его реализацию на стороне клиента и использование на стороне клиента.

Чтобы включить пользовательский интерфейс Swagger в проект, необходимо выполнить несколько шагов.

Во-первых, нам нужно добавить зависимости Maven в файл pom.xml:

    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-swagger2</artifactId>
        <version>2.9.2</version>
    </dependency>
    <dependency>
        <groupId>io.springfox</groupId>
        <artifactId>springfox-swagger-ui</artifactId>
        <version>2.9.2</version>
    </dependency>

Для справки вы можете увидеть полный файл pom.xml здесь. После добавления зависимостей Maven пришло время добавить конфигурацию Swagger. Внутри папки конфигурации нам нужно создать новый класс — SwaggerConfig.java

    @Configuration
    @EnableSwagger2
    public class SwaggerConfig {

    @Bean    
    public Docket apiDocket() {   
        return new Docket(DocumentationType.SWAGGER_2)  
            .apiInfo(metadata())    
            .select()    
            .apis(RequestHandlerSelectors.basePackage("com.amarin"))    
            .build();    
    }

    private ApiInfo metadata(){
        return new ApiInfoBuilder()
        .title("Url shortener API")    
        .description("API reference for developers")    
        .version("1.0")    
        .build();    
        }  
    }

В верхней части класса нам нужно добавить пару аннотаций.

@Configuration указывает, что класс объявляет один или несколько методов @Beans и может обрабатываться контейнером Spring для создания определений компонентов и запросов на обслуживание для этих компонентов во время выполнения. .

@EnableSwagger2 указывает, что необходимо включить поддержку Swagger.

Затем мы должны добавить компонент Docket, который предоставляет основную конфигурацию API с разумными значениями по умолчанию и удобными методами настройки.

Метод apiInfo() принимает объект ApiInfo, в котором мы можем настроить всю необходимую информацию API — в противном случае он использует некоторые значения по умолчанию. Чтобы сделать код чище, мы должны создать закрытый метод, который будет настраивать и возвращать объект ApiInfo и передавать этот метод в качестве параметра метода apiInfo(). В данном случае это метод metadata().

Метод apis() позволяет фильтровать документируемые пакеты.

Пользовательский интерфейс Swagger настроен, и мы можем начать документировать наш API. Внутри UrlController, выше каждой конечной точки, мы можем использовать аннотацию @ApiOperation для добавления описания. В зависимости от ваших потребностей вы можете использовать некоторые другие аннотации.

Также возможно документировать DTO< /a> с помощью @ApiModelProperty, что позволяет добавлять допустимые значения, описания и т. д.

Кэширование

Согласно Википедии, [кэш](https://en.wikipedia.org/wiki/Cache_(computing) – это аппаратный или программный компонент, который хранит данные, чтобы будущие запросы на это данные могут обслуживаться быстрее; данные, хранящиеся в кеше, могут быть результатом более ранних вычислений или копией данных, хранящихся в другом месте.

Наиболее часто используемый тип кеша — кэш в памяти, в котором кэшированные данные хранятся в ОЗУ. Когда данные запрашиваются и находятся в кеше, они обслуживаются из оперативной памяти, а не из базы данных. Таким образом, мы избегаем вызова дорогостоящей серверной части, когда пользователь запрашивает данные.

Сокращатель URL-адресов – это тип приложения, у которого больше запросов на чтение, чем на запись, что означает, что это идеальное приложение для использования кеша.

Чтобы включить кэширование в приложении Spring Boot, нам просто нужно добавить @EnableCaching в Класс UrlShortenerApiApplication.

После этого в контроллере нам нужно установить @Cachable аннотация над методом GET. Эта аннотация автоматически сохраняет результаты метода, называемого кешем. В аннотации @Cachable мы устанавливаем параметр value, который является именем кеша, и параметр key, который является ключом кеша.

В этом случае для ключа кеша мы будем использовать «shortUrl», потому что мы уверены, что он уникален. Параметрам Sync задано значение true, чтобы гарантировать, что только один поток создает значение кэша.

И все — наш кеш настроен и при первой загрузке URL с какой-то короткой ссылкой результат будет сохранен в кеш и любой дополнительный вызов конечной точки с такой же короткой ссылкой будет извлекать результат из кеша, а не из базы данных.

Докеризация

Докеризация — это процесс упаковки приложения и его зависимостей в контейнер [Docker](https://en.wikipedia.org/wiki/Docker_(software). После настройки контейнера Docker мы можем легко запустить приложение на любом сервере или компьютере, поддерживающем Docker.

Первое, что нам нужно сделать, это создать Dockerfile.

Dockerfile – это текстовый файл, содержащий все команды, которые пользователь может вызвать в командной строке для сборки образа. .

    FROM openjdk:13-jdk-alpine   
    COPY ./target/url-shortener-api-0.0.1-SNAPSHOT.jar /usr/src/app/url-shortener-api-0.0.1-SNAPSHOT.jar    
    EXPOSE 8080    
    ENTRYPOINT ["java","-jar","/usr/src/app/url-shortener-api-0.0.1-SNAPSHOT.jar"]

FROM — здесь мы устанавливаем базовый образ для базы сборки. Мы собираемся использовать OpenJDK v13, бесплатную версию Java с открытым исходным кодом. Вы можете найти другие образы для вашего базового образа в Docker Hub, где можно делиться образами Docker.

COPY — эта команда копирует файлы из локальной файловой системы (вашего компьютера) в файловую систему контейнера по указанному нами пути. Мы собираемся скопировать файл JAR из целевой папки в папку /usr/src/app в контейнере. Я объясню создание JAR-файла чуть позже.

EXPOSE — инструкция, которая информирует Docker о том, что контейнер прослушивает указанные сетевые порты во время выполнения. Протокол по умолчанию — TCP, и вы можете указать, хотите ли вы использовать UDP.

ENTRYPOINT. Эта инструкция позволяет настроить контейнер, который будет работать как исполняемый файл. Здесь нам нужно указать, как Docker будет запускать приложения.

Команда для запуска приложения из файла .jar

    java -jar <app_name>.jar

поэтому мы помещаем эти 3 слова в массив, и все.

Теперь, когда у нас есть Dockerfile, мы должны собрать из него образ. Но, как я упоминал ранее, нам сначала нужно создать файл .jar из нашего проекта, чтобы команда COPY в Dockerfile могла работать правильно. Для создания исполняемого файла .jar мы будем использовать maven.

Нам нужно убедиться, что у нас есть Maven внутри нашего pom.xml. Если Maven отсутствует, мы можем добавить его

<build>    
    <plugins>    
        <plugin>    
            <groupId>org.springframework.boot</groupId>    
            <artifactId>spring-boot-maven-plugin</artifactId>    
        </plugin>    
    </plugins>    
</build>

После этого нам нужно просто запустить команду

    mvn clean package

Как только это будет сделано, мы можем создать образ Docker. Нам нужно убедиться, что мы находимся в той же папке, что и Dockerfile, чтобы мы могли запустить эту команду

    docker build -t url-shortener:latest .

-t используется для пометки изображения. В нашем случае это означает, что имя репозитория будет сокращено, а тег будет последним. Тегирование используется для управления версиями изображений. После того, как эта команда выполнена, мы можем убедиться, что мы создали образ с помощью команды

    docker images

Это даст нам что-то вроде этого

На последнем этапе мы должны создать наши изображения. Я говорю изображения, потому что мы также будем запускать сервер MySQL в док-контейнере. Контейнер базы данных будет изолирован от контейнера приложения. Чтобы запустить сервер MySQL в контейнере Docker, просто запустите

    $ docker run --name shortener -e MYSQL_ROOT_PASSWORD=my-secret-pw -d -p 3306:3306 mysql:8

Вы можете ознакомиться с документацией в концентраторе Docker.

Когда у нас есть база данных, работающая внутри контейнера, нам нужно настроить наше приложение для подключения к этому серверу MySQL. Внутри application.properties установите spring.datasource.url для подключения в контейнер «сокращение».

Поскольку мы внесли некоторые изменения в наш проект, необходимо упаковать наш проект в файл .jar с помощью Maven и снова собрать образ Docker из файла Docker.

Теперь, когда у нас есть образ Docker, нам нужно запустить наш контейнер. Мы сделаем это с помощью команды

    docker run -d --name url-shortener-api -p 8080:8080 --link shortener url-shortener

-d означает, что контейнер Docker работает в фоновом режиме вашего терминала. –name позволяет указать имя вашего контейнера

-p host-port:docker-port — это просто сопоставление портов на вашем локальном компьютере с портами внутри контейнера. В этом случае мы открыли порт 8080 внутри контейнера и решили сопоставить его с нашим локальным портом 8080.

-link с этим мы связываем наш контейнер приложения с контейнером базы данных, чтобы позволить контейнерам обнаруживать друг друга и безопасно передавать информацию об одном контейнере в другой контейнер.

Важно знать, что этот флаг теперь является устаревшим и будет удален в ближайшем будущем. Вместо ссылок нам нужно будет создать сеть для облегчения связи между двумя контейнерами.

url-shortener — имя образа докера, который мы хотим запустить.

На этом мы закончили — в браузере перейдите на http://localhost:8080/swagger-ui.html< /p>

Теперь вы можете публиковать свои образы в DockerHub и легко запускать свое приложение на любом компьютере или сервере.

Есть еще две вещи, о которых я хочу рассказать, чтобы улучшить работу с Docker. Один из них представляет собой многоэтапную сборку, а другой — компоновку с помощью docker.

Многоэтапная сборка

С многоэтапными сборками вы можете использовать несколько FROM в вашем Dockerfile. Каждая инструкция FROM может использовать разные базы, и каждая из них начинает новый этап сборки. Вы можете выборочно копировать артефакты с одного этапа на другой, оставляя все ненужное в конечном изображении.

Многоэтапные сборки хороши для нас, чтобы избежать ручного создания файлов .jar каждый раз, когда мы вносим какие-то изменения в наш код. При многоэтапных сборках мы можем определить один этап сборки, который будет выполнять команду пакета Maven, а другой этап будет копировать результат первой сборки в файловую систему контейнера Docker.

Вы можете увидеть полный файл Dockerfile здесь.

Компоновка Docker

Compose – это инструмент для определения и запуска многоконтейнерных приложений Docker. С Compose вы используете файл YAML для настройки служб вашего приложения. Затем с помощью одной команды вы создаете и запускаете все службы из своей конфигурации.

С помощью docker-compose мы упакуем наше приложение и базу данных в один файл конфигурации, а затем запустим все сразу. Таким образом, мы избегаем запуска контейнера MySQL и последующего связывания его с контейнером приложения каждый раз.

Docker-compose.yml говорит сам за себя — сначала мы настроим контейнер MySQL, установив образ mysql v8.0 и учетные данные для сервера MySQL. После этого мы настраиваем контейнер приложения, задавая параметры сборки, потому что нам нужно построить образ, а не вытягивать его, как мы делали с MySQL. Кроме того, нам нужно настроить зависимость контейнера приложения от контейнера MySQL.

Теперь мы можем запустить весь проект всего одной командой:

docker-compose up

Запланированное событие MySQL

Эта часть необязательна, но я думаю, что кому-то она все равно может оказаться полезной. Я говорил о сроке действия короткой ссылки, который может быть задан пользователем или иметь значение по умолчанию. Для этой проблемы мы можем установить запланированное событие в нашей базе данных. Это событие будет выполняться каждые x минут и удалит из базы данных все строки, срок действия которых меньше текущего времени. Просто как тот. Это хорошо работает с небольшим объемом данных в базе данных.

Теперь мне нужно предупредить вас о нескольких проблемах с этим решением.

* Первый — это событие удалит записи из базы данных, но не удалит данные из кеша. Как мы уже говорили ранее, кеш не будет заглядывать внутрь базы данных, если сможет найти там совпадающие данные. Таким образом, даже если данных больше нет в базе данных из-за того, что мы их удалили, мы все равно можем получить их из кеша. * Второй — в моем примере сценария я установите это событие для запуска каждые 2 минуты. Если наша база данных станет огромной, может случиться так, что событие не завершит выполнение в течение своего интервала планирования, результатом может быть одновременное выполнение нескольких экземпляров события.

Заключение

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


Оригинал