2025 年 12 月 8 日,小樊完成苍穹外卖后端业务学习,特此整理本文档,总结项目中的知识点。

适用人群:项目初学者(快速熟悉架构与技术栈)、已完成学习的学者(巩固知识)。

内容说明:仅放置重点代码,精炼核心知识点,欢迎批评指正。

一、项目简介

“苍穹外卖” 是餐饮外卖类 Java Web 项目,核心价值如下:

  1. 提供大量增删改查接口,夯实后端基础;
  2. 覆盖主流后端技术,包括SSM 框架(Spring + SpringMVC + MyBatis) 、JWT 令牌、过滤器 / 拦截器、MD5 密码加密、MySQL、Redis 缓存等;
  3. 包含辅助开发工具与协议:Maven、Lombok、Knife4j、Http、Websocket 等。

注:本文重点讲述后端技术。前端技术后续我在学习后完成补充。

  • 技术架构

三、核心技术列表及说明

3.1 Nginx

  • 功能:部署前端资源、实现反向代理、负载均衡;
  • 核心价值:高性能 Web 开源服务器,大型项目中通过负载均衡分发请求,减少后端服务器压力。

3.2 SSM 框架(核心技术)

3.2.1 Spring
  • 定位:业务层(Service)核心支撑;
  • 核心能力:事务管理、依赖注入(DI)、Bean 生命周期管理;
  • 关键特性IOC 控制反转(对象创建权交给 Spring 容器,无需手动 new)、AOP 面向切面编程(处理横切关注点)。
3.2.2 Spring MVC
  • 定位:基于 MVC 模式的 Web 应用开发框架;
  • 核心作用:提供结构清晰、模块化的 Web 应用构建方式,负责请求接收与响应。
3.2.3 MyBatis
  • 定位:持久层框架,操作数据库;
  • 核心优势:实现 SQL 与 Java 代码解耦,支持通过 Mapper 接口 + XML / 注解编写自定义 SQL,支持动态 SQL。

3.3 Maven 依赖管理工具

  • 功能:自动下载管理项目依赖包,避免版本冲突;
  • 附加能力:一键将项目打包为可运行 jar 包,简化部署流程。

3.4 MySQL

  • 类型:关系型数据库;
  • 存储方式:基于表结构存储数据,直接写入磁盘。

3.5 分层解耦

  • 实现方式:拆分为 Controller、Service、Mapper 三层,各层通过接口交互;
  • 优势:便于多人协作,简化代码维护与功能扩展。

3.6 IOC 控制反转

  • 定义:对象创建权交给 Spring 容器,替代程序员手动 new 对象;
  • 价值:降低组件耦合度,统一管理对象生命周期。

3.7 增删改查功能复习

  • 学习内容:以菜品管理为例,讲解 Controller+Service+Mapper 三层实现流程;
  • 涉及技术:注解使用、PageHelper 分页、动态 SQL。

3.8 过滤器 Filter / 拦截器 Interceptor

  • 核心作用:请求前置处理(如校验 JWT 令牌,无有效令牌则拒绝访问);
  • 处理对象:前端向后端发起的 HTTP 请求。

3.9 JWT 令牌

  • 定义:加密字符串,用户登录成功后由后端生成并返回前端;
  • 使用流程:前端后续请求(下单、查菜品)携带令牌,后端校验有效性以确认用户身份。

3.10 ThreadLocal

  • 定义:Java 线程专属内存,不同线程变量互不干扰;
  • 项目应用:存储当前登录用户 ID,Controller/Service/Mapper 层无需层层传参即可直接获取,避免多线程数据安全问题。

3.11 AOP 面向切面编程

  • 定位:软件开发技术与编程范式;
  • 核心目标:解决横切关注点问题(通用功能如日志、权限,与核心业务无关但影响多模块)。

3.12 MD5 密码加密

  • 作用:保障用户密码安全;
  • 实现方式:数据库不存储明文密码,仅存储 MD5 加密后的字符串,防止数据库泄露导致密码暴露。

3.13 Redis 缓存

  • 类型:键值型存储系统;
  • 存储方式:基于键值对存储数据,数据存入缓存,提升读取效率。

3.14 HttpClient

  • 定位:Java 后端主动发送 HTTP 请求的工具。

3.15 Websocket 通信

  • 定义:前后端实时双向通信通道;
  • 优势:区别于 HTTP “一问一答” 单向模式,连接建立后可主动收发消息,无需频繁请求。

