Spring Boot 进阶实战:整合 MyBatis、Redis、JWT,搭一个更像真实项目的后端服务

前言

如果说 Spring Boot 基础篇解决的是"项目怎么起起来",那么进阶篇更关心的是:

  • 数据访问怎么做得更可控
  • 登录态怎么设计得更适合前后端分离
  • 缓存怎么落地
  • 代码结构怎么撑得住真实业务

这篇文章用一个偏典型的用户认证 + 商品查询场景,把下面四块内容串起来:

  • Spring Boot
  • MyBatis
  • Redis
  • JWT

目标是给出一套更像真实业务系统的实现骨架,而不是只给你几个零散知识点。


1. 场景设计

假设我们现在要做一个简单的后端服务,支持这些能力:

  1. 用户登录
  2. 登录成功后签发 JWT
  3. JWT 中携带用户基础身份信息
  4. 用户会话信息放 Redis
  5. 查询商品列表时,先查 Redis 缓存,缓存没有再查数据库
  6. 所有接口统一返回格式

一个合理的技术职责拆分大致是:

  • Spring Boot:项目骨架、Web、依赖管理
  • MyBatis:数据库访问
  • Redis:缓存、会话辅助
  • JWT:无状态认证令牌

2. 依赖清单

先看 Maven 依赖。

xml 复制代码
<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 http://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>3.3.2</version>
        <relativePath/>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>advanced-demo</artifactId>
    <version>1.0.0</version>

    <properties>
        <java.version>17</java.version>
        <mybatis-spring-boot.version>3.0.3</mybatis-spring-boot.version>
        <jjwt.version>0.12.6</jjwt.version>
    </properties>

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

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

        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>${mybatis-spring-boot.version}</version>
        </dependency>

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

        <dependency>
            <groupId>com.mysql</groupId>
            <artifactId>mysql-connector-j</artifactId>
            <scope>runtime</scope>
        </dependency>

        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-api</artifactId>
            <version>${jjwt.version}</version>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-impl</artifactId>
            <version>${jjwt.version}</version>
            <scope>runtime</scope>
        </dependency>
        <dependency>
            <groupId>io.jsonwebtoken</groupId>
            <artifactId>jjwt-jackson</artifactId>
            <version>${jjwt.version}</version>
            <scope>runtime</scope>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
        </dependency>
    </dependencies>
</project>

3. 项目结构建议

建议按下面这种方式组织:

text 复制代码
src/main/java/com/example/advanced
├─ AdvancedApplication.java
├─ common
│  ├─ exception
│  ├─ response
│  └─ util
├─ config
│  ├─ RedisConfig.java
│  └─ WebMvcConfig.java
├─ security
│  ├─ JwtAuthenticationInterceptor.java
│  ├─ JwtProperties.java
│  └─ JwtTokenProvider.java
├─ user
│  ├─ controller
│  ├─ service
│  ├─ mapper
│  ├─ entity
│  └─ dto
└─ product
   ├─ controller
   ├─ service
   ├─ mapper
   ├─ entity
   └─ dto

这么分的好处是:

  • 认证相关能力集中在 security
  • 业务模块按领域分组
  • MyBatis Mapper 不和 Controller/Service 混在一起
  • 通用基础设施集中在 commonconfig

4. 配置文件

yaml 复制代码
server:
  port: 8080

spring:
  application:
    name: advanced-demo
  datasource:
    url: jdbc:mysql://localhost:3306/advanced_demo?useSSL=false&serverTimezone=UTC
    username: root
    password: 123456
    driver-class-name: com.mysql.cj.jdbc.Driver
  data:
    redis:
      host: localhost
      port: 6379
      database: 0
      timeout: 3000ms

mybatis:
  mapper-locations: classpath:/mapper/**/*.xml
  type-aliases-package: com.example.advanced.user.entity,com.example.advanced.product.entity
  configuration:
    map-underscore-to-camel-case: true

jwt:
  secret: 01234567890123456789012345678901
  expire-seconds: 7200
  issuer: advanced-demo

logging:
  level:
    root: info
    com.example.advanced: debug

