前言
今天开始做一个短链接的项目,准备之后写到简历上,因此会认真的记录每天的学习情况,争取把里面的每个点都弄懂,也争取找到一些优化的内容。
1.什么是短链接
短链接(Short Link)是指将一个原始的长 URL(Uniform Resource Locator)通过特定的算法或服务转化为一个更短、易于记忆的 URL。短链接通常只包含几个字符,而原始的长 URL 可能会非常长。
短链接的原理非常简单,通过一个原始链接生成个相对短的链接,然后通过访问短链接跳转到原始链接。
2.功能分析
今天要实现的是用户模块,其功能主要包括以下几个方面:
- 检查用户名是否存在
- 注册用户
- 修改用户
- 根据用户名查询用户
- 用户登录
- 检查用户是否登录
- 用户退出登录
- 注销用户
3 数据持久层
持久层首先需要创建一张用户表,包含了用户的基本信息:用户名、密码、真实姓名、手机号、邮箱等,还包括一些数据的基本信息,比如创建时间、修改时间等。
创建完用户表后,就要进行一些基础配置,包括:引入持久层框架、持久层配置文件以及添加持久层接口扫描器。
这边尤其需要说的是添加持久层接口扫描器 ,这个注解是加在启动类上的,它的作用是让你只写接口,不用写实现类,就可以操作数据库。
扫描器在项目启动时,会按照注解中的包名去寻找所有的接口文件,并查找对应的sql语句在哪(可能是XML文件,也可能是注解),然后通过JDK动态代理,在内存中生成一个虚拟的实现类,并放入IOC容器中,这样我们在Service层中注入时,注入的就是这个虚拟代理。
java
@MapperScan("com.nageoffer.shortlink.admin.dao.mapper")
下面开始写代码。
用一个实体类UserDO来封装数据库的User:
java
@TableName("t_user")
@Data
public class UserDO {
/**
* ID
*/
private Long id;
/**
* 用户名
*/
private String username;
/**
* 密码
*/
private String password;
/**
* 真实姓名
*/
private String realName;
/**
* 手机号
*/
private String phone;
/**
* 邮箱
*/
private String mail;
/**
* 注销时间戳
*/
private Long deletionTime;
/**
* 创建时间
*/
private Date createTime;
/**
* 修改时间
*/
private Date updateTime;
/**
* 删除标识 0:未删除 1:已删除
*/
private Integer delFlag;
}
4.异常设计
4.1异常码说明
异常码的设计参考阿里巴巴开发手册(泰山版),其核心设计理念是:一看错误码,就知道是哪一端报错了 。
阿里规定,错误码不应该是简单的数字,而应该是一个5位的字符串。
4.2 异常码设计
手册将错误分为了三大类型,分别对应三个字母:
A:用户端错误 :用户(前端)的错,比如用户输入了空的用户名,或是密码长度不够等。
B:系统执行出错 :后端的错,例如空指针异常,数据库连接超时,或是代码有问题,这种比较严重 ,需要立即报警。
C:远程调用出错:别人的错,依赖的外部服务有问题,例如调用微信支付接口,结果微信返回超时。
- IErrorCode.java
java
/**
* 平台错误码
*/
public interface IErrorCode {
/**
* 错误码
*/
String code();
/**
* 错误信息
*/
String message();
}
- BaseErrorCode.java
java
public enum BaseErrorCode implements IErrorCode {
// ========== 一级宏观错误码 客户端错误 ==========
CLIENT_ERROR("A000001", "用户端错误"),
// ========== 二级宏观错误码 用户注册错误 ==========
USER_REGISTER_ERROR("A000100", "用户注册错误"),
USER_NAME_VERIFY_ERROR("A000110", "用户名校验失败"),
USER_NAME_EXIST_ERROR("A000111", "用户名已存在"),
USER_NAME_SENSITIVE_ERROR("A000112", "用户名包含敏感词"),
USER_NAME_SPECIAL_CHARACTER_ERROR("A000113", "用户名包含特殊字符"),
PASSWORD_VERIFY_ERROR("A000120", "密码校验失败"),
PASSWORD_SHORT_ERROR("A000121", "密码长度不够"),
PHONE_VERIFY_ERROR("A000151", "手机格式校验失败"),
// ========== 二级宏观错误码 系统请求缺少幂等Token ==========
IDEMPOTENT_TOKEN_NULL_ERROR("A000200", "幂等Token为空"),
IDEMPOTENT_TOKEN_DELETE_ERROR("A000201", "幂等Token已被使用或失效"),
// ========== 一级宏观错误码 系统执行出错 ==========
SERVICE_ERROR("B000001", "系统执行出错"),
// ========== 二级宏观错误码 系统执行超时 ==========
SERVICE_TIMEOUT_ERROR("B000100", "系统执行超时"),
// ========== 一级宏观错误码 调用第三方服务出错 ==========
REMOTE_ERROR("C000001", "调用第三方服务出错");
private final String code;
private final String message;
BaseErrorCode(String code, String message) {
this.code = code;
this.message = message;
}
@Override
public String code() {
return code;
}
@Override
public String message() {
return message;
}
}
4.3 异常设计
第一部分:拦截机制

