URL短链接生成器是一种依据简短的URL,创立短链接的服务。通常,短链接的长度只有原始URL的三分之一、甚至四分之一。因而它们更容易被输出、出现、以及推送。用户只需单击短链接,便可被主动重定向到原始的URL处。

目前,tiny.cc、bitly.com和cutt.ly都可能提供在线式的URL缩短服务。当然,您也能够为利用零碎自行设计和开发出缩短URL的服务。上面,我和您探讨具体的实现过程。首先,让咱们来探讨一下与之相干的功能性和非功能性的需要。

性能要求:
保留用户输出的长URL,并据此生成相应的短链接。
容许用户抉择到期日期,以便生成的短链接在该日期后主动有效。
不便用户在单击短链接后,重定向到原始的长链接处。
作为可选的形式,容许用户创立服务帐户,并让生成的短链接仅对该账户无效。
以可选的形式,容许用户自行创立短链接。
以可选的形式,容许用户标记出那些最常拜访的链接。
非功能性要求:
生成服务具备继续的有效性和可拜访性。
重定向的用时应不超过2秒。
URL转换的形式
URL短链接生成器中最重要的是转换算法。不同的转换形式通常会产生不同的输入,而且它们各有优、毛病。假如咱们须要一个最长为7个字符的短链接。那么咱们能够采纳 MD5 或 SHA-2 之类的哈希函数,对原始的URL进行散列解决。因为散列的后果会超过7个字符,因而咱们只取前7个字符。不过,因为前7个字符可能曾经被用于其余短链接,并由此会引发抵触,因而咱们须要顺次截取前面的7个字符,直至找到一个被应用过的短链接为止。

生成短链接的第二种办法是应用 UUID 。UUID被复制的概率近似为零,因而能够齐全疏忽抵触的可能。因为UUID是由36个字符组成,依然可能遇到上述问题,因而咱们该当截取前7个字符,而后查看该组合是否已被占用。

第三种办法是将数字从 Base 10 转换为Base 62。Base是可用于示意特定数字的字符数。Base 10是咱们日常生活中应用的数字,即:[0-9],而Base 62则是:0-9[AZ]。这意味着,以10为Base的四位数字,将与以62为Base、但具备两个字符的数字雷同。因而在URL转换中,应用最大长度为7个字符的Base 62,将容许咱们为短链接提供62^7个惟一值。

Base 62的转换机制
我应用如下算法,将一个Base为10的数字转换为Base为62:

while(number > 0)
remainder = number % 62
number = number / 62
attach remainder to start of result collection
据此,咱们只须要将后果集中的数字映射到Base为62的字符 [0,1,2,...,a,b,c...,A,B,C,...]即可。

上面,我通过将1000从Base 10转换为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 

[16,8] 被映射到Base 62后为g8,即1000base10 = g8base62。

而从Base 62转换为Base 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 

实现
我应用Spring Boot和MySQL来实现该服务。请参看我在 Github上的具体代码 。我用到了数据库的主动递增性能来实现Base 62的转换。当然,您也能够应用任何其余具备主动递增性能的数据库。

首先,请拜访 Spring initializr ,并抉择Spring Web与MySQL Driver。接着,请单击“生成(Generate)”按钮,并下载对应的zip文件。实现解压缩之后,咱们就能够在本人的IDE中关上该我的项目了。

我通过创立文件夹:控制器、实体、服务、存储库、dto和配置,实现在逻辑上划分程序代码。

在“实体”文件夹中,我创立了一个具备id、longUrl、createdDate和expiresDate四个属性的 Url.java类 。

请留神,此处既没有短链接的属性,也不会去保留短链接。每次只有有GET申请的呈现,咱们都会将id属性从Base 10转换为Base 62,以便节俭数据库中的空间。

用户在拜访该短链接时,应依据longURL属性重定向到指标网站。createdDate则只是为了查看longURL何时被保留(并不重要)。而如果用户心愿在一段时间后让短链接生效的话,能够对expiresDate进行设置。

接着,我在“服务”文件夹中,创立了一个 BaseService.java 文件。其中蕴含了从Base 10到Base 62互相转换的办法。

private static final String allowedString = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
private char[] allowedCharacters = allowedString.toCharArray();
private int base = allowedCharacters.length;
正如后面所提到的,若要应用Base 62转换,则须要有一个被称为allowedCharacters的Base 62的字母表。此外,为了不便按需更改被容许的字符,咱们可依据字符的长度,计算出根本变量的值。其中,编码(encode)办法会将一个数字作为输出,返回一个短链接;而解码(decode)办法则会承受一个字符串(如:短链接)作为输出,并返回一个数字。

