编辑
2025-07-30
Java
00

目录

数据库创建
users表
项目配置内容
Pom.xml
application.yml
项目代码设计

为了重新复习 Java 方面相关的内容,以及进一步熟悉代码,开始手撸代码。

手撸第一课 从基本登录开始

数据库创建

考虑到后续可能直接是在这个项目上进行扩展衍生,用户表这里的数据库设计就不在采用简单的自增ID,简单记录下现在的思路

为了方便以后的衍生到分布式架构,这里采用 全局唯一ID

users表

sql
CREATE 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里面的,可以自行百度了解。

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> <!-- 父工程(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>

application.yml

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

java
public 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 后,在不进行全局异常编写的情况下,它会抛出异常

image.png 完整的异常内容:

java
2025-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 格式,并将其发送给客户端。

image.png

也就是现在这样的结果,但是为了实现统一的返回结构和规范化,所以要建立一个统一的返回结构

java
private 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; } }

枚举记录统一响应体

java
public 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; } }

最终的全局异常处理代码

java
package 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 许可协议。转载请注明出处!