在SpringBoot中,通过AOP 设计一个全局异常拦截器,一旦报错,异常都会向外抛出,全局异常拦截器会在异常到达前端之前抓住它,并把它格式化为一个JSON格式对象返回给前端。
java
/**
* 全局异常处理器
*
*/
@Component
@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
/**
* 拦截参数验证异常
*/
@SneakyThrows
@ExceptionHandler(value = MethodArgumentNotValidException.class)
public Result validExceptionHandler(HttpServletRequest request, MethodArgumentNotValidException ex) {
BindingResult bindingResult = ex.getBindingResult();
FieldError firstFieldError = CollectionUtil.getFirst(bindingResult.getFieldErrors());
String exceptionStr = Optional.ofNullable(firstFieldError)
.map(FieldError::getDefaultMessage)
.orElse(StrUtil.EMPTY);
log.error("[{}] {} [ex] {}", request.getMethod(), getUrl(request), exceptionStr);
return Results.failure(BaseErrorCode.CLIENT_ERROR.code(), exceptionStr);
}
/**
* 拦截应用内抛出的异常
*/
@ExceptionHandler(value = {AbstractException.class})
public Result abstractException(HttpServletRequest request, AbstractException ex) {
if (ex.getCause() != null) {
log.error("[{}] {} [ex] {}", request.getMethod(), request.getRequestURL().toString(), ex.toString(), ex.getCause());
return Results.failure(ex);
}
log.error("[{}] {} [ex] {}", request.getMethod(), request.getRequestURL().toString(), ex.toString());
return Results.failure(ex);
}
/**
* 拦截未捕获异常
*/
@ExceptionHandler(value = Throwable.class)
public Result defaultErrorHandler(HttpServletRequest request, Throwable throwable) {
log.error("[{}] {} ", request.getMethod(), getUrl(request), throwable);
return Results.failure();
}
private String getUrl(HttpServletRequest request) {
if (StringUtils.isEmpty(request.getQueryString())) {
return request.getRequestURL().toString();
}
return request.getRequestURL().toString() + "?" + request.getQueryString();
}
}
第二部分:异常体系

