实体类的写法,可以先在数据库建表,然后使用MybatisPlus进行反向编译,需配置相应插件

右键MybatisX-Generator

springboot 启动会扫描以下位置的application.properties或者application.yml文件作为Spring boot的默认配置文件:

优先级1:项目路径下的config文件夹配置文件
优先级2:项目路径下配置文件
优先级3:资源路径下的config文件夹配置文件
优先级4:资源路径下配置文件

优先级由高到底,高优先级的配置会覆盖低优先级的配置;SpringBoot会从这四个位置全部加载主配置文件;互补配置;

静态资源调用优先级:resources>static>public

自动配置原理:SpringBoot启动时会加载大量的自动配置类,看需要的功能有没有在SpringBoot默认写好的自动配置类当中,再来看这个自动配置类中到底配置了哪些组件;(只要要用的组件存在其中,就不需要再去手动配置了,如果不存在再手动配置)给容器中自动配置类添加组件的时候,会从properties类中获取某些属性,只需要在配置文件中指定这些属性即可修改属性就是SpringBoot配置.yaml、.properties这些文件。

XXXXAutoConfiguration自动配置类:给容器添加组件,这些组件要赋值就需要绑定一个XXProperties类
XXXXProperties:里面封装配置文件中相关属性;


Thymeleaf

先导入依赖

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-thymeleaf</artifactId>
        </dependency>

只需要把html页面放在类路径下的templates下,thymeleaf就可以自动渲染,不需要配置


常用的Maven配置

<?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>
	<parent>
		<groupId>org.springframework.boot</groupId>
		<artifactId>spring-boot-starter-parent</artifactId>
		<version>2.0.6.RELEASE</version>
		<relativePath/> <!-- lookup parent from repository -->
	</parent>
	<groupId>com.ldf</groupId>
	<artifactId>demo1128</artifactId>
	<version>0.0.1-SNAPSHOT</version>
	<name>demo1128</name>
	<description>demo1128</description>
	<url/>
	<licenses>
		<license/>
	</licenses>
	<developers>
		<developer/>
	</developers>
	<scm>
		<connection/>
		<developerConnection/>
		<tag/>
		<url/>
	</scm>
	<properties>
		<java.version>8</java.version>
	</properties>
	<dependencies>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter</artifactId>
		</dependency>

		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-devtools</artifactId>
			<scope>runtime</scope>
			<optional>true</optional>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-test</artifactId>
			<scope>test</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web</artifactId>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-configuration-processor</artifactId>
			<scope>annotationProcessor</scope>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-web-services</artifactId>
		</dependency>

		<dependency>
			<groupId>mysql</groupId>
			<artifactId>mysql-connector-java</artifactId>
			<scope>runtime</scope>
		</dependency>


		<dependency>
			<groupId>com.baomidou</groupId>
			<artifactId>mybatis-plus-boot-starter</artifactId>
			<version>3.5.3.2</version>
		</dependency>
		<dependency>
			<groupId>org.projectlombok</groupId>
			<artifactId>lombok</artifactId>
		</dependency>
		<dependency>
			<groupId>com.alibaba</groupId>
			<artifactId>fastjson</artifactId>
			<version>2.0.39</version>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-thymeleaf</artifactId>
		</dependency>
		<dependency>
			<groupId>cn.hutool</groupId>
			<artifactId>hutool-all</artifactId>
			<version>5.7.20</version>
		</dependency>
		<dependency>
			<groupId>org.springframework.boot</groupId>
			<artifactId>spring-boot-starter-data-redis</artifactId>
		</dependency>
		<dependency>
			<groupId>org.junit.jupiter</groupId>
			<artifactId>junit-jupiter</artifactId>
			<version>RELEASE</version>
			<scope>test</scope>
		</dependency>
		<!-- Swagger 核心 -->
		<dependency>
			<groupId>io.springfox</groupId>
			<artifactId>springfox-swagger2</artifactId>
			<version>2.9.2</version>
		</dependency>

		<!-- Swagger UI 页面 -->
		<dependency>
			<groupId>io.springfox</groupId>
			<artifactId>springfox-swagger-ui</artifactId>
			<version>2.9.2</version>
		</dependency>
	</dependencies>

	<build>
		<plugins>
			<plugin>
				<groupId>org.springframework.boot</groupId>
				<artifactId>spring-boot-maven-plugin</artifactId>
			</plugin>
		</plugins>
	</build>

</project>

统一返回体

新建返回类CommonResult和PageResult,Controller返回常见的统一json格式(即状态码code、数据data、状态描述message)

package com.ldf.demo1128.reponse.enums;

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

@Data
@AllArgsConstructor
@NoArgsConstructor
public class CommonResult<T> {
    private Integer code;
    private String message;
    private T data;

    /**
     * 成功结果(无数据)
     */
    public static CommonResult success() {
        CommonResult result = new CommonResult();
        result.setCode(200);
        result.setMessage("成功");
        return result;
    }

