Spring Boot 生成短链接完整指南
项目概述
本文档将详细介绍如何在Spring Boot项目中实现短链接生成和重定向功能。该功能允许将长URL转换为短URL,并实现短URL的重定向功能。
项目结构
springboot-mp-demo/
├── pom.xml
├── src/
│ └── main/
│ ├── java/
│ │ └── com/
│ │ └── example/
│ │ └── demo/
│ │ ├── SpringbootMpDemoApplication.java
│ │ ├── controller/
│ │ │ └── UserController.java
│ │ ├── entity/
│ │ │ └── ShortUrl.java
│ │ ├── mapper/
│ │ │ └── ShortUrlMapper.java
│ │ ├── request/
│ │ │ └── CreateShortUrlRequest.java
│ │ ├── service/
│ │ │ ├── Base62Encoder.java
│ │ │ └── ShortUrlService.java
│ └── resources/
│ └── application.yml
├── sql/
│ └── short_url.sql
项目依赖配置
在 [pom.xml](file:///E:/xiangmu/mpj/pom.xml) 中添加必要的依赖:
xml
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.18</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.example</groupId>
<artifactId>springboot-mp-demo</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>springboot-mp-demo</name>
<description>Spring Boot 短链接生成系统</description>
<properties>
<java.version>1.8</java.version>
<mybatis-plus.version>3.5.14</mybatis-plus.version>
<mybatis-plus-join.version>1.5.5</mybatis-plus-join.version>
<hutool.version>5.8.23</hutool.version>
</properties>
<dependencies>
<!-- Spring Boot Web核心 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MySQL驱动(适配8.0) -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>8.0.33</version>
<scope>runtime</scope>
</dependency>
<!-- jdk 8+ 引入可选模块 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-jsqlparser-4.9</artifactId>
<version>3.5.14</version>
</dependency>
<!-- MyBatis Plus 核心 -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>${mybatis-plus.version}</version>
</dependency>
<!-- MyBatis Plus Join(多表连接) -->
<dependency>
<groupId>com.github.yulichang</groupId>
<artifactId>mybatis-plus-join-boot-starter</artifactId>
<version>${mybatis-plus-join.version}</version>
</dependency>
<!-- Hutool 工具库 -->
<dependency>
<groupId>cn.hutool</groupId>
<artifactId>hutool-all</artifactId>
<version>${hutool.version}</version>
</dependency>
<!-- Lombok(简化实体类) -->
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<optional>true</optional>
</dependency>
<!-- 测试 -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-openapi2-spring-boot-starter</artifactId>
<version>4.4.0</version>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
<configuration>
<excludes>
<exclude>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
</exclude>
</excludes>
</configuration>
</plugin>
</plugins>
</build>
</project>
数据库表结构
首先创建短链接表结构:
sql
CREATE TABLE `short_url` (
`id` bigint NOT NULL AUTO_INCREMENT COMMENT '主键ID',
`uni_code` bigint NOT NULL COMMENT '唯一编码',
`short_code` varchar(20) NOT NULL COMMENT '短码',
`original_url` text NOT NULL COMMENT '原始URL',
`create_time` datetime DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
`expire_time` datetime COMMENT '过期时间',
`access_count` bigint DEFAULT 0 COMMENT '访问次数',
PRIMARY KEY (`id`),
UNIQUE KEY `uk_short_code` (`short_code`),
KEY `idx_uni_code` (`uni_code`),
KEY `idx_create_time` (`create_time`)
) ENGINE=InnoDB AUTO_INCREMENT=1 DEFAULT CHARSET=utf8mb4 COLLATE=utf8mb4_0900_ai_ci COMMENT='短链接表';
实体类
创建 [ShortUrl](file:///E:/xiangmu/mpj/src/main/java/com/example/demo/entity/Order.java#L12-L28) 实体类:
java
package com.example.demo.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.annotation.TableName;
import lombok.Data;
import java.time.LocalDateTime;
// 2. 实体类
@Data
@TableName("short_url")
public class ShortUrl {
@TableId(type = IdType.AUTO)
private Long id;
private Long uniCode;
private String shortCode;
private String originalUrl;
private LocalDateTime createTime;
private LocalDateTime expireTime;
private Long accessCount = 0L;
}
数据访问层
创建 [ShortUrlMapper](file:///E:/xiangmu/mpj/src/main/java/com/example/demo/mapper/OrderMapper.java#L5-L5) 接口:
java
package com.example.demo.mapper;
import com.example.demo.entity.ShortUrl;
import com.github.yulichang.base.MPJBaseMapper;
public interface ShortUrlMapper extends MPJBaseMapper<ShortUrl> {
}
服务层
Base62编码器
创建 [Base62Encoder](file:///E:/xiangmu/mpj/src/main/java/com/example/demo/service/Base62Encoder.java#L5-L35) 编码器类:
java
package com.example.demo.service;
import org.springframework.stereotype.Component;
// 3. 工具类 - Base62编码
@Component
public class Base62Encoder {
private static final String BASE62_CHARS =
"0123456789ABCDEFGHIJKLMNOPQRSTUVWXYZabcdefghijklmnopqrstuvwxyz";
private static final int BASE = 62;
/**
* 将数字ID转换为Base62字符串
*/
public String encode(long num) {
StringBuilder sb = new StringBuilder();
while (num > 0) {
sb.append(BASE62_CHARS.charAt((int)(num % BASE)));
num /= BASE;
}
return sb.reverse().toString();
}
/**
* 将Base62字符串转换为数字ID
*/
public long decode(String str) {
long num = 0;
for (int i = 0; i < str.length(); i++) {
num = num * BASE + BASE62_CHARS.indexOf(str.charAt(i));
}
return num;
}
}
短链接服务
创建 [ShortUrlService](file:///E:/xiangmu/mpj/src/main/java/com/example/demo/service/ShortUrlService.java#L7-L86) 服务类:
java
package com.example.demo.service;
import cn.hutool.core.util.IdUtil;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.example.demo.entity.ShortUrl;
import com.example.demo.mapper.ShortUrlMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
// 4. 服务层
@Service
public class ShortUrlService {
@Autowired
private ShortUrlMapper mapper;
@Autowired
private Base62Encoder base62Encoder;
/**
* 生成短链接
*/
public String generateShortUrl(String originalUrl) {
// 检查是否已存在
ShortUrl existing = this.findByOriginalUrl(originalUrl);
if (existing != null) {
return buildShortUrl(existing.getShortCode());
}
// 创建新记录
ShortUrl shortUrl = new ShortUrl();
shortUrl.setUniCode(IdUtil.getSnowflakeNextId());
shortUrl.setOriginalUrl(originalUrl);
shortUrl.setCreateTime(LocalDateTime.now());
shortUrl.setExpireTime(LocalDateTime.now().plusDays(30));
// 生成短码
String shortCode = base62Encoder.encode(shortUrl.getUniCode());
shortUrl.setShortCode(shortCode);
mapper.insert(shortUrl);
return buildShortUrl(shortCode);
}
/**
* 根据短码获取原始URL
*/
public String getOriginalUrl(String shortCode) {
// 2. 从数据库获取
ShortUrl shortUrl = this.findByShortCode(shortCode);
if (shortUrl == null) {
throw new RuntimeException("短链接不存在");
}
// 3. 检查是否过期
if (shortUrl.getExpireTime() != null &&
shortUrl.getExpireTime().isBefore(LocalDateTime.now())) {
throw new RuntimeException("短链接已过期");
}
// 4. 更新访问次数
shortUrl.setAccessCount(shortUrl.getAccessCount() + 1);
mapper.updateById(shortUrl);
return shortUrl.getOriginalUrl();
}
private String buildShortUrl(String shortCode) {
// 这里应该使用配置文件中的域名
return "http://your-domain.com/" + shortCode;
}
private ShortUrl findByShortCode(String shortCode){
LambdaQueryWrapper<ShortUrl> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ShortUrl::getShortCode, shortCode);
return mapper.selectOne(queryWrapper);
}
private ShortUrl findByOriginalUrl(String originalUrl){
LambdaQueryWrapper<ShortUrl> queryWrapper = new LambdaQueryWrapper<>();
queryWrapper.eq(ShortUrl::getOriginalUrl, originalUrl);
return mapper.selectOne(queryWrapper);
}
}
请求类
创建 [CreateShortUrlRequest](file:///E:/xiangmu/mpj/src/main/java/com/example/demo/request/CreateShortUrlRequest.java#L3-L8) 请求类:
java
package com.example.demo.request;
import lombok.Data;
@Data
public class CreateShortUrlRequest {
private String url;
}
控制器层
在 [UserController](file:///E:/xiangmu/mpj/src/main/java/com/example/demo/controller/UserController.java#L15-L132) 中添加短链接相关方法:
java
package com.example.demo.controller;
import cn.hutool.core.collection.IterUtil;
import cn.hutool.core.collection.ListUtil;
import cn.hutool.core.util.StrUtil;
import cn.hutool.json.JSONUtil;
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.example.demo.entity.User;
import com.example.demo.request.CreateShortUrlRequest;
import com.example.demo.request.UserRequest;
import com.example.demo.response.UserResponse;
import com.example.demo.service.ShortUrlService;
import com.example.demo.service.UserService;
import com.example.demo.vo.UserOrderVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.RestController;
import java.net.URI;
import java.util.ArrayList;
import java.util.Map;
import java.util.HashMap;
/**
* User API controller
*/
@RestController
@Api(tags = "用户管理")
public class UserController {
@Autowired
private UserService userService;
@Autowired
private ShortUrlService shortUrlService;
/**
* 短链接重定向 - 演示接口
*/
@GetMapping("/redirect/{shortCode}")
@ApiOperation(value = "短链接重定向", notes = "短链接重定向")
public ResponseEntity<Void> redirect(@PathVariable String shortCode) {
try {
String originalUrl = shortUrlService.getOriginalUrl(shortCode);
return ResponseEntity.status(HttpStatus.FOUND)
.location(URI.create(originalUrl))
.build();
} catch (Exception e) {
return ResponseEntity.notFound().build();
}
}
/**
* 短链接重定向 - 生产接口
* 实际部署时,通过域名直接访问短码
*/
@GetMapping("/{shortCode}")
public ResponseEntity<Void> redirectToOriginal(@PathVariable String shortCode) {
try {
String originalUrl = shortUrlService.getOriginalUrl(shortCode);
return ResponseEntity.status(HttpStatus.FOUND)
.location(URI.create(originalUrl))
.build();
} catch (Exception e) {
return ResponseEntity.notFound().build();
}
}
/**
* 创建短链接
*/
@PostMapping("/create")
@ApiOperation(value = "创建短链接", notes = "创建短链接")
public ResponseEntity<String> createShortUrl(@RequestBody CreateShortUrlRequest request) {
try {
String shortUrl = shortUrlService.generateShortUrl(request.getUrl());
return ResponseEntity.ok(shortUrl);
} catch (Exception e) {
return ResponseEntity.ok("生成短链接失败: " + e.getMessage());
}
}
}
配置文件
在 [application.yml](file:///E:/xiangmu/mpj/src/main/resources/application.yml) 中添加配置:
yaml
server:
port: 8080
spring:
# 数据源配置
datasource:
driver-class-name: com.mysql.cj.jdbc.Driver
url: jdbc:mysql://localhost:3306/test_db?useUnicode=true&characterEncoding=utf8&useSSL=false&serverTimezone=Asia/Shanghai&allowPublicKeyRetrieval=true
username: root # 替换为你的MySQL用户名
password: 123456 # 替换为你的MySQL密码
# MyBatis Plus 配置
mybatis-plus:
# Mapper.xml文件路径(如果需要)
mapper-locations: classpath:mapper/**/*.xml
# 实体类别名包
type-aliases-package: com.example.demo.entity
configuration:
# 开启驼峰命名自动转换
map-underscore-to-camel-case: true
# Knife4j API文档配置
knife4j:
enable: true
setting:
language: zh-CN
production: false
basic:
enable: false
username: admin
password: 123456
主启动类
java
package com.example.demo;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
@MapperScan("com.example.demo.mapper") // 扫描Mapper接口
public class SpringbootMpDemoApplication {
public static void main(String[] args) {
SpringApplication.run(SpringbootMpDemoApplication.class, args);
}
}
功能说明
1. 短链接生成
- 用户通过POST请求向
/create接口提交原始URL - 系统首先检查该URL是否已经生成过短链接,如果已存在则直接返回
- 使用雪花算法生成唯一编码,然后通过Base62编码转换为短码
- 设置30天的过期时间
- 将原始URL、短码、过期时间等信息存储到数据库中
- 返回生成的短链接
2. 短链接重定向
- 用户访问
/{shortCode}接口 - 系统根据短码从数据库中查询原始URL
- 检查短链接是否过期
- 如果未过期,更新访问次数并重定向到原始URL
- 如果已过期或不存在,返回404错误
API接口说明
生成短链接
-
接口 :
POST /create -
请求参数 :
json{ "url": "https://www.example.com/very/long/url/here" } -
响应示例 :
http://your-domain.com/aBcDeFg
短链接重定向
- 接口1 :
GET /redirect/{shortCode}- 演示接口 - 接口2 :
GET /{shortCode}- 生产接口 - 路径参数 :
shortCode- 短码 - 功能: 重定向到原始URL
安全考虑
- 防止恶意URL: 在存储原始URL前,应对URL进行合法性校验
- 过期机制: 设置合理的过期时间,避免数据库无限增长
- 访问统计: 记录访问次数,可用于分析和安全监控
- 唯一性约束: 在数据库层面确保短码的唯一性
性能优化建议
- 缓存机制: 对频繁访问的短链接进行缓存
- 索引优化: 为short_code字段建立唯一索引
- 批量处理: 对于大量URL转换需求,可考虑异步处理
- 负载均衡: 在高并发场景下考虑使用负载均衡
扩展功能
- 自定义短码: 允许用户自定义短码
- 统计分析: 提供访问统计功能
- 权限控制: 对短链接的创建和管理进行权限控制
- 批量操作: 支持批量生成和管理短链接