首先定义一个抽象规约异常,它是所有自定义异常的父类,它通常继承自RunTimeException,要求所有子类必须包含两个核心要素:错误码和错误信息,其下面又有三大分支。
- AbstractException.java
java
@Getter
public abstract class AbstractException extends RuntimeException {
public final String errorCode;
public final String errorMessage;
public AbstractException(String message, Throwable throwable, IErrorCode errorCode) {
super(message, throwable);
this.errorCode = errorCode.code();
this.errorMessage = Optional.ofNullable(StringUtils.hasLength(message) ? message : null).orElse(errorCode.message());
}
}
- ClientException.java
java
/**
* 客户端异常
*/
public class ClientException extends AbstractException {
public ClientException(IErrorCode errorCode) {
this(null, null, errorCode);
}
public ClientException(String message) {
this(message, null, BaseErrorCode.CLIENT_ERROR);
}
public ClientException(String message, IErrorCode errorCode) {
this(message, null, errorCode);
}
public ClientException(String message, Throwable throwable, IErrorCode errorCode) {
super(message, throwable, errorCode);
}
@Override
public String toString() {
return "ClientException{" +
"code='" + errorCode + "'," +
"message='" + errorMessage + "'" +
'}';
}
}
- ServiceException.java
java
/**
* 服务端异常
*/
public class ServiceException extends AbstractException {
public ServiceException(String message) {
this(message, null, BaseErrorCode.SERVICE_ERROR);
}
public ServiceException(IErrorCode errorCode) {
this(null, errorCode);
}
public ServiceException(String message, IErrorCode errorCode) {
this(message, null, errorCode);
}
public ServiceException(String message, Throwable throwable, IErrorCode errorCode) {
super(Optional.ofNullable(message).orElse(errorCode.message()), throwable, errorCode);
}
@Override
public String toString() {
return "ServiceException{" +
"code='" + errorCode + "'," +
"message='" + errorMessage + "'" +
'}';
}
}
- RemoteException.java
java
/**
* 远程服务调用异常
*/
public class RemoteException extends AbstractException {
public RemoteException(String message) {
this(message, null, BaseErrorCode.REMOTE_ERROR);
}
public RemoteException(String message, IErrorCode errorCode) {
this(message, null, errorCode);
}
public RemoteException(String message, Throwable throwable, IErrorCode errorCode) {
super(message, throwable, errorCode);
}
@Override
public String toString() {
return "RemoteException{" +
"code='" + errorCode + "'," +
"message='" + errorMessage + "'" +
'}';
}
}
5.全局统一返回实体
如果没有统一的返回实体,那么不同的接口返回的数据就是千奇百怪的,查询用户会返回一个User对象,删除用户又会返回一个Boolean对象,前端会无法统一判断请求是否成功,也不知道如何去找错误提示。因此需要一个Result< T >对象,将返回值标准化。
- Result.java
java
/**
* 全局返回对象
*/
@Data
@Accessors(chain = true)
public class Result<T> implements Serializable {
@Serial
private static final long serialVersionUID = 5679018624309023727L;
/**
* 正确返回码
*/
public static final String SUCCESS_CODE = "0";
/**
* 返回码
*/
private String code;
/**
* 返回消息
*/
private String message;
/**
* 响应数据
*/
private T data;
/**
* 请求ID
*/
private String requestId;
public boolean isSuccess() {
return SUCCESS_CODE.equals(code);
}
}
Result对象是一个纯粹的数据容器,它定义了返回值包含哪些字段(code、msg、data)。
注 :这里的@Accessors(chain = true)注解是一个好用的工具,它允许链式调用,如:new Result().setCode("0").setMessage("OK")
- Results.java
java
/**
* 全局返回对象构造器
*/
public final class Results {
/**
* 构造成功响应
*/
public static Result<Void> success() {
return new Result<Void>()
.setCode(Result.SUCCESS_CODE);
}
/**
* 构造带返回数据的成功响应
*/
public static <T> Result<T> success(T data) {
return new Result<T>()
.setCode(Result.SUCCESS_CODE)
.setData(data);
}
/**
* 构建服务端失败响应
*/
public static Result<Void> failure() {
return new Result<Void>()
.setCode(BaseErrorCode.SERVICE_ERROR.code())
.setMessage(BaseErrorCode.SERVICE_ERROR.message());
}
/**
* 通过 {@link AbstractException} 构建失败响应
*/
public static Result<Void> failure(AbstractException abstractException) {
String errorCode = Optional.ofNullable(abstractException.getErrorCode())
.orElse(BaseErrorCode.SERVICE_ERROR.code());
String errorMessage = Optional.ofNullable(abstractException.getErrorMessage())
.orElse(BaseErrorCode.SERVICE_ERROR.message());
return new Result<Void>()
.setCode(errorCode)
.setMessage(errorMessage);
}
/**
* 通过 errorCode、errorMessage 构建失败响应
*/
public static Result<Void> failure(String errorCode, String errorMessage) {
return new Result<Void>()
.setCode(errorCode)
.setMessage(errorMessage);
}
}
Results类是一个工具类,用于构造,它负责生产结果。它与Result类进行了分离,好处是把复杂的创建逻辑和Result的属性分离开来,不让Result类显的很臃肿。
6.用户敏感信息脱敏展示
对于用户的一些敏感信息,如手机、身份证号等,我们是需要进行脱敏展示的,这里只针对手机号进行脱敏展示,使用序列化器。
java
/**
* 手机号脱敏反序列化
*/
public class PhoneDesensitizationSerializer extends JsonSerializer<String> {
@Override
public void serialize(String phone, JsonGenerator jsonGenerator, SerializerProvider serializerProvider) throws IOException {
String phoneDesensitization = DesensitizedUtil.mobilePhone(phone);
jsonGenerator.writeString(phoneDesensitization);
}
}
另外要在返回对象实体类的对应属性上打上标签:
java
/**
* 手机号
*/
@JsonSerialize(using = PhoneDesensitizationSerializer.class)
private String phone;
使用apifox进行测试,发现完美达成预期目标:
