为了重新复习 Java 方面相关的内容,以及进一步熟悉代码,开始手撸代码。
手撸第一课 从基本登录开始
考虑到后续可能直接是在这个项目上进行扩展衍生,用户表这里的数据库设计就不在采用简单的自增ID,简单记录下现在的思路
为了方便以后的衍生到分布式架构,这里采用 全局唯一ID
sqlCREATE TABLE `users` (
`id` BIGINT NOT NULL COMMENT '雪花ID',
`username` VARCHAR(64) NOT NULL COMMENT '登录账号',
`password` VARCHAR(255) NOT NULL COMMENT 'BCrypt加密后的密码', -- 字段名简化
`email` VARCHAR(100) DEFAULT NULL COMMENT '邮箱',
`phone` VARCHAR(20) DEFAULT NULL COMMENT '手机号',
`status` TINYINT NOT NULL DEFAULT 1 COMMENT '状态(0-禁用,1-正常)',
`created_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP,
`updated_at` DATETIME NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP,
PRIMARY KEY (`id`),
UNIQUE KEY `uk_username` (`username`),
UNIQUE KEY `uk_email` (`email`),
UNIQUE KEY `uk_phone` (`phone`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='用户表';
首先这里说明,项目在这里使用了Springboot、Mybatis-plus、validation、Lombok,pom文件里面就需要导入这些包支持,但是Lombok这是集成在Idea里面的,可以自行百度了解。
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>
<!-- 父工程(Spring Boot版本控制) -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.18</version> <!-- 选稳定版,别用最新版踩坑 -->
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>org.rose</groupId>
<artifactId>Test</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>Test</name>
<description>Test</description>
<properties>
<java.version>17</java.version>
</properties>
<dependencies>
<!-- 基础Web支持(Controller、JSON等) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- 参数校验(NotBlank等注解) -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-validation</artifactId>
</dependency>
<!-- 数据库框架 -->
<dependency>
<groupId>org.mybatis.spring.boot</groupId>
<artifactId>mybatis-spring-boot-starter</artifactId>
<version>2.3.0</version>
</dependency>
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.3.1</version>
</dependency>
<!-- MySQL 5.7 驱动(版本建议 5.1.49+) -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.49</version> <!-- 5.7 推荐版本 -->
<scope>runtime</scope>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
yml# 应用基础配置
spring:
datasource:
url: jdbc:mysql://localhost:3306/bigTest?useSSL=false&useUnicode=true&characterEncoding=utf8&autoReconnect=true&serverTimezone=Asia/Shanghai
username: root
password: root
driver-class-name: com.mysql.jdbc.Driver # MySQL 5.7驱动
# MyBatis-Plus 配置
mybatis-plus:
global-config:
db-config:
id-type: assign_id # 雪花ID(默认)
configuration:
map-underscore-to-camel-case: true # 下划线转驼峰
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 打印SQL(调试用)
# MyBatis-Plus 其他可选配置
mybatis-plus:
type-aliases-package: org.rose.test.entity
在之前我写项目的时候基本上都是在业务层进行参数校验,为了实现判断这些字段不为空,我们的业务层 UserService.java
javapublic String addUser(User user) {
if (StringUtils.isEmpty(user.getUsername()) || StringUtils.isEmpty(user.getPassword()) || StringUtils.isEmpty(user.getEmail())) {
return "不能输入空字符串";
}
if (user.getUsername().length() < 6 || user.getUsername().length() > 11) {
return "账号长度必须是6-11个字符";
}
if (user.getPassword().length() < 6 || user.getPassword().length() > 16) {
return "密码长度必须是6-16个字符";
}
if (!Pattern.matches("^[a-zA-Z0-9_-]+@[a-zA-Z0-9_-]+(\\.[a-zA-Z0-9_-]+)+$", user.getEmail())) {
return "邮箱格式不正确";
}
// 参数校验完毕后这里就写上业务逻辑
return "success";
}
这样业务层的逻辑清晰,一目了然,但是这样太繁琐了,如果我们有更多的字段,这样的书写肯定是不足够“优雅”,所以我们可以使用到 Validator 来进行参数校验。
实体 user.java
java
@Data
@TableName("users") // 指定表名
public class User {
@TableId(type = IdType.ASSIGN_ID) // 雪花ID(默认策略)
private Long id;
@NotNull(message = "用户名不能为空")
@Size(min = 6, max = 11, message = "账号长度必须是6-11个字符")
private String username;
@NotNull(message = "用户密码不能为空")
@Size(min = 6, max = 11, message = "密码长度必须是6-16个字符")
private String password;
@NotNull(message = "用户邮箱不能为空")
@Email(message = "邮箱格式不正确")
private String email;
@NotNull(message = "手机号不能为空")
// @Pattern(
// regexp = "^1[3-9]\\d{9}$", // 匹配中国大陆手机号
// message = "手机号格式不正确"
// )
private String phone;
private Integer status;
@TableField(fill = FieldFill.INSERT) // 自动填充创建时间
private LocalDateTime createdAt;
@TableField(fill = FieldFill.INSERT_UPDATE) // 自动填充更新时间
private LocalDateTime updatedAt;
}
这里可以看到我注释掉一个 @Pattern 注解,这个注解就是如代码里面看到的,主要是用于自己写匹配方式,看了代码就清楚知道:
@Notnull 主要是判断为空
@Size 控制字段长度
那我们的服务类就很简单了,if 语句统统去掉
服务类 UserService.java
java
@Service
public class UserService {
@Autowired
private UserMapper userMapper;
// 插入用户(自动填充时间)
public String addUser(User user) {
userMapper.insert(user); // 雪花ID会自动生成
return "success";
}
}
可以看到,我们相当于把 if 判断语句的内容放在了实体里面,并且在实体里面也设置了返回值,这样可以减少掉业务层代码,另外对比下控制类
修改前-控制类 UserController.java
java@RestController
@RequestMapping("user")
public class UserController {
@Autowired
private UserService userService;
@PostMapping("/addUser")
public String addUser(@RequestBody User user) {
return userService.addUser(user);
}
}
修改后-控制类 UserController.java
java@RestController
@RequestMapping("user")
public class UserController {
@Autowired
private UserService userService;
@PostMapping("/addUser")
public String addUser(@RequestBody @Valid User user, BindingResult bindingResult) {
// 如果有参数校验失败,会将错误信息封装成对象组装在BindingResult里
for (ObjectError error : bindingResult.getAllErrors()) {
return error.getDefaultMessage();
}
return userService.addUser(user);
}
}
可以看到主要是在对象的前面使用@Valid 的注解,以及使用BindingResult,但是方法里面多了一个循环获取校验的错误信息,这就意味着每个方法都需要去使用这样一个循环,并且每个方法都需要使用到BindingResult,找到一篇关于写后端接口的文章
可以通过全局异常处理去掉BindingResult的复写,根据这里的思路来排查代码
在控制类里面去掉 BindingResult 后,在不进行全局异常编写的情况下,它会抛出异常
完整的异常内容:
java2025-07-30 08:34:26.653 WARN 26124 --- [nio-8080-exec-1] .w.s.m.s.DefaultHandlerExceptionResolver : Resolved
[org.springframework.web.bind.MethodArgumentNotValidException:
Validation failed for argument [0] in public java.lang.String
org.rose.test.user.controller.UserController.addUser(org.rose.test.user.entity.User) with 2 errors:
[Field error in object 'user' on field 'password':
rejected value []; codes [Size.user.password,Size.password,Size.java.lang.String,Size];
arguments [org.springframework.context.support.DefaultMessageSourceResolvable:
codes [user.password,password];
arguments [];
default message [password],11,6];
default message [密码长度必须是6-16个字符]]
[Field error in object 'user' on field 'username':
rejected value []; codes [Size.user.username,Size.username,Size.java.lang.String,Size];
arguments [org.springframework.context.support.DefaultMessageSourceResolvable:
codes [user.username,username];
arguments []; default message [username],11,6];
default message [账号长度必须是6-11个字符]] ]
也就是说可以通过捕获这个类发出的异常情况,就可以处理错误详情
来写一个简单的全局异常处理器
java@RestControllerAdvice
public class GlobalExceptionHandler {
// 专门处理参数校验异常
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MethodArgumentNotValidException.class)
public Map<String, String> handleValidationException(MethodArgumentNotValidException ex) {
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error -> {
errors.put(error.getField(), error.getDefaultMessage());
});
return errors; // 返回字段名和错误信息的键值对
}
}
1. @RestControllerAdvice 注解 @RestControllerAdvice 是一个结合了 @ControllerAdvice 和 @ResponseBody 的注解。 它能够处理全局的异常,并返回一个 JSON 响应,而不是跳转到错误页面。 它是 Spring 中全局异常处理机制的一部分。当请求处理过程中抛出异常时,这个类中的异常处理方法就会被触发。 2. @ExceptionHandler(MethodArgumentNotValidException.class) 这个注解用于标注一个方法来处理 MethodArgumentNotValidException 类型的异常。 3. @ResponseStatus(HttpStatus.BAD_REQUEST) 这个注解表示如果该方法处理了异常,将会返回 HttpStatus.BAD_REQUEST (即 400 错误码)。 4. handleValidationException 方法 该方法是处理 MethodArgumentNotValidException 异常的逻辑。当参数校验失败时,这个方法会被调用。 ex.getBindingResult().getFieldErrors():返回包含所有字段验证失败信息的列表。 FieldError 对象包含了字段名(error.getField())和默认的错误信息(error.getDefaultMessage())。 Map<String, String> errors:将字段名和错误信息封装到一个 Map 中。 最终,方法返回一个 Map,其中键是字段名,值是相应的错误信息。 5. 返回值 由于该方法返回的是一个 Map<String, String>,Spring 会将其自动转换为 JSON 格式,并将其发送给客户端。
也就是现在这样的结果,但是为了实现统一的返回结构和规范化,所以要建立一个统一的返回结构
javaprivate class ApiResponse<T> {
private int code;
private String message;
private T data;
public ApiResponse(T data) {
this(ResultCode.SUCCESS,data);
}
public ApiResponse(ResultCode resultCode, T data) {
this.code = resultCode.getCode();
this.message = resultCode.getMsg();
this.data = data;
}
}
枚举记录统一响应体
javapublic enum ResultCode {
SUCCESS(200, "操作成功"),
FAILED(201, "响应失败"),
VALIDATE_FAILED(202, "参数校验失败"),
ERROR(400, "未知错误");
private int code;
private String msg;
ResultCode(int code, String msg) {
this.code = code;
this.msg = msg;
}
}
最终的全局异常处理代码
javapackage org.rose.test.user.config;
@RestControllerAdvice
public class GlobalExceptionHandler {
// 参数校验异常(@RequestBody @Valid 触发)
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(MethodArgumentNotValidException.class)
public ApiResponse<Map<String, String>> handleValidationException(
MethodArgumentNotValidException ex) {
// 提取错误信息
Map<String, String> errors = new HashMap<>();
ex.getBindingResult().getFieldErrors().forEach(error -> {
errors.put(error.getField(), error.getDefaultMessage());
});
return new ApiResponse<>(
ResultCode.VALIDATE_FAILED,
errors
);
}
// 参数校验异常(@RequestParam/@PathVariable 触发)
@ResponseStatus(HttpStatus.BAD_REQUEST)
@ExceptionHandler(ConstraintViolationException.class)
public ApiResponse<String> handleConstraintViolationException(
ConstraintViolationException ex) {
return new ApiResponse<>(
ResultCode.VALIDATE_FAILED,
ex.getMessage()
);
}
// 其他所有未捕获异常
@ResponseStatus(HttpStatus.INTERNAL_SERVER_ERROR)
@ExceptionHandler(Exception.class)
public ApiResponse<Void> handleException(Exception ex, HttpServletRequest request) {
return new ApiResponse<>(
ResultCode.ERROR,
null
);
}
}
在测试的过程中,如果我们使用@NotNull 注解,当postman 测试里面传递的参数是
json{
"username":"",
"password": "",
"email": "",
"phone": ""
}
它是不会判断为空的,应该将@NotNull 改为 @NotEmpty
本文作者:Rose
本文链接:
版权声明:本博客所有文章除特别声明外,均采用 BY-NC-SA 许可协议。转载请注明出处!