java实现长链接转为短链接
我们经常看到微博和短信中用到了短链接,其目的就是能够将冗余的长链接精简。
然后在码云上看到一个生成短链接一个项目:urlshorter: 满足多种场景下的短链接生成需求 (gitee.com),然后自己在此基础上修改了一下,本人技术有限,仅供参考
一、随机字符串发生器
public class SnowFlakeGeneratorRandom implements StringGenerator {
@Override
public String generate(String url) {
SnowFlakeShortUrl snowFlake = new SnowFlakeShortUrl(2, 3);
Long id = snowFlake.nextId(); //10进制
return NumericConvertUtils.toOtherNumberSystem(id, 62); //62进制
}
@Override
public void setLength(int length) {}
}
public class NumericConvertUtils {
/**
* 在进制表示中的字符集合,0-Z分别用于表示最大为62进制的符号表示
*/
private static final char[] digits = {'0', '1', '2', '3', '4', '5', '6', '7', '8', '9',
'a', 'b', 'c', 'd', 'e', 'f', 'g', 'h', 'i', 'j', 'k', 'l', 'm',
'n', 'o', 'p', 'q', 'r', 's', 't', 'u', 'v', 'w', 'x', 'y', 'z',
'A', 'B', 'C', 'D', 'E', 'F', 'G', 'H', 'I', 'J', 'K', 'L', 'M',
'N', 'O', 'P', 'Q', 'R', 'S', 'T', 'U', 'V', 'W', 'X', 'Y', 'Z'};
/**
* 将十进制的数字转换为指定进制的字符串
* @param number 十进制的数字
* @param seed 指定的进制
* @return 指定进制的字符串
*/
public static String toOtherNumberSystem(long number, int seed) {
if (number < 0) {
number = ((long) 2 * 0x7fffffff) + number + 2;
}
char[] buf = new char[32];
int charPos = 32;
while ((number / seed) > 0) {
buf[--charPos] = digits[(int) (number % seed)];
number /= seed;
}
buf[--charPos] = digits[(int) (number % seed)];
return new String(buf, charPos, (32 - charPos));
}
}
public class SnowFlakeShortUrl {
/**
* 起始的时间戳
*/
private final static long START_TIMESTAMP = 1480166465631L;
/**
* 每一部分占用的位数
*/
private final static long SEQUENCE_BIT = 12; //序列号占用的位数
private final static long MACHINE_BIT = 5; //机器标识占用的位数
private final static long DATA_CENTER_BIT = 5; //数据中心占用的位数
/**
* 每一部分的最大值
*/
private final static long MAX_SEQUENCE = -1L ^ (-1L << SEQUENCE_BIT);
private final static long MAX_MACHINE_NUM = -1L ^ (-1L << MACHINE_BIT);
private final static long MAX_DATA_CENTER_NUM = -1L ^ (-1L << DATA_CENTER_BIT);
/**
* 每一部分向左的位移
*/
private final static long MACHINE_LEFT = SEQUENCE_BIT;
private final static long DATA_CENTER_LEFT = SEQUENCE_BIT + MACHINE_BIT;
private final static long TIMESTAMP_LEFT = DATA_CENTER_LEFT + DATA_CENTER_BIT;
private long dataCenterId; //数据中心
private long machineId; //机器标识
private long sequence = 0L; //序列号
private long lastTimeStamp = -1L; //上一次时间戳
/**
* 根据指定的数据中心ID和机器标志ID生成指定的序列号
* @param dataCenterId 数据中心ID
* @param machineId 机器标志ID
*/
public SnowFlakeShortUrl(long dataCenterId, long machineId) {
if (dataCenterId > MAX_DATA_CENTER_NUM || dataCenterId < 0) {
throw new IllegalArgumentException("DtaCenterId can't be greater than MAX_DATA_CENTER_NUM or less than 0!");
}
if (machineId > MAX_MACHINE_NUM || machineId < 0) {
throw new IllegalArgumentException("MachineId can't be greater than MAX_MACHINE_NUM or less than 0!");
}
this.dataCenterId = dataCenterId;
this.machineId = machineId;
}
/**
* 产生下一个ID
* @return
*/
public synchronized long nextId() {
long currTimeStamp = getNewTimeStamp();
if (currTimeStamp < lastTimeStamp) {
throw new RuntimeException("Clock moved backwards. Refusing to generate id");
}
if (currTimeStamp == lastTimeStamp) {
//相同毫秒内,序列号自增
sequence = (sequence + 1) & MAX_SEQUENCE;
//同一毫秒的序列数已经达到最大
if (sequence == 0L) {
currTimeStamp = getNextMill();
}
} else {
//不同毫秒内,序列号置为0
sequence = 0L;
}
lastTimeStamp = currTimeStamp;
return (currTimeStamp - START_TIMESTAMP) << TIMESTAMP_LEFT //时间戳部分
| dataCenterId << DATA_CENTER_LEFT //数据中心部分
| machineId << MACHINE_LEFT //机器标识部分
| sequence; //序列号部分
}
private long getNextMill() {
long mill = getNewTimeStamp();
while (mill <= lastTimeStamp) {
mill = getNewTimeStamp();
}
return mill;
}
private long getNewTimeStamp() {
return System.currentTimeMillis();
}
}
二、存储短链接映射方法
public class ShorterStorageMemory<T extends ShorterGetter> implements ShorterStorage<T> {
/**
* 存储shorter,url
*/
private static RedisUtils redisUtils = SpringUtils.getBean(RedisUtils.class);
private static final Long expiration_time = 60 * 60 * 24L;
public String get(String shorterKey) {
Object o = redisUtils.get(shorterKey);
if (ObjectUtils.isNotEmpty(o)) {
return o + "";
}
return null;
}
public String getMethod(String shorterKey) {
Object o = redisUtils.get(shorterKey);
if (ObjectUtils.isNotEmpty(o)) {
String url = o + "";
Object o1 = redisUtils.get(url);
if (ObjectUtils.isNotEmpty(o1)) {
return o1.getClass().getName();
}
}
return null;
}
public String getMethodClass(String url) {
Object o = redisUtils.get(url);
if (ObjectUtils.isNotEmpty(o)) {
return JsonUtil.ObjectToJson(o);
}
return null;
}
public String getShortUrl(String url) {
T o = (T) redisUtils.get(url);
if (ObjectUtils.isNotEmpty(o)) {
return o.getShorter();
}
return null;
}
public void clean(String url) {
ShorterGetter shorter = (ShorterGetter) redisUtils.get(url);
if (shorter != null) {
redisUtils.delete(url);
redisUtils.delete(shorter.getShorter());
}
}
@Override
public void cleanShorter(String shorter) {}
/**
* 保存映射
* @param url
* @param shorter
*/
public void save(String url, T shorter) {
redisUtils.set(url,shorter,expiration_time);
redisUtils.set(shorter.getShorter(),url,expiration_time);
}
/**
* 保存带失效时间的映射
* @param url
* @param shorter
* @param time
*/
@Override
public void save(String url, T shorter, long time) {
redisUtils.set(url,shorter,time);
redisUtils.set(shorter.getShorter(),url,time);
}
/**
* 短链接续期
* @param url
* @param shorter
*/
@Override
public void renew(String url, T shorter) {
redisUtils.expire(url,expiration_time, TimeUnit.SECONDS);
redisUtils.expire(shorter.getShorter(),expiration_time, TimeUnit.SECONDS);
}
@Override
public void clean() {}
}
三、短链接的服务
@Slf4j
@RestController
@RequestMapping("/")
public class UrlController {
@Autowired
private ShortUrlService shortUrlService;
@PostMapping
public String createShortUrl(@RequestBody ShortUrlRequest request) {
return shortUrlService.generationShortUrl(request.getUrl(),request.getMethod(),1000L,1000L);
}
@GetMapping("/{shortUrl}")
public void url(@PathVariable("shortUrl") String shortUrl,HttpServletResponse response) throws IOException {
shortUrlService.getUrl(shortUrl,response);
}
}
@Slf4j
@Service
public class ShortUrlService {
private static final int urlLen = 6;
private static final String HTTP_PREFIX = "http://127.0.0.1:8088/";
@Autowired
private RedisUtils redisUtils;
/**
* 生成短链接
*
* @param url
* @return
*/
public String generationShortUrl(String url, String method, Long period, Long time) {
if (redisUtils.hasKey(url)) {
ShorterStorageMemory storageMemory = new ShorterStorageMemory();
return HTTP_PREFIX + storageMemory.getShortUrl(url);
}
GeneratorShortUrlMethodEnum generatorShortUrlMethodEnum = GeneratorShortUrlMethodEnum.getShortUrlMethodEnum(method);
if (generatorShortUrlMethodEnum == null) {
throw new RuntimeException("方法参数有误");
}
String shortUrl = "";
synchronized (this) {
UrlShorterGeneratorFactory factory = new UrlShorterGeneratorFactory();
UrlShorterGenerator urlShorterGenerator = factory.getShortUrl(generatorShortUrlMethodEnum, period, time);
shortUrl = urlShorterGenerator.generate(url).getShorter();
return HTTP_PREFIX + shortUrl;
}
}
/**
* 根据短链接获取原本的链接
*
* @param shortUrl
* @return
*/
public void getUrl(String shortUrl, HttpServletResponse response) {
try {
ShorterStorageMemory shorterStringShorterStorageMemory = new ShorterStorageMemory<>();
String url = shorterStringShorterStorageMemory.get(shortUrl);
if (StringUtils.isBlank(url)) {
throw new RuntimeException("该链接已失效");
}
String method = shorterStringShorterStorageMemory.getMethod(shortUrl);
String key = shortUrl + "_increment";
if (method.contains("ShorterString")) {
ShorterString shorterString = JsonUtil.JsonToObject(shorterStringShorterStorageMemory.getMethodClass(url), ShorterString.class);
shorterStringShorterStorageMemory.renew(url,shorterString);
} else if (method.contains("ShorterWithPassword")) {
ShorterWithPassword shorterWithPassword = JsonUtil.JsonToObject(shorterStringShorterStorageMemory.getMethodClass(url), ShorterWithPassword.class);
} else if (method.contains("ShorterWithPeriod")) {
ShorterWithPeriod shorterWithPeriod = JsonUtil.JsonToObject(shorterStringShorterStorageMemory.getMethodClass(url), ShorterWithPeriod.class);
shorterStringShorterStorageMemory.renew(url,shorterWithPeriod);
limitCount(shorterStringShorterStorageMemory, url, key, shorterWithPeriod.getPeriod());
} else if (method.contains("ShorterWithPeriodAndTimes")) {
ShorterWithPeriodAndTimes shorterWithPeriodAndTimes = JsonUtil.JsonToObject(shorterStringShorterStorageMemory.getMethodClass(url), ShorterWithPeriodAndTimes.class);
limitCount(shorterStringShorterStorageMemory,url,key,shorterWithPeriodAndTimes.getPeriod());
} else if (method.contains("ShorterWithTimes")) {
ShorterWithTimes shorterWithTimes = JsonUtil.JsonToObject(shorterStringShorterStorageMemory.getMethodClass(url), ShorterWithTimes.class);
}
if (ObjectUtils.isNotEmpty(url) && StringUtils.isNotBlank(method)) {
response.sendRedirect(url);
}
} catch (Exception e) {
log.error("获取短链接失败:{}", e.getMessage());
throw new RuntimeException("该链接已失效");
}
}
public void limitCount(ShorterStorageMemory memory, String url, String key, long count) {
if (redisUtils.hasKey(key)) {
int limit = Integer.parseInt(redisUtils.get(key) + "");
if (limit > count) {
memory.clean(url);
redisUtils.delete(key);
throw new RuntimeException("该链接已失效");
}
redisUtils.incrBy(key, 1L);
} else {
redisUtils.set(key, 1);
}
}
public class UrlShorterGeneratorFactory {
public UrlShorterGenerator getShortUrl(GeneratorShortUrlMethodEnum shortUrlMethodEnum, long period, long time) {
UrlShorterGenerator urlShorterGenerator = null;
switch (shortUrlMethodEnum) {
case SHORT_URL_LIMIT:
urlShorterGenerator = getUrlShorterGeneratorSimple();
break;
case SHORT_URL_LIMIT_PERIOD:
urlShorterGenerator = getUrlShorterGeneratorLimitPeriod(period);
break;
case SHORT_URL_LIMIT_TIME:
urlShorterGenerator = getUrlShorterGeneratorLimitTimes(time);
case SHORT_URL_LIMIT_PERIOD_TIME:
urlShorterGenerator = getUrlShorterGeneratorLimitPeriodAndTimes(period, time);
case SHORT_URL_LIMIT_PASSWORD:
urlShorterGenerator = getUrlShorterGeneratorWithPassword();
default:
throw new RuntimeException();
}
return urlShorterGenerator;
}
private UrlShorterGeneratorSimple getUrlShorterGeneratorSimple() {
UrlShorterGeneratorSimple simple = new UrlShorterGeneratorSimple();
simple.setGenerator(new SnowFlakeGeneratorRandom());
simple.setShorterStorage(new ShorterStorageMemory<ShorterString>());
return simple;
}
private UrlShorterGeneratorLimitPeriod getUrlShorterGeneratorLimitPeriod(long count) {
UrlShorterGeneratorLimitPeriod limitPeriod = new UrlShorterGeneratorLimitPeriod();
limitPeriod.setGenerator(new SnowFlakeGeneratorRandom());
limitPeriod.setPeriod(count);
limitPeriod.setShorterStorage(new ShorterStorageMemory<ShorterWithPeriod>());
return limitPeriod;
}
private UrlShorterGeneratorLimitPeriodAndTimes getUrlShorterGeneratorLimitPeriodAndTimes(long count, long time) {
UrlShorterGeneratorLimitPeriodAndTimes limitPeriodAndTimes = new UrlShorterGeneratorLimitPeriodAndTimes();
limitPeriodAndTimes.setGenerator(new SnowFlakeGeneratorRandom());
limitPeriodAndTimes.setPeriod(count);
limitPeriodAndTimes.setTimes(time);
limitPeriodAndTimes.setShorterStorage(new ShorterStorageMemory<ShorterWithPeriodAndTimes>());
return limitPeriodAndTimes;
}
private UrlShorterGeneratorLimitTimes getUrlShorterGeneratorLimitTimes(long time) {
UrlShorterGeneratorLimitTimes limitTimes = new UrlShorterGeneratorLimitTimes();
limitTimes.setGenerator(new SnowFlakeGeneratorRandom());
limitTimes.setTimes(time);
limitTimes.setShorterStorage(new ShorterStorageMemory<ShorterWithTimes>());
return limitTimes;
}
private UrlShorterGeneratorWithPassword getUrlShorterGeneratorWithPassword() {
UrlShorterGeneratorWithPassword withPassword = new UrlShorterGeneratorWithPassword();
withPassword.setShorterGenerator(new SnowFlakeGeneratorRandom());
withPassword.setPasswordGenerator(new StringGeneratorRandom());
withPassword.setShorterStorage(new ShorterStorageMemory<ShorterWithPassword>());
return withPassword;
}
}
四、其余类
@Getter
@NoArgsConstructor
@AllArgsConstructor
public enum GeneratorShortUrlMethodEnum {
SHORT_URL_LIMIT_PERIOD("limit.period","用于生成限制访问次数的短链接"),
SHORT_URL_LIMIT_PERIOD_TIME("limit.period.time","用于生成限制访问次数和生效时间的短链接"),
SHORT_URL_LIMIT_TIME("limit.time","用于生成限制生效时间的短链接"),
SHORT_URL_LIMIT("limit","用于短链接"),
SHORT_URL_LIMIT_PASSWORD("limit.password","用于生成带密码的短链接");
private String method;
private String methodDesc;
public static GeneratorShortUrlMethodEnum getShortUrlMethodEnum(String method) {
if (StringUtils.isBlank(method)) {
return null;
}
for (GeneratorShortUrlMethodEnum s : GeneratorShortUrlMethodEnum.values()) {
if (method.equals(s.getMethod())) {
return s;
}
}
return null;
}
}
五、说明
1.若没有指定失效时间,默认失效时间是24小时
2.本次保存短链接映射的使用的是redis,当然也可以用mysql等
3.生成短链接的前缀是自己服务器的域名,这样就可以通过在网页输入短链接直接跳
转到原链接,自己也可以记录访问短链接的次数;
注:这边方法一些类没有写,这个可以去上面作者的链接去看