在存储库文件夹中,我创立了 UrlRepository.java 文件。它只是JpaRepository的一个扩大,并给出了诸如“findById”,“save”等办法。在此,咱们无需进行任何增加。

而后,我在“控制器”文件夹中创立了一个URLController.java文件(请参见如下代码)。它提供一种用于创立短链接的POST办法,以及一种被用于重定向到原始URL的GET办法。

@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办法所应用。它只是在数据库中创立了一条新的记录,并获取一个id,以便将其转换为Base 62的短链接,并返回给控制器。

控制器应用GetOriginalUrl办法,首先将字符串转换为Base 10类型的id。而后,它通过该id从数据库中获取一条记录。当然,如果该记录不存在的话,则会抛出异样。最初,它会将原始的URL返回给控制器。

上面,我将和您探讨Swagger文档、利用的dockerization(容器化)、缓存以及MySQL的打算事件。

Swagger的用户界面
在开发过程中文档记录无疑可能使得API更易于了解和应用。在该我的项目中,我应用Swagger UI来记录API。 Swagger UI 容许任何人在没有任何实现逻辑的状况下,可视化API资源,并与之交互。它岂但可能主动生成,而且带有可视化的文档,以便于后端的实现和客户端的应用。

我通过执行如下步骤,在我的项目中引入了Swagger UI。首先,我在 pom.xml文件 中增加了Maven依赖项:

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>
增加了Maven依赖项后,咱们便能够增加Swagger的相干配置了。我在“配置”文件夹中,创立了一个新的类-- SwaggerConfig.java ,请参考如下代码段。

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容器通过解决,在运行时为这些bean生成相应的定义和服务申请。
@EnableSwagger2示意应该启用Swagger反对。
接下来,我增加了Docket bean。它提供的次要API配置,带有各种正当的默认值、以及便捷的配置办法。

此处的apiInfo()办法除了能够应用默认值,还可能承受ApiInfo对象,以便咱们配置所有必要的API信息。为了使代码更加简洁,咱们能够创立一个公有的办法—metadata(),来配置和返回ApiInfo对象,并将该办法作为apiInfo()办法的参数进行传递。同时,apis()办法也容许咱们过滤那些被文档化的包。

在实现了Swagger UI的配置后,咱们便能够开始文档化API了。在 UrlController 外部的每个端点上,咱们能够应用 @ApiOperation 来增加描述性的正文。当然,您也能够按需应用 其余类型的正文 。

咱们还能够文档化 DTO ,并应用 @ApiModelProperty 来增加各种容许的值和形容。

缓存
依据维基百科的定义, 缓存 是存储数据的软、硬件组件,可用来更快地解决后续对于雷同数据的申请。而存储在缓存中的数据,往往是晚期计算的后果、或是已存储在其余中央的数据正本。

目前,最罕用的缓存类型是内存缓存(in-memory cache)。它可能将缓存的数据存储到RAM中。当被申请数据与缓存统一时,它是从RAM、而非从数据库被提取。据此,咱们防止频繁调用后端的开销。

因为URL短链接生成器能够被认为是一种读取多于写入的申请利用,因而它是应用缓存的现实利用场景。若想在Spring Boot利用中启用缓存,咱们只须要在 UrlShortenerApiApplication类 中增加 @EnableCaching 正文即可。

接着,在 控制器 中,咱们须要在GET办法上设置 @Cachable 注解,以实现主动将办法调用的后果存入缓存中。在@Cachable的注解中,我设置了缓存名称的value参数和缓存键的key参数。鉴于缓存键的唯一性,我应用了“shortUrl”,并将Sync参数设置为true,以确保只有一个线程正在构建缓存值。

至此,当咱们首次加载带有短链接的URL时,其后果将会被保留到缓存中。后续,任何端点若想调用雷同短链接,都会从缓存、而非从数据库中检索后果。