3.16 阿里云 OSS

  • 定位:云端存储空间;
  • 用途:存储图片类静态资源。

3.17 Lombok

  • 功能:通过注解(@Data@NoArgsConstructor)自动生成实体类冗余代码(getter/setter、构造方法等)。

3.18 Knife4j(基于 Swagger)

  • 功能:通过简单注解自动生成可视化接口文档,便于接口调试。

3.19 内网穿透(Cpolar)

  • 作用:将内网服务(如 8080 端口)映射为公开访问 URL,便于外部访问内网项目。

3.20 Git 代码仓库

  • 结构:分为本地仓库(存储开发者代码)与远程仓库(团队共享);
  • 核心作用:版本控制(错漏可回滚)、多人协作(合并代码避免冲突),保障代码迭代有序可追溯。

3.21 POI 操作 Excel 文件

  • 定位:Java 处理 Excel 文件的核心工具包;
  • 项目应用:实现 Excel 数据读写功能。

四、技术详解

管理端采用 B/S(浏览器 - 服务器)架构,启动 Nginx 后,通过http://localhost:80访问前端页面(端口冲突不建议修改,避免影响 WebSocket 通信)。

4.1 Nginx


4.1.1 Nginx 前端部署

  • 原理:将前端打包的静态资源(Vue.js、ElementUI 开发的页面、JS、CSS)放入 Nginx 指定目录(如nginx-1.20.2\html);
  • 优势:无需依赖后端应用服务器,Nginx 直接根据 URL 匹配资源并返回,提升加载效率。

核心配置(nginx.conf)

4.1.2 Nginx 反向代理

  • 核心价值:统一请求入口、隐藏后端地址、适配 WebSocket 通信;
  • 示例场景

前端登录请求地址:http://localhost/api/employee/login(默认 80 端口,/80可省略);

后端接口地址:http://localhost:8080/admin/employee/login,由 Nginx 转发请求。

4.1.3 Nginx 负载均衡

  • 作用:将请求分发至多台服务器,均衡负载、提升可靠性;
  • 常用策略
    1. 轮询(默认):依次分发请求;
    2. IP 哈希:同一客户端请求固定分发至同一服务器(会话保持);
    3. 加权轮询:按权重分配请求(高权重服务器处理更多请求);
    4. 最少连接:分发至当前连接数最少的服务器。

通过这个Nginx就可以把请求分发给指定的多台服务器,并且我们也设置了权重。不过因为本次演示的是单机项目,我们只有一台电脑,因此我们把第二个地址注释了起来。

4.1.4 Nginx 配置注意事项

  • 端口说明:默认使用 80 端口,修改端口会导致前端 WebSocket 请求无法连接,影响订单状态实时推送等功能,不建议修改。

4.2后端核心架构:SSM

4.2.1 Spring(核心容器,业务层核心)

 核心特性

  1. IOC 控制反转:对象创建、依赖管理交给 Spring 容器,降低组件耦合;
  2. AOP 面向切面编程:统一处理日志、事务、权限(如全局业务方法事务管理);
  3. 附加能力:事务管理、依赖注入(DI)、Bean 生命周期管理。

4.2.2 SpringMVC(表现层框架,处理 Web 请求)

 MVC 角色分工

  • Controller(控制器):接收请求→调用 Service→返回结果;
  • Model(模型):封装业务数据(实体类、集合);
  • View(视图):展示数据(HTML 等)。

核心流程

前端请求 → 核心调度器DispatcherServlet → 处理器映射器HandlerMapping → 控制器Controller → 业务层处理 → 模型 / 视图ModelAndView → 视图解析器 → 前端响应。

4.2.3 MyBatis(持久层框架,操作数据库)

 核心优势

  1. 简化 JDBC:无需手动管理连接、Statement、结果集;
  2. 灵活控 SQL:支持 XML / 注解编写 SQL,适配复杂查询;
  3. ORM 映射:数据库表记录自动绑定 Java 实体类;
  4. 动态 SQL:通过<if>``<where>等标签拼接 SQL。

4.2.4 SSM 整合核心流程Maven依赖管理工具

用户发起请求 → SpringMVC 的 Controller接收请求 → 调用Spring 管理的 Service 层(业务逻辑) → Service 调用MyBatis 的 Mapper 层(操作数据库) → 数据库返回结果 → 结果逐层回传(Mapper→Service→Controller) → SpringMVC 返回视图 / 数据给前端。

4.3 Maven依赖管理工具

Maven 核心说明