    /**
     * 成功结果(带数据)
     */
    public static <T> CommonResult<T> success(T data) {
        CommonResult<T> result = new CommonResult<T>();
        result.setData(data);
        result.setCode(200);
        result.setMessage("成功");
        return result;
    }

    /**
     * 成功结果(自定义消息)
     */
    public static <T> CommonResult<T> success(T data, String message) {
        CommonResult<T> result = new CommonResult<T>();
        result.setData(data);
        result.setCode(200);
        result.setMessage(message);
        return result;
    }

    /**
     * 错误结果
     */
    public static CommonResult error(Integer code, String msg) {
        CommonResult result = new CommonResult();
        result.setCode(code);
        result.setMessage(msg);
        return result;
    }

    /**
     * 错误结果(带数据)
     */
    public static <T> CommonResult<T> error(Integer code, String msg, T data) {
        CommonResult<T> result = new CommonResult<T>();
        result.setCode(code);
        result.setMessage(msg);
        result.setData(data);
        return result;
    }

    /**
     * 系统错误
     */
    public static CommonResult systemError() {
        return error(500, "系统错误");
    }

    /**
     * 参数错误
     */
    public static CommonResult paramError() {
        return error(400, "参数错误");
    }

    /**
     * 未授权
     */
    public static CommonResult unauthorized() {
        return error(401, "未授权");
    }

    /**
     * 判断是否成功
     */
    public boolean isSuccess() {
        return code != null && code == 200;
    }
}
package com.ldf.demo1128.reponse.enums;

import lombok.AllArgsConstructor;
import lombok.Data;

import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Data

@AllArgsConstructor
public class PageResult<T> {

    private long count;
    private Integer pageNo;
    private Integer pageSize;
    private Map<String, Object> extend;
    private List<T> lists = new ArrayList<>();

    // 删除了手动编写的无参构造函数,由 @NoArgsConstructor 自动生成

    /**
     * 使用列表构造
     */
    public PageResult(List<T> lists) {
        if (lists != null) {
            this.lists = new ArrayList<>(lists);
        }
    }

    /**
     * 使用单个元素构造
     */
    public PageResult(T element) {
        if (element != null) {
            this.lists = new ArrayList<>();
            this.lists.add(element);
        }
    }

    /**
     * 使用列表和总数构造
     */
    public PageResult(List<T> lists, long count) {
        this(lists);
        this.count = count;
    }

    /**
     * 使用列表、页码和页大小构造
     */
    public PageResult(List<T> lists, Integer pageNo, Integer pageSize, long count) {
        this(lists, count);
        this.pageNo = pageNo;
        this.pageSize = pageSize;
    }

    /**
     * 添加扩展属性
     */
    public void addExtend(String key, Object value) {
        if (this.extend == null) {
            this.extend = new HashMap<>();
        }
        this.extend.put(key, value);
    }

    /**
     * 获取扩展属性
     */
    public Object getExtend(String key) {
        return this.extend != null ? this.extend.get(key) : null;
    }

    /**
     * 判断是否为空
     */
    public boolean isEmpty() {
        return lists == null || lists.isEmpty();
    }

    /**
     * 获取列表大小
     */
    public int getSize() {
        return lists != null ? lists.size() : 0;
    }

    /**
     * 计算总页数
     */
    public Integer getTotalPages() {
        if (pageSize == null || pageSize <= 0) {
            return 0;
        }
        return (int) Math.ceil((double) count / pageSize);
    }

    /**
     * 创建一个空的PageResult
     */
    public static <T> PageResult<T> empty() {
        return new PageResult<>(new ArrayList<>());
    }

    /**
     * 创建一个成功的PageResult
     */
    public static <T> PageResult<T> success(List<T> lists, long count) {
        return new PageResult<>(lists, count);
    }

    /**
     * 创建一个带分页信息的成功PageResult
     */
    public static <T> PageResult<T> success(List<T> lists, Integer pageNo, Integer pageSize, long count) {
        return new PageResult<>(lists, pageNo, pageSize, count);
    }
}

CRUD

Service层基本的接口不需要写,IService里都有,除了自定义getUserByName的功能。

package com.ldf.demo1128.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.ldf.demo1128.domain.User;

public interface UserService extends IService<User> {

    User getUserByName(String name);
}
package com.ldf.demo1128.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.ldf.demo1128.domain.User;
import com.ldf.demo1128.mapper.UserMapper;
import com.ldf.demo1128.service.UserService;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User>
        implements UserService {

    @Override
    public User getUserByName(String name) {
        return baseMapper.getUserByName(name);
    }
}

Controller层要加相应的路径、接口和注解,注意自定义的方法getUserByName()需要调用Service层的,其他的直接使用库里的方法如save()、getById()、list()、updateById()等

package com.ldf.demo1128.controller;

import com.ldf.demo1128.domain.User;
import com.ldf.demo1128.reponse.enums.CommonResult;
import com.ldf.demo1128.service.UserService;
import org.springframework.web.bind.annotation.*;

import javax.annotation.Resource;
import java.util.List;