说明:

  • map-underscore-to-camel-case=true 可以让数据库下划线字段自动映射成 Java 驼峰属性
  • jwt.secret 至少应足够长,生产环境不要写死在配置文件里
  • Redis 这里既可以做缓存,也可以做会话辅助存储

5. 统一返回结构和异常处理

5.1 统一返回体

java 复制代码
package com.example.advanced.common.response;

public class ApiResponse<T> {

    private boolean success;
    private String code;
    private String message;
    private T data;

    public static <T> ApiResponse<T> success(T data) {
        ApiResponse<T> response = new ApiResponse<>();
        response.success = true;
        response.code = "OK";
        response.message = "success";
        response.data = data;
        return response;
    }

    public static <T> ApiResponse<T> fail(String code, String message) {
        ApiResponse<T> response = new ApiResponse<>();
        response.success = false;
        response.code = code;
        response.message = message;
        return response;
    }

    public boolean isSuccess() {
        return success;
    }

    public String getCode() {
        return code;
    }

    public String getMessage() {
        return message;
    }

    public T getData() {
        return data;
    }
}

5.2 业务异常

java 复制代码
package com.example.advanced.common.exception;

public class BizException extends RuntimeException {

    private final String code;

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

    public String getCode() {
        return code;
    }
}

5.3 全局异常处理

java 复制代码
package com.example.advanced.common.exception;

import com.example.advanced.common.response.ApiResponse;
import java.util.stream.Collectors;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@RestControllerAdvice
public class GlobalExceptionHandler {

    @ExceptionHandler(BizException.class)
    public ApiResponse<Void> handleBizException(BizException ex) {
        return ApiResponse.fail(ex.getCode(), ex.getMessage());
    }

    @ExceptionHandler(MethodArgumentNotValidException.class)
    public ApiResponse<Void> handleValidationException(MethodArgumentNotValidException ex) {
        String message = ex.getBindingResult()
            .getFieldErrors()
            .stream()
            .map(error -> error.getField() + ": " + error.getDefaultMessage())
            .collect(Collectors.joining("; "));
        return ApiResponse.fail("PARAM_INVALID", message);
    }

    @ExceptionHandler(Exception.class)
    public ApiResponse<Void> handleException(Exception ex) {
        return ApiResponse.fail("INTERNAL_ERROR", ex.getMessage());
    }
}

6. JWT:签发、解析、校验

6.1 配置类

java 复制代码
package com.example.advanced.security;

import org.springframework.boot.context.properties.ConfigurationProperties;

@ConfigurationProperties(prefix = "jwt")
public class JwtProperties {

    private String secret;
    private Long expireSeconds;
    private String issuer;

    public String getSecret() {
        return secret;
    }

    public void setSecret(String secret) {
        this.secret = secret;
    }

    public Long getExpireSeconds() {
        return expireSeconds;
    }

    public void setExpireSeconds(Long expireSeconds) {
        this.expireSeconds = expireSeconds;
    }

    public String getIssuer() {
        return issuer;
    }

    public void setIssuer(String issuer) {
        this.issuer = issuer;
    }
}

6.2 Token Provider

java 复制代码
package com.example.advanced.security;

import io.jsonwebtoken.Claims;
import io.jsonwebtoken.Jwts;
import io.jsonwebtoken.security.Keys;
import java.nio.charset.StandardCharsets;
import java.security.Key;
import java.time.Instant;
import java.util.Date;
import java.util.Map;
import org.springframework.stereotype.Component;

@Component
public class JwtTokenProvider {

    private final JwtProperties jwtProperties;

    public JwtTokenProvider(JwtProperties jwtProperties) {
        this.jwtProperties = jwtProperties;
    }

    public String generateToken(Long userId, String username) {
        Instant now = Instant.now();
        Instant expireAt = now.plusSeconds(jwtProperties.getExpireSeconds());

        return Jwts.builder()
            .issuer(jwtProperties.getIssuer())
            .subject(String.valueOf(userId))
            .claims(Map.of("username", username))
            .issuedAt(Date.from(now))
            .expiration(Date.from(expireAt))
            .signWith(getSigningKey())
            .compact();
    }

    public Claims parseToken(String token) {
        return Jwts.parser()
            .verifyWith((javax.crypto.SecretKey) getSigningKey())
            .build()
            .parseSignedClaims(token)
            .getPayload();
    }