1.定位:Java 项目的依赖管理与自动化构建工具,是苍穹外卖项目开发的基础辅助工具之一。

核心功能:

2.依赖管理:自动识别项目所需依赖(如 SSM 框架、Lombok 等),从仓库下载并统一管理,避免版本冲突;

3.构建打包:支持一键将项目编译、打包为可运行 jar 包,简化后端服务的部署流程。

项目价值:减少手动管理依赖的工作量,保障项目依赖环境一致性,提升开发与部署效率。

在苍穹外卖这种分模块的项目中,为了方便统一管理依赖,我们利用Maven的依赖传递的特性, 通过父模块统一管理依赖,再在子模块中引入父模块的依赖。

Maven详情可见此博主,这里简单熟悉即可。

Maven之依赖管理_maven依赖-CSDN博客

4.4 MySQL 数据库

Mysql是一种关系型存储系统,基于表的形式对数据进行存储,直接存储数据到磁盘当中。苍穹外卖使用MySQL存储订单、菜品等数据,具体表如下图所见:

我们使用Mybatis来操作数据库,利用注解或者Xml文件中编写sql语句大大简便了增删改查的效率。

本数据库建表依据阿里巴巴开源手册

        【依据】表名,字段必须时使用小写字母或数字,禁止出现数字开头。

        【依据】表名不考虑复数形式,避免混意

此外,本项目的数据表在处理逻辑关系的时候,采用   使用逻辑外键,舍弃物理外键的形式。也就是说:我们的表之间的关系不通过物理外键的形式来进行关联,而是在代码层面使用代码逻辑的方式进行关联。

简单的讲:不在数据库中创建外键的方式构建表关系,而是在代码处理阶段,用代码逻辑来形成表关联,这样可以降低对数据库的访问压力,而且对数据库的修改也会轻松。

详情见此博主,这里简单熟悉项目中用到的表。

【狂神-MySQL】MySQL全部详细知识点整理(共10章)_狂神 mysql-CSDN博客

4.5 分层解耦

采用SpringBoot的三层架构追求项目模块之间的高内聚、低耦合。

其中Dao层也就是Mapper层,用于访问数据库

那么层与层之间是如何调用接口的呢,这里用到了后面会详细将的IOC控制饭庄,简单来说就是,我们会通过注解的方式,将Service层和Mapper/Dao层创建本身对象的权力交给Spring容器管理,举个订单模块的例子,如当CategoryController层需要调用Service层的接口的时候,我们可以通过

@Autowired

private CategoryService categoryService;

这样的方式创建一个Service层对象,这个对象是从IOC容器中注入来的,而不用我手动new出来。

4.6 IOC控制反转

1. 定位:Spring 框架的核心思想之一,是苍穹外卖项目实现分层解耦的关键技术。

 2. 核心定义:将对象的创建权从程序员手动 new实例的方式,转移给 Spring 容器统一管理;程序员仅需通过注解(如 @Autowired)声明依赖,由容器自动注入对象,而非手动创建。

3.实现方式:通过 Spring 容器扫描注解(如 @Controller、@Service、@Mapper)识别需要管理的类,完成对象实例化、依赖注入及生命周期管控。

 4. 项目价值:

- 降低组件耦合:如苍穹外卖中 `Controller` 层无需手动创建 `Service` 对象,`Service` 层无需手动创建 `Mapper` 对象,修改实现类时无需改动调用代码;

 - 统一管理对象:Spring 容器集中管控所有业务对象(如菜品管理、订单管理的 Service 类),避免重复创建,提升代码可维护性。

4.7 各种注解详情

补充:@RequestParam是SpringMVC中获取前端GET请求参数的核心注解,就像给后端接口“对接”前端传参的“连接器”。在苍穹外卖里,比如前端请求“查询菜品列表”时传`?page=1&categoryId=10`,后端接口方法只需用`@RequestParam("page") Integer page`、`@RequestParam("categoryId") Long cid`,就能直接拿到参数值,不用手动解析请求;还能通过`required=false`设置参数非必传、`defaultValue`给默认值(比如默认页码为1),适配不同传参场景,大幅简化请求参数的获取逻辑。

4.8 分页查询功能复习

这里套餐模块分页查询的例子,带大家快速回顾一下pagehelper分页,分层架构,IOC容器,Mybatis操作数据库,动态SQl。

先看一下需求和参数:

