Spring Boot 生成短链接

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

安全考虑

  1. 防止恶意URL: 在存储原始URL前,应对URL进行合法性校验
  2. 过期机制: 设置合理的过期时间,避免数据库无限增长
  3. 访问统计: 记录访问次数,可用于分析和安全监控
  4. 唯一性约束: 在数据库层面确保短码的唯一性

性能优化建议

  1. 缓存机制: 对频繁访问的短链接进行缓存
  2. 索引优化: 为short_code字段建立唯一索引
  3. 批量处理: 对于大量URL转换需求,可考虑异步处理
  4. 负载均衡: 在高并发场景下考虑使用负载均衡

扩展功能

  1. 自定义短码: 允许用户自定义短码
  2. 统计分析: 提供访问统计功能
  3. 权限控制: 对短链接的创建和管理进行权限控制
  4. 批量操作: 支持批量生成和管理短链接
相关推荐
华仔啊2 小时前
如何查看 SpringBoot 当前线程数?3 种方法亲测有效
java·后端
shark_chili2 小时前
浅谈CPU流水线的艺术
后端
毕设源码-钟学长2 小时前
【开题答辩全过程】以 小区物业管理APP为例,包含答辩的问题和答案
java·spring boot
这周也會开心2 小时前
Java面试题-JVM
java·开发语言·jvm
while(1){yan}2 小时前
Spring,SpringBoot,SpringMVC
java·spring boot·spring
秋饼2 小时前
【spring-framework 本地下载部署,以及环境搭建】
java·后端·spring
程序员泠零澪回家种桔子2 小时前
ReAct Agent 后端架构解析
后端·spring·设计模式·架构
刘宇涵492 小时前
根节点Java
java
zwjapple2 小时前
React + Java 技术面试完整指南
java·开发语言·jvm·react