Skip to content

SpringBoot 项目开发

12812字约43分钟

JavaSpringBootmeavn

2025-08-28

SpringBoot 项目开发

0.环境搭建

  • 执行资料中的 big_event.sql 脚本,准备数据库表。

    -- 创建数据库
    create database javaDB;
    
    -- 使用数据库
    use javaDB;
    
    -- 用户表
    create table user (
                          id int unsigned primary key auto_increment comment 'ID',
                          username varchar(20) not null unique comment '用户名',
                          password varchar(32)  comment '密码',
                          nickname varchar(10)  default '' comment '昵称',
                          email varchar(128) default '' comment '邮箱',
                          user_pic varchar(128) default '' comment '头像',
                          create_time datetime not null comment '创建时间',
                          update_time datetime not null comment '修改时间'
    ) comment '用户表';
    
    -- 分类表
    create table category(
                             id int unsigned primary key auto_increment comment 'ID',
                             category_name varchar(32) not null comment '分类名称',
                             category_alias varchar(32) not null comment '分类别名',
                             create_user int unsigned not null comment '创建人ID',
                             create_time datetime not null comment '创建时间',
                             update_time datetime not null comment '修改时间',
                             constraint fk_category_user foreign key (create_user) references user(id) -- 外键约束
    );
    
    -- 文章表
    create table article(
                            id int unsigned primary key auto_increment comment 'ID',
                            title varchar(30) not null comment '文章标题',
                            content varchar(10000) not null comment '文章内容',
                            cover_img varchar(128) not null  comment '文章封面',
                            state varchar(3) default '草稿' comment '文章状态: 只能是[已发布] 或者 [草稿]',
                            category_id int unsigned comment '文章分类ID',
                            create_user int unsigned not null comment '创建人ID',
                            create_time datetime not null comment '创建时间',
                            update_time datetime not null comment '修改时间',
                            constraint fk_article_category foreign key (category_id) references category(id),-- 外键约束
                            constraint fk_article_user foreign key (create_user) references user(id) -- 外键约束
    )
  • 创建 springboot 工程,引入对应的依赖(webmybatismysql驱动)

    (1)使用 IDEA 进行 springboot 工程的创建,如下图所示。

    image-20250612094633807

    (2)继承 Spring Boot 的默认配置。

    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.3.3</version>
    </parent>

    (3)引入 依赖信息。

    <dependencies>
        <!-- web 依赖-->
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <version>3.3.4</version>
        </dependency>
        <!-- mybatis 依赖-->
        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.2</version>
        </dependency>
        <!-- mysql 驱动依赖-->
        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <version>9.0.0</version>
        </dependency>
        <!-- Lombok(用于简化代码) -->
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>
    </dependencies>

    (4)刷新 pom 文件,让 Maven 重新进行依赖加载。

  • 配置文件 application.yml 引入 mybatis 的配置信息。

    这里可能需要进行 resources 目录的创建。

    spring:
      datasource:
        driver-class-name: com.mysql.cj.jdbc.Driver
        url: jdbc:mysql://localhost:3306/javaDB
        username: root
        password: 1234
  • 创建包结构,并准备实体类。

    依次创建 controllermapperpojoserviceservice.implutils包,并将 UserCategoryArticle 实体类导入到 pojo 包下方。

    User 实体类:

    import lombok.Data;
    import java.time.LocalDateTime;
    
    @Data
    public class User {
        private Integer id;
        private String username;
        private String password;
        private String nickname;
        private String email;
        private String userPic;
        private LocalDateTime createTime;
        private LocalDateTime updateTime;
    }

    Category 实体类:

    import lombok.Data;
    import java.time.LocalDateTime;
    
    @Data
    public class Category {
        private Integer id;
        private String categoryName;
        private String categoryAlias;
        private Integer createUser;
        private LocalDateTime createTime;
        private LocalDateTime updateTime;
    }

    Article 实体类:

    # Article.java
    import lombok.Data;
    import java.time.LocalDateTime;
    
    @Data
    public class Article {
        private Integer id;
        private String title;
        private String content;
        private String coverImg;
        private String state; // 应该只允许 "已发布" 或 "草稿"
        private Integer categoryId;
        private Integer createUser;
        private LocalDateTime createTime;
        private LocalDateTime updateTime;
    }

    启动类需要进行修改,一般默认的类名为 App,修改为项目 + Application的形式,如项目名称是 big-event 则对应的启动类名称为 BigEventApplication,修改启动类的内容,如下所示:

    package com.euansu;
    
    import org.springframework.boot.SpringApplication;
    import org.springframework.boot.autoconfigure.SpringBootApplication;
    
    // SpringBootApplication注解
    @SpringBootApplication
    public class BigEventApplication {
        public static void main(String[] args) {
            // 这里是固定的组合,传入类和参数
            SpringApplication.run(BigEventApplication.class, args);
        }
    }

创建结束后,项目的目录结构如下:

big-event
  pom.xml											# Maven 项目配置文件
├─src
  └─main
      ├─java										# Java 源代码目录
  └─com
      └─euansu								# 包名 com.euansu
  BigEventApplication.java		# Spring Boot 启动类
          ├─controller						# 控制器类(处理 HTTP 请求)
          ├─mapper							# Mapper 接口(数据库操作)
          ├─pojo							# 实体类(POJO)
      Article.java				# 实体类(Article)	
      Category.java				# 实体类(Category)	
      User.java					# 实体类(User)	

          ├─service
  └─impl							# 服务实现类(业务逻辑)
          └─utils							# 工具类(通用方法)
      └─resources									# 资源目录
              application.yml						# Spring Boot 主配置文件

使用启动类运行项目,如下图所示:

image-20250612141613822

1.接口开发

1.1 用户模块接口开发

用户模块有六个接口需要开发:注册、登录、获取用户详细信息、更新用户基本信息、更新用户头像、更新用户密码

1.1.1 注册接口

在开发注册接口前,先对 SpringBoot 项目的返回进行规范,项目的响应为三个公共字段:codemessagedata,这里增加一个实体类进行返回信息的处理。

package com.euansu.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

// 统一响应结果
@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> {
    private Integer code; // 业务状态码,0-成功 1-失败
    private String message; // 提示信息
    private T data; // 响应数据

    // 快速返回操作成功响应结果(带响应数据)
    public static <E> Result<E> success(E data) {
        return new Result<>(0, "操作成功", data);
    }

    // 快速返回操作成功响应结果
    public static Result success() {
        return new Result(0, "操作成功", null);
    }

    public  static Result error(String message) {
        return new Result(1, message, null);
    }
}

注册接口的开发流程图如下所示,先进行 Controller 的开发,接下来是 ServiceMapper

image-20250612153732338

  1. 创建所要用到的 Java 类,UserController.javaUserService.javaUserServiceImpl.javaUserMapper.java

    • UserController.java

      package com.euansu.controller;
      
      public class UserController {
      }
    • UserService.java

      package com.euansu.service;
      
      public interface UserService {
      }
    • UserServiceImpl.java

      package com.euansu.service.impl;
      
      import com.euansu.service.UserService;
      
      public class UserServiceImpl implements UserService {
      }
    • UserMapper.java

      package com.euansu.mapper;
      
      public interface UserMapper {
      }
  2. UserController.java

    package com.euansu.controller;
    
    import com.euansu.pojo.Result;
    import com.euansu.pojo.User;
    import com.euansu.service.UserService;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.web.bind.annotation.PostMapping;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RestController;
    
    @RestController
    @RequestMapping("/user")
    public class UserController {
        @Autowired
        private UserService userService;
    
        @PostMapping("/register")
        public Result register(String username, String password) {
            // 查询用户名是否存在
            User u = userService.findByUserName(username);
            if(u != null) {
                // 被占用了
                return Result.error("用户名已被占用");
            } else{
                // 注册新用户
                userService.register(username,password);
                return Result.success();
            }
        }
    }
  3. UserService.java

    package com.euansu.service;
    
    import com.euansu.pojo.User;
    
    public interface UserService {
        // 根据用户名查询用户
        User findByUserName(String username);
        // 注册
        void register(String username, String password);
    }
  4. UserServiceImpl.java,这里需要引入 MD5 算法对用户名进行加密处理。

    # UserServiceImpl.java
    package com.euansu.service.impl;
    
    import com.euansu.mapper.UserMapper;
    import com.euansu.pojo.User;
    import com.euansu.service.UserService;
    import com.euansu.utils.Md5Util;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.stereotype.Service;
    
    @Service
    public class UserServiceImpl implements UserService {
        @Autowired
        private UserMapper userMapper;
        @Override
        public User findByUserName(String username) {
            User u = userMapper.findByUserName(username);
            return u;
        }
    
        @Override
        public void register(String username, String password) {
            // 密码做加密处理
            String md5String = Md5Util.getMD5String(password);
            // 用户注册
            userMapper.add(username, md5String);
    
        }
    }
  5. UserMapper.java

    package com.euansu.mapper;
    
    import com.euansu.pojo.User;
    import org.apache.ibatis.annotations.Insert;
    import org.apache.ibatis.annotations.Mapper;
    import org.apache.ibatis.annotations.Select;
    
    @Mapper
    public interface UserMapper {
        // 根据用户名查询用户
        @Select("select * from user where username=#{username}")
        User findByUserName(String username);
        // 添加
        @Insert("insert into user (username,password,create_time,update_time)" +
        " values (#{username}, #{md5String}, now(), now())")
        void add(String username, String md5String);
    }
  6. 实现如上代码后,使用 postman 进行测试,能够正常对用户的注册请求进行响应处理。

    image-20250612160013359

