Implementar un servicio de acortamiento de URL no es una tarea compleja y, a menudo, es parte de las entrevistas de diseño del sistema . En este post, intentaré explicar el proceso de implementación del servicio. Un acortador de URL es un servicio que se utiliza para crear enlaces cortos a partir de URL muy largas.
Por lo general, los enlaces cortos tienen un tamaño de un tercio o incluso un cuarto de la URL original, lo que los hace más fáciles de escribir, presentar o twittear. Al hacer clic en el enlace corto, el usuario será redirigido automáticamente a la URL original. Hay muchos servicios de acortamiento de URL disponibles en línea, como tiny.cc, bitly.com, cutt.ly, etc.
Antes de la implementación, siempre es una buena idea anotar lo que se necesita hacer en forma de requisitos funcionales y no funcionales.
Digamos que queremos tener un enlace corto con una longitud máxima de 7. Lo más importante en un acortador de URL es el algoritmo de conversión. La conversión de URL se puede implementar de varias formas diferentes, y cada forma tiene sus pros y sus contras.
Una forma de generar enlaces cortos sería aplicar hash a la URL original con alguna función hash (por ejemplo, MD5 o SHA-2 ). Cuando se usa una función hash, es seguro que diferentes entradas darán como resultado diferentes salidas. El resultado del hash tiene más de siete caracteres, por lo que tendríamos que tomar los primeros siete caracteres. Pero, en este caso, podría haber una colisión porque los primeros siete caracteres ya podrían estar en uso como un enlace corto. Luego, tomamos los siguientes siete caracteres, hasta encontrar un enlace corto que no se usa.
La segunda forma de generar un enlace corto es mediante el uso de UUID . La probabilidad de que un UUID se duplique no es cero, pero es lo suficientemente cercana a cero como para ser insignificante. Dado que un UUID tiene 36 caracteres, eso significa que tenemos el mismo problema que el anterior. Deberíamos tomar los primeros siete caracteres y verificar si esa combinación ya está en uso.
La tercera opción sería convertir números de base 10 a base 62. Una base es una cantidad de dígitos o caracteres que se pueden usar para representar un número en particular. La base 10 son dígitos [0-9] que usamos en la vida cotidiana y la base 62 son [0-9][az][AZ]. Esto quiere decir que, por ejemplo, un número en base 10 con cuatro dígitos sería el mismo número en base 62 pero con dos caracteres.
El uso de la base 62 en la conversión de URL con una longitud máxima de siete caracteres nos permite tener 62^7 valores únicos para enlaces cortos.
Entonces, ¿cómo funcionan las conversiones de base 62?
Tenemos un número de base 10 que queremos convertir a base 62. Vamos a utilizar el siguiente algoritmo:
while(number > 0) remainder = number % 62 number = number / 62 attach remainder to start of result collection
Después de eso, solo necesitamos asignar los números de la colección de resultados al Alfabeto base 62 = [0,1,2,…,a,b,c…,A,B,C,…].
Veamos cómo funciona esto con un ejemplo real. En este ejemplo, vamos a convertir 1000 de base 10 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
Mapear [16,8] a la base 62 sería g8. Esto significa que 1000base10 = g8base62.
La conversión de base 62 a base 10 también es simple:
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++
ejemplo 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
Nota: La solución completa está en mi Github . Implementé este servicio usando Spring Boot y MySQL.
Vamos a utilizar la función de incremento automático de nuestra base de datos. El número de incremento automático se utilizará para la conversión de base 62. Puede usar cualquier otra base de datos que tenga una función de incremento automático.
Primero, visite Spring initializr y seleccione Spring Web y MySql Driver. Después de eso, haga clic en el botón Generar y descargue el archivo zip. Descomprima el archivo y abra el proyecto en su IDE favorito. Cada vez que empiezo un nuevo proyecto, me gusta crear algunas carpetas para dividir lógicamente mi código. Mis carpetas en este caso son controlador, entidad, servicio, repositorio, dto y config.
Dentro de la carpeta de la entidad, vamos a crear una clase Url.java con cuatro atributos: id, longUrl, createdDate, expiresDate.
Tenga en cuenta que no hay un atributo de enlace corto. No guardaremos enlaces cortos. Vamos a convertir el atributo id de base 10 a base 62 cada vez que haya una solicitud GET. De esta manera, estamos ahorrando espacio en nuestra base de datos.
El atributo LongUrl es la URL a la que debemos redirigir una vez que un usuario accede a un enlace corto. La fecha de creación es solo para ver cuándo se guarda longUrl (no es importante) y expiresDate está allí si un usuario desea que un enlace corto no esté disponible después de un tiempo.
A continuación, creemos un BaseService .java en la carpeta del servicio. BaseService contiene métodos para convertir de base 10 a base 62 y viceversa.
private static final String allowedString = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; private char[] allowedCharacters = allowedString.toCharArray(); private int base = allowedCharacters.length;
Como mencioné antes, si queremos usar conversiones de base 62, necesitamos tener un alfabeto de base 62, que en este caso se llama caracteres permitidos. Además, el valor de la variable base se calcula a partir de la longitud de los caracteres permitidos en caso de que queramos cambiar los caracteres permitidos.
El método de codificación toma un número como entrada y devuelve un enlace corto. El método de decodificación toma una cadena (enlace corto) como entrada y devuelve un número. Los algoritmos deben implementarse como se explicó anteriormente.
Después de eso, dentro de la carpeta del repositorio, creemos el archivo UrlRepository .java , que es solo una extensión de JpaRepository y nos brinda muchos métodos como 'findById', 'save', etc. No necesitamos agregar nada más. a esto.
Luego, creemos un archivo UrlController.java en la carpeta del controlador. El controlador debe tener un método POST para crear enlaces cortos y un método GET para redirigir a la 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(); }
El método POST tiene UrlLongRequest como cuerpo de solicitud. Es solo una clase con atributos longUrl y expiresDate.
El método GET toma una URL corta como variable de ruta y luego obtiene y redirige a la URL original. En la parte superior del controlador, se inyecta UrlService como una dependencia, que se explicará a continuación.
UrlService .java es donde se encuentra la mayor parte de la lógica y es el servicio utilizado por el controlador.
ConvertToShortUrl es utilizado por el método POST del controlador. Simplemente crea un nuevo registro en la base de datos y obtiene una identificación. Luego, la ID se convierte en un enlace corto de base 62 y se devuelve al controlador.
GetOriginalUrl es un método utilizado por el método GET del controlador. Primero convierte una cadena a base 10, y el resultado es una identificación. Luego obtiene un registro de la base de datos con esa identificación y lanza una excepción si no existe. Después de eso, devuelve la URL original al controlador.
En esta parte, hablaré sobre la documentación de Swagger, la dockerización de la aplicación, el caché de la aplicación y el evento programado de MySql.
Cada vez que desarrollas una API, es bueno documentarla de alguna manera. La documentación hace que las API sean más fáciles de entender y usar. La API para este proyecto está documentada mediante la interfaz de usuario de Swagger.
La interfaz de usuario de Swagger permite que cualquier persona visualice e interactúe con los recursos de la API sin tener implementada ninguna lógica de implementación.
Se genera automáticamente, con documentación visual que facilita la implementación de back-end y el consumo del lado del cliente.
Hay varios pasos que debemos seguir para incluir Swagger UI en el proyecto.
Primero, debemos agregar las dependencias de Maven al archivo 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 su referencia, puede ver el archivo pom.xml completo aquí . Después de agregar las dependencias de Maven, es hora de agregar la configuración de Swagger. Dentro de la carpeta de configuración, necesitamos crear una nueva clase: 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(); } }
En la parte superior de la clase, necesitamos agregar un par de anotaciones.
@Configuration indica que una clase declara uno o más métodos @Beans y puede ser procesada por el contenedor Spring para generar definiciones de beans y solicitudes de servicio para esos beans en tiempo de ejecución.
@EnableSwagger2 indica que se debe habilitar la compatibilidad con Swagger.
A continuación, debemos agregar el bean Docket que proporciona la configuración principal de la API con valores predeterminados sensibles y métodos convenientes para la configuración.
El método apiInfo() toma el objeto ApiInfo donde podemos configurar toda la información API necesaria; de lo contrario, usa algunos valores predeterminados. Para que el código sea más limpio, debemos crear un método privado que configure y devuelva el objeto ApiInfo y pase ese método como parámetro del método apiInfo() . En este caso, es el método metadata() .
El método apis() nos permite filtrar paquetes que se están documentando.
La interfaz de usuario de Swagger está configurada y podemos comenzar a documentar nuestra API. Dentro de UrlController , sobre cada punto final, podemos usar la anotación @ApiOperation para agregar una descripción. Dependiendo de sus necesidades, puede utilizar otras anotaciones .
También es posible documentar los DTO utilizando @ApiModelProperty, lo que le permite agregar valores permitidos, descripciones, etc.
Según Wikipedia, un [caché](https://en.wikipedia.org/wiki/Cache_(computing) es un componente de hardware o software que almacena datos para que las futuras solicitudes de esos datos puedan atenderse más rápido; los datos almacenados en un caché puede ser el resultado de un cálculo anterior o una copia de datos almacenados en otro lugar.
El tipo de caché que se usa con más frecuencia es un caché en memoria que almacena datos en caché en la RAM. Cuando se solicitan datos y se encuentran en la memoria caché, se sirven desde la RAM en lugar de desde una base de datos. De esta manera, evitamos llamar al backend costoso cuando un usuario solicita datos.
Un acortador de URL es un tipo de aplicación que tiene más solicitudes de lectura que de escritura, lo que significa que es una aplicación ideal para usar el caché.
Para habilitar el almacenamiento en caché en la aplicación Spring Boot, solo necesitamos agregar la anotación @EnableCaching en la clase UrlShortenerApiApplication .
Después de eso, en el controlador , debemos configurar la anotación @Cachable sobre el método GET. Esta anotación almacena automáticamente los resultados del método denominado caché. En la anotación @Cachable, establecemos el parámetro de valor , que es el nombre del caché, y el parámetro clave , que es la clave del caché.
En este caso, para la clave de caché, vamos a usar 'shortUrl' porque estamos seguros de que es única. Los parámetros de sincronización se establecen en verdadero para garantizar que solo un subproceso genere el valor de caché.
Y eso es todo: nuestro caché está configurado y cuando cargamos la URL por primera vez con algún enlace corto, el resultado se guardará en el caché y cualquier llamada adicional al punto final con el mismo enlace corto recuperará el resultado del caché en lugar de de la base de datos
La dockerización es el proceso de empaquetar una aplicación y sus dependencias en un contenedor [Docker](https://en.wikipedia.org/wiki/Docker_(software). Una vez que configuramos el contenedor Docker, podemos ejecutar fácilmente la aplicación en cualquier servidor o computadora que admita Docker.
Lo primero que debemos hacer es crear un Dockerfile.
Un Dockerfile es un archivo de texto que contiene todos los comandos que un usuario podría llamar en la línea de comandos para ensamblar una imagen.
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"]
DESDE : aquí es donde establecemos la imagen base para la base de compilación. Vamos a utilizar OpenJDK v13, que es una versión gratuita y de código abierto de Java. Puede encontrar otras imágenes para su imagen base en Docker Hub, que es un lugar para compartir imágenes de Docker.
COPIAR : este comando copia archivos del sistema de archivos local (su computadora) al sistema de archivos del contenedor en la ruta que especificamos. Vamos a copiar el archivo JAR de la carpeta de destino a la carpeta /usr/src/app en el contenedor. Explicaré la creación del archivo JAR un poco más tarde.
EXPOSE : instrucción que informa a Docker que el contenedor escucha los puertos de red especificados en tiempo de ejecución. El protocolo predeterminado es TCP y puede especificar si desea utilizar UDP.
PUNTO DE ENTRADA: esta instrucción le permite configurar un contenedor que se ejecutará como un ejecutable. Aquí debemos especificar cómo Docker se quedará sin aplicaciones.
El comando para ejecutar una aplicación desde el archivo .jar es
java -jar <app_name>.jar
entonces ponemos esas 3 palabras en una matriz y eso es todo.
Ahora que tenemos Dockerfile, debemos construir la imagen a partir de él. Pero como mencioné antes, primero debemos crear un archivo .jar desde nuestro proyecto para que el comando COPIAR en Dockerfile pueda funcionar correctamente. Para crear un .jar ejecutable vamos a usar maven .
Necesitamos asegurarnos de tener a Maven dentro de nuestro pom .xml . Si falta Maven, podemos agregarlo
<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
Después de eso, deberíamos ejecutar el comando
mvn clean package
Una vez hecho esto, podemos construir una imagen de Docker. Necesitamos asegurarnos de que estamos en la misma carpeta donde está el Dockerfile para que podamos ejecutar este comando
docker build -t url-shortener:latest .
-t se usa para etiquetar una imagen. En nuestro caso, eso significa que el nombre del repositorio será url-shortener y una etiqueta será la última. El etiquetado se utiliza para el control de versiones de las imágenes. Después de que se complete ese comando, podemos asegurarnos de que creamos una imagen con el comando
docker images
Eso nos dará algo como esto.
Para el último paso, debemos construir nuestras imágenes. Digo imágenes porque también ejecutaremos el servidor MySQL en un contenedor docker. El contenedor de la base de datos estará aislado del contenedor de la aplicación. Para ejecutar el servidor MySQL en el contenedor acoplable, simplemente ejecute
$ docker run --name shortener -e MYSQL_ROOT_PASSWORD=my-secret-pw -d -p 3306:3306 mysql:8
Puede ver la documentación en Docker Hub .
Cuando tenemos una base de datos ejecutándose dentro de un contenedor, necesitamos configurar nuestra aplicación para conectarse a ese servidor MySQL. Dentro de application.properties configure spring.datasource.url para conectarse al contenedor 'acortador'.
Dado que hicimos algunos cambios en nuestro proyecto, es necesario empaquetar nuestro proyecto en un archivo .jar usando Maven y compilar la imagen de Docker desde Dockerfile nuevamente.
Ahora que tenemos una imagen de Docker, necesitamos ejecutar nuestro contenedor. Lo haremos con el comando
docker run -d --name url-shortener-api -p 8080:8080 --link shortener url-shortener
-d significa que un contenedor Docker se ejecuta en segundo plano en su terminal. –name le permite establecer el nombre de su contenedor
-p host-port:docker-port : esto es simplemente asignar puertos en su computadora local a puertos dentro del contenedor. En este caso, expusimos el puerto 8080 dentro de un contenedor y decidimos asignarlo a nuestro puerto local 8080.
–enlazar con esto vinculamos nuestro contenedor de aplicaciones con el contenedor de la base de datos para permitir que los contenedores se descubran entre sí y transfieran información de forma segura sobre un contenedor a otro contenedor.
Es importante saber que esta bandera ahora es un legado y será removida en un futuro cercano. En lugar de enlaces, necesitaríamos crear una red para facilitar la comunicación entre los dos contenedores.
url-shortener : es el nombre de la imagen acoplable que queremos ejecutar.
Y con esto, hemos terminado: en el navegador, visite http://localhost:8080/swagger-ui.html
Ahora puede publicar sus imágenes en DockerHub y ejecutar fácilmente su aplicación en cualquier computadora o servidor.
Hay dos cosas más de las que quiero hablar para mejorar nuestra experiencia Docker. Una es una compilación de varias etapas y la otra es docker-compose.
Con compilaciones de varias etapas , puede usar varias instrucciones FROM en su Dockerfile. Cada instrucción FROM puede usar una base diferente, y cada una de ellas comienza una nueva etapa de la construcción. Puede copiar artefactos de forma selectiva de una etapa a otra, dejando atrás todo lo que no desea en la imagen final.
Las compilaciones de varias etapas son buenas para nosotros para evitar la creación manual de archivos .jar cada vez que hacemos algunos cambios en nuestro código. Con compilaciones de varias etapas, podemos definir una etapa de la compilación que ejecutará el comando del paquete Maven y la otra etapa copiará el resultado de la primera compilación en el sistema de archivos de un contenedor Docker.
Puedes ver el Dockerfile completo aquí .
Compose es una herramienta para definir y ejecutar aplicaciones Docker de varios contenedores. Con Compose, utiliza un archivo YAML para configurar los servicios de su aplicación. Luego, con un solo comando, crea e inicia todos los servicios desde su configuración.
Con docker-compose, empaquetaremos nuestra aplicación y base de datos en un solo archivo de configuración y luego ejecutaremos todo a la vez. De esta forma evitamos ejecutar el contenedor MySQL y luego vincularlo al contenedor de la aplicación cada vez.
Docker -compose.yml se explica por sí mismo: primero, configuramos el contenedor MySQL configurando la imagen mysql v8.0 y las credenciales para el servidor MySQL. Después de eso, configuramos el contenedor de la aplicación estableciendo parámetros de compilación porque necesitamos compilar una imagen en lugar de extraerla como hicimos con MySQL. Además, debemos configurar que el contenedor de la aplicación dependa del contenedor MySQL.
Ahora podemos ejecutar todo el proyecto con un solo comando:
docker-compose up
Esta parte es opcional , pero creo que alguien podría encontrarla útil de todos modos. Hablé sobre la fecha de vencimiento del enlace corto que puede ser definido por el usuario o algún valor predeterminado. Para este problema, podemos configurar un evento programado dentro de nuestra base de datos. Este evento se ejecutará cada x minutos y eliminará cualquier fila de la base de datos donde la fecha de vencimiento sea anterior a la hora actual. Simple como eso. Esto funciona bien en una pequeña cantidad de datos en la base de datos.
Ahora necesito advertirle sobre un par de problemas con esta solución.
Espero que esta publicación te haya ayudado un poco a tener una idea general sobre cómo crear un servicio de acortador de URL. Puedes tomar esta idea y mejorarla. Escriba algunos requisitos funcionales nuevos e intente implementarlos. Si tiene alguna pregunta, puede publicarla debajo de esta publicación.