这里我们采用实体类SetmealPageQueryDTO来接受前端传过来的参数,返回值我们采用项目约定的格式Result返回格式,返回的数据我们采用PageResult类来封装,PageResult类如下:

具体代码如下:

SetmealController.java:

package com.sky.controller.admin;

import com.sky.dto.SetmealDTO;
@RestController
@RequestMapping("/admin/setmeal")
@Slf4j
public class SetmealController {
    @Autowired
    private SetmealService setmealService;
    /**
     * 分页查询
     * @param setmealPageQueryDTO
     * @return
     */
    @GetMapping("/page")
    public Result<PageResult> page(SetmealPageQueryDTO setmealPageQueryDTO) {
        PageResult pageResult = setmealService.pageQuery(setmealPageQueryDTO);
        return Result.success(pageResult);
    }
  
}

SetmealService 接口:

public interface SetmealService {
    

    /**
     * 分页查询
     * @param setmealPageQueryDTO
     * @return
     */
    PageResult pageQuery(SetmealPageQueryDTO setmealPageQueryDTO);

}

SetmealServiceImpl.java(接口的实现类):

@Service
@Slf4j
public class SetmealServiceImpl  implements SetmealService {
    @Autowired
    private SetmealMapper setmealMapper;
 
    /**
     * 分页查询
     * @param setmealPageQueryDTO
     * @return
     */
    public PageResult pageQuery(SetmealPageQueryDTO setmealPageQueryDTO) {
        int pageNum = setmealPageQueryDTO.getPage();
        int pageSize = setmealPageQueryDTO.getPageSize();

        PageHelper.startPage(pageNum, pageSize);
        Page<SetmealVO> page = setmealMapper.pageQuery(setmealPageQueryDTO);
        return new PageResult(page.getTotal(), page.getResult());
    }

}

SetmealMapper接口:

 @Mapper
public interface DishMapper {

        /**
     * 菜品分页
     * 查询
     */
    Page<Dish> pageQuery(DishPageQueryDTO dishPageQueryDTO);
}