用户注册接口的调用流程总结如下:

  • 客户端(浏览器或前端)发送 HTTP 请求 ↓

  • Controller 层UserController 接收请求 ↓

  • Service 层:通过 UserService 接口调用 UserServiceImpl

  • Mapper/DAO 层UserServiceImpl 调用 UserMapper 直接与数据库交互 ↓

  • 数据库:执行 SQL 向下访问数据

1.1.2 注册接口的参数校验

参数校验的要求如下:

参数名称说明类型是否必须备注
username用户名string5-16位非空字符
password密码string5-16位非空字符

使用 Spring Validation 来实现接口的参数校验,操作步骤如下:

  1. 导入 Spring Validation 依赖信息,刷新 pom 文件。

    <!-- Spring Validation 起步依赖 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-validation</artifactId>
    </dependency>
  2. 在参数上添加 @Pattern 注解,指定校验规则。

    @RestController
    @RequestMapping("/user")
    public class UserController {
        @Autowired
        private UserService userService;
    
        @PostMapping("/register")
        // Pattern 注解
        public Result register(@Pattern(regexp = "^\\S{5,16}$") String username,@Pattern(regexp = "^\\S{5,16}$") String password) {
            // 查询用户名是否存在
            User u = userService.findByUserName(username);
            if(u != null) {
                // 被占用了
                return Result.error("用户名已被占用");
            } else{
                // 注册新用户
                userService.register(username,password);
                return Result.success();
            }
        }
    }
  3. Controller 类上添加 Validated 注解。

    @RestController
    @RequestMapping("/user")
    // Validated注解
    @Validated
    public class UserController {
        @Autowired
        private UserService userService;
    
        @PostMapping("/register")
        public Result register(@Pattern(regexp = "^\\S{5,16}$") String username,@Pattern(regexp = "^\\S{5,16}$") String password) {
            // 查询用户名是否存在
            User u = userService.findByUserName(username);
            if(u != null) {
                // 被占用了
                return Result.error("用户名已被占用");
            } else{
                // 注册新用户
                userService.register(username,password);
                return Result.success();
            }
        }
    }

    添加完成后,使用 Postman 接口测试工具进行测试,出现如下报错。

    image-20250613103548807

    查看控制台,会发现这是参数校验不通过的报错,但是没有对报错进行处理导致直接返回了 500 的异常报错,接下来还需要增加异常的全局处理。

    image-20250613103746720

  4. 在全局异常处理器中处理参数校验失败的内容,在项目中增加一个 exception 的包,添加 GlobalExceptionHandler.java,全局异常处理器的内容如下所示:

    import com.euansu.pojo.Result;
    import org.springframework.util.StringUtils;
    import org.springframework.web.bind.annotation.ExceptionHandler;
    import org.springframework.web.bind.annotation.RestControllerAdvice;
    
    @RestControllerAdvice
    public class GlobalExceptionHandler {
    
        @ExceptionHandler(Exception.class)
        public Result handlerException(Exception e) {
            e.printStackTrace();
            return Result.error(StringUtils.hasLength(e.getMessage()) ? e.getMessage() : "操作失败");
        }
    }

    再次请求,能够正常进行错误的响应。

    image-20250613104426072

1.1.3 登录主逻辑

登录接口的开发流程如下图所示:

image-20250613111006679

Controller中添加登录接口,主逻辑实现代码如下:

    @PostMapping("/login")
    public Result<String> login(@Pattern(regexp = "^\\S{5,16}$") String username,@Pattern(regexp = "^\\S{5,16}$") String password){
        // 根据用户名查询用户
        User loginUser = userService.findByUserName(username);
        // 判断用户是否存在
        if(loginUser == null) {
            return Result.error("用户不存在");
        }

        // 判断密码是否正确
        if(Md5Util.getMD5String(password).equals(loginUser.getPassword())) {
            // 登录成功
            return Result.success("jwt token令牌");
        }else{
            return Result.error("登录密码错误");
        }
    }

1.1.4 登录令牌(JWT)

还有一个 ArticleController下的接口,在未登录的时候需要进行检查,确认用户只有登录后才允许访问该接口。

package com.euansu.controller;

import com.euansu.pojo.Result;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/article")
public class ArticleController {

    @GetMapping("/list")
    public Result<String> list(){
        return Result.success("文章列表信息。。。");
    }
}

如下图所示,用户未进行登录也访问到了接口信息,这里需要对接口添加认证,其中的一种形式是登录令牌。

image-20250613133435129

JWT,全称 Json Web Token,定义了一种简洁的、自包含的格式,用于通信双方以 json 数据格式安全的传输信息。

组成:

  • 第一部分:Header(头),记录令牌类型、签名算法等,例如:{"alg":"HS256","type":"JWT"}
  • 第二部分:Payload(有效载荷),携带一些自定义信息、默认信息等。例如:{"id":"1","username":"euansu"}
  • 第三部分:Signature(签名),防止Token被篡改、确保安全性。将headerpayload 加入制定秘钥,通过签名算法计算而来。

image-20250613134139701

这里在 SpringBoot 自带的测试方法中测试 JWT 令牌的相关功能,测试步骤如下:

  1. 引入 jwt 依赖和 springboot-test 依赖,添加后刷新 pom 依赖。

    <!-- Java JWT依赖 -->
    <dependency>
        <groupId>com.auth0</groupId>
        <artifactId>java-jwt</artifactId>
        <version>4.4.0</version>
    </dependency>
    <!-- SpringBoot test依赖 -->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-test</artifactId>
    </dependency>
  2. test 包下添加 JwtTest.java 测试类,代码如下:

    import com.auth0.jwt.JWT;
    import com.auth0.jwt.JWTVerifier;
    import com.auth0.jwt.algorithms.Algorithm;
    import com.auth0.jwt.interfaces.Claim;
    import com.auth0.jwt.interfaces.DecodedJWT;
    import org.junit.jupiter.api.Test;
    
    import java.util.Date;
    import java.util.HashMap;
    import java.util.Map;
    
    
    public class JwtTest {
    
        @Test
        public void testGen(){
    
            // 生成JWT对象
            Map<String, Object> claims = new HashMap<>();
            claims.put("id",1);
            claims.put("username", "euansu");
            String token = JWT.create()
                    .withClaim("user",claims)
                    .withExpiresAt(new Date(System.currentTimeMillis() + 1000 * 60 * 30))
                    .sign(Algorithm.HMAC256("euansu"));
            System.out.println(token);
        }
    
        @Test
        public void testParse(){
            // 解析JWT对象
            String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjp7ImlkIjoxLCJ1c2VybmFtZSI6ImV1YW5zdSJ9LCJleHAiOjE3NDk4MTkzMjd9.5q-XBmgY6XDPS_f_F-OVW1Gn6jMC7VvooAfvphaSPeI";
            JWTVerifier jwtVerifier = JWT.require(Algorithm.HMAC256("euansu")).build();
            DecodedJWT decodedJWT = jwtVerifier.verify(token);
            Map<String, Claim> claims = decodedJWT.getClaims();
            System.out.println(claims.get("user"));
        }
    }
  3. 测试一下正常的 JWT 令牌的生成和解析。

    如下图所示,正常生成一个 JWT 令牌。

    image-20250613202946865

    将生成的 JWT 令牌进行解析,能够正常获取到载荷信息,如下图所示。

    image-20250613203003499

  4. 测试 JWT 令牌被篡改后,是否能够正常解析。

    // 修改header
    String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjp7ImlkIjoxLCJ1c2VybmFtZSI6ImV1YW5zdSJ9LCJleHAiOjE3NDk4MTkzMjd9.5q-XBmgY6XDPS_f_F-OVW1Gn6jMC7VvooAfvphaSPeI";

    image-20250613203046678

    // 修改载荷
    String token = "eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJ1c2VyIjp7ImlkIjoxLCJ1c2VybmFtZSI6ImV1YW5zdSJ9LCJleHAiOjE3NDk4MTkzMjd0.5q-XBmgY6XDPS_f_F-OVW1Gn6jMC7VvooAfvphaSPeI";

    image-20250613203130474

