paint-brush
Como encurtar URLs: guia passo a passo de Java e Springpor@marinsborg
3,555 leituras
3,555 leituras

Como encurtar URLs: guia passo a passo de Java e Spring

por marinsborg2022/06/06
Read on Terminal Reader
Read this story w/o Javascript

Muito longo; Para ler

Encurtador de URL implementado com Java e Spring Boot. O tutorial cobre tudo - solicitações funcionais e não funcionais, o que é conversão base64, como criar um novo projeto e como implementar todas as etapas para o encurtador de URL. No final é explicado como dockerizar a solução.

Companies Mentioned

Mention Thumbnail
Mention Thumbnail
featured image - Como encurtar URLs: guia passo a passo de Java e Spring
marinsborg HackerNoon profile picture


A implementação de um serviço de encurtamento de URL não é uma tarefa complexa e geralmente faz parte das entrevistas de design do sistema . Neste post, tentarei explicar o processo de implementação do serviço. Um encurtador de URL é um serviço usado para criar links curtos a partir de URLs muito longos.


Normalmente, os links curtos têm o tamanho de um terço ou até um quarto do URL original, o que os torna mais fáceis de digitar, apresentar ou twittar. Ao clicar no link curto, o usuário será redirecionado automaticamente para a URL original. Existem muitos serviços de encurtamento de URL disponíveis online, como tiny.cc, bitly.com, cutt.ly, etc.


Teoria

Antes da implementação, é sempre uma boa ideia anotar o que precisa ser feito na forma de requisitos funcionais e não funcionais.


Requisitos funcionais

  • Os usuários precisam ser capazes de inserir um URL longo. Nosso serviço deve salvar esse URL e gerar um link curto.
  • Clicar no link curto deve redirecionar o usuário para o URL longo original.
  • Os usuários devem ter a opção de inserir a data de expiração. Uma vez que essa data tenha passado, o link curto deve ser inválido.
  • Os usuários devem criar uma conta para usar o serviço. Os serviços podem ter um limite de uso por usuário (opcional)
  • O usuário pode criar seu próprio link curto - O serviço deve ter métricas, por exemplo, links mais visitados (opcional)


requisitos não Funcionais

  • O serviço deve estar funcionando 100% do tempo
  • O redirecionamento não deve durar mais de dois segundos


conversão de URL

Digamos que queremos um link curto com comprimento máximo de 7. O mais importante em um encurtador de URL é o algoritmo de conversão. A conversão de URL pode ser implementada de várias maneiras diferentes, e cada uma tem seus prós e contras.


Uma forma de gerar links curtos seria fazer o hash da URL original com alguma função de hash (por exemplo, MD5 ou SHA-2 ). Ao usar uma função hash, é certo que diferentes entradas resultarão em diferentes saídas. O resultado do hash tem mais de sete caracteres, então precisaríamos pegar os primeiros sete caracteres. Mas, neste caso, pode haver uma colisão porque os primeiros sete caracteres já podem estar sendo usados como um link curto. Então, pegamos os próximos sete caracteres, até encontrarmos um link curto que não é usado.


A segunda forma de gerar um link curto é usando UUIDs . A probabilidade de que um UUID seja duplicado não é zero, mas é próxima o suficiente de zero para ser insignificante. Como um UUID tem 36 caracteres, isso significa que temos o mesmo problema acima. Devemos pegar os sete primeiros caracteres e verificar se essa combinação já está em uso.


A terceira opção seria converter números da base 10 para a base 62. Uma base é um número de dígitos ou caracteres que podem ser usados para representar um determinado número. A base 10 são dígitos [0-9] que usamos na vida cotidiana e a base 62 são [0-9][az][AZ]. Isso significa que, por exemplo, um número na base 10 com quatro dígitos seria o mesmo número na base 62, mas com dois caracteres.


Usar a base 62 na conversão de URL com um comprimento máximo de sete caracteres nos permite ter 62^7 valores exclusivos para links curtos.


