Java短链接案例如何实现

wen java案例 31

本文目录导读:

Java短链接案例如何实现

  1. 基础实现:哈希+进制转换
  2. 自增ID实现(防止冲突)
  3. 完整Spring Boot实现
  4. 雪花算法ID生成器
  5. 使用建议

我来介绍几种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;
    }
}

使用建议

  1. 简单场景:使用自增ID或随机字符串
  2. 高并发场景:使用雪花算法或Redis生成ID
  3. 防冲突:使用数据库唯一索引+重试机制
  4. 性能优化:使用缓存(Redis/本地缓存)
  5. 统计分析:记录点击次数、来源等

这个实现方案可以根据你的具体需求进行调整和优化。

抱歉,评论功能暂时关闭!