1.1.5 登录认证

登录认证这里的流程是:

  • 使用拦截器统一验证令牌。
  • 登录和注册接口需要放行。
  1. 新建 JwtUtil.java 工具类,写入创建和解析 JWT 会话令牌的方法,详细代码如下所示:

    import com.auth0.jwt.JWT;
    import com.auth0.jwt.algorithms.Algorithm;
    
    import java.util.Date;
    import java.util.Map;
    
    public class JwtUtil {
    
        private static final String SECRET = "euansu";
    
        // 接收业务数据,生成token并返回
        public static String generateToken(Map<String, Object> claims) {
            return JWT.create()
                    .withClaim("claims",claims)
                    .withExpiresAt(new Date(System.currentTimeMillis() + 1000 * 60 * 30))
                    .sign(Algorithm.HMAC256(SECRET));
        }
    
        // 接收token,验证token,返回业务数据
        public static Map<String, Object> parseToken(String token) {
            return JWT.require(Algorithm.HMAC256(SECRET))
                    .build()
                    .verify(token)
                    .getClaim("claims")
                    .asMap();
    
        }
    }
  2. 修改 UserController.java 中登录的方法,当用户登录成功后,返回一个 JWTtokne 串,修改后的代码如下所示:

    @PostMapping("/login")
    public Result<String> login(@Pattern(regexp = "^\\S{5,16}$") String username,@Pattern(regexp = "^\\S{5,16}$") String password){
        // 根据用户名查询用户
        User loginUser = userService.findByUserName(username);
        // 判断用户是否存在
        if(loginUser == null) {
            return Result.error("用户不存在");
        }
    
        // 判断密码是否正确
        if(Md5Util.getMD5String(password).equals(loginUser.getPassword())) {
            // 登录成功
            Map<String, Object> claims = new HashMap<>();
            claims.put("id",loginUser.getId());
            claims.put("username", loginUser.getUsername());
            JwtUtil.generateToken(claims);
            return Result.success(JwtUtil.generateToken(claims));
        }else{
            return Result.error("登录密码错误");
        }
    }
  3. 添加一个登录拦截器,用于校验请求头中是否携带 Authorization 串,具体代码如下所示:

    # 这里新建了一个 interceptors 的包
    # 创建了LoginInterceptor类
    package com.euansu.interceptors;
    
    import com.euansu.utils.JwtUtil;
    import jakarta.servlet.http.HttpServletRequest;
    import jakarta.servlet.http.HttpServletResponse;
    import org.springframework.stereotype.Component;
    import org.springframework.web.servlet.HandlerInterceptor;
    
    import java.util.Map;
    
    @Component
    public class LoginInterceptor implements HandlerInterceptor {
    
        @Override
        public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
            // 令牌验证
            String token = request.getHeader("Authorization");
            // 验证token
            try{
                Map<String, Object> claims = JwtUtil.parseToken(token);
                // 不报错,放行
                return true;
            } catch (Exception e){
                // http 响应状态码为401
                response.setStatus(401);
                // 不放行
                return false;
            }
    
        }
    }
  4. 增加一个SpringBoot MVC配置类,主要用于配置拦截器,详细代码如下所示:

    package com.euansu.config;
    
    import com.euansu.interceptors.LoginInterceptor;
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.context.annotation.Configuration;
    import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
    import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
    
    @Configuration
    public class WebConfig implements WebMvcConfigurer {
    
        @Autowired
        private LoginInterceptor loginInterceptor;
    
    
        @Override
        public void addInterceptors(InterceptorRegistry registry) {
            System.out.println("LoginInterceptor");
            // 登录和注册接口不做拦截
            registry.addInterceptor(loginInterceptor).excludePathPatterns("/user/login", "/user/register");
        }
    }
  5. 修改完成后,重启项目进行测试。

    当请求头中不含有 Authorization 时,返回 401 状态的错误。

    image-20250613205903612

    当请求头中包含 Authorization 时,能够正常返回请求的响应信息。

    image-20250613210006288

1.1.6 获取用户的详细信息

获取用户详细信息的接口流程

image-20250616102757658

首先在 UserController 中添加 userInfo 接口,由于根据用户名查询的方法在 ServiceMapper 中已有,这里就不在做开发了,代码如下:

@GetMapping("/userInfo")
public Result<User> UserInfo(@RequestHeader(name="Authorization") String token){
    // 这个接口从token令牌中解析用户信息,无参数输入
    // 根据用户名查询用户
    Map<String, Object> map = JwtUtil.parseToken(token);
    String username = (String) map.get("username");
    User user = userService.findByUserName(username);
    return Result.success(user);
}

使用接口工具测试,能够正常返回响应信息,测试截图如下:

image-20250616105600957

每增加一个接口,需要手动添加 Authorization 请求头,否则会报用户认证的错误,这里过于繁琐,Postman 提供了集合统一增加请求头的方式,在 Script中,选择 pre-request,增加如下内容,就完成了 Authorization 请求头的增加。

pm.request.addHeader("Authorization:eyJhbGciOiJIUzI1NiIsInR5cCI6IkpXVCJ9.eyJjbGFpbXMiOnsiaWQiOjMsInVzZXJuYW1lIjoibmFuZ2UifSwiZXhwIjoxNzUwMDQzMTY4fQ.sk7dnq82ZPaVxmNRdfuGgLhccKUxWOjmAMfLRqKezw0")

image-20250616105754896

在单个接口的请求中,移除 Authorization,能够正常得到响应。

image-20250616105812785

再查看用户信息接口的返回信息,返回的数据字段中将 password 字段也进行了返回,这是不对的,需要在用户实体类中给 password 字段增加 JsonIgnore 注解,这样在 SpringMVC 将当前对象转化为 JSON 字符串的时候忽略掉 password 这个属性,如下:

import com.fasterxml.jackson.annotation.JsonIgnore;
// 这里特别注意引入JsonIgnore是com.fasterxml.jackson.annotation.JsonIgnore,否则会导致注解不生效

@Data
public class User {
    ...
    @JsonIgnore // 让SpringMVC把当前对象转换成JSON字符串的时候,忽略password,最终的JSON字符串中就没有password这个属性了
    private String password;
}

检查数据库中 User 用户表,其中的 create_timeupdate_time 均不为空,但是返回为空,这里是因为 User 实体类中使用了驼峰命名的方式,与表字段对象不上。

image-20250616111736883

@Data
public class User {
    ...
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
}

这里需要在 SpringBoot 的项目配置文件中增加 map-underscore-to-camel-case 配置,开启驼峰命名方式和下划线的自动转换。

mybatis:
  configuration:
    map-underscore-to-camel-case: true # 开启驼峰命名和下划线命名的转换

