前言
短链接作用
- 给用户发送链接地址的时候,避免字符过长,避免短信费用增加,以及用户点击或者复制时出现问题。
- 隐藏真实链接,避免参数等信息直接暴露出来
- 做链接失效时,不用去动真实链接,可以直接在短链接这里拦截操作
百度短网址
项目之前使用的是百度短网址,本来是挺好用的,但是后面开始要求 网站注册,然后又分长期和短期,然后又开始又要收费啦,最关键的一点,有时候链接里面包含一些关键字的时候会被误判为违规字符,然后无法生成。这个就比较影响项目了,后面就想着自己实现一套,自己内部使用 redis 然后一个简单不重复的加密算法,数据量并发什么的,单公司内部应用是可以抗住。
实现
- 使用雪花算法生成一个唯一 id
- 使用生成的 id 调用 encode 加密方法,获取一个 不会重复的 字母串
- 使用特殊标识+字母串作为 key , 真实网址作为 value ,存入 redis 及数据库中 ,返回 访问地址加字母串
- 访问时,通过字母串查询到真实地址并转发
代码
加密代码
import java.util.Stack;
/**
* @Description 加解密算法
* @Author ChengQichuan
* @Version V1.0.0
* @Since 1.0
* @Date 2020/4/26
*/
public class PECode {
/**
* 打乱改字符数组的组合顺序,可以得到不同的转换结果
*/
private static final char[] array = { 'q', 'w', 'e', 'r', 't', 'y', 'u', 'i', 'o', 'p', 'a', 's', 'd', 'f', 'g',
'h', 'j', 'k', 'l', 'z', 'x', 'c', 'v', 'b', 'n', 'm', '8', '5', '2', '7', '3', '6', '4', '0', '9', '1',
'Q', 'W', 'E', 'R', 'T', 'Y', 'U', 'I', 'O', 'P', 'A', 'S', 'D', 'F', 'G', 'H', 'J', 'K', 'L', 'Z', 'X',
'C', 'V', 'B', 'N', 'M' };
/**
* @param number double类型的10进制数,该数必须大于0
*/
public static String encode(Long number) {
double rest = number;
// 创建栈
Stack<Character> stack = new Stack<Character>();
StringBuilder result = new StringBuilder(0);
while (rest >= 1) {
// 进栈,
stack.add(array[new Double(rest % array.length).intValue()]);
rest = rest / array.length;
}
for (; !stack.isEmpty();) {
// 出栈
result.append(stack.pop());
}
return result.toString();
}
/**
* 支持范围是A-Z,a-z,0-9,+,-
*
*/
public static Long decode(String str) {
// 倍数
int multiple = 1;
Long result = 0L;
Character c;
for (int i = 0; i < str.length(); i++) {
c = str.charAt(str.length() - i - 1);
result += decodeChar(c) * multiple;
multiple = multiple * array.length;
}
return result;
}
private static int decodeChar(Character c) {
for (int i = 0; i < array.length; i++) {
if (c == array[i]) {
return i;
}
}
return -1;
}
}
Service 实现
import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.cf.ai.common.utils.PECode;
import com.cf.ai.model.dao.master.entity.CfAiDwz;
import com.cf.ai.model.dao.redis.RedisUtils;
import com.cf.ai.common.utils.SnowFlake.IdSnowFlake;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Service;
import java.util.Date;
import java.util.HashMap;
import java.util.Map;
/**
* @Description
* @Author ChengQichuan
* @Version V1.0.0
* @Since 1.0
* @Date 2020/5/11
*/
@Service
public class DwzService {
/**
* 服务对外的地址
*/
@Value("${dwz.url}")
private String selfUrl;
/**
* 有效时间
*/
@Value("${dwz.time}")
private long time;
/**
* 拼接地址
*/
private String urlStr = "/dwz/go/";
@Autowired
RedisUtils redisUtils;
@Autowired
CfAiDwzService cfAiDwzService;
/**
* 创建短连接
* @param longUrl
* @return
*/
public Map create(String longUrl){
Map map = new HashMap();
//雪花算法id
Long snowFlakeId = Long.valueOf(IdSnowFlake.nextId());
String url = PECode.encode(snowFlakeId);
if(StringUtils.isBlank(url)){
map.put("error","生成失败");
}
//生成的短链接
String shortUrl = selfUrl + urlStr + url;
//保存redis
redisUtils.set("dwz:" + url, longUrl, time);
//保存数据库
CfAiDwz cfAiDwz = new CfAiDwz();
cfAiDwz.setCreatedTime(new Date());
cfAiDwz.setLongUrl(longUrl);
cfAiDwz.setUrl(url);
cfAiDwzService.save(cfAiDwz);
map.put("shortUrl",shortUrl);
return map;
}
/**
* 访问url
* @param url
* @return
*/
public Map goUrl(String url){
Map map = new HashMap();
// 先从redis取
String longUrl = String.valueOf(redisUtils.get("dwz:" + url));
if (longUrl == null || "null".equals(longUrl)){
//如果 redis 没取到,就从 数据库取
QueryWrapper queryWrapper = new QueryWrapper();
queryWrapper.eq("URL",url);
queryWrapper.orderByDesc("CREATED_TIME");
CfAiDwz cfAiDwz = cfAiDwzService.getOne(queryWrapper);
if (cfAiDwz == null){
map.put("error","短连接有误");
}
longUrl = cfAiDwz.getLongUrl();
}
map.put("longUrl",longUrl);
return map;
}
/**
* 查询原地址
* @param shortUrl
* @return
*/
public Map query(String shortUrl){
//还原长链接
String splitStr = selfUrl + urlStr;
String[] ArrayStr = shortUrl.split(splitStr);
Map map = goUrl(ArrayStr[1]);
return map;
}
}
Controller 实现
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.cf.ai.api.common.annotation.ApiLog;
import com.cf.ai.api.common.exception.ApiException;
import com.cf.ai.api.common.response.Result;
import com.cf.ai.model.service.dwz.DwzService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.apache.commons.lang3.StringUtils;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.*;
import java.util.Map;
import java.util.regex.Matcher;
import java.util.regex.Pattern;
/**
* @Description 短链接
* @Author ChengQichuan
* @Version V1.0.0
* @Since 1.0
* @Date 2020/4/26
*/
@Api(tags = "短链接")
@Controller
@RequestMapping("/api/dwz")
public class DwzController {
private final Logger logger = LoggerFactory.getLogger(this.getClass());
@Autowired
DwzService dwzService;
@ApiLog(apiName = "短网址生成")
@ApiOperation("短网址生成")
@PostMapping("/create")
@ResponseBody
public Result create(
@ApiParam(name="链接地址",value="{\"longUrl\":\"http://localhost\"}",required=true)
@RequestBody JSONObject jsonObject){
logger.info("===================== 短网址生成接口 /create:{}", JSON.toJSONString(jsonObject));
String longUrl = jsonObject.getString("longUrl");
if(StringUtils.isBlank(longUrl)){
throw new ApiException("链接不能为空");
}
String regEx="^[hH][tT][tT][pP]([sS]?):\\/\\/(\\S+\\.)+\\S{2,}$";
Pattern p = Pattern.compile(regEx);
Matcher m = p.matcher(longUrl);
if (!m.matches() || m.start() != 0){
return Result.fail("网址不合规:"+longUrl);
}
Map map = dwzService.create(longUrl);
if (map.get("error") != null){
return Result.fail(String.valueOf(map.get("error")));
}
String shortUrl = String.valueOf(map.get("shortUrl"));
logger.info("=====================短网址生成接口 shortUrl:{}", shortUrl);
return Result.ok(shortUrl);
}
@ApiLog(apiName = "短网址访问")
@ApiOperation("短网址访问")
@GetMapping("/go/{url}")
public String goUrl(@PathVariable String url){
logger.info("=====================短网址访问接口 url:{}", url);
if(StringUtils.isBlank(url)){
throw new ApiException("链接不能为空");
}
Map map = dwzService.goUrl(url);
if (map.get("error") != null){
throw new ApiException(String.valueOf(map.get("error")));
}
String longUrl = String.valueOf(map.get("longUrl"));
return "redirect:" + longUrl;
}
@ApiLog(apiName = "短网址还原")
@ApiOperation("短网址还原")
@PostMapping("/query")
@ResponseBody
public Result query(@RequestBody JSONObject jsonObject){
logger.info("=====================短网址还原接口 query:{}", JSON.toJSONString(jsonObject));
String shortUrl = jsonObject.getString("shortUrl");
if(StringUtils.isBlank(shortUrl)){
throw new ApiException("链接不能为空");
}
Map map = dwzService.query(shortUrl);
if (map.get("error") != null){
throw new ApiException(String.valueOf(map.get("error")));
}
String longUrl = String.valueOf(map.get("longUrl"));
return Result.ok(longUrl);
}
}
雪花算法
生成唯一 id 使用
import java.lang.management.ManagementFactory;
import java.lang.management.RuntimeMXBean;
import java.net.NetworkInterface;
import java.net.SocketException;
import java.util.Enumeration;
/**
* 雪花算法
*/
public class IdSnowFlake {
private final static long twepoch = 12888349746579L;
// 机器标识位数
private final static long workerIdBits = 5L;
// 数据中心标识位数
private final static long datacenterIdBits = 5L;
// 毫秒内自增位数
private final static long sequenceBits = 12L;
// 机器ID偏左移12位
private final static long workerIdShift = sequenceBits;
// 数据中心ID左移17位
private final static long datacenterIdShift = sequenceBits + workerIdBits;
// 时间毫秒左移22位
private final static long timestampLeftShift = sequenceBits + workerIdBits + datacenterIdBits;
//sequence掩码,确保sequnce不会超出上限
private final static long sequenceMask = -1L ^ (-1L << sequenceBits);
//上次时间戳
private static long lastTimestamp = -1L;
//序列
private long sequence = 0L;
//服务器ID
private long workerId = 1L;
private static long workerMask = -1L ^ (-1L << workerIdBits);
//进程编码
private long processId = 1L;
private static long processMask = -1L ^ (-1L << datacenterIdBits);
private static IdSnowFlake snowFlake = null;
static{
snowFlake = new IdSnowFlake();
}
public static synchronized String nextId(){
return String.valueOf(snowFlake.getNextId());
}
private IdSnowFlake() {
//获取机器编码
this.workerId=this.getMachineNum();
//获取进程编码
RuntimeMXBean runtimeMXBean = ManagementFactory.getRuntimeMXBean();
this.processId=Long.valueOf(runtimeMXBean.getName().split("@")[0]).longValue();
//避免编码超出最大值
this.workerId=workerId & workerMask;
this.processId=processId & processMask;
}
public synchronized long getNextId() {
//获取时间戳
long timestamp = timeGen();
//如果时间戳小于上次时间戳则报错
if (timestamp < lastTimestamp) {
try {
throw new Exception("Clock moved backwards. Refusing to generate id for " + (lastTimestamp - timestamp) + " milliseconds");
} catch (Exception e) {
e.printStackTrace();
}
}
//如果时间戳与上次时间戳相同
if (lastTimestamp == timestamp) {
// 当前毫秒内,则+1,与sequenceMask确保sequence不会超出上限
sequence = (sequence + 1) & sequenceMask;
if (sequence == 0) {
// 当前毫秒内计数满了,则等待下一秒
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0;
}
lastTimestamp = timestamp;
// ID偏移组合生成最终的ID,并返回ID
long nextId = ((timestamp - twepoch) << timestampLeftShift) | (processId << datacenterIdShift) | (workerId << workerIdShift) | sequence;
return nextId;
}
/**
* 再次获取时间戳直到获取的时间戳与现有的不同
* @param lastTimestamp
* @return 下一个时间戳
*/
private long tilNextMillis(final long lastTimestamp) {
long timestamp = this.timeGen();
while (timestamp <= lastTimestamp) {
timestamp = this.timeGen();
}
return timestamp;
}
private long timeGen() {
return System.currentTimeMillis();
}
/**
* 获取机器编码
* @return
*/
private long getMachineNum(){
long machinePiece;
StringBuilder sb = new StringBuilder();
Enumeration<NetworkInterface> e = null;
try {
e = NetworkInterface.getNetworkInterfaces();
} catch (SocketException e1) {
e1.printStackTrace();
}
while (e.hasMoreElements()) {
NetworkInterface ni = e.nextElement();
sb.append(ni.toString());
}
machinePiece = sb.toString().hashCode();
return machinePiece;
}
}