    public boolean validateToken(String token) {
        try {
            parseToken(token);
            return true;
        } catch (Exception ex) {
            return false;
        }
    }

    private Key getSigningKey() {
        return Keys.hmacShaKeyFor(jwtProperties.getSecret().getBytes(StandardCharsets.UTF_8));
    }
}

JWT 做的事情很明确:

  • 登录成功后签发 Token
  • 后续请求携带 Token
  • 服务端解析 Token,拿到用户身份信息

但要记住一件事:

JWT 适合表达身份,不适合承载太多会频繁变化的业务状态。


7. Redis:会话辅助与缓存

7.1 RedisTemplate 配置

java 复制代码
package com.example.advanced.config;

import com.fasterxml.jackson.databind.ObjectMapper;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.connection.RedisConnectionFactory;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.data.redis.serializer.GenericJackson2JsonRedisSerializer;
import org.springframework.data.redis.serializer.StringRedisSerializer;

@Configuration
public class RedisConfig {

    @Bean
    public RedisTemplate<String, Object> redisTemplate(
            RedisConnectionFactory connectionFactory,
            ObjectMapper objectMapper) {
        RedisTemplate<String, Object> template = new RedisTemplate<>();
        template.setConnectionFactory(connectionFactory);

        GenericJackson2JsonRedisSerializer serializer =
            new GenericJackson2JsonRedisSerializer(objectMapper);

        template.setKeySerializer(new StringRedisSerializer());
        template.setValueSerializer(serializer);
        template.setHashKeySerializer(new StringRedisSerializer());
        template.setHashValueSerializer(serializer);
        template.afterPropertiesSet();
        return template;
    }
}

7.2 用户会话写入 Redis

登录成功后,可以把会话信息写进去:

java 复制代码
package com.example.advanced.user.service;

import java.time.Duration;
import java.util.Map;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

@Service
public class UserSessionService {

    private final RedisTemplate<String, Object> redisTemplate;

    public UserSessionService(RedisTemplate<String, Object> redisTemplate) {
        this.redisTemplate = redisTemplate;
    }

    public void saveLoginSession(Long userId, String username, String token, long expireSeconds) {
        String key = "login:token:" + token;
        Map<String, Object> session = Map.of(
            "userId", userId,
            "username", username
        );
        redisTemplate.opsForValue().set(key, session, Duration.ofSeconds(expireSeconds));
    }

    public Object getSessionByToken(String token) {
        return redisTemplate.opsForValue().get("login:token:" + token);
    }

    public void removeSession(String token) {
        redisTemplate.delete("login:token:" + token);
    }
}

这样做有几个好处:

  • 支持手动踢下线
  • 支持注销立即失效
  • 可以在 Redis 层附加更多会话控制能力

8. MyBatis:数据库访问层

8.1 用户实体

java 复制代码
package com.example.advanced.user.entity;

public class UserEntity {

    private Long id;
    private String username;
    private String passwordHash;
    private Integer status;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPasswordHash() {
        return passwordHash;
    }

    public void setPasswordHash(String passwordHash) {
        this.passwordHash = passwordHash;
    }

    public Integer getStatus() {
        return status;
    }

    public void setStatus(Integer status) {
        this.status = status;
    }
}

8.2 Mapper 接口

java 复制代码
package com.example.advanced.user.mapper;

import com.example.advanced.user.entity.UserEntity;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Param;

@Mapper
public interface UserMapper {

    UserEntity findByUsername(@Param("username") String username);
}

8.3 Mapper XML

resources/mapper/user/UserMapper.xml

xml 复制代码
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.advanced.user.mapper.UserMapper">

    <select id="findByUsername" resultType="com.example.advanced.user.entity.UserEntity">
        SELECT
            id,
            username,
            password_hash,
            status
        FROM t_user
        WHERE username = #{username}
        LIMIT 1
    </select>

</mapper>

MyBatis 的优势在于:

  • SQL 明确可控
  • 复杂查询更自然
  • 对性能调优更友好

它特别适合:

  • SQL 复杂度高
  • 需要细粒度优化
  • 查询结果结构灵活多变