修改完成后,重启项目再次进行请求,请求结果中的 password 字段消失,createTimeupdateTime 字段值不为空。

image-20250616112611829

1.1.7 获取用户详细信息优化(ThreadLocal)

ThreadLocal :提供线程局部变量

  • 用来存取数据:set()/get()
  • 使用 ThreadLocal 存储的数据,线程安全。

image-20250616113709708

使用 ThreadLocal 能够实现线程间的共享,可以在拦截器中进行设置,在 ControllerService或者 Dao层实现变量的获取。

image-20250616152805877

这里对获取用户详细信息的接口进行改造,在拦截中进行 ThreadLocal 的设置,在 UserService中进行获取,拦截器中增加 ThreadLocal 变量的设置和清理,代码如下:

@Component
public class LoginInterceptor implements HandlerInterceptor {

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        // 令牌验证
        String token = request.getHeader("Authorization");
        // 验证token
        try{
            Map<String, Object> claims = JwtUtil.parseToken(token);
            // 不报错,放行
            // 设置 ThreadLocal 变量
            ThreadLocalUtil.set(claims);
            return true;
        } catch (Exception e){
            // http 响应状态码为401
            response.setStatus(401);
            // 不放行
            return false;
        }

    }

    @Override
    public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) throws Exception {
        // 请求完成,清空 ThreadLocal中的数据
        ThreadLocalUtil.remove();
    }
}

UserController 中,直接获取 ThreadLocal 设置的变量信息,代码如下:

@GetMapping("/userInfo")
public Result<User> UserInfo(@RequestHeader(name="Authorization") String token){
    // 根据用户名查询用户
    // Map<String, Object> map = JwtUtil.parseToken(token);
    // String username = (String) map.get("username");
    // 通过拦截器后的请求,直接获取ThreadLocal中的变量信息
    Map<String,Object> map = ThreadLocalUtil.get();
    String username = (String) map.get("username");
    User user = userService.findByUserName(username);
    return Result.success(user);
}

修改完成后,重启 SpringBoot 项目工程,能够正常获取到用户信息的接口响应。

image-20250616155108521

1.1.8 更新用户基本信息

更新用户信息的开发流程图如下所示,需要依次在 UserControllerUserServiceUserMapper 进行开发。

image-20250616184214655

UserController 进行 update 接口的开发,新增代码如下所示:

@PutMapping("/update")
public Result update(@RequestBody User user){
    userService.update(user);
    return Result.success();
}

UserService 中进行 update 方法的签名(即方法名称、参数和返回值类型),这里不提供具体的实现。

public interface UserService {
    // 更新
    void update(User user);
}

UserServiceImpl 实现类中进行 UserService 中方法的实现,具体代码如下。

@Override
public void update(User user) {
    user.setUpdateTime(LocalDateTime.now());
    userMapper.update(user);
}

UserMapper 数据交互层做数据的更新操作,具体代码如下所示。

@Mapper
public interface UserMapper {
    // 更新
    @Update("update user set nickname=#{nickname},email=#{email},update_time=#{updateTime} where id=#{id}")
    void update(User user);
}

编写完如上代码后,重启 SpringBoot项目,做用户更新请求前,检查数据库中记录信息,如下所示。

image-20250618101106312

使用 postman 接口测试工具进行请求,如下所示,显示请求成功。

image-20250618101044748

再去检查数据库中的记录信息,nicknameemailupdate_time 字段均被修改,更新操作成功。

image-20250618101132318

1.1.9 更新用户基本信息_参数校验

用户基本信息参数校验规则如下:

参数名称说明类型是否必须备注
id主键IDnumber
username用户名称string5~16位非空字符
nickname昵称string1~10位非空字符
email邮箱string满足邮箱格式

参数校验的视线流程只有两步:1.修改User实体类,对要进行校验的字段进行相关规则的注解。2.在请求参数前添加 Validated 注解。

修改 User 实体类,对要校验的参数进行相关注解,如下所示。

@Data
public class User {
    @NotNull
    private Integer id;
    private String username;
    @JsonIgnore // 让SpringMVC把当前对象转换成JSON字符串的时候,忽略password,最终的JSON字符串中就没有password这个属性了
    private String password;
    @NotEmpty
    @Pattern(regexp = "^\\S{1,10}$")
    private String nickname;
    @NotEmpty
    @Email
    private String email;
    private String userPic;
    private LocalDateTime createTime;
    private LocalDateTime updateTime;
}

注解的相关说明如下表格中所示。

注解作用
NotNull值不能为null
NotEmpty值不能为null,并且内容不为空
Email满足邮箱格式

修改 UserController 中的 update 的请求参数,添加 @Validated 注解,如下所示。

@PutMapping("/update")
public Result update(@RequestBody @Validated User user){
    userService.update(user);
    return Result.success();
}

修改完成后使用 postman 接口测试工具进行请求,能够正常返回参数校验不通过的信息。

image-20250618103605387

1.1.10 更新用户头像

请求路径:/user/updateAvatar

请求方式:PATCH

更新用户头像的开发流程如下所示:

image-20250619153852059

UserController 中增加 updateAvatar 接口的代码,调用 userService 下的 updateAvatar 来实现具体的逻辑。

@PatchMapping("/updateAvatar")
public Result updateAvatar(@RequestParam String avatarUrl){
    userService.updateAvatar(avatarUrl);
    return Result.success();
}

userService 中增加 updateAvatar 的方法签名,如下所示:

public interface UserService {
    // 更新头像
    void updateAvatar(String avatarUrl);
}

UserServiceImpl 添加 updateAvatar 的具体代码,调用 UserMapper 中的 updateAvatar 来负责具体的数据交互操作,如下所示:

@Override
public void updateAvatar(String avatarUrl) {
    Map<String,Object> map = ThreadLocalUtil.get();
    Integer id = (Integer) map.get("id");
    userMapper.updateAvatar(avatarUrl, id);
}

UserMapper 中增加 updateAvatar 的数据交互操作,如下所示:

@Mapper
public interface UserMapper {
    // 更新头像
    @Update("update user set user_pic=#{avatarUrl},update_time=now() where id=#{id}")
    void updateAvatar(String avatarUrl,Integer id);
}

如上代码编写完成后,重启 SpringBoot 项目程序,使用 postman 测试工具进行测试。

image-20250620114947693

操作成功后,这里检查数据库,user_pic 字段出现对应的记录信息。

image-20250620115036405

如上所示,这里传入的字段只是一个字符串,并不是实际的头像地址,这样是不对的,需要增加头像地址的参数校验,修改 UserController 中的 updateAvatar 方法,在方法的参数中增加 URL 的校验注解,如下所示。

import org.hibernate.validator.constraints.URL;
public Result updateAvatar(@RequestParam @URL String avatarUrl)

再次使用 postman 接口测试工具请求服务时,就提示接口校验不通过了,如下图所示。

image-20250620115333428

修改传入的 avatarUrl 为一个实际的图片地址,再次请求就能够得到正常的响应,如下所示。

image-20250620115357994

检查数据库中的记录,user_pic 存储了一个正常的图片地址。

image-20250620133259434

1.1.10 更新用户密码

请求路径:/user/upatePwd

请求方式:PATCH

请求参数格式:application/json

更新用户密码的开发流程图如下所示,在 UserController 中的参数指定类型为 Map,这里是因为传入的参数没有与实体类相对应,因此声明为 Map 对象。

image-20250620143430441

首先在 userController 中增加 updatePwd 的接口代码,生成参数类型为 Map 类型,在接口中队参数进行校验后,调用 userServiceupdatePwd 方法。

@PatchMapping("/updatePwd")
public Result updatePwd(@RequestBody Map<String,String> parmas){
    // 1.参数校验
    String oldPwd = parmas.get("old_pwd");
    String newPwd = parmas.get("new_pwd");
    String rePwd = parmas.get("re_pwd");
    if (!StringUtils.hasLength(oldPwd) || !StringUtils.hasLength(newPwd) || !StringUtils.hasLength(rePwd)) {
        return Result.error("缺少必要的参数");
    }
    // 原密码是否正确
    Map<String,Object> map = ThreadLocalUtil.get();
    String username = (String) map.get("username");
    User loginUser = userService.findByUserName(username);
    System.out.println(oldPwd);
    if(!loginUser.getPassword().equals(Md5Util.getMD5String(oldPwd))) {
        return Result.error("原密码填写不正确");
    }

    if(!rePwd.equals(newPwd)){
        return Result.error("两次填写的新密码不一致");
    }

    // 2.调用service完成密码更新
    userService.updatePwd(newPwd);

    return Result.success();
}

