Spring Boot 2.7 入门到精通笔记(苍穹外卖实战版)
一、Spring Boot 概述
1.1 什么是 Spring Boot
Spring Boot 是 Spring 框架的“脚手架”,核心设计理念是 约定优于配置。
与传统 Spring 的区别:
| 对比项 | 传统 Spring | Spring Boot |
|---|---|---|
| 配置方式 | 繁琐的 XML 配置文件 | 注解 + 自动配置 |
| 依赖管理 | 手动管理版本、处理冲突 | 起步依赖(Starter)自动引入 |
| 部署方式 | 打包成 WAR 部署到 Tomcat | 内嵌 Tomcat,直接 java -jar |
| 开发效率 | 需要大量样板代码 | 只需关心业务逻辑 |
1.2 Spring Boot 核心特性
- 自动配置:根据类路径中的依赖自动配置 Bean
- 起步依赖:通过 Starter 机制简化依赖管理
- 内嵌容器:默认集成 Tomcat/Jetty/Undertow
- 生产就绪:Actuator 提供监控、健康检查等能力
1.3 Spring Boot 2.7 新特性(重点)
Spring Boot 2.7 是一个重要版本,2.5 已停止维护[reference:0]。
自动配置变更:
- 自动配置注册文件从
META-INF/spring.factories迁移到META-INF/spring/org.springframework.boot.autoconfigure.AutoConfiguration.imports - 新增
@AutoConfiguration注解,用于标识自动配置类[reference:1] - 支持 after/before 属性进行配置排序
其他新特性:
- 支持 GraphQL(
spring-boot-starter-graphql) - 支持 Podman 容器引擎
- Web Server SSL 增强[reference:2]
1.4 苍穹外卖项目概述
苍穹外卖是一个企业级 O2O 外卖平台,分为管理端和用户端(微信小程序):
| 角色 | 功能 |
|---|---|
| 管理端 | 员工管理、菜品/套餐管理、订单管理、数据统计 |
| 用户端 | 登录授权、浏览点餐、购物车结算、订单追踪 |
技术栈:Spring Boot 2.7 + Spring MVC + MyBatis-Plus + MySQL + Redis + OSS(阿里云)+ AOP + Swagger(Knife4j)+ WebSocket[reference:3]
二、快速开始
2.1 项目结构(苍穹外卖)
苍穹外卖采用 Maven 多模块结构,各模块职责明确,便于维护和扩展:
sky-take-out(父工程)
├── pom.xml # 统一依赖版本管理
├── sky-common/ # 公共模块:工具类、常量、异常类
│ ├── constant/ # 常量定义(如JwtClaimsConstant)
│ ├── context/ # ThreadLocal上下文
│ ├── exception/ # 自定义异常
│ ├── properties/ # 配置属性类
│ └── utils/ # 工具类(JWT、阿里OSS、微信支付等)
├── sky-pojo/ # 实体模块:Entity、DTO、VO
│ ├── entity/ # 数据库实体类(与表一一对应)
│ ├── dto/ # 数据传输对象(层间传递)
│ └── vo/ # 视图对象(返回给前端)
└── sky-server/ # 后端服务模块(核心业务代码)
├── config/ # 配置类(Redis、Swagger、WebMvc等)
├── controller/ # 控制器层
├── service/ # 业务逻辑层
├── mapper/ # 数据访问层
├── interceptor/ # 拦截器(如JWT认证拦截器)
├── aspect/ # AOP切面(公共字段填充)
└── SkyApplication.java # 启动类POJO 分类说明:
- Entity:实体类,与数据库表一一对应
- DTO(Data Transfer Object):数据传输对象,用于层间传递数据
- VO(View Object):视图对象,专门返回给前端展示[reference:4]
2.2 父工程 POM 配置(版本管理)
<!-- sky-take-out/pom.xml -->
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.7.0</version>
<relativePath/>
</parent>
<groupId>com.sky</groupId>
<artifactId>sky-take-out</artifactId>
<version>1.0-SNAPSHOT</version>
<packaging>pom</packaging>
<properties>
<java.version>1.8</java.version>
<mybatis-plus.version>3.5.2</mybatis-plus.version>
</properties>
<dependencies>
<!-- 统一管理的依赖在这里声明版本 -->
</dependencies>2.3 子模块依赖配置
<!-- sky-server/pom.xml -->
<dependencies>
<!-- Spring Boot Web Starter -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<!-- MyBatis-Plus Starter -->
<dependency>
<groupId>com.baomidou</groupId>
<artifactId>mybatis-plus-boot-starter</artifactId>
<version>3.5.2</version>
</dependency>
<!-- MySQL 驱动 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<scope>runtime</scope>
</dependency>
<!-- Redis -->
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-redis</artifactId>
</dependency>
<!-- 引用公共模块 -->
<dependency>
<groupId>com.sky</groupId>
<artifactId>sky-common</artifactId>
<version>1.0-SNAPSHOT</version>
</dependency>
</dependencies>2.4 启动类
package com.sky;
import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.transaction.annotation.EnableTransactionManagement;
@Slf4j
@SpringBootApplication
@EnableTransactionManagement // 开启事务管理
public class SkyApplication {
public static void main(String[] args) {
SpringApplication.run(SkyApplication.class, args);
log.info("苍穹外卖后端服务启动成功!");
}
}@SpringBootApplication 复合注解解析:
| 注解 | 作用 |
|---|---|
@SpringBootConfiguration | 标识为配置类(@Configuration 的派生) |
@ComponentScan | 开启组件扫描,扫描启动类所在包及子包 |
@EnableAutoConfiguration | 开启自动配置(核心) |
自动配置原理(5 步必背):
@SpringBootApplication→@EnableAutoConfiguration@Import(AutoConfigurationImportSelector.class)导入选择器- 加载
META-INF/spring.factories(或 2.7 新路径AutoConfiguration.imports) - 读取自动配置类的全限定类名
- 通过
@ConditionalOnXxx条件注解判断是否加载
三、配置文件
3.1 application.yml(苍穹外卖示例)
server:
port: 8080
spring:
datasource:
url: jdbc:mysql://localhost:3306/sky_take_out?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8
username: root
password: 123456
driver-class-name: com.mysql.cj.jdbc.Driver
redis:
host: localhost
port: 6379
database: 0
main:
allow-circular-references: true # 允许循环依赖
mybatis-plus:
configuration:
log-impl: org.apache.ibatis.logging.stdout.StdOutImpl # 打印 SQL 日志
global-config:
db-config:
id-type: auto # 主键自增
mapper-locations: classpath:mapper/*.xml
sky:
jwt:
admin-secret-key: "itcast"
admin-ttl: 7200000
admin-token-name: "token"3.2 多环境配置(必背)
创建多个配置文件,通过 spring.profiles.active 激活:
| 文件 | 环境 |
|---|---|
application.yml | 默认配置(公共) |
application-dev.yml | 开发环境 |
application-prod.yml | 生产环境 |
# application.yml(主配置)
spring:
profiles:
active: dev # 激活开发环境
# application-dev.yml
server:
port: 8081
spring:
config:
activate:
on-profile: dev
# application-prod.yml
server:
port: 80
spring:
config:
activate:
on-profile: prod激活方式:
# 命令行激活
java -jar sky-server.jar --spring.profiles.active=prod
# IDEA VM options
-Dspring.profiles.active=prod四、分层架构(苍穹外卖实战)
4.1 分层架构图解(外卖公司比喻)
| 层级 | 角色 | 苍穹外卖示例 |
|---|---|---|
| Controller | 客服经理(接电话) | 接收 HTTP 请求,返回 JSON 响应 |
| Service | 业务主管(管流程) | 实现业务逻辑,调用 Mapper |
| Mapper | 仓库管理员(管库存) | 执行 SQL,操作数据库 |
| Entity | 货物清单 | 与数据库表一一对应的实体 |
4.2 Entity 层(实体类)
package com.sky.entity;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;
import java.io.Serializable;
import java.time.LocalDateTime;
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class Employee implements Serializable {
private static final long serialVersionUID = 1L;
private Long id;
private String username;
private String name;
private String password;
private String phone;
private String sex;
private String idNumber;
private Integer status;
private LocalDateTime createTime;
private LocalDateTime updateTime;
private Long createUser;
private Long updateUser;
}4.3 Mapper 层(数据访问层)
package com.sky.mapper;
import com.sky.entity.Employee;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
@Mapper
public interface EmployeeMapper {
@Select("select * from employee where username = #{username}")
Employee getByUsername(String username);
// 分页查询员工
Page<Employee> pageQuery(EmployeePageQueryDTO pageQueryDTO);
// 更新员工信息
void update(Employee employee);
}MyBatis-Plus 增强版(推荐) :
// 继承 BaseMapper,自动拥有 CRUD 方法
@Mapper
public interface DishMapper extends BaseMapper<Dish> {
// 无需写任何基础 CRUD,BaseMapper 已提供:
// insert(), deleteById(), updateById(), selectById(), selectList() 等
}
// Service 层还可以继承 IService
@Service
public class DishService extends ServiceImpl<DishMapper, Dish> {
// 自动拥有 saveBatch(), updateBatchById() 等批量方法
}4.4 Service 层(业务逻辑层)
package com.sky.service.impl;
import com.sky.context.BaseContext;
import com.sky.dto.EmployeeDTO;
import com.sky.entity.Employee;
import com.sky.mapper.EmployeeMapper;
import com.sky.service.EmployeeService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.util.DigestUtils;
@Slf4j
@Service
public class EmployeeServiceImpl implements EmployeeService {
@Autowired
private EmployeeMapper employeeMapper;
@Override
@Transactional // 开启事务
public void save(EmployeeDTO employeeDTO) {
Employee employee = new Employee();
// 拷贝属性(DTO → Entity)
BeanUtils.copyProperties(employeeDTO, employee);
// 设置默认密码(MD5 加密)
employee.setPassword(DigestUtils.md5DigestAsHex("123456".getBytes()));
employee.setStatus(1);
employee.setCreateTime(LocalDateTime.now());
employee.setUpdateTime(LocalDateTime.now());
// 从 ThreadLocal 获取当前操作用户 ID
employee.setCreateUser(BaseContext.getCurrentId());
employee.setUpdateUser(BaseContext.getCurrentId());
employeeMapper.insert(employee);
}
}4.5 Controller 层(控制层)
package com.sky.controller.admin;
import com.sky.dto.EmployeeDTO;
import com.sky.dto.EmployeeLoginDTO;
import com.sky.dto.EmployeePageQueryDTO;
import com.sky.result.PageResult;
import com.sky.result.Result;
import com.sky.service.EmployeeService;
import com.sky.vo.EmployeeLoginVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/admin/employee")
@Api(tags = "员工管理接口")
@Slf4j
public class EmployeeController {
@Autowired
private EmployeeService employeeService;
@PostMapping("/login")
@ApiOperation("员工登录")
public Result<EmployeeLoginVO> login(@RequestBody EmployeeLoginDTO loginDTO) {
log.info("员工登录:{}", loginDTO.getUsername());
EmployeeLoginVO employeeLoginVO = employeeService.login(loginDTO);
return Result.success(employeeLoginVO);
}
@PostMapping
@ApiOperation("新增员工")
public Result save(@RequestBody EmployeeDTO employeeDTO) {
log.info("新增员工:{}", employeeDTO);
employeeService.save(employeeDTO);
return Result.success();
}
@GetMapping("/page")
@ApiOperation("员工分页查询")
public Result<PageResult> page(EmployeePageQueryDTO pageQueryDTO) {
log.info("员工分页查询:{}", pageQueryDTO);
PageResult pageResult = employeeService.pageQuery(pageQueryDTO);
return Result.success(pageResult);
}
}4.6 分层调用链路总结
HTTP 请求
↓
Controller(接收参数,调用 Service)
↓
Service(业务逻辑,调用 Mapper)
↓
Mapper(执行 SQL,操作数据库)
↓
Entity(数据载体)
↓
返回结果 → VO → JSON 响应⚠️ 常见错误:
- ❌ Controller 直接调用 Mapper(越级)
- ❌ Mapper 层写业务判断逻辑
- ❌ Service 层写 SQL 拼接
五、核心注解速查表
5.1 组件注解
| 注解 | 用途 | 层级 |
|---|---|---|
@Component | 通用组件,交给 Spring 管理 | 任意 |
@Controller | Web 控制器(配合 @ResponseBody 返回 JSON) | 表现层 |
@RestController | @Controller + @ResponseBody 的合并 | 表现层 |
@Service | 业务逻辑层组件 | 业务层 |
@Repository | 数据访问层组件,会转换持久层异常 | 数据层 |
@Repository 的特殊功能:将数据库操作抛出的异常自动转换为 Spring 统一的数据访问异常体系[reference:5]。
5.2 请求映射注解
| 注解 | 作用 |
|---|---|
@RequestMapping | 通用请求映射,可指定 method |
@GetMapping | 处理 GET 请求 |
@PostMapping | 处理 POST 请求 |
@PutMapping | 处理 PUT 请求 |
@DeleteMapping | 处理 DELETE 请求 |
5.3 参数绑定注解
| 注解 | 作用 | 示例 |
|---|---|---|
@PathVariable | 获取 URL 路径参数 | /user/{id} → @PathVariable Long id |
@RequestParam | 获取 URL 查询参数 | ?name=张三 → @RequestParam String name |
@RequestBody | 获取请求体(JSON → 对象) | @RequestBody EmployeeDTO dto |
@RequestHeader | 获取请求头 | @RequestHeader("token") String token |
5.4 依赖注入注解
| 注解 | 作用 |
|---|---|
@Autowired | 按类型自动注入(Spring 原生) |
@Resource | 按名称自动注入(JSR-250) |
@Qualifier | 配合 @Autowired,指定 Bean 名称 |
@Value | 注入配置文件中的值(如 ${server.port}) |
@ConfigurationProperties | 批量注入配置属性 |
5.5 配置与条件注解
| 注解 | 作用 |
|---|---|
@Configuration | 标识配置类 |
@Bean | 在配置类中声明 Bean |
@ConditionalOnClass | 类路径存在指定类时才生效 |
@ConditionalOnMissingBean | 容器中没有指定 Bean 时才生效 |
@ConditionalOnProperty | 配置文件存在指定属性时才生效 |
六、全局异常处理
6.1 苍穹外卖全局异常处理器
package com.sky.handler;
import com.sky.constant.MessageConstant;
import com.sky.exception.BaseException;
import com.sky.result.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {
// 捕获业务异常
@ExceptionHandler(BaseException.class)
public Result exceptionHandler(BaseException ex) {
log.error("业务异常:{}", ex.getMessage());
return Result.error(ex.getMessage());
}
// 捕获 SQL 异常(如用户名重复)
@ExceptionHandler(DuplicateKeyException.class)
public Result handleDuplicateKeyException(DuplicateKeyException ex) {
log.error("SQL 异常:{}", ex.getMessage());
return Result.error(MessageConstant.ALREADY_EXISTS);
}
// 兜底异常处理
@ExceptionHandler(Exception.class)
public Result exceptionHandler(Exception ex) {
log.error("系统异常:{}", ex.getMessage());
return Result.error(MessageConstant.UNKNOWN_ERROR);
}
}6.2 统一响应对象
package com.sky.result;
import lombok.Data;
import java.io.Serializable;
@Data
public class Result<T> implements Serializable {
private Integer code; // 1 成功,0 失败
private String msg;
private T data;
public static <T> Result<T> success() {
Result<T> result = new Result<>();
result.code = 1;
return result;
}
public static <T> Result<T> success(T data) {
Result<T> result = new Result<>();
result.code = 1;
result.data = data;
return result;
}
public static <T> Result<T> error(String msg) {
Result<T> result = new Result<>();
result.code = 0;
result.msg = msg;
return result;
}
}七、JWT 认证与拦截器
7.1 JWT 工具类
package com.sky.utils;
import io.jsonwebtoken.*;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
import java.util.Date;
import java.util.Map;
@Data
@Component
@ConfigurationProperties(prefix = "sky.jwt")
public class JwtUtil {
private String adminSecretKey;
private Long adminTtl;
private String adminTokenName;
// 生成 JWT 令牌
public String createJWT(Map<String, Object> claims, Long ttlMillis) {
long nowMillis = System.currentTimeMillis();
Date now = new Date(nowMillis);
JwtBuilder builder = Jwts.builder()
.setClaims(claims)
.setIssuedAt(now)
.signWith(SignatureAlgorithm.HS256, adminSecretKey);
if (ttlMillis > 0) {
builder.setExpiration(new Date(nowMillis + ttlMillis));
}
return builder.compact();
}
// 解析 JWT 令牌
public Claims parseJWT(String token) {
return Jwts.parser()
.setSigningKey(adminSecretKey)
.parseClaimsJws(token)
.getBody();
}
}7.2 JWT 认证拦截器
package com.sky.interceptor;
import com.sky.constant.JwtClaimsConstant;
import com.sky.context.BaseContext;
import com.sky.properties.JwtProperties;
import com.sky.utils.JwtUtil;
import io.jsonwebtoken.Claims;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.method.HandlerMethod;
import org.springframework.web.servlet.HandlerInterceptor;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
@Component
@Slf4j
public class JwtTokenAdminInterceptor implements HandlerInterceptor {
@Autowired
private JwtUtil jwtUtil;
@Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
// 判断当前拦截的是否为 Controller 方法
if (!(handler instanceof HandlerMethod)) {
return true;
}
// 从请求头中获取令牌
String token = request.getHeader(jwtUtil.getAdminTokenName());
// 解析令牌
try {
Claims claims = jwtUtil.parseJWT(token);
Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());
// 将员工 ID 存入 ThreadLocal(供后续业务使用)
BaseContext.setCurrentId(empId);
log.info("当前员工 ID:{}", empId);
return true;
} catch (Exception ex) {
log.error("JWT 解析失败:{}", ex.getMessage());
response.setStatus(401);
return false;
}
}
@Override
public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) {
// 请求结束后清除 ThreadLocal,防止内存泄漏
BaseContext.removeCurrentId();
}
}7.3 ThreadLocal 工具类(苍穹外卖)
package com.sky.context;
public class BaseContext {
private static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
public static void setCurrentId(Long id) {
threadLocal.set(id);
}
public static Long getCurrentId() {
return threadLocal.get();
}
public static void removeCurrentId() {
threadLocal.remove();
}
}ThreadLocal 原理:每个线程拥有自己的变量副本,线程隔离。用完必须 remove(),否则在线程池环境下会导致内存泄漏[reference:6]。
7.4 注册拦截器
package com.sky.config;
import com.sky.interceptor.JwtTokenAdminInterceptor;
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.WebMvcConfigurationSupport;
@Configuration
public class WebMvcConfig extends WebMvcConfigurationSupport {
@Autowired
private JwtTokenAdminInterceptor jwtTokenAdminInterceptor;
@Override
protected void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(jwtTokenAdminInterceptor)
.addPathPatterns("/admin/**") // 拦截所有 /admin 开头的请求
.excludePathPatterns("/admin/employee/login") // 排除登录接口
.excludePathPatterns("/admin/employee/logout");
}
}八、AOP 切面编程(公共字段自动填充)
8.1 自定义注解
package com.sky.annotation;
import com.sky.enumeration.OperationType;
import java.lang.annotation.*;
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {
OperationType value(); // UPDATE 或 INSERT
}8.2 切面类实现
package com.sky.aspect;
import com.sky.annotation.AutoFill;
import com.sky.constant.AutoFillConstant;
import com.sky.context.BaseContext;
import com.sky.enumeration.OperationType;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import java.lang.reflect.Method;
import java.time.LocalDateTime;
@Aspect
@Component
@Slf4j
public class AutoFillAspect {
// 切点:标注了 @AutoFill 注解的方法
@Pointcut("@annotation(com.sky.annotation.AutoFill)")
public void autoFillPointCut() {}
@Before("autoFillPointCut()")
public void autoFill(JoinPoint joinPoint) {
// 获取当前被拦截的方法上的注解
MethodSignature signature = (MethodSignature) joinPoint.getSignature();
AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class);
OperationType operationType = autoFill.value();
// 获取方法的参数(实体对象)
Object[] args = joinPoint.getArgs();
if (args == null || args.length == 0) return;
Object entity = args[0];
// 准备要赋的值
LocalDateTime now = LocalDateTime.now();
Long currentId = BaseContext.getCurrentId();
// 根据操作类型设置不同字段
if (operationType == OperationType.INSERT) {
// 插入操作:设置 createTime, updateTime, createUser, updateUser
try {
Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class);
Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
Method setCreateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class);
Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);
setCreateTime.invoke(entity, now);
setUpdateTime.invoke(entity, now);
setCreateUser.invoke(entity, currentId);
setUpdateUser.invoke(entity, currentId);
} catch (Exception e) {
log.error("公共字段填充失败:{}", e.getMessage());
}
} else if (operationType == OperationType.UPDATE) {
// 更新操作:设置 updateTime, updateUser
try {
Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);
setUpdateTime.invoke(entity, now);
setUpdateUser.invoke(entity, currentId);
} catch (Exception e) {
log.error("公共字段填充失败:{}", e.getMessage());
}
}
}
}8.3 使用示例
@Mapper
public interface EmployeeMapper {
@AutoFill(OperationType.INSERT)
void insert(Employee employee);
@AutoFill(OperationType.UPDATE)
void update(Employee employee);
}九、缓存与 Redis
9.1 Spring Cache 配置
package com.sky.config;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.data.redis.cache.RedisCacheConfiguration;
import org.springframework.data.redis.serializer.RedisSerializationContext;
import org.springframework.data.redis.serializer.StringRedisSerializer;
import java.time.Duration;
@Configuration
@EnableCaching
public class RedisConfig {
@Bean
public RedisCacheConfiguration cacheConfiguration() {
return RedisCacheConfiguration.defaultCacheConfig()
.entryTtl(Duration.ofMinutes(30)) // 缓存过期时间 30 分钟
.disableCachingNullValues() // 不缓存 null 值
.serializeKeysWith(RedisSerializationContext.SerializationPair
.fromSerializer(new StringRedisSerializer()));
}
}9.2 缓存注解使用
@Service
@Slf4j
public class DishServiceImpl implements DishService {
@Autowired
private DishMapper dishMapper;
// 查询时缓存:key = "dish:categoryId:" + categoryId
@Cacheable(value = "dish", key = "'dish:categoryId:' + #categoryId")
public List<Dish> getByCategoryId(Long categoryId) {
return dishMapper.getByCategoryId(categoryId);
}
// 更新时清空缓存
@CacheEvict(value = "dish", key = "'dish:categoryId:' + #dishDTO.categoryId")
public void updateWithFlavor(DishDTO dishDTO) {
// 更新业务逻辑
}
// 批量清空缓存(key 模糊匹配)
@CacheEvict(value = "dish", allEntries = true)
public void deleteBatch(List<Long> ids) {
// 删除业务逻辑
}
}9.3 常用 Redis 操作(StringRedisTemplate)
@Service
public class ShopStatusService {
@Autowired
private StringRedisTemplate redisTemplate;
private static final String SHOP_STATUS_KEY = "SHOP_STATUS";
public void setStatus(Integer status) {
redisTemplate.opsForValue().set(SHOP_STATUS_KEY, String.valueOf(status));
}
public Integer getStatus() {
String status = redisTemplate.opsForValue().get(SHOP_STATUS_KEY);
return status == null ? 0 : Integer.parseInt(status);
}
}十、Spring Task 定时任务
10.1 开启定时任务
@SpringBootApplication
@EnableScheduling // 开启定时任务
public class SkyApplication {
public static void main(String[] args) {
SpringApplication.run(SkyApplication.class, args);
}
}10.2 订单状态定时处理
package com.sky.task;
import com.sky.service.OrderService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
@Component
@Slf4j
public class OrderTask {
@Autowired
private OrderService orderService;
// 每天凌晨 1 点处理超时未支付的订单
@Scheduled(cron = "0 0 1 * * ?")
public void processTimeoutOrder() {
log.info("定时任务:处理超时未支付订单");
orderService.processTimeoutOrders();
}
// 每分钟执行一次,处理配送中的订单(送达后自动完成)
@Scheduled(cron = "0 * * * * ?")
public void processDeliveryOrder() {
log.info("定时任务:处理配送中订单");
orderService.processDeliveryOrders();
}
}10.3 Cron 表达式速查
| 字段 | 允许值 | 特殊字符 |
|---|---|---|
| 秒 | 0-59 | , - * / |
| 分 | 0-59 | , - * / |
| 时 | 0-23 | , - * / |
| 日 | 1-31 | , - * ? / L W |
| 月 | 1-12 | , - * / |
| 周 | 0-7(0 或 7 都代表周日) | , - * ? / L # |
| 年(可选) | 空或 1970-2099 | , - * / |
常用表达式:
0 0 1 * * ?→ 每天凌晨 1 点0 */5 * * * ?→ 每 5 分钟一次0 0 12 * * MON-FRI→ 工作日中午 12 点
十一、WebSocket 实时通信
11.1 WebSocket 配置
package com.sky.config;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;
@Configuration
public class WebSocketConfig {
@Bean
public ServerEndpointExporter serverEndpointExporter() {
return new ServerEndpointExporter();
}
}11.2 WebSocket 服务端
package com.sky.websocket;
import lombok.extern.slf4j.Slf4j;
import org.springframework.stereotype.Component;
import javax.websocket.*;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.util.concurrent.ConcurrentHashMap;
@Component
@ServerEndpoint("/ws/{sid}")
@Slf4j
public class WebSocketServer {
// 存储所有在线会话(线程安全)
private static ConcurrentHashMap<String, Session> sessionMap = new ConcurrentHashMap<>();
@OnOpen
public void onOpen(Session session, @PathParam("sid") String sid) {
sessionMap.put(sid, session);
log.info("WebSocket 连接建立:{}", sid);
}
@OnClose
public void onClose(@PathParam("sid") String sid) {
sessionMap.remove(sid);
log.info("WebSocket 连接关闭:{}", sid);
}
@OnMessage
public void onMessage(String message, Session session) {
log.info("收到消息:{}", message);
}
@OnError
public void onError(Session session, Throwable error) {
log.error("WebSocket 错误:{}", error.getMessage());
}
// 发送消息给指定用户
public void sendToClient(String sid, String message) {
Session session = sessionMap.get(sid);
if (session != null && session.isOpen()) {
try {
session.getBasicRemote().sendText(message);
log.info("发送消息给 {}:{}", sid, message);
} catch (Exception e) {
log.error("发送失败:{}", e.getMessage());
}
}
}
}11.3 来单提醒/催单场景
@Service
public class OrderServiceImpl implements OrderService {
@Autowired
private WebSocketServer webSocketServer;
// 用户下单 → 来单提醒(推送给管理端)
public void submit(OrdersSubmitDTO submitDTO) {
// ... 下单业务逻辑
// 来单提醒
Map<String, Object> reminder = new HashMap<>();
reminder.put("type", 1); // 1=来单提醒
reminder.put("orderId", order.getId());
reminder.put("content", "您有一笔新订单");
webSocketServer.sendToClient("admin", JSON.toJSONString(reminder));
}
// 用户催单 → 推送给管理端
public void reminder(Long orderId) {
Map<String, Object> reminder = new HashMap<>();
reminder.put("type", 2); // 2=用户催单
reminder.put("orderId", orderId);
reminder.put("content", "用户催单啦");
webSocketServer.sendToClient("admin", JSON.toJSONString(reminder));
}
}十二、文件上传(阿里云 OSS)
12.1 OSS 配置属性类
package com.sky.properties;
import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;
@Data
@Component
@ConfigurationProperties(prefix = "sky.alioss")
public class AliOssProperties {
private String endpoint;
private String accessKeyId;
private String accessKeySecret;
private String bucketName;
}12.2 OSS 工具类
package com.sky.utils;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.sky.properties.AliOssProperties;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Component;
import org.springframework.web.multipart.MultipartFile;
import java.io.IOException;
import java.util.UUID;
@Component
@Slf4j
public class AliOssUtil {
@Autowired
private AliOssProperties aliOssProperties;
public String upload(MultipartFile file) {
String endpoint = aliOssProperties.getEndpoint();
String accessKeyId = aliOssProperties.getAccessKeyId();
String accessKeySecret = aliOssProperties.getAccessKeySecret();
String bucketName = aliOssProperties.getBucketName();
// 生成唯一文件名(防止覆盖)
String originalFilename = file.getOriginalFilename();
String extension = originalFilename.substring(originalFilename.lastIndexOf("."));
String objectName = UUID.randomUUID().toString() + extension;
OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);
try {
ossClient.putObject(bucketName, objectName, file.getInputStream());
// 返回文件访问 URL
return "https://" + bucketName + "." + endpoint + "/" + objectName;
} catch (IOException e) {
log.error("文件上传失败:{}", e.getMessage());
throw new RuntimeException(e);
} finally {
ossClient.shutdown();
}
}
}12.3 通用上传接口
@RestController
@RequestMapping("/admin/common")
@Api(tags = "通用接口")
@Slf4j
public class CommonController {
@Autowired
private AliOssUtil aliOssUtil;
@PostMapping("/upload")
@ApiOperation("文件上传")
public Result<String> upload(MultipartFile file) {
log.info("文件上传:{}", file.getOriginalFilename());
String url = aliOssUtil.upload(file);
return Result.success(url);
}
}十三、Knife4j + Swagger 接口文档
13.1 添加依赖
<dependency>
<groupId>com.github.xiaoymin</groupId>
<artifactId>knife4j-spring-boot-starter</artifactId>
<version>3.0.3</version>
</dependency>13.2 Swagger 配置类
package com.sky.config;
import com.github.xiaoymin.knife4j.spring.annotations.EnableKnife4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import springfox.documentation.builders.ApiInfoBuilder;
import springfox.documentation.builders.PathSelectors;
import springfox.documentation.builders.RequestHandlerSelectors;
import springfox.documentation.service.ApiInfo;
import springfox.documentation.spi.DocumentationType;
import springfox.documentation.spring.web.plugins.Docket;
import springfox.documentation.swagger2.annotations.EnableSwagger2;
@Configuration
@EnableSwagger2
@EnableKnife4j
public class SwaggerConfig {
@Bean
public Docket adminApi() {
return new Docket(DocumentationType.SWAGGER_2)
.groupName("管理端接口")
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.sky.controller.admin"))
.paths(PathSelectors.any())
.build();
}
@Bean
public Docket userApi() {
return new Docket(DocumentationType.SWAGGER_2)
.groupName("用户端接口")
.apiInfo(apiInfo())
.select()
.apis(RequestHandlerSelectors.basePackage("com.sky.controller.user"))
.paths(PathSelectors.any())
.build();
}
private ApiInfo apiInfo() {
return new ApiInfoBuilder()
.title("苍穹外卖 API 文档")
.version("1.0")
.description("苍穹外卖后端接口文档")
.build();
}
}13.3 使用示例
@Api(tags = "员工管理接口")
@RestController
@RequestMapping("/admin/employee")
public class EmployeeController {
@PostMapping("/login")
@ApiOperation("员工登录")
@ApiImplicitParams({
@ApiImplicitParam(name = "username", value = "用户名", required = true),
@ApiImplicitParam(name = "password", value = "密码", required = true)
})
public Result<EmployeeLoginVO> login(@RequestBody EmployeeLoginDTO loginDTO) {
// ...
}
}访问地址:http://localhost:8080/doc.html十四、常用工具类速查
14.1 BeanUtils(属性拷贝)
// DTO → Entity
Employee employee = new Employee();
BeanUtils.copyProperties(employeeDTO, employee);14.2 DigestUtils(MD5 加密)
// 密码 MD5 加密
String encryptedPassword = DigestUtils.md5DigestAsHex("123456".getBytes());14.3 Jackson(JSON 序列化)
// 对象 → JSON 字符串
String jsonStr = JSON.toJSONString(object);
// JSON 字符串 → 对象
User user = JSON.parseObject(jsonStr, User.class);14.4 LocalDateTime(时间处理)
// 获取当前时间
LocalDateTime now = LocalDateTime.now();
// 时间格式化
DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
String formatted = now.format(formatter);十五、项目部署与运行
15.1 打包
# 在项目根目录执行
mvn clean package
# 跳过测试打包
mvn clean package -DskipTests15.2 运行
# 运行 JAR 包
java -jar sky-server.jar
# 指定环境运行
java -jar sky-server.jar --spring.profiles.active=prod
# 后台运行
nohup java -jar sky-server.jar > app.log 2>&1 &15.3 Nginx 反向代理配置
server {
listen 80;
server_name api.sky.com;
location / {
proxy_pass http://127.0.0.1:8080;
proxy_set_header Host $host;
proxy_set_header X-Real-IP $remote_addr;
}
}十六、快速记忆口诀
启动流程
SpringApplication.run → 初始化上下文 → 加载自动配置 → 启动内嵌容器 → 注册 Controller → 应用就绪自动配置
@SpringBootApplication → @EnableAutoConfiguration → @Import → spring.factories → @ConditionalOnXxx分层架构
Controller 接请求,Service 写逻辑,Mapper 查数据库,Entity 存数据JWT 认证
登录签发 Token → 请求携带 Header → 拦截器校验 → ThreadLocal 存用户 ID → 业务使用 → 请求结束清除缓存注解
@Cacheable 查缓存,@CachePut 更新缓存,@CacheEvict 清缓存定时任务
@EnableScheduling 开启,@Scheduled(cron) 标记,Cron 表达式定时间全局异常
@RestControllerAdvice 全局处理,@ExceptionHandler 分类捕获本笔记基于 Spring Boot 2.7 + 苍穹外卖实战项目整理,覆盖了从项目搭建到部署上线的完整流程,适合日常开发查阅和面试前背诵。建议结合源码实践加深理解。
评论