@RestController
@RequestMapping("/user")
public class UserController {

    @Resource
    private UserService userService;

    /**
     * 原来的:根据用户名查询
     * GET /user/getUserByName?name=张三
     */
    @GetMapping("/getUserByName")
    public CommonResult<User> getUserByName(@RequestParam String name) {
        User user = userService.getUserByName(name);
        if (user == null) {
            return CommonResult.error(404, "用户不存在");
        }
        return CommonResult.success(user);
    }

    /**
     * 1. 新增用户
     * POST /user
     * Body(JSON):{"name":"张三","phone":"13800000000"}
     */
    @PostMapping
    public CommonResult<User> createUser(@RequestBody User user) {
        // 新增时,让数据库/MP自己生成id,不要自己传
        boolean ok = userService.save(user);
        if (!ok) {
            return CommonResult.error(500, "新增用户失败");
        }
        return CommonResult.success(user, "新增用户成功");
    }

    /**
     * 2. 按id查询用户
     * GET /user/1
     */
    @GetMapping("/{id}")
    public CommonResult<User> getUserById(@PathVariable Long id) {
        User user = userService.getById(id);
        if (user == null) {
            return CommonResult.error(404, "用户不存在");
        }
        return CommonResult.success(user);
    }

    /**
     * 3. 查询所有用户
     * GET /user/list
     */
    @GetMapping("/list")
    public CommonResult<List<User>> listUsers() {
        List<User> users = userService.list();
        return CommonResult.success(users);
    }

    /**
     * 4. 根据id修改用户
     * PUT /user/1
     * Body(JSON):{"name":"李四","phone":"13900000000"}
     */
    @PutMapping("/{id}")
    public CommonResult<User> updateUser(
            @PathVariable Long id,
            @RequestBody User user) {

        // 确保要更新哪条记录
        user.setId(id);
        boolean ok = userService.updateById(user);
        if (!ok) {
            return CommonResult.error(500, "更新用户失败");
        }
        return CommonResult.success(user, "更新用户成功");
    }

    /**
     * 5. 根据id删除用户
     * DELETE /user/1
     */
    @DeleteMapping("/{id}")
    public CommonResult<Void> deleteUser(@PathVariable Long id) {
        boolean ok = userService.removeById(id);
        if (!ok) {
            return CommonResult.error(500, "删除用户失败");
        }
        return CommonResult.success(null, "删除用户成功");
    }
}

在Mapper层写相应的方法getUserByName(),其他的不需要因为baseMapper库里面有

package com.ldf.demo1128.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.ldf.demo1128.domain.User;

import com.ldf.demo1128.dto.UserInfoDTO;
import org.apache.ibatis.annotations.*;

/**
* @author 李东峰
* @description 针对表【user】的数据库操作Mapper
* @createDate 2025-11-30 15:49:31
* @Entity generator.domain.User
*/
@Mapper
public interface UserMapper extends BaseMapper<User> {
    // 自定义查询:按名字查
    @Select("select * from user where name = #{name}")
    User getUserByName(@Param("name") String name);   

}

登录拦截器

1、写一个拦截器类LoginInterceptor

package com.ldf.demo1128.interceptor;

import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;

// 标记成 Spring 的 Bean,方便注入
@Component
public class LoginInterceptor implements HandlerInterceptor {

    /**
     * 1. preHandle:在 Controller 方法执行之前执行
     * 返回 true:放行
     * 返回 false:拦截,不再往后执行
     */
    @Override
    public boolean preHandle(HttpServletRequest request,
                             HttpServletResponse response,
                             Object handler) throws Exception {

        // 1)拿到请求的路径(方便调试)
        String uri = request.getRequestURI();
        System.out.println("拦截到请求:" + uri);

        // 2)这里做你的“校验逻辑”
        // 举例:我们约定前端在 header 里传一个 token
        String token = request.getHeader("token");

        // 假设 token 等于 "123456" 就算登录成功(只是示例)
        if ("123456".equals(token)) {
            // 校验通过,放行
            return true;
        }

        // 3)校验不通过,返回一个简单提示(JSON 或字符串都可以)
        response.setStatus(401); // 未授权
        response.setContentType("application/json;charset=UTF-8");
        response.getWriter().write("{\"code\":401,\"message\":\"未登录或token无效\"}");

        // 重要:返回 false,后面的 Controller 不再执行
        return false;
    }

    // 2. postHandle:Controller 执行完,但还没返回给前端的时候
    // 你暂时用不到,可以先不写(或空实现)

    // 3. afterCompletion:整个请求结束(包括视图渲染完),一般用来清理资源
    // 你也可以先不写
}

2、写一个配置类,实现 WebMvcConfigurer,在里面注册刚刚的拦截器

package com.ldf.demo1128.config;

import com.ldf.demo1128.interceptor.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) {

        registry.addInterceptor(loginInterceptor) // 使用哪个拦截器
                .addPathPatterns("/user/**")      // 要拦截哪些路径
                .excludePathPatterns(
                        "/user/login",            // 不拦登录接口
                        "/user/register",         // 不拦注册
                        "/error"                  // 一般也放行错误页
                );
    }
}

