本文目录导读:

我来介绍几种Java实现短链接的方式,从简单到完整方案:
基础实现:哈希+进制转换
import java.security.MessageDigest;
import java.security.NoSuchAlgorithmException;
public class SimpleShortUrl {
private static final String BASE62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
/**
* 将10进制数转为62进制
*/
public static String toBase62(long num) {
StringBuilder sb = new StringBuilder();
while (num > 0) {
sb.append(BASE62.charAt((int)(num % 62)));
num /= 62;
}
return sb.reverse().toString();
}
/**
* 生成短链接
*/
public static String generateShortUrl(String longUrl) {
try {
// 使用MD5生成哈希
MessageDigest md5 = MessageDigest.getInstance("MD5");
md5.update(longUrl.getBytes());
byte[] digest = md5.digest();
// 取前6位作为短码
long hash = 0;
for (int i = 0; i < 6; i++) {
hash = (hash << 8) | (digest[i] & 0xFF);
}
// 转为62进制
return toBase62(hash);
} catch (NoSuchAlgorithmException e) {
e.printStackTrace();
return null;
}
}
public static void main(String[] args) {
String url = "https://www.example.com/very/long/url/path";
String shortCode = generateShortUrl(url);
System.out.println("原始URL: " + url);
System.out.println("短链接码: " + shortCode);
System.out.println("短链接: https://short.url/" + shortCode);
}
}
自增ID实现(防止冲突)
import java.util.concurrent.ConcurrentHashMap;
import java.util.concurrent.atomic.AtomicLong;
public class IncrementShortUrl {
private static final String BASE62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
private final AtomicLong counter = new AtomicLong(1000000);
private final ConcurrentHashMap<String, String> urlMap = new ConcurrentHashMap<>();
private final ConcurrentHashMap<String, String> shortMap = new ConcurrentHashMap<>();
/**
* 生成短链接
*/
public String shorten(String longUrl) {
// 检查是否已存在
String existing = urlMap.get(longUrl);
if (existing != null) {
return existing;
}
// 生成新ID并转换
long id = counter.incrementAndGet();
String shortCode = toBase62(id);
// 存储映射关系
urlMap.put(longUrl, shortCode);
shortMap.put(shortCode, longUrl);
return shortCode;
}
/**
* 根据短码获取原始URL
*/
public String getOriginalUrl(String shortCode) {
return shortMap.get(shortCode);
}
private String toBase62(long num) {
StringBuilder sb = new StringBuilder();
do {
sb.insert(0, BASE62.charAt((int)(num % 62)));
num /= 62;
} while (num > 0);
return sb.toString();
}
public static void main(String[] args) {
IncrementShortUrl shortUrl = new IncrementShortUrl();
String url1 = "https://www.example.com/article/2024/01/15/how-to-create-short-url";
String url2 = "https://www.example.com/product/12345/details";
System.out.println("短链接1: " + shortUrl.shorten(url1));
System.out.println("短链接2: " + shortUrl.shorten(url2));
// 还原
System.out.println("还原1: " + shortUrl.getOriginalUrl("4C92"));
System.out.println("还原2: " + shortUrl.getOriginalUrl("4C93"));
}
}
完整Spring Boot实现
1 数据库表结构
CREATE TABLE `short_url` (
`id` bigint(20) NOT NULL AUTO_INCREMENT,
`short_code` varchar(10) NOT NULL COMMENT '短链接码',
`long_url` varchar(500) NOT NULL COMMENT '原始URL',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP,
`expire_time` datetime DEFAULT NULL,
`click_count` int(11) DEFAULT '0',
PRIMARY KEY (`id`),
UNIQUE KEY `idx_short_code` (`short_code`),
KEY `idx_long_url` (`long_url`(191))
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4;
2 实体类
import javax.persistence.*;
import java.time.LocalDateTime;
@Entity
@Table(name = "short_url")
public class ShortUrl {
@Id
@GeneratedValue(strategy = GenerationType.IDENTITY)
private Long id;
@Column(name = "short_code", unique = true, length = 10)
private String shortCode;
@Column(name = "long_url", length = 500)
private String longUrl;
@Column(name = "create_time")
private LocalDateTime createTime;
@Column(name = "expire_time")
private LocalDateTime expireTime;
@Column(name = "click_count")
private Integer clickCount = 0;
// getters and setters
}
3 服务层实现
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import java.security.SecureRandom;
import java.time.LocalDateTime;
import java.util.Optional;
@Service
public class ShortUrlService {
@Autowired
private ShortUrlRepository repository;
private static final String BASE62 = "0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
private static final int SHORT_CODE_LENGTH = 6;
private SecureRandom random = new SecureRandom();
@Transactional
public String createShortUrl(String longUrl, int expireDays) {
// 检查是否已存在
Optional<ShortUrl> existing = repository.findByLongUrl(longUrl);
if (existing.isPresent()) {
return existing.get().getShortCode();
}
// 生成短码
String shortCode = generateShortCode();
ShortUrl shortUrl = new ShortUrl();
shortUrl.setShortCode(shortCode);
shortUrl.setLongUrl(longUrl);
shortUrl.setCreateTime(LocalDateTime.now());
shortUrl.setClickCount(0);
if (expireDays > 0) {
shortUrl.setExpireTime(LocalDateTime.now().plusDays(expireDays));
}
repository.save(shortUrl);
return shortCode;
}
public String getLongUrl(String shortCode) {
Optional<ShortUrl> shortUrl = repository.findByShortCode(shortCode);
if (shortUrl.isPresent()) {
ShortUrl url = shortUrl.get();
// 检查是否过期
if (url.getExpireTime() != null && url.getExpireTime().isBefore(LocalDateTime.now())) {
throw new RuntimeException("短链接已过期");
}
// 增加点击次数
repository.incrementClickCount(shortCode);
return url.getLongUrl();
}
throw new RuntimeException("短链接不存在");
}
private String generateShortCode() {
StringBuilder sb = new StringBuilder();
for (int i = 0; i < SHORT_CODE_LENGTH; i++) {
sb.append(BASE62.charAt(random.nextInt(62)));
}
// 检查是否已存在
String code = sb.toString();
if (repository.findByShortCode(code).isPresent()) {
return generateShortCode(); // 递归生成直到唯一
}
return code;
}
}
4 控制器
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import java.net.URI;
@RestController
@RequestMapping("/api")
public class ShortUrlController {
@Autowired
private ShortUrlService shortUrlService;
@PostMapping("/shorten")
public ResponseEntity<?> createShortUrl(@RequestParam String url,
@RequestParam(defaultValue = "0") int expireDays) {
try {
String shortCode = shortUrlService.createShortUrl(url, expireDays);
String shortUrl = "http://localhost:8080/s/" + shortCode;
return ResponseEntity.ok(new ShortUrlResponse(shortUrl, shortCode));
} catch (Exception e) {
return ResponseEntity.badRequest().body("创建短链接失败: " + e.getMessage());
}
}
@GetMapping("/s/{shortCode}")
public ResponseEntity<?> redirect(@PathVariable String shortCode) {
try {
String longUrl = shortUrlService.getLongUrl(shortCode);
HttpHeaders headers = new HttpHeaders();
headers.setLocation(URI.create(longUrl));
return new ResponseEntity<>(headers, HttpStatus.FOUND);
} catch (Exception e) {
return ResponseEntity.notFound().build();
}
}
}
class ShortUrlResponse {
private String shortUrl;
private String shortCode;
public ShortUrlResponse(String shortUrl, String shortCode) {
this.shortUrl = shortUrl;
this.shortCode = shortCode;
}
// getters
}
5 性能优化版本(使用缓存)
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.stereotype.Service;
@Service
public class CachedShortUrlService {
@Autowired
private ShortUrlRepository repository;
@Cacheable(value = "shortUrls", key = "#shortCode")
public String getLongUrl(String shortCode) {
ShortUrl shortUrl = repository.findByShortCode(shortCode)
.orElseThrow(() -> new RuntimeException("短链接不存在"));
if (shortUrl.getExpireTime() != null &&
shortUrl.getExpireTime().isBefore(LocalDateTime.now())) {
throw new RuntimeException("短链接已过期");
}
return shortUrl.getLongUrl();
}
@CacheEvict(value = "shortUrls", key = "#shortCode")
public void evictCache(String shortCode) {
// 清除缓存
}
}
雪花算法ID生成器
public class SnowflakeIdGenerator {
private final long workerId;
private final long datacenterId;
private long sequence = 0L;
private long lastTimestamp = -1L;
private static final long WORKER_ID_BITS = 5L;
private static final long DATACENTER_ID_BITS = 5L;
private static final long SEQUENCE_BITS = 12L;
private static final long MAX_WORKER_ID = ~(-1L << WORKER_ID_BITS);
private static final long MAX_DATACENTER_ID = ~(-1L << DATACENTER_ID_BITS);
// 时序
protected long timeGen() {
return System.currentTimeMillis();
}
public synchronized long nextId() {
long timestamp = timeGen();
if (timestamp < lastTimestamp) {
throw new RuntimeException("时钟回拨");
}
if (lastTimestamp == timestamp) {
sequence = (sequence + 1) & SEQUENCE_MASK;
if (sequence == 0) {
timestamp = tilNextMillis(lastTimestamp);
}
} else {
sequence = 0L;
}
lastTimestamp = timestamp;
return ((timestamp - twepoch) << TIMESTAMP_LEFT_SHIFT) |
(datacenterId << DATACENTER_ID_SHIFT) |
(workerId << WORKER_ID_SHIFT) |
sequence;
}
}
使用建议
- 简单场景:使用自增ID或随机字符串
- 高并发场景:使用雪花算法或Redis生成ID
- 防冲突:使用数据库唯一索引+重试机制
- 性能优化:使用缓存(Redis/本地缓存)
- 统计分析:记录点击次数、来源等
这个实现方案可以根据你的具体需求进行调整和优化。