共计 8651 个字符,预计需要花费 22 分钟才能阅读完成。
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 处克隆我的项目的存储库,并创立本人的前端。