9. 登录流程实现

9.1 登录请求 DTO

java 复制代码
package com.example.advanced.user.dto;

import jakarta.validation.constraints.NotBlank;

public class LoginRequest {

    @NotBlank(message = "username 不能为空")
    private String username;

    @NotBlank(message = "password 不能为空")
    private String password;

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }
}

9.2 登录响应 VO

java 复制代码
package com.example.advanced.user.dto;

public class LoginResponse {

    private String token;
    private Long userId;
    private String username;

    public String getToken() {
        return token;
    }

    public void setToken(String token) {
        this.token = token;
    }

    public Long getUserId() {
        return userId;
    }

    public void setUserId(Long userId) {
        this.userId = userId;
    }

    public String getUsername() {
        return username;
    }

    public void setUsername(String username) {
        this.username = username;
    }
}

9.3 AuthService

java 复制代码
package com.example.advanced.user.service;

import com.example.advanced.common.exception.BizException;
import com.example.advanced.security.JwtProperties;
import com.example.advanced.security.JwtTokenProvider;
import com.example.advanced.user.dto.LoginRequest;
import com.example.advanced.user.dto.LoginResponse;
import com.example.advanced.user.entity.UserEntity;
import com.example.advanced.user.mapper.UserMapper;
import org.springframework.stereotype.Service;

@Service
public class AuthService {

    private final UserMapper userMapper;
    private final JwtTokenProvider jwtTokenProvider;
    private final UserSessionService userSessionService;
    private final JwtProperties jwtProperties;

    public AuthService(
            UserMapper userMapper,
            JwtTokenProvider jwtTokenProvider,
            UserSessionService userSessionService,
            JwtProperties jwtProperties) {
        this.userMapper = userMapper;
        this.jwtTokenProvider = jwtTokenProvider;
        this.userSessionService = userSessionService;
        this.jwtProperties = jwtProperties;
    }

    public LoginResponse login(LoginRequest request) {
        UserEntity user = userMapper.findByUsername(request.getUsername());
        if (user == null) {
            throw new BizException("USER_NOT_FOUND", "用户不存在");
        }

        if (!mockPasswordMatch(request.getPassword(), user.getPasswordHash())) {
            throw new BizException("PASSWORD_INVALID", "用户名或密码错误");
        }

        if (user.getStatus() == null || user.getStatus() != 1) {
            throw new BizException("USER_DISABLED", "用户已禁用");
        }

        String token = jwtTokenProvider.generateToken(user.getId(), user.getUsername());
        userSessionService.saveLoginSession(
            user.getId(),
            user.getUsername(),
            token,
            jwtProperties.getExpireSeconds()
        );

        LoginResponse response = new LoginResponse();
        response.setToken(token);
        response.setUserId(user.getId());
        response.setUsername(user.getUsername());
        return response;
    }

    private boolean mockPasswordMatch(String rawPassword, String passwordHash) {
        return rawPassword != null && rawPassword.equals(passwordHash);
    }
}

生产环境里,密码当然不能明文比较,应该用 BCryptPasswordEncoder 之类的安全方案。


10. 登录接口

java 复制代码
package com.example.advanced.user.controller;

import com.example.advanced.common.response.ApiResponse;
import com.example.advanced.user.dto.LoginRequest;
import com.example.advanced.user.dto.LoginResponse;
import com.example.advanced.user.service.AuthService;
import jakarta.validation.Valid;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/auth")
public class AuthController {

    private final AuthService authService;

    public AuthController(AuthService authService) {
        this.authService = authService;
    }

    @PostMapping("/login")
    public ApiResponse<LoginResponse> login(@Valid @RequestBody LoginRequest request) {
        return ApiResponse.success(authService.login(request));
    }
}

请求示例:

http 复制代码
POST /api/auth/login
Content-Type: application/json

{
  "username": "alice",
  "password": "123456"
}

返回示例:

json 复制代码
{
  "success": true,
  "code": "OK",
  "message": "success",
  "data": {
    "token": "eyJhbGciOiJIUzI1NiJ9...",
    "userId": 1,
    "username": "alice"
  }
}

11. JWT 拦截器:保护需要登录的接口

