La mise en œuvre d'un service de raccourcissement d'URL n'est pas une tâche complexe et fait souvent partie des entretiens de conception du système . Dans cet article, je vais essayer d'expliquer le processus de mise en œuvre du service. Un raccourcisseur d'URL est un service utilisé pour créer des liens courts à partir d'URL très longues.
Habituellement, les liens courts ont la taille d'un tiers ou même d'un quart de l'URL d'origine, ce qui les rend plus faciles à taper, présenter ou tweeter. En cliquant sur le lien court, l'utilisateur sera automatiquement redirigé vers l'URL d'origine. Il existe de nombreux services de raccourcissement d'URL disponibles en ligne, comme tiny.cc, bitly.com, cutt.ly, etc.
Avant la mise en œuvre, c'est toujours une bonne idée d'écrire ce qu'il faut faire sous la forme d'exigences fonctionnelles et non fonctionnelles.
Disons que nous voulons avoir un lien court d'une longueur maximale de 7. La chose la plus importante dans un raccourcisseur d'URL est l'algorithme de conversion. La conversion d'URL peut être mise en œuvre de plusieurs manières différentes, et chaque manière a ses avantages et ses inconvénients.
Une façon de générer des liens courts serait de hacher l'URL d'origine avec une fonction de hachage (par exemple MD5 ou SHA-2 ). Lors de l'utilisation d'une fonction de hachage, il est certain que différentes entrées entraîneront différentes sorties. Le résultat du hachage est plus long que sept caractères, nous aurions donc besoin de prendre les sept premiers caractères. Mais, dans ce cas, il pourrait y avoir une collision car les sept premiers caractères pourraient déjà être utilisés comme lien court. Ensuite, nous prenons les sept caractères suivants, jusqu'à ce que nous trouvions un lien court qui n'est pas utilisé.
La deuxième façon de générer un lien court consiste à utiliser des UUID . La probabilité qu'un UUID soit dupliqué n'est pas nulle, mais elle est suffisamment proche de zéro pour être négligeable. Puisqu'un UUID a 36 caractères, cela signifie que nous avons le même problème que ci-dessus. Nous devrions prendre les sept premiers caractères et vérifier si cette combinaison est déjà utilisée.
La troisième option consisterait à convertir des nombres de base 10 en base 62. Une base est un nombre de chiffres ou de caractères pouvant être utilisés pour représenter un nombre particulier. La base 10 sont les chiffres [0-9] que nous utilisons dans la vie de tous les jours et la base 62 sont [0-9][az][AZ]. Cela signifie que, par exemple, un nombre en base 10 à quatre chiffres serait le même nombre en base 62 mais avec deux caractères.
L'utilisation de la base 62 dans la conversion d'URL avec une longueur maximale de sept caractères nous permet d'avoir 62^7 valeurs uniques pour les liens courts.
Alors, comment fonctionnent les conversions en base 62 ?
Nous avons un nombre en base 10 que nous voulons convertir en base 62. Nous allons utiliser l'algorithme suivant :
while(number > 0) remainder = number % 62 number = number / 62 attach remainder to start of result collection
Après cela, nous avons juste besoin de mapper les nombres de la collection de résultats à l'Alphabet de base 62 = [0,1,2,…,a,b,c…,A,B,C,…].
Voyons comment cela fonctionne avec un exemple réel. Dans cet exemple, convertissons 1000 de base 10 en 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
Mapper [16,8] en base 62 serait g8. Cela signifie que 1000base10 = g8base62.
La conversion de la base 62 à la base 10 est également 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++
Exemple réel :
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
Remarque : Toute la solution est sur mon Github . J'ai implémenté ce service en utilisant Spring Boot et MySQL.
Nous allons utiliser la fonction d'auto-incrémentation de notre base de données. Le nombre auto-incrémenté va être utilisé pour la conversion en base 62. Vous pouvez utiliser n'importe quelle autre base de données dotée d'une fonction d'auto-incrémentation.
Tout d'abord, visitez Spring initializr et sélectionnez Spring Web et MySql Driver. Après cela, cliquez sur le bouton Générer et téléchargez le fichier zip. Décompressez le fichier et ouvrez le projet dans votre IDE préféré. Chaque fois que je démarre un nouveau projet, j'aime créer des dossiers pour diviser logiquement mon code. Mes dossiers dans ce cas sont controller, entity, service, repository, dto et config.
Dans le dossier de l'entité, créons une classe Url.java avec quatre attributs : id, longUrl, createdDate, expiresDate.
Notez qu'il n'y a pas d'attribut de lien court. Nous n'enregistrerons pas de liens courts. Nous allons convertir l'attribut id de base 10 en base 62 à chaque requête GET. De cette façon, nous économisons de l'espace dans notre base de données.
L'attribut LongUrl est l'URL vers laquelle nous devons rediriger une fois qu'un utilisateur accède à un lien court. La date de création est juste pour voir quand le longUrl est enregistré (ce n'est pas important) et expiresDate est là si un utilisateur veut rendre un lien court indisponible après un certain temps.
Ensuite, créons un BaseService .java dans le dossier de service. BaseService contient des méthodes pour convertir de la base 10 à la base 62 et vice versa.
private static final String allowedString = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789"; private char[] allowedCharacters = allowedString.toCharArray(); private int base = allowedCharacters.length;
Comme je l'ai mentionné précédemment, si nous voulons utiliser des conversions en base 62, nous devons disposer d'un alphabet en base 62, qui dans ce cas s'appelle des caractères autorisés. De plus, la valeur de la variable de base est calculée à partir de la longueur des caractères autorisés au cas où nous voudrions modifier les caractères autorisés.
La méthode encode prend un nombre en entrée et renvoie un lien court. La méthode de décodage prend une chaîne (lien court) en entrée et renvoie un nombre. Les algorithmes doivent être mis en œuvre comme expliqué ci-dessus.
Après cela, dans le dossier du référentiel, créons le fichier UrlRepository .java , qui est juste une extension de JpaRepository et il nous donne beaucoup de méthodes comme 'findById', 'save', etc. Nous n'avons pas besoin d'ajouter autre chose pour ça.
Ensuite, créons un fichier UrlController.java dans le dossier du contrôleur. Le contrôleur doit avoir une méthode POST pour créer des liens courts et une méthode GET pour rediriger vers l'URL d'origine.
@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(); }
La méthode POST a UrlLongRequest comme corps de requête. C'est juste une classe avec les attributs longUrl et expiresDate.
La méthode GET prend une URL courte comme variable de chemin, puis récupère et redirige vers l'URL d'origine. En haut du contrôleur, UrlService est injecté en tant que dépendance, ce qui sera expliqué ensuite.
UrlService .java est l'endroit où se trouve la plupart de la logique et est le service utilisé par le contrôleur.
ConvertToShortUrl est utilisé par la méthode POST du contrôleur. Il crée simplement un nouvel enregistrement dans la base de données et obtient un ID. L'ID est ensuite converti en un lien court en base 62 et renvoyé au contrôleur.
GetOriginalUrl est une méthode utilisée par la méthode GET du contrôleur. Il convertit d'abord une chaîne en base 10, et le résultat est un identifiant. Ensuite, il obtient un enregistrement de la base de données avec cet identifiant et lève une exception s'il n'existe pas. Après cela, il renvoie l'URL d'origine au contrôleur.
Dans cette partie, je parlerai de la documentation swagger, de la dockerisation de l'application, du cache de l'application et de l'événement programmé MySql.
Chaque fois que vous développez une API, il est bon de la documenter d'une manière ou d'une autre. La documentation facilite la compréhension et l'utilisation des API. L'API de ce projet est documentée à l'aide de l'interface utilisateur Swagger.
L'interface utilisateur Swagger permet à quiconque de visualiser et d'interagir avec les ressources de l'API sans avoir aucune logique d'implémentation en place.
Il est généré automatiquement, avec une documentation visuelle facilitant l'implémentation back-end et la consommation côté client.
Nous devons suivre plusieurs étapes pour inclure l'interface utilisateur Swagger dans le projet.
Tout d'abord, nous devons ajouter des dépendances Maven au fichier 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>
Pour votre référence, vous pouvez voir le fichier pom.xml complet ici . Après avoir ajouté les dépendances Maven, il est temps d'ajouter la configuration Swagger. Dans le dossier de configuration, nous devons créer une nouvelle 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(); } }
En haut de la classe, nous devons ajouter quelques annotations.
@Configuration indique qu'une classe déclare une ou plusieurs méthodes @Beans et peut être traitée par le conteneur Spring pour générer des définitions de bean et des demandes de service pour ces beans lors de l'exécution.
@ EnableSwagger2 indique que la prise en charge de Swagger doit être activée.
Ensuite, nous devons ajouter le bean Docket qui fournit la configuration de l'API principale avec des valeurs par défaut sensibles et des méthodes pratiques pour la configuration.
La méthode apiInfo() prend l'objet ApiInfo où nous pouvons configurer toutes les informations d'API nécessaires - sinon, elle utilise certaines valeurs par défaut. Pour rendre le code plus propre, nous devons créer une méthode privée qui configurera et renverra l'objet ApiInfo et transmettra cette méthode en tant que paramètre de la méthode apiInfo() . Dans ce cas, il s'agit de la méthode metadata() .
La méthode apis() nous permet de filtrer les packages en cours de documentation.
L'interface utilisateur Swagger est configurée et nous pouvons commencer à documenter notre API. Dans UrlController , au-dessus de chaque point de terminaison, nous pouvons utiliser l'annotation @ApiOperation pour ajouter une description. Selon vos besoins, vous pouvez utiliser d'autres annotations .
Il est également possible de documenter les DTO à l'aide de @ApiModelProperty qui vous permet d'ajouter des valeurs autorisées, des descriptions, etc.
Selon Wikipédia, un [cache](https://en.wikipedia.org/wiki/Cache_(informatique) est un composant matériel ou logiciel qui stocke des données afin que les demandes futures de ces données puissent être traitées plus rapidement ; les données stockées dans un cache peut être le résultat d'un calcul antérieur ou d'une copie de données stockées ailleurs.
Le type de cache le plus fréquemment utilisé est un cache en mémoire qui stocke les données mises en cache dans la RAM. Lorsque des données sont demandées et trouvées dans le cache, elles sont servies à partir de la RAM au lieu d'une base de données. De cette façon, nous évitons d'appeler un backend coûteux lorsqu'un utilisateur demande des données.
Un raccourcisseur d'URL est un type d'application qui a plus de demandes de lecture que de demandes d'écriture, ce qui signifie que c'est une application idéale pour utiliser le cache.
Pour activer la mise en cache dans l'application Spring Boot, il suffit d'ajouter l'annotation @EnableCaching dans la classe UrlShortenerApiApplication .
Après cela, dans le contrôleur , nous devons définir l'annotation @Cachable au-dessus de la méthode GET. Cette annotation stocke automatiquement les résultats de la méthode appelée le cache. Dans l'annotation @Cachable, nous définissons le paramètre value qui est le nom du cache, et le paramètre key qui est la clé du cache.
Dans ce cas, pour la clé de cache, nous allons utiliser 'shortUrl' car nous sommes sûrs qu'elle est unique. Les paramètres de synchronisation sont définis sur true pour garantir qu'un seul thread crée la valeur du cache.
Et c'est tout - notre cache est défini et lorsque nous chargeons l'URL avec un lien court pour la première fois, le résultat sera enregistré dans le cache et tout appel supplémentaire au point de terminaison avec le même lien court récupérera le résultat du cache au lieu de de la base de données.
La dockerisation est le processus d'empaquetage d'une application et de ses dépendances dans un conteneur [Docker](https://en.wikipedia.org/wiki/Docker_(software). Une fois que nous avons configuré le conteneur Docker, nous pouvons facilement exécuter l'application sur n'importe quel serveur ou ordinateur prenant en charge Docker.
La première chose que nous devons faire est de créer un Dockerfile.
Un Dockerfile est un fichier texte qui contient toutes les commandes qu'un utilisateur peut appeler sur la ligne de commande pour assembler une image.
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 - C'est ici que nous définissons l'image de base pour la base de construction. Nous allons utiliser OpenJDK v13 qui est une version gratuite et open-source de Java. Vous pouvez trouver d'autres images pour votre image de base sur le hub Docker qui est un endroit pour partager des images Docker.
COPY – Cette commande copie les fichiers du système de fichiers local (votre ordinateur) vers le système de fichiers du conteneur au chemin que nous avons spécifié. Nous allons copier le fichier JAR du dossier cible vers le dossier /usr/src/app du conteneur. J'expliquerai la création du fichier JAR un peu plus tard.
EXPOSE – Instruction qui informe Docker que le conteneur écoute les ports réseau spécifiés lors de l'exécution. Le protocole par défaut est TCP et vous pouvez spécifier si vous souhaitez utiliser UDP.
ENTRYPOINT - Cette instruction vous permet de configurer un conteneur qui s'exécutera en tant qu'exécutable. Ici, nous devons spécifier comment Docker manquera d'applications.
La commande pour exécuter une application à partir du fichier .jar est
java -jar <app_name>.jar
donc nous mettons ces 3 mots dans un tableau et c'est tout.
Maintenant que nous avons Dockerfile, nous devons créer l'image à partir de celui-ci. Mais comme je l'ai déjà mentionné, nous devons d'abord créer un fichier .jar à partir de notre projet afin que la commande COPY dans Dockerfile puisse fonctionner correctement. Pour créer un exécutable .jar, nous allons utiliser maven .
Nous devons nous assurer d'avoir Maven dans notre pom .xml . Si Maven est manquant, nous pouvons l'ajouter
<build> <plugins> <plugin> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-maven-plugin</artifactId> </plugin> </plugins> </build>
Après cela, nous devrions simplement exécuter la commande
mvn clean package
Une fois cela fait, nous pouvons créer une image Docker. Nous devons nous assurer que nous sommes dans le même dossier que le Dockerfile afin de pouvoir exécuter cette commande
docker build -t url-shortener:latest .
-t est utilisé pour taguer une image. Dans notre cas, cela signifie que le nom du référentiel sera url-shortener et une balise sera la plus récente. Le balisage est utilisé pour la gestion des versions des images. Une fois cette commande terminée, nous pouvons nous assurer que nous avons créé une image avec la commande
docker images
Cela nous donnera quelque chose comme ça
Pour la dernière étape, nous devons construire nos images. Je dis images car nous exécuterons également le serveur MySQL dans un conteneur Docker. Le conteneur de la base de données sera isolé du conteneur de l'application. Pour exécuter le serveur MySQL dans le conteneur Docker, exécutez simplement
$ docker run --name shortener -e MYSQL_ROOT_PASSWORD=my-secret-pw -d -p 3306:3306 mysql:8
Vous pouvez voir la documentation sur le hub Docker .
Lorsque nous avons une base de données en cours d'exécution dans un conteneur, nous devons configurer notre application pour qu'elle se connecte à ce serveur MySQL. Dans application.properties, définissez spring.datasource.url pour vous connecter au conteneur 'shortener'.
Depuis que nous avons apporté quelques modifications à notre projet, il est nécessaire de compresser notre projet dans un fichier .jar à l'aide de Maven et de reconstruire l'image Docker à partir du Dockerfile.
Maintenant que nous avons une image Docker, nous devons exécuter notre conteneur. Nous allons le faire avec la commande
docker run -d --name url-shortener-api -p 8080:8080 --link shortener url-shortener
-d signifie qu'un conteneur Docker s'exécute en arrière-plan de votre terminal. –name vous permet de définir le nom de votre conteneur
-p host-port:docker-port - Il s'agit simplement de mapper les ports de votre ordinateur local aux ports à l'intérieur du conteneur. Dans ce cas, nous avons exposé le port 8080 à l'intérieur d'un conteneur et avons décidé de le mapper sur notre port local 8080.
–link with this nous lions notre conteneur d'application au conteneur de base de données pour permettre aux conteneurs de se découvrir et de transférer en toute sécurité des informations sur un conteneur à un autre conteneur.
Il est important de savoir que ce drapeau est maintenant un héritage et qu'il sera retiré dans un proche avenir. Au lieu de liens, nous aurions besoin de créer un réseau pour faciliter la communication entre les deux conteneurs.
url-shortener - est le nom de l'image docker que nous voulons exécuter.
Et avec cela, nous avons terminé - dans le navigateur, visitez http://localhost:8080/swagger-ui.html
Vous pouvez désormais publier vos images sur DockerHub et exécuter facilement votre application sur n'importe quel ordinateur ou serveur.
Il y a deux autres choses dont je veux parler pour améliorer notre expérience Docker. L'un est une construction en plusieurs étapes et l'autre est docker-compose.
Avec les builds en plusieurs étapes , vous pouvez utiliser plusieurs instructions FROM dans votre Dockerfile. Chaque instruction FROM peut utiliser une base différente, et chacune d'elles commence une nouvelle étape de la construction. Vous pouvez copier sélectivement des artefacts d'une étape à une autre, en laissant derrière vous tout ce que vous ne voulez pas dans l'image finale.
Les builds en plusieurs étapes nous permettent d'éviter de créer manuellement des fichiers .jar chaque fois que nous apportons des modifications à notre code. Avec les builds en plusieurs étapes, nous pouvons définir une étape du build qui exécutera la commande de package Maven et l'autre étape copiera le résultat du premier build dans le système de fichiers d'un conteneur Docker.
Vous pouvez voir le Dockerfile complet ici .
Compose est un outil permettant de définir et d'exécuter des applications Docker multi-conteneurs. Avec Compose, vous utilisez un fichier YAML pour configurer les services de votre application. Ensuite, avec une seule commande, vous créez et démarrez tous les services de votre configuration.
Avec docker-compose, nous regrouperons notre application et notre base de données dans un seul fichier de configuration , puis nous exécuterons tout en même temps. De cette façon, nous évitons d'exécuter le conteneur MySQL, puis de le lier au conteneur d'application à chaque fois.
Dockercompose .yml est assez explicite - d'abord, nous configurons le conteneur MySQL en définissant l'image mysql v8.0 et les informations d'identification pour le serveur MySQL. Après cela, nous configurons le conteneur d'application en définissant les paramètres de construction, car nous devons créer une image au lieu de l'extraire comme nous l'avons fait avec MySQL. De plus, nous devons définir que le conteneur d'application dépend du conteneur MySQL.
Nous pouvons maintenant exécuter l'ensemble du projet avec une seule commande :
docker-compose up
Cette partie est facultative mais je pense que quelqu'un pourrait trouver cela utile de toute façon. J'ai parlé de la date d'expiration du lien court qui peut être définie par l'utilisateur ou une valeur par défaut. Pour ce problème, nous pouvons définir un événement planifié dans notre base de données. Cet événement s'exécutera toutes les x minutes et supprimera toutes les lignes de la base de données dont la date d'expiration est inférieure à l'heure actuelle. Aussi simple que cela. Cela fonctionne bien sur une petite quantité de données dans la base de données.
Maintenant, je dois vous avertir de quelques problèmes avec cette solution.
J'espère que cet article vous a un peu aidé à vous faire une idée générale de la création d'un service de raccourcissement d'URL. Vous pouvez prendre cette idée et l'améliorer. Notez quelques nouvelles exigences fonctionnelles et essayez de les mettre en œuvre. Si vous avez des questions, vous pouvez les poster sous ce post.