Então, como funcionam as conversões de base 62?

Temos um número de base 10 que queremos converter para base 62. Vamos usar o seguinte algoritmo:


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


Depois disso, basta mapear os números da coleção de resultados para a base 62 Alfabeto = [0,1,2,…,a,b,c…,A,B,C,…].


Vamos ver como isso funciona com um exemplo real. Neste exemplo, vamos converter 1000 da base 10 para a base 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


O mapeamento [16,8] para a base 62 seria g8. Isso significa que 1000base10 = g8base62.


A conversão da base 62 para a base 10 também é simples:


 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++


Exemplo real:


 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


Implementação

Nota: Toda a solução está no meu Github . Eu implementei este serviço usando Spring Boot e MySQL.


Vamos usar o recurso de auto-incremento do nosso banco de dados. O número de incremento automático será usado para conversão de base 62. Você pode usar qualquer outro banco de dados que tenha um recurso de incremento automático.


Primeiro, visite Spring initializr e selecione Spring Web and MySql Driver. Depois disso, clique no botão Gerar e baixe o arquivo zip. Descompacte o arquivo e abra o projeto em seu IDE favorito. Sempre que começo um novo projeto, gosto de criar algumas pastas para dividir logicamente meu código. Minhas pastas neste caso são controlador, entidade, serviço, repositório, dto e configuração.


Dentro da pasta da entidade, vamos criar uma classe Url.java com quatro atributos: id, longUrl, createdDate, expiresDate.


Observe que não há nenhum atributo de link curto. Não salvaremos links curtos. Vamos converter o atributo id da base 10 para a base 62 toda vez que houver uma solicitação GET. Desta forma, estamos economizando espaço em nosso banco de dados.


O atributo LongUrl é a URL para a qual devemos redirecionar quando um usuário acessa um link curto. A data criada é apenas para ver quando o longUrl é salvo (não é importante) e expiresDate está lá se um usuário quiser tornar um link curto indisponível depois de algum tempo.


Em seguida, vamos criar um BaseService .java na pasta do serviço. BaseService contém métodos para converter da base 10 para a base 62 e vice-versa.


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


Como mencionei antes, se quisermos usar conversões de base 62, precisamos ter um alfabeto de base 62, que neste caso é chamado de allowCharacters. Além disso, o valor da variável base é calculado a partir do comprimento dos caracteres permitidos caso desejemos alterar os caracteres permitidos.


O método encode recebe um número como entrada e retorna um link curto. O método decode recebe uma string (link curto) como entrada e retorna um número. Os algoritmos devem ser implementados conforme explicado acima.


Depois disso, dentro da pasta do repositório, vamos criar o arquivo UrlRepository .java , que é apenas uma extensão do JpaRepository e nos dá vários métodos como 'findById', 'save', etc. para isso.


Então, vamos criar um arquivo UrlController.java na pasta do controlador. O controlador deve ter um método POST para criar links curtos e um método GET para redirecionar para a URL original.


 @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(); }


O método POST tem UrlLongRequest como corpo da solicitação. É apenas uma classe com os atributos longUrl e expiresDate.


O método GET usa uma URL curta como uma variável de caminho e, em seguida, obtém e redireciona para a URL original. Na parte superior do controlador, UrlService é injetado como uma dependência, que será explicada a seguir.


UrlService .java é onde está a maior parte da lógica e é o serviço usado pelo controlador.


ConvertToShortUrl é usado pelo método POST do controlador. Ele apenas cria um novo registro no banco de dados e obtém um ID. O ID é então convertido em um link curto de base 62 e retornado ao controlador.


GetOriginalUrl é um método usado pelo método GET do controlador. Ele primeiro converte uma string em base 10, e o resultado disso é um id. Em seguida, ele obtém um registro do banco de dados com esse id e lança uma exceção se ele não existir. Depois disso, ele retorna a URL original para o controlador.