11.1 拦截器实现

java 复制代码
package com.example.advanced.security;

import com.example.advanced.common.exception.BizException;
import jakarta.servlet.http.HttpServletRequest;
import jakarta.servlet.http.HttpServletResponse;
import org.springframework.stereotype.Component;
import org.springframework.web.servlet.HandlerInterceptor;

@Component
public class JwtAuthenticationInterceptor implements HandlerInterceptor {

    private final JwtTokenProvider jwtTokenProvider;

    public JwtAuthenticationInterceptor(JwtTokenProvider jwtTokenProvider) {
        this.jwtTokenProvider = jwtTokenProvider;
    }

    @Override
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
        String authHeader = request.getHeader("Authorization");
        if (authHeader == null || !authHeader.startsWith("Bearer ")) {
            throw new BizException("UNAUTHORIZED", "缺少有效 Authorization 头");
        }

        String token = authHeader.substring(7);
        if (!jwtTokenProvider.validateToken(token)) {
            throw new BizException("TOKEN_INVALID", "Token 非法或已过期");
        }

        return true;
    }
}

11.2 注册拦截器

java 复制代码
package com.example.advanced.config;

import com.example.advanced.security.JwtAuthenticationInterceptor;
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 WebMvcConfig implements WebMvcConfigurer {

    private final JwtAuthenticationInterceptor jwtAuthenticationInterceptor;

    public WebMvcConfig(JwtAuthenticationInterceptor jwtAuthenticationInterceptor) {
        this.jwtAuthenticationInterceptor = jwtAuthenticationInterceptor;
    }

    @Override
    public void addInterceptors(InterceptorRegistry registry) {
        registry.addInterceptor(jwtAuthenticationInterceptor)
            .addPathPatterns("/api/**")
            .excludePathPatterns("/api/auth/login");
    }
}

这样登录接口放行,其他接口默认需要带 JWT。


12. 商品查询:先查 Redis,查不到再查 MyBatis

12.1 商品实体

java 复制代码
package com.example.advanced.product.entity;

import java.math.BigDecimal;

public class ProductEntity {

    private Long id;
    private String productCode;
    private String productName;
    private BigDecimal price;

    public Long getId() {
        return id;
    }

    public void setId(Long id) {
        this.id = id;
    }

    public String getProductCode() {
        return productCode;
    }

    public void setProductCode(String productCode) {
        this.productCode = productCode;
    }

    public String getProductName() {
        return productName;
    }

    public void setProductName(String productName) {
        this.productName = productName;
    }

    public BigDecimal getPrice() {
        return price;
    }

    public void setPrice(BigDecimal price) {
        this.price = price;
    }
}

12.2 ProductMapper

java 复制代码
package com.example.advanced.product.mapper;

import com.example.advanced.product.entity.ProductEntity;
import java.util.List;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface ProductMapper {

    List<ProductEntity> findAll();
}

12.3 ProductMapper.xml

xml 复制代码
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN"
        "http://mybatis.org/dtd/mybatis-3-mapper.dtd">
<mapper namespace="com.example.advanced.product.mapper.ProductMapper">

    <select id="findAll" resultType="com.example.advanced.product.entity.ProductEntity">
        SELECT
            id,
            product_code,
            product_name,
            price
        FROM t_product
        ORDER BY id DESC
    </select>

</mapper>

12.4 ProductService:带缓存

java 复制代码
package com.example.advanced.product.service;

import com.example.advanced.product.entity.ProductEntity;
import com.example.advanced.product.mapper.ProductMapper;
import java.time.Duration;
import java.util.List;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.stereotype.Service;

@Service
public class ProductService {

    private static final String PRODUCT_LIST_CACHE_KEY = "product:list:all";

    private final ProductMapper productMapper;
    private final RedisTemplate<String, Object> redisTemplate;

    public ProductService(ProductMapper productMapper, RedisTemplate<String, Object> redisTemplate) {
        this.productMapper = productMapper;
        this.redisTemplate = redisTemplate;
    }

