一、背景
現在在各種圈的產品各種推廣地址,由於URL地址過長,不美觀、不方便收藏、發布、傳播以及各種發文字數限制等問題,微信、微博都在使用短鏈接技術。最近由於使用的三方的生成、解析短鏈接服務開始限制使用以及准備收費、不方便統計分析、不方便流控等問題,決定自建一個短地址服務。
二、原理
比如,http://a.b.com/15uOVS 這個短地址
第1步,瀏覽器請求這個地址
第2步,通過DNS后到短地址服務端,還原這個短地址對應的原始長地址。
第3步,請求http 301 或302到原始的長地址上
第4步,瀏覽器拿到原始長地址的響應response
三、實現
短地址服務的核心是短地址和長地址的轉化映射算法。
最簡單的算法是把原來的長地址做MD5摘要記為key,長地址記為value。把key value放入服務端緩存中比如redis中。
反向解析時通過URL解決出key來,比如上面的短地址key = 15uOVS 。然后通過key去緩存中獲取原始長地址value實現URL地址還原。
MD5摘要有幾個明顯的問題:
1、短地址的長度受限,比如MD5后的數據長度是32位,需要進行分段循環處理,使短地址足夠短
2、MD5的哈希碰撞問題,有一定的概率重復,解決此問題,需要不斷的提升算法的復雜度,有些得不償失
當然不止MD5實現算法比較多,大家可以自行谷歌。
筆者的實現原理如下:
1、新建一張表,主要字段包括自增ID【這里有個小技巧,可以給自增ID設置一個漂亮的起步值,使短地址碼隨機性更好一些,不建議從0開始自增,比如alter table mapping_info AUTO_INCREMENT=1112345612;】,原始長地址URL等,做一個自增的10進制長整型短地址key,然后把key轉化成62進制,映射到0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz這62個字符上,做成6位的短鏈接碼,如開頭的例子15uOVS,【 6位62進制數,對應的號碼空間為62的6次方,約等於568億,即可以生成586億的短網址,基本能滿足需求】。
2、把短地址碼及長地址,通過key value的形式放入redis和數據庫中。
3、寫一個過濾器ShortUrlMappingFilter 過濾器(為什么是過濾器,因為過濾器是對請求進行過濾轉發的,不明白自己谷歌)的執行order設為第一個執行,即值最小。
注: 攔截器、過濾器、監聽器的執行順序 ---> 監聽器 > 過濾器 > 攔截器 > controller執行 > 攔截器 > 過濾器 > 監聽器
相關核心代碼如下:
(一) 、ShortUrlMappingFilter過濾器如下:
/**
* @author xuzhujack
**/
public class ShortUrlMappingFilter implements Filter {
private static final Logger LOGGER = LoggerFactory.getLogger(ShortUrlMappingFilter.class);
private static final int shortUrlKeyLength = 6;
@Override
public void init(FilterConfig filterConfig) throws ServletException {
LOGGER.info("ShortUrlMappingFilter init ...........");
}
@Override
public void doFilter(ServletRequest servletRequest, ServletResponse servletResponse, FilterChain filterChain) throws IOException, ServletException {
LOGGER.info("ShortUrlMappingFilter url mapping start......");
HttpServletRequest request = (HttpServletRequest) servletRequest;
HttpServletResponse response = (HttpServletResponse) servletResponse;
BeanFactory factory = WebApplicationContextUtils
.getRequiredWebApplicationContext(request.getServletContext());
ShortUrlService shortUrlService = (ShortUrlService) factory.getBean("shortUrlService");
if(request.getRequestURI().substring(1).trim().length()== shortUrlKeyLength){
ShortUrlDto query = new ShortUrlDto();
query.setId(ShortUrlUtils.base62Decode(request.getRequestURI().substring(1).trim()));
ShortUrl shortUrl = shortUrlService.queryShortUrl(query);
if (shortUrl == null) {
LOGGER.error("ShortUrlMappingFilter shortUrl is null ...........dto is {}", query);
throw new ServletException();
} else {
String srcUrl = shortUrl.getSrcUrl();
LOGGER.info("ShortUrlMappingFilter shortUrl is not null .shortUrl is {}...........dto is {}", shortUrl, query);
response.sendRedirect(srcUrl);
}
}else{
filterChain.doFilter(request, response);
LOGGER.info("非短鏈接正常后續的doFilter..........");
}
}
@Override
public void destroy() {
}
}
(二)、 ShortUrlMappingFilter在項目的啟動主類中增加如下配置:
@Bean
public FilterRegistrationBean contextFilterRegistrationBean2() {
FilterRegistrationBean registrationBean = new FilterRegistrationBean();
registrationBean.setFilter(new ShortUrlMappingFilter());
registrationBean.addUrlPatterns("/*");
registrationBean.setName("ShortUrlMappingFilter");
registrationBean.setOrder(0);
return registrationBean;
}
(三)、62進制和10進制的相互轉換
/**
* 將數字轉為62進制
* @param num Long 型數字
* @return 62進制字符串
*/
public static String base62Encode(long num) {
String chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
StringBuilder sb = new StringBuilder();
int remainder = 0;
int scale = 62;
while (num > scale - 1) {
remainder = Long.valueOf(num % scale).intValue();
sb.append(chars.charAt(remainder));
num = num / scale;
}
sb.append(chars.charAt(Long.valueOf(num).intValue()));
String value = sb.reverse().toString();
return StringUtils.leftPad(value, 6, '0');
}
/**
* 62進制字符串轉為數字
* @param str 編碼后的62進制字符串
* @return 解碼后的 10 進制字符串
*/
public static long base62Decode(String str) {
String chars = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
int scale = 62;
str = str.replace("^0*", "");
long num = 0;
int index = 0;
for (int i = 0; i < str.length(); i++) {
index = chars.indexOf(str.charAt(i));
num += (long) (index * (Math.pow(scale, str.length() - i - 1)));
}
return num;
}