短網址
介紹
短網址,顧名思義
就是把原本冗長的 URL 轉換成簡短的 URL
讓使用者可以更容易的分享連結
例如 TinyURL 的 https://tinyurl.com/
或是先前已停止服務的 Google URL shortener 的 http://goo.gl/
我們以 TinyURL 為例
本文章所在論壇的java分類網址 https://www.tpisoftware.com/tpu/category/8
輸入後得到一個較短的網址 https://tinyurl.com/y4hke9ks
如下圖
運作流程
經由DNS解析URL獲得IP位置後
會向此IP位置發送https請求進行查詢 y4hke9ks
在取得查詢結果時
TinyURL 伺服器會把請求通過 HTTP STATUS 301 或 302
進行轉址到原本的網址 https://www.tpisoftware.com/tpu/category/8
以達到縮短網址仍能導向正確網站的功能
原理
其原理是把原本真實冗長的URL進行hash雜湊後
將雜湊碼與URL儲存於資料庫中
而後續使用短網址時
再藉由雜湊碼去查詢URL進行轉址導向原網址
雜湊演算
當然演算法有很多種
有MD4、MD5、SHA-256、SHA-516等
各種演算法都有其特性
例如有的是追求安全至上
設計為不可逆轉破解的
而短網址並無安全性問題
只有需要降低碰撞率
才能去mapping出正確的URL
所以我們使用MurmurHash
是一種高性能與低碰撞率的算法
在此Demo中是藉由Google的Guava來達成雜湊演算
Demo
接下來不囉唆,直接上code
我們使用 SpringBoot 配上 Redis
將 Url 存於 Redis 中,供後續的 hashUrl Mapping
除了基本的SpringBoot相關套件,額外加入了下列套件
guava 雜湊演算相關
jedis 連接redis套件
lombok 簡化程式碼相關套件
pom.xml
<dependency>
<groupId>com.google.guava</groupId>
<artifactId>guava</artifactId>
<version>28.2-jre</version>
</dependency>
<dependency>
<groupId>redis.clients</groupId>
<artifactId>jedis</artifactId>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
順便附上Redis相關參數設定
RedisAutoConfig.java
@Slf4j
@Configuration
public class RedisAutoConfig extends CachingConfigurerSupport {
@Value("${spring.redis.host}")
private String host;
@Value("${spring.redis.port}")
private Integer port;
@Value("${spring.redis.password}")
private String password;
@Value("${spring.redis.jedis.pool.max-active}")
private Integer maxActive;
@Value("${spring.redis.jedis.pool.max-wait}")
private Integer maxWait;
@Value("${spring.redis.jedis.pool.max-idle}")
private Integer maxIdle;
@Value("${spring.redis.jedis.pool.min-idle}")
private Integer minIdle;
@Value("${spring.redis.timeout}")
private Integer timeout;
@Bean
public JedisPool redisPoolFactory() {
JedisPoolConfig jedisPoolConfig = new JedisPoolConfig();
jedisPoolConfig.setBlockWhenExhausted(true); //連線數用完時是否等待,false拋錯,true會等到直到超時
jedisPoolConfig.setMaxWaitMillis(maxWait); //當連線取完時,欲取得連線的最大的等待時間
jedisPoolConfig.setTestOnBorrow(true); //取得連接線時檢查有效性
jedisPoolConfig.setTestOnReturn(false); //歸還連線時檢查有效性
jedisPoolConfig.setTestWhileIdle(false); //等待時檢查有效性
jedisPoolConfig.setMaxTotal(maxActive); //最大連線數
jedisPoolConfig.setMinIdle(minIdle); //最小空閒連線數
jedisPoolConfig.setMaxIdle(maxIdle); //最大空閒連線數
JedisPool jedisPool = null;
if(StringUtils.isBlank(password)) {
log.info("init redis with no Auth...");
jedisPool = new JedisPool(jedisPoolConfig, host, port, timeout);
} else {
log.info("init redis with Auth...");
jedisPool = new JedisPool(jedisPoolConfig, host, port, timeout, password);
}
return jedisPool;
}
}
讓我們回到短網址上吧
基於資料傳輸方便使用
我們先建立DTO
UrlShortenerDTO.java
@Getter
@Setter
@ToString
@EqualsAndHashCode(callSuper=false)
@NoArgsConstructor
@AllArgsConstructor
@JsonIgnoreProperties(ignoreUnknown = true)
public class UrlShortenerDTO extends Object {
@JsonProperty("SHORT_URL")
private String shortUrl;
@JsonProperty("REAL_URL")
private String realUrl;
@JsonProperty("EXPIRE_TIME")
private Long expireTime;
@JsonProperty("QUERY_PARAMS")
private Map<String, String> queryParams;
}
接著建立註冊短網址的method吧
1.檢核其網址是否正確
2.檢查此Url是否存在於Redis中
2-1.若不存在,進行Url的雜湊取得短網址的雜湊碼
將相關資訊存放於Redis中
2-2.若存在,則直接撈出當初的註冊資料
3.串上Server的Domain與雜湊碼
4.回覆短網址
MainService.java
public ResponseEntity<?> getShortUrl(UrlShortenerDTO dto) throws MalformedURLException {
Map<String, Object> resBodyMap = validationParams(dto);
if(!resBodyMap.isEmpty()) return ResponseEntity.badRequest().body(resBodyMap);
URL realUrl = new URL(dto.getRealUrl());
if(!jedisUtil.checkKeyIsExists("rawUrl:" + realUrl.getPath())) {
//短網址不存在,新建Record
String shortUrl = Hashing.murmur3_32().hashString(dto.getRealUrl(), Charset.defaultCharset()).toString();
if(null == dto.getQueryParams()) dto.setQueryParams(new HashMap<>());
if(null == dto.getExpireTime()) dto.setExpireTime(0L);
dto.setShortUrl(shortUrl);
log.info("短網址不存在,新建Record。RealUrl:{} ,ShortUrl:{}", dto.getRealUrl(), shortUrl);
Map<String, String> data = new HashMap<>();
data.put("DTO", PojoUtil.transBean2JsStr(dto));
data.put("ACTIVE_COUNT", "1");
if(dto.getExpireTime() > 0) {
jedisUtil.addStr("rawUrl:" + realUrl.getPath(), shortUrl, dto.getExpireTime());
jedisUtil.addHash("shortUrl:" + shortUrl, data, dto.getExpireTime());
} else {
jedisUtil.addStr("rawUrl:" + realUrl.getPath(), shortUrl);
jedisUtil.addHash("shortUrl:" + shortUrl, data);
}
} else {
//有舊的短網址,直接回傳
String shortUrl = jedisUtil.getStr("rawUrl:" + realUrl.getPath());
Map<String, String> data = jedisUtil.getHash("shortUrl:" + shortUrl);
dto = PojoUtil.transJsStr2Bean(data.get("DTO"), UrlShortenerDTO.class);
}
dto.setShortUrl(String.format("%s%s", domain, dto.getShortUrl())); //串上domain
return ResponseEntity.ok(dto);
}
新增重導向method (主要實現短網址的功能)
1.檢查有無對應的雜湊碼
2-1.若無,則導向404頁面
2-2.若有,取得先前註冊的資訊進行轉址
MainService.java
public RedirectView redirectUrl(String shortUrl) {
RedirectView rv = new RedirectView();
if(!jedisUtil.checkKeyIsExists("shortUrl:" + shortUrl)) {
log.error("無此短網址:{}", shortUrl);
rv.setUrl(pageNotFoundUrl);
return rv;
}
//觸發次數+1
jedisUtil.incrementHashSingleValue("shortUrl:" + shortUrl, "ACTIVE_COUNT", 1L);
Map<String, String> data = jedisUtil.getHash("shortUrl:" + shortUrl);
UrlShortenerDTO dto = PojoUtil.transJsStr2Bean(data.get("DTO"), UrlShortenerDTO.class);
if(!dto.getQueryParams().isEmpty()) rv.setAttributesMap(dto.getQueryParams());
rv.setUrl(dto.getRealUrl());
return rv;
}
新增另一個更新短網址物件的method
1.檢查有無對應的網址
2-1.若無就建立
2-2.若有舊更新資料(雜湊碼沿用)
3.更新完畢後串上Server的Domain與雜湊碼
4.回覆短網址
MainService.java
public ResponseEntity<?> updateShortUrl(UrlShortenerDTO dto) throws MalformedURLException {
Map<String, Object> resBodyMap = validationParams(dto);
if(!resBodyMap.isEmpty()) return ResponseEntity.badRequest().body(resBodyMap);
URL realUrl = new URL(dto.getRealUrl());
if(!jedisUtil.checkKeyIsExists("rawUrl:" + realUrl.getPath())) {
//短網址不存在,新建Record
return this.getShortUrl(dto);
} else {
//有舊的短網址,更新資料
String shortUrl = jedisUtil.getStr("rawUrl:" + realUrl.getPath());
if(null == dto.getQueryParams()) dto.setQueryParams(new HashMap<>());
if(null == dto.getExpireTime()) dto.setExpireTime(0L);
dto.setShortUrl(shortUrl);
log.info("更新Record。RealUrl:{} ,ShortUrl:{}", dto.getRealUrl(), shortUrl);
Map<String, String> data = new HashMap<>();
data.put("DTO", PojoUtil.transBean2JsStr(dto));
data.put("ACTIVE_COUNT", jedisUtil.getHash("shortUrl:" + shortUrl).get("ACTIVE_COUNT"));
if(dto.getExpireTime() > 0) {
jedisUtil.addStr("rawUrl:" + realUrl.getPath(), shortUrl, dto.getExpireTime());
jedisUtil.addHash("shortUrl:" + shortUrl, data, dto.getExpireTime());
} else {
jedisUtil.addStr("rawUrl:" + realUrl.getPath(), shortUrl);
jedisUtil.addHash("shortUrl:" + shortUrl, data);
}
}
dto.setShortUrl(String.format("%s%s", domain, dto.getShortUrl())); //串上domain
return ResponseEntity.ok(dto);
}
新增查詢現有所有短網址物件的method
MainService.java
public ResponseEntity<?> queryShortUrl() {
Set<String> allKeys = jedisUtil.getAllKeysInRedis("shortUrl:*");
List<Map> res = new ArrayList<>();
for(String key: allKeys) {
Long expireTime = jedisUtil.getExpireTime(key);
Map data = jedisUtil.getHash(key); // get Map<String, String>
UrlShortenerDTO dto = PojoUtil.transJsStr2Bean(data.get("DTO").toString(), UrlShortenerDTO.class);
data.put("DTO", dto);
data.put("REDIS_EXPIRE_TIME_TTL", expireTime);
res.add(data);
}
return ResponseEntity.ok(res);
}
最後使用RestController來建立相關接口
以及接入任何雜湊碼提供重導功能
MainController.java
@Slf4j
@RestController
public class MainController {
@Autowired
private MainService mainService;
@RequestMapping(value= "/shortUrl/get")
public ResponseEntity<?> getShortUrl(@RequestBody UrlShortenerDTO dto) throws MalformedURLException {
log.info("GetShortUrl Req:{}", dto);
return mainService.getShortUrl(dto);
}
@RequestMapping(value= "/shortUrl/update")
public ResponseEntity<?> updateShortUrl(@RequestBody UrlShortenerDTO dto) throws MalformedURLException {
log.info("UpdateShortUrl Req:{}", dto);
return mainService.updateShortUrl(dto);
}
@RequestMapping(value = "/shortUrl/query")
public ResponseEntity<?> queryShortUrl(@RequestBody Map<String, Object> params) {
log.info("QueryShortUrl Req:{}", params);
return mainService.queryShortUrl();
}
@RequestMapping(value= "/{shortUrl}",
method = RequestMethod.GET)
public RedirectView redirectUrl(@PathVariable String shortUrl) {
log.info("Redirect Req:{}", shortUrl);
RedirectView rv = mainService.redirectUrl(shortUrl);
log.info("Redirect To:{} with attributes:{}", rv.getUrl(), rv.getAttributesMap());
return rv;
}
}
想不到短網址功能這簡單吧
只是簡單的資料儲存與雜湊演算
加上一個撈資料的動作而已