    @SuppressWarnings("unchecked")
    public List<ProductEntity> findAllProducts() {
        Object cached = redisTemplate.opsForValue().get(PRODUCT_LIST_CACHE_KEY);
        if (cached != null) {
            return (List<ProductEntity>) cached;
        }

        List<ProductEntity> products = productMapper.findAll();
        redisTemplate.opsForValue().set(PRODUCT_LIST_CACHE_KEY, products, Duration.ofMinutes(10));
        return products;
    }

    public void clearProductCache() {
        redisTemplate.delete(PRODUCT_LIST_CACHE_KEY);
    }
}

这里是最基础的 Cache Aside 模式:

  1. 先读缓存
  2. 没命中再查数据库
  3. 查到结果后回写缓存

这是业务系统里非常高频的一种缓存方式。


13. 商品接口

java 复制代码
package com.example.advanced.product.controller;

import com.example.advanced.common.response.ApiResponse;
import com.example.advanced.product.entity.ProductEntity;
import com.example.advanced.product.service.ProductService;
import java.util.List;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/api/products")
public class ProductController {

    private final ProductService productService;

    public ProductController(ProductService productService) {
        this.productService = productService;
    }

    @GetMapping
    public ApiResponse<List<ProductEntity>> list() {
        return ApiResponse.success(productService.findAllProducts());
    }
}

调用这个接口时,请求头要带上:

text 复制代码
Authorization: Bearer <token>

14. 一次完整请求会怎么流转

