縮址

短網址

吳明修 2020/09/23 09:30:36
130

介紹

短網址,顧名思義
就是把原本冗長的 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;
	}
}

 

想不到短網址功能這簡單吧
只是簡單的資料儲存與雜湊演算
加上一個撈資料的動作而已

 

吳明修