3、Apipost进行测试时,要在头部加token才能登陆成功


分页查询

注意:如果查询条件复杂且种类繁多,可采用动态sql.xml文件,之后在mapper层直接写方法就行 不再需要注解。

1、先配置分页插件:新建配置类 com.ldf.demo1128.config.MybatisPlusConfig

package com.ldf.demo1128.config;

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.mybatis.spring.annotation.MapperScan;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
@MapperScan("com.ldf.demo1128.mapper") // 扫描 mapper 接口
public class MybatisPlusConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 加入分页拦截器,指定数据库类型为 MySQL
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MYSQL));
        return interceptor;
    }
}

2、在 Controller 里写分页接口(用 Service 的 page 方法)

// ① 导入分页相关类
import com.baomidou.mybatisplus.core.metadata.IPage;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import com.ldf.demo1128.domain.User;

// ② 声明接口:GET /user/page
@GetMapping("/page")
public CommonResult<IPage<User>> pageUsers(
        // ③ 接收页码参数,默认 1
        @RequestParam(defaultValue = "1") Integer pageNum,
        // ④ 接收每页条数,默认 10
        //只访问:/user/page → 相当于 pageNum=1&pageSize=10

或者:/user/page?pageNum=3&pageSize=5
        @RequestParam(defaultValue = "10") Integer pageSize,
        // ⑤ 接收可选的 name 参数(模糊查询用)
        @RequestParam(required = false) String name
) {
    // ⑥ 构造分页参数:第几页,每页几条
    Page<User> page = new Page<>(pageNum, pageSize);

    // ⑦ 构造查询条件(可选)
    LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<>();
    if (name != null && !name.trim().isEmpty()) {
        // ⑧ 如果传了 name,就按 name 做模糊查询
        wrapper.like(User::getName, name);
    }

    // ⑨ 调用 MyBatis-Plus 默认的分页方法
    IPage<User> resultPage = userService.page(page, wrapper);

    // ⑩ 用你自己的 CommonResult 包一层返回
    return CommonResult.success(resultPage);
}

3、Apipost 测试分页

  • 方法:GET

  • URL 示例 1:
    http://localhost:8080/user/page?pageNum=1&pageSize=3
    不带 name,就是查全部用户,第 1 页 3 条。

  • URL 示例 2(带条件):
    http://localhost:8080/user/page?pageNum=1&pageSize=5&name=张
    查名字里带“张”的用户,第 1 页 5 条。


多表查询

1、新建数据库表,利用MybitsPlus进行反向编译生成新的实体类

2、新建dto类,com.ldf.demo1128.dto.UserInfoDTO接收多表结果

package com.ldf.demo1128.dto;

import lombok.Data;

@Data
public class UserInfoDTO {
    private Long id;
    private String name;
    private String phone;
    private Integer age;
    private String address;
}

3、在UserMapper中多加查询规则,由于库里没有,所以需要自定义查询规则,并导入相应的包

 // 多表查询:user + user_home
    @Select("select u.id, u.name, u.phone, h.age, h.address " +
            "from user u " +
            "left join user_home h on u.id = h.id " +
            "where u.id = #{id}")
    UserInfoDTO selectUserInfoById(@Param("id") Long id);

4、在UserService中增加getUserInfoById方法接口

UserInfoDTO getUserInfoById(Long id);

5、UserServiceImpl实现类中 加入getUserInfoById实现规则(即UserMapper的selectUserInfoById()方法)

  @Override
    public UserInfoDTO getUserInfoById(Long id) {
        return baseMapper.selectUserInfoById(id);
    }

6、在Controller增加接受请求路径及查询参数