UserService 中增加 updatePwd 方法的签名,如下所示

public interface UserService {
    // 更新密码
    void updatePwd(String newPwd);
}

UserServiceImpl 实体类中增加 updatePwd 的方法操作,调用userMapper中的 updatePwd 做数据交互的操作,如下所示

@Override
public void updatePwd(String newPwd) {
    Map<String,Object> map = ThreadLocalUtil.get();
    Integer id = (Integer) map.get("id");
    userMapper.updatePwd(Md5Util.getMD5String(newPwd), id);
}

userMapper 中增加 updatePwd 方法的实现代码,如下所示:

@Mapper
public interface UserMapper {
    // 更新密码
    @Update("update user set password=#{md5String},update_time=now() where id=#{id}")
    void updatePwd(String md5String, Integer id);
}

代码编写完成后,重启 SpringBoot 工程,使用 postman 调用接口进行测试,能够正常修改密码,如下图所示。

image-20250620145710659

使用修改后的密码,能够正常进行登录,如下所示。

image-20250620145738055

1.2 文章分类模块接口开发

1.2.1 文章分类接口新增

请求路径:/category

请求方式:POST

请求参数格式:applictaion/json

请求参数说明

参数名称说明类型是否必须备注
categoryName分类名称String
categoryAlias分类别名String

新增文章分类接口开发流程图

image-20250624094243745

这里是第一次新增 Category 模块的接口,因此首先需要新建 CategoryControllerCategoryServiceCategoryServiceImplCategoryMapper,对应的实体类 Category 已经提前创建完成。

首先在 CategoryController 中新增 RestController 等相关注解,并增加 add 接口。

@RestController
@RequestMapping("/category")
public class CategoryController {

    @Autowired
    private CategoryService categoryService;

    @PostMapping
    public Result addCategory(@RequestBody Category category) {
        categoryService.add(category);
        return Result.success();
    }
}

CategoryService 中增加 add 接口的声明,如下所示

public interface CategoryService {
    // 新增文章分类
    void add(Category category);
}

CategoryServiceImpl 中增加 add 接口的实现方法以及 Service 等注解

@Service
public class CategoryServiceImpl implements CategoryService {

    @Autowired
    private CategoryMapper categoryMapper;

    @Override
    public void add(Category category) {
        // 补充属性值
        category.setCreateTime(LocalDateTime.now());
        category.setUpdateTime(LocalDateTime.now());
        Map<String,Object> map = ThreadLocalUtil.get();
        Integer userId = (Integer) map.get("id");
        category.setId(userId);
        categoryMapper.add(category);
    }
}

CategoryMapper 中增加 add 的数据交互操作以及 Mapper 的注解,如下所示

@Mapper
public interface CategoryMapper {

    @Insert("insert into category (category_name,category_alias,create_user,create_time,update_time) values (#{categoryName},#{categoryAlias},#{id},#{createTime},#{updateTime})" )
    void add(Category category);
}

重启 SpringBoot 工程,使用 postman 工具进行测试,如下所示,能够成功进行文章分类的添加操作

image-20250624103558142

该接口还缺少相关的数据校验操作,如下所示,当设置 categoryAlias 时,应该提示字段为空而不是操作成功

image-20250624103817918

Category 实体类中的 categoryNamecategoryAlias 增加相关校验注解,如下所示

@Data
public class Category {
    private Integer id;                 // 主键ID
    @NotEmpty
    private String categoryName;        // 分类名称
    @NotEmpty
    private String categoryAlias;       // 分类别名
    private Integer createUser;         // 创建人ID
    private LocalDateTime createTime;   // 创建时间
    private LocalDateTime updateTime;   // 更新时间
}

修改 CateoryControlleradd 接口的参数,增加校验的注解,如下所示

@PostMapping
// 增加 Validated 校验注解
public Result addCategory(@RequestBody @Validated Category category) {
    categoryService.add(category);
    return Result.success();
}

使用 postman 测试工具进行接口的调用,提示 categoryAlias 参数为空,如下所示

image-20250624104028704

1.2.2 文章分类列表接口

请求路径:/category/

请求方式:GET

请求参数:无

文章分类列表接口的开发流程图如下所示,这里需要注意最后的 Mapper 层只需要查询用户相关的分类,不能查询他人新增的分类

image-20250624112256284

文章分类新增接口和文章分类列表接口路由一致,但对应的请求方法存在差异,这里的开发流程也一样,首先编写 Controller 层的代码,然后是 Service 层,最后则是 Mapper层。

CategoryController 层新增文章列表接口的代码,调用 CategoryService 层的相关代码。

@GetMapping
public Result<List<Category>> list() {
    List<Category> cs = categoryService.list();
    return Result.success(cs);
}

CategroyService 层增加列表方法的声明,如下所示

public interface CategoryService {
    // 新增文章分类
    void add(Category category);
    // 文章分类列表
    List<Category> list();
}

CategoryServiceImpl 中增加列表方法的具体实现,如下所示

@Override
public List<Category> list() {
    Map<String,Object> map = ThreadLocalUtil.get();
    Integer userId = (Integer) map.get(("id"));
    return categoryMapper.list(userId);
}

最后则是在 CategoryMapper 中编写数据交互的具体操作,如下所示

@Select("select * from category where create_user=#{userId}")
List<Category> list(Integer userId);

重启 SpringBoot 项目后,使用 postman 工具进行测试,能够正常获取到文章分类的列表,如下所示

image-20250624113455827

这里获取到的时间格式存在问题,还需要在实体类的时间字段上增加注解,设置返回数据中的时间格式,如下所示

@Data
public class Category {
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;   // 创建时间
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime updateTime;   // 更新时间
}

重启 SpringBoot 工程项目,再次使用 postman 接口工具进行测试,能够返回调整时间格式后的字段信息

image-20250624113627670

1.2.3 文章分类详情

请求路径:/category/detail

请求方式:GET

请求参数格式:queryString

请求参数说明

参数名称说明类型是否必须备注
id主键IDNumber

获取文章分类详情的开发流程图如下

image-20250624115140195

首先,在 CategoryController 在编写分类详情接口,调用 Service 层的方法实现具体逻辑,代码如下所示

@GetMapping("/detail")
public Result<Category> detail(@RequestParam Integer id) {
    Category category = categoryService.findById(id);
    return Result.success(category);
}

CategoryService 声明 findById 的方法,代码如下所示

public interface CategoryService {
    // 新增文章分类
    void add(Category category);
    // 文章分类列表
    List<Category> list();
    // 文章详情
    Category findById(Integer id);
}

CategoryServiceImpl 中编写 findById 的具体代码实现,如下所示

@Override
public Category findById(Integer id) {
    return categoryMapper.findById(id);
}

CategoryMapper 中编写数据交互的操作方法,代码如下所示

@Select("select * from category where id=#{id}")
Category findById(Integer id);

重启 SpringBoot 项目,使用 postman 测试工具请求 detai 接口,如下所示能够正常返回文章分类的详情信息

image-20250624115548054

1.2.4 文章分类更新

请求路径:/category

请求方式:PUT

请求参数格式:application/json

请求参数说明

参数名称说明类型是否必须备注
id主键IDNumber
categoryName分类名称String
categoryAlias分类别名String

更新文章分类的接口开发流程图

image-20250624135930215

首先在CategoryCrontoller 中添加文章分类接口的代码实现,如下所示

@PutMapping
public Result updateCategory(@RequestBody @Validated Category category) {
    categoryService.update(category);
    return Result.success();
}

CategoryService 中编写对应方法的声明,如下所示

public interface CategoryService {
    // 新增文章分类
    void add(Category category);
    // 文章分类列表
    List<Category> list();
    // 文章分类详情
    Category findById(Integer id);
    // 文章分类更新
    void update(Category category);
}

