如何使用Java和Spring Boot創(chuàng)建短鏈接生成器
譯文【51CTO.com快譯】URL短鏈接生成器是一種根據(jù)冗長的URL,創(chuàng)建短鏈接的服務(wù)。通常,短鏈接的長度只有原始URL的三分之一、甚至四分之一。因此它們更容易被輸入、呈現(xiàn)、以及推送。用戶只需單擊短鏈接,便可被自動(dòng)重定向到原始的URL處。
目前,tiny.cc、bitly.com和cutt.ly都能夠提供在線式的URL縮短服務(wù)。當(dāng)然,您也可以為應(yīng)用系統(tǒng)自行設(shè)計(jì)和開發(fā)出縮短URL的服務(wù)。下面,我和您討論具體的實(shí)現(xiàn)過程。首先,讓我們來探討一下與之相關(guān)的功能性和非功能性的需求。
功能要求:
- 保存用戶輸入的長URL,并據(jù)此生成相應(yīng)的短鏈接。
- 允許用戶選擇到期日期,以便生成的短鏈接在該日期后自動(dòng)無效。
- 方便用戶在單擊短鏈接后,重定向到原始的長鏈接處。
- 作為可選的方式,允許用戶創(chuàng)建服務(wù)帳戶,并讓生成的短鏈接僅對該賬戶有效。
- 以可選的方式,允許用戶自行創(chuàng)建短鏈接。
- 以可選的方式,允許用戶標(biāo)記出那些最常訪問的鏈接。
非功能性要求:
- 生成服務(wù)具有持續(xù)的有效性和可訪問性。
- 重定向的用時(shí)應(yīng)不超過2秒。
URL轉(zhuǎn)換的方式
URL短鏈接生成器中最重要的是轉(zhuǎn)換算法。不同的轉(zhuǎn)換方式通常會(huì)產(chǎn)生不同的輸出,而且它們各有優(yōu)、缺點(diǎn)。假設(shè)我們需要一個(gè)最長為7個(gè)字符的短鏈接。那么我們可以采用MD5或SHA-2之類的哈希函數(shù),對原始的URL進(jìn)行散列處理。由于散列的結(jié)果會(huì)超過7個(gè)字符,因此我們只取前7個(gè)字符。不過,由于前7個(gè)字符可能已經(jīng)被用于其他短鏈接,并由此會(huì)引發(fā)沖突,因此我們需要依次截取后面的7個(gè)字符,直至找到一個(gè)被使用過的短鏈接為止。
生成短鏈接的第二種方法是使用UUID。UUID被復(fù)制的概率近似為零,因此可以完全忽略沖突的可能。由于UUID是由36個(gè)字符組成,仍然可能遇到上述問題,因此我們應(yīng)當(dāng)截取前7個(gè)字符,然后檢查該組合是否已被占用。
第三種方法是將數(shù)字從Base 10轉(zhuǎn)換為Base 62。Base是可用于表示特定數(shù)字的字符數(shù)。Base 10是我們?nèi)粘I钪惺褂玫臄?shù)字,即:[0-9],而Base 62則是:[0-9][az][AZ]。這意味著,以10為Base的四位數(shù)字,將與以62為Base、但具有兩個(gè)字符的數(shù)字相同。因此在URL轉(zhuǎn)換中,使用最大長度為7個(gè)字符的Base 62,將允許我們?yōu)槎替溄犹峁?2^7個(gè)唯一值。
Base 62的轉(zhuǎn)換機(jī)制
我使用如下算法,將一個(gè)Base為10的數(shù)字轉(zhuǎn)換為Base為62:
- while(number > 0)
- remainder = number % 62
- number = number / 62
- attach remainder to start of result collection
據(jù)此,我們只需要將結(jié)果集中的數(shù)字映射到Base為62的字符 [0,1,2,...,a,b,c...,A,B,C,...]即可。
下面,我通過將1000從Base 10轉(zhuǎn)換為Base 62的例子,來討論其工作機(jī)制。
- 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轉(zhuǎn)換為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++
所以其對應(yīng)的代碼示例為:
- 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
實(shí)現(xiàn)
我使用Spring Boot和MySQL來實(shí)現(xiàn)該服務(wù)。請參看我在Github上的具體代碼。我用到了數(shù)據(jù)庫的自動(dòng)遞增功能來實(shí)現(xiàn)Base 62的轉(zhuǎn)換。當(dāng)然,您也可以使用任何其他具有自動(dòng)遞增功能的數(shù)據(jù)庫。
首先,請?jiān)L問Spring initializr,并選擇Spring Web與MySQL Driver。接著,請單擊“生成(Generate)”按鈕,并下載對應(yīng)的zip文件。完成解壓縮之后,我們就可以在自己的IDE中打開該項(xiàng)目了。
我通過創(chuàng)建文件夾:控制器、實(shí)體、服務(wù)、存儲(chǔ)庫、dto和配置,實(shí)現(xiàn)在邏輯上劃分程序代碼。
在“實(shí)體”文件夾中,我創(chuàng)建了一個(gè)具有id、longUrl、createdDate和expiresDate四個(gè)屬性的Url.java類。
請注意,此處既沒有短鏈接的屬性,也不會(huì)去保存短鏈接。每次只要有GET請求的出現(xiàn),我們都會(huì)將id屬性從Base 10轉(zhuǎn)換為Base 62,以便節(jié)省數(shù)據(jù)庫中的空間。
用戶在訪問該短鏈接時(shí),應(yīng)根據(jù)longURL屬性重定向到目標(biāo)網(wǎng)站。createdDate則只是為了查看longURL何時(shí)被保存(并不重要)。而如果用戶希望在一段時(shí)間后讓短鏈接失效的話,可以對expiresDate進(jìn)行設(shè)置。
接著,我在“服務(wù)”文件夾中,創(chuàng)建了一個(gè)BaseService.java文件。其中包含了從Base 10到Base 62相互轉(zhuǎn)換的方法。
- private static final String allowedString = "abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ0123456789";
- private char[] allowedCharacters = allowedString.toCharArray();
- private int base = allowedCharacters.length;
正如前面所提到的,若要使用Base 62轉(zhuǎn)換,則需要有一個(gè)被稱為allowedCharacters的Base 62的字母表。此外,為了方便按需更改被允許的字符,我們可根據(jù)字符的長度,計(jì)算出基本變量的值。其中,編碼(encode)方法會(huì)將一個(gè)數(shù)字作為輸入,返回一個(gè)短鏈接;而解碼(decode)方法則會(huì)接受一個(gè)字符串(如:短鏈接)作為輸入,并返回一個(gè)數(shù)字。
在存儲(chǔ)庫文件夾中,我創(chuàng)建了UrlRepository.java文件。它只是JpaRepository的一個(gè)擴(kuò)展,并給出了諸如“findById”,“save”等方法。在此,我們無需進(jìn)行任何添加。
然后,我在“控制器”文件夾中創(chuàng)建了一個(gè)URLController.java文件(請參見如下代碼)。它提供一種用于創(chuàng)建短鏈接的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方法會(huì)將UrlLongRequest作為請求體。它是一個(gè)具有l(wèi)ongURL和expiresDate屬性的類。而GET方法會(huì)將一個(gè)短的URL作為路徑變量,以獲取并重定向到原始的URL處。
在控制器的上層,urlService會(huì)作為依賴項(xiàng)被注入,以便后續(xù)進(jìn)行解釋。
UrlService.java既包含了大量邏輯,又為控制器提供了服務(wù)。ConvertToShortUrl僅供控制器的POST方法所使用。它只是在數(shù)據(jù)庫中創(chuàng)建了一條新的記錄,并獲取一個(gè)id,以便將其轉(zhuǎn)換為Base 62的短鏈接,并返回給控制器。
控制器使用GetOriginalUrl方法,首先將字符串轉(zhuǎn)換為Base 10類型的id。然后,它通過該id從數(shù)據(jù)庫中獲取一條記錄。當(dāng)然,如果該記錄不存在的話,則會(huì)拋出異常。最后,它會(huì)將原始的URL返回給控制器。
下面,我將和您討論Swagger文檔、應(yīng)用的dockerization(容器化)、緩存以及MySQL的計(jì)劃事件。
Swagger的用戶界面
在開發(fā)過程中文檔記錄無疑能夠使得API更易于理解和使用。在該項(xiàng)目中,我使用Swagger UI來記錄API。Swagger UI允許任何人在沒有任何實(shí)現(xiàn)邏輯的情況下,可視化API資源,并與之交互。它不但能夠自動(dòng)生成,而且?guī)в锌梢暬奈臋n,以便于后端的實(shí)現(xiàn)和客戶端的使用。
我通過執(zhí)行如下步驟,在項(xiàng)目中引入了Swagger UI。首先,我在pom.xml文件中添加了Maven依賴項(xiàng):
- 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依賴項(xiàng)后,我們便可以添加Swagger的相關(guān)配置了。我在“配置”文件夾中,創(chuàng)建了一個(gè)新的類--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表示一個(gè)類聲明了一到多個(gè)@Beans方法,并且可以由Spring容器通過處理,在運(yùn)行時(shí)為這些bean生成相應(yīng)的定義和服務(wù)請求。
- @EnableSwagger2表示應(yīng)該啟用Swagger支持。
接下來,我添加了Docket bean。它提供的主要API配置,帶有各種合理的默認(rèn)值、以及便捷的配置方法。
此處的apiInfo()方法除了可以使用默認(rèn)值,還能夠接受ApiInfo對象,以便我們配置所有必要的API信息。為了使代碼更加簡潔,我們可以創(chuàng)建一個(gè)私有的方法—metadata(),來配置和返回ApiInfo對象,并將該方法作為apiInfo()方法的參數(shù)進(jìn)行傳遞。同時(shí),apis()方法也允許我們過濾那些被文檔化的包。
在完成了Swagger UI的配置后,我們便可以開始文檔化API了。在UrlController內(nèi)部的每個(gè)端點(diǎn)上,我們可以使用@ApiOperation來添加描述性的注釋。當(dāng)然,您也可以按需使用其他類型的注釋。
我們還可以文檔化DTO,并使用@ApiModelProperty來添加各種允許的值和描述。
緩存
根據(jù)維基百科的定義,緩存是存儲(chǔ)數(shù)據(jù)的軟、硬件組件,可用來更快地處理后續(xù)對于相同數(shù)據(jù)的請求。而存儲(chǔ)在緩存中的數(shù)據(jù),往往是早期計(jì)算的結(jié)果、或是已存儲(chǔ)在其他地方的數(shù)據(jù)副本。
目前,最常用的緩存類型是內(nèi)存緩存(in-memory cache)。它能夠?qū)⒕彺娴臄?shù)據(jù)存儲(chǔ)到RAM中。當(dāng)被請求數(shù)據(jù)與緩存一致時(shí),它是從RAM、而非從數(shù)據(jù)庫被提取。據(jù)此,我們避免頻繁調(diào)用后端的開銷。
由于URL短鏈接生成器可以被認(rèn)為是一種讀取多于寫入的請求應(yīng)用,因此它是使用緩存的理想應(yīng)用場景。若想在Spring Boot應(yīng)用中啟用緩存,我們只需要在UrlShortenerApiApplication類中添加@EnableCaching注釋即可。
接著,在控制器中,我們需要在GET方法上設(shè)置@Cachable注解,以實(shí)現(xiàn)自動(dòng)將方法調(diào)用的結(jié)果存入緩存中。在@Cachable的注解中,我設(shè)置了緩存名稱的value參數(shù)和緩存鍵的key參數(shù)。鑒于緩存鍵的唯一性,我使用了“shortUrl”,并將Sync參數(shù)設(shè)置為true,以確保只有一個(gè)線程正在構(gòu)建緩存值。
至此,當(dāng)我們首次加載帶有短鏈接的URL時(shí),其結(jié)果將會(huì)被保存到緩存中。后續(xù),任何端點(diǎn)若想調(diào)用相同短鏈接,都會(huì)從緩存、而非從數(shù)據(jù)庫中檢索結(jié)果。
Dockerization
Dockerization是將應(yīng)用程序及其依賴項(xiàng)打包到Docker容器中的過程。一旦配置了Docker容器,我們便可以輕松地在任何支持Docker的服務(wù)器、或主機(jī)上運(yùn)行應(yīng)用程序。
因此,我們首先需要?jiǎng)?chuàng)建一個(gè)包含所有命令的Dockerfile文本文件,以便用戶通過調(diào)用命令行的方式,掛載某個(gè)鏡像。
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:表示需要構(gòu)建的基礎(chǔ)鏡像。我使用的是Java免費(fèi)開源版--OpenJDK v13。您也可以在共享的Docker鏡像平臺(tái)--Docker hub(https://hub.docker.com/)上,找到其他類型base鏡像。
- COPY:此命令會(huì)將文件從本地文件系統(tǒng),復(fù)制到指定路徑的容器文件系統(tǒng)中。在此,我將目標(biāo)文件夾中的JAR文件,復(fù)制到容器中的/usr/src/app文件夾中(稍后我將解釋如何創(chuàng)建JAR文件)。
- EXPOSE:負(fù)責(zé)通知Docker容器在運(yùn)行時(shí),偵聽指定網(wǎng)絡(luò)端口的指令。其默認(rèn)協(xié)議為TCP,您也可以使用UDP。
- ENTRYPOINT:負(fù)責(zé)配置可執(zhí)行的容器。在此,我通過命令為“java -jar
.jar”,指定Docker將如何運(yùn)行一個(gè).jar文件類型的應(yīng)用程序。
為了在項(xiàng)目中創(chuàng)建.jar文件,以便Dockerfile中的COPY命令能夠正常工作,我使用Maven來創(chuàng)建可執(zhí)行的.jar。如果您的pom.xml缺少M(fèi)aven,請用如下方式進(jìn)行添加:
XML
- <build>
- <plugins>
- <plugin>
- <groupId>org.springframework.boot</groupId>
- <artifactId>spring-boot-maven-plugin</artifactId>
- </plugin>
- </plugins>
- </build>
隨后,我運(yùn)行命令:mvn clean package,以構(gòu)建出一個(gè)Docker鏡像。接著,在Dockerfile文件夾中,我運(yùn)行了命令:docker build -t url-shortener:latest。其中,-t可用于標(biāo)記一個(gè)鏡像,并實(shí)現(xiàn)版本控制。在此,即為最新的存儲(chǔ)庫URL-shortener。我們可以使用命令“docker images”來創(chuàng)建鏡像。屏幕上的顯示結(jié)果為:
最后,我還需要在docker容器中構(gòu)建MySQL服務(wù)器鏡像,以方便數(shù)據(jù)庫容器與應(yīng)用容器相隔離。為此,我在Docker容器中運(yùn)行了如下命令:
- $ docker run --name shortener -e MYSQL_ROOT_PASSWORD=my-secret-pw -d -p 3306:3306 mysql:8
您可以在Docker hub上查看到相關(guān)文檔。
為了在容器內(nèi)運(yùn)行數(shù)據(jù)庫,我通過配置,將現(xiàn)有的應(yīng)用程序連接上該MySQL服務(wù)器。即:在application.properties中設(shè)置spring.datasource.url,以連接到shortener容器。
然后,我使用以下命令來運(yùn)行已構(gòu)建好的Docker 鏡像容器:
- docker run -d –-name url-shortener-api -p 8080:8080 --link shortener url-shortener
- -d表示Docker容器在終端的后臺(tái)運(yùn)行。
- --name可設(shè)置容器的名稱。
- -p host-port:docker-port:是將本地端口映射到容器內(nèi)的端口上。在本例中,我在容器內(nèi)公開了端口8080,并映射到了本地的8080上。
- --link:用于鏈接應(yīng)用容器與數(shù)據(jù)庫容器,以實(shí)現(xiàn)容器間的相互發(fā)現(xiàn)和安全傳輸。
- url-shortener:則指明了待運(yùn)行的Docker鏡像名稱。
至此,我們便可以在瀏覽器中訪問http://localhost:8080/swagger-ui.html了。通過將鏡像發(fā)布到Docker Hub上,任何計(jì)算機(jī)和服務(wù)器都可以輕松地運(yùn)行該應(yīng)用。
當(dāng)然,為了改善該Docker的使用體驗(yàn),我們需要注意多階段構(gòu)建,以及docker-compose兩個(gè)方面。
多階段構(gòu)建
使用多階段構(gòu)建,您將可以在Dockerfile中使用多個(gè)FROM語句。每個(gè)FROM指令都可以使用不同的base,并且每個(gè)指令都能夠開啟構(gòu)建的新階段。您可以有選擇性地將各個(gè)工件(artifacts)從一個(gè)階段復(fù)制到另一個(gè)階段,并在最終鏡像中去掉不想要的內(nèi)容。
多階段構(gòu)建有利于我們避免每次對代碼進(jìn)行更改后,都必須手動(dòng)重建.jar文件。據(jù)此,我們可以定義一個(gè)構(gòu)建階段,來執(zhí)行Maven包命令。而另一個(gè)階段會(huì)將來自第一次構(gòu)建的結(jié)果,直接復(fù)制到Docker容器的文件系統(tǒng)中。您可以通過鏈接--https://github.com/AnteMarin/UrlShortener-API/blob/develop/Dockerfile,查看完整的Dockerfile。
Docker-compose
Compose是一個(gè)用于定義和運(yùn)行多容器Docker應(yīng)用的工具。借助Compose,您可以使用YAML文件,來配置應(yīng)用程序的服務(wù),然后使用單個(gè)命令,從配置中創(chuàng)建并啟動(dòng)所有的服務(wù)。
使用docker-compose,我們能夠?qū)?yīng)用程序和數(shù)據(jù)庫打包到一個(gè)配置文件中,以便立即運(yùn)行所有的內(nèi)容。據(jù)此,我們避免了每次去運(yùn)行MySQL容器,將其鏈接到應(yīng)用容器的繁瑣。
由Docker-compose.yml文件的具體配置內(nèi)容可知:首先,我們通過設(shè)置鏡像mysql v8.0和MySQL服務(wù)器的憑據(jù),來配置MySQL容器。接著,我們通過設(shè)置構(gòu)建參數(shù),來配置應(yīng)用容器,畢竟我們需要的是鏡像,而非使用MySQL進(jìn)行拉取。此外,我們還需要通過設(shè)置,讓應(yīng)用容器依賴于MySQL容器。最終,我們可以使用命令“docker-compose up”,來運(yùn)行整個(gè)項(xiàng)目。
MySQL計(jì)劃事件(Scheduled Event)
說到短鏈接的到期設(shè)置,我們既可以讓用戶自定義,又可以保持默認(rèn)值。為此,我們可以在數(shù)據(jù)庫中設(shè)置一個(gè)計(jì)劃事件。通過每x分鐘運(yùn)行一次該事件,到期時(shí)間只要小于當(dāng)前時(shí)間,數(shù)據(jù)庫就會(huì)自動(dòng)刪除某一行,就這么簡單。這非常適用于保持?jǐn)?shù)據(jù)庫中的少量數(shù)據(jù)。不過,該方法有兩個(gè)問題值得注意:
- 首先,該事件只會(huì)從數(shù)據(jù)庫中刪除記錄,而不會(huì)從緩存中刪除數(shù)據(jù)。如前所述,如果緩存可以找到匹配的數(shù)據(jù)的話,就不會(huì)去查看數(shù)據(jù)庫。因此,某條短鏈接即便已經(jīng)在數(shù)據(jù)庫中被刪除了,我們?nèi)匀豢梢詮木彺嬷蝎@取它。
- 其次,在示例腳本中,我設(shè)置該事件為每隔2分鐘運(yùn)行一次。如果數(shù)據(jù)庫的記錄變動(dòng)較大,則可能出現(xiàn)前一個(gè)事件尚未在其預(yù)定的間隔周期內(nèi)執(zhí)行完畢,后一個(gè)事件已被觸發(fā),進(jìn)而出現(xiàn)多個(gè)事件實(shí)例同時(shí)在執(zhí)行的混亂局面。
小結(jié)
通過上述示例和討論,我向您展示了如何使用Java和Spring Boot,來創(chuàng)建URL短鏈接生成器的API。這是一個(gè)十分常見的面試問題,您既可以據(jù)此創(chuàng)建自己的改進(jìn)版本,又可以從上述GitHub處克隆項(xiàng)目的存儲(chǔ)庫,并創(chuàng)建自己的前端。
原文標(biāo)題:URL Shortener Complete Tutorial,作者:Ante Marin
【51CTO譯稿,合作站點(diǎn)轉(zhuǎn)載請注明原文譯者和出處為51CTO.com】






