Dockerization
Dockerization是将应用程序及其依赖项打包到 Docker 容器中的过程。一旦配置了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:示意须要构建的根底镜像。我应用的是Java收费开源版--OpenJDK v13。您也能够在共享的Docker镜像平台--Docker hub(https://hub.docker.com/)上,找到其余类型base镜像。
COPY:此命令会将文件从本地文件系统,复制到指定门路的容器文件系统中。在此,我将指标文件夹中的JAR文件,复制到容器中的/usr/src/app文件夹中(稍后我将解释如何创立JAR文件)。
EXPOSE:负责告诉Docker容器在运行时,侦听指定网络端口的指令。其默认协定为TCP,您也能够应用UDP。
ENTRYPOINT:负责配置可执行的容器。在此,我通过命令为“java -jar.jar”,指定Docker将如何运行一个.jar文件类型的应用程序。
为了在我的项目中创立.jar文件,以便Dockerfile中的COPY命令可能失常工作,我应用Maven来创立可执行的.jar。如果您的pom.xml短少Maven,请用如下形式进行增加:

XML

<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可用于标记一个镜像,并实现版本控制。在此,即为最新的存储库URL-shortener。咱们能够应用命令“docker images”来创立镜像。屏幕上的显示后果为:

最初,我还须要在docker容器中构建MySQL服务器镜像,以不便数据库容器与利用容器相隔离。为此,我在Docker容器中运行了如下命令:

$ docker run --name shortener -e MYSQL_ROOT_PASSWORD=my-secret-pw -d -p 3306:3306 mysql:8
您能够在 Docker hub 上查看到相干文档。

为了在容器内运行数据库,我通过配置,将现有的应用程序连贯上该MySQL服务器。即:在 application.properties 中设置spring.datasource.url,以连贯到shortener容器。

而后,我应用以下命令来运行已构建好的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:则指明了待运行的Docker镜像名称。
至此,咱们便能够在浏览器中拜访http://localhost:8080/swagger...了。通过将镜像公布到Docker Hub上,任何计算机和服务器都能够轻松地运行该利用。

当然,为了改善该Docker的应用体验,咱们须要留神多阶段构建,以及docker-compose两个方面。

多阶段构建
应用 多阶段构建 ,您将能够在Dockerfile中应用多个FROM语句。每个FROM指令都能够应用不同的base,并且每个指令都可能开启构建的新阶段。您能够有选择性地将各个工件(artifacts)从一个阶段复制到另一个阶段,并在最终镜像中去掉不想要的内容。

多阶段构建有利于咱们防止每次对代码进行更改后,都必须手动重建.jar文件。据此,咱们能够定义一个构建阶段,来执行Maven包命令。而另一个阶段会将来自第一次构建的后果,间接复制到Docker容器的文件系统中。您能够通过链接--https://github.com/AnteMarin/...,查看残缺的Dockerfile。

Docker-compose
Compose 是一个用于定义和运行多容器Docker利用的工具。借助Compose,您能够应用YAML文件,来配置应用程序的服务,而后应用单个命令,从配置中创立并启动所有的服务。

应用docker-compose,咱们可能将应用程序和数据库打包到一个配置文件中,以便立刻运行所有的内容。据此,咱们防止了每次去运行MySQL容器,将其链接到利用容器的繁琐。

由 Docker-compose.yml 文件的具体配置内容可知:首先,咱们通过设置镜像mysql v8.0和MySQL服务器的凭据,来配置MySQL容器。接着,咱们通过设置构建参数,来配置利用容器,毕竟咱们须要的是镜像,而非应用MySQL进行拉取。此外,咱们还须要通过设置,让利用容器依赖于MySQL容器。最终,咱们能够应用命令“docker-compose up”,来运行整个我的项目。

MySQL打算事件(Scheduled Event)
说到短链接的到期设置,咱们既能够让用户自定义,又能够放弃默认值。为此,咱们能够在数据库中设置一个打算事件。通过每x分钟运行一次该事件,到期工夫只有小于以后工夫,数据库就会主动删除某一行,就这么简略。这十分实用于放弃数据库中的大量数据。不过,该办法有两个问题值得注意:

首先,该事件只会从数据库中删除记录,而不会从缓存中删除数据。如前所述,如果缓存能够找到匹配的数据的话,就不会去查看数据库。因而,某条短链接即使曾经在数据库中被删除了,咱们依然能够从缓存中获取它。
其次,在示例脚本中,我设置该事件为每隔2分钟运行一次。如果数据库的记录变动较大,则可能呈现前一个事件尚未在其预约的距离周期内执行结束,后一个事件已被触发,进而呈现多个事件实例同时在执行的凌乱场面。
小结
通过上述示例和探讨,我向您展现了如何应用Java和Spring Boot,来创立URL短链接生成器的API。这是一个非常常见的面试问题,您既能够据此创立本人的改良版本,又能够从上述GitHub处克隆我的项目的存储库,并创立本人的前端。