CategoryServiceImpl 编写对应的实现方法,如下所示

@Override
public void update(Category category) {
    category.setUpdateTime(LocalDateTime.now());
    categoryMapper.update(category);
}

Categroy 中编写对应数据交互的方法,如下所示

@Update("update category set category_name=#{categoryName},category_alias=#{categoryAlias} where id=#{id}")
void update(Category category);

更新操作的时候,要求 idcategoryNamecategoryAlias 字段不能为空,需要修改实体类,在对应的字段上添加注解

@Data
public class Category {
    @NotNull
    private Integer id;                 // 主键ID
    @NotEmpty(groups = {Add.class, Update.class})
    private String categoryName;        // 分类名称
    @NotEmpty(groups = {Add.class, Update.class})
    private String categoryAlias;       // 分类别名
    private Integer createUser;         // 创建人ID
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;   // 创建时间
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime updateTime;   // 更新时间
}

重启 SpringBoot 项目后,使用 postman 接口测试工具请求测试,能够正常更新文章分类的记录信息,如下所示

image-20250624140555771

使用postman 调用详情接口,对应的文章分类详情被修改,如下所示

image-20250624140611801

1.2.5 分组校验

上一章节添加了文章分类更新的接口,对 id 字段添加了 NotNull 的注解,需要注意到的一点是,文章分类更新和文章分类新增的参数都用了Category实体类,但是新增文章分类的接口并不需要校验 id 字段,再次请求新增文章分类的时候,提示 id 字段要求不为空,这里就需要用到分组校验,也即不同的分组下的接口校验参数不一样。

image-20250624141435565

把校验项进行归类分组,在完成不同的功能的时候,校验指定组中的校验项:

  1. 定义分组(在实体类的内部定义接口)

    public interface Add{}
    public interface Update{}
  2. 定义校验项时指定归属的分组(通过groups属性指定)

    @Data
    public class Category {
        @NotNull(groups = Update.class)
        private Integer id;                 // 主键ID
        @NotEmpty(groups = {Add.class, Update.class})
        private String categoryName;        // 分类名称
        @NotEmpty(groups = {Add.class, Update.class})
        private String categoryAlias;       // 分类别名
    }
  3. 校验时指定要校验的分组(给@Validated注解的value属性赋值)

    @RestController
    @RequestMapping("/category")
    public class CategoryController {
    
        @Autowired
        private CategoryService categoryService;
    
        @PostMapping
        public Result addCategory(@RequestBody @Validated(Category.Add.class) Category category) {
            categoryService.add(category);
            return Result.success();
        }
        
        @PutMapping
        public Result updateCategory(@RequestBody @Validated(Category.Update.class) Category category) {
            categoryService.update(category);
            return Result.success();
        }
    }

image-20250624141221833

按照如上依次进行修改后,重启 SpringBoot 项目,使用 postman 测试文章分类新增接口,能够正常新增分类。

image-20250624151617154

使用如上方式能够实现接口分组参数校验的需求,但是如果要校验的分组过多,在字段添加 groups = {Add.class, Update.class,...}就会显得很臃肿,这里有一个默认分组的概念:

参考分组集成的的概念修改实体类中分组校验的定义,修改代码为如下样式

@Data
public class Category {
    @NotNull(groups = Update.class)
    private Integer id;                 // 主键ID
    @NotEmpty
    private String categoryName;        // 分类名称
    @NotEmpty
    private String categoryAlias;       // 分类别名
    private Integer createUser;         // 创建人ID
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;   // 创建时间
    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime updateTime;   // 更新时间

    public interface Add extends Default {}

    public interface Update extends Default {}
}

重启 SpringBoot 进行测试,能够正常新增文章分类。

image-20250624153133180

去掉 id 字段,更新时能够校验出缺少 id 字段,如下所示。

image-20250624153150589

1.2.6 文章分类删除

请求路径:/category/{id}

请求方式:DELETE

请求参数格式:路由参数

请求参数说明

参数名称说明类型是否必须备注
id主键IDNumber

删除接口的开发流程依旧是 ControllerServiceMapper,首先在 Category 中新增文章分类删除的接口代码,如下所示

@DeleteMapping("/{id}")
public Result delete(@PathVariable Integer id) {
    System.out.println("id");
    categoryService.deleteById(id);
    return Result.success();
}

CategoryService 中编写 deleteById 方法的声明,如下所示

public interface CategoryService {
    // 新增文章分类
    void add(Category category);
    // 文章分类列表
    List<Category> list();
    // 文章分类详情
    Category findById(Integer id);
    // 文章分类更新
    void update(Category category);
    // 文章分类删除
    void deleteById(Integer id);
}

CategoryServiceImpl中编写deleteById方法的具体实现,如下所示

@Override
public void deleteById(Integer id) {
    categoryMapper.deleteById(id);
}

CategroyMapper 中编写与数据的交互操作,如下所示

@Delete("delete from category where id=#{id}")
void deleteById(Integer id);

重启 SpringBoot 项目,使用 postman 接口测试工具请求文章分类删除的接口,能够正常进行对应文章分类的删除

image-20250624155029445

调用文章分类列表接口,返回的数据中不存在已删除的文章分类信息

image-20250624155056194

1.3 文章模块接口开发

1.3.1 新增文章接口

请求路径:/article

请求方式:POST

请求参数格式:application/json

请求参数说明

参数名称说明类型是否必须备注
title文章标题String1~10个非空字符
content文章正文String
coverImg封面图像地址String必须是 url 地址
state发布状态String已发布、草稿
categoryId文章分类IDNumber

新增文章接口的开发流程图

image-20250625094351357

文章模块这里是首次开发,因此需要先创建 ArticleControllerArticleServiceArticleServiceImplArticleMapper

ArticleController 中增加新增文章的接口代码,这里调用 Service层的代码来实现,如下所示

@PostMapping
public Result addArticle(@RequestBody Article article) {
    articleService.addArticle(article);
    return Result.success();
}

ArticleService中添加文章新增代码的声明,如下所示

public interface ArticleService {
    // 新增文章
    void addArticle(Article article);
}

ArticleServiceImpe 实现类中编写新增文章的具体实现,如下所示

@Override
public void addArticle(Article article) {

    article.setCreateTime(LocalDateTime.now());
    article.setUpdateTime(LocalDateTime.now());
    Map<String,Object> map = ThreadLocalUtil.get();
    Integer userId = (Integer) map.get("id");
    article.setCreateUser(userId);
    articleMapper.addArticle(article);
}

ArticleMapper 中编写与数据的交互操作,如下所示

@Insert("INSERT INTO article (title, content, cover_img, category_id, create_user, state, create_time, update_time) " +
        "VALUES (#{title},#{content},#{coverImg},#{categoryId},#{createUser},#{state},#{createTime},#{updateTime})")
void addArticle(Article article);

如上代码编写完成后,重启 SpringBoot 工程项目,使用 postman 工具进行测试,请求文章新增接口,能够正常进行文章的新增操作

image-20250625101851544

1.3.2 新增文章参数校验(自定义校验)

自定义校验:已有的注解不能满足所有的校验需求,特殊的情况需要自定义校验(自定义校验注解)