SetmealMapper.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.sky.mapper.DishMapper">

    <!--    菜品分页查询-->
    <select id="pageQuery" resultType="com.sky.entity.Dish">
        select * from dish
        <where>
                <if test="name != null and name != ''">
                 name like concat('%',#{name},'%')
                </if>
                 <if test="categoryId != null">
                and category_id = #{categoryId}
                </if>
                 <if test="status != null">
                and status = #{status}
                </if>
        </where>
        order by update_time desc
    </select>
</mapper>

4.9JWT令牌

 JWT技术最常见的应用就是给用户下发身份令牌,在本项目中,我们通过JWT来实现资源请求拦截思想。

        如果没有JWT技术,我们只需要知道资源请求的路径,就可以向后端发送相关请求,因为一整个后端的处理逻辑是:接收前端请求,进行业务操作。

      但这明显是一个很严重的缺陷,例如用户知道了我们的删除员工的请求路径是:http/xxx/delete/{id}。那他发送这个请求,后端只要接收到这请求,就会执行对应的操作。

        而这种缺陷的解决思路也很简单,我们为已经登录的用户下发JWT令牌,后端拦截除了登录请求之外的所有请求,而前端每一次请求都要携带这个JWT令牌。后端对请求拦截后,要对请求中携带的JWT令牌进行处理,如果可以校验通过就放行请求,如果没有令牌或者解析错误,就返回登录界面。

1. JwtUtil.java - JWT工具类

这个类提供了JWT令牌的核心操作功能:

createJWT() 方法:

  1. 1.生成JWT令牌
  2. 2.使用HS256签名算法
  3. 3.设置自定义声明(claims)和过期时间
  4. 4.返回加密后的token字符串

parseJWT() 方法:

  1. 解析和验证JWT令牌
  2. 验证token的有效性和完整性
  3. 解析出存储在token中的声明信息(claims)

2. JwtProperties.java - JWT配置属性类

这个类负责管理JWT相关的配置参数:

管理端配置:

  • adminSecretKey:管理员JWT签名密钥
  • adminTtl:管理员令牌过期时间
  • adminTokenName:管理员令牌在请求头中的名称

用户端配置:

  • userSecretKey:用户JWT签名密钥
  • userTtl:用户令牌过期时间
  • userTokenName:用户令牌在请求头中的名称

配置通常从application.yml文件中读取。

4.10 过滤器Filler/拦截器Interceptor

苍穹外卖中使用拦截器Interceptor实现了对用户请求的拦截,用于校验用户的合法身份,比如未登录的用户无法请求后端服务的相关端口(登录端口除外),此外在之前涛哥的Javaweb课程中的智能学习辅助系统中我们使用了过滤器Filler来拦截用户的请求用于日志记录,既然过滤器和拦截器都能拦截用户的请求,那么我们来简单了解一下它们的区别。

过滤器是 JavaWeb 层面的全局请求拦截器,属于 Servlet 规范,作用于所有 HTTP 请求(包括静态资源请求),运行在 Tomcat 容器中,与 Spring 容器无关。

拦截器是 SpringMVC 的核心组件,仅作用于 Controller 层的请求(过滤静态资源、非接口请求),运行在 Spring 容器中,能深度关联 Spring 的业务组件。

 过滤器 和 拦截器 均体现了AOP的编程思想,都可以实现诸如日志记录、登录鉴权等功能。

关于更加详细的使用方法和区别大家可以查看下面这个博主的博客。

过滤器 和 拦截器的 6个区别,别再傻傻分不清了_拦截器和过滤器的区别-CSDN博客

来看一下苍穹外卖中的用法:

JwtTokenAdminInterceptor.java - 管理员JWT拦截器

这个拦截器专门处理管理员端的JWT验证:

拦截所有到达Controller的请求

从请求头获取管理员token(使用 jwtProperties.getAdminTokenName())

使用 JwtUtil.parseJWT() 验证token

解析出员工ID并存储到 BaseContext 中

验证成功则放行,失败返回401状态码

 JwtTokenUserInterceptor.java - 用户JWT拦截器

这个拦截器专门处理用户端的JWT验证:

拦截所有到达Controller的请求

从请求头获取用户token(使用 jwtProperties.getUserTokenName())

使用 JwtUtil.parseJWT() 验证token

解析出用户ID并存储到 BaseContext 中

验证成功则放行,失败返回401状态码

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;

/**
 * jwt令牌校验的拦截器
 */
@Component
@Slf4j
public class JwtTokenAdminInterceptor implements HandlerInterceptor {
    @Autowired
    private JwtProperties jwtProperties;
    /**
     * 校验jwt
     *
     * @param request
     * @param response
     * @param handler
     * @return
     * @throws Exception
     */
    public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
        //判断当前拦截到的是Controller的方法还是其他资源
        if (!(handler instanceof HandlerMethod)) {
            //当前拦截到的不是动态方法,直接放行
            return true;
        }

        //1、从请求头中获取令牌
        String token = request.getHeader(jwtProperties.getAdminTokenName());

        //2、校验令牌
        try {
            log.info("jwt校验:{}", token);
            Claims claims = JwtUtil.parseJWT(jwtProperties.getAdminSecretKey(), token);
            Long empId = Long.valueOf(claims.get(JwtClaimsConstant.EMP_ID).toString());
            log.info("当前员工id:{}", empId);
            BaseContext.setCurrentId(empId);
            //3、通过,放行
            return true;
        } catch (Exception ex) {
            //4、不通过,响应401状态码
            response.setStatus(401);
            return false;
        }
    }
}

如果是登录还没获得jwt令牌的拦截器怎么校验,这就要引进拦截器的配置类了,可以选择拦截什么文件放行什么文件让我们看看下面的代码吧。

/**
 * 配置类,注册web层相关组件
 */
@Configuration
@Slf4j
public class WebMvcConfiguration extends WebMvcConfigurationSupport {

    @Autowired
    private JwtTokenAdminInterceptor jwtTokenAdminInterceptor;