#mermaid-svg-GaYHe0oMO0IE7oFd{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;fill:#333;}@keyframes edge-animation-frame{from{stroke-dashoffset:0;}}@keyframes dash{to{stroke-dashoffset:0;}}#mermaid-svg-GaYHe0oMO0IE7oFd .edge-animation-slow{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 50s linear infinite;stroke-linecap:round;}#mermaid-svg-GaYHe0oMO0IE7oFd .edge-animation-fast{stroke-dasharray:9,5!important;stroke-dashoffset:900;animation:dash 20s linear infinite;stroke-linecap:round;}#mermaid-svg-GaYHe0oMO0IE7oFd .error-icon{fill:#552222;}#mermaid-svg-GaYHe0oMO0IE7oFd .error-text{fill:#552222;stroke:#552222;}#mermaid-svg-GaYHe0oMO0IE7oFd .edge-thickness-normal{stroke-width:1px;}#mermaid-svg-GaYHe0oMO0IE7oFd .edge-thickness-thick{stroke-width:3.5px;}#mermaid-svg-GaYHe0oMO0IE7oFd .edge-pattern-solid{stroke-dasharray:0;}#mermaid-svg-GaYHe0oMO0IE7oFd .edge-thickness-invisible{stroke-width:0;fill:none;}#mermaid-svg-GaYHe0oMO0IE7oFd .edge-pattern-dashed{stroke-dasharray:3;}#mermaid-svg-GaYHe0oMO0IE7oFd .edge-pattern-dotted{stroke-dasharray:2;}#mermaid-svg-GaYHe0oMO0IE7oFd .marker{fill:#333333;stroke:#333333;}#mermaid-svg-GaYHe0oMO0IE7oFd .marker.cross{stroke:#333333;}#mermaid-svg-GaYHe0oMO0IE7oFd svg{font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:16px;}#mermaid-svg-GaYHe0oMO0IE7oFd p{margin:0;}#mermaid-svg-GaYHe0oMO0IE7oFd .label{font-family:"trebuchet ms",verdana,arial,sans-serif;color:#333;}#mermaid-svg-GaYHe0oMO0IE7oFd .cluster-label text{fill:#333;}#mermaid-svg-GaYHe0oMO0IE7oFd .cluster-label span{color:#333;}#mermaid-svg-GaYHe0oMO0IE7oFd .cluster-label span p{background-color:transparent;}#mermaid-svg-GaYHe0oMO0IE7oFd .label text,#mermaid-svg-GaYHe0oMO0IE7oFd span{fill:#333;color:#333;}#mermaid-svg-GaYHe0oMO0IE7oFd .node rect,#mermaid-svg-GaYHe0oMO0IE7oFd .node circle,#mermaid-svg-GaYHe0oMO0IE7oFd .node ellipse,#mermaid-svg-GaYHe0oMO0IE7oFd .node polygon,#mermaid-svg-GaYHe0oMO0IE7oFd .node path{fill:#ECECFF;stroke:#9370DB;stroke-width:1px;}#mermaid-svg-GaYHe0oMO0IE7oFd .rough-node .label text,#mermaid-svg-GaYHe0oMO0IE7oFd .node .label text,#mermaid-svg-GaYHe0oMO0IE7oFd .image-shape .label,#mermaid-svg-GaYHe0oMO0IE7oFd .icon-shape .label{text-anchor:middle;}#mermaid-svg-GaYHe0oMO0IE7oFd .node .katex path{fill:#000;stroke:#000;stroke-width:1px;}#mermaid-svg-GaYHe0oMO0IE7oFd .rough-node .label,#mermaid-svg-GaYHe0oMO0IE7oFd .node .label,#mermaid-svg-GaYHe0oMO0IE7oFd .image-shape .label,#mermaid-svg-GaYHe0oMO0IE7oFd .icon-shape .label{text-align:center;}#mermaid-svg-GaYHe0oMO0IE7oFd .node.clickable{cursor:pointer;}#mermaid-svg-GaYHe0oMO0IE7oFd .root .anchor path{fill:#333333!important;stroke-width:0;stroke:#333333;}#mermaid-svg-GaYHe0oMO0IE7oFd .arrowheadPath{fill:#333333;}#mermaid-svg-GaYHe0oMO0IE7oFd .edgePath .path{stroke:#333333;stroke-width:2.0px;}#mermaid-svg-GaYHe0oMO0IE7oFd .flowchart-link{stroke:#333333;fill:none;}#mermaid-svg-GaYHe0oMO0IE7oFd .edgeLabel{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-GaYHe0oMO0IE7oFd .edgeLabel p{background-color:rgba(232,232,232, 0.8);}#mermaid-svg-GaYHe0oMO0IE7oFd .edgeLabel rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-GaYHe0oMO0IE7oFd .labelBkg{background-color:rgba(232, 232, 232, 0.5);}#mermaid-svg-GaYHe0oMO0IE7oFd .cluster rect{fill:#ffffde;stroke:#aaaa33;stroke-width:1px;}#mermaid-svg-GaYHe0oMO0IE7oFd .cluster text{fill:#333;}#mermaid-svg-GaYHe0oMO0IE7oFd .cluster span{color:#333;}#mermaid-svg-GaYHe0oMO0IE7oFd div.mermaidTooltip{position:absolute;text-align:center;max-width:200px;padding:2px;font-family:"trebuchet ms",verdana,arial,sans-serif;font-size:12px;background:hsl(80, 100%, 96.2745098039%);border:1px solid #aaaa33;border-radius:2px;pointer-events:none;z-index:100;}#mermaid-svg-GaYHe0oMO0IE7oFd .flowchartTitleText{text-anchor:middle;font-size:18px;fill:#333;}#mermaid-svg-GaYHe0oMO0IE7oFd rect.text{fill:none;stroke-width:0;}#mermaid-svg-GaYHe0oMO0IE7oFd .icon-shape,#mermaid-svg-GaYHe0oMO0IE7oFd .image-shape{background-color:rgba(232,232,232, 0.8);text-align:center;}#mermaid-svg-GaYHe0oMO0IE7oFd .icon-shape p,#mermaid-svg-GaYHe0oMO0IE7oFd .image-shape p{background-color:rgba(232,232,232, 0.8);padding:2px;}#mermaid-svg-GaYHe0oMO0IE7oFd .icon-shape .label rect,#mermaid-svg-GaYHe0oMO0IE7oFd .image-shape .label rect{opacity:0.5;background-color:rgba(232,232,232, 0.8);fill:rgba(232,232,232, 0.8);}#mermaid-svg-GaYHe0oMO0IE7oFd .label-icon{display:inline-block;height:1em;overflow:visible;vertical-align:-0.125em;}#mermaid-svg-GaYHe0oMO0IE7oFd .node .label-icon path{fill:currentColor;stroke:revert;stroke-width:revert;}#mermaid-svg-GaYHe0oMO0IE7oFd :root{--mermaid-font-family:"trebuchet ms",verdana,arial,sans-serif;} miss
Client Login Request
AuthController
AuthService
UserMapper
MySQL
JwtTokenProvider
Redis Session
JWT Token
Client
Protected API Request
JwtAuthenticationInterceptor
ProductController
ProductService
Redis Cache
ProductMapper