Tópicos 'avançados'

Nesta parte, falarei sobre a documentação do swagger, dockerização do aplicativo, cache do aplicativo e evento agendado do MySql.


Swagger UI

Toda vez que você desenvolve uma API, é bom documentá-la de alguma forma. A documentação torna as APIs mais fáceis de entender e usar. A API para este projeto é documentada usando Swagger UI.

Swagger UI permite que qualquer pessoa visualize e interaja com os recursos da API sem ter nenhuma lógica de implementação em vigor.


Ele é gerado automaticamente, com documentação visual que facilita a implementação de back-end e o consumo do lado do cliente.


Existem várias etapas que precisamos seguir para incluir a interface do usuário do Swagger no projeto.

Primeiro, precisamos adicionar as dependências do Maven ao arquivo 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>


Para sua referência, você pode ver o arquivo pom.xml completo aqui . Depois de adicionar as dependências do Maven, é hora de adicionar a configuração do Swagger. Dentro da pasta config, precisamos criar uma nova classe – 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(); } }


No início da classe, precisamos adicionar algumas anotações.

@Configuration indica que uma classe declara um ou mais métodos @Beans e pode ser processada pelo contêiner Spring para gerar definições de bean e solicitações de serviço para esses beans em tempo de execução.


@EnableSwagger2 indica que o suporte Swagger deve ser ativado.


Em seguida, devemos adicionar o bean Docket, que fornece a configuração primária da API com padrões sensatos e métodos convenientes para configuração.


O método apiInfo() leva o objeto ApiInfo onde podemos configurar todas as informações necessárias da API – caso contrário, ele usa alguns valores padrão. Para deixar o código mais limpo, devemos fazer um método private que irá configurar e retornar o objeto ApiInfo e passar esse método como parâmetro do método apiInfo() . Neste caso, é o método metadata() .


O método apis() nos permite filtrar os pacotes que estão sendo documentados.


Swagger UI está configurado e podemos começar a documentar nossa API. Dentro de UrlController , acima de cada endpoint, podemos usar a anotação @ApiOperation para adicionar uma descrição. Dependendo de suas necessidades, você pode usar algumas outras anotações .


Também é possível documentar DTOs usando @ApiModelProperty, que permite adicionar valores permitidos, descrições, etc.


Cache