@GetMapping("/info/{id}")
public CommonResult<UserInfoDTO> getUserInfo(@PathVariable Long id) {
    UserInfoDTO info = userService.getUserInfoById(id);
    if (info == null) {
        return CommonResult.error(404, "用户不存在或没有详情");
    }
    return CommonResult.success(info);

7、Apipost 测试:

  • 方法:GET

  • URL: http://localhost:8080/user/info/1


统一日志

使用注解 @Slf4j

log函数哪里需要放哪里

 @GetMapping("/getUserByName")
    public CommonResult<User> getUserByName(@RequestParam String name) {
        log.info("收到查询用户请求,name={}", name);
        User user = userService.getUserByName(name);
        return CommonResult.success(user);
    }

    @PostMapping("/add")
    public CommonResult<Boolean> addUser(@RequestBody User user) {
        log.info("收到新增用户请求,user={}", user);
        boolean ok = userService.save(user);
        return CommonResult.success(ok, ok ? "新增成功" : "新增失败");
    }

logger类包括的各种函数:优先级从低到高: trace()、调试用日志debug()、业务流程日志info()、warn()、error()

日志配置里如果设置了一个级别,比如 INFO,那:

  • INFO / WARN / ERROR 会打印

  • DEBUG / TRACE 会被过滤掉

debug()用于·:中间变量、查询条件、数据库返回的结果、逻辑分支(if 走了哪条)

info()用于:看系统做过哪些事(新增用户成功、用户登录成功等)

warn()用于:配置缺失但使用默认值、数据不完整但系统可以兜底、重试成功,但第一次是失败的

error()用于:表示程序出问题了,不符合预期需要排查,伴随Exception

异常error必须放参数e

@ExceptionHandler(Exception.class)
public CommonResult<?> handleException(Exception e) {
    log.error("系统异常:", e);  // 错误级别 + 打印堆栈
    return CommonResult.error(500, "系统开小差了,请稍后再试");
}

异常处理

省去写这段话

try {
    // 业务逻辑
} catch (Exception e) {
    e.printStackTrace();
    return CommonResult.error(500, "系统错误");
}

方法:

1、新建一个类 GlobalExceptionHandler全局异常处理

package com.ldf.demo1128.exception;

import com.ldf.demo1128.reponse.enums.CommonResult; // 你这里的包名按自己项目改,之前你写过 CommonResult
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

/**
 * 全局异常处理类
 */
@Slf4j
@RestControllerAdvice   // 重点注解:对所有 @RestController 生效
public class GlobalExceptionHandler {

    /**
     * 专门处理所有 Exception(兜底)
     */
    @ExceptionHandler(Exception.class)
    public CommonResult<?> handleException(Exception e) {
        // 1. 打印日志(方便你排查问题)
        log.error("系统异常:", e);

        // 2. 返回统一格式给前端
        return CommonResult.error(500, "系统开小差了,请稍后再试");
    }
}

2、可以再细化一点,如处理非法参数异常

// 举例:处理非法参数异常
@ExceptionHandler(IllegalArgumentException.class)
public CommonResult<?> handleIllegalArgument(IllegalArgumentException e) {
    log.warn("参数异常:{}", e.getMessage());
    return CommonResult.error(400, e.getMessage());
}

举例:

@PostMapping("/add")
public CommonResult<?> addUser(@RequestBody User user) {
    if (user.getName() == null) {
        throw new IllegalArgumentException("用户名不能为空");
    }
    userService.save(user);
    return CommonResult.success("新增用户成功");
}

IllegalArgumentException()方法来自自带的库RuntimeException类

常见的异常:

NullPointerException(空指针异常):接收到的参数是 null,还没判空就用了、Mapper 查数据库没查到,返回 null,你当成有值用

IllegalArgumentException(非法参数异常):参数值不符合方法的要求时主动抛,如年龄不能为负数

IllegalStateException(非法状态异常):参数本身没问题但是当前对象/系统状态不允许这么干,如用户未激活,不能下单

也可以自定义异常:

public class BusinessException extends RuntimeException {
    private Integer code;

    public BusinessException(Integer code, String message) {
        super(message);
        this.code = code;
    }

    public Integer getCode() {
        return code;
    }
}

在业务里写上BusinessException

if (user.getName() == null) {
    throw new BusinessException(400, "用户名不能为空");
}

Redis

用来缓存热点数据、计数器、分布式锁、消息队列、排行榜、会话 Session 等。

Redis(默认)是单线程执行命令,一次只干一件事,一个线程同时“看着”很多连接,轮流处理请求,效率高。

Redis 快,是因为“使用内存 + 单线程 + IO 多路复用”。

持久化策略:RDB:段时间存一次“快照”(备份)、AOF:把每条写操作追加到日志,可以重放。

1、下载解压以后,先双击redis-server.exe启动服务器

2、双击redis-cli.exe启动命令窗口即可

3、配置pom.xml

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>

4、在application.yml里配置 Redis

spring:
  redis:
    host: 127.0.0.1
    port: 6379
    # 如果你在 Redis 里设置了密码,就在这里写
    # password: yourpassword
    database: 0   # 用第 0 个数据库,默认即可

5、预备工作:新建一个Redis类

package com.ldf.demo1128.redis;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.stereotype.Component;

import java.util.concurrent.TimeUnit;

@Component
public class RedisService {

    @Autowired
    private StringRedisTemplate stringRedisTemplate;

    /**
     * 设置字符串 key-value
     */
    public void set(String key, String value) {
        stringRedisTemplate.opsForValue().set(key, value);
    }

    /**
     * 设置字符串 key-value,并带过期时间(秒)
     */
    public void set(String key, String value, long timeoutSeconds) {
        stringRedisTemplate.opsForValue().set(key, value, timeoutSeconds, TimeUnit.SECONDS);
    }

    /**
     * 根据 key 获取字符串
     */
    public String get(String key) {
        return stringRedisTemplate.opsForValue().get(key);
    }

    /**
     * 删除 key
     */
    public Boolean delete(String key) {
        return stringRedisTemplate.delete(key);
    }
}

先查 Redis,没有再查 MySQL,并把结果放到 Redis

6、在UserServiceImpl实现类中

@Override
    public User getUserByName(String name) {
        // 1. 先尝试从 Redis 拿
        String key = "user:name:" + name; // key 的命名习惯:模块:字段:值
        String cacheJson = stringRedisTemplate.opsForValue().get(key);

        if (cacheJson != null && !cacheJson.trim().isEmpty()) {
            // 命中缓存,直接反序列化返回
            System.out.println("【命中缓存】从 Redis 拿到了用户:" + cacheJson);
            return JSON.parseObject(cacheJson, User.class);
            // 将Redis里的json格式转化成User类
        }

        System.out.println("【未命中缓存】去查数据库...");

        // 2. 查数据库
        User user = baseMapper.getUserByName(name);

        // 3. 放到 Redis 里,下次就有了
        if (user != null) {
            System.out.println("【数据库结果】user = " + user);
            // 将User类转化成Redis里的json格式
            String json = JSON.toJSONString(user);
            // 设置一个过期时间,比如 5 分钟
            stringRedisTemplate.opsForValue().set(key, json, 5, java.util.concurrent.TimeUnit.MINUTES);
            System.out.println("【写入缓存】已将用户信息存入 Redis,key = " + key);
        }

        return user;
    }

stringRedisTemplate中的各常用方法:

①opsForValue():

操作字符串:

// 设置
stringRedisTemplate.opsForValue().set("key", "value");

// 设置并加过期时间(秒)
stringRedisTemplate.opsForValue().set("key", "value", 60, TimeUnit.SECONDS);

// 获取
String v = stringRedisTemplate.opsForValue().get("key");

计数(浏览量、点赞):

stringRedisTemplate.opsForValue().increment("article:1:viewCount"); // +1

删除键:

stringRedisTemplate.delete("user:name:xx");

②opsForHash()相当于 Redis 里的 Map:

// 假设 key = "user:1"
stringRedisTemplate.opsForHash().put("user:1", "name", "张三");
stringRedisTemplate.opsForHash().put("user:1", "age", "18");

// 取单个字段
Object name = stringRedisTemplate.opsForHash().get("user:1", "name");

// 一次取全部字段
Map<Object, Object> userMap = stringRedisTemplate.opsForHash().entries("user:1");

// 删除某个字段
stringRedisTemplate.opsForHash().delete("user:1", "age");

③opsForList()有序列表 / 队列:

// 从左边 push(相当于头部插入)
stringRedisTemplate.opsForList().leftPush("msg-queue", "msg1");
stringRedisTemplate.opsForList().leftPush("msg-queue", "msg2");

// 从右边 pop(相当于从队尾拿出:先进先出)
String msg = stringRedisTemplate.opsForList().rightPop("msg-queue");

// 查看范围内的列表元素
List<String> list = stringRedisTemplate.opsForList().range("msg-queue", 0, -1);

// 获取列表长度
Long size = stringRedisTemplate.opsForList().size("msg-queue");

④opsForSet()无序不重复集合:

// 加入一个集合
stringRedisTemplate.opsForSet().add("login:2025-12-08", "user1", "user2");

// 判断成员是否存在
Boolean has = stringRedisTemplate.opsForSet().isMember("login:2025-12-08", "user1");

// 获取所有成员
Set<String> users = stringRedisTemplate.opsForSet().members("login:2025-12-08");

// 删除成员
stringRedisTemplate.opsForSet().remove("login:2025-12-08", "user2");

⑤Key 相关通用方法:

// 设置过期时间(比如 60 秒)
stringRedisTemplate.expire("user:1", 60, TimeUnit.SECONDS);

// 删除 key
stringRedisTemplate.delete("user:1");

// 判断 key 是否存在
Boolean exist = stringRedisTemplate.hasKey("user:1");

// 匹配一些 key(开发环境可以用,生产慎用)
Set<String> keys = stringRedisTemplate.keys("user:*");

Redis注意事项:

1、写缓存、读缓存时用的 key一定要稳定一致 : "user:name:" + name

2、序列化和反序列化(Redis中存的是Json格式,而返回给Controller时是class格式):写入时用 JSON.toJSONString(user)读出时一定要 JSON.parseObject(cacheJson, User.class)

3、过期时间(TTL)要合理:太短缓存刚写进去就没了,太长数据不更新,可能和数据库信息不一致。


Redis列表缓存

类似单次用户查询,把类型换成list

1、先在Service层新增

List<User> listUsersWithCache();

2、在 UserServiceImpl 里实现列表缓存,其实就是换一下类型,基本流程一样的

// 列表缓存用的 key
private static final String USER_LIST_KEY = "user:list:all";
@Override
    public List<User> listUsersWithCache() {
        // 1. 先从 Redis 里拿
        String cacheJson = stringRedisTemplate.opsForValue().get(USER_LIST_KEY);

        if (cacheJson != null && !cacheJson.trim().isEmpty()) {
            System.out.println("【命中缓存-列表】从 Redis 拿到了用户列表");
            // 注意:这里是列表,用 parseArray
            return JSON.parseArray(cacheJson, User.class);
        }

        System.out.println("【未命中缓存-列表】去查数据库...");

        // 2. 查数据库(MyBatis-Plus 自带:select * from user)
        List<User> list = this.list();

        // 3. 如果查到了,就写入 Redis
        if (list != null && !list.isEmpty()) {
            String json = JSON.toJSONString(list);
            stringRedisTemplate.opsForValue().set(USER_LIST_KEY, json, 5, TimeUnit.MINUTES);
            System.out.println("【写入缓存-列表】已将用户列表存入 Redis,key = " + USER_LIST_KEY);
        }

        return list;
    }

3、由于增删改这三种方法会改变用户表,查询用户表时会发生脏数据的情况,因此需要新增:增删改用户列表并缓存,还是在ServiceImpl层

 @Override
    public boolean save(User user) {
        boolean ok = super.save(user);   // 调用 MyBatis-Plus 自带的 save()
        if (ok) {
            // 删掉列表缓存,下次查询会重新查库再放缓存
            stringRedisTemplate.delete(USER_LIST_KEY);
        }
        return ok;
    }

    @Override
    public boolean updateById(User user) {
        boolean ok = super.updateById(user);
        if (ok) {
            stringRedisTemplate.delete(USER_LIST_KEY);
        }
        return ok;
    }

    @Override
    public boolean removeById(java.io.Serializable id) {
        boolean ok = super.removeById(id);
        if (ok) {
            stringRedisTemplate.delete(USER_LIST_KEY);
        }
        return ok;
    }

4、在Controller层新加查询用户列表带缓存的查询路径

import java.util.List;
 //查询用户列表(带缓存)
    @GetMapping("/list")
    public CommonResult<List<User>> listUsers() {
        List<User> users = userService.listUsersWithCache();
        return CommonResult.success(users);
    }

5、使用Apipost进行测试: GET  /user/list


缓存穿透

请求的是数据库里本来就不存在的数据,没有存进Redis缓存,导致每次都重复查缓存再查数据库

解决办法:

1、参数校验:比如 id < 0、name 为空,直接在接口层拦截,不往下传

2、给Redis缓存空值:查数据库发现无用户,也往 Redis 里放一份,设置一个比较短的过期时间

3、布隆过滤器:在 Redis 或其它地方维护一个大集合,用位图判断“这个 id 是否有可能存在”,如  果判定“绝对不可能存在”,直接返回,不查库


缓存击穿

Redis里某个常用的热门key 过期瞬间,Redis里没了,突然大量的请求查Redis发现没有,一起去查数据库。

解决办法:

1、给热点 key 加互斥锁:当发现缓存过期了,只有一个线程能拿到“重建缓存”的锁,其他线程要么等一会再查缓存,要么直接失败/降级。

2、逻辑过期 + 后台异步刷新:key 本身不删,而是内部带一个“逻辑过期时间”,过期后,先返回旧数据给用户,然后后台异步去更新 Redis的key

3、热点数据设置为永不过期/超长过期 + 定时更新:适合一些配置类数据、首页内容


缓存雪崩

大量 key 在短时间内集体失效(如设置相同的过期时间),或者缓存系统整体不可用,压力突然全部砸到数据库。

解决办法:

1、过期时间加随机数(TTL 加随机抖动) :不要所有 key 都 5 分钟,改成:5 分钟 + [0,5] 分钟随机,让大量 key 不至于同时过期

2、限流 + 降级:Redis 崩了 / DB 顶不住,就对接口做限流,或者直接返回“系统繁忙”

@Slf4j
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User>
        implements UserService {

    @Resource
    private StringRedisTemplate stringRedisTemplate;

    private static final String USER_CACHE_PREFIX = "user:name:";
    private static final String USER_NULL_FLAG = "NULL"; // 空值标记

    // 其他方法略...

    @Override
    public User getUserByName(String name) {
        // ===== 0. 参数校验(防止无效请求,顺便防点穿透) =====
        if (name == null || name.trim().isEmpty()) {
            return null;
        }

        String key = USER_CACHE_PREFIX + name;

        // ===== 1. 先查 Redis(包括空值) =====
        String cacheJson = stringRedisTemplate.opsForValue().get(key);
        if (cacheJson != null) {
            if (USER_NULL_FLAG.equals(cacheJson)) {
                System.out.println("【命中缓存-空值】用户不存在:" + name);
                return null;
            }
            System.out.println("【命中缓存】从 Redis 拿到了用户:" + cacheJson);
            return JSON.parseObject(cacheJson, User.class);
        }

        System.out.println("【未命中缓存】准备查询数据库...");

        // ===== 2. 简单防击穿:对同一个 name 做同步锁 =====
        // 说明:intern() 让相同字符串使用同一个对象做锁
        synchronized ((USER_CACHE_PREFIX + name).intern()) {

            // ----- 再查一遍缓存,防止别的线程已经写好了 -----
            cacheJson = stringRedisTemplate.opsForValue().get(key);
            if (cacheJson != null) {
                if (USER_NULL_FLAG.equals(cacheJson)) {
                    System.out.println("【命中缓存-空值(二次检查)】用户不存在:" + name);
                    return null;
                }
                System.out.println("【命中缓存(二次检查)】从 Redis 拿到了用户:" + cacheJson);
                return JSON.parseObject(cacheJson, User.class);
            }

            // ===== 3. 真正去查数据库 =====
            User user = baseMapper.getUserByName(name);

            // ===== 4. 计算过期时间:5 分钟 + 0~4 分钟随机(防雪崩) =====
            int baseMinutes = 5;
            int randomExtra = ThreadLocalRandom.current().nextInt(5); // 0,1,2,3,4
            long ttl = baseMinutes + randomExtra;

            // ===== 5. 写缓存:分存在和不存在两种情况 =====
            if (user == null) {
                // 写空值,防穿透
                stringRedisTemplate.opsForValue()
                        .set(key, USER_NULL_FLAG, ttl, TimeUnit.MINUTES);
                System.out.println("【写入空值缓存】用户不存在,key=" + key + ",ttl=" + ttl + " 分钟");
                return null;
            } else {
                String json = JSON.toJSONString(user);
                stringRedisTemplate.opsForValue()
                        .set(key, json, ttl, TimeUnit.MINUTES);
                System.out.println("【写入缓存】user=" + json + ",ttl=" + ttl + " 分钟");
                return user;
            }
        }
    }
}

Redis持久化

① RDB快照(可能丢快照到宕机之间的数据,文件小)

生成 dump.rdb 文件

② AOF日志(几乎不丢数据,但文件大)

将写操作写入 appendonly.aof 文件,Redis 重启时读取 aof重新执行一遍命令

操作:

1、找到 Redis 配置文件 redis.windows.conf

2、开启 RDB快照

生成文件dir ./data/dbfilename dump.rdb

3、开启 AOF日志

appendonly yes
appendfilename "appendonly.aof"
appendfsync everysec

必须重启 Redis redis-server redis.conf

4、用 ApiPost 验证 Redis 持久化

访问接口 GET /user/getByName?name=张三,关掉 Redis再启动Redis,再查数据,若命中缓存则持久化成功
 


分布式锁

1、配置依赖

<!-- Redis 分布式锁(推荐) -->
<dependency>
    <groupId>org.redisson</groupId>
    <artifactId>redisson-spring-boot-starter</artifactId>
    <version>3.23.5</version>
</dependency>

2、新建RedisLockService类

package com.ldf.demo1128.lock;

import org.redisson.api.RLock;
import org.redisson.api.RedissonClient;
import org.springframework.stereotype.Component;

import javax.annotation.Resource;
import java.util.concurrent.TimeUnit;

@Component
public class RedisLockService {

    @Resource
    private RedissonClient redissonClient;

    /**
     * 尝试加锁
     */
    public RLock lock(String lockKey) {
        // 根据 key 从 Redis 拿一把“分布式锁”
        RLock lock = redissonClient.getLock(lockKey);

        // 加锁:最多等 1 秒,10 秒后自动释放(防死锁)
        lock.lock(10, TimeUnit.SECONDS);

        return lock;
    }

    /**
     * 解锁
     */
    public void unlock(RLock lock) {
        //  只允许当前线程解锁
        if (lock != null && lock.isHeldByCurrentThread()) {
            lock.unlock();
        }
    }
}

3、在UserServiceImpl中改进save()

@Override
    public boolean save(User user) {
        //  定义锁的 key(业务级,确保不同用户分开锁,给每个用户单独生成锁的 key)
        String lockKey = "user:add:" + user.getName();
        // 获取锁,如果别的线程已经拿走了,会等待锁释放
        RLock lock = redisLockService.lock(lockKey);
        try {
            //  保存用户到数据库
            boolean success = saveUserToDatabase(user);
            //  如果保存成功,发送消息、邮件和异步日志
            if (success) {
                String message = "用户 " + user.getName() + " 创建成功";
                messageProducer.sendMessage(message);      // 发送 RabbitMQ 消息
                sendEmailAsync(user.getEmail(), "欢迎您注册!");  // 异步发送邮件
                logUserInfoAsync(user);                     // 异步记录日志
            }
            return success;
        } finally {
            //  一定要解锁(防死锁)
            redisLockService.unlock(lock);
        }
    }

lockKey即user:add:张三 user的add操作名为张三的锁,单独生成锁的 key唯一标识

第二线程拿锁时,Redisson内部执行以下操作(源码方便理解)

while (true) {
    // 尝试拿锁
    boolean success = redis.setIfAbsent(lockKey, threadId, timeout);

    if (success) {
        // 成功拿到锁,退出循环
        break;
    }

    // 拿不到锁,睡一小会儿,再重试
    Thread.sleep(50); // 50ms 后再尝试
}

Logo

Agent 垂直技术社区,欢迎活跃、内容共建。

更多推荐