设计一个短连接系统:Java实习生面试实践(修复版)
在面试 Java 实习生岗位时,面试官让我设计一个短连接系统。经过多次迭代,我修复了之前的问题,包括自增 ID 未正确返回的潜在风险。以下是修复后的内容,基于 MyBatis 和 Spring Boot 实现。
需求分析
短连接系统将长 URL 转换为短链接,用户访问短链接时重定向到原始 URL。主要需求:
- 生成短链接:输入长 URL,返回唯一短链接。
- 重定向:通过短链接跳转到长 URL。
- 持久化:映射关系存储到数据库。
- 唯一性:短链接必须唯一。
- 扩展性:支持高并发。
设计思路
- 短链接生成:用数据库自增 ID 转为 62 进制字符串。
- 流程:插入长 URL 后获取 ID,生成短链接并更新记录。
- 技术栈:Spring Boot + MyBatis + MySQL。
代码实践
1. 数据库表设计
CREATE TABLE short_url (
id BIGINT AUTO_INCREMENT PRIMARY KEY,
long_url VARCHAR(512) NOT NULL,
short_code VARCHAR(8) NOT NULL UNIQUE,
created_at TIMESTAMP DEFAULT CURRENT_TIMESTAMP
);
-- 设置自增起始值,避免短链接太短
ALTER TABLE short_url AUTO_INCREMENT = 10000;
说明:
自增 ID 从 10000 开始,确保初始短链接长度合理(如 10000
转为 2Bi
)。
2. 实体类
package com.example.shorturl.entity;
public class ShortUrl {
private Long id;
private String longUrl;
private String shortCode;
private String createdAt;
// Getters and Setters
public Long getId() { return id; }
public void setId(Long id) { this.id = id; }
public String getLongUrl() { return longUrl; }
public void setLongUrl(String longUrl) { this.longUrl = longUrl; }
public String getShortCode() { return shortCode; }
public void setShortCode(String shortCode) { this.shortCode = shortCode; }
public String getCreatedAt() { return createdAt; }
public void setCreatedAt(String createdAt) { this.createdAt = createdAt; }
}
3. Mapper 接口
package com.example.shorturl.mapper;
import com.example.shorturl.entity.ShortUrl;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface ShortUrlMapper {
void insert(ShortUrl shortUrl);
void update(ShortUrl shortUrl);
ShortUrl findByShortCode(String shortCode);
}
4. MyBatis XML
文件:resources/mapper/ShortUrlMapper.xml
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.shorturl.mapper.ShortUrlMapper">
<insert id="insert" parameterType="com.example.shorturl.entity.ShortUrl" useGeneratedKeys="true" keyProperty="id">
INSERT INTO short_url (long_url, short_code) VALUES (#{longUrl}, #{shortCode})
</insert>
<update id="update" parameterType="com.example.shorturl.entity.ShortUrl">
UPDATE short_url SET short_code = #{shortCode} WHERE id = #{id}
</update>
<select id="findByShortCode" parameterType="string" resultType="com.example.shorturl.entity.ShortUrl">
SELECT * FROM short_url WHERE short_code = #{shortCode}
</select>
</mapper>
修复说明:
insert
使用 useGeneratedKeys="true"
和 keyProperty="id"
,确保数据库生成的自增 ID 被自动设置到 shortUrl
对象的 id
字段。
5. 服务层
package com.example.shorturl.service;
import com.example.shorturl.entity.ShortUrl;
import com.example.shorturl.mapper.ShortUrlMapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
@Service
public class ShortUrlService {
private static final String BASE62 = "0123456789abcdefghijklmnopqrstuvwxyzABCDEFGHIJKLMNOPQRSTUVWXYZ";
@Autowired
private ShortUrlMapper shortUrlMapper;
public String createShortUrl(String longUrl) {
ShortUrl shortUrl = new ShortUrl();
shortUrl.setLongUrl(longUrl);
shortUrl.setShortCode("temp"); // 临时值,避免 NOT NULL 约束
// 插入记录,MyBatis 会将自增 ID 设置到 shortUrl.id
shortUrlMapper.insert(shortUrl);
// 检查 ID 是否正确返回
if (shortUrl.getId() == null) {
throw new RuntimeException("Failed to retrieve generated ID");
}
// 生成 shortCode 并更新
String shortCode = toBase62(shortUrl.getId());
shortUrl.setShortCode(shortCode);
shortUrlMapper.update(shortUrl);
return "http://short.url/" + shortCode;
}
public String getLongUrl(String shortCode) {
ShortUrl shortUrl = shortUrlMapper.findByShortCode(shortCode);
return shortUrl != null ? shortUrl.getLongUrl() : null;
}
private String toBase62(long num) {
StringBuilder sb = new StringBuilder();
while (num > 0) {
sb.append(BASE62.charAt((int) (num % 62)));
num /= 62;
}
return sb.reverse().toString();
}
}
修复说明:
- 添加了
if (shortUrl.getId() == null)
检查,确保insert
后 ID 被正确返回。 - 如果 ID 未返回,抛出异常,便于调试。这是对 MyBatis 配置的一种防御性编程。
6. 控制器
package com.example.shorturl.controller;
import com.example.shorturl.service.ShortUrlService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
@RestController
@RequestMapping("/api")
public class ShortUrlController {
@Autowired
private ShortUrlService shortUrlService;
@PostMapping("/shorten")
public ResponseEntity<String> shortenUrl(@RequestBody String longUrl) {
String shortUrl = shortUrlService.createShortUrl(longUrl);
return new ResponseEntity<>(shortUrl, HttpStatus.OK);
}
@GetMapping("/{shortCode}")
public void redirect(@PathVariable String shortCode, HttpServletResponse response) throws IOException {
String longUrl = shortUrlService.getLongUrl(shortCode);
if (longUrl != null) {
response.sendRedirect(longUrl);
} else {
response.sendError(HttpServletResponse.SC_NOT_FOUND);
}
}
}
7. 配置文件
文件:resources/application.yml
spring:
datasource:
url: jdbc:mysql://localhost:3306/short_url_db
username: root
password: your_password
driver-class-name: com.mysql.cj.jdbc.Driver
mybatis:
mapper-locations: classpath:mapper/*.xml
8. 主应用
package com.example.shorturl;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class ShortUrlApplication {
public static void main(String[] args) {
SpringApplication.run(ShortUrlApplication.class, args);
}
}
测试
- 启动应用。
- POST 请求
http://localhost:8080/api/shorten
,Body 为{"longUrl": "https://www.example.com"}
,返回短链接。 - 访问
http://localhost:8080/api/2Bi
,跳转到原始 URL。
优化方向
- 缓存:用 Redis 提升查询性能。
- 分布式 ID:用雪花算法支持高并发。
- 短链接长度:调整自增起始值控制初始长度。
总结
修复后的方案确保了自增 ID 被正确返回,避免了 NullPointerException
的风险。通过在服务层添加检查和异常抛出,我提高了代码的健壮性。这个设计思路清晰且实用,适合面试展示能力。