De acordo com a Wikipedia, um [cache](https://en.wikipedia.org/wiki/Cache_(computing) é um componente de hardware ou software que armazena dados para que solicitações futuras desses dados possam ser atendidas mais rapidamente; os dados armazenados em um cache pode ser o resultado de uma computação anterior ou uma cópia dos dados armazenados em outro lugar.


O tipo de cache usado com mais frequência é um cache na memória que armazena dados armazenados em cache na RAM. Quando os dados são solicitados e encontrados no cache, eles são servidos da RAM em vez de um banco de dados. Dessa forma, evitamos chamadas de back-end caras quando um usuário solicita dados.

Um encurtador de URL é um tipo de aplicativo que possui mais solicitações de leitura do que de gravação, o que significa que é um aplicativo ideal para usar o cache.


Para habilitar o cache no aplicativo Spring Boot, basta adicionar a anotação @EnableCaching na classe UrlShortenerApiApplication .


Depois disso, no controlador , precisamos definir a anotação @Cachable acima do método GET. Essa anotação armazena automaticamente os resultados do método chamado cache. Na anotação @Cachable, definimos o parâmetro value que é o nome do cache e o parâmetro key que é a chave do cache.


Nesse caso, para a chave de cache, usaremos 'shortUrl' porque temos certeza de que é único. Os parâmetros de sincronização são definidos como true para garantir que apenas um único encadeamento esteja construindo o valor do cache.


E é isso - nosso cache está definido e quando carregamos pela primeira vez o URL com algum link curto, o resultado será salvo no cache e qualquer chamada adicional para o terminal com o mesmo link curto recuperará o resultado do cache em vez de do banco de dados.


Dockerização

Dockerization é o processo de empacotar um aplicativo e suas dependências em um contêiner [Docker](https://en.wikipedia.org/wiki/Docker_(software). Depois de configurar o contêiner Docker, podemos executar facilmente o aplicativo em qualquer servidor ou computador compatível com Docker.

A primeira coisa que precisamos fazer é criar um Dockerfile.


Um Dockerfile é um arquivo de texto que contém todos os comandos que um usuário pode chamar na linha de comando para montar uma imagem.


 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 – Aqui é onde definimos a imagem base para a base de construção. Vamos usar o OpenJDK v13, que é uma versão gratuita e de código aberto do Java. Você pode encontrar outras imagens para sua imagem base no hub Docker, que é um local para compartilhar imagens docker.


COPY – Este comando copia arquivos do sistema de arquivos local (seu computador) para o sistema de arquivos do contêiner no caminho que especificamos. Vamos copiar o arquivo JAR da pasta de destino para a pasta /usr/src/app no contêiner. Explicarei a criação do arquivo JAR um pouco mais tarde.


EXPOSE – Instrução que informa ao Docker que o contêiner escuta as portas de rede especificadas em tempo de execução. O protocolo padrão é o TCP e você pode especificar se deseja usar o UDP.


ENTRYPOINT – Esta instrução permite configurar um container que será executado como um executável. Aqui precisamos especificar como o Docker ficará sem aplicativos.


O comando para executar um aplicativo a partir do arquivo .jar é


 java -jar <app_name>.jar


então colocamos essas 3 palavras em uma matriz e é isso.


Agora que temos o Dockerfile, devemos construir a imagem a partir dele. Mas, como mencionei antes, primeiro precisamos criar o arquivo .jar de nosso projeto para que o comando COPY no Dockerfile funcione corretamente. Para criar um .jar executável vamos usar o maven .


Precisamos ter certeza de que temos o Maven dentro de nosso pom .xml . Se o Maven estiver faltando, podemos adicioná-lo


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


Depois disso, devemos apenas executar o comando


 mvn clean package


Feito isso, podemos criar uma imagem do Docker. Precisamos ter certeza de que estamos na mesma pasta onde está o Dockerfile para que possamos executar este comando


 docker build -t url-shortener:latest .

-t é usado para marcar uma imagem. No nosso caso, isso significa que o nome do repositório será url-shortener e uma tag será a mais recente. A marcação é usada para o controle de versão das imagens. Após a execução desse comando, podemos garantir que criamos uma imagem com o comando

 docker images

Isso nos dará algo assim

Para a última etapa, devemos construir nossas imagens. Digo imagens porque também executaremos o servidor MySQL em um contêiner docker. O contêiner do banco de dados será isolado do contêiner do aplicativo. Para executar o servidor MySQL no contêiner docker, basta executar


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


Você pode ver a documentação no hub do Docker .


Quando temos um banco de dados rodando dentro de um container, precisamos configurar nossa aplicação para se conectar a esse servidor MySQL. Dentro de application.properties, defina spring.datasource.url para se conectar ao contêiner 'shortener'.


Como fizemos algumas alterações em nosso projeto, é necessário compactar nosso projeto em um arquivo .jar usando Maven e criar a imagem do Docker a partir do Dockerfile novamente.


Agora que temos uma imagem do Docker, precisamos executar nosso contêiner. Faremos isso com o comando


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


-d significa que um contêiner do Docker é executado em segundo plano no seu terminal. –name permite definir o nome do seu contêiner


-p host-port:docker-port – Isso é simplesmente mapear portas em seu computador local para portas dentro do contêiner. Nesse caso, expusemos a porta 8080 dentro de um contêiner e decidimos mapeá-la para nossa porta local 8080.


–link com isso, vinculamos nosso contêiner de aplicativo ao contêiner de banco de dados para permitir que os contêineres se descubram e transfiram com segurança as informações de um contêiner para outro contêiner.

É importante saber que esta bandeira agora é um legado e será removida em um futuro próximo. Ao invés de links, precisaríamos criar uma rede para facilitar a comunicação entre os dois containers.


url-shortener – é o nome da imagem docker que queremos executar.


E com isso, terminamos - no navegador, visite http://localhost:8080/swagger-ui.html

Agora você pode publicar suas imagens no DockerHub e executar facilmente seu aplicativo em qualquer computador ou servidor.


Há mais duas coisas sobre as quais quero falar para melhorar nossa experiência com o Docker. Um é uma compilação de vários estágios e o outro é docker-compose.


Construção em vários estágios

Com compilações de vários estágios , você pode usar várias instruções FROM em seu Dockerfile. Cada instrução FROM pode usar uma base diferente, e cada uma delas inicia uma nova etapa da construção. Você pode copiar seletivamente artefatos de um estágio para outro, deixando para trás tudo o que não deseja na imagem final.


As compilações em vários estágios são boas para evitarmos a criação manual de arquivos .jar toda vez que fizermos algumas alterações em nosso código. Com builds de vários estágios, podemos definir um estágio do build que fará o comando do pacote Maven e o outro estágio copiará o resultado do primeiro build para o sistema de arquivos de um container Docker.


Você pode ver o Dockerfile completo aqui .


Docker-compose

O Compose é uma ferramenta para definir e executar aplicativos Docker de vários contêineres. Com o Compose, você usa um arquivo YAML para configurar os serviços do seu aplicativo. Então, com um único comando, você cria e inicia todos os serviços da sua configuração.


Com o docker-compose, empacotaremos nosso aplicativo e banco de dados em um único arquivo de configuração e executaremos tudo de uma vez. Dessa forma, evitamos executar o contêiner MySQL e, em seguida, vinculá-lo ao contêiner do aplicativo todas as vezes.


Docker - compose .yml é praticamente auto-explicativo – primeiro, configuramos o container MySQL definindo a imagem mysql v8.0 e as credenciais para o servidor MySQL. Depois disso, configuramos o contêiner do aplicativo definindo os parâmetros de compilação porque precisamos criar uma imagem em vez de puxá-la como fizemos com o MySQL. Além disso, precisamos definir que o contêiner do aplicativo depende do contêiner do MySQL.


Agora podemos executar todo o projeto com apenas um comando:


 docker-compose up


Evento agendado do MySQL

Esta parte é opcional , mas acho que alguém pode achar isso útil de qualquer maneira. Falei sobre a data de expiração do link curto que pode ser definido pelo usuário ou algum valor padrão. Para esse problema, podemos definir um evento agendado em nosso banco de dados. Este evento será executado a cada x minutos e excluirá todas as linhas do banco de dados cuja data de expiração seja inferior à hora atual. Simples assim. Isso funciona bem em uma pequena quantidade de dados no banco de dados.


Agora, preciso alertá-lo sobre alguns problemas com esta solução.


  • First – Este evento removerá registros do banco de dados, mas não removerá dados do cache. Como dissemos antes, o cache não procurará dentro do banco de dados se puder encontrar dados correspondentes lá. Portanto, mesmo que os dados não existam mais no banco de dados porque os excluímos, ainda podemos obtê-los do cache.
  • Segundo – No meu script de exemplo , defino esse evento para ser executado a cada 2 minutos. Se nosso banco de dados ficar enorme, pode acontecer que o evento não termine a execução dentro do intervalo de agendamento, o resultado pode ser várias instâncias do evento sendo executadas simultaneamente.


Conclusão

Espero que este post tenha ajudado um pouco você a ter uma ideia geral sobre como criar um serviço de encurtador de URL. Você pode pegar essa ideia e melhorá-la. Anote alguns novos requisitos funcionais e tente implementá-los. Se você tiver alguma dúvida, pode postá-la nesta postagem.