这个链路里每个组件各司其职:

  • Controller 处理 HTTP
  • Service 处理业务逻辑
  • MyBatis 负责 SQL
  • JWT 负责身份令牌
  • Redis 负责缓存和会话辅助

这就是一个很典型的前后端分离后端结构。


15. 生产环境里要注意的几个关键点

1. JWT Secret 不要写死在仓库里

开发环境可以临时写配置,生产环境应该走:

  • 环境变量
  • 配置中心
  • 密钥平台

2. 密码必须加密存储

示例里为了突出主线逻辑,使用了简化比对。真实项目必须使用:

  • BCrypt
  • Argon2
  • PBKDF2

3. Redis 缓存要考虑一致性

商品数据更新后,应主动清缓存,或者使用更稳妥的缓存策略。

4. 不要把太多业务信息塞进 JWT

JWT 最适合放:

  • userId
  • username
  • role

不适合放:

  • 频繁变化的用户状态
  • 大量业务对象
  • 敏感明文信息

5. 登录态失效不能只依赖 JWT 自身过期

如果只依赖过期时间,那就很难做"立即下线"。

所以更实用的方式通常是:

  • JWT 负责身份声明
  • Redis 负责会话有效性控制

16. 为什么这个组合很常见

Spring Boot + MyBatis + Redis + JWT 这组技术经常一起出现,不是偶然。

因为它们分别解决了后端最常见的几类问题:

  • Spring Boot:让项目快速启动并具备工程基础设施
  • MyBatis:让 SQL 可控
  • Redis:让缓存和高频数据访问更高效
  • JWT:让认证适配前后端分离场景

它们不是一套"高级炫技组合",而是非常务实的一套工程组合。


17. 总结

如果只学 Spring Boot 基础,你能写接口。

如果把 MyBatis、Redis、JWT 也整合起来,你才开始接近一个真实后端服务的基本形态。

这篇文章最值得带走的几个点是:

  • MyBatis 适合复杂 SQL 和可控数据访问
  • Redis 适合做缓存和会话辅助
  • JWT 适合前后端分离身份认证
  • Spring Boot 负责把这些能力组织成一套项目骨架

一个真正能上线的后端系统,往往不是靠某一个技术点取胜,而是靠它们之间的边界设计是否清晰。

如果你下一步还想继续往真实项目靠近,那么自然会继续进入这些主题:

  • Spring Security 接管权限体系
  • Refresh Token 设计
  • Redis 分布式锁
  • 多级缓存
  • 接口幂等
  • 审计日志
  • Docker 和 Kubernetes 部署

但不管后面再叠多少能力,今天这套组合已经足够构成很多业务系统的核心骨架了。

相关推荐
llz_1122 小时前
web-第六次课后作业
前端·spring boot·后端
柏舟飞流2 小时前
Spring Boot + Spring Security + RBAC:从登录鉴权到权限模型设计
java·spring boot·spring
南部余额2 小时前
Canal解决MySQL与Redis数据一致性问题
数据库·redis·mysql·canal·数据·数据同步
2601_961963382 小时前
Spring Boot集成电子签章的7个典型问题与解决方案:从入门到生产级实践
大数据·人工智能·spring boot·python·区块链·智能合约
星轨zb2 小时前
[Corner项目实战]Spring Boot + LangChain4j Tool Calling实战:让AI自动选择推荐策略
人工智能·spring boot·后端·langchain4j
逻极3 小时前
Spring Boot 微服务开发提速:我们如何将接口响应时间降低60%
java·spring boot·微服务·性能优化·自动配置
摇滚侠3 小时前
SSM 框架实战教程 SpringBoot 自动配置 176-179
java·spring boot·后端
典学长编程3 小时前
Redis分布式缓存超详细教学(微服务版)!
redis·微服务·持久化·主从复制·redis哨兵集群
坚持是一种态度3 小时前
Spring AI Demo - 多模型智能聊天应用
人工智能·spring boot