以新增文章为例,其中有一个参数要求是 state 参数的值只能是已发布或者草稿,这样的特殊需求无法使用 @NotEmpty@URL@Pattern等来实现,这里就需要我们手动实现一个自定义校验方法。

  1. 自定义注解 State

    这里新增一个 anno包,将State注解类放到包下,这里的内容可以通过 @NotEmpty 来参考修改。

    package com.euansu.anno;
    
    import com.euansu.validation.StateValidation;
    import jakarta.validation.Constraint;
    import jakarta.validation.Payload;
    
    import java.lang.annotation.*;
    
    @Documented // 元注解,标识State是可以抽取到帮助文档
    @Target({ElementType.FIELD}) // 元注解,标识State是可以作用到字段上
    @Retention(RetentionPolicy.RUNTIME) // 元注解,标识State在那个阶段会被保留
    @Constraint(validatedBy = {StateValidation.class}) // 指定谁来给State提供校验规则
    
    public @interface State {
        // 提供校验失败后的提供信息
        String message() default "state参数的值只能是已发布或者草稿";
        // 指定分组
        Class<?>[] groups() default {};
        // 负载,可以获取State注解的附加信息,一般不用
        Class<? extends Payload>[] payload() default {};
    
    }
  2. 自定义校验数据的类 StateValidation实现ConstraubrValidator接口

    这里同样是新增一个包 validation,将StateValidation放到包下,重写 isValid 提供对应的校验规则

    package com.euansu.validation;
    
    import com.euansu.anno.State;
    import jakarta.validation.ConstraintValidator;
    import jakarta.validation.ConstraintValidatorContext;
    
    // <给那个注解提供校验规则,校验的数据类型>
    public class StateValidation implements ConstraintValidator<State, String> {
        @Override
        public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) {
            // 提供校验规则
            /**
             * @parma   value 将来要校验的数据
             * @parma   constraintValidatorContext
             * @return  如果返回false,校验不通过
             *
             */
            if (s == null) {
                return false;
            }
            if(s.equals("已发布")||s.equals("草稿")){
                return true;
            }
    
            return false;
        }
    }
  3. 在需要校验的地方使用自定义注解

    package com.euansu.pojo;
    
    import com.euansu.anno.State;
    import jakarta.validation.constraints.NotEmpty;
    import jakarta.validation.constraints.NotNull;
    import jakarta.validation.constraints.Pattern;
    import lombok.Data;
    import org.hibernate.validator.constraints.URL;
    
    import java.time.LocalDateTime;
    
    @Data
    public class Article {
        private Integer id;
        @NotEmpty
        @Pattern(regexp = "^\\S{1,10}$")
        private String title;
        @NotEmpty
        private String content;
        @NotEmpty
        @URL
        private String coverImg;
        // 注解在字段的上方
        @State
        private String state; // 应该只允许 "已发布" 或 "草稿"
        @NotNull // 涉及数据使用NotNull,字符串使用NotEmpty
        private Integer categoryId;
        private Integer createUser;
        private LocalDateTime createTime;
        private LocalDateTime updateTime;
    }

使用 postman 测试工具测试,返回的信息中提示 state参数的值只能是已发布或者草稿

image-20250625105945763

1.3.3 文章列表接口(条件分页)

请求路径:/article

请求方式:GET

请求参数格式:queryString

请求参数说明

参数名称说明类型是否必须备注
pageNum当前页码Number
pageSize每页条数Number
categoryId文章分类IDNumber
state发布状态String已发布 | 草稿

文章列表(条件分页)的实现流程图

image-20250625111247983

首先需要新增一个 PageBean 的实体类,用来处理分页返回的结果数据,代码实现如下

package com.euansu.pojo;

import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;

// 分页返回结果对象
@Data
@NoArgsConstructor
@AllArgsConstructor
public class PageBean<T> {
    private Long total;     // 总条数
    private List<T> items;  // 当前页数据集合
}

Controller中添加文章列表的接口,代码实现如下所示,需要注意这个接口并不一定需要 categoryIdstate 两个字段

@GetMapping("/list")
public Result<PageBean<Article>> list(
    Integer pageNum,
    Integer pageSize,
    @RequestParam(required = false) Integer categoryId,
    @RequestParam(required = false) String state
){
    PageBean<Article> pb = articleService.list(pageNum,pageSize,categoryId,state);
    return Result.success(pb);
}

ArticleService 中增加条件分页列表查询的方法声明,如下所示

public interface ArticleService {
    // 新增文章
    void addArticle(Article article);

    // 条件分页列表查询
    PageBean<Article> list(Integer pageNum, Integer pageSize, Integer categoryId, String state);
}

ArticleServiceImpl实现类中编写声明的方法,这里用到了 PageHelper 插件,需要先在 pom 文件中引入这个坐标,引入完成后刷新 pom 文件

<dependency>
    <groupId>com.github.pagehelper</groupId>
    <artifactId>pagehelper-spring-boot-starter</artifactId>
    <version>1.4.6</version>
</dependency>

ArticleServiceImpl中编写使用 PageHelper插件实现的条件分页查询方法

@Override
public PageBean<Article> list(Integer pageNum, Integer pageSize, Integer categoryId, String state) {
    // 1.创建PageBean对象,用于封装查询完成的对象
    PageBean<Article> pb = new PageBean<>();
    // 2.开启分页查询,需要用到 PageHelper 插件
    PageHelper.startPage(pageNum,pageSize);
    // 3.调用Mapper
    Map<String,Object> map = ThreadLocalUtil.get();
    Integer userId = (Integer) map.get("id");
    List<Article> as = articleMapper.list(userId, categoryId, state);
    // Page中提供了方法,可以获取PageHelper分页查询后,得到的总记录条数和当前页数据
    Page<Article> p = (Page<Article>) as;
    // 把数据填充到PageBean中
    pb.setTotal(p.getTotal());
    pb.setItems(p.getResult());
    return pb;
}

ArticleMapper 中编写条件查询的方法,这里不要直接写查询方法,因为传入的参数是动态的

@Mapper
public interface ArticleMapper {
    List<Article> list(Integer userId, Integer categoryId, String state);
}

这里使用映射文件,在和 mapper 对应路径的 resources 下方编写映射文件,路径的关系如下图所示

image-20250625124834155

映射文件的内容如下

<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper
        PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "https://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.euansu.mapper.ArticleMapper">
    <!--动态SQL-->
    <select id="list" resultType="com.euansu.pojo.Article">
        select * from article
        <where>
            <if test="categoryId!=null">
                category_id=#{categoryId}
            </if>
            <if test="state!=null">
                and state=#{state}
            </if>
            and create_user=#{userId}
        </where>
    </select>

</mapper>

编写完成后,重启 SpringBoot 工程,使用 postman 接口测试工具测试,能够正常实现条件查询分页效果

image-20250625124934725

1.3.4 文章更新接口

请求路径:/article

请求方式:UPDATE

请求参数格式:application/json

请求参数说明

参数名称说明类型是否必须备注
title文章标题String
content文章内容String
categoryId文章分类IDNumber
state发布状态String已发布 | 草稿
coverImg文章封面String

文章更新接口的实现流程:

  • ArticleController 中增加文章更新的接口,调用 ArticleService 中的方法来实现。
  • ArticleService 声明文章更新的方法,在 ArticleServiceImpl中实现。
  • ArticleServiceImpl 实现文章更新的方法,调用 ArticleMapper 来实现与数据库的交互操作。
  • ArticleMapper 实现文章与数据库的交互操作。

ArticleController 中增加文章更新的接口,代码如下所示

@PutMapping
public Result updateArticle(@RequestBody @Validated Article article) {
    articleService.updateArticle(article);
    return Result.success();
}

ArticleService 声明文章更新的方法,代码如下所示

void updateArticle(Article article);

ArticleServiceImpl 实现文章更新的方法,代码如下所示

@Override
public void updateArticle(Article article) {
    article.setUpdateTime(LocalDateTime.now());
    articleMapper.update(article);
}

ArticleMapper 实现文章与数据库的交互操作,代码如下所示

@Update("UPDATE article SET title=#{title}, content=#{content}, cover_img=#{coverImg}, category_id=#{categoryId}, state=#{state}, update_time=#{updateTime}  WHERE id=#{id}")
void update(Article article);

重启 SpringBoot 项目后,使用 postman 接口测试工具进行测试,能够正常进行信息的修改。

image-20250707110107327

使用 Postman 测试工具调用接口查询文章的存储信息,能够看到存储的文章发生了变化。

image-20250707110141098

1.3.5 文章删除接口

请求路径:/article

请求方式:DELETE

请求参数格式:multipart/form-data

文章删除接口的实现流程:

  • ArticleController 中增加文章删除的接口,调用 ArticleService 中的方法来实现。
  • ArticleService 声明文章删除的方法,在 ArticleServiceImpl中实现。
  • ArticleServiceImpl 实现文章删除的方法,调用 ArticleMapper 来实现与数据库的交互操作。
  • ArticleMapper 实现文章与数据库的交互操作。

ArticleController 中增加文章删除的接口,接口的代码如下所示