    @Autowired
    private JwtTokenUserInterceptor jwtTokenUserInterceptor;
    /**
     * 注册自定义拦截器
     *
     * @param registry
     */
    protected void addInterceptors(InterceptorRegistry registry) {
        log.info("开始注册自定义拦截器...");
        registry.addInterceptor(jwtTokenAdminInterceptor)
                .addPathPatterns("/admin/**")
                .excludePathPatterns("/admin/employee/login");

        registry.addInterceptor(jwtTokenUserInterceptor)
                .addPathPatterns("/user/**")
                .excludePathPatterns("/user/user/login")
                .excludePathPatterns("/user/shop/status");
    }

重写addInterceptors方法​​:这是配置拦截器的核心方法

.addPathPatterns(“/admin/**”):拦截所有/admin/开头的路径
.excludePathPatterns(…):排除登录接口(不需要JWT验证)

拦截所有/user/开头的路径,排除两个特殊接口:
/user/user/login:用户登录接口
/user/shop/status:店铺状态查询接口(通常需公开访问)

总结拦截器和JWT令牌校验的过程如下:

苍穹外卖里 “管理员请求怎么验证身份” 的流程,像个 “保安查通行证” 的过程:

你(客户端)带着 “通行证”(Token)请求后台接口,先到 “保安岗”(JwtTokenAdminInterceptor)。

保安先确认这是要找业务部门(Controller)的请求,再从请求里拿出你的 “通行证”(从 Header 取 Token)。

保安用 “钥匙”(JwtUtil)检查通行证真假,能解开就拿到你的管理员 ID(emplId)。

把你的 ID 存到 “随身口袋”(BaseContext 的 ThreadLocal)里,方便后面业务部门直接用。

检查通过,放你去找业务部门(Controller)处理请求;要是通行证假的 / 过期,直接把你拦在外面(返回 401)。

整个流程就是 “查通行证→验真假→存身份→放通行”,保证只有合法管理员能访问后台接口.

4.11 ThreadLocal

每个用户的请求都会分配一个独立线程处理,ThreadLocal 就是给这个线程配一个只属于它的抽屉;拦截器验证完管理员身份后,把管理员 ID 放进这个抽屉里;后续不管是 Controller、Service 还是 Mapper 层,只要是这个线程在处理请求,都能直接从自己的抽屉里拿管理员 ID,不用一层一层传参数;不同线程的抽屉互不干扰,比如 A 用户的线程拿不到 B 用户的 ID,不会串数据;请求处理完线程销毁,抽屉里的东西也会清空,不会留 “垃圾”。

而我们把Thread的所有的方法都封装到了包中,放在了Context类下:

通过这个包,我们也可以更好的理解Context包的作用:存放可以操作和访问程序上下文的各种包和接口。

我们看一下在代码中我们是怎么实现的:

1.在拦截器拦截到请求并且下发令牌的时候,就利用ThreadLocal拿到当前登录员工的ID

2.在需要使用的时候调用context包的接口 

在这里我们就用到了封装了ThreadLocal的工具包BaseContext来拿当前登录的员工ID,这里被注释掉是因为我们在后面的业务中对这种填充字段方法进行了统一处理,因此被注释掉了。

4.12  AOP面向切面编程

在涛哥的javaWeb中 我们利用AOP实现了对用户请求的日志记录,在苍穹外卖中我们利用AOP面向切面编程和自定义注解实现了菜品、套餐、分类、用户等Mapper层插入和修改操作的公共字段的自动填充。

1)什么叫AOP?

AOP(Aspect-Oriented Programming,面向切面编程)是一种编程范式,用于在不改变原有代码的情况下,通过“切面”(Aspect)来增强或修改程序的行为。它通过将横切关注点(如日志记录、安全性、事务管理等)从核心业务逻辑中分离出来,减少代码的重复性,提高代码的可维护性。

2)AOP的核心概念

  • 切面(Aspect):包含横切关注点逻辑的模块,例如日志功能或安全性校验。
  • 连接点(Join Point):程序执行过程中可以插入切面的点,比如方法的调用或字段的访问。
  • 切入点(Pointcut):定义了在哪些连接点应用切面逻辑的表达式。
  • 通知(Advice):在特定连接点执行的操作,通常与切入点相关联。通知有不同类型,如前置通知、后置通知、异常通知等。

常见的通知类型包括:

  • 前置通知(Before):在方法执行之前执行。
  • 后置通知(After):在方法执行之后执行。
  • 返回通知(After Returning):在方法成功返回之后执行。
  • 异常通知(After Throwing):在方法抛出异常之后执行。
  • 环绕通知(Around):包围方法的执行,可以在方法执行之前和之后自定义行为。
  • 4.13 MD5密码加密

    MD5 是不可逆的哈希算法(无法从加密后的密文反推明文),专为 “密码存储 / 数据校验” 设计。

    举个例子:明文密码:123456 → MD5 加密→ 密文:e10adc3949ba59abbe56e057f20f883e;

    因此我们在数据库中员工表中存储的密码都是经过MD5加密的

所以在用户登录的时候,也需要把用户传递的密码进行加密后与数据库中的进行对比。

4.14 Redis缓存

此技术十分十分重要,务必学会!!!!!

      与MySQL数据库不同的是,Redis的数据是存在内存中的。它的读写速度非常快,每秒可以处理超过10万次读写操作。因此redis被广泛应用于缓存,Redis是一个基于内存的 key-value 结构数据库。作为数据库,与 MySQL 类似,都是用来存储数据的。但不同的是,MySQL 是将数据以数据文件的方式存在磁盘上,本质上是磁盘存储,而 Redis 则是将数据存储在内存中的,本质上是内存存储。除了存储介质不一样,二者存储数据的结构也是不一样的,MySQL 是通过二维表来存储数据,而 Redis 则是基于 key-value 结构的,也就是键值对。如下所示

Redis有以下这五种基本类型:

  • String(字符串)
  • Hash(哈希)
  • List(列表)
  • Set(集合)
  • zset(有序集合)

Redis 在苍穹外卖中的核心用法,就是把用户查询频繁的菜品列表、商家营业状态等存入redis缓存,这样当用户只需要第一次查询的时候从数据库查询,后续都可以直接从缓存中取数据,大大提高了查询数据的速度。

RedisTemplate 是 Spring 框架提供的操作 Redis 的一站式工具类,可以理解成 “Java 程序和 Redis 之间的翻译官 + 操作面板”—— 它封装了 Redis 的底层通信(比如连接池管理、序列化),提供了面向对象的 API,让开发者不用写原生 Redis 命令(如 SET/GET/HSET),直接用 Java 方法就能操作 Redis 的各种数据结构,是苍穹外卖等 Spring 项目中操作 Redis 的核心方式。

同时我们可以通过注解的形式实现redis缓存的配置

@Cacheable 是 Spring Cache 框架的核心注解,本质是基于 AOP 实现的 “自动缓存查询结果” —— 可以理解成 “给查询方法贴个‘自动缓存贴纸’”,调用方法时先查 Redis(或其他缓存),有数据直接返回;没数据才执行方法,再把结果存到缓存里。

4.15 Httpclient 

Httpclient是一个服务器端进行HTTP通信的库,他使得后端可以发送各种HTTP请求和接收HTTP响应,使用HTTPClient,可以轻松的发送GET,POST,PUT,DELETE等各种类型的的请求

他是一个很常用的技术,因为很多第三方接口的使用方式就需要我们的后端发送请求到指定资源路径,这样才可以调用相关服务。

4.16 Websocket通信

WebSocket 是一种基于 TCP 协议的全双工、双向、持久化的通信协议,解决了传统 HTTP 协议 “请求 - 响应” 模式下无法实现服务器主动推送数据的痛点,能在单个 TCP 连接上实现客户端与服务器的实时双向通信。

一、核心背景:为什么需要 WebSocket?

传统 HTTP 通信存在天然局限:

  • 单向性:只能由客户端主动发起请求,服务器被动响应,无法主动向客户端推送数据;
  • 短连接 / 高开销:每次请求都需要建立 TCP 连接、发送 HTTP 头(如 Cookie、User-Agent 等),即使 “长轮询(Long Polling)” 或 “间隔轮询” 也会产生大量无效请求,消耗带宽和服务器资源;
  • 实时性差:轮询间隔无法做到极致(过短增加服务器压力,过长降低实时性)。

WebSocket 正是为解决 “实时双向通信” 而生,比如聊天、股票行情、实时监控等场景。

二、WebSocket 核心特点

  • 全双工通信:连接建立后,客户端和服务器可同时向对方发送数据,无需等待对方响应;
  • 持久连接:一次握手后,连接持续有效,直到主动关闭,避免频繁建立 / 断开 TCP 连接;
  • 低开销:握手完成后,数据传输仅需少量帧头(而非完整 HTTP 头),带宽占用极低;
  • 兼容性:握手阶段基于 HTTP 协议(80/443 端口),可穿透大部分防火墙 / 代理服务器;
  • 数据类型支持:支持文本(UTF-8)、二进制数据(如图片、音频)传输;
  • 跨域支持:可通过配置实现跨域通信,类似 CORS;
  • 状态保持:连接是有状态的,服务器可识别单个客户端的连续通信

在苍穹外卖中我们使用WebSocket 实现了商家订单提醒和用户催单功能的实现:

我们通过WebSocketServer.java中的sendToAllClient通过服务器向所有建立websocket连接的客户端发送消息。

4.17 内网穿透工具Cpolar:

 在微信支付接口的流程中,当支付成功之后,微信后台要向我们的服务器后台返送支付结果,但是存在一个问题:我们的IP地址都是私有IP地址,微信后台根本访问不了,这样就接收不到支付结果,因此我们需要一个公有的IP地址

而我们给出的解决方案是使用内网穿透工具Capolar

简单的说:内网穿透就是在私有IP地址和公有IP地址之间建立一个临时的映射关系,使得我们的内网服务器暴漏到公网之中,这样我们就可以为微信后台提供一个可以在公网访问的地址,用于接收支付结果。
 

4.18 Spring Task 

 Spring Task 是 Spring 框架提供的一种任务调度工具,用于在应用程序中执行定时任务或者周期性任务。它基于线程池机制,可以创建并管理多个线程来执行任务。

通过 Spring Task,开发人员可以通过注解或者配置的方式定义需要执行的任务,并设置执行的时间间隔或者执行时间点。Spring Task 提供了灵活的任务调度能力,可以满足各种任务执行的需求,例如定时的数据同步、定时的报表生成、定时的缓存清理等。

简单的说:Spring Task为我们提供了一种基于注解的方式来使得我们的后端具有定时处理任务的能力,这项功能可以说是十分常见:我们CSDN的每日周报,就是定时任务。

在苍穹外卖中利用 Spring Task我们实现了对超时订单的定期取消和异常订单的完成。代码如下

/**
 * 自定义定时任务类
 */
@Component
@Slf4j
public class OrderTask {
    @Autowired
    private OrderMapper orderMapper;
    /**
     * 定时任务,检查订单状态,如果订单超过15分钟未支付则取消订单
     */
    @Scheduled(cron = "0 * * * * ?")//每分钟执行一次
    public void processTimeoutOrders(){
        log.info("处理超时订单:{}", LocalDateTime.now());
        //获取十五分钟前的时间
        LocalDateTime time = LocalDateTime.now().plusMinutes(-15);
        //查找状态是待支付,创建时间小于十五分钟前的订单
       List<Orders> ordersList = orderMapper.selectByStatusAndOrderTimeLT(Orders.PENDING_PAYMENT,time);
        if (ordersList != null && !ordersList.isEmpty()) {
            for (Orders orders : ordersList) {
                log.info("取消订单,订单号:{}",orders.getNumber());
                orders.setCancelTime(LocalDateTime.now());
                orders.setCancelReason("支付超时");
                orders.setStatus(Orders.CANCELLED);
                orderMapper.update(orders);
            }
        }
    }
    /**
     * 定时检查订单状态,如果订单处于派送中,则完成订单
     */
    @Scheduled(cron = "0 0 1 * * ?")//每天凌晨1点执行一次
    public void processCompletionOrders(){
        log.info("处理凌晨一点还在配送的订单:{}",LocalDateTime.now());
        //获取一小时前的时间
        LocalDateTime time = LocalDateTime.now().plusHours(-1);
        //获取派送中的订单
        List<Orders> ordersList = orderMapper.selectByStatusAndOrderTimeLT(Orders.DELIVERY_IN_PROGRESS,time);
        if (ordersList != null && !ordersList.isEmpty()) {
            for (Orders orders : ordersList) {
                orders.setStatus(Orders.COMPLETED);
                orderMapper.update(orders);
            }
        }
    }
}

@Scheduled(cron = "0 * * * * ?"): 这是一个cron表达式,用来设置定时任务的执行时间。这里的表达式表示每分钟执行一次任务。
cron表达式的含义为:

  • 第1位:0  秒(0秒时触发)
  • 第2位:*  分钟(任意分钟)
  • 第3位:*  小时(任意小时)
  • 第4位:*  日(任意日期)
  • 第5位:*  月(任意月份)
  • 第6位:?  星期(不指定星期几)

因此,被该注解标记的方法 processTimeoutOrders() 会每分钟执行一次,用于检查并处理超时未支付的订单。 

结束:

感谢我程序员牛肉哥的文档,是牛肉哥激发我写了此片技术总结,其中一部分内容转载来自牛肉哥的万字总结。【苍穹外卖 | 项目日记】第九天 万字总结-CSDN博客。爱你,牛肉哥。
欢迎大家在评论区批评指正,也可以补充新的知识点,我看到后会马上进行修改和补充。

Logo

火山引擎开发者社区是火山引擎打造的AI技术生态平台,聚焦Agent与大模型开发,提供豆包系列模型(图像/视频/视觉)、智能分析与会话工具,并配套评测集、动手实验室及行业案例库。社区通过技术沙龙、挑战赛等活动促进开发者成长,新用户可领50万Tokens权益,助力构建智能应用。

更多推荐