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