@DeleteMapping
public Result delete(@RequestParam Integer id) {
    articleService.delete(id);
    return Result.success();
}

ArticleService 声明文章删除的方法,声明代码如下所示

void delete(Integer id);

ArticleServiceImpl 实现文章删除的方法,代码如下所示

@Override
public void delete(Integer id) {
    articleMapper.delete(id);
}

ArticleMapper 实现文章与数据库的交互操作,代码如下所示

@Delete("DELETE FROM article WHERE id=#{id}")
void delete(Integer id);

重启 SpringBoot 项目后,使用 postman 接口测试工具进行测试,能够正常进行文章的删除。

image-20250707111128686

使用 postman 接口测试工具,无法查询到 id2 的文章记录信息。

image-20250707111204105

1.4 其他接口

1.4.1 文件上传(本地存储)

请求路径:/upload

请求方式:POST

请求参数格式:multipart/form-data

请求参数说明

参数名称说明类型是否必须备注
file表单中文件请求参数的名称file

文件上传接口的实现思路

image-20250707113056174

新增 FileUploadController 编写文件上传接口的相关实现,代码如下:

package com.euansu.controller;

import com.euansu.pojo.Result;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

import java.io.File;
import java.io.IOException;
import java.util.UUID;

@RestController
public class FileUploadController {
    @PostMapping("/upload")
    public Result<String> upload(MultipartFile file) throws IOException {
        // 获取文件原始名称
        String originalFilename = file.getOriginalFilename();
        // 拼接存储名称
        String filename = UUID.randomUUID() + originalFilename.substring(originalFilename.lastIndexOf("."));
        // 把文件的内容存储到本地磁盘
        file.transferTo(new File("C:\\Code\\Java\\JavaStudy\\SpringBootStudy\\upload\\" + filename));
        return Result.success("文件url地址是...");
    }
}

重启 SpringBoot工程后,使用 Postman 进行接口测试,能够正常存储文件。

image-20250707114538483

检查本地磁盘中对应位置,能够查看到存储的文件,如下图所示

image-20250707114601879

1.4.2 文件上传(七牛云)

七牛云接口文档:https://developer.qiniu.com/kodo/1239/java

首先在 pom.xml 中引入七牛云的相关坐标,如下所示

<!--七牛云-->
<dependency>
    <groupId>com.qiniu</groupId>
    <artifactId>qiniu-java-sdk</artifactId>
    <version>[7.19.0, 7.19.99]</version>
    <exclusions>
        <exclusion>
            <groupId>com.squareup.okhttp3</groupId>
            <artifactId>okhttp</artifactId>
        </exclusion>
    </exclusions>
</dependency>
<dependency>
    <groupId>com.squareup.okhttp3</groupId>
    <artifactId>okhttp</artifactId>
    <version>3.14.2</version>

</dependency>
<dependency>
    <groupId>com.google.code.gson</groupId>
    <artifactId>gson</artifactId>
    <version>2.11.0</version>
</dependency>
<dependency>
    <groupId>com.qiniu</groupId>
    <artifactId>happy-dns-java</artifactId>
    <version>0.1.6</version>
</dependency>
<dependency>
    <groupId>junit</groupId>
    <artifactId>junit</artifactId>
    <version>4.12</version>
</dependency>

拷贝七牛云 https://developer.qiniu.com/kodo/1239/java#upload-file 文件上传的代码到本地进行测试,如下所示

package com.euansu;
import com.qiniu.common.QiniuException;
import com.qiniu.http.Response;
import com.qiniu.storage.UploadManager;
import com.qiniu.common.Zone;
import com.qiniu.storage.Configuration;
import com.qiniu.util.Auth;

import java.io.IOException;


public class UploadDemo {
    //设置好账号的ACCESS_KEY和SECRET_KEY
    String ACCESS_KEY = "ACCESS_KEY";
    String SECRET_KEY = "SECRET_KEYT";
    //要上传的空间
    String bucketname = "bucketname";
    //上传到七牛后保存的文件名
    String key = "f03fce72-18d1-4e69-b260-88878122ac7b.png";
    //上传文件的路径
    String FilePath = "C:\\Code\\Java\\JavaStudy\\SpringBootStudy\\upload\\f03fce72-18d1-4e69-b260-88878122ac7b.png";

    //密钥配置
    Auth auth = Auth.create(ACCESS_KEY, SECRET_KEY);

    ///////////////////////指定上传的Zone的信息//////////////////
    //第一种方式: 指定具体的要上传的zone
    //注:该具体指定的方式和以下自动识别的方式选择其一即可
    //要上传的空间(bucket)的存储区域为华东时
    // Zone z = Zone.zone0();
    //要上传的空间(bucket)的存储区域为华北时
    // Zone z = Zone.zone1();
    //要上传的空间(bucket)的存储区域为华南时
    // Zone z = Zone.zone2();

    //第二种方式: 自动识别要上传的空间(bucket)的存储区域是华东、华北、华南。
    Zone z = Zone.autoZone();
    Configuration c = new Configuration(z);

    //创建上传对象
    UploadManager uploadManager = new UploadManager(c);

    public static void main(String args[]) throws IOException {
        new UploadDemo().upload();
    }

    //简单上传,使用默认策略,只需要设置上传的空间名就可以了
    public String getUpToken() {
        return auth.uploadToken(bucketname);
    }

    public void upload() throws IOException {
        try {
            //调用put方法上传
            Response res = uploadManager.put(FilePath, key, getUpToken());
            //打印返回的信息
            System.out.println(res.bodyString());
        } catch (QiniuException e) {
            Response r = e.response;
            // 请求失败时打印的异常的信息
            System.out.println(r.toString());
            try {
                //响应的文本信息
                System.out.println(r.bodyString());
            } catch (QiniuException e1) {
                //ignore
            }
        }
    }

}

这里进行本地文件的上传,使用工具进行测试如下图所示

image-20250707163906985

在七牛云后台能够查看到上传的图片文件

image-20250707164715992

1.4.3 文件上传(七牛云程序集成)

封装七牛云的工具类为 QiniuUtil,增加文件上传的方法

package com.euansu.utils;

import com.qiniu.common.QiniuException;
import com.qiniu.common.Zone;
import com.qiniu.http.Response;
import com.qiniu.storage.Configuration;
import com.qiniu.storage.UploadManager;
import com.qiniu.util.Auth;

import java.io.IOException;
import java.io.InputStream;

public class QiniuUtil {
    //设置好账号的ACCESS_KEY和SECRET_KEY
    private static final String ACCESS_KEY = "ACCESS_KEY";
    private static final String SECRET_KEY = "SECRET_KEY";
    //要上传的空间
    private static final String BUCKET_NAME = "BUCKET_NAME";

    public static String uploadFiles(InputStream file, String fileName) {
        //密钥配置
        Auth auth = Auth.create(ACCESS_KEY, SECRET_KEY);
        // 自动识别要上传的区域
        Zone z = Zone.autoZone();
        Configuration c = new Configuration(z);
        // 创建上传对象
        UploadManager uploadManager = new UploadManager(c);
        // 获取上传的Token
        String uploadToken = auth.uploadToken(BUCKET_NAME);
        try {
            //调用put方法上传
            Response res = uploadManager.put(file.readAllBytes(), fileName, uploadToken);

            //打印返回的信息
            System.out.println(res.bodyString());
            // 构造返回的地址
            String url = "http://img.euansu.cn/" + fileName;
            return url;
        } catch (QiniuException e) {
            Response r = e.response;
            // 请求失败时打印的异常的信息
            System.out.println(r.toString());
            try {
                //响应的文本信息
                System.out.println(r.bodyString());
            } catch (QiniuException e1) {
                //ignore
            }
        } catch (IOException e) {
            throw new RuntimeException(e);
        }
        return null;

    }
}

Controller 层调用七牛云的 uploadFiles 方法,如下所示

String url = QiniuUtil.uploadFiles(file.getInputStream(),filename);

重启 SpringBoot 项目,使用 Postman 进行接口测试,显示能够正常上传图片文件。

image-20250707184137775

在浏览器中能够使用返回的地址正常访问。

image-20250707174223364