LOADING...

加载过慢请开启缓存(浏览器默认开启)

loading

P-luminary

洪哥面试题

2025/2/9

线程池的执行流程大致如下:

线程池:ThreadPoolExecutor
一开始new的时候没有 是空的。先当一个任务提交给线程池时,线程池首先检查当前运行的线程数是否达到核心线程数。如果没有达到核心线程数,线程池会创建一个新的线程来执行任务。如果已经达到核心线程数,线程池会将任务放入工作队列中等待执行。如果工作队列满了,并且当前运行的线程数小于最大线程数,线程池会创建新的线程来执行任务。如果工作队列满了,并且当前运行的线程数等于最大线程数,线程池会根据拒绝策略

  • 丢弃任务抛出异常
  • 丢弃任务不抛弃异常
  • 丢弃队列最前面的任务,然后重新提交被拒绝的任务、
  • 由主线程处理该任务来处理无法执行的任务。【线程池无法起到异步问题】
    • 问题:想继续异步且不丢弃任务怎么办?
    • 把这个业务先存到别的地方 ↓↓↓
  • 自定义拒绝策略 自己写实现类实现拒绝策略 可以先存到mysql到时候再慢慢搞

怎么确定核心线程数和最大线程数

核心线程数
  • CPU密集型任务:如果任务是CPU密集型的,即任务主要是进行计算而不是等待I/O操作,核心线程数通常设置为CPU核心数加1。这样可以确保CPU在忙于计算的同时,还有额外的线程来处理可能出现的临时高峰。【纯内存计算 不涉及到网络计算和io计算】
    • 八个核 创建十个cpu 没意义 因为最多并发只是8,建议保持一致或者+1,减少加入队列和创建队列的开销
    • 先把其当成io密集 因为层级不一样 不断压测去逼近最理想值
  • I/O密集型任务:对于I/O密集型任务,由于线程在等待I/O操作时会阻塞,因此可以设置更多的核心线程数。一个常用的经验法则是核心线程数设置为CPU核心数的两倍。【线程数越多越好】【压测无限逼近取最合适的线程数】
最大线程数

需要一开始创建好线程等着访问来,如果 核心=最大,此时没有临时线程

创建线程有几种方式(必会)

1.继承Thread类并重写 run 方法创建线程,实现简单但不可以继承其他类
2.实现Runnable接口并重写 run 方法。避免了单继承局限性,编程更加灵活,实现解耦。
3.实现 Callable接口并重写 call 方法,创建线程。可以获取线程执行结果的返回值,并且可以抛出异常。
4.使用线程池创建(使用java.util.concurrent.Executor接口)

  • 想获得线程池里的返回结果用什么?execute + submit
  • 线程有哪些状态? java线程有哪些状态?
  • 线程池有哪些状态?
// 1. 继承Thread类并重写run方法
class MyThread extends Thread {
    @Override
    public void run() {
        System.out.println("线程运行中 - 继承Thread类");
    }
}

// 2. 实现Runnable接口并重写run方法
class MyRunnable implements Runnable {
    @Override
    public void run() {
        System.out.println("线程运行中 - 实现Runnable接口");
    }
}

// 3. 实现Callable接口并重写call方法
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

class MyCallable implements Callable<String> {
    @Override
    public String call() throws Exception {
        System.out.println("线程运行中 - 实现Callable接口");
        return "Callable线程返回结果";
    }
}

// 4. 使用线程池创建线程
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;

public class ThreadCreationExamples {
    public static void main(String[] args) {
        // 继承Thread类创建线程
        Thread thread1 = new MyThread();
        thread1.start();

        // 实现Runnable接口创建线程
        Thread thread2 = new Thread(new MyRunnable());
        thread2.start();

        // 实现Callable接口创建线程
        MyCallable callable = new MyCallable();
        FutureTask<String> futureTask = new FutureTask<>(callable);
        Thread thread3 = new Thread(futureTask);
        thread3.start();
        try {
            // 获取线程执行结果的返回值
            String result = futureTask.get();
            System.out.println(result);
        } catch (Exception e) {
            e.printStackTrace();
        }

        // 使用线程池创建线程
        ExecutorService executor = Executors.newFixedThreadPool(2);
        executor.execute(new MyRunnable()); // 提交Runnable任务
        executor.submit(new MyCallable()); // 提交Callable任务
        executor.shutdown(); // 关闭线程池
    }
}

线程池哪些类型?通过JUC[包]的executes可以创建这四个类型的线程池

问题:为什么阿里巴巴不推荐JUC?有可能会出现OOM、资源浪费

  • 单线程线程池
  • 可缓存线程池/定长
  • 变长的线程池
  • 定时任务的线程池

java 线程池创建时核心参数(高薪常问)

核心线程池大小、线程池创建线程的最大个数(核心+非核心[临时线程])、临时线程存活时间、时间单位、阻塞队列、线程工厂(指定线程池创建线程的命名)、拒绝策略
线程工厂可以设置创建的属性
守护线程:主线程(main)一天不死 守护线程不死 [同生共死]
非守护线程:new一个就是 [不是同生共死]

阻塞队列常用的队列

  1. ArrayBlockingQueue: 基于数组结构的有界阻塞队列,此队列按照先进先出(FIFO)的原则对元素进行排序。创建时需要指定容量。【底层是数组 随机读写的 **时间复杂度O(1)**】
    • 开辟新空间创建新数组 把旧数组的数据迁移过去 new ArrayList为空 需要add才可以 扩容是+10 取1.5倍
    • 高并发不会超过某个值 数组不会涉及到扩容 性能会好一些【比较稳定能预估】
    • new的时候不用指定长度
  2. LinkedBlockingQueue: 基于链表结构的有界阻塞队列(如果不指定容量,则默认为Integer.MAX_VALUE,即视为无界)。按照先进先出的原则排序元素。【随机读写的 时间复杂度O(n) 随机读写快 查询慢 是通过二分查找定位到下标元素(通过下标访问数组和链表) 只会走一次二分查找】
    • 读中间的慢 读头尾快
    • 新增元素不涉及到数组的迁移
    • 一般情况下高并发推荐使用,因为队列高级数据结构(可以用数组和链表的实现 由于底层数据结构不同)的特性是先进先出,链表不涉及到数组的扩容 末尾的最快是O(1)【不稳定】
    • new的时候可指定长度是最大链表的长度
    • 不可指定长度 [有界队列&无界队列] → 可能产生JVM的OOM

线程池的应用要有实际的业务场景

  • 异步任务处理:将任务提交到线程池异步执行,而不阻塞主线程

假设我们有一个电商平台,其中一个核心业务是处理用户订单。在订单处理过程中,我们需要执行以下任务:

  1. 验证订单信息(例如:检查库存、验证用户信息等)。
  2. 计算订单金额(包括商品价格、折扣、运费等)。
  3. 生成订单并保存到数据库。
  4. 发送订单确认邮件给用户。

由于这些任务相对独立,并且处理时间可能较长,我们希望在不影响用户操作的前提下异步执行它们。以下是使用线程池处理这些异步任务的模拟代码:

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;

class Order {
    // 订单信息
    private String orderId;
    private String userId;
    private double amount;

    // 构造函数、getter和setter省略
}

class OrderService {
    private ExecutorService executorService = Executors.newFixedThreadPool(10); // 假设线程池大小为10

    // 处理订单
    public void processOrder(Order order) {
        // 1. 验证订单信息
        executorService.submit(() -> {
            System.out.println("验证订单信息: " + order.getOrderId());
            // 假设验证成功
        });

        // 2. 计算订单金额
        executorService.submit(() -> {
            System.out.println("计算订单金额: " + order.getOrderId());
            // 假设计算成功,设置订单金额
            order.setAmount(100.0); // 示例金额
        });

        // 3. 生成订单并保存到数据库
        executorService.submit(() -> {
            System.out.println("生成订单并保存到数据库: " + order.getOrderId());
            // 假设保存成功
        });

        // 4. 发送订单确认邮件
        executorService.submit(() -> {
            System.out.println("发送订单确认邮件: " + order.getOrderId());
            // 假设邮件发送成功
        });
    }

    // 关闭线程池
    public void shutdown() {
        try {
            executorService.shutdown();
            if (!executorService.awaitTermination(60, TimeUnit.SECONDS)) {
                executorService.shutdownNow();
            }
        } catch (InterruptedException e) {
            executorService.shutdownNow();
        }
    }
}

public class ThreadPoolApplication {
    public static void main(String[] args) {
        OrderService orderService = new OrderService();
        Order order = new Order();
        order.setOrderId("ORDER12345");
        order.setUserId("USER12345");

        // 处理订单
        orderService.processOrder(order);

        // 假设主线程还有其他任务,这里模拟等待其他任务完成
        try {
            Thread.sleep(5000); // 等待5秒
        } catch (InterruptedException e) {
            e.printStackTrace();
        }

        // 关闭线程池
        orderService.shutdown();
    }
}

在这个模拟场景中,我们创建了一个 OrderService 类,它包含一个线程池用于异步处理订单的各个步骤。当用户创建订单时,processOrder 方法会将订单处理的各个任务提交到线程池中异步执行。这样,主线程不会被阻塞,可以继续处理其他用户请求或执行其他任务。在所有任务都提交到线程池后,主线程可能会执行其他逻辑,最后调用 shutdown 方法来优雅地关闭线程池。

你单独部署过项目吗?

前端打包ng配置文件
git所有人都用 需要拉分支 maven打包后端 包放到远程服务器 java -jar 启动!【不应该有人去做】
有专门工具去流水线制作 → Jenkins是一个开源的自动化服务器,它可以帮助您实现自动化构建、测试和部署项目 JenKins + docker 做自动化部署
部署是建立本地的项目再推到服务器

安装 Docker:确保你的部署服务器上安装了 Docker。
安装 Jenkins:可以通过 Docker 安装 Jenkins,或者直接在服务器上安装。

步骤 1:安装 Jenkins 和 Docker

确保 Jenkins 和 Docker 在服务器上都已经安装并运行。

步骤 2:配置 Jenkins

  1. 启动 Jenkins

    使用 Docker 启动 Jenkins:

    docker run -d --name jenkins -p 8080:8080 -p 50000:50000 jenkins/jenkins:lts
    
  2. 访问 Jenkins:在浏览器中访问 http://<服务器地址>:8080,并按照指示完成 Jenkins 的初始设置。

  3. 安装必要的插件:安装 Docker、Git 等相关插件。

步骤 3:创建 Jenkins 任务

  1. 新建任务:在 Jenkins 主页上,点击“新建任务”。

  2. 配置源码管理:配置 Git 仓库地址。

  3. 配置构建触发器:选择合适的触发器。

  4. 配置构建环境:勾选“Build inside a Docker container”。

  5. 添加构建步骤

    • 执行 Shell

      docker build -t myapp .
      
  6. 添加构建后操作

    • Push built image:如果需要将镜像推送到 Docker 仓库,填写仓库信息。

    • 执行 Shell

      docker stop myapp || true
      docker rm myapp || true
      docker run -d --name myapp -p 8080:8080 myapp
      

步骤 4:执行构建

保存配置后,可以手动触发构建或者等待触发器自动执行构建。

步骤 5:验证部署

构建完成后,访问服务器的指定端口(例如 http://<服务器地址>:8080),验证应用是否成功部署。

你的期望薪资?

我目前的薪资是8000,考虑到我即将承担的职责和我的职业发展,我期望的薪资是在现有基础上有所提升,大约在8000到10000之间。当然,我对整体的薪酬包[包括福利、奖金和职业发展机会]也很感兴趣。薪资是如何构成的,包括固定工资、奖金、股权、福利等。

线程池场景题

核心线程数5个,最大线程数设置了10个,队列也设置了10个,现在有并发6个任务来,线程池中有多少个任务?

在您描述的线程池配置下,当有6个并发任务到来时,这些任务的处理情况如下:

  • 核心线程数是5,意味着线程池会首先创建5个线程来处理任务。
  • 当第6个任务到来时,由于核心线程都在忙,线程池会将这个任务放入队列中,因为队列的大小也是10。

所以,在这种情况下,线程池中会有6个任务:5个任务正在被5个核心线程处理,另外1个任务在队列中等待。线程池并没有达到最大线程数10个,因为当前的任务数量和队列容量还未超过核心线程数和队列的总和。

在您提供的线程池配置下(核心线程数5个,最大线程数10个,队列容量10个),当6个并发任务到来时,线程池不会立即创建10个线程,原因如下:

  1. 核心线程数优先:线程池首先会使用核心线程来处理任务。核心线程数是5,所以前5个任务会分别由5个核心线程来处理。
  2. 队列缓冲:当核心线程都在忙碌时,额外的任务会被放入队列中等待,而不是立即创建新的线程。您的队列容量是10,足以容纳当前的第6个任务。
  3. 按需创建线程:线程池会根据任务的处理速度和队列的饱和度来决定是否需要创建超出核心线程数的线程。在您的例子中,尽管有6个并发任务,但队列还未满,因此没有必要创建额外的线程。
  4. 最大线程数限制:最大线程数是线程池可以创建的线程数量的上限,但这并不意味着线程池会一开始就创建到这个上限。只有当队列满了,且还有新的任务到来时,线程池才会创建额外的线程(最多达到最大线程数)来处理这些任务。

因此,在您的场景中,当6个并发任务到来时,线程池的操作是:

  • 5个核心线程各自处理一个任务。
  • 第6个任务被放入队列中等待。

此时,线程池中只有5个线程在运行,队列中有1个任务,总共6个任务。线程池不会创建额外的线程,因为当前的任务数量还未超过核心线程数和队列的总容量。只有当队列满了(即有10个任务在队列中),且还有新的任务到来时,线程池才会考虑创建额外的线程,直到达到最大线程数10个。

get请求和post请求的区别

get请求
  • 请求指定的资源。使用GET的目的是获取数据,
  • 数据在URL中传输,通过将数据附在URL之后,以查询字符串的形式出现
  • 由于数据在URL中可见,因此安全性较低,敏感数据不应通过过GET请求发送
  • URL长度限制通常在2000个字符左右,这意味着GET请求能够传输的数据有限
  • 可以被缓存,也会被浏览器保存在历史记录中
  • 常用于信息查询、数据检索等操作.
post请求
  • 向指定资源提交数据进行处理请求(例如提交表单或者上传文件)。数据被包含在请求体中。
  • 数据存储在请求体(HTTP消息主体)中,不会密在URL上
  • 数据不会出现在URL中,相对更安全,适合传输敏感信息。
  • 理论上没有大小限制,适用于传输大量数据.
  • 不会被缓存,且不会保存在浏觉器历史记录中
  • 常用于数据提交、表单提交等操作

请求行:请求类型 请求方法 url http版本1.1 老式1.0不支持长连接
请求头:key value
常见请求头: 请求数据类型,restful基于json
Content-Type:上传文件不用application 要用 multipart/form-data”
Host:指定请求的服务器的域名和端口号。
User-Agent:包含发出请求的用户代理软件信息,通常包括浏览器类型和版本
请求体:get请求可以有请求体
响应:响应行 响应体 状态码 描述
常见响应头Content-Type:返回数据的格式 Content-Length:响应体的长度,以字节为单位

post请求也可以用问号的形式拼接到浏览器 也可以用路径参数

很大区别:get一般放在url后面 会展示url和后面参数 会暴露传参隐私 登录接口用post来做 有密码敏感信息
表单、密码、长数据用post 不过怕黑客抓包 相对来说安全
get请求后面传参的大小限制 理论上没有限制 只是浏览器厂商会有限制
get用来查询 post新增提交表单

是否上传过图片

阿里云是最后存储的
完整的上传图片应该:
前端要配合(表单 post提交) Content-Type:上传文件不用application 要用 multipart/form-data” 同一个请求能边上传图片和文本数据
后端的操作:传到后端controller接收,有一个类multipart 专门接收二进制数据 图片视频等,有很多api → get input stream封装util 调用upload上传。中小型企业都用阿里云oss 因为要考虑容灾 地震 容易数据丢失,要考虑备份→集群,服务器有物理硬件上限(要有运维成本),文件维护很专业交给专业的人。阿里云的机房在深圳,广州的人访问会比哈尔滨的更快。光纤受物理限制 越长越有损耗。大型运营商在全国各地都有机房,可以智能判断比如哈尔滨的就去访问黑龙江服务器。CSDN内容分发(收费)

前端传过来的图片怎么设置图片大小 有没有什么办法?

思考:为什么后端要限制前端图片传的大小?
大图片 + 多人上传,首先后端要读到jvm内存再二进制流给到阿里云,同时并发有可能超出OM的java内存大小
springboot yml加文件上传大小配置

spring:
  servlet:
    multipart:
      max-file-size: 10MB # 单个文件的最大大小
      max-request-size: 20MB # 整个请求的最大大小,包括多个文件的总和
  1. 读取到 JVM 内存
    • 当前端发送图片文件到后端时,后端服务器需要接收这个文件的数据。
    • 在 Java 应用程序中,接收到的文件数据首先会被加载到 JVM(Java 虚拟机)的内存中。这是因为在 Java 应用程序中处理任何数据之前,数据必须先被加载到内存中。
  2. 二进制流给到阿里云
    • 一旦文件数据被加载到 JVM 内存中,后端服务通常会将这些数据以二进制流的形式上传到云存储服务,比如阿里云的对象存储服务(OSS)。
    • 这个过程涉及到数据的读取和写入操作,即从 JVM 内存读取数据,然后写入到云存储服务。
  3. 并发可能导致内存溢出
    • 如果有多个用户同时上传大图片,后端服务器可能会同时处理多个上传请求。
    • 每个上传请求都会占用一部分 JVM 内存。如果上传的图片非常大,且并发请求的数量很多,那么所有请求加起来的内存使用量可能会迅速增加。
    • 如果内存使用量超过了 JVM 分配给应用程序的内存大小(即 OutOfMemory,简称 OOM),就会发生内存溢出错误。这种错误会导致应用程序崩溃或者变得不稳定。
  4. 限制上传大小的重要性
    • 为了防止内存溢出错误,后端通常会限制上传文件的大小。
    • 通过限制单个文件的最大大小(max-file-size)和整个请求的最大大小(max-request-size),可以有效地控制内存的使用,避免因大量并发上传大文件而耗尽服务器内存。

你在里面主要负责哪方面的工作?

我之前负责后端开发 也会参与一部分设计工作
开发完会协助测试 和前端进行联调
和组长一起进行测试
和前后端的逻辑基本上都是可以的

上家公司的离职原因,薪资多少,薪资结构

不要说一些面试官能挑刺的理由
发展前景?表明上家公司不好
太想进步?表名上家公司提供的技术不好 自己技术不好
在上家公司我学习了很多 成长了很多,个人发展原因 ,想要涨薪

// 来自AI的答案 仅供参考
我在上家公司学到了很多,但我觉得为了我的职业发展,我需要寻找一个能够提供更多成长机会和挑战的职位。我想要在[技能/领域]上进一步深耕,而贵公司的职位看起来非常符合我的职业规划;我在上家公司的年薪大约在6000到7000之间;我的薪资结构主要包括基本工资、每年两次的绩效奖金、股票期权以及一些标准福利,比如健康保险、退休金计划等。此外,公司还提供了一些额外的福利,比如灵活的工作时间和远程工作的机会
简历公司

上家公司如果问工作不好找 为什么不先找到再离职
我在这一块想好好准备面试 但是上班的时间不好分配 我想专心去找工作
上家工作繁忙抽不出时间去准备 所以我想多多准备
异地公司 → 万能理由:现在面的公司在哪家里人就在哪[地理位置要接近 精确到哪个城市] 异地很多都线下不方便先离职专心准备

薪资多少

现在期望12k 上家最好保证**20%-30%**区间→8-9-10k(参考城市不同)

薪资结构

基本工资(七八成)+绩效工资(20%-30%) 有公司先扣除 有的当月发
A 120% S 150%-200% C 80%

你对上家公司的看法

不能贬低 要说优点 学习成长了很多 同事和领导都很照顾我

什么时候能入职?

三个工作日 到 一周之间

你离职了 现在有多少个offer了?

不能说一个都没有

  • 我已经有2个offer 但是一定要表达对当前公司的期待 经过我的了解 我更喜欢贵公司的发展和文化
  • 我也是刚刚开始找工作…

你可以接受加班吗

(必须完全接受全部加班 先拿到offer再说)

Controller和RestController的区别

@RestController = @Controller + @ResponseBody

@Controller如果要返回JSON/XML等格式的数据给客户端,必须显式的使用@ResponseBody注解将返回的对象转换为HTTP响应体内容。
@RestController 专门为构建RESTful Web服务设计的控制器。它简化了创建API的过程,因为所有方法默认都会将返回值直接写入HTTP响应体中作为JSON或XML格式的数据。

@Controller可以声明一个类为一个bean 控制器用
@ResponseBody 具体方法和类都可以 不是包装类和字符 都可以自动转成json数据格式 更符合restful风格

在yaml文件中定义了一些参数,该怎么调用

  • 使用 @Value 注解,这是最直接的方式,适用于简单的属性注入。是bean的注解 用${key}还可以用#
    • ${}:用于注入外部配置文件的值。它告诉Spring需要从环境变量、属性文件、系统属性等地方查找相应的值。
    • #{}:用于执行SpEL(Spring Expression Language,Spring表达式语言)表达式。它允许你在注入值时执行一些简单的计算或逻辑。
    • 如果在多个类里引用 配置多 杂乱 可以写个配置类写一堆的属性 提供get set方法 配置类.get获取到配置
# application.yml
server:
port: 8080

custom:
property: myCustomValue
number: 42
enabled: true
import org.springframework.beans.factory.annotation.Value;
import org.springframework.stereotype.Component;

@Component
public class CustomComponent {

 // 注入server.port属性
 @Value("${server.port}")
 private String serverPort;

 // 注入custom.property属性
 @Value("${custom.property}")
 private String customProperty;

 // 注入custom.number属性,并转换为int类型
 @Value("${custom.number}")
 private int customNumber;

 // 注入custom.enabled属性,并转换为boolean类型
 @Value("${custom.enabled}")
 private boolean customEnabled;

 // 使用SpEL表达式来计算值
 @Value("#{${custom.number} * 2}")
 private int doubleCustomNumber;

 // 使用SpEL表达式来获取环境变量
 @Value("#{systemProperties['os.name']}")
 private String osName;

 // ... 使用注入的值进行操作

 // Getter和Setter方法
 // ...
}
  • 使用 @ConfigurationProperties 注解,通常会指定一个前缀prefix),这个前缀用于指定配置文件中哪些属性应该被绑定到这个 Bean 上。然后确保主应用程序类或某个配置类上有@EnableConfigurationProperties(AppProperties.class) 注解
    @ConfigurationProperties(prefix = "prefix")

IOC和DI有了解过吗,它们的好处是什么

它们的目的是为了解耦
IOC(控制反转)是Spring的两大核心之一,DI(依赖注入)
IOC把控制权交给spring容器
对象创建好之后 之间会有依赖关系 DI因此而生
实现方式:DI通常有四种实现方式

  • 属性注入 注解注入

    • @Autowired 是 Spring 提供的注解,用于自动装配 Bean。它可以用于字段、构造函数、方法或设置器上。当 Spring 容器启动时,它会自动查找并注入匹配的 Bean。
      • 偶尔有不影响程序运行的报错?写spring技术人员是根据jdk写,怕别人不用。
    • @Resource 是 Java 的注解[JDK的],用于依赖注入,它也可以用于字段、方法或设置器上。与 @Autowired 不同的是,@Resource 默认通过名称进行匹配,如果未指定名称,则尝试通过类型进行匹配。
    • 两者区别
      • @Autowired 先根据属性类型 去容器里面找 如果找不到 再根据**属性名称[字段]**去找 如果实在找不到就会报错 [@Autowired永远不会放弃你的 尽其所能去帮你找]
      • @Resource 先根据属性名称去找 要么找不到 要么找到一个 找到就去注入 如果找不到 可以再根据属性类型去找 [类型找不到 或者 找到多个 也会报错]
    它俩最大的区别是什么?

    @Autowired 更倾向于按类型注入,如果类型不唯一,则需要指定注入的名称。
    @Resource 更倾向于按名称注入,如果没有指定名称,则尝试按类型注入。
    两者的不同在于默认的注入策略和如何处理不唯一的 Bean 定义。

    @Autowired@Resource 最大的区别在于它们的默认注入策略和所依赖的注入机制:

    1. 默认注入策略
      • @Autowired:默认是按照类型(Type)进行注入的。如果容器中存在多个相同类型的 Bean,则需要通过 @Qualifier 注解指定具体的 Bean 名称,或者通过设置 @Autowiredrequired 属性为 false 来允许没有找到匹配的 Bean 时不抛出异常。
      • @Resource:默认是按照名称(Name)进行注入的。如果未指定名称,则尝试按类型进行注入。如果容器中存在多个相同类型的 Bean,且没有指定名称,可能会抛出异常。
    2. 依赖的注入机制
      • @Autowired:是 Spring 框架提供的注解,因此它只能用于 Spring 管理的上下文中。
      • @Resource:是 Java 的扩展包(javax.annotation)提供的注解,它是 JSR-250 规范的一部分,因此可以在任何实现了 JSR-250 规范的容器中使用,不仅限于 Spring。

    简而言之,最大的区别在于 @Autowired 更侧重于类型匹配,而 @Resource 更侧重于名称匹配,并且 @Resource 是 Java 标准的一部分,具有更广泛的适用性。

  • 构造函数注入 [默认生成空参构造方法 若写有参构造原来无参会被覆盖 参数根据类型去找和@Autowired类型一样 可以写多个构造方法 如果去多个构造方法重载会报错 怎么办?加个@Autowired[属性,构造方法,参数]都可加 不可多个方法都加@Autowired 反射会触发构造方法 @Bean => new ]

    public class MyService {
    
        private DependencyA dependencyA;
        private DependencyB dependencyB;
    
        // 构造函数注入
        @Autowired
        public MyService(DependencyA dependencyA) {
            this.dependencyA = dependencyA;
        }
    
        // 另一个构造函数
        @Autowired
        public MyService(DependencyB dependencyB) {
            this.dependencyB = dependencyB;
        }
    
        // ... 其他方法 ...
    }
    /////////////////////////////////////////////////////
    在上面的例子中,由于有两个构造函数都使用了 @Autowired 注解,Spring 将无法确定使用哪一个构造函数,因此会抛出异常。要解决这个问题,你应该只在一个构造函数上使用 @Autowired 注解。
    
  • Set方法注入[原生spring 用xml去定义才有 SpringBoot没有这个注入 ]

    public class MyService {
    
        private DependencyA dependencyA;
    
        // Set 方法注入
        @Autowired
        public void setDependencyA(DependencyA dependencyA) {
            this.dependencyA = dependencyA;
        }
    
        // ... 其他方法 ...
    }
    //////////////////////////////////////////////////////
    在 Spring Boot 中,虽然不常用 XML 配置,但是你仍然可以通过注解来实现 Set 方法注入。
    
  • 普通方法注入

    public class MyService {
    
        private DependencyA dependencyA;
    
        // 普通方法注入
        @Autowired
        public void init(DependencyA dependencyA) {
            this.dependencyA = dependencyA;
        }
    
        // ... 其他方法 ...
    }
    //////////////////////////////////////////////////////
    普通方法注入指的是在类中的任意非构造函数方法上使用 @Autowired 注解
    

测试过程有没有出现反复的困扰?

客户需求频繁更改
测试用例没有覆盖到
开发和测试环境未协调

太复杂的改动要先报备技术经理、项目经理

测试:自测 单元测试 专业人员

公司使用哪些技术?

后端:Redis RabbitMQ 搜索引擎 微服务常用组件 远程调用 统一网关 Springboot Springcloud MybatisPlus

项目有多少个成员?

2前 8后 1测 1运维 1项目经理(小公司约13人左右) 要具体人数
自研公司?外包?

自研公司

  • 创业型自研公司:通常员工人数在10-50人之间,初期可能更少,只有几人到十几人。
  • 成熟自研公司:员工人数可能从几十人到几百人甚至更多。

外包公司

  • 小型外包公司:员工人数可能在10-50人之间。
  • 中型外包公司:员工人数可能在50-200人之间。
  • 大型外包公司:员工人数可能超过200人。

HashMap底层原理

底层数据结构

jdk1.8之前底层结构是数组+链表(key+value) 数据结构通用的[键值对+哈希表的数据结构]
jdk1.8以后【数组+链表+红黑树】在解决哈希冲突时有了较大的变化,当链表长度大于阈值(默认为8)时且数组长度大于64时,将链表转化为红黑树,以减少搜索时间。扩容时,红黑树拆分成的树的结点数小于等于临界值6个,则退化成链表。后期使用map获取值时,直接找到hash值对应的下标,在进一步判断key是否相同,从而找到对应值。
底层的地址运算出来 如果地址不一样计算出来的hashcode不一样,hashcode一般是数字[整数(±或0)] 通过key进行hashcode运算 对数组长度取模 eg:任何整数去取模10 可以定位到value可以放在哪个桶下面

hashcode本身不同的对象算出来的hashcde值是相同的怎么办呢?
两个value不可能放在同一个桶 这就是hash冲突 如果数组长度是8 算出来一个hashcode值是8 和 16 此时取模余数相同这样的情况也是相同。所以此时应该用拉链法[小葡萄串],即使桶一样 可以用指针一个个指,此时的链表是单向链表[linkedlist才是双向链表]。
在1.8后指出如果同一个桶的葡萄串太多了,此时要拿出数据,时间复杂度就是O(n),如果没有很多就是O(1)数组的长度。红黑树的引入是解决链表过长的问题。
红黑树是树形的高级数据结构 时间复杂度O(logn)
二叉树在某些情况下会退化成链表 右子树永远比根节点大
红黑树会旋转自平衡[局部旋转达到平衡] 超过多少层会旋转 不至于退化成链表。

拉链法(Chaining)是 Java 的 HashMap 在 JDK 1.8 之前以及之后都使用的一种解决哈希冲突的方法。在 JDK 1.8 之前,HashMap 的实现主要是基于拉链法,即使用链表来解决哈希冲突。当不同的键通过哈希函数计算出相同的哈希码,并且映射到同一个桶(bucket)时,这些键值对将以链表的形式存储在同一个桶中。

在 JDK 1.8 中,HashMap 的实现进行了改进,当链表的长度超过一定阈值(默认是 8)时,链表会被转换成红黑树。这是为了优化哈希表的性能,特别是当哈希冲突严重时,链表的查询效率会降低到 O(n),而红黑树可以将查询效率提升到 O(log n)。

红黑树如何避免哈希冲突

红黑树本身并不直接解决哈希冲突,而是优化了哈希冲突发生后的数据结构。以下是红黑树在 HashMap 中是如何工作的:

  1. 哈希冲突:当不同的键产生相同的哈希码或经过取模运算后落在同一个桶时,就会发生哈希冲突。
  2. 链表:在 JDK 1.8 中,如果桶中的元素少于一定数量(默认为 8),就会使用链表来存储这些元素。
  3. 红黑树转换:当链表的长度超过阈值(默认为 8)时,并且数组的长度超过 64,链表会被转换成红黑树。这样可以减少查找时间,因为红黑树是一种自平衡的二叉搜索树。
  4. 自平衡:红黑树通过旋转和重新着色操作来保持树的平衡,从而避免了二叉搜索树退化成链表的情况。

红黑树的旋转和自平衡

红黑树通过以下规则保持平衡:

  • 节点颜色:每个节点要么是红色,要么是黑色。
  • 根节点:根节点是黑色的。
  • 红色规则:如果一个节点是红色的,则它的子节点必须是黑色的(不能有两个连续的红色节点)。
  • 黑色高度:从任一节点到其每个叶子的所有路径都包含相同数目的黑色节点。

当插入或删除节点时,可能会破坏这些规则,此时红黑树会通过以下操作来重新平衡:

  • 左旋转:当右子节点是红色,而左子节点是黑色或不存在时,进行左旋转。
  • 右旋转:当左子节点是红色,并且它的左子节点也是红色时,进行右旋转。
  • 重新着色:在某些情况下,通过改变节点颜色来维持红黑树的性质。

通过这些操作,红黑树确保了即使在最坏的情况下,树的高度也不会超过 2log(n+1),从而保证了 O(log n) 的时间复杂度。

总结来说,红黑树并不直接解决哈希冲突,而是优化了哈希冲突后的数据结构,使得即使在发生大量哈希冲突的情况下,HashMap 的性能也不会显著下降。

扩容机制

new的初始化 数组为空
当第一次put的时候才不会为空 为16
扩容因子为什么是0.75?hashmap不仅仅java 其他语言也有这种数据结构 但扩容因子可能不同,是通过大量的数学概率统计出的最好最平衡的值。达到12的时候会扩容成2倍
new一个hashmap可以指定数组长度为7 此时数组长度是8【hashmap的长度永远是2的幂次方 比你传入的长度永远大 且 是2的幂次方】 为什么2的幂次方?因为1.7要数组取模 怎么打这个符号 shift+5 => %,1.8之后用了位运算,>>2 <<2 让你的取模运算更快。如果出现hash冲突会拉链 当它的数组长度大于64 并且 链表长度大于8时,当链表长度小于等于6临界值会变回来【为什么是6?避免频繁切换(离8太近) 链表 ←→ 红黑树[消耗性能]】

链表1.7之前是头插法 会产生一条首尾相接的死循环【并发情况[但是hashmap线程不安全不会用在并发,要用ConcurrentHashMap]一起put 且 同一个桶】
1.8之后是尾插法,并发情况下不会出现cpu飙高,

HashSet底层数据结构

底层是包装了一个hashmap,无序 key不允许重复 value可重复
HashSet单列无序不重复的 key就是那个元素 value就是new了一个无意义的object对象

ArrayList和LinkedList不是线程安全的 用什么?

  • Vector 读写都加锁。
  • CopyOnWriteArrayList 读不加锁 写加锁

ConcurrentHashMap能存null吗?不允许使用 null 作为键,但是允许使用 null 作为值。
HashMap:null默认放在第一个桶下面 下标写死为0

Hashcode相同equals一定相同吗?

hashCode() 相同不一定意味着 equals() 相同,但 equals() 相同则 hashCode() 必须相同。

  • equals() 方法用于判断两个对象是否逻辑上相等。
  • hashCode() 方法用于返回对象的哈希码,这个哈希码通常用于哈希表的快速查找。

key可以放复合对象,要注意要重写 hashcode()和equals() 如果不重写 new了的两个对象有可能会相同
"重地""通话"计算hashcode会比equals更快,一个对象new出来后hashcode已经计算出来了。equals要比较每个对象值,所以先判断hashcode 再判断equals 重写:@Override 用属性里面的hashcode,user里面包含了复杂对象order 此时order也要重写。包装类已经重写了hashcode,要整个对象返回true才为正确的,要层层递进去判断。hashmap重写复杂对象就一定要重写那俩个 ∵ 是比较对象里的属性值
list 有序可重复单列
map 双列key不能重复value可重复 treemap是有序的
set 单列不重复无序 hashset 无序 treeset 有序

== 值 + 地址值
equals 是对象属性值是否一 一相等

HashMap是线程安全的吗

不安全的,可以使用ConcurrentHashMap线程安全、Collections.synchronizedMap()、HashTable
线程安全:多线程对同一个数据进行增删改是否受到影响
怎么办?

  • 加锁

    • synchronized
    • ReentrantLock

    加锁为什么能解决线程安全问题?线程访问资源的先后顺序
    多线程访问同一个数据 => 多个线程访问同一个数据
    秒杀 =>[思想] 1w个人买 对 100个库存进行扣减,只搞100个线程 把100个库存分成10份 其中每份有10个

    初始化库存: 创建一个共享的库存计数器,初始值为100。
    创建线程: 创建100个线程,每个线程在启动时分配到一个特定的库存分片。
    扣减库存: 每个线程尝试扣减其分配到的库存分片中的一个商品。扣减操作必须是原子的,以确保线程安全。
    同步机制: 使用适当的同步机制(如synchronized关键字、ReentrantLock等)来保护库存扣减操作,防止并发问题。
    库存检查: 在扣减前,线程需要检查当前分片是否有剩余库存。如果没有,则线程可以终止或进行其他处理。
    

    hashtable不管读写都会用synchronized加锁,并发一起来读都加锁 没必要,所以用了ConcurrentHashMap读不加锁 写加锁。
    随着时间的推移,Hashtable 已经被认为是遗留代码,现代Java代码更倾向于使用 HashMap(非线程安全)或 ConcurrentHashMap(线程安全)。

Synchronized

public class SynchronizedExample {
    public synchronized void synchronizedMethod() {
        // 这里是同步代码块
        System.out.println("进入同步方法");
        // 执行一些操作
        System.out.println("退出同步方法");
    }
}
-----------------------------------------------------
public class SynchronizedBlockExample {
    private final Object lock = new Object();

    public void synchronizedBlock() {
        synchronized (lock) {
            // 这里是同步代码块
            System.out.println("进入同步代码块");
            // 执行一些操作
            System.out.println("退出同步代码块");
        }
    }
}

ReentrantLock

import java.util.concurrent.locks.ReentrantLock;

public class ReentrantLockExample {
    private final ReentrantLock lock = new ReentrantLock();

    public void lockedMethod() {
        lock.lock(); // 加锁
        try {
            // 这里是同步代码块
            System.out.println("进入锁定的方法");
            // 执行一些操作
            System.out.println("退出锁定的方法");
        } finally {
            lock.unlock(); // 解锁
        }
    }
}

红黑树查询效率高的原因

红黑树是一种自平衡的二叉搜索树,它通过特定的规则来保持树的平衡,这些规则确保了树的高度大约是log(n)
自平衡 局部旋转

hashMap时间复杂度

  • O(1):不涉及到拉链
  • O(n):拉链不是树
  • O(logn):桶为红黑树

这个测试是你做的吗,还是你们团队去做的?

像自测的话是我自己去做的(测试用例、apifox),然后交给专业测试人员

团队是怎么协作的?

像我们团队的话,有使用禅道来做我们的文档管理,需求管理和需求的变更控制,和工作的一个统计报表,大部分的协作任务都可以在禅道上完成,代码这一块我们是使用git来做一个代码管理和协作的。

linux查看每个文件夹下的文件大小的命令

ls -lh

linux查看文件夹大小的命令

du -sh
du -sh –all 隐藏
du -sh ./* : 单独列出各子项占用的容量

linux查看进程的命令,动态查看一个文件的最后100行的命令

ps -ef
ps aux [查看所有用户的进程(包括其他用户的进程):]
ps -ef | grep mysql

动态:tail -n 100 -f xxx 【-f → follow】
静态:tail -n 100 xxx

常用的Linux命令

rm = romove

用于删除文件和目录
rm [-rf] name
-r(recursive递归):将目录及目录中所有文件(目录)逐一删除,即递归删除
-f(force):无需确认,直接删除

rmdir = remove directory

它用于删除空目录。如果目录不为空,即目录中包含文件或其他子目录,rmdir 命令将无法删除该目录

pwd = print working directory

打印出当前工作目录的绝对路径。当你需要知道你在文件系统中的当前位置时,这个命令非常有用

cp = copy

复制文件和目录。这个命令可以用来创建文件的副本或将文件从一个位置移动到另一个位置。
cp [-r] source dest
-r (recursive递归):如果复制的是目录需要使用此选项,此时将复制该目录下所有的子目录和文件

mv = move

为文件或目录改名、或将文件或目录移动到其他位置【移动 重命名 修改】

grep

用于搜索文本数据,特别是使用正则表达式来匹配指定的模式
查看特定进程的详细信息,例如进程名为 mysql
ps -ef | grep mysql

tar [tape archive]

用于打包多个文件和目录到一个归档文件中,或者从归档文件中提取文件

cd

切换路径

vim

编辑文件

cat

查看文件[head]

如何查日志

查看/var/log/user.log文件,并且想要跟踪用户 name:pcy 的活动

tail -f /var/log/user.log | grep “pcy”
高级专用使用awk 可以用正则等一些逻辑操作去获取日志

内建函数

awk 非常强大,可以用于执行复杂的文本分析和报告生成,awk 有许多内建函数,如 length()toupper()tolower() 等。

awk '{print toupper($0)}' filename  # 将所有内容转换为大写

条件语句

awk '{if ($1 > 100) print$1}' filename

循环

awk '{for (i=1; i<=NF; i++) print $i}' filename

数组

awk '{count[$1]++} END {for (word in count) print word, count[word]}' filename

你们接口是如何让前端调用的

我们会在设计阶段提前设计好给前端 并行开发 前后端联调[本地ip端口告诉前端]

接口文档怎么定下来的

根据页面原型、需求设计接口文档[后端自己写],绝大部分后端看原型的出参入参 无太大需求和前端商量。【前端组件库】[若修改返回结构的时候] [按照数据结构修改] 需要听前端意见

前端调用后端用的是什么请求方式

WebSocket【基于长连接通讯】
HTTP

前端开发中,以下是一些常见的使用场景:

  • 获取数据:使用GET请求。
  • 提交表单或数据:使用POST请求。
  • 更新资源:使用PUT或PATCH请求。
  • 删除资源:使用DELETE请求。

前端可以通过多种方式发起这些请求,例如:

  • 使用HTML表单(通常用于GET和POST请求)。
  • 使用JavaScript的XMLHttpRequest对象或者更现代的fetch API来发起各种类型的HTTP请求。
  • 使用各种前端框架和库(如React, Angular, Vue.js)中提供的封装好的HTTP服务。

SpringBoot主要的一些注解?都有哪些,以及主要作用

SpringBoot:
@SpringBootApplication [见↓↓]
@ConfigurationProperties:注解用于将外部配置(如来自properties文件、YAML文件或环境变量)绑定到JavaBean上。它的作用是将配置文件中的属性映射到JavaBean的属性上,这样就可以在应用程序中使用这些配置属性。
@SpringBootTest:用于测试 Spring Boot 应用,提供测试环境的支持
@EnableConfigurationProperties:启用对配置属性的支持,允许将配置文件中的属性注入到 bean 中。


Spring:
@Component 
@ComponentScan 
@Conditional 
@SpringBootApplication 是一个组合注解,它结合了以下三个注解的功能:
1. @SpringBootConfiguration: 表示这是一个Spring Boot配置类,它本质上是一个@Configuration注解,用于定义配置类,可以包含多个@Bean注解的方法。
2. @EnableAutoConfiguration: 告诉Spring Boot基于类路径设置、其他bean和各种属性设置来添加bean。例如,如果你添加了spring-webmvc和thymeleaf的依赖,这个注解就会自动配置你的应用程序为一个web应用程序。
3. @ComponentScan: 告诉Spring在包及其子包下扫描注解定义的组件(如@Component, @Service, @Repository等)。

aop在项目中有没有使用?aop使用的一些注解及其功能

一定要描述项目场景,web使用aop打印操作日志、使用aop做数据脱敏(150***8786)
过滤器是Servlet技术的一部分,它是Java EE规范的一部分
拦截器是Spring MVC框架的一部分,用于在处理HTTP请求时拦截控制器方法调用。
AOP底层是动态代理设计模式,在理论上效果在一定程度上相同
过滤器拦截器一般拦截某个web的前后,在controller执行前后
AOP是万物皆可拦截、甚至接口和类都可以切,可以增强controller、service、mapper……

定义一个切面类 @Aspect 声明为切面类 + @Component
定义切点 @Pointcut 声明切点表达式

eg:@AfterReturning(pointcut = “execution(public String com.example.yourpackage.Controller.*(..))”, returning = “result”)

通知
  • 前置 @Before
  • 后置 @After
  • 返回 @AfterReturning
  • 异常 @AfterThrowing
  • 环绕 @Around

你在公司里负责的内容

想在controller访问完之后,想在aop实现之后再进行操作

UserThreadLocal 在执行完之后要 remove 出去,抛异常也会执行
@After 不管有无异常都会执行
@Around 结合try…catch…finally 里也可以达到同样效果

@After:这个注解用于定义一个通知(Advice),它在目标方法执行之后执行,无论目标方法执行的结果如何(成功或异常)。

@Aspect
@Component
public class AroundFinallyAspect {

    // 定义切点
    @Pointcut("execution(* com.example.yourpackage.controller..*(..))")
    public void controllerMethods() {
    }

    // 环绕通知
    @Around("controllerMethods()")
    public Object aroundAdvice(ProceedingJoinPoint joinPoint) throws Throwable {
        Object result = null;
        try {
            // 在目标方法执行之前执行
            result = joinPoint.proceed(); // 执行目标方法
            // 在目标方法成功执行之后执行
        } catch (Throwable e) {
            // 在目标方法抛出异常时执行
            throw e; // 可以选择处理异常或者重新抛出
        } finally {
            // 无论目标方法是否成功执行或者是否抛出异常,这里的代码都会执行
            performFinallyAction();
        }
        return result;
    }

    private void performFinallyAction() {
        // 在这里放置最终要执行的代码
    }
}

--------------------------------------------------------------------------------
// 后置通知
    @After("execution(* com.example.service.*.*(..))")
    public void afterAdvice(JoinPoint joinPoint) {
        // 在目标方法执行之后执行的逻辑
    }

    // 返回后通知
    @AfterReturning(pointcut = "execution(* com.example.service.*.*(..))", returning = "result")
    public void afterReturningAdvice(JoinPoint joinPoint, Object result) {
        // 在目标方法成功执行并返回结果后执行的逻辑
    }

在第一个例子中,你直接在@After注解中指定了切点表达式,因此不需要额外的pointcut属性。

对于@AfterReturning注解,它不仅需要在目标方法执行之后执行通知,还需要访问目标方法的返回值。因此,@AfterReturning注解有一个额外的pointcut属性,用于指定切点表达式。此外,@AfterReturning注解还有一个returning属性,用于指定一个参数名,该参数将接收目标方法的返回值

@AfterReturning:在方法执行后返回结果后执行通知。【如果有异常不会处理】

你们这个项目怎么技术选型的

我进到项目中很多已经确认下来的 一般由组长确定了

那你自己怎么想的?[开放性问题]

选xxx技术 网上资料/备书 比较多可以参考[用的人多]
学习成本[框架厉害但上手复杂不好用]
社区的活跃度[官网持续更新版本 框架会不断发展]

你处于后端的什么位置

初级 中级 高级
中级 骨干开发位置[协助组长完成]
中高级 完成设计类

你对你的职业规划是什么

讲实际的话
想成为高级开发/某个领域的专家
提前了解公司领域,有备而来

java基本类型

image-20241222141950707

short 可以占两个字节 可以用16位
int 可以占四个字节 -21亿 ~ 21亿
long 可以占八个字节 天文数字
float 可以占四个字节 0.2F/f
double 可以占八个字节 0.2D/d

float double尽量不要进行运算 ,在Java中进行金钱运算时,应当特别注意浮点数的精度问题,因为浮点数(如floatdouble)在表示某些数值时可能会丢失精度,这对于需要精确计算的金融计算来说是非常不合适的。

1.金钱转成分 向下取整
2.BigDecimal

ASCII码占1个字节 → Unicode字符占2个字节【有些汉字存不了】→ UTF-8占1-3个字节[灵活可变]
在我们性能中一般走Unicode编码性能更高一点 在网络中/存入磁盘Unicode转成ASCII码

jdk?之后 jdk开始存储大量英文和数字 String类也作了更新 不是基于基本数据类型 而是基于byte数组

在Java的早期版本中(例如JDK 1.4及之前版本),String类内部确实使用char数组来存储字符串数据。每个char在Java中占用16位(2个字节),这意味着不管存储的是英文字符还是数字,每个字符都会占用2个字节的内存空间。

从JDK 5开始,Java平台引入了一些变化,但String类的内部表示仍然基于char数组。直到JDK 6和JDK 7,String类的内部表示并没有改为基于byte数组。

真正发生变化的是在JDK 9中,String类内部表示从char数组转变为byte数组加上一个编码标识(coder),这种改变是为了更有效地存储只有ASCII字符的字符串。ASCII字符只需要一个字节来表示,因此使用byte数组可以节省内存空间。当字符串包含Unicode字符时,String类可能会使用更多的编码方式,例如LATIN1或UTF-16。

Vue的生命周期

生命周期的八个阶段:每触发一个生命周期事件,会自动执行一个生命周期方法(钩子)

  1. beforeCreate创建前
  2. created创建后
  3. beforeMount载入前
  4. mounted挂载完成
  5. beforeUpdate数据更新前
  6. updated数据更新后
  7. beforeUnmount组件销毁前
  8. unmounted组件销毁后

String是基础类型吗

不是,是java.lang下的类

String 在 Java 中并不是基础类型,而是一个引用类型。因为 String 是一个类,所以它是引用类型,意味着当我们声明一个 String 变量时,你实际上是指向一个 String 对象的引用

String 的特性
不可变性:String 对象一旦创建就不能被修改。任何改变 String 内容的操作都会创建一个新的 String 对象。
线程安全:由于 String 的不可变性,它们是线程安全的,可以自由地在多个线程之间共享。
字符串池:为了提高性能和减少内存使用,Java 为 String 提供了字符串常量池(String Pool)。当创建一个新字符串时,如果字符串池中已经存在相同内容的字符串,则会返回池中的实例,而不是创建新的对象。

java集合中list和set的区别?

都是接口 某个实现类

单链 有顺序 可重复 有索引[有下标]
单链 不可重复 无索引[无下标] 不能说是无序 因为TreeSet有序 HashSet就是无序的

做了几年开发呢? 实际几个项目?

三年[初中级] → 四~五个项目

你觉得敲代码最重要的是什么?

理解需求前期设计工作[数据库、接口 → 流程图(思路清晰)]、编码阶段[考虑方法封装、注释、考虑代码后期和维护性(设计模式 → 可维护性+扩展性)]、编码风格[阿里巴巴规范]

你的项目有上线吗? 多少人进行开发? 你主要负责后端吗?

有,介绍一下项目组成结构,是的[再问再回答]

SpringBoot的自动装配原理[启动过程中的一部分]SpringBoot启动原理&&如何内嵌外部原件

Spring Boot的自动装配原理是基于Spring框架的IoC(控制反转)和DI(依赖注入)的核心概念,并结合了一系列的约定和条件注解来实现配置类的自动加载和Bean的自动注册

  1. 启动类:Spring Boot 应用通常有一个带有 @SpringBootApplication 注解的启动类。这个注解是一个组合注解,它包含了 @Configuration@EnableAutoConfiguration@ComponentScan
  2. @EnableAutoConfiguration:这个注解是自动装配的关键。它告诉 Spring Boot 根据类路径下的类、Bean 的定义以及各种属性设置,自动配置 Spring 应用。这个注解会导入 AutoConfigurationImportSelector 类,该类会读取所有 spring.factories 文件中的 EnableAutoConfiguration 条目,并将它们作为配置类导入。
  3. 条件化配置:Spring Boot 使用 @Conditional 注解及其一系列的派生注解(如 @ConditionalOnClass@ConditionalOnMissingBean 等)来确保只有在满足特定条件时,配置类或 Bean 才会被创建。
  4. 配置类:自动装配是通过一系列的配置类来实现的,这些配置类包含了 @Bean 方法,用于创建和配置 Spring 容器中的 Bean。
自定义Starter
<!-- Maven项目的依赖示例 -->
<dependency>
    <groupId>com.xxx</groupId>
    <artifactId>xxx-spring-boot-starter</artifactId>
    <version>1.0.0</version>
</dependency>

三大优点:依赖Maven特性[依赖传递] 自动配置 内嵌Tomcat
Spring Boot的自动装配原理是
src/main/resources/META-INF目录下创建spring.factories文件,添加自动配置类的全限定名
我们可以在-info定义spring.factories位于META-INF目录下,Spring Boot使用它来发现和加载自动配置类。

配置类扫描: 通过@SpringBootApplication注解,Spring Boot会触发对@EnableAutoConfiguration注解的处理,该注解会查找spring.factories文件中定义的自动配置类。

Maven里面写test类 用configuration声明 写很多的test类 但是我可以自己写test类然后调不同的方法 应该怎么办?@Conditional[Spring的注解] → 做成非常灵活的 如果没有就用自己写的

Spring里面的事务传播行为

在Spring框架中,事务传播行为定义了事务方法之间的调用关系,即一个事务方法被另一个事务方法调用时,事务应该如何传播。

  1. REQUIRED(默认值) required
    • 如果当前存在事务,则加入该事务;如果当前没有事务,则创建一个新的事务。
  2. SUPPORTS supports
    • 如果当前存在事务,则加入该事务;如果当前没有事务,则以非事务方式执行。
  3. MANDATORY mandatory
    • 如果当前存在事务,则加入该事务;如果当前没有事务,则抛出异常。
  4. REQUIRES_NEW requires_new
    • 创建一个新的事务,如果当前存在事务,则挂起当前事务。
  5. NOT_SUPPORTED not_supported
    • 以非事务方式执行操作,如果当前存在事务,则挂起当前事务。
  6. NEVER never
    • 以非事务方式执行,如果当前存在事务,则抛出异常。
  7. NESTED nested
    • 如果当前存在事务,则在嵌套事务内执行;如果当前没有事务,则行为类似于REQUIRED

REQUIRED:通常用于方法需要在一个事务中运行,但如果已经有一个事务在运行,那么它应该加入这个事务。
SUPPORTS:用于方法不需要事务上下文,但如果已经在一个事务中,它也可以在这个事务中运行。
MANDATORY:用于方法必须在事务中运行,如果没有事务,则会抛出异常。
REQUIRES_NEW:用于方法必须在自己的新事务中运行,即使当前已经有一个事务在运行。
[一般适用于不管有没有抛出异常 都要记录某些操作日志 不能在同一个类里底层是动态代理]
[如果a()和b()方法在同一个类中,并且a()直接调用b(),那么Spring的事务代理无法拦截这个内部调用,因此b()的REQUIRES_NEW事务传播行为不会生效。这是因为内部方法调用不会通过代理,而是直接在同一个对象实例上调用。]

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import org.springframework.transaction.annotation.Propagation;

@Service
public class MyService {

@Autowired
private MyService self; // 注入自身代理实例

public void methodA() {
  // ... 业务逻辑 ...

  self.methodB(); // 通过代理实例调用,事务注解将生效
}

@Transactional(propagation = Propagation.REQUIRES_NEW)
public void methodB() {
  // ... 业务逻辑 ...
}
}

NOT_SUPPORTED:用于方法不应该在事务中运行,如果有一个事务在运行,它将被挂起。
NEVER:用于方法绝对不应该在事务中运行,如果有一个事务在运行,将抛出异常。
NESTED:用于方法应该在嵌套事务中运行,嵌套事务可以独立于外部事务进行提交或回滚

用过Spring的事务吗

一组数据库的增删改操作
声明式事务管理:这是Spring推荐的用法,它通过使用注解(如@Transactional)或基于XML的配置来声明事务边界。底层基于AOP实现动态代理增强方法
编程式事务管理:允许你通过编程的方式直接管理事务,通常使用TransactionTemplate或者直接使用底层的PlatformTransactionManager

声明式事务管理

// 你需要在 Spring 配置中启用事务注解支持:
@Configuration
@EnableTransactionManagement
public class SpringConfig {

    @Bean
    public PlatformTransactionManager transactionManager(EntityManagerFactory entityManagerFactory) {
        return new JpaTransactionManager(entityManagerFactory);
    }
    
    // ... 其他配置
}


// 然后,你可以在服务层的方法上使用 @Transactional 注解来声明事务边界:
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

@Service
public class AccountService {

    @Autowired
    private AccountRepository accountRepository;

    @Transactional
    public void transferMoney(Long fromAccountId, Long toAccountId, BigDecimal amount) {
        // 执行转账操作,比如:
        Account fromAccount = accountRepository.findById(fromAccountId).orElseThrow(...);
        Account toAccount = accountRepository.findById(toAccountId).orElseThrow(...);
        
        fromAccount.setBalance(fromAccount.getBalance().subtract(amount));
        toAccount.setBalance(toAccount.getBalance().add(amount));
        
        accountRepository.save(fromAccount);
        accountRepository.save(toAccount);
        
        // 如果这里发生异常,Spring 将回滚事务
    }
}

编程式事务管理

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.TransactionStatus;
import org.springframework.transaction.support.TransactionCallbackWithoutResult;
import org.springframework.transaction.support.TransactionTemplate;

@Service
public class AccountService {

    @Autowired
    private TransactionTemplate transactionTemplate;
    
    public void transferMoney(Long fromAccountId, Long toAccountId, BigDecimal amount) {
        transactionTemplate.execute(new TransactionCallbackWithoutResult() {
            @Override
            protected void doInTransactionWithoutResult(TransactionStatus status) {
                try {
                    // 执行转账操作,与声明式事务管理中的操作相同
                    // ...
                } catch (Exception e) {
                    status.setRollbackOnly(); // 如果发生异常,标记事务回滚
                }
            }
        });
    }
}

你熟悉的技术栈有哪些,用了哪些?

SpringBoot Vue Git Maven MyBatis……

解决难点的决策有和用户沟通的吗?

有过沟通 让他们了解一下我们的方案

万一用户听不懂怎么办?

我会用一些更加直白通俗的语言让用户理解我们的方案

用户不接受这个方案怎么办?

我们可以提供不止一个方案 或者 提供他提的方案 综合一下各种方案告诉其优缺点(站在我们的专业角度意见)和风险 让客户明知 让其选择

通常一般怎么学习的?最近在研究什么技术?

想面试的目的 要給公司带来一些好的
一般以公司的实际项目中为切入点去学习会更有效率

学习一个新的技术大概要多久?

1-2天 首先看官网 这个技术是解决哪些 看我们的项目需要哪些技术切入点能引用 然后去专门针对这个技术功能点去学习快速上手的接口文档

redis为什么这么快?

  • 主数据基于内存操作
  • Redis是单线程[操作数据的线程],避免上下文的频繁切换整个redis不是就一个线程
  • 底层基于C语言实现 得益于底层良好的数据结构[]
  • 基于非阻塞的IO提升IO读写性能,NIO,BIO,AIO…
Java四大杀手

集合数据结构 jvm 并发编程 网络IO

非阻塞IO(Non-blocking I/O)是一种IO模型,它允许程序在执行IO操作时不会被阻塞,即程序可以在发起IO请求后继续执行其他任务,而不需要等待IO操作完成。以下是关于非阻塞IO的一些关键点:

非阻塞IO的特点:

  1. 异步操作:非阻塞IO操作通常是异步的,意味着程序发起IO请求后,不需要等待IO操作完成,而是可以立即返回去做其他事情。
  2. 事件驱动:非阻塞IO往往与事件驱动模型结合使用,程序可以通过监听器来响应IO事件(如数据可读、连接可写等)。
  3. 减少等待时间:由于程序在等待IO操作完成时不会阻塞,它可以继续处理其他任务,从而提高了程序的响应性和吞吐量。

非阻塞IO的实现方式:

  • NIO(New I/O):在Java中,NIO提供了一种非阻塞的IO方式,使用Selector来管理多个通道(Channel)上的IO事件。
  • AIO(Asynchronous I/O):AIO是另一种非阻塞IO模型,它允许程序完全异步地执行IO操作,通常是通过完成端口(Completion Ports)来实现。
  • BIO(Blocking I/O):与非阻塞IO相对的是阻塞IO,其中每个IO操作都会阻塞调用线程,直到操作完成。

非阻塞IO的优势:

  • 资源利用率:非阻塞IO可以更有效地利用系统资源,因为单个线程可以处理多个IO操作。
  • 高并发处理:在处理大量并发连接时,非阻塞IO可以显著提高系统的并发处理能力。

非阻塞IO在Redis中的应用:

  • 单线程模型:Redis是一个基于内存的键值存储数据库,它使用单线程模型来处理所有客户端请求。由于操作是基于内存的,速度非常快,而单线程避免了上下文切换的开销。
  • 非阻塞IO和多路复用:尽管Redis是单线程的,但它使用非阻塞IO和多路复用技术(如epoll或kqueue)来同时处理多个IO流。这意味着Redis可以在等待IO操作(如网络响应)时不阻塞,从而可以继续处理其他请求。
  • 高性能:Redis的非阻塞IO和多路复用机制使得它即使在面对大量并发请求时也能保持高性能。

redis的数据类型以及使用场景分别是什么

写入依赖
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.7.0</version> <!-- Use the latest version available -->
</dependency>
  • String:存储对象信息(转JSON)
    将用户信息序列化为JSON字符串后存储。

    SET user:1000 '{"name":"Alice","age":30,"email":"alice@example.com"}'
    
    ----------------------------------------------------------------------
    import redis.clients.jedis.Jedis;
    
    public class RedisStringExample {
        public static void main(String[] args) {
            Jedis jedis = new Jedis("localhost");
            jedis.set("user:1000", "{\"name\":\"Alice\",\"age\":30,\"email\":\"alice@example.com\"}");
            String userInfo = jedis.get("user:1000");
            System.out.println(userInfo);
            
            jedis.incr("visits");
            long visits = jedis.get("visits");
            System.out.println("Visits: " + visits);
            
            jedis.close();
        }
    }
    
  • List:链表,查询记录的缓存、列表,朋友圈,微博,队列数据结构
    可以将数据库查询结果缓存为一个列表

    # 查询记录的缓存
    LPUSH recent:queries "SELECT * FROM users WHERE age > 30"
    
    # 消息队列
    使用List作为消息队列,生产者将消息LPUSH到列表,消费者从列表中RPOP消息
    LPUSH message:queue "message1"
    RPOP message:queue
    
    ----------------------------------------------------------------------
    import redis.clients.jedis.Jedis;
    
    public class RedisListExample {
        public static void main(String[] args) {
            Jedis jedis = new Jedis("localhost");
            jedis.lpush("recent:queries", "SELECT * FROM users WHERE age > 30");
            String query = jedis.rpop("recent:queries");
            System.out.println("Recent Query: " + query);
            
            jedis.close();
        }
    }
    w
    
  • Hash:获取局部属性,小key不能设置过期时间Hash是一个键值对集合,适合存储对象
    Hash是一个键值对集合,适合存储对象

    HSET user:1000 name "Alice" age 30 email "alice@example.com"
    HGET user:1000 name
    
    ----------------------------------------------------------------------
    import redis.clients.jedis.Jedis;
    
    public class RedisHashExample {
        public static void main(String[] args) {
            Jedis jedis = new Jedis("localhost");
            jedis.hset("user:1000", "name", "Alice");
            jedis.hset("user:1000", "age", "30");
            jedis.hset("user:1000", "email", "alice@example.com");
            
            String name = jedis.hget("user:1000", "name");
            System.out.println("Name: " + name);
            
            jedis.close();
        }
    }
    
  • Set:无序不可重复的,收藏,点赞,社交场景,聚合计算(∩∪差集)
    社交场景:使用Set来存储用户的关注列表,确保关注关系的唯一性。

    SADD user:1000:following 2000 3000 4000
    
    ----------------------------------------------------------------------
    import redis.clients.jedis.Jedis;
    
    public class RedisSetExample {
        public static void main(String[] args) {
            Jedis jedis = new Jedis("localhost");
            jedis.sadd("user:1000:following", "2000", "3000", "4000");
            Set<String> following = jedis.smembers("user:1000:following");
            System.out.println("Following: " + following);
            
            jedis.close();
        }
    }
    

    聚合计算:计算两个用户的共同关注。

    SINTER user:1000:following user:2000:following
    
  • Zset:排序场景,排行榜,姓名排序

    排行榜:根据用户得分来存储排行榜

    ZADD leaderboard 1000 Alice 950 Bob 900 Charlie
    ZRANGE leaderboard 0 -1 WITHSCORES
    
    ----------------------------------------------------------------------
    import redis.clients.jedis.Jedis;
    import java.util.Set;
    
    public class RedisZsetExample {
        public static void main(String[] args) {
            Jedis jedis = new Jedis("localhost");
            jedis.zadd("leaderboard", 1000, "Alice");
            jedis.zadd("leaderboard", 950, "Bob");
            jedis.zadd("leaderboard", 900, "Charlie");
            
            Set<String> leaderboard = jedis.zrange("leaderboard", 0, -1);
            System.out.println("Leaderboard: " + leaderboard);
            
            jedis.close();
        }
    }
    

    姓名排序:存储学生姓名和成绩,并按成绩排序。

    ZADD students 92 John 85 Mary 88 Alice
    ZRANGE students 0 -1 WITHSCORES
    

分布式锁都可以用。Redisson是Redis的儿子,底层为Hash

redis数据过期策略

  • 惰性删除:键过期时不会立即删除,当访问该键时判断是否过期,如果过期就删除
    惰性删除策略是在访问键时检查键是否过期,如果过期则删除。

    import redis.clients.jedis.Jedis;
    
    public class LazyExpiration {
        private Jedis jedis;
    
        public LazyExpiration() {
            // 连接到Redis服务器
            this.jedis = new Jedis("localhost");
        }
    
        public String getKey(String key) {
            // 检查键是否存在
            if (!jedis.exists(key)) {
                return null;
            }
    
            // 检查键是否过期
            if (isExpired(key)) {
                // 如果键已过期,则删除它
                jedis.del(key);
                return null;
            }
    
            // 如果键未过期,返回键的值
            return jedis.get(key);
        }
    
        private boolean isExpired(String key) {
            // 获取键的剩余生存时间,如果返回值大于0,则键未过期
            return jedis.ttl(key) == -2;
        }
    
        public static void main(String[] args) {
            LazyExpiration lazyExpiration = new LazyExpiration();
            String value = lazyExpiration.getKey("myKey");
            if (value != null) {
                System.out.println("Key value: " + value);
            } else {
                System.out.println("Key does not exist or has expired.");
            }
        }
    }
    
  • 定时删除:设置键的过期时间,当键过期时,立即删除

    import redis.clients.jedis.Jedis;
    
    public class ActiveExpiration {
        private Jedis jedis;
    
        public ActiveExpiration() {
            // 连接到Redis服务器
            this.jedis = new Jedis("localhost");
        }
    
        public void activeExpireCycle() {
            // 随机检查一定数量的键
            for (int i = 0; i < 10; i++) {
                String key = jedis.randomKey();
                if (key != null && isExpired(key)) {
                    // 如果键已过期,则删除它
                    jedis.del(key);
                }
            }
        }
    
        private boolean isExpired(String key) {
            // 获取键的剩余生存时间,如果返回值大于0,则键未过期
            return jedis.ttl(key) == -2;
        }
    
        public void runPeriodicTask() {
            // 定时任务,按照一定的频率运行
            while (true) {
                activeExpireCycle();
                try {
                    Thread.sleep(1000); // 每秒执行一次
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            }
        }
    
        public static void main(String[] args) {
            ActiveExpiration activeExpiration = new ActiveExpiration();
            // 启动定时任务
            activeExpiration.runPeriodicTask();
        }
    }
    
高薪冲刺 → 定时删除详细策略

要扫描所有的定期任务删除 有策略可以设置阈值

啥时候离职的?半个月太长了

刚刚离职 也是刚刚开始投

主要工作职责

主要负责后端工作,协助测试,运维上线

你对前端有了解过吗?

有了解过,如HTML、CSS、JavaScript、框架[Vue、Element]等 可以很快的上手
我主要专长在于后端开发,可以学习和了解更多的前端知识

各种淘汰策略介绍

Redis提供了8种淘汰策略,可以分成两大类:

1、针对所有键的策略:对所有键进行选择和淘汰。

2、仅针对有过期时间的键的策略:只在设置了过期时间的键中选择淘汰对象。

以下具体策略:
可以区分为两类:[有设置过期时间的key 不管你有没有设置过期时间]

1. noeviction【默认】
  • 描述:达到内存限制时,不再执行删除操作,直接拒绝所有写入请求(包括插入和更新)。[可以读 但是拒绝写请求]
  • 适用场景希望数据永不丢失的场景,但需要保证内存充足,否则会导致写入操作失败。
2. allkeys-lru(最近最少使用)
  • 描述:在所有的键中使用 LRU算法,删除最近最少使用的键。
  • 适用场景:适合缓存场景,保留频繁访问的键,逐出很少被访问的键。
3. allkeys-lfu(最少使用频率)【电商】
  • 描述:在所有键中使用 LFU 算法,删除使用频率最低的键。
  • 适用场景:适用于需根据使用频率进行淘汰的场景,更关注访问次数而非访问时间。
4. volatile-lru(最近最少使用)
  • 描述:仅对设置了过期时间的键使用 LRU 算法。
  • 适用场景:适合缓存一些有过期时间的数据,希望根据访问频率来进行内存管理的场景。
5. volatile-lfu(最少使用频率)【电商】
  • 描述:仅对设置了过期时间的键使用 LFU算法。
  • 适用场景:同 volatile-lru,但更关注使用频率
6. allkeys-random
  • 描述:在所有键中随机选择删除某个键。
  • 适用场景:适用于缓存数据访问频率没有明显差异的情况。
7. volatile-random
  • 描述:在所有设置了过期时间的键中随机选择删除某个键。
  • 适用场景:适合缓存带有过期时间的数据,删除哪个数据不重要的场景。
8. volatile-ttl
  • 描述:在设置了过期时间的键中,优先删除剩余生存时间(TTL)较短的键。
  • 适用场景:适合希望优先清理即将过期的数据的场景。
import redis.clients.jedis.Jedis;

public class RedisMaxMemoryPolicyExample {
    private Jedis jedis;

    public RedisMaxMemoryPolicyExample() {
        // 连接到Redis服务器
        this.jedis = new Jedis("localhost");
    }

    public void setMaxMemoryPolicy(String policy) {
        // 设置Redis的内存淘汰策略
        jedis.configSet("maxmemory-policy", policy);
    }

    public String getMaxMemoryPolicy() {
        // 获取当前Redis的内存淘汰策略
        return jedis.configGet("maxmemory-policy").get(1);
    }

    public static void main(String[] args) {
        RedisMaxMemoryPolicyExample example = new RedisMaxMemoryPolicyExample();

        // 设置不同的内存淘汰策略
        example.setMaxMemoryPolicy("noeviction");       // 默认策略,拒绝写请求
        example.setMaxMemoryPolicy("allkeys-lru");      // 所有键使用LRU淘汰
        example.setMaxMemoryPolicy("allkeys-lfu");      // 所有键使用LFU淘汰
        example.setMaxMemoryPolicy("volatile-lru");     // 仅有过期时间的键使用LRU淘汰
        example.setMaxMemoryPolicy("volatile-lfu");     // 仅有过期时间的键使用LFU淘汰
        example.setMaxMemoryPolicy("allkeys-random");   // 所有键随机淘汰
        example.setMaxMemoryPolicy("volatile-random");  // 仅有过期时间的键随机淘汰
        example.setMaxMemoryPolicy("volatile-ttl");     // 优先淘汰TTL较短的键

        // 获取当前内存淘汰策略
        String currentPolicy = example.getMaxMemoryPolicy();
        System.out.println("Current Max Memory Policy: " + currentPolicy);
    }
}

缓存三兄弟(穿透、击穿、雪崩)

一般在读缓存的时候出现的问题。思路:产生的原因 + 解决的方案

==缓存穿透==:用户或前端查询到一个在数据库中不存在的数据,先查redis再走数据库。对数据库压力会很大。关系型数据库是性能的瓶颈 希望把高数量都挡在数据库前面。查询一个不存在的数据,mysql查询不到数据也不会直接写入缓存,就会导致每次请求都查询数据库(可能原因是数据库被攻击了 发送了假的/大数据量的请求url)

  • 解决方案一缓存空数据,查询返回的数据为空,仍把这个空结果进行缓存 {key:1, value:null} 【空字符串】没有Null的数据类型下一次读取直接把空串返回
    优点:简单
    缺点:消耗内存,可能会发生不一致的问题

如果一直模拟一个不同的不存在的key 这时候就要用到布隆过滤器

  • 解决方案二布隆过滤器 (拦截不存在的数据)
    [商品读多写少上缓存,要把商品数据写到布隆过滤器中,以商品的id独一无二计算hashcode,用布隆过滤器。取模数组落到桶内 会把0置为1]
    有很多个二进制数组每个二进制数组用不同的hash算法进行计算此时落到的桶就不一样
    作用:读的时候 前端传id 之前怎么写进去就怎么拿出来。[位运算(与)速度很快 把多个数组的数据拿出来与运算如果都是1 则这个数据可能存在再查一遍缓存 若不存在直接return返回] 布隆过滤器说你不存在 一定不存在,说你存在 则可能存在[哈希冲突]

    ★ 项目上线很久了 商品早就下架不卖了 这时候会发生什么问题

    这时候布隆过滤器还有之前的痕迹,需要把那些1设置为0。
    布隆过滤器 不支持对某个的1设置0 → 因为有哈希冲突我不知道这个1曾经是誰设置的
    支持将整个都置为0,之后可以搞个定时任务
    布隆过滤器具体实现:Redis、Redission亲儿子、1cache、咖啡因(Caffeine提供了一种非常高效且易于使用的缓存解决方案,它支持多种缓存过期策略)、Guava谷歌

    ★ 以前没设置过且上架过 后面加了布隆,后面要把之前所有数据重新搞进去 怎么解决存量数据

    写一个定时任务

    ★ 场景:工商银行统计每天的用户日活量[上线就算] 要查询某个人连续七天签到 怎么查(用位图)用户量太多了

    搞一个二进制数组,10亿长度的数组,每个数组是一个bit = 10亿个位,一个字节1/bit=8个位,综合计算后大概消耗119MB的空间每天。用用户id去hash 如果用户登录将0置为1有单独的位图结构,统计时间就可以拿日期 往前面数 拿某个id去取模得到桶 找前七个,去进行与运算,连续为1就达到了重复连续七天前端。否则非连续七天。

    在缓存预热时,要预热布隆过滤器。根据id查询文章时查询布隆过滤器如果不存在直接返回

    import redis.clients.jedis.Jedis;
    
    public class DailyActiveUserCounter {
        private Jedis jedis;
    
        public DailyActiveUserCounter() {
            this.jedis = new Jedis("localhost"); // 连接到Redis服务器
        }
    
        // 映射用户ID到位图的键
        private String getUserBitmapKey(long userId, int day) {
            return "user:bitmap:" + userId + ":" + day;
        }
    
        // 用户签到
        public void userSignIn(long userId, int day) {
            String key = getUserBitmapKey(userId, day);
            jedis.setbit(key, userId % 86400, true); // 假设一天有86400秒,使用秒数作为偏移量
        }
    
        // 检查用户连续七天的签到情况
        public boolean checkContinuousSignIn(long userId, int day) {
            for (int i = 0; i < 7; i++) {
                String key = getUserBitmapKey(userId, day - i);
                if (jedis.getbit(key, userId % 86400) == false) {
                    return false; // 如果在连续的七天内有一天没有签到,则返回false
                }
            }
            return true; // 连续七天都有签到
        }
    
        public static void main(String[] args) {
            DailyActiveUserCounter counter = new DailyActiveUserCounter();
    
            // 假设用户ID为12345,今天签到
            long userId = 12345;
            int today = 1; // 假设今天是第1天
            counter.userSignIn(userId, today);
    
            // 检查用户是否连续七天签到
            boolean isContinuous = counter.checkContinuousSignIn(userId, today);
            System.out.println("User " + userId + " has signed in for 7 consecutive days: " + isContinuous);
        }
    }
    

    **bitmap(位图)巨大的二进制数组**:相当于一个以bit位为单位的数组,数组中每个单元只能存储二进制数0或1

    布隆过滤器作用:可以用于检索一个元素是否在集合中

    • 存储数据:id为1的数据,通过多个hash函数获取hash值,根据hash计算数组对应位置改为1
    • 查询数据:使用相同hash函数获取hash值,判断对应位置是否都为1

    存在误判率:数组越小 误判率越大 【要数组足够大 误判率就小】

    bloomFilter.tryInit(size, 0.05) //误判率5%
    

==缓存击穿==:给某一个热点key设置了过期时间,当key过期的时候,恰好这个时间点对这个key有大量的并发请求过来,这些并发请求可能一瞬间把DB击穿微博[鹿晗+关晓彤]【并发同一时间访问】

  • 解决方案一互斥锁【数据强一致性 性能差 (银行)】[控制一个个来访问的次数]

    AQS、ReentrantLock是进程级别的互斥锁,因为有数据在节点1或节点2,分布式锁是在不同场景都可以锁也可以控制访问顺序。

    以商品id作为key 先redis开始查缓存 判断是否为空 不为空直接return后解锁,空就先加锁 去数据库查完备份一份redis后解锁。被锁的其他线程在外面等待。

    ★ 100个人访问同一个商品,只有一个抢到锁,剩下的99个人也要查redis缓存和数据库。

    方案:**双重缓存校验** 先查缓存 查不到加锁 再查缓存 查不到再去数据库 查完后看是否备份后解锁冷代码

    1.查询缓存,未命中 → 2.获取互斥锁成功 → 3.查询数据库重建缓存数据 → 4.写入缓存 → 5.释放锁

    1.查询缓存,未命中 → 2.获取互斥锁失败 → 3.休眠一会再重试 → 4.写入缓存重试 → 5.缓存命中

  • 解决方案二逻辑过期[ 不设置过期时间 ] 【高可用 性能优 不能保证数据绝对一致 (用户体验)】

    在数据库一条数据里面添加一个 “expire”: 153213455

    1.查询缓存,发现逻辑时间已过期 → 2.获取互斥锁成功 → 3.开启线程 ↓→ 4.返回过期数据

    ​ 【在新的线程】→ 1.查询数据库重建缓存数据 → 2.写入缓存,重置逻辑过期时间 → 3.释放锁
    1.查询数据缓存,发现逻辑时间已过期 → 2.获取互斥锁失败 → 3.返回过期数据

==缓存雪崩==:在同一个时段内大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来压力

  • 解决方案一:给不同的key的TTL(过期时间)添加随机值不在同一时间过期
  • 解决方案二:利用Redis集群提高服务的可用性 【哨兵模式、集群模式】
  • 解决方案三:给缓存业务添加降级限流策略【nginx、springcloud、gateway】
  • 解决方案四:给业务添加多级缓存 【Guava(做一级缓存 然后Redis是二级缓存)或Caffeine】
★ redis宕机的时候 再RedisTemplate.set()后会报错 但是现在mysql还可以访问 应该怎么办?

try catch 在里面继续再去查mysql数据库

降级代码:对于读操作,如果Redis缓存失效,可以直接从MySQL数据库读取数据。
public boolean setData(String key, String value) {
    try {
        // 尝试将数据设置到Redis
        redisTemplate.opsForValue().set(key, value);
        return true;
    } catch (Exception e) {
        // 日志记录Redis错误
        log.error("Redis is down, failing over to MySQL", e);
        
        // Redis设置失败,降级到MySQL
        return setDataToMySQL(key, value);
    }
}

private boolean setDataToMySQL(String key, String value) {
    // 这里实现将数据写入MySQL的逻辑
    // 例如:
    // mySqlTemplate.update("INSERT INTO cache (key, value) VALUES (?, ?)", key, value);
    // 注意:这里的mySqlTemplate和SQL语句需要根据实际情况调整
    return true; // 假设写入成功
}
如果公司对要求更高,需要限流降级、熔断

同一时间设置QPS为100 超过的返回友好提示[商品太火爆啦,请稍后再试]

你有自己部署过环境吗

公司里面用 Jenkins + docker 测试环境我们部署 生产环境是组长部署

未来1-3年规划

将具体一点,从业务技术上提升自己的深度和广度达到高级工程师

你平时做笔记吗

有做笔记 Xmind + Markdown
因为我觉得无论从网上的还是别人请教的不经历我的消化都不是我的东西
我还是会将这些知识点总结起来变成自己的知识

什么是动态代理?&& 动态代理有哪些,他们之间的区别?

代理是一种设计模式 用来增强目标的逻辑 与被增强的并没有太大关系装饰者模式

在程序运行期间才会产生代理类加载到我们jvm中yaml文件

  • JDK动态代理是 基于接口实现来实现增强

    [txt文本 把目标增强类 作为接口本身就是接口 实现过来写成源码 源文件 再用jdk工具把源码编译成class字节码 再用类加载器把class加载到jvm中]

  • CGLIB动态代理是 基于继承目标类并覆写其方法来实现

    [ASN字节码机制直接生成class 直接加载到内存中]性能较高,速度更快。因为直接生成class

要调用某个方法 CGLIB性能高 是通过反射来实现的 老版本的jdk的反射性能较低。如今在调用方法的性能上差距不大

区别

  • JDK动态代理要求目标类必须实现一个或多个接口,而CGLIB没有这个要求。
  • JDK动态代理生成的代理类是接口的实现,而CGLIB生成的代理类是目标类的子类。
  • 性能上,CGLIB通常比JDK动态代理更快,因为它直接操作字节码生成新的类。

什么样的代码是静态代理?

发生在我们写代码的过程中 在编译阶段产生了代理类
静态代理是指代理类在编译时就已经确定,通常由程序员手动编写

你用过Linux吗?

是的,我在工作中经常使用Linux操作系统。我熟悉Linux的基本命令
基础的命令:xxx【查看之前笔记】

你工作的时候有需求文档吗?

有的,有一些简单的需求是没有的[沟通成本太高了]
稍微复杂的需求会有需求文档,我会根据需求文档来理解项目需求,并进行系统设计和开发。

你有什么需要了解的?不要难为面试官,不问技术栈

我想了解一下贵公司的业务是什么…好的那我这块已经没有什么想了解的了 感谢面试官
HR:想了解一下贵公司的上班时间…
我没有什么想了解的,来之前有了解过贵公司

平时用注解创建的bean是单例的还是多例的?

默认情况下,通过注解(如@Component、@Service、@Repository、@Bean等)创建的Bean是单例的。如果需要创建多例Bean,可以在注解上添加@Scope(“prototype”)来指定。

// 单例Bean
import org.springframework.stereotype.Component;

@Component
public class SingletonBean {
    // Bean的代码
}

----------------------------------------------------
    
// 多例Bean

import org.springframework.context.annotation.Scope;
import org.springframework.stereotype.Component;

@Component
@Scope("prototype")
public class PrototypeBean {
    // Bean的代码
}

SQL语句的执行顺序,为什么顺序是这样排的,这样的顺序有什么优势或者好处?

FROM -> Join -> WHERE -> GROUP BY -> HAVING -> SELECT -> ORDER BY -> limit
这样的顺序是为了优化查询性能。首先确定数据来源(FROM),然后筛选出满足条件的数据(WHERE),接着进行分组(GROUP BY),在分组的基础上进行进一步筛选(HAVING),然后选择需要的数据(SELECT),最后对结果进行排序(ORDER BY)。这样的顺序可以减少中间结果集的大小,提高查询效率。

书写顺序

select -> from -> join -> on -> where -> group by -> having -> order by -> limit

线上项目发生死锁如何去解决? 我暂时没遇到过→分布式事务上去答

死锁:两个线程争夺两个资源的时候 1线程拿到a 想拿b 2线程拿到了b 想拿a
四个原因互斥条件 请求保持 不可剥夺 循环等待
产生死锁的四个因素 同时满足才会死锁 想要解决死锁 需要打破其中一个原因就行

  1. 互斥条件(Mutual Exclusion):资源不能被多个线程同时使用。即某个资源在一段时间内只能由一个线程占用,其他线程必须等待该资源被释放后才能使用。
  2. 持有和等待条件(Hold and Wait):线程至少持有一个资源,并且正在等待获取额外的资源,而该资源又被其他线程持有。
  3. 非抢占条件(No Preemption):已经分配给某个线程的资源在该线程完成任务前不能被抢占,即只能由线程自己释放。
  4. 循环等待条件(Circular Wait):存在一种线程资源的循环等待链,每个线程都在等待下一个线程所持有的资源。

在实际操作中,以下是一些打破死锁的具体方法:

  • 资源分配图:使用资源分配图来检测循环等待条件,并在检测到循环时采取措施。
  • 锁排序:确保所有线程以相同的顺序获取锁,从而避免循环等待。
  • 超时机制:线程在请求资源时设置超时时间,如果超过时间未获得资源,则放弃当前任务并释放已持有的资源。
  • 死锁检测算法:运行死锁检测算法,如银行家算法,来检测系统中的死锁,并在必要时采取措施。
  • 线程中断:允许系统或其他线程中断正在等待资源的线程。
  • 回滚操作:如果检测到死锁,可以让某些线程回滚它们的工作,并释放资源,从而打破死锁。

MySQL是不会有死锁的 自身会检测 [让后面的超时释放回滚]
在分布式事务 线程1拿着资源a是数据库1 线程2拿着资源b是数据库2
JVM中也有死锁,jvm没有超时机制不会解决 可以查看命令打印堆栈信息可以查看哪里产生死锁

你可以使用jstack命令来打印指定进程ID的Java堆栈跟踪信息。这个命令可以帮助你分析线程的状态

  1. 首先,找到你的Java进程ID(PID)。你可以使用jps命令来列出所有正在运行的Java进程及其PID。

    jps
    
  2. 使用jstack命令打印出该Java进程的堆栈跟踪。

    jstack <PID>
    

    <PID>替换为实际的进程ID。

  3. 查找堆栈跟踪中的”DEADLOCK”关键字。jstack会自动检测死锁并在输出中报告。

如果你遇到(新的)技术栈,怎么去解决?

【return Previous.notes(NowDay);】

如果你在实际开发中遇到问题,你怎么去解决,怎么去沟通?

首先尝试自己解决问题,通过搜索引擎、官方文档、Debug等。
尽可能不让这个问题不出现风险 实在解决不了就向上反馈 寻求帮助 请教上司领导或同事
平常和项目经理进行沟通 如果需求评审有些不理解还是会及时沟通 不清楚的一定要及时明确

对于加班情况怎么看?

为了确保项目进度和团队利益,加班是可以接受的。

多线程怎么保证线程之间的安全

加锁 不让多线程抢夺资源

互斥锁、读写锁、线程局部存储(ThreadLocal每个线程独享自己变量)

mybatis中${}和#{}的区别,哪个更好? 为什么?

  1. ${}(字符串替换):
    • ${}会将参数直接替换到SQL语句中,不进行任何转义处理。
    • 它适用于动态SQL中的表名或列名,或者在SQL语句中需要使用特定数据库函数的情况。
    • 使用${}时,如果参数是用户输入的,那么可能会引发SQL注入攻击,因为它不会对参数进行转义。
  2. #{}(预处理语句参数):
    • #{}会创建预处理语句(prepared statement)的参数占位符,并在设置参数时进行适当的转义处理。
    • 它适用于大部分情况,特别是当参数是用户输入时,可以有效防止SQL注入攻击。
    • MyBatis会根据参数的类型自动选择setStringsetIntsetDate等预处理语句方法。
  3. 在大多数情况下,#{}是更好的选择,因为它提供了以下优势:
    • 安全性#{}可以防止SQL注入攻击,因为它会自动转义参数。
    • 类型处理:MyBatis会根据参数的实际类型来设置预处理语句的参数,这减少了类型错误的可能性。
    • 可读性和维护性:使用#{}可以使SQL语句更加清晰,因为它清楚地标识了参数的位置。

    然而,在某些特定的场景下,如需要动态地指定表名或列名时,${}是必要的,因为预处理语句

说一下内连接和外连接的区别

左外连接(Left Outer Join)

  • 定义:左外连接返回左表中的所有行,即使在右表中没有匹配的行。对于左表中没有匹配的行,结果集中的右表部分将包含NULL。
  • 如果左表是主表,或者左表中的数据是查询的主要关注点,而右表中的数据是辅助信息时,通常使用左外连接。

右外连接(Right Outer Join)

  • 定义:右外连接返回右表中的所有行,即使在左表中没有匹配的行。对于右表中没有匹配的行,结果集中的左表部分将包含NULL。
  • 如果右表是主表,或者右表中的数据是查询的主要关注点,而左表中的数据是辅助信息时,通常使用右外连接

全外连接(Full Outer Join)

  • 定义:全外连接返回左表和右表中的所有行。当某行在另一个表中没有匹配时,结果集中的相应部分将包含NULL。
  • 全外连接不常用,因为它通常会返回大量的包含NULL的数据,这可能会导致查询结果难以解释。只有在确实需要两表中的所有数据时才使用。

  • 性能考虑:外连接可能会比内连接(Inner Join)更消耗资源,特别是当表很大时。如果可能,尽量使用内连接。
  • 数据完整性:如果业务逻辑要求查询结果必须包含某个表的所有记录,那么应该使用相应的外连接。

自我介绍

xxx

你觉得学习我们这些技术最重要的是什么?

首先要清楚**这个技术是解决什么领域的问题**,学习技术很多方面都是用来服务业务的,结合实际业务来学习技术融合性会更强

技术栈有些不同,有没有想过换方向发展?

没问题的 因为技术是相通的 可以去学新技术

ThreadLocal相关面试题

1.概述

ThreadLocal(定义全局静态变量 项目中共用)是Java中的一个线程局部变量工具类,它提供了一种在多线程环境下,每个线程都可以独立访问自己的变量副本的机制。ThreadLocal中存储的数据对于每个线程来说都是独立的,互不干扰。

2. 使用场景

ThreadLocal适用于以下场景:

  • 在多线程环境下,需要保持线程安全性的数据访问。
  • 需要在多个方法之间共享数据,但又不希望使用传递参数的方式。
    • 在传递登录用户id是非常方便且适用

以后获取用户id不用再解析token了,线程拿仅仅拿当前线程的数据 每个登录的用户都有自己的threadlocal数据

ThreadLocal并不是一个Thread,而是Thread的局部变量【可以存储数据】
ThreadLocal为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获取到对应的值,线程外则不能访问。**ThreadLocal实现一个线程内传递数据**[就不用一个个参数往后传递了]
注意:客户端发送的每次请求,后端的tomcat服务器都会分配一个单独的线程来处理请求

  • 一个请求是一个线程[意义:在第一个线程里面使用ThreadLocal存储用户Id 在后面controller或service中就可以取出来用户id]
  • 第二个请求就是另一个线程 线程池用完第一个放回线程池 也有可能把上一个线程接着给它用

postHandle 只有在正确调用返回才会引用 如果抛出异常则不会使用
afterCompletion 无论怎样最后都要运行

ThreadLocal

3.1 创建ThreadLocal对象

首先,我们需要创建一个ThreadLocal对象来存储线程局部变量。可以使用ThreadLocal的默认构造函数创建一个新的实例。【给每个线程拷贝一份 synchn + Lock锁】

ThreadLocal<String> threadLocal = new ThreadLocal<>();
3.2 设置线程局部变量的值

使用set()方法可以设置当前线程的局部变量的值。

threadLocal.set("value");
3.3 获取线程局部变量的值

使用get()方法可以获取当前线程的局部变量的值。

String value = threadLocal.get();
3.4 清除线程局部变量的值

使用remove()方法可以清除当前线程的局部变量的值,建议在整个请求使用完一定要执行remove清除数据,不然可能会发生内存泄漏问题。

threadLocal.remove();
下面是一个简单的示例代码,演示了如何使用ThreadLocal。
public class ThreadLocalTest {

    private static final ThreadLocal THREAD_LOCAL = new ThreadLocal();

    public static void main(String[] args) {

        Thread t1 = new Thread(() -> {
            THREAD_LOCAL.set("itheima");
            getData("t1");
        }, "t1");

        Thread t2 = new Thread(() -> {
            THREAD_LOCAL.set("itcast");
            getData("t2");
        }, "t1");

        t1.start();
        t2.start();

    }

    private static void getData(String threadName){
        Object data = THREAD_LOCAL.get();
        System.out.println(threadName+"-"+data);
    }
}

运行以上代码,输出结果为:

t1-itheima
t2-itcast

在任意位置都可以调用Threadlocal,线程隔离互不影响,解决了线程安全问题:[每个线程存一份 线程不共享]

用mybatis+建造者模式 一定要在类里面加 一定要具有有参和无参构造缺一不可 否则mybatis数据封装无法映射
@AllArgsConstructor
@NoArgsConstructor
@Builder

字符和字符串类型的区别

字符是基本数据类型 没有null 用单引号
字符串是引用数据类型[一个类] 用双引号

image-20250102142357092

操作字符串的工具类是什么

apache的common提供的String Utils工具类,hutool也有String Utils

局部变量和成员变量的区别

成员变量是在jvm的
局部变量是在jvm的
基本数据类型的引用类型 类在堆
基本数据类型的成员变量在堆 非静态在堆 静态变量在方法区
引用类型 无论静态还是非静态成员变量都在堆区

局部变量是在方法或代码块内部声明的变量,其作用域仅限于声明它的方法或代码块
局部变量不能被static修饰
局部变量必须被初始化才能使用

成员变量是在类内部声明的变量,其作用域是整个类
成员变量可以被static修饰
成员变量有默认值

你编写完代码,写完这个功能后,会进行什么操作呢

进行代码审查,检查代码是否符合编码规范和设计要求。

  • 进行单元测试,确保代码的功能正确无误。
  • 进行代码优化,提高代码的性能和可维护性。
  • 与团队成员进行代码合并,确保代码的集成。
  • 编写文档,记录功能实现和代码变更。

上一家公司的薪资是多少?期望薪资是多少?上一家还有什么其他的福利吗?

期望薪资:
了解过广东这边的市场 我想换工作想涨薪10~20%
节假日会发放礼品和福利

可以接受低代码平台吗?

可以接受
低代码平台:类若依

具体说说Java面向对象

Java面向对象是一种编程范式,它将现实世界的事物抽象成程序中的对象。Java面向对象的主要特征包括:

  • 封装:将对象的属性行为封装在一起,对外只暴露必要的接口,隐藏内部实现细节。
  • 继承:允许子类继承父类的属性和行为,实现代码的复用。
  • 多态同一个接口可以有多个不同的实现,通过对象的类型和方法的调用,实现不同的功能。

== 和 equals 的区别

  • ==:比较基本数据类型时,比较的是;比较引用数据类型时,比较的是对象的内存地址
  • equals:是Object类的一个方法,默认比较的是对象的内存地址。但在很多类中(如String、Integer等),equals方法被重写,用于比较对象的内容是否相等。
    没重写 就是 == 比较对象地址。重写过的话就比较对象的值。

有没有做过权限控制,整个系统的权限

有过 SpringSecurity

能具体说一下权限控制怎么做?

使用RBAC模型 不是把用户关联资源 而是中间利用角色间接关联
用户+角色+资源+用户角色中间表+角色资源中间表多对多

SpringSecurity 具体怎么实现

我的项目是基于JWT的前后端分离的项目,在自定义认证管理器AuthenticationManager认证成功后,生成JWT令牌并返回给前端。前端在随后的请求中携带这个JWT令牌。这时候,我们使用AccessDecisionManager来实现接口的鉴权逻辑,其中包括一个check方法,该方法会校验JWT令牌的有效性。如果校验通过,就去查询数据库以确定用户拥有哪些权限。在用户登录时,其权限信息已经被缓存到Redis中。后续的请求中,我们可以直接从Redis中检索用户的权限信息。如果请求的接口权限与用户缓存中的权限匹配,则放行;如果不匹配,则返回一个友好的错误信息。

线程池有哪些状态,这些状态是怎么进行转换的

线程池有以下几种状态:

  • RUNNING:线程池正常运行,可以接受新的任务和处理任务队列中的任务。
  • SHUTDOWN:线程池不再接受新的任务,但会处理任务队列中的任务。
  • STOP:线程池不再接受新的任务,也不处理任务队列中的任务,并且会中断正在执行的任务。
  • TIDYING:所有任务都已终止,线程池即将关闭。
  • TERMINATED:线程池已关闭。

状态转换过程如下:

  • RUNNING -> SHUTDOWN:调用shutdown()方法。
  • RUNNING -> STOP:调用shutdownNow()方法。
  • SHUTDOWN -> TIDYING:当线程池和任务队列都为空时。
  • STOP -> TIDYING:当线程池为空时。
  • TIDYING -> TERMINATED:当terminated()钩子方法执行完成后。

说一下怎么使用多线程?

  • 继承Thread类,并重写run()方法。

  • 实现Runnable接口,并将实现类传递给Thread对象。

  • 实现Callable接口,实现call()方法

  • 使用Executor框架,如ExecutorService和ThreadPoolExecutor来管理线程池。

操作系统上的线程有多少种状态[5]?Java线程有多少种状态[6]?

  • 新建(New):创建后尚未启动的线程处于这个状态。new Thread
  • 可运行(Runnable):包括运行(Running)和就绪(Ready)状态,线程正在执行或等待CPU调度。
  • 阻塞(Blocked):线程因为等待某些资源或锁而被阻塞。notify可以唤醒阻塞状态 睡眠完会自动唤醒
  • 等待(Waiting):线程等待其他线程执行特定操作(如通知)。
  • 计时等待(Timed Waiting):线程在一定时间内等待另一个线程的通知。
  • 终止(Terminated):线程执行完成或因异常而终止。
怎么把线程杀死 终止

stop()方法[暴力方法] interrupt()方法[优雅关闭线程] 正常回收

乐观锁和悲观锁的区别

乐观锁:读多写少 线程执行时间相差较大 并发不太激烈

悲观锁:写多读少 线程执行时间相差不大 竞争激烈 并发锁多

加锁的时机不一样,
悲观锁:没改数据的时候先加锁 比较明显利用底层操作系统api实现
乐观锁:在改数据的时候才加锁 依靠底层的硬件

java层面synchronized ReentrantLock

数据库层面
悲观锁:select for update是mysql的的实现
乐观锁:JUC Java Util Concurrent)是Java并发工具包

SELECT ... FOR UPDATE:这个语句在读取记录时会锁定这些记录,直到事务提交或回滚。其他的事务不能更新这些锁定的记录,这是悲观锁的一个典型实现

乐观锁要读取目前旧的值再将新设置的值以及旧的值比较 如果相同 就把新的值更新 如果不相同 就把旧的值重新提取 因为在这期间有人读取了这个数据跟我之前不一样(底层api 要调用两个 一个旧的值 一个新的值)。一般乐观锁是结合自旋 类于while(true)直到读到为止 要设计次数后再报错

要更新数据库某个值 把旧的值读出来 想更新银行里的余额
这是典型的ABA问题要用时间戳自增版本号去做

Stream流的使用及常用API

Stream是Java 8中引入的一种新特性,用于简化数据处理和操作。它可以用来解决集合循环遍历处理的问题。在此之前用循环来代替

基础Stream操作

  • stream(): 为集合创建串行流。
  • parallelStream(): 为集合创建并行流。
  • forEach: 对每个元素执行操作。
  • map: 将每个元素映射到对应的结果。
  • filter: 过滤出满足条件的元素。
  • limit: 限制流的大小。
  • skip: 跳过流中的前n个元素。
  • sorted: 对流进行排序。

终端操作

  • collect: 将流转换为其他形式,比如列表、集合或Map。
  • reduce: 通过一个起始值,反复利用BinaryOperator来处理和累积元素,返回一个值。
  • count: 返回流中元素的数量。
  • min / max: 找到流中的最小/最大值。
  • anyMatch: 流中是否有一个元素匹配给定的谓词。
  • allMatch: 流中的所有元素是否都匹配给定的谓词。
  • noneMatch: 流中没有任何元素匹配给定的谓词。
  • findFirst: 返回第一个元素。
  • findAny: 返回当前流中的任意元素。

项目中具体用到哪些设计模式

单例模式:确保一个类只有一个实例,例如配置文件管理器。[Spring原本设计好的]
工厂模式:创建对象时无需指定具体的类,例如日志工厂。
观察者模式:当一个对象状态发生改变时,
所有依赖于它的对象
都得到通知并自动更新,例如事件监听。
**策略模式**:定义一系列算法,将每个算法封装起来,并使它们可以互换,例如支付策略。
模板方法模式:在项目中,我有一些具有相同操作步骤但具体实现不同的算法,我使用了模板方法模式来定义这些步骤的骨架,将具体的步骤实现留给子类。任链模式的目的是将请求的发送者和接收者解耦,从而使得多个对象都有机会处理请求,将这些对象连成一条链,并沿着这条链传递请求,直到有一个对象处理它为止。
责任链模式:它允许将请求沿着处理者链进行发送。收到请求后,每个处理者都有机会对请求进行处理,或者将其传递给链上的下一个处理者。这样,请求就能在一系列处理者中传递,直到有一个处理者对其进行处理为止。

1.递归方式 :在递归模式中,每个处理者内部调用下一个处理者的处理方法。如果当前处理者无法处理请求,它会直接调用下一个处理者的处理方法。这种方式通常是通过递归调用来实现的
2.迭代模式:在迭代模式中,处理者链被构建为一个线性结构,每个处理者都有一个指向下一个处理者的引用。请求从链的第一个处理者开始,依次传递给下一个处理者,直到找到能够处理该请求的处理者为止。这种方式通常是通过循环迭代来实现的
代理模式:为了控制对远程服务的访问,我使用了代理模式。代理负责处理所有与服务对象的交互,并在必要时进行延迟加载。

在我的项目中结合工厂模式策略模式来设计登录接口时,我们可以将登录验证的逻辑抽象为一个策略接口,并为每种登录方式(如:用户名密码登录、手机验证码登录、社交账号登录等)实现具体的策略类。工厂类则负责创建并管理这些策略对象

思考一个问题:哪些方式创建单例模式?

1. 懒汉式,线程不安全

这种方式在类加载时不初始化。在需要的时候才创建对象,节约资源。

public class Singleton {
    private static Singleton instance;
    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

2. 懒汉式,线程安全

通过同步方法确保线程安全。

public class Singleton {
    private static Singleton instance;
    private Singleton() {}

    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

3. 饿汉式

类加载时就完成了初始化,保证了线程的安全性。容易浪费资源

public class Singleton {
    private static Singleton instance = new Singleton();
    private Singleton() {}

    public static Singleton getInstance() {
        return instance;
    }
}

4. 双重校验锁

线程安全且在实例域需要延迟加载时提高性能。

public class Singleton {
    private volatile static Singleton singleton;
    private Singleton() {}

    public static Singleton getInstance() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

5. 静态内部类

这种方式既实现了懒加载,又保证了线性安全。

public class Singleton {
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    private Singleton() {}

    public static final Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

6. 枚举

实现单例的最佳方法,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象。

public enum Singleton {
    INSTANCE;
    public void whateverMethod() {
    }
}

MySQL支持四种隔离级别

  • 第一个读未提交(readuncomm itted)它解决不了刚才提出的所有问题,一般项目中也不用这个。存在脏读问题 可解决不可重复读 幻读
    **不可解决脏读**:读未提交允许一个事务读取另一个事务尚未提交的数据变更。如果一个事务读取了另一个事务的未提交数据,然后那个事务回滚,则第一个事务读取的数据就是无效的(脏数据)

  • 第二个读已提交(readcomm ited)它能解决脏读的问题的,但是解决不了不可重复读和幻读。
    **解决脏独**:读已提交确保一个事务只能读取已经提交的数据变更。如果一个事务正在修改某些数据,那么在它提交之前,其他事务不能读取这些数据。这样就可以避免脏读。
    **不可解决不可重复读**:一个事务在读取某些数据后,另一个事务修改了这些数据并提交,导致第一个事务再次读取时得到不同的结果
    **不可解决幻读**:一个事务在读取某个范围的数据后,另一个事务插入了一些新数据并提交,导致第一个事务在执行范围查询时看到了新插入的数据

  • 第三个可重复读(repeatable read)它能解决脏读和不可重复读,但是解决不了幻读[解决了一部分],这个也是mysql默认的隔离级别。

  • 第四个串行化(serializable)它可以解决刚才提出来的所有问题,但是由于让是事务串行执行的,性能比较低。
    串行化里的读也要加锁 表锁:整个表上锁 行锁:只对一行加锁
    串行化是最严格的事务隔离级别。它通过强制事务串行执行来避免上述所有问题。在一个事务执行时,它会锁定涉及的所有数据行或表,直到事务完成。这确保了事务完全隔离,但是会显著降低系统的并发性能

什么时候上行锁/表锁?

INSERT不带查询筛选条件 上行锁底层是索引,b+树底层叶子

  • 行锁:通常情况下,插入操作会锁定插入行所在的索引项,以防止其他事务同时修改同一行。这是因为数据库通常使用B+树来维护索引,插入操作需要在B+树中找到正确的位置来插入新的索引项。如果插入操作涉及到唯一索引,数据库还会检查是否有重复的键值,这也会触发行锁。
  • 注意:即使插入操作没有查询筛选条件,它仍然可能涉及到行锁,因为数据库需要保证新插入的数据不会与现有数据冲突。

UPDATE看where后面的条件 带索引加行锁构建b+树 不带索引的加表锁
表锁的速度比行锁速度快

带索引的条件:
  • 行锁:如果更新操作的条件是索引列,数据库能够快速定位到需要更新的行,因此只会锁定那些特定的行。行锁可以最大程度地减少锁定的数据量,从而提高并发性能。
  • 原理:数据库使用B+树索引来快速查找满足条件的行,然后对这些行加锁。
不带索引的条件:
  • 表锁:如果更新操作的条件不是索引列,数据库可能需要扫描整个表来找到需要更新的行。在这种情况下,为了简化锁定逻辑并防止在扫描过程中数据被修改,数据库可能会选择锁定整个表。
  • 原理:由于没有索引可以利用,数据库必须检查每一行来确定是否满足更新条件,因此使用表锁可以避免复杂的锁定管理。

MVCC底层是多版本并发控制 但底层并不怎么了解

深拷贝和浅拷贝的区别?

浅拷贝:只复制对象的基本数据类型引用类型地址,不复制引用类型指向的对象。如果原对象和浅拷贝对象中的一个改变了引用类型,另一个也会受到影响。旧对象改变新对象也会改变。
深拷贝:创建一个新的对象,并复制对象的所有字段,包括基本数据类型和引用类型指向的对象。原对象和深拷贝对象之间不会相互影响。旧对象改变新对象不会改变
Java是值传递

如何实现深拷贝?数组不需要重写【体现了原型设计模式
  • 实现Cloneable接口并重写clone方法 会调用构造方法

    这是最常见的实现深拷贝的方法。首先,你的类需要实现Cloneable接口,然后重写clone()方法构造新对象的过程,并在该方法中调用super.clone(),同时递归地克隆所有引用类型的字段。[如果里面有多层嵌套复杂对象 在每层都要实现Cloneable接口一直重写到基本数据类型的时候才停止]

public class Person implements Cloneable {
    private int age;
    private Address address;

    // 构造器、getter、setter 省略

    @Override
    protected Object clone() throws CloneNotSupportedException {
        Person cloned = (Person) super.clone();
        // 假设Address也实现了Cloneable接口
        cloned.address = (Address) this.address.clone(); 
        return cloned;
    }
}

public class Address implements Cloneable {
    private String street;
    private String city;

    // 构造器、getter、setter 省略

    @Override
    protected Object clone() throws CloneNotSupportedException {
        return super.clone();
    }
}
  • 通过序列化对象转二进制反序列化二进制转对象。这种方式不需要实现Cloneable接口,但你的类需要实现Serializable接口。反序列化不会调用构造方法

开启线程的时候为什么用的是thread.start方法:

thread.start()方法用于启动一个新线程,并执行该线程的run()方法。调用start()方法后,线程会被放入线程调度队列,等待CPU调度执行。

直接调用run()方法,并不会启动一个新线程,而是在当前线程中执行run()方法,这不符合多线程编程的目的。使用start()方法可以确保线程并发执行,提高程序的性能和响应速度。

java没权限开启一个线程 要调用底层的操作系统 在JVM的底层实现中,会有相应的本地(C或C++)方法来处理线程的创建和管理

你在你们项目中使用过多线程吗?

是的,在我们的项目中,我确实使用过多线程。 【结合项目去说】
在处理大量数据计算或执行耗时的IO操作时,我会使用Java的线程池(如ExecutorService)来并行处理任务,以提高系统的响应速度和吞吐量
image-20250107153246645

我们将使用多线程来处理一个在线电子商务平台的后台订单处理系统
项目需求

该系统需要处理大量的订单,包括订单验证、库存检查、支付处理和订单状态更新。为了提高处理效率,我们决定使用多线程来并行处理订单。

import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.concurrent.TimeUnit;
import java.util.Random;

public class OrderProcessingSystem {

    public static void main(String[] args) {
        // 创建一个固定大小的线程池
        int numberOfProcessors = Runtime.getRuntime().availableProcessors();
        ExecutorService executor = Executors.newFixedThreadPool(numberOfProcessors);

        // 模拟订单队列
        Random random = new Random();
        for (int orderId = 1; orderId <= 100; orderId++) {
            int finalOrderId = orderId;
            executor.submit(() -> {
                processOrder(finalOrderId, random.nextInt(1000));
            });
        }

        // 关闭线程池
        executor.shutdown();
        try {
            if (!executor.awaitTermination(60, TimeUnit.SECONDS)) {
                executor.shutdownNow();
            }
        } catch (InterruptedException e) {
            executor.shutdownNow();
            Thread.currentThread().interrupt();
        }
    }

    private static void processOrder(int orderId, int orderAmount) {
        System.out.println("Processing order ID: " + orderId + " - Thread: " + Thread.currentThread().getName());
        
        // 模拟订单验证
        validateOrder(orderId);
        
        // 模拟库存检查
        checkInventory(orderId);
        
        // 模拟支付处理
        processPayment(orderId, orderAmount);
        
        // 更新订单状态
        updateOrderStatus(orderId, "Completed");
    }

    private static void validateOrder(int orderId) {
        // 模拟订单验证逻辑
        try {
            Thread.sleep(100);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        System.out.println("Order ID " + orderId + " validated.");
    }

    private static void checkInventory(int orderId) {
        // 模拟库存检查逻辑
        try {
            Thread.sleep(200);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        System.out.println("Inventory checked for Order ID " + orderId + ".");
    }

    private static void processPayment(int orderId, int orderAmount) {
        // 模拟支付处理逻辑
        try {
            Thread.sleep(300);
        } catch (InterruptedException e) {
            Thread.currentThread().interrupt();
        }
        System.out.println("Payment processed for Order ID " + orderId + " - Amount: " + orderAmount);
    }

    private static void updateOrderStatus(int orderId, String status) {
        // 模拟订单状态更新逻辑
        System.out.println("Order ID " + orderId + " status updated to " + status + ".");
    }
}

在这个示例中,我们创建了一个固定大小的线程池,其大小等于可用处理器的数量。然后,我们模拟了一个包含100个订单的队列,并为每个订单提交了一个任务到线程池中。每个任务代表订单处理的整个流程,包括验证、库存检查、支付处理和状态更新。每个步骤都通过休眠来模拟耗时操作。最后,我们关闭线程池并等待所有任务完成。

sleep和wait的区别

sleep是Thread类的一个静态方法,它使当前线程暂停执行指定的时间,但不会释放锁资源。
wait是Object类的一个方法,它使当前线程暂停执行释放当前对象上的锁,直到另一个线程调用同一个对象的notify()notifyAll()方法,或者过了指定的等待时间。

sleep是线程内的静态方法 需要指定睡眠的时间 或者自动自己唤醒 不会释放锁
wait是Object类的一个方法 可以指定睡眠时间 不指定就等于无限期 要释放锁
wait一定要搭配synchronized,且都为同一个对象 synchronized锁住了wait万物对象皆为锁
可以被唤醒notify()notifyAll()方法 区别:notify是随机唤醒一个 notifyall会唤醒全部

普通方法上 锁的是this
静态方法上 锁的是当前类的class对象

ConcurrentHashMap 和 HashTable的区别

是否支持传入NULL

HashMap可以支持为null
若尝试将 null 作为键或值放入 ConcurrentHashMap 将会抛出 NullPointerException
ConcurrentHashMap 不能支持存null

底层实现

ConcurrentHashMap 1.8之前是分段锁来实现 默认是16个HashTable
1.8之后无限接近单个的HashMap 底层用CAS+synchronized
HashTable通过加synchronized锁来控制线程安全

ConcurrentHashMap 读不要加锁 [读写的读也不会加锁] 会走最终一致性
HashTable 读要加锁 [读读都加锁]

为什么要用Redis

高性能:Redis是基于内存的数据结构存储,可以提供高速的数据读写操作。
数据结构丰富:Redis支持多种数据结构,如字符串、列表、集合、散列表、有序集合等,非常适合各种场景。
持久化:Redis支持数据持久化,可以将内存中的数据保存到磁盘中,防止数据丢失。
分布式:Redis支持主从复制哨兵集群模式,可以轻松实现分布式缓存。

Redis中缓存了哪些数据

可以存储一下类型的数据
  1. 会话缓存(Session Store):用户会话信息,如用户登录状态、用户偏好设置等。
  2. 页面缓存:动态生成的网页内容,以减少数据库的读取次数。
  3. 对象缓存:例如,用户信息、商品详情等,减少数据库访问。
  4. 消息队列:用作消息队列,处理异步任务。
  5. 排行榜或计数器:如用户点赞数、视频播放次数等。
  6. 地理空间数据:用于实现基于地理位置的查询。
  7. 分布式锁:在分布式系统中协调不同服务或节点的操作
关于您提到的替代JWT的方案,即使用Redis来管理登录状态而不是使用JWT,这里有一些详细说明:

若放登录的信息到Redis的时候 不再用JWT了
Session在集群里面不能用了
替代方案:用Redis 不用JWT
JWT是无状态 无需集中存储

在我们的项目中,Redis中缓存了以下类型的数据:
会话信息:如用户登录信息、购物车内容等。
热点数据:如热门商品信息、推荐内容等。
计数器:如用户访问次数、点赞数、评论数等。
缓存数据库查询结果:减少数据库访问次数,提高系统响应速度。

JWT(JSON Web Tokens)

JWT是一种在各方之间传递安全可靠信息的简洁的、URL安全的表达方式。其特点包括:

  • 无状态:服务器不存储任何会话信息,每个请求都携带包含所有必要信息的JWT。
  • 自包含:JWT中包含了用户的所有声明,减少了服务器的数据库查询。
  • 跨域认证:特别适用于单点登录(SSO)。

JWT的局限性

  • 无法失效:一旦签发了JWT,在它过期之前,它在任何地方都是有效的,无法提前失效。
  • 续签问题:JWT过期后需要重新签发,处理起来相对复杂。
  • 携带信息量大:每个请求都携带JWT,如果JWT中包含的信息较多,会增加请求的大小。

使用Redis替代JWT

使用Redis作为会话存储,可以解决JWT的一些问题:

  1. 中心化控制:通过Redis,服务器可以集中管理会话信息,可以随时使会话失效。
  2. 灵活的过期策略:可以设置更细粒度的过期时间,并在需要时刷新会话。
  3. 状态管理:对于需要频繁更改用户状态的应用,使用Redis可以更方便地管理。
  4. 安全性:虽然Redis存储会话信息,但可以结合HTTPS和合适的加密策略来保证传输过程的安全

实现方案

  1. 用户登录:用户登录成功后,生成一个唯一的会话标识(如UUID),并将其作为key存储在Redis中,value可以是用户ID或者其他必要信息,并设置适当的过期时间。
  2. 请求验证:用户每次请求时,需要在请求头中携带会话标识,服务器端通过这个标识在Redis中查找会话信息,进行验证。
  3. 会话过期或失效:当用户登出或会话过期时,从Redis中删除对应的会话信息。

注意事项

  • 数据持久性:Redis的数据是存储在内存中的,需要考虑持久化策略以防止数据丢失。
  • 高可用性:在集群环境下,需要配置Redis的高可用方案,如哨兵(Sentinel)或集群模式。
  • 安全性:确保Redis的安全性,防止未授权访问。

检测数据存在Redis中,有过期时间吗? 过期时间是多少?仅参考

是的,我们在Redis中缓存的数据通常会设置过期时间,以避免过时的数据占用内存。具体的过期时间取决于数据的类型和业务需求。对于会话信息,我们可能会设置较短的过期时间,如30分钟或1小时;而对于热点数据,可能会设置较长的过期时间,如几小时或一天。具体的过期时间需要根据实际业务场景和数据访问模式来决定。

   // 用户登录,创建会话
    public String loginUser(String userId) {
        String sessionId = UUID.randomUUID().toString();
        String sessionData = createSessionData(userId);
        jedis.setex(sessionId, 1800, sessionData); // 设置会话过期时间为30分钟
        return sessionId;
    }

微服务之间如何调用?

通过注册中心去协调的
首先是有三个重要的概念,服务消费者注册中心服务提供者提供者在第一次会把自己的信息注册到注册中心中,比如ip端口,服务功能等消费者需要到注册中心来寻找服务进行消费,在服务消费者第一次请求的时候会拉取服务提供者的信息,注册中心会把提供者的实例列表给到消费者供消费者选择,使用负载均衡来选择服务,默认为轮询,还有加权轮询,随机。同时服务消费者还会定时去注册中心拉取服务提供者的信息

如果我们的服务挂掉了怎么办?

服务提供者会每隔一段时间去向注册中心报告自己的状态[发送心跳ping 30s/次 共90s],如果没有向注册中心报告状态,那么这个时候注册中心会认为服务提供者已经宕机,同时会推送到我们的服务消费者,这个服务提供者已经宕机

微服务的五大组件

  1. 服务注册与发现:如Eureka已过时Nacos、Consul,用于服务的注册和发现。
  2. 配置管理:如Spring Cloud Config、OpenFeign 用于集中管理服务的配置。

Feign是一个声明式的Web服务客户端(Web服务客户端就是Http客户端),让编写Web服务客户端变得非常容易,只需创建一个接口并在接口上添加注解即可。

``OpenFeign是Spring Cloud 在Feign的基础上支持了SpringMVC的注解,如@RequesMapping等等。OpenFeign的@FeignClient可以解析SpringMVC的@RequestMapping注解下的接口,并通过动态代理的方式产生实现类,实现类中做负载均衡并调用其他服务。`

@FeignClient(name = "feignTestService", url = "http://localhost/8001")
public interface FeignTestService {
}

@Component
@FeignClient(url = "http://localhost/8001")
public interface PaymentFeignService{
}
二、OpenFeign使用
2.1.OpenFeign 常规远程调用
所谓常规远程调用,指的是对接第三方接口,和第三方并不是微服务模块关系,所以肯定不可能通过注册中心来调用服务。

第一步:导入OpenFeign的依赖

第二步:启动类需要添加@EnableFeignClients
    
第三步:提供者的接口
@RestController
@RequestMapping("/test")
public class FeignTestController {

    @GetMapping("/selectPaymentList")
    public CommonResult<Payment> selectPaymentList(@RequestParam int pageIndex, @RequestParam int pageSize) {
        System.out.println(pageIndex);
        System.out.println(pageSize);
        Payment payment = new Payment();
        payment.setSerial("222222222");
        return new CommonResult(200, "查询成功, 服务端口:" + payment);
    }

    @GetMapping(value = "/selectPaymentListByQuery")
    public CommonResult<Payment> selectPaymentListByQuery(Payment payment) {
        System.out.println(payment);
        return new CommonResult(200, "查询成功, 服务端口:" + null);
    }

    @PostMapping(value = "/create", consumes = "application/json")
    public CommonResult<Payment> create(@RequestBody Payment payment) {
        System.out.println(payment);
        return new CommonResult(200, "查询成功, 服务端口:" + null);
    }

    @GetMapping("/getPaymentById/{id}")
    public CommonResult<Payment> getPaymentById(@PathVariable("id") String id) {
        System.out.println(id);
        return new CommonResult(200, "查询成功, 服务端口:" + null);
    }
    
第四步:消费者调用提供者接口
@FeignClient(name = "feignTestService", url = "http://localhost/8001")
public interface FeignTestService {

    @GetMapping(value = "/payment/selectPaymentList")
    CommonResult<Payment> selectPaymentList(@RequestParam int pageIndex, @RequestParam int pageSize);

    @GetMapping(value = "/payment/selectPaymentListByQuery")
    CommonResult<Payment> selectPaymentListByQuery(@SpringQueryMap Payment payment);

    @PostMapping(value = "/payment/create", consumes = "application/json")
    CommonResult<Payment> create(@RequestBody Payment payment);

    @GetMapping("/payment/getPaymentById/{id}")
    CommonResult<Payment> getPaymentById(@PathVariable("id") String id);
}
  1. 服务网关:如Zuul、Spring Cloud Gateway,作为系统的唯一入口,处理外部请求的路由和过滤。
  2. 负载均衡:如Ribbon,用于在多个服务实例之间分配请求。
  3. 断路器:如Hystrix,用于服务熔断,防止系统雪崩

对于服务注册这块有什么了解?

  • 服务注册中心:服务实例在启动时向服务注册中心注册自己的地址和端口信息。检查 心跳 如果未查询就剔除,同时也有注册中心主动发起请求。
  • 健康检查:服务注册中心通常会定期对已注册的服务进行健康检查,以确保服务的可用性。
  • 服务发现:服务消费者通过服务注册中心查找可用的服务实例,以进行服务调用。
  • 服务去注册:当服务实例关闭或出现故障时,它需要从服务注册中心注销,以避免调用不可用的服务。

你能说一下小程序的登录流程吗?

调用微信api,根据code获取openid;根据openid查询用户为空就新增;调用微信api WechatService + WechatServiceImpl(openId+phoneCode) 获取用户绑定的手机号;保存或修改该用户;将用户id存入token返回(JWT生成token)

有哪些方式可以创建单例?

  1. 饿汉式:在类加载时就立即初始化并创建单例对象。
  2. 懒汉式:在第一次调用时初始化单例对象,通常需要考虑线程安全问题。
  3. 双重校验锁:在懒汉式的基础上,通过双重校验锁确保线程安全。
  4. 静态内部类:利用静态内部类的加载机制来确保单例对象的唯一性。
  5. 枚举:利用枚举的特性,保证单例对象的唯一性和线程安全【不可用反射】
并发情况下严格控制单例?volatile→禁止进行指令重排序

双重校验锁:在懒汉式的基础上,通过双重校验锁确保线程安全。

思考一个问题:哪些方式创建单例模式?

1. 懒汉式,线程不安全

这种方式在类加载时不初始化。在需要的时候才创建对象,节约资源。

public class Singleton {
    private static Singleton instance;
    private Singleton() {}

    public static Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

2. 懒汉式,线程安全

通过同步方法确保线程安全。

public class Singleton {
    private static Singleton instance;
    private Singleton() {}

    public static synchronized Singleton getInstance() {
        if (instance == null) {
            instance = new Singleton();
        }
        return instance;
    }
}

3. 饿汉式

类加载时就完成了初始化,保证了线程的安全性。

public class Singleton {
    private static Singleton instance = new Singleton();
    private Singleton() {}

    public static Singleton getInstance() {
        return instance;
    }
}

4. 双重校验锁

线程安全且在实例域需要延迟加载时提高性能。

public class Singleton {
    private volatile static Singleton singleton;
    private Singleton() {}

    public static Singleton getInstance() {
        if (singleton == null) {
            synchronized (Singleton.class) {
                if (singleton == null) {
                    singleton = new Singleton();
                }
            }
        }
        return singleton;
    }
}

5. 静态内部类

这种方式既实现了懒加载,又保证了线性安全。

public class Singleton {
    private static class SingletonHolder {
        private static final Singleton INSTANCE = new Singleton();
    }
    private Singleton() {}

    public static final Singleton getInstance() {
        return SingletonHolder.INSTANCE;
    }
}

6. 枚举

实现单例的最佳方法,它不仅能避免多线程同步问题,而且还自动支持序列化机制,防止反序列化重新创建新的对象。

public enum Singleton {
    INSTANCE;
    public void whateverMethod() {
    }
}

公平锁和非公平锁的区别?

  • 公平锁:多个线程按照它们请求锁的顺序来获取锁,先来先得。这种方式不会产生饥饿现象,但可能会降低吞吐量,因为需要额外的开销来维护等待队列的顺序。【僵尸线程】对资源顺序有严格要求

    为什么会降低吞吐量?
    1. 维护等待队列:公平锁为了保证按照请求锁的顺序来获取锁,必须维护一个有序的等待队列。这意味着每次有线程请求锁或者释放锁时,都需要进行额外的操作来管理这个队列,这会增加开销。
    2. 上下文切换开销:当锁被释放时,公平锁需要唤醒等待队列中的第一个线程。这个过程涉及到线程的上下文切换,上下文切换是有成本的,因为它涉及到保存和恢复线程的状态。
    3. 减少并发机会:由于公平锁要求先来先得,即使锁被释放,后请求锁的线程即使处于可运行状态,也必须等待前面的线程先执行。这可能会减少并发执行的机会,从而降低吞吐量。
    4. 增加延迟:公平锁可能会增加线程获取锁的平均等待时间,因为每个线程都必须等待前一个线程完成。这种延迟可能会导致系统整体性能下降。
  • 非公平锁:线程获取锁的顺序不一定是按照请求锁的顺序,允许线程“插队”。这种方式可能会提高吞吐量,但可能导致某些线程长时间无法获取锁,产生饥饿现象。【为什么性能高?不用沉睡和阻塞 避免来回切换】对性能要求高

ReentrantLock 看传参 公平/非公平都支持
ReentrantLock 是Java提供的显式锁,它允许你通过构造函数参数来选择是使用公平锁还是非公平锁:

公平锁(Fair Lock):通过传递true给ReentrantLock的构造函数来创建。这确保了锁的获取是按照线程请求锁的顺序进行的,防止饥饿现象。
ReentrantLock fairLock = new ReentrantLock(true);

非公平锁(Non-Fair Lock):通过传递false或者不传递参数(默认值)给ReentrantLock的构造函数来创建。这种方式允许线程“插队”,可能会提高吞吐量,但也可能导致某些线程长时间无法获取锁。
ReentrantLock nonFairLock = new ReentrantLock(false); // 或者
ReentrantLock nonFairLock = new ReentrantLock(); // 默认是非公平锁
Synchronized 公平锁
从Java 6开始,synchronized的实现得到了改进,它试图实现一种偏向锁和轻量级锁的策略,以提高性能。
以下是关于synchronized的一些关键点:

偏向性:synchronized在锁竞争不激烈的情况下,会尝试偏向第一个获取锁的线程,这有助于减少不必要的同步开销。
轻量级锁:当没有竞争时,synchronized会使用轻量级锁,这比使用操作系统提供的重量级锁要快得多。
重量级锁:当存在竞争时,synchronized会升级为重量级锁,这涉及到操作系统的线程调度。

synchronized不会严格地保证公平性,因为它允许锁的“重入”和偏向性,这意味着它更倾向于非公平锁的行为。然而,在锁竞争激烈的情况下,synchronized会尽量保持一定的公平性,因为它会按照线程在监视器队列中的顺序来获取锁。

总的来说,synchronized不是严格意义上的公平锁,但它的实现细节和调度策略可能会在一定程度上表现出公平锁的特性。与ReentrantLock相比,synchronized的锁获取机制更为复杂,并且它是由JVM内部实现的,因此具体的调度细节对开发者来说是透明的。

SpringMVC的工作原理

  • 前端的HTTP请求到达时首先被DispatcherServlet接收

  • DispatcherServlet根据请求信息路径查找合适的HandlerMapping来确定哪个Controller应该处理该请求。

  • 找到合适的Controller后,DispatcherServlet将请求转发给它处理。

  • Controller处理完请求后返回一个ModelAndView对象给DispatcherServlet。

  • DispatcherServlet再通过ViewResolver解析ModelAndView中的视图逻辑名,找到对应的视图。

  • 最后,DispatcherServlet将模型数据渲染到视图上并响应给客户端。

image-20250206094925513

OpenFeign的底层原理

OpenFeign 实现了简洁、声明式的 HTTP 请求调用,并且与 Spring Cloud 集成后能提供更多的功能如负载均衡等

动态代理: OpenFeign 使用 Java 动态代理技术,基于接口创建代理类,代理类会自动发起 HTTP 请求。你定义的接口方法会映射到 HTTP 请求上,OpenFeign 会根据注解(如 @RequestMapping, @GetMapping 等)来构建请求。

注解解析: OpenFeign 会解析接口方法上的注解,构造 HTTP 请求的 URL、请求方法类型(GET、POST 等),以及请求体和请求头等信息。

请求拦截和处理: 在请求发起之前,OpenFeign 允许通过拦截器(RequestInterceptor)来修改请求,比如设置请求头、参数等。

负载均衡与容错: 如果与 Spring Cloud 一起使用,OpenFeign 会集成 Ribbon(负载均衡)和 Hystrix(容错),使得服务调用更加健壮和可靠。

序列化与反序列化: OpenFeign 会利用 Jackson 等库进行请求和响应的序列化和反序列化,将 Java 对象与 HTTP 请求/响应内容相互转换

在使用OpenFeign时,开发者只需要定义接口并添加相应的注解,OpenFeign会在运行时动态生成实现类来执行HTTP请求。

对Volatile的理解

volatile 是Java语言中的一个关键字,用于修饰变量,以确保该变量的读写操作对所有线程立即可见,并且防止指令重排序优化。

确保了不同线程对这个变量进行读写操作时的可见性。
是java的关键字是修饰共享的变量,不能修饰局部变量。
修饰普通或静态成员变量,主要用来保证可见性有序性

可见性(Visibility)

在一个多线程程序中,为了提高性能,每个线程可能会将共享变量缓存到自己的CPU缓存中。如果一个线程修改了这个变量的值,而这个新值没有及时写回主内存,那么其他线程可能会读取到旧值。使用volatile关键字可以确保:

  • 每次读写变量都是直接操作主内存。
  • 当一个线程修改了一个volatile变量时,新值会立即被写入主内存。
  • 其他线程读取volatile变量时,会从主内存中读取最新值。

这样,volatile就保证了不同线程之间共享变量的可见性。

有序性(Ordering)

在没有volatile修饰的变量上,Java编译器和处理器可能会进行指令重排序,以提高程序运行的效率。指令重排序可能会导致程序的执行顺序与代码的编写顺序不一致。使用volatile可以防止以下两种类型的重排序:

  • 写操作的重排序volatile变量的写操作不允许与它之前的操作重排序。
  • 读操作的重排序volatile变量的读操作不允许与它之后的操作重排序。

这样,volatile就提供了一定的有序性保证。

Spring Security的实现

我的项目是基于JWT的前后端分离的项目,在自定义认证管理器AuthenticationManager认证成功后,生成JWT令牌并返回给前端。前端在随后的请求中携带这个JWT令牌。这时候,我们使用AccessDecisionManager来实现接口的鉴权逻辑,其中包括一个check方法,该方法会校验JWT令牌的有效性。如果校验通过,就去查询数据库以确定用户拥有哪些权限。在用户登录时,其权限信息已经被缓存到Redis中。后续的请求中,我们可以直接从Redis中检索用户的权限信息。如果请求的接口权限与用户缓存中的权限匹配,则放行;如果不匹配,则返回一个友好的错误信息

什么是AQS

是多线程中的抽象队列同步器。是一种锁机制,它是做为一个基础框架使用的,是一个抽象类。
像ReentrantLock都是基于AQS实现的

在Java的并发编程中,AbstractQueuedSynchronizer(简称AQS)是一个非常重要的类,它提供了一个框架,用于实现依赖于先进先出(FIFO)等待队列的阻塞锁和其他同步器(例如信号量、事件等)。AQS 本身是一个抽象类,它内部定义了获取资源(锁)释放资源(锁)的基本方法,以及管理同步状态队列的机制。

当说“ReentrantLock是基于AQS实现的”,意味着ReentrantLock这个具体锁的实现类,是继承并利用了AQS提供的模板方法来构建其功能的。具体来说:

  • 继承ReentrantLock内部有一个内部类叫做Sync,这个Sync类直接继承自AbstractQueuedSynchronizer
  • 实现Sync类(及其子类)会根据需要重写AQS的一些方法,如tryAcquiretryRelease,这些方法用于定义获取锁和释放锁的具体行为。
  • 利用模板方法:AQS提供了一系列的模板方法(如acquirerelease等),这些方法内部会调用前面提到的可重写方法(如tryAcquiretryRelease),从而允许ReentrantLock按照特定的逻辑来管理锁的状态。

因此,“基于AQS实现”的表述强调了ReentrantLock并不是从头开始构建锁的所有细节,而是站在AQS这个强大的基础框架之上,通过实现特定的策略来完成锁的具体功能。这样做的好处是减少了代码量,提高了代码的可维护性和可重用性,并且由于AQS经过了严格的测试,基于它实现的锁也更加可靠。

定义了一个并发情况下一些抽象的资源 资源能否共享/独享 定义了公平/非公平
如果是非公平锁如果来了个新的线程来抢线程 也是会去抢一次
AQS成为了JUC很多类都去继承的 它抽象了很多并发的属性和行为,让子类去继承它扩展自己

Synchronized的锁升级

  • Monitor实现的锁属于重量级锁,里面涉及到了用户态权限低和内核态权限高的切换、进程的上下文切换,成本较高,性能比较低
  • 在JDK1.6引入了两种新型锁机制:偏向锁和轻量级锁,它们的引入是为了解决在没有多线程竞争或基本没有竞争的场景下使用传统锁机制带来的性能开销问题

一段很长的时间内都只被一个线程使用锁 偏向锁
有线程交替或线程加锁的时间是错开的 轻量级锁
有很多线程来抢 重量级锁

偏向锁

偏向锁是一种优化锁的机制,它的设计初衷是:如果一个锁在大多数时间里只被一个线程访问,那么就没有必要进行线程间的同步操作,因为不存在锁竞争。在这种情况下,JVM会为这个锁赋予一个“偏向”,即偏向于第一个获取它的线程。在后续的锁操作中,如果该线程再次请求这个锁,就不需要进行同步操作,从而提高性能。偏向锁适用于只有一个线程访问同步块的场景。

轻量级锁

轻量级锁是另一种锁的优化,它适用于锁竞争不是很激烈,且锁持有的时间短的场景。当线程交替访问同步块时,使用轻量级锁可以减少传统的重量级锁带来的性能开销。轻量级锁是通过在对象头中的一些标记位来实现的,当锁处于轻量级锁状态时,线程通过CAS操作来尝试获取锁,如果成功,则直接进入同步块执行,从而避免了使用操作系统级别的重量级锁机制。

重量级锁

重量级锁是JVM中最传统的锁实现,也是性能开销最大的锁。当有很多线程同时竞争同一个锁时,JVM会使用重量级锁来确保线程安全。重量级锁依赖于操作系统的互斥量(mutex),会导致线程状态在用户态和核心态之间转换,这种转换是非常耗时的。因此,当锁竞争非常激烈时,使用重量级锁可以保证公平性和线程安全,但会带来较大的性能开销。

在处理多线程同时竞争同一个锁的情况时,并不一定总是需要使用重量级锁。以下是一些优化和策略,可以帮助您更好地处理并发场景:

  1. 最小化同步范围
    • 仅对必要的代码块进行同步,减少锁的竞争。
    • 使用细粒度锁,比如对不同的数据结构使用不同的锁,而不是对整个对象加锁。
  2. 使用并发工具类
    • Java提供了许多并发工具类,如java.util.concurrent包中的ReentrantLockReadWriteLockSemaphoreCountDownLatchConcurrentHashMap等,它们提供了比synchronized更丰富的功能。
  3. 锁分离
    • 对于读多写少的场景,可以使用读写锁(ReadWriteLock),它允许多个读线程同时访问,而写线程则互斥。
  4. 锁优化
    • 在锁竞争不是很激烈的情况下,可以使用轻量级锁或偏向锁,这些锁的开销比重量级锁小。
  5. 无锁编程
    • 使用原子类(如AtomicIntegerAtomicReference)和线程安全的数据结构,这些类通过CAS操作实现了无锁的线程安全。
  6. 线程池
    • 使用线程池来管理线程,避免频繁创建和销毁线程带来的开销。
  7. 避免死锁
    • 设计代码时注意锁的顺序,避免循环等待条件,减少死锁的发生。
  8. 性能测试
    • 对并发代码进行性能测试,了解不同锁策略对性能的影响,并根据测试结果选择合适的锁。

java语言是高级语言如果想调用底层的操作系统和硬件要通过操作系统的API去操作。以前老的JDK版本 数据是在操作系统找的数据,Monitor的标志0 和 1,底层使用的Monitor实现,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低 。
引入新型锁后,java里是用对象头找个地方存一把锁,这样就不涉及到调用操作系统底层。一开始new了个对象 此时是无锁状态。接下来来人拿锁,长期一个人拿到那个锁 此时是偏向锁竞争不激烈。后面多线程一起来 交替抢锁 此时是轻量级锁。随着并发越来越高 此时在一个线程拿到锁后很多线程来抢锁 线程先尝试自己先获取几次(自旋锁64次未拿到锁就会升级为重量级锁) 这时就涉及到操作系统的底层对象涉及到了用户态权限低和内核态权限高的切换、进程的上下文切换,成本较高,性能比较低。锁不可逆可能新版本可以降级

Dockerfile 常用命令

  • FROM: 指定基础镜像。
  • ENV: 设置环境变量。
  • RUN: 执行命令并创建新的镜像层。
  • COPY: 将文件从宿主机复制到容器中。
  • EXPOSE: 声明容器运行时将监听的端口。
  • ENTRYPOINT: 配置容器启动时运行的命令。

常用的 Docker 命令

  • docker run: 创建一个新的容器并运行一个命令。
  • docker pull: 从仓库中拉取或者更新一个镜像。
  • docker push:推送镜像到服务
  • docker build: 从 Dockerfile 构建一个镜像。
  • docker images: 列出本地镜像。
  • docker ps: 列出运行中的容器。
  • docker stop: 停止一个运行中的容器。
  • docker start: 启动一个停止的容器。
  • docker rm: 删除一个容器。
  • docker rmi: 删除一个镜像。
  • docker exec: 在运行中的容器内执行命令。
  • docker logs: 获取容器的日志。
- docker volume create:创建数据卷
- docker volume ls:查看所有数据卷
- docker volume inspect:查看数据卷详细信息,包括关联的宿主机目录位置
- docker volume rm:删除指定数据卷

Docker Compose 常用命令

  • docker-compose up: 启动所有服务的容器。
  • docker-compose down: 停止并删除容器、网络、卷和镜像。
  • docker-compose ps: 列出项目中所有的容器。
  • docker-compose exec: 进入指定的容器。
  • docker-compose build: 构建或重建服务。
  • docker-compose logs: 查看服务的日志输出。
  • docker-compose stop: 停止运行的容器。

synchronized 和 ReentrantLock 的区别

  • synchronized 是Java的一个关键字用于方法和代码块中,而 ReentrantLock 是JUC包的一个类。
  • synchronized 可以自动加锁和解锁,而 ReentrantLock 需要手动加锁和解锁。
  • synchronized 的锁是非公平的,而 ReentrantLock 默认也是非公平的,但可以设置为公平锁。

你们公司是怎么部署项目的

是通过docker + jenkins
测试环境我们参与 生产环境组长部署

varchar 与 char 区别

  • varchar 是可变长度的字符串,而 char 是固定长度的字符串。
  • varchar 的性能通常比 char 差,因为需要处理额外的长度信息。
  • 当数据长度变化很大时,推荐使用 varchar;当数据长度几乎固定时,使用 char 可能更合适。

Redis的持久化有哪几种? 它们的区别是什么?

完整性 大小 数据恢复速度 建议

Redis持久化:RDB全称Redis Database Backup file(Redis数据备份文件),也被叫做Redis数据快照,简单来说就是把内存中的所有数据都记录到磁盘中。当Redis实例故障重启后,从磁盘读取快照文件,数据恢复。

[root@localhost ~]# redis-cli
127.0.0.1:6379> save          #由Redis主进程来执行RDB,会阻塞所有命令
ok

127.0.0.1:6379> bgsave        #开启子进程执行RDB,避免主进程受到影响
Background saving started

Redis内部有触发RDB的机制,可以在redis.conf文件中找到,格式如下:

// 900秒内,如果至少有1个key被修改,则执行bgsave
save 900 1
save 300 10
save 60 10000

==RDB的执行原理?==数据完整性高用RDB

bgsave开始时会fork主进程得到子进程,子进程共享主进程的内存数据,完成fork后读取内存数据并写入RDB文件

在LInux中主进程并无法直接读取物理内存,它只能通过虚拟内存去读。因此有页表(记录虚拟地址与物理地址的映射关系)去执行操作 同时 主进程也会fork(复制页表) 成为一个新的子进程(携带页表) → 写新RDB文件替换旧的RDB文件 → 磁盘

fork采用的是copy-on-write技术:

  • 当主进程执行读操作时,访问共享内存
  • 当主进程执行写操作时,则会拷贝一份数据,执行写操作

优点:二进制数据重启后 Redis无需过多解析 直接恢复

==AOF==对数据不敏感要求不高

AOF全称为Append Only File(追加文件)底层硬盘顺序读写。Redis处理的每个写命令都会记录在AOF,可以看作是命令日志文件
AOF默认是关闭的,需要修改redis.conf配置文件来开启AOF

# 是否开启AOF功能,默认是no
appendonly yes
# AOF文件的名称
appendfilename "appendonly.aof"

AOF的命令记录的频率也可以通过redis.conf文件来配

# 表示每执行一次写命令,立即记录到AOF文件
appendfsync always
# 写命令执行完毕先放入AOF缓冲区,然后表示每隔一秒将缓冲区数据写到AOF文件,是默认方案
appendfsync everysec
# 写命令执行完先放入AOF缓冲区,由操作系统决定何时将缓冲区内容写回磁盘
appendfsync no
配置项 刷盘时机 优点 缺点
Always 同步刷盘 可靠性高,几乎不丢数据 性能影响大
everysec 每秒刷盘 性能适中 最多丢失1秒数据
no 操作系统控制 性能最好 可靠性较差,可能丢失大量数据

因为是记录命令,AOF文件会比RDB文件大的多。而且AOF会记录对同一个key的多次写操作,但只有最后一次写操作才有意义通过执行bgrewriteaof命令,可以让AOF文件执行重读功能,用最少的命令达到相同效

Redis会在出发阈值时自动重写AOF文件。阈值也可以在redis.conf中配置

# AOF文件比上次文件 增多超过多少百分比则触发重写
auto-aof-rewrite-percentage 100
# AOF文件体积最小多大以上才触发重写
auto-aof-rewrite-min-size 64mb

★★★★★★★★ RDB与AOF对比 ★★★★★★★★

RDB和AOF各有优缺点,如果对数据安全性要求较高,在实际开发中往往会结合两者来使用
RDB是二进制文件,在保存时体积较小恢复较快,但也有可能丢失数据,我们通常在项目中使用AOF来恢复数据,虽然慢但丢失数据风险小,在AOF文件中可以设置刷盘策略(每秒批量写入一次命令)

RDB AOF
持久化方式 定时对整个内存做快照哦 记录每一次执行的命令
数据完整性 不完整,两次备份之间会丢失 相对完整,取决于刷盘策略
文件大小 会有压缩,文件体积小 记录命令,文件体积大
宕机恢复速度 很快
数据恢复优先级 低,因为数据完整性不如AOF 高,因为数据完整性更高
系统资源占用 高,大量CPU和内存消耗 低,主要是磁盘IO资源
但AOF重写时会占用大量CPU和内存资源
使用场景 可以容忍数分钟的数据丢失,追求更快的启动速度 对数据安全性要求较高常见

项目中是怎么使用redis的

需要结合项目中的业务进行回答,通常情况下,分布式锁的使用场景:
集群情况下的定时任务、抢单、幂等性场景
如果使用互斥锁的话 那么在集群项目有多个服务器就会出现问题

用Hash类型 大Key是Id 小key是商品id value是商品数量
数据量点击量 用String类型
用Set类型 Zset做排行榜

你的项目中哪里使用到分布式锁?

==Redis分布式锁==

Redis实现分布式锁主要利用Redis的setnx命令,setnx是**SET if not exists**(如果不存在,则SET)的简写

  • 获取锁

    添加锁,NX是互斥、EX是设置超时时间
    SET lock value NX EX 10

  • 释放锁

    释放锁,删除即可
    DEL key

你可以说一下redis的分布式锁的原理吗

我在项目中是集成了redisson(底层基于Lua脚本[具有原子性])

==redisson实现分布式锁 - 执行流程==

加锁 ↓→ 加锁成功 → Watch dog(看门狗)每隔(releaseTime/3的时间做一次续期) → Redis
↓ 操作redis → Redis
↓→→ 释放锁↑ → 通知看门狗无需继续监听 → Redis

加锁 → → → 是否加锁成功?→→→ ↓
↑←←while循环不断尝试获取锁←←←↓

public void redisLock() throws InterruptedException{
    RLock lock = redissonClient.getLock("heimalock");
 // boolean isLock = lock.tryLock(10, 30, TimeUnit.SECONDS);
// 如果不设置中间的过期时间30 才会触发看门狗
// 加锁,设置过期时间等操作都是基于lua脚本完成的[调用redis命令来保证多条命令的原子性]
    boolean isLock = lock.tryLock(10, TimeUnit.SECONDS);
    if(isLock){
        try{
            sout("执行业务");
        } finally{
            lock.unlock();
        }
    }
}

==redisson实现分布式锁 - 可重入==

redis实现分布式锁是不可重入的 但是 redisson实现分布式锁是可以重入的
可重入原理:它俩是同一个线程 每个线程都有唯一的线程id 根据线程id唯一标识做判断 判断之前获取锁是不是同一个线程
利用hash结构记录线程id重入次数

KEY VALUE VALUE
field value
heimalock thread1 0
public void add1(){
  RLock lock = redissonClient.getLock("heimalock");
  boolean isLock = lock.tryLock();
// 执行业务
  add2();
// 释放锁
  lock.unlock();
}
public void add2(){
  RLock lock = redissonClient.getLock("heimalock");
  boolean isLock = lock.tryLock();
// 执行业务
// 释放锁 锁次数-1不完全释放
  lock.unlock();
}

==redisson实现分布式锁 - 主从一致性==

Redis Master主节点:主要负责写操作(增删改) 只能写
Redis Slave从节点:主要负责读操作只能读

当RedisMaster主节点突然宕机后 Java应用会去格外获取锁 这时两个线程就同时持有一把锁 容易出现脏数据
怎么解决呢?

  • RedLock(红锁):不能只在一个redis实例上创建锁,应该是在多个redis实例上创建锁(n/2+1),避免在一个redis实例【实现复杂、性能差、运维繁琐】怎么解决?→ CP思想zookeeper

redis和mysql怎么保证数据一致性

先插入数据库
更新先更新数据库 更新数据库成功但redis不成功 影响不大 因为后面会有过期删除 最终会一致,更新mysql后缓存可以删除也可以修改
更新完数据库直接删除缓存了 有过期时间兜底 最终会保持一致 我们项目中对数据敏感性一致性不高 我们追求实时性
如果是最终保持一致性的就MQ 我们对实时性不高 对数据敏感性 一致性高
删除问题不大 哪里都行!
读多写少的可以上缓存
mysql保存购物车表 但是再页面操作的时候 只操作redis 用mq给到消费者修改或定时任务 更新数据到mysql,MQ问题:我们对数据实时性要求不高 只需要保存最终一致性就行
你如果只写redis 万一丢了数据怎么办
购物车丢点订单无影响 数据安全性要求不太高 mysql尽量不要搞购物车的表 都在redis的表 丢就丢了呗。或者异步同步/定时任务
实时性要求 安全性要求 → MySQL
电商一般数据库和mysql都要存 → 读多写少

一定、一定、一定要设置前提,介绍自己的业务背景 (一致性要求高?允许延迟一致?)

① 介绍自己简历上的业务,我们当时是把文章的热点数据存入到了缓存中,虽然是热点数据,但是实时要求性并没有那么高,所以我们采用的是异步的方案同步的数据

② 我们当时是把抢卷的库存存入到了缓存中,这个需要实时的进行数据同步,为了保证数据的强一致性,我们当时采用的是redission提供的读写锁来保证数据的同步

双写一致性:当修改了数据库的数据也要同时更新缓存的数据,缓存和数据库的数据要保持一致

  • 读操作:缓存命中,直接返回;缓存未命中查询数据库,写入缓存,设定超时时间

  • 写操作:延迟双删 [因为无论先删除缓存还是先删除数据库都可能会出数据不一致问题 有脏数据]

  • ==基于redisson互斥锁:==[放入缓存中的数据 读多写少] 【强一致性业务 性能低】有过期时间兜底

    • 共享锁:读锁readLock,加锁之后,其他线程可以共享读操作,但**不允许写操作**
    • 排他锁:独占锁writeLock也叫,加锁之后,阻塞其他线程读写操作(只允许一个用户或进程独占地对数据进行读取和写入操作)排他锁确保了写操作的原子性和一致性
    • 读数据的时候添加共享锁(读不互斥、写互斥)
    • 写数据的时候添加排他锁(阻塞其他线程的读写 因为读多写少)

    redissionClient.getReadWriteLock(“xxxx”);

  • ==异步通知:==异步通知保证数据的最终一致性(需要保证MQ的可靠性)需要在Redis中更新数据的同时,通知另一个服务进行某些操作。

    • 使用场景
      • 缓存与数据库双写: 当应用需要同时更新Redis缓存和数据库时,可以先将数据写入Redis,然后通过异步通知机制触发数据库的更新操作。
      • 跨地域数据复制: 在跨地域部署的服务中,为了实现数据的最终一致性,可以在一个地域写入数据后,通过异步通知机制在另一个地域进行数据复制。
      • 系统间数据同步: 在微服务架构中,不同的服务可能有自己的数据存储。当一个服务更新了数据后,可以通过异步通知机制告知其他相关服务进行数据同步。
  • ==基于Canal的异步通知==:监听mysql的binlog
    可以解析binlog文件 可以存放mysql里面的数据 看最近有无增删改查 转换成redis命令 再给redis里面

    • 使用MQ中间件,更新数据之后,通知缓存删除
    • 利用canal中间件,不需要修改业务代码,伪装为mysqls的一个从节点,canal通过读取binlog数据更新缓存

synchronized可以作用在哪些地方(作用域),分别锁的是什么

在Java中,synchronized关键字可以用来实现线程同步,它可以作用在不同的地方,并且锁定的对象也不同:

  1. 实例方法

    • 作用在实例方法上时,锁的是调用该方法的对象实例(即**this对象**)。
    • 任何线程想要执行这个方法,都必须获得该对象实例的锁。
    public synchronized void synchronizedMethod() {
        // 方法体
    }
    
  2. 静态方法

    • 作用在静态方法上时,锁的是类的Class对象
    • 由于静态方法是属于类的,而不是属于任何特定实例,所以所有线程要想执行这个静态同步方法,都必须获得该类的Class对象的锁。
    public static synchronized void synchronizedStaticMethod() {
        // 方法体
    }
    
  3. 代码块

    • 作用在代码块上时,可以指定一个锁对象括号里的对象,可以是任何对象
    • 当进入这个代码块时,线程必须获得指定锁对象的锁。
    public void synchronizedBlock() {
        synchronized(this) { // 锁定当前对象实例
            // 代码块
        }
    }
    
    public void synchronizedBlockWithObject() {
        Object lock = new Object();
        synchronized(lock) { // 锁定指定的对象
            // 代码块
        }
    }
    

什么情况下索引会失效?

  • 违反最左前缀法则
  • 范围查询右边的列,不能使用索引
  • 不要在索引列上进行运算操作,索引将失效
  • 字符串不加单引号,造成索引失效。(类型转换)
  • 以%开头的Like模糊查询,索引失效
    [不影响正常查询业务 但未运用超大分页查询优化 会导致索引失效]

索引创建原则有哪些?索引很多就会有很多B+树

数据量较大,且查询比较频繁的表
常作为查询条件、排序、分组的字段 [where、group by、order by]
③ 字段内容区分度高
④ 内容较长,使用前缀索引
尽量联合索引对存储节省空间

如果我们经常根据客户ID和订单日期来查询订单,那么可以在 customer_id 和 order_date 上创建一个联合索引。
CREATE INDEX idx_customer_date ON orders (customer_id, order_date);

这个联合索引 idx_customer_date 有以下几个特点:

索引顺序:首先根据 customer_id 排序,然后在每个 customer_id 的基础上根据 order_date 排序。
查询优化:以下查询可以利用这个联合索引:
SELECT * FROM orders WHERE customer_id = ? AND order_date = ?;
SELECT * FROM orders WHERE customer_id = ?;

要控制索引的数量
⑦ 如果索引列不能存储NULL值,请在创建表时使用NOT NULL约束它
大字段不建议建立索引是因为B+树一个叶子节点/一个非叶子节点 差不多16k 一个节点对应一个[页] 多的话会更多层
尽量不用性别去创建索引

  • 先陈述自己再实际工作中是怎么用的
  • 主键索引
  • 唯一索引
  • 根据业务创建的索引(复合索引)

索引的底层数据结构了解过吗?

MySQL的InnoDB引擎采用的B+树的数据结构来存储索引

  • 阶数更多,路径更短
  • 磁盘读写代价B+树更低,非叶子节点只存储指针,叶子阶段存储数据
  • B+树便于扫库和区间查询,叶子节点是一个双向链表

**MySQL默认使用的索引底层数据结构是B+树**。再聊B+树之前,先来聊聊二叉树和B树

==B Tree(矮胖树)==,B树是一种多叉路衡查找树,相对于二叉树,B树每个节点可以有多个分支,即多叉。以一颗最大度数(max-degree)为5(5阶)的b-tree为例,那这个B树每个节点最多存储4个key

==B+Tree== 是再BTree基础上的一种优化,使其更适合实现外存储索引结构,InnoDB存储引擎就是B+Tree实现其索引结构

B树与B+树对比

  • 磁盘读写代价B+树更低
  • 查询效率B+树更加稳定
  • B+树便于扫库和区间查询

B树要找12 首先找38 左面小 再去缩小范围16和29 找到12 → 但是我们只想要12的数据 B树会额外的把38,16,29的数据全查一遍最后才到12的数据

B+树是在叶子节点才会存储数据,在非叶子节点全是指针,这样就没有其他乱七八糟的数据影响 。且查找路径是差不多的,效率较稳定

便于扫库:比如我们要查询6-34区间的数据,先去根节点扫描一次38 → 16-29 → 由于叶子节点之间有双向指针,就可以一次性把所有数据都给拿到[无需再去根节点找一次]

mysql底层为什么用B+树利用二分查找,树越矮经过磁盘IO次数越少,它是稳定的每次都查到最底层

二叉树 O(logn) 容易退化成链表 所以不用它
平衡二叉树 全部倾斜
红黑树 一个节点只能存一个数据
B树能不能除了叶子节点其他不存数据呢?

你可以设计一种变体的B树,其中只有叶子节点存储数据,而所有其他非叶子节点仅作为导航节点,不存储实际的数据。这种结构在概念上类似于B树的一个特例,通常被称为B树索引结构的一部分,其中非叶子节点存储的是键值,而叶子节点存储的是实际的数据记录或者是指向数据记录的指针

B+树第三层2000多万条数据,尽量不要把数据达到2000多万
B+树叶子节点加了双向链表 让我们查询更加稳定 范围查询会更快

mysql索引底层不一定只有B+树,也可能是Hash 在精准查询性能比它高

R—Tree:地理位置搜索

联合索引

where b= AND c= AND a= 这样走索引都能走 底层自己排序
为什么联合索引要遵循最左匹配原则【里面的b+树 先按照a排序 再b 因为要二分查找 不排序怎么找?】

在MySQL中,如何定位慢查询?查询前用explain查询是否走了索引等问题

explain查询后的列:
id:查询中SELECT语句的序列号,如果该行引用其他行的并集结果,则该值可以为空。
select_type:表示查询的类型,常见的类型有:
SIMPLE:简单的SELECT查询,不使用UNION或子查询。
PRIMARY:最外层的SELECT查询。
UNION:在UNION中的第二个或随后的SELECT查询。
DEPENDENT UNION:在UNION中的第二个或随后的SELECT查询,取决于外层查询。
UNION RESULT:UNION的结果。
SUBQUERY:子查询中的第一个SELECT。
DEPENDENT SUBQUERY:子查询中的第一个SELECT,取决于外层查询。
table:查询的是哪个表。
type:这是你提到的一个非常重要的列,它表示MySQL在表中找到所需行的方式,也称为“访问类型”。以下是一些常见的访问类型,从最好到最差排序:
system:表只有一行(系统表)。
const:表最多有一个匹配行,它将在查询开始时被读取。因为仅有一行,所以优化器的其余部分可以将这一行视为常量。
eq_ref:对于每个来自于前面的表的行组合,从该表中读取一行。这通常是最好的联接类型,除了const类型。
ref:对于每个来自于前面的表的行组合,所有有匹配索引值的行将从这张表中读取。
fulltext:使用全文索引执行查询。
ref_or_null:与ref类似,但是MySQL会额外搜索包含NULL值的行。
index_merge:表示查询使用了两个或更多的索引。
unique_subquery:用于IN子查询,子查询返回不重复的值集。
index_subquery:用于IN子查询,子查询返回不重复的值集,可以使用索引。
range:使用索引来检索给定范围的行。
index:全索引扫描(比ALL快,因为索引通常比数据行小)。
ALL:全表扫描,这是最差的一种类型,因为MySQL必须检查每一行以找到匹配的行。
possible_keys:指出MySQL能使用哪些索引来优化查询。
key:MySQL实际决定使用的索引。
key_len:使用的索引的长度。越短越好。
ref:显示索引的哪一列被使用了,如果可能的话,是一个常数。
rows:MySQL认为必须检查的用来返回请求数据的行数。
filtered:显示了通过条件过滤出的行数的百分比估计。
Extra:包含MySQL解析查询的额外信息,例如是否使用了索引,是否排序了结果,是否使用了临时表等

1.介绍一下当时产生问题的场景(我们当时的一个接口测试的时候非常的慢,压测的结果大概5秒钟)
2.我们系统中当时采用了运维工具(Skywalking),可以监测出哪个接口,最终因为是sql的问题
3.在mysql中开启了慢日志查询,我们设置的值就是2秒,一旦sql执行超过2秒就会记录到日志中(调试阶段)

产生原因:

  • 聚合查询
  • 多表查询
  • 表数据量过大查询
  • 深度分页查询

方案一:==开源工具==[调试阶段才会开启 生产阶段不会开启]

  • 调试工具Arthas
  • 运维工具:Prometheus、SKywalking(接口访问时间)

方案二:==MySQL自带慢日志==

慢查询日志记录了所有执行时间超过指定参数(long_query_time, 单位:秒,默认10秒)的所有SQL语句的日志,如果要开启慢查询日志,需要在MySQL的配置文件(/etc/my.cnf)中配置信息:

# 开启MySQL慢日志查询开关
slow_query_log = 1
# 设置慢日志的时间为2秒,SQL语句执行时间超过2秒,就会被视为慢查询,记录慢查询日志
long_query_time = 2

什么是聚簇索引?什么是非聚簇索引(二级索引)?什么是回表?

  • 聚簇索引(聚集索引):数据与索引放到一块,B+树的叶子节点保存了整行数据,有且只有一个【id存放的b+树】
  • **非聚簇索引(二级索引)**:数据与索引分开存储,B+树的叶子节点保存对应的主键,可以有多个【叶子就是id的字段】
  • 回表查询:通过二级索引找到对应的主键值,到聚集索引中查找正行数据,这个过程就是回表

怎么避免回表 → 使用覆盖索引!
需要name 直接 select name 而不用 select *
要按需来查找

除了InnoDB,MySQL数据库还支持多种其他存储引擎,其中最著名的是MyISAM。以下是InnoDB和MyISAM两个存储引擎的主要区别:

事务支持:
InnoDB:支持事务,它遵循ACID原则(原子性、一致性、隔离性和持久性)。如果事务中的某个操作失败,整个事务可以回滚到开始状态。
MyISAM:不支持事务,这意味着你无法回滚操作,这对于数据完整性和恢复可能是一个问题。
    
锁定机制:
InnoDB:使用行级锁定,只锁定需要的特定行,这可以大大减少数据库操作的冲突。
MyISAM:使用表级锁定,每次操作都会锁定整个表,这在并发操作较多时可能导致性能问题。
    
崩溃恢复:
InnoDB:具有自动崩溃恢复功能,即使数据库崩溃,也不会丢失数据,因为它将事务日志写入磁盘。
MyISAM:在崩溃后恢复较为困难,可能会丢失数据,因为它不记录事务日志。
    
全文搜索:
InnoDB(MySQL 5.6及以后版本):支持全文索引,但功能上不如MyISAM的全文搜索强大。
MyISAM:提供了更强大的全文搜索功能,但在MySQL 5.6之前,这是MyISAM相对于InnoDB的主要优势。
    
存储限制:
InnoDB:表的大小理论上受限于操作系统的文件大小限制,通常可以处理更大的数据量。
MyISAM:表的大小受限于最大文件大小,通常是2GB到4GB,这取决于文件系统的限制。
    
外键支持:
InnoDB:支持外键约束,这有助于保持数据的引用完整性。
MyISAM:不支持外键约束。
    
存储空间:
InnoDB:通常需要更多的存储空间,因为它存储了额外的信息来支持事务和行级锁定。
MyISAM:通常占用更少的存储空间,因为它不需要存储这些额外的信息
分类 含义 特点
==聚集索引(Clustered Index)== 将数据存储与索引放到了一块,索引结构的叶子节点保存了行数据 必须有, 而且只有一个
==二级索引(Secondary Index)== 将数据与索引分开存储,索引结构的叶子节点关联的是对应的主键 可以存在多个

聚集索引选取规则:

  • 如果存在主键,主键索引就是聚集索引
  • 如果不存在主键,将使用第一个唯一 (UNIQUE) 索引作为聚集索引
  • 如果表没有主键,或没有合适的唯一索引,则InnoDB会自动生成一个rowid作为隐藏的聚集索引

==回表查询==

select * from user where name = 'Arm';

MySQL的日志文件有哪些,它们的作用是什么

MySQL的日志文件有哪些,它们的作用是什么?

MySQL主要有以下几种日志文件:

  • 错误日志(Error Log):记录MySQL服务的启动、运行或停止过程中的错误信息。
  • 查询日志(General Query Log):记录所有MySQL执行的SQL命令,无论这些命令是否正确执行。
  • 慢查询日志(Slow Query Log):记录执行时间超过指定阈值的查询语句。
  • 二进制日志(Binary Log)记录所有更改数据的SQL语句,用于主从复制和数据恢复。事务的提交 和 主从复制
  • 事务日志/重做日志(InnoDB Redo Log)记录InnoDB存储引擎的事务操作,用于崩溃恢复。
  • 回滚日志/撤销日志(InnoDB Undo Log)用于事务回滚,保证事务的原子性。

undo log 和 redo log的区别?

redo log:记录的是数据页的物理变化,服务宕机可用来同步数据
undo log:记录的是逻辑日志,当事务回滚时,通过逆操作恢复原来的数据
redo log 保证了事务的持久性,undo log保证了事务的原子性和一致性

  • 缓冲池(buffer pool):主内存中的一个区域,里面可以缓存磁盘上经常操作的真实数据,在执行增删改査操作时,先操作缓冲池中的数据(若缓冲池没有数据,则从磁盘加载并缓存),以一定频率刷新到磁盘,从而减少磁盘IO,加快处理速度
  • 数据页(page):是InnoD8 存储引擎磁盘管理的最小单元,每个页的大小默认为 16KB。页中存储的是行数据

==redo log==

重做日志,记录的是事务提交时数据页的物理修改,是用来实现事务的持久性
该日志文件由两部分组冲:重做日志缓冲(redo log buffer) 以及 **重做日志文件(redo log file)**,前者是在内存中,后者是在磁盘中。当事务提交之后会把所有修改信息都保存到该日志文件中,用于在刷新脏页到磁盘,发生错误时,进行数据恢复使用。

==undo log==

回滚日志,用于记录数据被修改前的信息,作用包含两个:提供回滚MVCC(多版本并发控制)。undolog 和 redolog记录物理日志不一样,它是逻辑日志

  • 可以认为当delete一条记录时,undo log中会记录一条对应的insert记录,反之亦然
  • 当update一条记录时,它记录一条对应相反的update记录。当执行rolback时,就可以从undolog中的逻辑记录读取到相应的内容并进行回滚。

undo log可以实现事务的一致性和原子性

MySQL主从同步原理?

MySQL主从复制的核心就是二进制日志binlog[DDL(数据定义语言)语句DML(数据操纵语言)语句]
主库在事务提交时,会把数据变更记录在二进制日志文件 Binlog 中。
从库读取主库的二进制日志文件 Binlog,写入到从库的中继日志 Relay Log。
从库重做中继日志中的事件,将改变反映它自己的数据。

主服务器(Master)上的数据更改(如INSERT、UPDATE、DELETE操作)会被记录到二进制日志中。
从服务器(Slave)上的I/O线程连接到主服务器,请求主服务器上的二进制日志。
主服务器将二进制日志发送给从服务器,从服务器将这些日志事件写入到本地的中继日志(Relay Log)。
从服务器上的SQL线程读取中继日志中的事件,并在本地执行这些事件,从而实现数据的复制。

MySQL主从复制的核心就是二进制日志

二进制文件(BINLOG) 记录了所有的DDL(数据定义语言)语句DML(数据操纵语言)语句,但不包括数据查询(SELECT、SHOW)语句

复制分成三步:
  • Master主库在事务提交时,会把数据变更记录在二进制日志文件Binlog中
  • 从库读取主库的二进制日志文件Binlog,写入到从库的中继日志Relay Log
  • slave重做中继日志中的事件,将改变反应他自己的数据

项目中哪里涉及到分布式事务问题? 你是怎么解决的? 能说一下

分布式事务问题可能出现在跨多个服务或数据库的操作中,例如在订单服务中同时更新订单状态和扣减库存。秒杀案例:是先扣库存提前缓存到redis里,判断够不够,用RabbitMQ异步下来

解决方案:可以使用分布式事务框架,如Seata,其中AT模式是一种常见的解决方案。

AT模式原理:

  • AT模式基于两阶段提交,分为两个阶段:一阶段 prepare 和二阶段 commit/rollback。
  • 在业务方法开始时,Seata会拦截业务SQL,记录业务数据在执行前后的镜像,生成行锁。
  • 如果一阶段 prepare 成功,则二阶段进行 commit,直接提交事务;如果 prepare 失败,则执行 rollback,利用之前保存的数据镜像回滚到执行前的状态。

项目中哪里用到MQ,用来干什么?

异步发优惠卷 + 积分 [用户对于实时性要求不是很高]

  • 使用MQ的场景:订单处理
    具体场景

    当用户在电子商务平台上成功下单后,订单服务需要执行以下操作:

    1. 更新订单状态为“已支付”。
    2. 扣减商品库存。
    3. 通知支付服务处理支付。
    4. 通知物流服务准备发货。
    使用MQ的原因

    在这些操作中,更新订单状态和扣减库存是实时且同步的操作,但通知支付服务和物流服务则可以异步进行。使用MQ可以帮助我们实现以下目标:

    • 解耦服务:订单服务不需要直接调用支付服务和物流服务,降低了服务间的耦合度。
    • 异步处理:订单服务可以立即响应客户端,不必等待支付和物流服务的处理结果。
    • 流量削峰:在高峰期,MQ可以缓冲大量的订单处理请求,避免服务被压垮。

订单服务生产消息: 当订单服务完成订单状态更新和库存扣减后,它将以下消息发送到

{
  "orderId": "123456789",
  "status": "paid",
  "userId": "user123",
  "items": [
    {"productId": "prod123", "quantity": 1},
    {"productId": "prod456", "quantity": 2}
  ]
}

这个消息将被发送到不同的主题或队列,例如payment_topiclogistics_topic

2. 支付服务和物流服务消费消息:

  • 支付服务订阅payment_topic,当接收到订单支付消息后,它会处理支付逻辑,如验证支付状态、记录交易日志等。
  • 物流服务订阅logistics_topic,当接收到订单消息后,它会准备发货,更新物流信息,并通知用户。

通过这种方式,订单服务可以快速响应用户请求,而支付和物流服务可以按照自己的节奏处理订单相关的操作,整个系统因此变得更加灵活和可扩展。

如何保证消息不丢失?

保证生产者能够成功发送到交换机和队列(存储消息),生产者提供了消息确认机制
到队列后消息要有持久化机制
消费者要有一个消息确认机制 保证消费者至少消费成功消息一次

  • 开启生产者确认机制,确保生产者的消息能到达队列
    confirm到交换机ack 不到nack 和 return没到返回nack机制保证生产者把消息发过去

  • 开启持久化功能,确保消息未消费前在队列中不会丢失
    万一broker挂掉就惨了 保证至少成功一次消费

  • 开启消费者确认机制为auto,由spring确认消息处理成功后完成ack
    消费者三种机制:

    RabbitMQ支持消费者确认机制,即:消费者处理消息后可以向MQ发送ack回执,MQ收到ack回执后才会删除该消息,而SpringAMQP则允许配置三种确认模式:

    • manual:手动ack,需要在业务代码结束后,调用api发送ack。

    • auto:自动ack,由spring监测listener代码是否出现异常,没有异常则返回ack;抛出异常则返回nack

    • none:关闭ack,MQ假定消费者获取消息后会成功处理,因此消息投递后立即被删除

  • 开启消费者失败重试机制,多次重试失败后将消息投递到异常交换机,交由人工处理

    在开启重试模式后,重试次数耗尽,如果消息依然失败,则需要有MessageRecoverer接口来处理,它包含三种不同的实现:

    • RejectAndDontRequeueRecoverer:重试耗尽后,直接reject,丢弃消息。默认就是这种方式

    • ImmediateRequeueMessageRecoverer:重试耗尽后,返回nack,消息重新入队

    • RepublishMessageRecoverer:重试耗尽后,将失败消息投递到指定的交换机

  • 异步发送(验证码、短信、邮件)
  • MySQL和Redis,ES之间的数据同步
  • 分布式事务
  • 削峰填谷

如何解决消息积压?

产生原因:当生产者发送消息的速度超过了消费者处理消息的速度,就会导致队列中的消息堆积,直到队列存储消息达到上限。之后发送的消息就会成为死信,可能会被丢弃,这就是堆积问题

**解决消息堆积有三种思路 **

  • 增加更多消费者,提高消费速度
  • 在消费者内开启线程池加快消息处理速度
  • 扩大队列容积,提高堆积上限

如何保证消费幂等性【MQ】

幂等性是指同一个操作执行多次和执行一次的效果相同。在消息消费的场景中,保证幂等性通常有以下几种方法:
利用数据库的唯一约束
在数据库中为消息设置唯一标识(如消息ID),在处理消息前先检查该标识是否已存在。
导致重复消费 返回ack,blocker未收到。一定要在
生产者
投递的时候生成全局唯一的id,消费者就会去判断。异步生成 拿订单号去数据库查 如果查得到就直接return
精髓就是全局唯一
UUID不行 因为每次发送的消息都不是同一个UUID 要用业务上的

哪些地方还会有幂等问题?

提供者的openfegin、xxl-job、被别人调用且涉及到增删改

状态记录:

在消费消息前,记录消息的状态(如已处理),处理完毕后再更新状态。要根据订单ID+状态 来保证消费幂等性。订单存在且未支付 → 更新锁订单ID

并发情况幂等性:

完美的幂等要加上分布式锁对敏感性要求高,且要控制好锁的力度

如何保证消费有序性

队列中可以指定消息的消费顺序

RabbitMQ → 多个生产者并发投,所以生产者不能保证有序性,只考虑消费有序性。以消息进入的MQ的消息去回答。

怎么保证?
单线程消费:

在消费者端使用单个线程处理消息,确保消息按顺序处理。

分区有序:

在如Kafka这样的消息队列中,可以保证同一个分区内的消息是有序的。

如何既要又要【有序 + 速度快】

既要又要”通常指的是在保证消息的幂等性和有序性的同时,还需要考虑其他特性(如高性能、高可用等)
在一些场景下,可能需要在性能和一致性之间做权衡。例如,可以选择最终一致性来换取更高的性能。
Kafka 和 RocketMQ可以天生实现【底层Hash取模】

若非要用RabbitMQ实现呢?

不同订单之间是否要求一定顺序??
镜像集群,先搭3个节点的镜像集群,建立三个队列分为不同的镜像节点 各占一个队列,需要自己去实现
对订单号进行hash取模看到落到哪个节点
三个队列至少三个消费者 分别去消费它们
此时就可以并行有三个消费者去执行
把业务数据没关系的放在不同的队列去管理

万一挂掉了呢?

队列有持久化不用担心

能说一下如何使用死信交换机吗支付

死信交换机(DLX)用于处理无法正常消费的消息
创建一个正常的交换机和队列,以及一个死信交换机和死信队列。

  1. 定义死信交换机:创建一个用于处理死信的交换机。
  2. 定义死信队列:创建一个队列用于接收死信,并将其绑定到死信交换机。
  3. 配置主队列的死信交换机属性:在主队列上设置参数,指定当消息无法正常处理时应该发送到哪个死信交换机。
  4. 发送消息到主队列:生产者将消息发送到主交换机,进而路由到主队列。
  5. 消费主队列消息:消费者从主队列中获取消息并进行处理。如果消息处理失败,它将被路由到死信交换机。
  6. 消费死信队列消息:设置消费者来处理死信队列中的消息,进行错误处理或记录日志等操作。

mysql如何提升深分页查询效率子查询+索引

使用索引

  • 确保查询中使用的列上有适当的索引,这样可以加快查找速度。

**避免使用OFFSETLIMIT**:

  • 使用OFFSET进行深分页时,MySQL需要遍历所有OFFSET之前的行。可以通过记住上一次查询的最大ID来避免使用OFFSET

使用条件过滤

  • 如果可能,使用WHERE子句来减少需要扫描的数据量。

增加LIMIT的大小

  • 如果业务允许,可以增加每次查询返回的结果集大小,减少分页次数。

缓存

  • 对于不经常变更的数据,可以使用缓存来存储已经查询过的页。

使用EXPLAIN分析查询

  • 使用EXPLAIN来分析查询计划,找出性能瓶颈并进行优化。

能说一下常用的存储引擎以及它们的差异吗

InnoDB
支持事务、行级锁和外键。
适合处理大量短期事务。
为了维护数据的完整性,写操作相对较慢

MyISAM

不支持事务、不支持行锁只支持表锁
并发没那么大 事务要求没那么高可以用

能说一下倒排索引的原理吗?

根据参与文档中的字段 要构建倒排就会去分词
根据用户索引也会分词 就会去查文档id 再去查文档
中文词库为IK (Ikun 你干嘛 哎哟~)

es的text和keyword的区别

text

用于全文搜索,会分词,字符串类型

keywod

用于精确搜索字段,不会被分词,字符串类型

es在你的项目中是用来做什么的

快速搜索商品(C端)、订单(后台) + 日志查询 + 地理位置搜索经纬度定位附近的事物

mysql和ElasticSearch如何做数据同步

mysql进行增删改的时候
对数据敏感性实时性要求没那么高 只看可靠性[MQ异步 + 定时任务 = 没有那么强一致性]

如果数据量没那么大 有没有必要上ES?

没有必要,正排索引不走全表扫描也蛮快
组长进行技术选型 考虑到以后的业务增长

项目已经上线了 但是中途想换成ES 怎么办

mysql是全量数据 mq只能同步增量数据 怎么办呢?
新上架的只能到ES 那应该如何?

此时涉及到全量和增量的同步与Redis不一样
加定时任务每周/每天 会定期重构一次索引库晚上跑→兜底模式,全量同步,后期再增量同步

能说一下分词的原理吗

底层是大数据量的内容 树的结构来构建分词 IK,字符分割、词汇识别、过滤停用词
不好意思面试官 具体底层原理不是很了解

使用ES有遇到什么问题吗

类似于深分页!
测试环境数据量不会很大 等到上线后才会有这种问题

说一下jvm的内存区域,以及每个区域是干什么的

虚拟机栈、本地方法栈、程序计数器、元空间、堆

运行时数据区

虚拟机栈:每个线程运行时所需要的内存(先进后出)方法调用过程。每个栈由多个栈帧组成,对应着每次方法调用所占用的内存。每个线程只能有一个活动栈帧,对应着当前执行的那个方法

本地方法栈:与虚拟机栈类似,区别→虚拟机栈执行java方法,本地方法栈执行native方法【被封装的方法 没有具体实现的 都封装在java虚拟机中】专门存储java写的局部方法的局部变量

程序计数器:是当前线程所执行的字节码指令的行号指示器 同一个核是错峰出行 会上下文切换,要用程序计数器记录下当前执行到哪里的代码

元空间:元空间的本质和永久代类似,都是对JVM规范中方法区的实现。最大区别是元空间在本地内存中而不是虚拟机中。1.8以前叫永久代 1.8后叫方法区或元空间 一般存储类元信息。还会存有运行时常量池。在里面还会存有静态变量

堆内存:是JVM所有线程共享的部分,唯一用途是来保存对象实例、数组;由年轻代和老年代组成。new一个都会开启一个空间

类加载器:类加载器(Class Loader)负责将类的字节码文件(.class文件)加载到JVM中,并转化为对应的java.lang.Class对象,以供Java程序使用。

执行引擎:执行引擎(Execution Engine)是负责执行字节码的核心组件。执行引擎的作用是读取字节码指令,对它们进行解析并执行,从而实现Java程序的功能

本地库接口:是Java虚拟机的一部分,它允许Java程序调用其他语言编写的本地应用程序和库(通常是C或C++)。这是因为Java本身设计为平台无关的语言,但它有时需要与特定平台的底层系统或硬件进行交互,而这通常是通过本地代码实现的。

直接内存(Direct Memory):直接内存并不是虚拟机运行时数据区的一部分,也不是Java虚拟机规范中定义的内存区域。但是这部分内存也被频繁地使用,而且也可能导致OutOfMemoryError异常出现。在JDK 1.4中新加入了NIO(New Input/Output)类,引入了一种基于通道(Channel)与缓冲区(Buffer)的I/O方式,可以使用Native函数库直接分配堆外内存,然后通过一个存储在Java堆中的DirectByteBuffer对象作为这块内存的引用进行操作。

说一下常见的垃圾回收算法 他们的特点是什么?

标记清除算法:将垃圾回收分为2个阶段,标记和清除。内存碎片高,效率高,清除快
标记整理算法:一般用于老年代,将存活的对象都向另一端移动,然后清理边界以外的垃圾。[对象要移动,效率低],效率慢一些 需要把内存碎片移动,内存碎片没有 但是效率低一点
复制算法:将原有的内存空间一分为二,每次只用其中一块,正在使用的对象复制到另一个内存中,然后将该内存空间清空,交换两个内存的角色,完成垃圾回收。

被标记的是没有被回收的

说一下常见的垃圾回收器以及他们的特点垃圾收集器是垃圾收集算法的具体实现

串行垃圾收集器[新生代区]:指使用单线程进行垃圾回收(用户请求不能访问STW),堆内存较小适合个人电脑。底层用的复制算法
并行垃圾收集器:JDK8默认使用此垃圾回收器,在垃圾回收时,多个线程在工作(用户请求不能访问STW),并且java应用中所有线程都要暂停,等待垃圾回收的完成。底层用的复制算法
CMS(并发[Concurrent Mark Sweep])垃圾收集器:是一款并发的、使用标记—清除算法的垃圾回收器(针对老年代) 初始标记 → 并发标记 → 并发预清理 → 最终标记 → 并发清除 → 并发重置 小于8G内存用CMS 一般用并发垃圾收集器配合收集年轻小内存
G1垃圾收集器[复制算法]:Eden(2M)、最大回收停顿时间、大内存适合用G1、大于8G内存用G1

说一下cms的各个阶段过程以及特点CMS一般都清理老年代

过程:初始标记 → 并发标记 → 并发预清理 → 最终标记 → 并发清除 → 并发重置
是一款以获取最短回收停顿时间为目标的收集器,停顿时间短,用户体验就好。
其最大特点是在进行垃圾回收时,应用仍然能正常运行

初始标记:找GCRoot根去的第一层对象[STW] 速度是非常快的。为什么STW?如果找的时候同时也要标记 那么若这时候有的未被标记那就有大BUG。
并发标记:从第一层找到后的之后那些对象不能被回收(CMS回收器会遍历老年代,标记出所有活动的对象),在这个阶段,应用程序线程与垃圾回收线程并发运行[能接收用户请求],会产生新的对象。
并发预清理:这个阶段也是并发执行的,目的是处理在并发标记阶段应用程序线程产生的新垃圾,CMS回收器会清理那些在并发标记阶段被修改的对象,并执行一些预清理工作,以减少下一个STW阶段的暂停时间。
最终标记:这个阶段是STW的,它是为了处理在并发标记和并发预清理阶段未被处理的对象。CMS回收器会完成所有剩余的标记工作,确保所有存活的对象都被正确标记
并发清除:在这个阶段,应用程序线程与垃圾回收线程再次并发运行。CMS回收器会清除未被标记的对象,释放内存空间。清除过程中不会移动存活对象,因此可能会产生内存碎片。
并发重置:这个阶段是并发执行的,目的是重置CMS数据结构,为下一次垃圾回收做准备。

说一下g1的各个阶段过程以及特点,

划分成多个区域,每个区域都可以充当eden,survivor,old,humongous,其中humongous专为大对象准备
分成三个阶段:新生代回收(STW)、并发标记(重新标记STW)、混合收集。如果并发失败(即回收速度赶不上创建新对象速度),就会触发Full GC

你们项目是用哪一个垃圾收集器,为什么用这个?

每个服务两个节点 8G4核 用CMS 太大就用G1

什么样的对象会被成为垃圾对象?

如果一个或多个对象没有任何的引用指向它了,那么这个对象现在就是垃圾,如果定位了垃圾,则有可能会被垃圾回收器回收

怎么确定什么是垃圾?
  • 引用计数法
    一个对象被引用了一次,在当前的对象头上递增一次引用次数,如果这个对象的引用次数为0,代表这个对象可回收

  • 可达性分析算法

    1. 标记阶段:从GCRoots开始,标记所有可达的对象。
    2. 回收阶段:遍历堆中的所有对象,回收那些未被标记的对象所占用的空间。

    采用的都是通过可达性分析算法来确定哪些内容是垃圾

    通过可达性分析算法,那些从任何GCRoots都无法到达的对象被认为是不可达的,因此可以被垃圾回收器回收。这种方法能够处理循环引用的情况,这是引用计数法所无法解决的

静态的成员变量[元空间 基本不会被回收] 局部变量 成员属性

说一下双亲委派机制以及优点

加载某一个类,先委托上一级的加载器进行加载,如果上级加载器也有上级,则会继续向上委托,如果该类委托上级没有被加载,子加载器尝试加载该类
优点:

  • 通过双亲委派机制可以避免某一个类被重复加载,当父类已经加载后则无需重复加载,保证唯一性
  • 为了安全,保证类库API不会被修改

用相同的加载器在同一个路径下只能用同一个路径的类 用不同的加载器就可以加载

如何打破双亲委派模型?

自定义类加载器

  • 通过自定义类加载器,并重写loadClass方法,可以实现在加载类时不遵循双亲委派模型。在自定义类加载器中,你可以直接尝试加载类,而不是先委派给父类加载器。

    public class CustomClassLoader extends ClassLoader {
        @Override
        public Class<?> loadClass(String name) throws ClassNotFoundException {
            // 直接加载,不委派给父类加载器
            return findClass(name);
        }
    }
    

jvm常用的参数有哪些?代表什么意思

jps 进程状态信息
jstack 查看进程内线程的堆栈信息(产生死锁也可以查看)
jmap 查看堆栈信息(生成堆栈内存快照,内存使用信息)
jhat 堆转存快照分析工具
jstat JVM统计监测工具

  1. -Xms-Xmx
    • -Xms:设置JVM堆内存的初始大小(最小值)。
    • -Xmx:设置JVM堆内存的最大大小。
  2. -Xss
    • 设置每个线程的堆栈大小。
  3. -XX:NewSize-XX:MaxNewSize
    • -XX:NewSize:设置新生代内存的初始大小。
    • -XX:MaxNewSize:设置新生代内存的最大大小。
  4. -XX:SurvivorRatio
    • 设置新生代中Eden区与Survivor区的容量比例。
  5. -XX:PermSize-XX:MaxPermSize(Java 8及之前版本):
    • -XX:PermSize:设置永久代(方法区)的初始大小。
    • -XX:MaxPermSize:设置永久代的最大大小。
  6. 设置元空间大小
    1. -XX:MetaspaceSize
      • 设置元空间的初始大小。当元空间耗尽时,JVM会尝试扩展元空间的大小,直到达到最大值(如果设置了的话)。
    2. -XX:MaxMetaspaceSize
      • 设置元空间的最大大小。如果没有设置这个参数,元空间的大小只受本地内存限制。

四大引用分别是什么,代表什么意思?

强引用:只要所有 GC Roots 能找到,就不会被回收。
软引用:需要配合SoftReference使用,当垃圾多次回收,内存依然不够时候会回收软引用对象
弱引用:需要配合WeakReference使用,只要进行了垃圾回收,就会把引用对象回收
虚引用:必须配合引用队列使用,被引用对象回收时,会将虚引用入队由 Reference Handler 线程调用虚引用相关方法释放直接内存

● 强引用指的就是代码中普遍存在的赋值方式,比如A 但是不a = new A()这种。强引用关联的对象,永远不会被GC回收。
● 软引用可以用SoftReference来描述,指的是那些有用是必须要的对象。系统在发生内存溢出前会对这类引用的对象进行回收。
● 弱引用可以用WeakReference来描述,他的强度比软引用更低一点,只要进行了垃圾回收,就会把引用对象回收。
● 虚引用他必须和ReferenceQueue一起使用,必须配合引用队列使用,被引用对象回收时,
会将虚引用入队由 Reference Handler 线程调用虚引用相关方法释放直接内存。

什么是内存溢出?什么是内存泄露?

内存溢出:内存溢出是指程序在申请内存时,没有足够的内存空间供其使用,导致所需要的内存超出了系统所能提供的最大内存。【会先进行一次GC实在不够就OOM 堆\栈(递归过多,局部变量过多)\方法区都有可能发生内存溢出】
内存泄露ThreadLocal会导致:内存泄露是指程序中已分配的内存由于某种原因未能释放,即使在不再需要这些内存的情况下,它们仍然保持分配状态,导致可用内存逐渐减少

你的项目中出现过内存泄漏吗?你是怎么排查并且解决的?

有的,内存泄漏通常是指堆内存,通常是指一些大对象不被回收的情况
1、通过jmap或设置jvm参数获取堆内存快照dump
2、通过工具,VisualVM去分析dump文件,VisualVM可以加载离线的dump文件
3、通过查看堆信息的情况,可以大概定位内存溢出是哪行代码出了问题
4、找到对应的代码,通过阅读上下文的情况,进行修复即可

异常的祖先是:Throwable

怎么解决cpu飙高

使用top命令查看占用cpu的情况
通过top命令查看后,可以查看是哪一个进程占用cpu较高
使用ps命令查看进程中的线程信息
使用jstack命令查看进程中哪些线程出现了问题,最终定位问题

  • 使用top命令查看占用cpu的情况哪个进程占用的cpu最高

    finalShell中输入 top

  • 查看进程中的线程信息 ps H -eo pid,tid,%cpu | gerp pid

  • jstack 查看进程内线程的堆栈信息产生死锁可以查看

    因为是十六进程所以要十进程转换十六进程
    直接linux输入 printf "%x\n" Pid
    然后就可以根据十六进制的去找哪个线程cpu占用
    之后查看文件是cat xxx

项目中遇到的难点

1.从业务角度 曾经做过保险项目 金融保险领域 行外人需要花很多时间了解需求还有同事的帮助才懂得需求知道如何去写

阅读全文

JL

2025/2/7

招聘流程

主流网站:Boss直聘
先过系统筛选[写本科 写计算机专业 过筛选 得到面试机会]
HR会帮我们的 需要KPI当月目标和业绩
黑马课程对标3年工作经验 大厂211都不好进

每次都要微信问一下HR:**技术面** 还是 人力面
可以打听面试官侧重点,问一下是领导还是主管还是技术组长

不要乱讲话!!不要乱讲话!!不要乱讲话!!不要乱讲话!!

学信网可查吗? 先说可查 若让 发编号 就下一家 [可填专科吗???]


简历怎么写好?

基本信息 籍贯 工作年限 求职岗位(Java开发工程师) 薪资(面议)
薪资范围6-18k 拉大范围 入职时间:一周内到岗
电话必写 邮箱也必写

面试问你什么时候能到岗3个工作日左右 / (3天左右)

image-20250207105915015

教育经历
25届 19-23 也可以大三出去实习 21-24
在校:往前推一年
学校 + 专业

工作经历
每一家一年多 建议2个公司
上一家公司距离现在 不能有很空窗时期【实时修改工作经验时间

上周离职
有没有面试其他?
没有 我也是刚好碰到贵公司 有 拿到了offer但是我还是意愿最大贵公司喜欢贵公司的环境

开发技能 [15条以上]
细节引导面试官去书写开发技能
记得要设置陷阱
从优先级高到低写
前端一定要写

Java基础先写到前面
大数据统计至少要 3年经验 4个项目经验

项目周期:时间控制在5-8个月
第一个项目经历至少7-8点 需要业务+技术点
第二个项目至少5-6条 达到项目一的七成
第三四项目至少3条 减量不减质【不要龙头蛇尾】

2025.2.10号中午收集好简历

文件名称:**Java开发工程师 _ 3年 _ 潘春尧**.pdf



求职意向

期望城市:不限
期望岗位:Java开发工程师
期望薪资:面议
到岗时间:一周内/随时
写虚岁!!

教育经历

民教网3年:2018.9-2022.7 xxx大学 专业:不相关可以不写

学信网
25届:实习
23-24届:1-2年

工作经历(一家公司超过两年左右) 5-8个月 / 一个项目

xxx公司(找自己熟悉城市,不要找在线招聘会被HR直接问对面招聘中的HR)

没有社保:不写期望城市
社保:写社保所在地
公司开业时间、小公司(100人以内)、科技有限公司、上班地点交通方式、背景调查写洪哥、法人(老板)、公司地址

背调:模板
写纸质版信息表 出生年月日 → 写真实的日期

第一步:写纸质信息表(证明人:电话)洪哥敏姐
背调信息:发给洪哥敏姐

hr会根据你给的电话直接
上级领导:项目
第三者:朋友/同学/同事 [可以拒绝回答很多]

第三方背调:查社保 查工作记录 查学信网

带身份证去的话 证明楼下的大厦需要登记身份信息才能进



Boss上的投递

上传PDF版本的简历
工作经历 → 对该公司隐藏我的简历

文件名称:**潘春尧-本科-3年-Java开发工程师**.pdf

尽量先从远的地方投递简历 试试状态!最想去广州的留到最后状态好的时候再投递

外包公司:跟培训的学生匹配 哈哈哈!



五险一金:基本养老保险[工作里面累计交满20年退休时领退休金] 、 基本医疗保险 [总部上海现在在广州一般都是本地医保方便]、 失业保险[工作满一年后因公司被辞退才可以领取] 、 工伤保险 、 生育保险 及 住房公积金

【要找公积金的公司】公积金5% → 买房可以用公积金来贷款;退休后这笔钱可以一次性提取出来养老;装修/租房的名义提取(半年一次)

面试题问:上家公司的工资待遇 → 城市最低标准买的
10k扣除五险一金 保险400-500 公积金大概300-400左右

个人所得税:不超过3.6w就是3%,每个月财务都会扣除发放税后工资,起征点5k,超过5k的部分才会交税。10k以内的大概是3%,

面试:公司有绩效考核 我们没有很明确的考核 日常都能完成工作量

入职当天要签劳务合同 1年或3年?
区别是什么?
1年的劳动合同只能定1个月试用期 2年2个月 3年3个月
大概率试用期8-9折 都是3-6个月

签订劳动合同可以离职 末尾淘汰在法律上不允许
转正以后被裁可以n+1个月 n代表年 试用期被辞退会赔偿半个月薪资

民教网(1000人以下的公司) 主攻针对小型企业 和 二三线城市
浙江→绍兴杭州
远公司约线上公司

现在在外地 能不能开一轮的线上面试 如果有需求我还是能过去的
阅读全文

ElasticSearch

2025/1/3

ElasticSearch结合Kibana、Logstash、Beats,核心是elastic stack的核心,负责存储、搜索、分析数据
Lucene的优势:容易扩展、高性能(基于倒排索引)
Lucene的缺点:直选与java语言开发
ElasticSearch是基于Lucene开发的

Elasticsearch:开源的分布式搜索引擎,可以用来实现搜索、日志统计、分析、系统监控等功能

正向索引和倒排索引

传统数据库是正向索引

ElasticSearch采用倒排索引

  • 文档(document):每条数据就是一个文档
  • 词条(term):文档按照语义分成词语
id title price
1 小米手机 3499
2 华为手机 4999
3 华为小米充电器 49
4 小米手环 299

↓↓↓↓ ↓↓ ↓↓ ↓↓ ↓↓ ↓↓ ↓↓ ↓↓ ↓↓ ↓↓ ↓↓ ↓↓ ↓↓ ↓↓ ↓↓ ↓↓ ↓↓ ↓↓ ↓↓ ↓↓ ↓↓

词条(term) 文档id
小米 1,3,4
手机 1,2
华为 2,3
充电器 3
手环 4

过程:搜索华为手机 → 得到:华为手机两个词条 → 得到每个词条所在文档id:华为:2,3 手机:1,2 → 得到id为1,2,3的文档 → 存入结果集
倒排索引:对文档内容分词,对词条创建索引,并记录词条所在文档的信息。查询时现根据词条查询到文档id,而后获取到文档
正排索引:基于文档id创建索引。查询词条时必须先找到文档,而后判断是否包含词条

文档

ElasticSearch是面向文档存储的,可以是数据库中的一条商品数据,一个订单信息,文档数据会被序列化为json格式后存储在ElasticSearch中
在ElasticSearch中

  • **索引(index)**:相同类型的文档的集合
  • **映射(mapping)**:索引中文档的字段约束信息,类似表的结构约束
概念对比
MySQL ElasticSearch 说明
Table Index 索引(index),就是文档的集合,类似于数据库的表(table)
Row Document 文档(Document),就是一条条的数据,类似数据库中的行(Row),文档都是JSON格式
Column Field 字段(Field),就是JSON文档中的字段,类似数据库中的列(Column)
Schema Mapping Mapping(映射),就是索引中文档的约束,例如字段类型约束。类似数据库的表结构(Schema)
SQL DSL DSL是ElasticSearch提供的JSON风格的请求语句,用来操作ElasticSearch,实现CRUD
架构

MySQL写操作:擅长事务类型操作,可以确保数据的安全和一致性
ElasticSearch查询:擅长海量数据的搜索、分析、计算
[可以互补达到数据双写一致性]

1.1.创建网络

因为我们还需要部署kibana容器,因此需要让es和kibana容器互联。这里先创建一个网络(创建过的不用再创建):

docker network create es-net

1.2.加载镜像

这里我们采用elasticsearch的7.12.1版本的镜像,这个镜像体积非常大,接近1G。不建议大家自己pull。

课前资料提供了镜像的tar包:

image-20210510165308064

大家将其上传到虚拟机中,然后运行命令加载即可:

# 导入数据
docker load -i es.tar

同理还有kibana的tar包也需要这样做。

1.3.运行

运行docker命令,部署单点es:

docker run -d \
    --name es \
    -e "ES_JAVA_OPTS=-Xms512m -Xmx512m" \
    -e "discovery.type=single-node" \
    -v es-data:/usr/share/elasticsearch/data \
    -v es-plugins:/usr/share/elasticsearch/plugins \
    --privileged \
    --network es-net \
    -p 9200:9200 \
    -p 9300:9300 \
elasticsearch:7.12.1

命令解释:

  • -e "cluster.name=es-docker-cluster":设置集群名称
  • -e "http.host=0.0.0.0":监听的地址,可以外网访问
  • -e "ES_JAVA_OPTS=-Xms512m -Xmx512m":内存大小
  • -e "discovery.type=single-node":非集群模式
  • -v es-data:/usr/share/elasticsearch/data:挂载逻辑卷,绑定es的数据目录
  • -v es-logs:/usr/share/elasticsearch/logs:挂载逻辑卷,绑定es的日志目录
  • -v es-plugins:/usr/share/elasticsearch/plugins:挂载逻辑卷,绑定es的插件目录
  • --privileged:授予逻辑卷访问权
  • --network es-net :加入一个名为es-net的网络中
  • -p 9200:9200:端口映射配置

在浏览器中输入:http://192.168.xxx.xxx:9200 即可看到elasticsearch的响应结果:

image-20210506101053676

2.部署kibana

kibana可以给我们提供一个elasticsearch的可视化界面,便于我们学习。

2.1.部署

运行docker命令,部署kibana,同理先加载镜像: docker load -i kibana.tar,然后启动:

docker run -d \
--name kibana \
-e ELASTICSEARCH_HOSTS=http://es:9200 \
--network=es-net \
-p 5601:5601  \
kibana:7.12.1
  • --network es-net :加入一个名为es-net的网络中,与elasticsearch在同一个网络中
  • -e ELASTICSEARCH_HOSTS=http://es:9200":设置elasticsearch的地址,因为kibana已经与elasticsearch在一个网络,因此可以用容器名直接访问elasticsearch
  • -p 5601:5601:端口映射配置

kibana启动一般比较慢,需要多等待一会,可以通过命令:

docker logs -f kibana

查看运行日志,当查看到下面的日志,说明成功:

image-20210109105135812

此时,在浏览器输入地址访问:http://192.168.xxx.xxx:5601,即可看到结果

分词效果概览 Dev Tools - Elastic

GET /_analyze
{
“analyzer”: “standard”,
“text”: “黑马程序员”
}

{
  "tokens" : [
    {
      "token" : "黑",
      "start_offset" : 0,
      "end_offset" : 1,
      "type" : "<IDEOGRAPHIC>",
      "position" : 0
    },
    {
      "token" : "马",
      "start_offset" : 1,
      "end_offset" : 2,
      "type" : "<IDEOGRAPHIC>",
      "position" : 1
    },
    {
      "token" : "程",
      "start_offset" : 2,
      "end_offset" : 3,
      "type" : "<IDEOGRAPHIC>",
      "position" : 2
    },
    {
      "token" : "序",
      "start_offset" : 3,
      "end_offset" : 4,
      "type" : "<IDEOGRAPHIC>",
      "position" : 3
    },
    {
      "token" : "员",
      "start_offset" : 4,
      "end_offset" : 5,
      "type" : "<IDEOGRAPHIC>",
      "position" : 4
    }
  ]
}

如果是分析中午就不能用它原有的,存在明显的问题:将中文逐字分词,没有任何业务语义,因此需要借助专业的分词器

3.安装IK分词器

3.1.在线安装ik插件(较慢)

# 进入容器内部
docker exec -it elasticsearch /bin/bash

# 在线下载并安装
./bin/elasticsearch-plugin  install https://github.com/medcl/elasticsearch-analysis-ik/releases/download/v7.12.1/elasticsearch-analysis-ik-7.12.1.zip

#退出
exit
#重启容器
docker restart elasticsearch

3.2.离线安装ik插件(推荐)

1)查看数据卷目录

安装插件需要知道elasticsearch的plugins目录位置,而我们用了数据卷挂载,因此需要查看elasticsearch的数据卷目录,通过下面命令查看:

docker volume inspect es-plugins

显示结果:

[
    {
        "CreatedAt": "2022-05-06T10:06:34+08:00",
        "Driver": "local",
        "Labels": null,
        "Mountpoint": "/var/lib/docker/volumes/es-plugins/_data",
        "Name": "es-plugins",
        "Options": null,
        "Scope": "local"
    }
]

说明plugins目录被挂载到了:/var/lib/docker/volumes/es-plugins/_data 这个目录中。

2)解压缩分词器安装包

下面我们需要把课前资料中的ik分词器解压缩,重命名为ik

image-20210506110249144

3)上传到es容器的插件数据卷中

也就是/var/lib/docker/volumes/es-plugins/_data

image-20210506110704293

4)重启容器

# 4、重启容器
docker restart es
# 查看es日志
docker logs -f es

5)测试:

IK分词器包含两种模式

  • ik_smart:最少切分

  • ik_max_word:最细切分

GET /_analyze
{
  "analyzer": "ik_max_word",
  "text": "黑马程序员学习java太棒了"
}

结果:

{
  "tokens" : [
    {
      "token" : "黑马",
      "start_offset" : 0,
      "end_offset" : 2,
      "type" : "CN_WORD",
      "position" : 0
    },
    {
      "token" : "程序员",
      "start_offset" : 2,
      "end_offset" : 5,
      "type" : "CN_WORD",
      "position" : 1
    },
    {
      "token" : "程序",
      "start_offset" : 2,
      "end_offset" : 4,
      "type" : "CN_WORD",
      "position" : 2
    },
    {
      "token" : "员",
      "start_offset" : 4,
      "end_offset" : 5,
      "type" : "CN_CHAR",
      "position" : 3
    },
    {
      "token" : "学习",
      "start_offset" : 5,
      "end_offset" : 7,
      "type" : "CN_WORD",
      "position" : 4
    },
    {
      "token" : "java",
      "start_offset" : 7,
      "end_offset" : 11,
      "type" : "ENGLISH",
      "position" : 5
    },
    {
      "token" : "太棒了",
      "start_offset" : 11,
      "end_offset" : 14,
      "type" : "CN_WORD",
      "position" : 6
    },
    {
      "token" : "太棒",
      "start_offset" : 11,
      "end_offset" : 13,
      "type" : "CN_WORD",
      "position" : 7
    },
    {
      "token" : "了",
      "start_offset" : 13,
      "end_offset" : 14,
      "type" : "CN_CHAR",
      "position" : 8
    }
  ]
}

3.3 扩展词词典

注意当前文件的编码必须是 UTF-8 格式,严禁使用Windows记事本编辑,可以直接linux系统vi编辑

随着互联网的发展,“造词运动”也越发的频繁。出现了很多新的词语,在原有的词汇列表中并不存在。比如:“奥力给”,“传智播客” 等。

所以我们的词汇也需要不断的更新,IK分词器提供了扩展词汇的功能。

1)打开IK分词器config目录:/var/lib/docker/volumes/es-plugins/_data/ik/config/IKAnalyzer.cfg.xml

image-20210506112225508

2)在IKAnalyzer.cfg.xml配置文件内容添加:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
        <comment>IK Analyzer 扩展配置</comment>
      
        <entry key="ext_dict">ext.dic</entry> 
</properties>

3)新建一个 ext.dic,可以参考config目录下复制一个配置文件进行修改

传智播客
奥力给

4)重启elasticsearch

docker restart es

# 查看 日志
docker logs -f es

image-20201115230900504

日志中已经成功加载ext.dic配置文件

5)测试效果:

GET /_analyze
{
  "analyzer": "ik_max_word",
  "text": "传智播客Java就业超过90%,奥力给!"
}

注意当前文件的编码必须是 UTF-8 格式,严禁使用Windows记事本编辑,可以直接linux系统vi编辑

3.4 停用词词典

在互联网项目中,在网络间传输的速度很快,所以很多语言是不允许在网络上传递的,如:关于宗教、政治等敏感词语,那么我们在搜索时也应该忽略当前词汇。

IK分词器也提供了强大的停用词功能,让我们在索引时就直接忽略当前的停用词汇表中的内容。

1)IKAnalyzer.cfg.xml配置文件内容添加:

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE properties SYSTEM "http://java.sun.com/dtd/properties.dtd">
<properties>
        <comment>IK Analyzer 扩展配置</comment>
        <!--用户可以在这里配置自己的扩展字典-->
        <entry key="ext_dict">ext.dic</entry>
         <!--用户可以在这里配置自己的扩展停止词字典  *** 添加停用词词典-->
        <entry key="ext_stopwords">stopword.dic</entry>
</properties>

3)在 stopword.dic 添加停用词

习大大

4)重启elasticsearch

# 重启服务
docker restart es
docker restart kibana

# 查看 日志
docker logs -f es

日志中已经成功加载stopword.dic配置文件

5)测试效果:

GET /_analyze
{
  "analyzer": "ik_max_word",
  "text": "传智播客Java就业率超过95%,习大大都点赞,奥力给!"
}

注意当前文件的编码必须是 UTF-8 格式,严禁使用Windows记事本编辑



索引库操作

mapping属性
mapping是对索引库中文档的约束,常见的mapping属性包括:

• type:字段数据类型,常见的简单类型有:
• 字符串:text(可分词的文本)、keyword(精确值,例如:品牌、国家、ip地址)
• 数值:long、integer、short、byte、double、float
• 布尔:boolean
• 日期:date
• 对象:object
• index:是否创建索引,默认为true【默认倒排】
• analyzer:使用哪种分词器【只有text才需要分词】
• properties:该字段的子字段

{
    "age": 21,
    "weight": 52.1,
    "isMarried": false,
    "info": "黑马程序员Java讲师",
    "email": "zy@itcast.cn",
    "score": [99.1, 99.5, 98.9],
    "name": {
        "firstName": "云",
        "lastName": "赵"
    }
}
索引库操作
创建索引库和mapping的请求语法
PUT /索引库名称
{
  "mappings": {
    "properties": {
      "字段名":{
        "type": "text",
        "analyzer": "ik_smart"
      },
      "字段名2":{
        "type": "keyword",
        "index": "false"
      },
      "字段名3":{
        "properties": {
          "子字段": {
            "type": "keyword"
          }
        }
      },
      // ...略
    }
  }
}

↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓ ↓

# 创建索引库
PUT /heima
{
  "mappings": {
    "properties": {
      "info":{
        "type": "text",
        "analyzer": "ik_max_word"
      },
      "email":{
        "type": "keyword",
        "index": false
      },
      "name":{
        "type": "object",
        "properties": {
          "firstName":{
            "type": "keyword"
          },
          "lastName":{
            "type": "keyword"
          }
        }
      }
    }
  }
}
---------------------------------------------------------------------
{
  "acknowledged" : true,
  "shards_acknowledged" : true,
  "index" : "heima"
}

操作索引库禁止修改索引库(因为已经映射好了)

查看索引语法:

GET/索引名

删除索引库的语法:

DELETE/索引库名

可以在修改索引的过程中添加新的字段
PUT /索引库名/_mapping
{
  "properties": {
    "新字段名":{
      "type": "integer"
    }
  }
}
---------------------------
PUT /heima/_mapping
{
  "properties": {
    "age":{
      "type": "integer"
    }
  }
}

当您使用Elasticsearch(ES)的PUT请求创建索引库时,这个索引库实际上是存储在Elasticsearch集群的节点上的。如果您的Elasticsearch集群是安装在Linux服务器上的,那么是的,索引库会被创建在Linux文件系统中。

Elasticsearch为每个索引分配一个或多个主分片,并为每个主分片分配一个或多个副本分片。这些分片实际上是存储在Elasticsearch节点的文件系统上的。具体来说,索引数据存储在以下路径:

复制

/path/to/elasticsearch/data/nodes/<node-id>/<index>/<shard-id>

这里的/path/to/elasticsearch是Elasticsearch的安装路径,data目录是默认的数据存储位置,nodes目录包含了集群中各个节点的数据,<node-id>是节点的唯一标识,<index>是您创建的索引名称,而<shard-id>则是分片的ID。

索引库的增删改查汇总


# 创建索引库
PUT /heima
{
  "mappings": {
    "properties": {
      "info":{
        "type": "text",
        "analyzer": "ik_max_word"
      },
      "email":{
        "type": "keyword",
        "index": false
      },
      "name":{
        "type": "object",
        "properties": {
          "firstName":{
            "type": "keyword"
          },
          "lastName":{
            "type": "keyword"
          }
        }
      }
    }
  }
}

# 查询
GET /heima

# 修改索引库
PUT /heima/_mapping
{
  "properties":{
    "age":{
      "type": "integer"
    }
  }
}

# 修改
DELETE /heima
文档操作——添加文档
# 每次写操作的时候 版本会增加 "_version ++"

# 插入文档
POST /heima/_doc/1
{
  "info": "广州黑马198班",
  "email": "390415049@qq.com",
  "name":{
    "firstName": "春",
    "lastName": "尧"
  }
}

# 查询文档
GET /heima/_doc/1

# 删除文档
DELETE /heima/_doc/1
文档操作——修改文档
方式一:全量修改,会删除旧文档,添加新文档
PUT /索引库名/_doc/文档id
{
    "字段1": "值1",
    "字段2": "值2",
    // ... 略
}
-------------------------
PUT /heima/_doc/1
{
    "info": "黑马程序员高级Java讲师",
    "email": "zy@itcast.cn",
    "name": {
        "firstName": "云",
        "lastName": "赵"
    }
}


# 全量修改文档
POST /heima/_doc/1
{
  "info": "广州黑马198班",
  "email": "90415049@qq.com",
  "name":{
    "firstName": "尧",
    "lastName": "春"
  }
}
方式二:增量修改,修改指定字段值
POST /索引库名/_update/文档id
{
    "doc": {
         "字段名": "新的值",
    }
}
-------------------------
POST /heima/_update/1
{
  "doc": {
    "email": "ZhaoYun@itcast.cn"
  }
}


# 局部修改文档
POST /heima/_update/1
{
  "doc": {
    "email": "ZYun@itcast.cn"
  }
}
总结

文档操作有哪些?

  • 创建文档:POST /索引库名/_doc/文档id { json文档 }

  • 查询文档:GET /索引库名/_doc/文档id

  • 删除文档:DELETE /索引库名/_doc/文档id

  • 修改文档:

    • 全量修改:PUT /索引库名/_doc/文档id { json文档 }

    • 增量修改:POST /索引库名/_update/文档id { “doc”: {字段}}

JavaRestClient

Elasticsearch目前最新版本是8.0,其Java客户端有很大变化。不过大多数企业使用的还是8以下版本,所以我们选择使用早期的JavaRestClient客户端来学习。官方文档地址:Elasticsearch Clients | Elastic

阅读全文

MyBatisPlus

2024/11/23

MyBatis-Plus
简介 | MyBatis-Plus

引入MybatisPlus起步依赖写依赖+认爸爸
  • MyBatisPlus官方提供了starter,其中集成了Mybatis和MybatisPlus的所有功能,并且实现了自动装配效果。因此我们可以用MybatisPlus的starter代替Mybatis的starter:
<!--MybatisPlus-->
<dependency>    
    <groupId>com.baomidou</groupId>    
    <artifactId>mybatis-plus-boot-starter</artifactId> 
    <version>3.5.3.1</version>
</dependency>
  • 自定义的Mapper继承MybatisPlus提供的BaseMapper接口
public interface UserMapper extends BaseMapper<User> {
}
UserMapper.java

public interface UserMapper extends BaseMapper<User> {
UserMapper.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.itheima.mp.mapper.UserMapper">

</mapper>

常用注解

  • @TableName:用来指定表名
  • @Tableld:用来指定表中的主键字段信息
    • IdType枚举
      • AUTO:数据库自增长
      • INPUT:通过set方法自行输入
      • ASSIGN_ID分配ID 默认实现类是雪花算法
  • @TableField:用来指定表中的普通字段信息默认驼峰转下划线,不一致需要改
    • 成员变量名与数据库字段名不一致
    • 成员变量名是以is开头,且是布尔值isMarried
    • 成员变量名与数据库关键字冲突order
    • 成员变量不是数据库字段address,要标记不存在不然会默认数据库字段
@Data
public class User {
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;
    
    @TableField("username")
    private String name;

    @TableField("is_married")
    private Boolean isMarried;  // is经过反射会默认变成变量名Married
    
    @TableField("`order`")
    private Integer order;
    
    @TableField(exist = false)
    private String address;
......
}

此时如果数据库的表名是:tb_user 就需要用到 @TableName

数据库名:tb_user (用户表)
#  名称
1  id
2  username
3  is_married
4  order

常见配置

MyBatisPlus的配置项继承了MyBatis原生配置和一些自己特有的配置
MP更擅长单表的增删改查,如果是多表还是推荐用xml

mybatis:
  mapper-locations: classpath*:mapper/*.xml # Mapper.xml文件地址,默认值
  type-aliases-package: com.itheima.po # 别名扫描包
  configuration:
    map-underscore-to-camel-case: true # 开启驼峰命名自动映射
    cache-enabled: false # 是否开启二级缓存
  global-config:
    db-config:
      id-type: assign_id # id为雪花算法生成
      update-strategy: not_null # 更新策略:只更新非空字段 类似于动态sql

MyBatisPlus使用的基本流程

  • 引入起步依赖
  • 自定义Mapper基础BaseMapper
  • 在实体类上添加注释声明 表信息
  • 在application.yml中根据需要添加配置

核心功能—条件构造器

条件构造器

MyBatisPlus支持各种复杂的where条件,满足日常开发的所有需求

  • 查询出名字中带o的,存款大于等于1000元的人的id、username、info、balance字段
# 原始SQL:
SELECT id,username,info,balance
FROM user
WHERE username LIKE ? AND balance >= ?
// MyBatisPlus:
@Test
    void testQueryWrapper(){
        // 1.构建查询条件
        QueryWrapper<User> wrapper = new QueryWrapper<User>()
                .select("id", "username", "phone")
                .like("username", "o")
                .ge("balance", 1000);
        // 2.查询
        List<User> users = userMapper.selectList(wrapper);
        users.forEach(System.out::println);
    }
// MyBatisPlus Lambda编码格式(解决硬编码):
 @Test
    void testLambdaQueryWrapper(){
        // 1.构建查询条件
        LambdaQueryWrapper<User> wrapper = new LambdaQueryWrapper<User>()
                // 利用反射 解决字符串硬编码
                .select(User::getId, User::getUsername, User::getPhone)
                .like(User::getUsername, "o")
                .ge(User::getBalance, 1000);
        // 2.查询
        List<User> users = userMapper.selectList(wrapper);
        users.forEach(System.out::println);
    }

-------------------------------------------------------------------------
@Test
void testLambdaQueryWrapperWithQueryWrapper(){
    // 1. 构建查询条件
    QueryWrapper<User> queryWrapper = new QueryWrapper<User>();
    LambdaQueryWrapper<User> wrapper = queryWrapper.lambda()
            .select(User::getId, User::getUsername, User::getPhone)
            .like(User::getUsername, "o")
            .ge(User::getBalance, 1000);

    // 2. 查询
    List<User> users = userMapper.selectList(wrapper);
    users.forEach(System.out::println);
}
  • 更新用户名为jack的用户的余额为2000
# 原始SQL:
UPDATE user
    SET balance = 2000
    WHERE (username = "jack")
// MyBatisPlus:
@Test
    void testUpdateByQueryWrapper(){
        // 1.要更新的数据
        User user = new User();
        user.setBalance(2000);
        // 2.更新的条件
        QueryWrapper<User> wrapper = new QueryWrapper<User>().eq("username", "jack");
        // 3.执行更新
        userMapper.update(user, wrapper);
    }
  • 更新id为1,2,4的用户的余额,扣200
# 原始SQL:
UPDATE user
    SET balance = balance - 200
    WHERE id in (1,2,4)
// MyBatisPlus:
@Test
    void testUpdateWrapper(){
        List<Long> ids = List.of(1L, 2L, 4L);
        UpdateWrapper<User> wrapper = new UpdateWrapper<User>()
                .setSql("balance = balance - 100")
                .in("id", ids);
        userMapper.update(null, wrapper);
    }
条件构造器的用法:
  • QueryWrapper和LambdaQueryWrapper通常用来构建select、delete、update的where条件部分

  • UpdateWrapper和LambdaUpdateWrapper通常只有在set语句比较特殊才使用

  • 尽量使用LambdaQueryWrapper和LambdaUpdateWrapper,避免硬编码

4. 字段映射与表名映射

4.1 问题一:表字段与编码属性设计不同步

  • 在模型类属性上方,使用**@TableField**属性注解,通过==value==属性,设置当前属性对应的数据库表中的字段关系。

1683796001750

4.2 问题二:编码中添加了数据库中未定义的属性

  • 在模型类属性上方,使用**@TableField注解,通过==exist==**属性,设置属性在数据库表字段中是否存在,默认为true。

1683796121907

4.3 问题三:表名与编码开发设计不同步

  • 模型类上方,使用**@TableName注解,通过==value==**属性,设置当前类对应的数据库表名称。

1683798660359

四、主键生成策略

id主键生成的策略有哪几种方式?

不同的表应用不同的id生成策略

  • 日志:自增(1,2,3,4,……)
  • 购物订单:特殊规则(FQ23948AK3843)
  • 外卖单:关联地区日期等信息(10 04 20200314 34 91)
  • 关系表:可省略id
  • ……

1 id生成策略控制(@TableId注解)

雪花算法:@TableId(type= IdType.ASSIGN_ID)
ASSIGN_UUID是趋势递增
用了分库分表就不能用默认的id自增了 要用雪花算法

  • 名称:@TableId

  • 类型:属性注解

  • 位置:模型类中用于表示主键的属性定义上方

  • 作用:设置当前类中主键属性的生成策略

  • 相关属性

    type:设置主键属性的生成策略,值参照IdType枚举值

    image-20210801192449901

2 全局策略配置

mybatis-plus:
  global-config:
    db-config:
      id-type: assign_id #全局设置主键id策略
      table-prefix: tbl_  #表名前缀设置
id生成策略全局配置

image-20210801183128266

表名前缀全局配置

image-20210801183157694

自定义SQL

我们可以利用MyBatisPlus的Wrapper来**构造复杂的where条件**,然后自己定义SQL语句中剩下的部分。

将id在指定范围的用户(1,2,4)的余额扣减指定值
<update id = "updateBalanceByIds">
    UPDATE user
    SET balance = balance - #{amount}
    WHERE id IN
    <foreach collection="ids" separator="," item="id" open="(" close=")">
    #{id}
    </foreach>
</update>
  • 基于Wrapper构建where条件
// 1.更新条件
  List<Long> ids = List.of(1L, 2L, 4L);
  int amount = 200;
// 2.定义条件
  QueryWrapper<User> wrapper = new QueryWrapper<User>().in(User::getId, ids);
// 3.调用自定义SQL方法
  userMapper.updateBalanceByIds(wrapper, amount);
  • 在mapper方法参数中用Param注解声明wrapper变量名称,必须是ew
void updateBalanceByIds(@Param(Constants.WRAPPER) QueryWrapper<User> wrapper, @Param("amount") int amount);
  • 自定义SQL,并使用Wrapper条件
<update id="updateBalanceByIds">
        update user
        set balance = balance - #{amount} ${ew.customSqlSegment}
    </update>

IService接口基本用法

  • 自定义Service接口继承IService接口
package com.itheima.mp.service.impl;

import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.mp.domain.po.User;

public interface IUserService extends IService<User> {

}
  • 自定义Service实现类,实现自定义接口并继承ServiceImpl类
package com.itheima.mp.service.impl.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itheima.mp.domain.po.User;
import com.itheima.mp.mapper.UserMapper;
import com.itheima.mp.service.impl.IUserService;
import org.springframework.stereotype.Service;

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

}
  • 搞了个测试类@Test
package com.itheima.mp.service.impl;

import com.itheima.mp.domain.po.User;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

import java.time.LocalDateTime;
import java.util.Arrays;
import java.util.List;

@SpringBootTest
class IUserServiceTest {
    @Autowired
    private IUserService userService;

    @Test
    void testSaveUser() {
        User user = new User();
        user.setId(5L);
        user.setUsername("Lucy");
        user.setPassword("123");
        user.setPhone("18688990011");
        user.setBalance(200);
        user.setInfo("{\"age\": 24, \"intro\": \"英文老师\", \"gender\": \"female\"}");
        user.setCreateTime(LocalDateTime.now());
        user.setUpdateTime(LocalDateTime.now());
        userService.save(user);
    }
    @Test
    void testQuery(){
        List<User> users = userService.listByIds(Arrays.asList(1L, 2L, 3L));
        users.forEach(System.out::println);
    }

}

IService开发基础业务接口

编号 接口 请求方式 请求路径 请求参数 返回值
1 新增用户 POST /users 用户表单实体
2 删除用户 DELETE /users/{id} 用户id
3 根据id查询用户 GET /users/{id} 用户id 用户VO
4 根据id批量查询 GET /users 用户id集合 用户VO集合
5 根据id扣减余额 PUT /users/{id}/deduction/{money} •用户id •扣减金额

解决在IDEA 的Maven下 出现 Cannot access in offline mode 问题 - Doyourself! - 博客园

管理接口文档

UserController.java
package com.itheima.mp.controller;

import cn.hutool.core.bean.BeanUtil;
import com.itheima.mp.domain.po.User;

import com.itheima.mp.domain.dto.UserFormDTO;

import com.itheima.mp.domain.vo.UserVO;
import com.itheima.mp.service.IUserService;

import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import io.swagger.annotations.ApiParam;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@Api(tags = "用户管理接口")
@RequiredArgsConstructor
@RestController
@RequestMapping("users")
public class UserController {

    private final IUserService userService;

    @PostMapping
    @ApiOperation("新增用户")
    public void saveUser(@RequestBody UserFormDTO userFormDTO) {
        // 1.转换DTO为PO
        User user = BeanUtil.copyProperties(userFormDTO, User.class);
        // 2.新增
        userService.save(user);
    }

    @DeleteMapping("/{id}")
    @ApiOperation("删除用户")
    public void removeUserById(@PathVariable("id") Long userId) {
        userService.removeById(userId);
    }

    @GetMapping("/{id}")
    @ApiOperation("根据id查询用户")
    public UserVO queryUserById(@PathVariable("id") Long userId) {
        // 1.查询用户
        User user = userService.getById(userId);
        // 2.处理vo
        return BeanUtil.copyProperties(user, UserVO.class);
    }

    @GetMapping
    @ApiOperation("根据id集合查询用户")
    public List<UserVO> queryUserByIds(@RequestParam("ids") List<Long> ids) {
        // 1.查询用户
        List<User> users = userService.listByIds(ids);
        // 2.处理vo
        return BeanUtil.copyToList(users, UserVO.class);
    }

    @PutMapping("{id}/deduction/{money}")
    @ApiOperation("扣减用户余额")
    public void deductBalance(@ApiParam("用户id") @PathVariable("id") Long id, @ApiParam("扣减的金额") @PathVariable("money") Integer money) {
        userService.deductBalance(id, money);
    }
}
UserFormDTO.java
package com.itheima.mp.domain.dto;

import com.baomidou.mybatisplus.annotation.TableField;
import com.baomidou.mybatisplus.extension.handlers.JacksonTypeHandler;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

@Data
@ApiModel(description = "用户表单实体")
public class UserFormDTO {

    @ApiModelProperty("id")
    private Long id;

    @ApiModelProperty("用户名")
    private String username;

    @ApiModelProperty("密码")
    private String password;

    @ApiModelProperty("注册手机号")
    private String phone;

    @ApiModelProperty("详细信息,JSON风格")
    private String info;

    @ApiModelProperty("账户余额")
    private Integer balance;
}
UserQuery.java
package com.itheima.mp.domain.query;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

@Data
@ApiModel(description = "用户查询条件实体")
public class UserQuery {
    @ApiModelProperty("用户名关键字")
    private String name;
    @ApiModelProperty("用户状态:1-正常,2-冻结")
    private Integer status;
    @ApiModelProperty("余额最小值")
    private Integer minBalance;
    @ApiModelProperty("余额最大值")
    private Integer maxBalance;
}
UserServiceImpl.java
package com.itheima.mp.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itheima.mp.domain.po.User;
import com.itheima.mp.mapper.UserMapper;
import com.itheima.mp.service.IUserService;
import org.springframework.stereotype.Service;

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
    @Override
    public void deductBalance(Long id, Integer money) {
        // 1.查询用户
        User user = getById(id);
        // 2.判断用户状态
        if (user == null || user.getStatus() == 2) {
            throw new RuntimeException("用户状态异常");
        }
        // 3.判断用户余额
        if (user.getBalance() < money) {
            throw new RuntimeException("用户余额不足");
        }
        // 4.扣减余额
        baseMapper.deductMoneyById(id, money);
    }
}
package com.itheima.mp.mapper;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.baomidou.mybatisplus.core.toolkit.Constants;
import com.itheima.mp.domain.po.User;
import org.apache.ibatis.annotations.Param;
import org.apache.ibatis.annotations.Update;

import java.util.List;

public interface UserMapper extends BaseMapper<User> {

    List<User> queryUserByIds(@Param("ids") List<Long> ids);

    void updateBalanceByIds(@Param(Constants.WRAPPER) QueryWrapper<User> wrapper, @Param("amount") int amount);

    @Update("UPDATE user SET balance = balance - #{money} WHERE id = #{id}")
    void deductMoneyById(Long id, Integer money);
}

Iservice的Lambda方法

需求:实现一个根据复杂条件查询用户的接口,查询条件如下:

name:用户名关键字,可以为空
status:用户状态,可以为空
minBalance:最小余额,可以为空
maxBalance:最大余额,可以为空

<select id="queryUsers" resultType="com.itheima.mp.domain.po.User">
    SELECT *
    FROM tb_user
    <where>
        <if test="name != null">
            AND username LIKE CONCAT('%', #{name}, '%')
        </if>
        <if test="status != null">
            AND `status` = #{status}
        </if>
        <if test="minBalance != null and maxBalance != null">
            AND balance BETWEEN #{minBalance} AND #{maxBalance}
        </if>
    </where>
</select>
UserQuery.java
package com.itheima.mp.domain.query;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

@Data
@ApiModel(description = "用户查询条件实体")
public class UserQuery {
    @ApiModelProperty("用户名关键字")
    private String name;
    @ApiModelProperty("用户状态:1-正常,2-冻结")
    private Integer status;
    @ApiModelProperty("余额最小值")
    private Integer minBalance;
    @ApiModelProperty("余额最大值")
    private Integer maxBalance;
}
UserController.java
    @ApiOperation("根据复杂条件查询用户接口")
    @GetMapping("/list")
    public List<UserVO> queryUsers(UserQuery query) {
        // 1.查询用户PO
        List<User> users = userService.queryUsers(query.getName(), query.getStatus(), query.getMinBalance(), query.getMaxBalance());
        // 2.把po拷贝到vo
        return BeanUtil.copyToList(users, UserVO.class);
    }
IUserService.java
package com.itheima.mp.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.mp.domain.po.User;

import java.util.List;

public interface IUserService extends IService<User> {
    void deductBalance(Long id, Integer money);

    List<User> queryUsers(String name, Integer status, Integer minBalance, Integer maxBalance);
}
UserServiceImpl.java
package com.itheima.mp.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itheima.mp.domain.po.User;
import com.itheima.mp.mapper.UserMapper;
import com.itheima.mp.service.IUserService;
import org.springframework.stereotype.Service;

import java.util.List;

@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements IUserService {
   @Override
    public List<User> queryUsers(String name, Integer status, Integer minBalance, Integer maxBalance) {
        return lambdaQuery()
                .like(name!=null, User::getUsername,name)
                .eq(status!=null, User::getStatus,status)
                .gt(minBalance!=null, User::getBalance,minBalance) // 大于
                .lt(maxBalance!=null, User::getBalance,maxBalance) // 小于
                .list();
    }
}

IService的Lambda更新LambdaUpdate()、LambdaQuery()

改造根据id修改用户余额的接口,要求如下
  • 完成对用户状态校验

  • 完成对用户余额校验

  • 如果扣减后余额为0,则将用户status修改为冻结状态 (2)

    UserController.java
 @PutMapping("{id}/deduction/{money}")
    @ApiOperation("扣减用户余额")
    public void deductBalance(@ApiParam("用户id") @PathVariable("id") Long id, @ApiParam("扣减的金额") @PathVariable("money") Integer money) {
        userService.deductBalance(id, money);
    }
UserServiceImpl.java
 @Override
    public void deductBalance(Long id, Integer money) {
        // 1.查询用户
        User user = getById(id);
        // 2.判断用户状态
        if (user == null || user.getStatus() == 2) {
            throw new RuntimeException("用户状态异常");
        }
        // 3.判断用户余额
        if (user.getBalance() < money) {
            throw new RuntimeException("用户余额不足");
        }
        // 4.扣减余额 update tb_user set balance = balance - ?
        int remainBalance = user.getBalance() - money;
        lambdaUpdate()
                .set(User::getBalance, remainBalance)
                .set(remainBalance == 0,User::getStatus, 2)
                .eq(User::getId, id)
                .eq(User::getBalance, user.getBalance()) // 乐观锁
                .update();
    }

IService的批量新增

批量插入10万条用户数据,并作出对比:
  • 普通for循环插入4分钟
  • IService的批量插入30秒
  • 开启rewriteBatchedStatements=true参数【6秒】重写Statement语句,在application.yaml的sql中url拼接
Test  com/itheima/mp/service/IUserServiceTest.java
@Test
    void testSaveOneByOne() {
        long b = System.currentTimeMillis();
        for (int i = 1; i <= 100000; i++) {
            userService.save(buildUser(i));
        }
        long e = System.currentTimeMillis();
        System.out.println("耗时:" + (e - b));
    }

    private User buildUser(int i) {
        User user = new User();
        user.setUsername("user_" + i);
        user.setPassword("123");
        user.setPhone("" + (18688190000L + i));
        user.setBalance(2000);
        user.setInfo("{\"age\": 24, \"intro\": \"英文老师\", \"gender\": \"female\"}");
        user.setCreateTime(LocalDateTime.now());
        user.setUpdateTime(user.getCreateTime());
        return user;
    }
MyBatisPlus的批处理
@Test
void testSaveBatch() {
    // 准备10万条数据
    List<User> list = new ArrayList<>(1000);
    long b = System.currentTimeMillis();
    for (int i = 1; i <= 100000; i++) {
        list.add(buildUser(i));
        // 每1000条批量插入一次
        if (i % 1000 == 0) {
            userService.saveBatch(list);
            list.clear();
        }
    }
    long e = System.currentTimeMillis();
    System.out.println("耗时:" + (e - b));
}

可以看到使用了批处理以后,比逐条新增效率提高了10倍左右,性能还是不错的。

可以发现其实MybatisPlus的批处理是基于PrepareStatement的预编译模式,然后批量提交,最终在数据库执行时还是会有多条insert语句,逐条插入数据。SQL类似这样:

Preparing: INSERT INTO user ( username, password, phone, info, balance, create_time, update_time ) VALUES ( ?, ?, ?, ?, ?, ?, ? )
Parameters: user_1, 123, 18688190001, "", 2000, 2023-07-01, 2023-07-01
Parameters: user_2, 123, 18688190002, "", 2000, 2023-07-01, 2023-07-01
Parameters: user_3, 123, 18688190003, "", 2000, 2023-07-01, 2023-07-01

而如果想要得到最佳性能,最好是将多条SQL合并为一条,像这样:

INSERT INTO user ( username, password, phone, info, balance, create_time, update_time )
VALUES 
(user_1, 123, 18688190001, "", 2000, 2023-07-01, 2023-07-01),
(user_2, 123, 18688190002, "", 2000, 2023-07-01, 2023-07-01),
(user_3, 123, 18688190003, "", 2000, 2023-07-01, 2023-07-01),
(user_4, 123, 18688190004, "", 2000, 2023-07-01, 2023-07-01);

该怎么做呢?

MySQL的客户端连接参数中有这样的一个参数:rewriteBatchedStatements。顾名思义,就是重写批处理的statement语句。参考文档:

https://dev.mysql.com/doc/connector-j/8.0/en/connector-j-connp-props-performance-extensions.html#cj-conn-prop_rewriteBatchedStatements

这个参数的默认值是false,我们需要修改连接参数,将其配置为true

修改项目中的application.yml文件,在jdbc的url后面添加参数&rewriteBatchedStatements=true:

spring:
  datasource:
    url: jdbc:mysql://127.0.0.1:3306/mp?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&serverTimezone=Asia/Shanghai&rewriteBatchedStatements=true
    driver-class-name: com.mysql.cj.jdbc.Driver
    username: root
    password: MySQL123

扩展功能 —— 代码生成器生成代码的代码

3.1 代码生成

在使用MybatisPlus以后,基础的MapperServicePO代码相对固定,重复编写也比较麻烦。因此MybatisPlus官方提供了代码生成器根据数据库表结构生成POMapperService等相关代码。只不过代码生成器同样要编码使用,也很麻烦。

这里推荐大家使用一款MybatisPlus的插件,它可以基于图形化界面完成MybatisPlus的代码生成,非常简单。

3.1.1.安装插件

Idea的plugins市场中搜索并安装MyBatisPlus插件:

然后重启你的Idea即可使用。

3.1.2.使用

刚好数据库中还有一张address表尚未生成对应的实体和mapper等基础代码。我们利用插件生成一下。 首先需要配置数据库地址,在Idea顶部菜单中,找到other,选择Config Database

点击OK保存。

然后再次点击Idea顶部菜单中的other,然后选择Code Generator:

在弹出的表单中填写信息:

img

最终,代码自动生成到指定的位置了:

扩展功能 —— DB静态工具两个Service相互注入

  • 改造根据id查询用户的接口,查询用户的同时user表,查询出用户对应的所有地址address表
  • 改造根据id批量查询用户的接口,查询用户的同时,查询出用户对应的所有地址
  • 实现根据用户id查询收货地址功能,需要验证用户状态,冻结用户抛出异常(练习)

3.2.静态工具

有的时候Service之间也会相互调用,为了避免出现循环依赖问题,MybatisPlus提供一个静态工具类:Db,其中的一些静态方法与IService中方法签名基本一致,也可以帮助我们实现CRUD功能:

UserController.java
    @GetMapping("/{id}")
    @ApiOperation("根据id查询用户")
    public UserVO queryUserById(@PathVariable("id") Long id) {
        // 1.查询用户
//        User user = userService.getById(userId);
        // 2.处理vo
        return userService.queryUserAndAddressById(id);
    }
IUserService.java
public interface IUserService extends IService<User> {
    UserVO queryUserAndAddressById(Long id);
}
UserServiceImpl.java
@Override
    public UserVO queryUserAndAddressById(Long id) {
        // 1.查询用户
        User user = getById(id);
        if (user == null || user.getStatus() == 2) {
            throw new RuntimeException("用户状态异常");
        }
        // 2.查询方法
        List<Address> addresses = Db.lambdaQuery(Address.class)
                .eq(Address::getUserId, id).list();
        // 3.封装VO
        // 3.1 转User的PO为VO
        UserVO userVO = BeanUtil.copyProperties(user, UserVO.class);
        if (CollUtil.isEmpty(addresses)) {
           userVO.setAddresses(BeanUtil.copyToList(addresses, AddressVO.class));
        }
        return userVO;
    }

扩展功能—DB静态工具(练习)

UserController.java
@GetMapping
    @ApiOperation("根据id集合查询用户")
    public List<UserVO> queryUserByIds(@RequestParam("ids") List<Long> ids) {
        // 1.查询用户
//        List<User> users = userService.listByIds(ids);
        // 2.处理vo
        return userService.queryUserAndAddressByIds(ids);
    }
IUserService.java
public interface IUserService extends IService<User> {
    List<UserVO> queryUserAndAddressByIds(List<Long> ids);
}
UserServiceImpl.java
@Override
    public List<UserVO> queryUserAndAddressByIds(List<Long> ids) {
        // 1.查询用户
        List<User> users = listByIds(ids);
        if (CollUtil.isEmpty(users)) {
            return Collections.emptyList();
        }
        // 2.查询地址
        // 2.1 获取用户id集合
        List<Long> userIds = users.stream().map(User::getId).collect(Collectors.toList());
        // 2.2 根据用户id查询地址 这是全部地址
        List<Address> addresses = Db.lambdaQuery(Address.class).in(Address::getUserId, userIds).list();
        // 2.3 转换地址VO
        List<AddressVO> addressVOList = BeanUtil.copyToList(addresses, AddressVO.class);
        // 2.4 梳理地址集合分组处理,分类整理,相同用户放入一个集合(组)中
        Map<Long, List<AddressVO>> addressMap = new HashMap<>(0);
        if (CollUtil.isNotEmpty(addressVOList)){
            addressMap = addressVOList.stream().collect(Collectors.groupingBy(AddressVO::getUserId));
        }
        // 3.转换VO返回
        List<UserVO> list = new ArrayList<>(users.size());
        for (User user : users) {
            // 3.1 转换User的Po为VO
            UserVO userVO = BeanUtil.copyProperties(user, UserVO.class);
            list.add(userVO);

            // 3.2 转换地址VO
            userVO.setAddresses(addressMap.get(user.getId()));
        }
        return null;
    }

扩展功能—逻辑删除要在数据库里面创建一个deleted表

订单不进行真实删除,一旦采用逻辑删除其他都不能用,需要添加配置信息

逻辑删除就是基于代码逻辑模拟删除效果,但并不会真正删除数据。思路如下:

  • 在表中添加一个字段标记数据是否被删除
  • 当删除数据时把标记置为1
  • 查询时只查询标记为0的数据

例如逻辑删除字段为deleted:

• 删除操作:

# 是0才删除 是1就不用删除 所以用AND
UPDATE user SET deleted = 1 WHERE id = 1 AND deleted = 0

• 查询操作:

# 查询未删除的数据
SELECT * FROM user WHERE deleted = 0
逻辑删除

MybatisPlus提供了逻辑删除功能,无需改变方法调用的方式,而是在底层帮我们自动修改CRUD的语句。我们要做的就是在application.yaml文件中配置逻辑删除的字段名称和值即可:

mybatis-plus: 
  global-config:    
   db-config:
    logic-delete-field: flag # 全局逻辑删除的实体字段名,字段类型可以是boolean、integer
    logic-delete-value: 1 # 逻辑已删除值(默认为 1)
    logic-not-delete-value: 0 # 逻辑未删除值(默认为 0)
逻辑删除本身也有自己的问题,比如:

• 会导致数据库表垃圾数据越来越多,影响查询效率
• SQL中全都需要对逻辑删除字段做判断,影响查询效率

因此,我不太推荐采用逻辑删除功能,如果数据不能删除,可以采用把数据迁移到其它表的办法。

扩展功能—枚举处理器

像这种字段我们一般会定义一个枚举,做业务判断的时候就可以直接基于枚举做比较。但是我们数据库采用的是int类型,对应的PO也是Integer。因此业务操作时必须手动把枚举Integer转换,非常麻烦。

因此,MybatisPlus提供了一个处理枚举的类型转换器,可以帮我们把枚举类型与数据库类型自动转换

3.3.1.定义枚举

我们定义一个用户状态的枚举:

User.java //使用枚举类型
// 使用状态(1正常 2冻结)
private UserStatus status;

要让MybatisPlus处理枚举与数据库类型自动转换,我们必须告诉MybatisPlus,枚举中的哪个字段的值作为数据库值。 MybatisPlus提供了@EnumValue注解来标记枚举属性:

package com.itheima.mp.enums;

import com.baomidou.mybatisplus.annotation.EnumValue;
import lombok.Getter;

@Getter
public enum UserStatus {
    NORMAL(1, "正常"),
    FREEZE(2, "冻结")
    ;
    @EnumValue
    private final int value;
    private final String desc;

    UserStatus(int value, String desc) {
        this.value = value;
        this.desc = desc;
    }
}

3.3.2.配置枚举处理器MP增加了Enum和JSON处理器

在application.yaml文件中添加配置:

mybatis-plus:
  configuration:
    default-enum-type-handler: com.baomidou.mybatisplus.core.handlers.MybatisEnumTypeHandler
UserVO.java
package com.itheima.mp.domain.vo;

import com.itheima.mp.enums.UserStatus;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

import java.util.List;

@Data
@ApiModel(description = "用户VO实体")
public class UserVO {

    @ApiModelProperty("用户id")
    private Long id;

    @ApiModelProperty("用户名")
    private String username;

    @ApiModelProperty("详细信息")
    private String info;

    @ApiModelProperty("使用状态(1正常 2冻结)")
    private UserStatus status;

    @ApiModelProperty("账户余额")
    private Integer balance;

    @ApiModelProperty("用户的收货地址")
    private List<AddressVO> addresses;
}
UserServiceImpl.java
 @Override
    public UserVO queryUserAndAddressById(Long id) {
        // 1.查询用户
        User user = getById(id);
        if (user == null || user.getStatus() == UserStatus.FREEZE) {
            throw new RuntimeException("用户状态异常");
        }
        // 2.查询方法
        List<Address> addresses = Db.lambdaQuery(Address.class)
                .eq(Address::getUserId, id).list();
        // 3.封装VO
        // 3.1 转User的PO为VO
        UserVO userVO = BeanUtil.copyProperties(user, UserVO.class);
        if (CollUtil.isEmpty(addresses)) {
           userVO.setAddresses(BeanUtil.copyToList(addresses, AddressVO.class));
        }
        return userVO;
    }
想要前端返回正常还是冻结 @JsonValue
package com.itheima.mp.enums;

import com.baomidou.mybatisplus.annotation.EnumValue;
import com.fasterxml.jackson.annotation.JsonFormat;
import com.fasterxml.jackson.annotation.JsonValue;
import lombok.Getter;

@Getter
public enum UserStatus {
    NORMAL(1, "正常"),
    FREEZE(2, "冻结")
    ;
    @EnumValue
    private final int value;
    @JsonValue
    private final String desc;

    UserStatus(int value, String desc) {
        this.value = value;
        this.desc = desc;
    }
}

扩展功能—JSON处理器AbstractJsonTypeHandler

数据库中user表中有一个json类型的字段

名称 数据类型 注释
info JSON 详细信息
。。。。。。。。。。。。

JSON:
{ “age”:20,
“intro”: “”青年”,
“gender”:”male”}

这样一来,我们要读取info中的属性时就非常不方便。如果要方便获取,info的类型最好是一个Map或者实体类。

而一旦我们把info改为对象类型,就需要在写入数据库时手动转为String,再读取数据库时,手动转换为对象,这会非常麻烦。

因此MybatisPlus提供了很多特殊类型字段的类型处理器,解决特殊字段类型与数据库类型转换的问题。例如处理JSON就可以使用JacksonTypeHandler处理器。

接下来,将User类的info字段修改为UserInfo类型,并声明类型处理器:

@TableField(typeHandler = JacksonTypeHandler.class) 定义类型处理器
@TableName(value = "user", autoResultMap = true)

User.java
@Data
@TableName(value = "user", autoResultMap = true)
public class User {

    /**
     * 用户id
     */
    @TableId(value = "id", type = IdType.AUTO)
    private Long id;

    /**
     * 用户名
     */
    private String username;

    /**
     * 详细信息
     */
    @TableField(typeHandler = JacksonTypeHandler.class)
    private UserInfo info;
}
UserVO.java
package com.itheima.mp.domain.vo;

import com.itheima.mp.domain.po.UserInfo;
import com.itheima.mp.enums.UserStatus;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

import java.util.List;

@Data
@ApiModel(description = "用户VO实体")
public class UserVO {

    @ApiModelProperty("用户id")
    private Long id;

    @ApiModelProperty("用户名")
    private String username;

    @ApiModelProperty("详细信息")
    private UserInfo info;

    @ApiModelProperty("使用状态(1正常 2冻结)")
    private UserStatus status;

    @ApiModelProperty("账户余额")
    private Integer balance;

    @ApiModelProperty("用户的收货地址")
    private List<AddressVO> addresses;
}
package com.itheima.mp.domain.po;

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

@Data
@AllArgsConstructor
@NoArgsConstructor(staticName = "of")
public class UserInfo {
    private Integer age;
    private String intro;
    private String gender;
    // 添加静态方法 of
    public static UserInfo of(Integer age, String intro, String gender) {
        return new UserInfo(age, intro, gender);
    }
}
UserMapperTest.java
@SpringBootTest
class UserMapperTest {

    @Autowired
    private UserMapper userMapper;

    @Test
    void testInsert() {
        User user = new User();
        user.setId(5L);
        user.setUsername("Lucy");
        user.setPassword("123");
        user.setPhone("18688990011");
        user.setBalance(200);
        user.setInfo(UserInfo.of(14, "英文老师", "female"));
        user.setCreateTime(LocalDateTime.now());
        user.setUpdateTime(LocalDateTime.now());
        userMapper.insert(user);
    }

插件功能—分页插件基本用法

MyBatisPlus提供的内置拦截器有下面这些:

序号 拦截器 描述
1 TenantLineInnerInterceptor 多租户插件
2 DynamicTableNameInnerInterceptor 动态表名插件
3 PaginationInnerInterceptor 分页插件
4 OptimisticLockerInnerInterceptor 乐观锁插件
5 IllegalSQLInnerInterceptor SQL性能规范插件,检测并拦截垃圾SQL
6 BlockAttackInnerInterceptor 防止全表更新和删除的插件
首先,要在配置类中注册MyBatisPlus的核心插件,同时添加分页插件:【总拦截器】
@Configuration
public class MybatisConfig {

    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor() {
        // 1. 初始化核心插件
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        // 2. 添加分页插件
        PaginationInnerInterceptor pageInterceptor = new PaginationInnerInterceptor(DbType.MYSQL);
        pageInterceptor.setMaxLimit(1000L); // 设置分页上限
        interceptor.addInnerInterceptor(pageInterceptor);
        return interceptor;
    }
}
接着,就可以使用分页的API了:[IService里面就有]
@Test
    void testPageQuery() {
        // 1. 查询
        int pageNo = 1, pageSize = 5;
        // 1.1. 分页参数
        Page<User> page = Page.of(pageNo, pageSize);
        // 1.2. 排序参数, 通过OrderItem来指定
        page.addOrder(new OrderItem("balance", true));
        // 先按balance排序 再按id排序
        page.addOrder(new OrderItem("id", true));
        // 1.3. 分页查询
        Page<User> p = userService.page(page);
        // 2. 总条数
        System.out.println("total = " + p.getTotal());
        // 3. 总页数
        System.out.println("pages = " + p.getPages());
        // 4. 分页数据
        List<User> records = p.getRecords();
        records.forEach(System.out::println);
    }

插件功能—通用分页实体

遵循下面的接口规范,编写一个UserController接口,实现User的分页查询
参数 说明
请求方式 GET
请求路径 /users/page
请求参数 “pageNo”: 1
“pageSize”: 5
“sortBy”: “balance”
“isAsc”: false
“name”: “jack”
“status”: 1
返回值 “total”: 1005
“pages”: 201
“list”: 包含两个元素的数组,每个元素都是一个对象,包含以下键值对:
“id”: 1 或 2
“username”: “Jack” 或 “Rose”
“info”: 包含以下键值对的对象:
“age”: 21 或 20
“gender”: “male” 或 “female”
“intro”: “佛系青年” 或 “文艺青年”
“status”: “正常” 或 “冻结”
“balance”: 2000 或 1000
特殊说明 如果排序字段为空,默认按照更新时间排序 •排序字段不为空,则按照排序字段排序
准备一下请求参数和实体【封装成xxxQuery,若只返回前端则VO,给其他使用则DTO】

写一个 【统一的分页条件】 和 【统一的分页结果】

com/itheima/mp/domain/query/UserQuery.java
// 要记得继承哦
package com.itheima.mp.domain.query;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

@Data
@ApiModel(description = "用户查询条件实体")
public class UserQuery extends PageQuery{
    @ApiModelProperty("用户名关键字")
    private String name;
    @ApiModelProperty("用户状态:1-正常,2-冻结")
    private Integer status;
    @ApiModelProperty("余额最小值")
    private Integer minBalance;
    @ApiModelProperty("余额最大值")
    private Integer maxBalance;
}
com/itheima/mp/domain/query/PageQuery.java
package com.itheima.mp.domain.query;

import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

@Data
@ApiModel(description = "分页查询实体")
public class PageQuery {
    @ApiModelProperty("页码")
    private Integer pageNo;
    @ApiModelProperty("页码个数")
    private Integer pageSize;
    @ApiModelProperty("排序字段")
    private String sortBy;
    @ApiModelProperty("是否升序")
    private Boolean isAsc;

    public <T>  Page<T> toMpPage(OrderItem ... orders){
        // 1.分页条件
        Page<T> p = Page.of(pageNo, pageSize);
        // 2.排序条件
        // 2.1.先看前端有没有传排序字段
        if (sortBy != null) {
            p.addOrder(new OrderItem(sortBy, isAsc));
            return p;
        }
        // 2.2.再看有没有手动指定排序字段
        if(orders != null){
            p.addOrder(orders);
        }
        return p;
    }

    public <T> Page<T> toMpPage(String defaultSortBy, boolean isAsc){
        return this.toMpPage(new OrderItem(defaultSortBy, isAsc));
    }

    public <T> Page<T> toMpPageDefaultSortByCreateTimeDesc() {
        return toMpPage("create_time", false);
    }

    public <T> Page<T> toMpPageDefaultSortByUpdateTimeDesc() {
        return toMpPage("update_time", false);
    }
}
com/itheima/mp/domain/dto/PageDTO.java
package com.itheima.mp.domain.dto;

import cn.hutool.core.bean.BeanUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Collections;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;

@Data
@ApiModel(description = "分页结果")
@NoArgsConstructor
@AllArgsConstructor
public class PageDTO<V> {
    @ApiModelProperty("总条数")
    private Long total;
    @ApiModelProperty("总页数")
    private Long pages;
    @ApiModelProperty("集合")
    private List<V> list;

    /**
     * 返回空分页结果
     * @param p MybatisPlus的分页结果
     * @param <V> 目标VO类型
     * @param <P> 原始PO类型
     * @return VO的分页对象
     */
    public static <V, P> PageDTO<V> empty(Page<P> p){
        return new PageDTO<>(p.getTotal(), p.getPages(), Collections.emptyList());
    }

    /**
     * 将MybatisPlus分页结果转为 VO分页结果
     * @param p MybatisPlus的分页结果
     * @param voClass 目标VO类型的字节码
     * @param <V> 目标VO类型
     * @param <P> 原始PO类型
     * @return VO的分页对象
     */
    public static <V, P> PageDTO<V> of(Page<P> p, Class<V> voClass) {
        // 1.非空校验
        List<P> records = p.getRecords();
        if (records == null || records.size() <= 0) {
            // 无数据,返回空结果
            return empty(p);
        }
        // 2.数据转换
        List<V> vos = BeanUtil.copyToList(records, voClass);
        // 3.封装返回
        return new PageDTO<>(p.getTotal(), p.getPages(), vos);
    }

    /**
     * 将MybatisPlus分页结果转为 VO分页结果,允许用户自定义PO到VO的转换方式
     * @param p MybatisPlus的分页结果
     * @param convertor PO到VO的转换函数
     * @param <V> 目标VO类型
     * @param <P> 原始PO类型
     * @return VO的分页对象
     */
    public static <V, P> PageDTO<V> of(Page<P> p, Function<P, V> convertor) {
        // 1.非空校验
        List<P> records = p.getRecords();
        if (records == null || records.size() <= 0) {
            // 无数据,返回空结果
            return empty(p);
        }
        // 2.数据转换
        List<V> vos = records.stream().map(convertor).collect(Collectors.toList());
        // 3.封装返回
        return new PageDTO<>(p.getTotal(), p.getPages(), vos);
    }
}

com/itheima/mp/controller/UserController.java
@ApiOperation("根据复杂条件查询用户接口")
    @GetMapping("/list")
    public List<UserVO> queryUsers(UserQuery query) {
        return (List<UserVO>) userService.queryUsersPage(query);
    }
// 如果你想在字符串中表示一个大于号,
你可以直接输入 >,或者使用HTML实体 &gt; 
小于号可以使用 < 或者 &lt;,
等于号可以使用 = 或者 &equals

插件功能—通用分页实体与MP转换

需求:

  • 在PageQuery中定义方法,将PageQuery对象转为MyBatisPlus中的Page对象
  • 在PageDTO中定义方法,将MyBatisPlus中的Page结果转为PageDTO结果
最好直接封装通用部分
封装查询
 @Override
    public PageDTO<UserVO> queryUsersPage(UserQuery query) {
        String name = query.getName();
        Integer status = query.getStatus();
        // 1.构建查询条件
        // 1.1 分页条件
        Page<User> page = Page.of(query.getPageNo(), query.getPageSize());
        // 1.2 排序条件
        if (StrUtil.isNotBlank(query.getSortBy())) {
            // 不为空
            page.addOrder(new OrderItem(query.getSortBy(), query.getIsAsc()));
        }else {
            // 为空,默认按照更新时间排序
            page.addOrder(new OrderItem("update_time", false));
        }
com/itheima/mp/domain/query/PageQuery.java
package com.itheima.mp.domain.query;

import com.baomidou.mybatisplus.core.metadata.OrderItem;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

@Data
@ApiModel(description = "分页查询实体")
public class PageQuery {
    @ApiModelProperty("页码")
    private Integer pageNo;
    @ApiModelProperty("页码个数")
    private Integer pageSize;
    @ApiModelProperty("排序字段")
    private String sortBy;
    @ApiModelProperty("是否升序")
    private Boolean isAsc;

    public <T>  Page<T> toMpPage(OrderItem ... orders){
        // 1.分页条件
        Page<T> p = Page.of(pageNo, pageSize);
        // 2.排序条件
        // 2.1.先看前端有没有传排序字段
        if (sortBy != null) {
            p.addOrder(new OrderItem(sortBy, isAsc));
            return p;
        }
        // 2.2.再看有没有手动指定排序字段
        if(orders != null){
            p.addOrder(orders);
        }
        return p;
    }

    public <T> Page<T> toMpPage(String defaultSortBy, boolean isAsc){
        return this.toMpPage(new OrderItem(defaultSortBy, isAsc));
    }

    public <T> Page<T> toMpPageDefaultSortByCreateTimeDesc() {
        return toMpPage("create_time", false);
    }

    public <T> Page<T> toMpPageDefaultSortByUpdateTimeDesc() {
        return toMpPage("update_time", false);
    }
}
封装结果
// 3.封装VO结果
        PageDTO<UserVO> dto = new PageDTO<>();
        // 3.1 总条数
        dto.setTotal(p.getTotal());
        // 3.2 总页数
        dto.setPages(p.getPages());
        // 3.3 当前页数据
        List<User> records = p.getRecords();
        if (CollUtil.isEmpty(records)) {
            dto.setList(Collections.emptyList());
            return dto;
        }
        // 3.4 拷贝user的VO
        dto.setList(BeanUtil.copyToList(records, UserVO.class));
        // 4.返回
        return dto;
    }
com/itheima/mp/domain/dto/PageDTO.java
package com.itheima.mp.domain.dto;

import cn.hutool.core.bean.BeanUtil;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.Collections;
import java.util.List;
import java.util.function.Function;
import java.util.stream.Collectors;

@Data
@ApiModel(description = "分页结果")
@NoArgsConstructor
@AllArgsConstructor
public class PageDTO<V> {
    @ApiModelProperty("总条数")
    private Long total;
    @ApiModelProperty("总页数")
    private Long pages;
    @ApiModelProperty("集合")
    private List<V> list;

    /**
     * 返回空分页结果
     * @param p MybatisPlus的分页结果
     * @param <V> 目标VO类型
     * @param <P> 原始PO类型
     * @return VO的分页对象
     */
    public static <V, P> PageDTO<V> empty(Page<P> p){
        return new PageDTO<>(p.getTotal(), p.getPages(), Collections.emptyList());
    }

    /**
     * 将MybatisPlus分页结果转为 VO分页结果
     * @param p MybatisPlus的分页结果
     * @param voClass 目标VO类型的字节码
     * @param <V> 目标VO类型
     * @param <P> 原始PO类型
     * @return VO的分页对象
     */
    public static <V, P> PageDTO<V> of(Page<P> p, Class<V> voClass) {
        // 1.非空校验
        List<P> records = p.getRecords();
        if (records == null || records.size() <= 0) {
            // 无数据,返回空结果
            return empty(p);
        }
        // 2.数据转换
        List<V> vos = BeanUtil.copyToList(records, voClass);
        // 3.封装返回
        return new PageDTO<>(p.getTotal(), p.getPages(), vos);
    }

    /**
     * 将MybatisPlus分页结果转为 VO分页结果,允许用户自定义PO到VO的转换方式
     * @param p MybatisPlus的分页结果
     * @param convertor PO到VO的转换函数
     * @param <V> 目标VO类型
     * @param <P> 原始PO类型
     * @return VO的分页对象
     */
    public static <V, P> PageDTO<V> of(Page<P> p, Function<P, V> convertor) {
        // 1.非空校验
        List<P> records = p.getRecords();
        if (records == null || records.size() <= 0) {
            // 无数据,返回空结果
            return empty(p);
        }
        // 2.数据转换
        List<V> vos = records.stream().map(convertor).collect(Collectors.toList());
        // 3.封装返回
        return new PageDTO<>(p.getTotal(), p.getPages(), vos);
    }
}
UserServiceImpl.java
@Override
    public PageDTO<UserVO> queryUsersPage(UserQuery query) {
        String name = query.getName();
        Integer status = query.getStatus();
        // 1.构建查询条件
        // 1.1 分页条件
        Page<User> page = query.toMpPageDefaultSortByUpdateTimeDesc();
        // 2. 分页查询
        Page<User> p = lambdaQuery()
                .like(name != null, User::getUsername, name)
                .eq(status != null, User::getStatus, status)
                .page(page);
        // 3. 封装VO结果
//        return PageDTO.of(p, UserVO.class); 属性转换 ↓
        return PageDTO.of(p, user -> {
            // 1.拷贝基础属性
            UserVO vo = BeanUtil.copyProperties(user, UserVO.class);
            // 2.处理特殊逻辑 密码加**
            vo.setUsername(vo.getUsername().substring(0, vo.getUsername().length()-2)+"**");
            return vo;
        });
    }



tilas-all 成功案例

package com.itheima.domain.dto;

import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

@Data
@AllArgsConstructor
@NoArgsConstructor
public class GenderStatisticsDTO {
    private String name;
    private Integer value;

}
==================================================
package com.itheima.domain.dto;

import com.baomidou.mybatisplus.annotation.TableName;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.util.List;

@Data
@AllArgsConstructor
@NoArgsConstructor
@TableName("emp")
public class JobStatisticsDTO {
    private List<String> jobList;
    private List<Long> dataList;
}
package com.itheima.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.itheima.domain.dto.CombinedStatisticsDTO;
import com.itheima.domain.dto.GenderStatisticsDTO;
import com.itheima.domain.dto.JobStatisticsDTO;
import com.itheima.pojo.Emp;

import java.util.List;

public interface ReportService extends IService<Emp> {
    List<GenderStatisticsDTO> getGenderStatistics(); // 获取性别统计信息
    JobStatisticsDTO getJobStatistics(); // 获取职位统计信息
}


//    CombinedStatisticsDTO getCombinedStatistics();
com/itheima/controller/ReportController.java
package com.itheima.controller;

import com.itheima.domain.dto.CombinedStatisticsDTO;
import com.itheima.domain.dto.GenderStatisticsDTO;
import com.itheima.domain.dto.JobStatisticsDTO;
import com.itheima.pojo.Result;
import com.itheima.service.ReportService;
import io.swagger.annotations.Api;
import lombok.RequiredArgsConstructor;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.List;

@Api(tags = "报表统计接口")
@RestController
@RequestMapping("/report")
@RequiredArgsConstructor
public class ReportController {

    private final ReportService reportService;

    // 获取合并后的统计信息
//    @GetMapping("/statistics")
//    public Result<CombinedStatisticsDTO> getStatistics() {
//        CombinedStatisticsDTO combinedStatistics = reportService.getCombinedStatistics();
//        return Result.success(combinedStatistics);
//    }

    // 获取员工性别统计信息
    @GetMapping("/empGenderData")
    public Result<List<GenderStatisticsDTO>> getEmployeeGenderStatistics() {
        List<GenderStatisticsDTO> genderStatistics = reportService.getGenderStatistics();
        return Result.success(genderStatistics);
    }

    // 获取员工职位统计信息
    @GetMapping("/empJobData")
    public Result<JobStatisticsDTO> getEmployeeJobStatistics() {
        JobStatisticsDTO jobStatistics = reportService.getJobStatistics();
        return Result.success(jobStatistics);
    }
}
com/itheima/service/impl/ReportServiceImpl.java
package com.itheima.service.impl;

import com.baomidou.mybatisplus.core.conditions.query.QueryWrapper;
import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.itheima.domain.dto.GenderStatisticsDTO;
import com.itheima.domain.dto.JobStatisticsDTO;
import com.itheima.mapper.ReportMapper;
import com.itheima.pojo.Emp;
import com.itheima.service.ReportService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.util.ArrayList;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

@Service
public class ReportServiceImpl extends ServiceImpl<ReportMapper, Emp> implements ReportService {

    private final ReportMapper reportMapper;

    @Autowired
    public ReportServiceImpl(ReportMapper reportMapper) {
        this.reportMapper = reportMapper;
    }

    // 获取员工职位统计信息
    @Override
    public JobStatisticsDTO getJobStatistics() {
        QueryWrapper<Emp> wrapper = new QueryWrapper<>();

        // 修改查询字段,注意这里你仍然需要写正确的字段名
        wrapper.select("CASE job WHEN 1 THEN '班主任' WHEN 2 THEN '讲师' WHEN 3 THEN '学工主管' WHEN 4 THEN '校研主管' WHEN 5 THEN '咨询师' ELSE '其他' END AS 职位",
                        "COUNT(*) AS 数量")
                .groupBy("job");

        // 通过 selectMaps 执行查询
        List<Map<String, Object>> statistics = reportMapper.selectMaps(wrapper);

        // 创建两个列表来存储职位和数量
        List<String> jobTitles = new ArrayList<>();
        List<Long> counts = new ArrayList<>();

        // 遍历查询结果并填充列表
        for (Map<String, Object> stat : statistics) {
            jobTitles.add((String) stat.get("职位"));
            counts.add((Long) stat.get("数量"));
        }

        // 返回JobStatisticsDTO对象,传入两个列表
        return new JobStatisticsDTO(jobTitles, counts);
    }

    // 获取员工性别统计信息
    @Override
    public List<GenderStatisticsDTO> getGenderStatistics() {
        QueryWrapper<Emp> wrapper = new QueryWrapper<>();
        wrapper.select("gender", "COUNT(gender) AS value")
                .groupBy("gender");

        List<Map<String, Object>> statistics = reportMapper.selectMaps(wrapper);

        // 转换为 GenderStatisticsDTO
        return statistics.stream()
                .map(stat -> {
                    String genderName = "1".equals(String.valueOf(stat.get("gender"))) ? "男性员工" : "女性员工";
                    int count = ((Number) stat.get("value")).intValue();
                    return new GenderStatisticsDTO(genderName, count);
                })
                .collect(Collectors.toList());
    }
}
上面的获取员工职位属性已修改为高级版本
// 获取员工职位统计信息
    @Override
    public JobStatisticsDTO getJobStatistics() {
        // 1. 构建查询条件
        QueryWrapper<Emp> wrapper = new QueryWrapper<>();
        wrapper.select("job", "COUNT(*) AS count")
                .groupBy("job");

        // 2. 查询数据
        List<Map<String, Object>> statistics = reportMapper.selectMaps(wrapper);

        // 如果返回结果为 null 或为空列表,返回默认对象
        if (statistics == null || statistics.isEmpty()) {
            return new JobStatisticsDTO(new ArrayList<>(), new ArrayList<>());
        }

        // 3. 转换结果:处理 `null` 值和字段映射
        List<String> jobTitles = new ArrayList<>();
        List<Long> counts = new ArrayList<>();

        for (Map<String, Object> stat : statistics) {
            if (stat == null) {
                continue; // 跳过 null 数据
            }

            // 使用 `getOrDefault` 方法,确保不会返回 null
            Integer jobCode = (Integer) stat.getOrDefault("job", -1);
            Long count = stat.get("count") == null ? 0L : ((Number) stat.get("count")).longValue();

            // 如果 jobCode 是 -1 或其他无效值,则视为“其他”
            String jobTitle = switch (jobCode) {
                case 1 -> "班主任";
                case 2 -> "讲师";
                case 3 -> "学工主管";
                case 4 -> "校研主管";
                case 5 -> "咨询师";
                default -> "其他";
            };

            jobTitles.add(jobTitle);
            counts.add(count);
        }

        // 返回封装好的 DTO 对象
        return new JobStatisticsDTO(jobTitles, counts);
    }
阅读全文

Stream模板+Lambda常用+@注释+常用方法模板集合

2024/11/22

Stream模板

中间方法

中间方法的特点惰性求值:中间操作不会立即执行,而是返回一个新的流。实际的计算会在遇到终结方法时进行。可以链式调用:多个中间操作可以链接在一起,形成一个操作链。返回类型:所有的中间操作返回的都是一个 Stream 对象。

Stream中间代码
功能:过滤流中的元素,仅保留满足给定条件的元素。
// 示例:
Stream.of(1, 2, 3, 4, 5)
      .filter(n -> n % 2 == 0) // 只保留偶数
      .forEach(System.out::println);//打印功能:将流中的元素映射为其他形式(通常是不同类型)。
// 示例:
Stream.of("a", "b", "c")
     .map(String::toUpperCase) // 将每个字符串转换为大写功能:将流中的每个元素映射为一个流,并将所有流连接成一个流。示例:Stream<List<String>> listStream = Stream.of(Arrays.asList("a", "b"), Arrays.asList("c", "d"));
listStream
    .flatMap(List::stream) // 将嵌套列表展平为一个流
    .forEach(System.out::println);功能:去除流中的重复元素。示例:Stream.of(1, 2, 2, 3, 4, 4)
     .distinct()
     .forEach(System.out::println); // 输出 1, 2, 3, 4功能:对流中的元素进行排序。示例:Stream.of(5, 3, 1, 4, 2)
     .sorted() // 默认升序排序
     .forEach(System.out::println);功能:截取流中的前 maxSize 个元素。示例:Stream.of(1, 2, 3, 4, 5)
     .limit(3) // 只保留前 3 个元素
     .forEach(System.out::println);功能:跳过流中的前 n 个元素。示例:Stream.of(1, 2, 3, 4, 5)
     .skip(2) // 跳过前 2 个元素
     .forEach(System.out::println); // 输出 3, 4, 5下面是一个示例,展示了多种中间方法的使用:import java.util.Arrays;
import java.util.List;
中间方法:
  1. 中间方法的特点
    惰性求值:中间操作不会立即执行,而是返回一个新的流。实际的计算会在遇到终结方法时进行。
    可以链式调用:多个中间操作可以链接在一起,形成一个操作链。
    返回类型:所有的中间操作返回的都是一个 Stream 对象。
stream中间操作

功能:过滤流中的元素,仅保留满足给定条件的元素。

示例:

Stream.of(1, 2, 3, 4, 5)
.filter(n -> n % 2 == 0) // 只保留偶数
.forEach(System.out::println);//打印

功能:将流中的元素映射为其他形式(通常是不同类型)。

示例:

Stream.of("a", "b", "c")
     .map(String::toUpperCase) // 将每个字符串转换为大写

功能:将流中的每个元素映射为一个流,并将所有流连接成一个流。

示例:

Stream<List<String>> listStream = Stream.of(Arrays.asList("a", "b"), Arrays.asList("c", "d"));
listStream
    .flatMap(List::stream) // 将嵌套列表展平为一个流
    .forEach(System.out::println);

功能:去除流中的重复元素。
示例:

Stream.of(1, 2, 2, 3, 4, 4)
     .distinct()
     .forEach(System.out::println); // 输出 1, 2, 3, 4

功能:对流中的元素进行排序。
示例:

Stream.of(5, 3, 1, 4, 2)
     .sorted() // 默认升序排序
     .forEach(System.out::println);

功能:截取流中的前 maxSize 个元素。
示例:

Stream.of(1, 2, 3, 4, 5)
     .limit(3) // 只保留前 3 个元素
     .forEach(System.out::println);

功能:跳过流中的前 n 个元素。
示例:

Stream.of(1, 2, 3, 4, 5)
     .skip(2) // 跳过前 2 个元素
     .forEach(System.out::println); // 输出 3, 4, 5

下面是一个示例,展示了多种中间方法的使用:

import java.util.Arrays;
import java.util.List;
public class StreamIntermediateOperations {
    public static void main(String[] args) {
        List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David", "Eve");

    // 使用中间方法
    names.stream()
        .filter(name -&gt; name.startsWith(&quot;A&quot;)) // 过滤以 'A' 开头的名字
        .map(String::toUpperCase) // 将名字转换为大写
        .sorted() // 排序
        .forEach(System.out::println); // 输出结果
}

}


终结方法

在 Java Stream API 中,终结方法(Terminal Operations)是指那些会触发流的计算并最终产生结果的方法。与中间操作不同,终结方法会结束流的操作链,并返回一个具体的结果或副作用。以下是对终结方法的详细介绍:

1. 终结方法的特点

触发计算:终结方法会对流中的数据进行处理并生成结果,通常会遍历流中的所有元素。

返回类型:终结方法可以返回不同类型的结果,包括:

基本类型(如 int、double)

对象(如 List、Set、Map)

特殊值(如 Optional、Void)

终结方法

功能:对流中的每个元素执行指定的操作。

示例:

Stream.of("a", "b", "c").forEach(System.out::println);

功能:将流中的元素收集到集合或其他形式。

示例:

List<String> list = Stream.of("a", "b", "c").collect(Collectors.toList());

能:对流中的元素进行归约,返回一个单一的结果。

示例:

int sum = Stream.of(1, 2, 3, 4).reduce(0, Integer::sum);

功能:返回流中元素的数量。

示例:

long count = Stream.of("a", "b", "c").count();

功能:检查流中是否有任何元素满足给定的条件。

示例:

boolean hasA = Stream.of("a", "b", "c").anyMatch(s -> s.equals("a"));

功能:检查流中所有元素是否满足给定的条件。

示例:

boolean allMatch = Stream.of(1, 2, 3).allMatch(n -> n < 5);

功能:返回流中的第一个元素(如果存在)。

示例:

Optional<String> first = Stream.of("a", "b", "c").findFirst();

下面是一个示例,展示了多种终结方法的使用:

import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;


public class StreamTerminalOperations {
public static void main(String[] args) {
List<String> names = Arrays.asList("Alice", "Bob", "Charlie", "David");

    // forEach
    names.stream().forEach(System.out::println);

    // collect
    List&lt;String&gt; filteredNames = names.stream()
        .filter(name -&gt; name.startsWith(&quot;A&quot;))
        .collect(Collectors.toList());
    System.out.println(filteredNames);

    // reduce
    String concatenated = names.stream()
        .reduce(&quot;&quot;, (a, b) -&gt; a + b);
    System.out.println(concatenated);
    
    // count
    long count = names.stream().count();
    System.out.println(&quot;Count: &quot; + count);
    
    // findFirst
    String firstName = names.stream().findFirst().orElse(&quot;No Name&quot;);
    System.out.println(&quot;First Name: &quot; + firstName);
}

}

stream流超强引用

package com.itheima.pojo.test;

import java.util.*;
import java.util.stream.Collectors;

/**
 * Arrays.asList 是 Java 中 java.util.Arrays 类的一个静态方法,
 * 用于将指定的数组或可变数量的参数转换为一个固定大小的 List。
 * 这个 List 是 ArrayList 的一个内部实现类,
 * 但它不是 java.util.ArrayList,
 * 而是一个不可变的列表
 */
public class Test2 {
    public static void main(String[] args) {
        // List<String> list:将上述列表赋值给 list 变量
        List<String> list = Arrays.asList("apple", "banana", "orange");
        // 定义一个映射,键为整数,值为字符串列表 = 创建一个新的空哈希映射
        // 键的类型是 Integer,值的类型是 List<String> 表示具有相同长度的字符串列表
        // 用HashMap  允许 null 值:键和值都可以为 null,但键只能有一个 null。
        Map<Integer, List<String>> groups = new HashMap<>();
        for (String s : list) {
            int length = s.length();
            // 检查 groups 映射中是否已经存在键为 length 的条目
            if (!groups.containsKey(length)) {
                // 将新创建的列表作为值,以 length 为键添加到 groups 映射中
                // 创建一个新的 ArrayList,并将当前字符串 s 添加到其中
                groups.put(length, new ArrayList<>(Arrays.asList(s)));
            } else {
                // 从 groups 映射中获取键为 length 的列表
                List<String> group = groups.get(length);
                group.add(s);
            }
            System.out.println(groups);
        }

        // 使用 Stream API 进行分组
        // 使用 Collectors.groupingBy 方法按字符串长度进行分组
        // 将分组结果收集到一个新的映射 group2 中。
        Map<Integer, List<String>> group2 = list.stream().collect(Collectors.groupingBy(String::length));
        System.out.println(group2);
    }
}

Lambda+Stream实用方法

// 创建一个包含字符串的列表
List<String> List = Arrays.asList("apple","banana", "orange");
1.for循环输出
for (String s : list){
System.out.println(s);}

2.表达式输出   
list.forEach(s ->
System.out.println(s);});

3.表达式最简洁输出
List.forEach(System.out::println);
// 创建一个包含字符串的列表
List<String> list = Arrays.asList("apple","banana", "orange");

1.使用重写Collections 排序
Collections.sort(list,new Comparator<String>() {
@override
public int compare(String o1, String o2) {
return o1.compareTo(o2);
}
});

2.使用lambda表达式排序
Collections.sort(list,(o1,o2) ->{
    return o1.compareTo(o2)
});

3.使用最简洁的表达式
Collections.sort(list,(o1,o2) ->{o1.compareTo(o2)});
// 创建一个包含字符串的列表
List<String> list = Arrays.asList("apple","banana", "orange");

1.普通方式过滤
List<String> list2 = new ArrayList<>();
List<String s : list2){
if(s.startsWith("a")){
     list2.add(s);
} }

2.使用 Stream API 进行过滤和收集,过滤以 'a' 开头的字符串,收集结果到一个新的 List 中
List<String>list3 = list.stream().filter(s - > s.startsWith("a")).collect(Collectrs.toList());
// 创建一个包含字符串的列表
List<String> list = Arrays.asList("apple","banana", "orange");

1.普通方式获取长度
List<Integer> List2 = new ArrayList<>();
for (String s:list){
    
list2.add(s.length());}

2.Lambda表达式+stream流获取长度
(map)这个函数对我们管道中的每个元素做了处理,在此处为把string转换为Integer类型 主要进行转换作用
List<Integer> list3 = list.stream().map(s -> s.length()).collect(Collectors.toList());
//新建一个List集合
List<Integer> list = Arrays.asList(1,2, 3, 4, 5);
1.普通方式相加操作
int sum =0;
for (Integer v : list) {
SUm +=V  }
System.out.println(sum);

2.Lambda+stream
(表达式含义):【0】操作的起始值,【a = a+b】 循环下去
int sum2 = list.stream().reduce( identity: 0, (a, b) -> a + b);
System.out.println(sum2)
//新建一个集合
List<String> list = Arrays.asList("apple", "banana", "orange");

Map<Integer,List<String>> groups = new HashMap<>();

1.普通方式分组
for (String s:list){
int length = s.length();
if (!groups.containsKey(length)){
groups.put(length,new ArrayList<>());
}
groups.get(Length) .add(s);
}
    System.out.println(groups);

2.Lambda+stream分组
Map<Integer,List<String>> groups2 = list.stream().collect(Collectors.groupingBy(String::length));
System.out.println(groups2)
1.普通方式创建线程

Thread thread = newThread(new Runnable(){
@Override
public void run(){
System.out.println("hello world");
}
});
thread.start();

2.Lambda表达式

Thread thread1 = new Threal(() -> System.out.println(hello world"));
thread1.start();
1.创建接口
interface  MyInterface{
public void doSomething(String s);}

2.普通实现接口
MyInterface myInterface = new MyInterface{
    @override
    public void doSomething(String s){
    System.out.println(s);
    }
};
myInterface.doSomething( s:"hello world");

3.Lambda表达式实现接口
MyInterface myInterface1 = (s) -> System.out.println(s);
myInterface1.doSomething( s:"hello worLd")
String str = "hello world";

1.普通方式
if(str !=null){
System.out.println(str.toUpperCase());}

2.Lambda表达式
Optional.ofNuLlable(str).map(String::toUpperCase).ifPresent(System.out::println);
List<String> List = Arrays.asList("apple","banana", "orange");
1.普通方式
List<String> list2 = new ArrayList<>();
for (String s:list2){ //遍历循环
    if (s.startsWith("a")){ //取出 包含a的元素
        list2.add(s.toUpperCase());//添加到list2中然后转换为大写
    }
}       Collections.sort(list2); //排序

2.Lambda+stream方式

List<String> list3 = list.stream().filter(s -> s.startsWith("a"))
.map(String::toupperCase).sorted().collect(collectors.toList());
public class Dept {
    private int id;

    public Dept(int id) {
        this.id = id;
    }

    public int getId() {
        return id;
    }

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

    @Override
    public String toString() {
        return "Dept{id=" + id + "}";
    }
}

public class TestCollectStopOptions {

    public void testCollectStopOptions() {
        // 创建一个包含 Dept 对象的列表
        List<Dept> ids = Arrays.asList(new Dept(17), new Dept(22), new Dept(23));

        // 使用 Stream API 过滤 id 大于 20 的 Dept 对象,并收集到 List 中
        List<Dept> collectList = ids.stream()
                                   .filter(dept -> dept.getId() > 20)
                                   .collect(Collectors.toList());
        System.out.println("collectList: " + collectList);

        // 使用 Stream API 过滤 id 大于 20 的 Dept 对象,并收集到 Set 中
        Set<Dept> collectSet = ids.stream()
                                 .filter(dept -> dept.getId() > 20)
                                 .collect(Collectors.toSet());
        System.out.println("collectSet: " + collectSet);

        // 使用 Stream API 过滤 id 大于 20 的 Dept 对象,并收集到 Map 中,key 为 id,value 为 Dept 对象
        Map<Integer, Dept> collectMap = ids.stream()
                                          .filter(dept -> dept.getId() > 20)
                                          .collect(Collectors.toMap(Dept::getId, dept -> dept));
        System.out.println("collectMap: " + collectMap);
    }

    public static void main(String[] args) {
        new TestCollectStopOptions().testCollectStopOptions();
    }
}

结果

collectList:[Dept{id=22}, Dept{id=23}]
collectSet:[Dept{id=23}, Dept{id=22}]
collectMap:{22=Dept{id=22}, 23=Dept{id=23}}
import java.util.Arrays;
import java.util.List;
import java.util.stream.Collectors;

public class User {
    private String id;

    public User() {
    }

    public String getId() {
        return id;
    }

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

    @Override
    public String toString() {
        return "User{id='" + id + '\'' + '}';
    }
}

public class TestStringToIntMap {

    /**
     * 演示map的用途:一对一转换
     */
    public void stringToIntMap() {
        // 创建一个包含字符串 ID 的列表
        List<String> ids = Arrays.asList("205", "105", "308", "469", "627", "193", "111");

        // 使用流操作
        List<User> results = ids.stream()
                               .map(id -> {
                                   // 创建一个新的 User 对象
                                   User user = new User();
                                   // 设置 User 对象的 id 属性
                                   user.setId(id);
                                   // 返回 User 对象
                                   return user;
                               })
                               .collect(Collectors.toList()); // 收集结果到一个新的 List 中

        // 打印结果
        System.out.println(results);
    }

    public static void main(String[] args) {
        new TestStringToIntMap().stringToIntMap();
    }
}

@注释笔记

@RequestBody :获取请全体json字符串数据 封装给java对象,封装的前提是 json字符串属性要与实体类属性名一致才可以封装。

—————————————————————————————–

#{name} 占位符 会从方法参数 对象里面调用getname封装方法获取数据映射到占位符位置。

#{参数名} 是Mybatis的参数占位符,可以自动将参数映射到SQL语句去执行

参数名要与接口方法的参数名要一致,但是方法只有一个参数时,参数名可以是任意的。

—————————————————————————————–

Spring MVC 的 @RequestMapping 注解能够处理 HTTP 请求的方法, 比如 GET, PUT, POST, DELETE 以及 PATCH。

所有的请求默认都会是 HTTP GET 类型的。比如@GetMapping

加入路径处理前端响应

—————————————————————————————–

注解@RequiredArgsConstructor 是 Lombok 提供的一个注解,其主要作用在于简化 @Autowired 的书写过程。在编写 Controller 层或 Service 层代码时,常常需要注入众多的 mapper 接口或 service 接口。若每个接口都使用 @Autowired 进行标注,代码会显得繁琐。而 @RequiredArgsConstructor 注解能够替代 @Autowired 注解,但需注意,在类上添加 @RequiredArgsConstructor 时,需要注入的类必须使用 final 进行声明。

—————————————————————————————–

<font style="color:#000000;background-color:rgb(249, 242, 244);">@Repository</font><font style="color:#000000;background-color:rgb(249, 242, 244);">@Repository</font>的作用与<font style="color:#000000;background-color:rgb(249, 242, 244);">@Controller</font><font style="color:#000000;background-color:rgb(249, 242, 244);">@Service</font>的作用都是把对象交给<font style="color:#000000;background-color:rgb(249, 242, 244);">Spring</font>管理。<font style="color:#000000;background-color:rgb(249, 242, 244);">@Repository</font>是标注在<font style="color:#000000;background-color:rgb(249, 242, 244);">Dao</font>层接口上,作用是将接口的一个实现类交给<font style="color:#000000;background-color:rgb(249, 242, 244);">Spring</font>管理。

—————————————————————————————–

@Mapper

@Mapper: 这个注解一般使用在Dao层接口上,相当于一个mapper.xml文件,它的作用就是将接口生成一个动态代理类。加入了@Mapper注解,目的就是为了不再写mapper映射文件。这个注解就是用来映射mapper.xml文件的。

使用@mapper后,不需要在spring配置中设置扫描地址,通过mapper.xml里面的namespace属性对应相关的mapper类,spring将动态的生成Bean后注入到ServiceImpl中

注意:

在Dao层不要存在相同名字的接口,也就是在Dao不要写重载。因为mapper文件是通过id与接口进行对应的,如果写了两个同名的接口,就会导致mapper文件映射出错。

—————————————————————————————–

@Transactional

Spring事务管理-控制事务 注解:@Transactional

作用:将当前方法交给spring进行事务管理,方法执行前,开启事务;成功执行完毕,提交事务;出现异常,回滚事务 放在类上 则是整个类都会启动事务 放在接口上 接口的实现类都会去启动事务。放在方法上此方法中的代码会启动事务。

—————————————————————————————–

规则:JSON数据的键名与方法形参对象的属性名相同,并需要使用@RequestBody注解标识。

—————————————————————————————–

MultipartFile 接收文件接口

—————————————————————————————–

@Service 表明这个是逻辑层 可以被调用

—————————————————————————————–

@ConfigurationPropertise()

—————————————————————————————–

@Autowired 注入bean

—————————————————————————————–

集合:@RequestParam[List ids

—————————————————————————————–

是一个在Java项目中常用的注解,特别是在使用日志框架如SLF4J时,通过在你的类上使用@Slf4j注解,Lombok会自动为你的类生成一个静态的日志字段,这个字段通常是org.slf4j.Logger类型的,并且通常命名为log。这样,你就可以在类中直接使用log.info(), log.error(), log.debug()等方法来记录日志,而无需手动声明和初始化Logger对象。

—————————————————————————————–

@PathVariable 是 Spring MVC 中用于将 URL 模板变量绑定到你控制器处理器方法参数上的注解。这个注解使得你可以从 URL 中提取出变量值,并将其作为参数传递给控制器的方法。

—————————————————————————————–

@RequestParam(defaultValue = “1”) 给参数设置默认值 如果前端没有参数传进来 默认值为1 可自己设置

—————————————————————————————–

@RequestBody 注解 使用对象去接收 的时候使用的注解

—————————————————————————————–

@RestControllerAdvice

—————————————————————————————–

@ExceptionHandler

—————————————————————————————–

限制请求的方式

@RequestMapping 可以放在类上,获取的路径可以当做所有方法的父路径

@PostMapping(value=”/depts”,method=RequestMethod.GET)注解 可以放方法上,获取前端的路径。

@GetMapping(”/depts”)

@PutMapping

@DeleteMapping

—————————————————————————————–

junnit5

@Test

@ParameterizedTest

@BeforeEach

@AfterEach

@BeforeAll 标识静态方法

@AfterAll 标识静态方法

—————————————————————————————–

@RestController = @Controller+@ResponseBody

标识当前控制类所有方法都有了@ResponseBody

@ResponseBody 将控制器方法直接输出给前端,将java对象转换为json字符串输出给前端

—————————————————————————————–

lombok

—————————————————————————————–

在定义完Filter之后,Filter其实并不会生效,还需要完成Filter的配置,Filter的配置非常简单,只需要在Filter类上添加一个注解:<font style="color:rgb(51, 51, 51);background-color:rgb(243, 244, 244);">@WebFilter</font>,并指定属性<font style="color:rgb(51, 51, 51);background-color:rgb(243, 244, 244);">urlPatterns</font>,通过这个属性指定过滤器要拦截哪些请求。

当我们在Filter类上面加了@WebFilter注解之后,接下来我们还需要在启动类上面加上一个注解@ServletComponentScan,通过这个@ServletComponentScan注解来开启SpringBoot项目对于Servlet组件的支持。

@Order注解 控制过滤器优先级数字越小 优先级越高

—————————————————————————————–

Spring Boot 中注解的作用

Spring Boot 利用注解来简化配置和提高开发效率。主要注解包括但不限于:

@SpringBootApplication: 启动 Spring Boot 应用程序。

@Component, @Service, @Repository, @Controller: 标记组件,以便 Spring 容器可以自动检测和管理它们。

@Bean 该方法会在spring项目启动时自动调用,并将方法的返回值交给IOC容器管理 – bean对象

@Autowired: 用于自动装配 Bean。

@Bean: 在配置类中定义 Bean。

@Configuration: 定义配置类。

@EnableAutoConfiguration: 开启自动配置。

这些注解通常被组合使用,以提供一个高度可配置且易于扩展的应用程序结构。

12. @PathVariable, @RequestParam, @ModelAttribute, @RequestBody, @ResponseBody

参数绑定

这些注解用于从 HTTP 请求中提取参数,并将它们绑定到方法参数上。

@PathVariable: 用于从 URL 中提取路径变量。

@RequestParam: 用于从查询字符串中提取参数。

@ModelAttribute: 用于将多个请求参数绑定到一个对象上。

@RequestBody: 用于将请求体中的数据绑定到方法参数上。 获取请求体json字符串数据封装给java对象

@ResponseBody: 用于将方法的结果直接写入响应体。

13. @RestController

控制器注解

@RestController 注解是一个组合注解,它等价于 @Controller 和 @ResponseBody 的组合。它表示这是一个 REST 控制器,所有返回值都将被序列化为 JSON 格式并直接写入 HTTP 响应体。

全局异常处理

<font style="color:rgb(199, 37, 78);background-color:rgb(249, 242, 244);">@ControllerAdvice</font> 注解用于定义全局异常处理类,它可以捕获控制器方法抛出的所有异常,并提供统一的错误响应。

@RunWith(SpringRunner.class)

测试运行器

@RunWith(SpringRunner.class) 注解告诉 JUnit 使用 Spring 测试运行器 (SpringRunner) 来运行测试。Spring 测试运行器提供了一种方便的方式来加载 Spring 上下文并管理测试生命周期。

整体测试

<font style="color:rgb(199, 37, 78);background-color:rgb(249, 242, 244);">@SpringBootTest</font> 注解用于执行整体测试,它会加载整个 Spring 应用上下文,包括所有自动配置的 Bean。这对于集成测试非常有用,因为它可以模拟完整的 Spring Boot 应用程序。

@Configuration 用于定义配置类,配置类中的bean可以自动装配到其他bean中

@Configuration类可以使用其他Spring注解,如@ComponentScan和@Import,来扫描组件或导入其他配置类

@Configuration类在Spring容器启动时会通过CGLIB动态代理机制生成代理类,以确保@Bean方法只被调用一次,从而保证单例bean的行为

环境和属性配置:
使用@PropertySource和@Value注解可以将外部属性文件中的值注入到配置类中

—————————————————————————————–

作用:按照一定的条件进行判断,在满足给定条件才会注册对应的bean对象到Spring IOC容器中。

位置:方法、类

@Conditional 本身是一个父注解,派生出大量子注解

@ConditionalonClass:判断环境中是否有对应节码文件才注册bean到IOC容器

对应的bean(类型或名称),才注册bean到IOC容器@ConditionalOnMissingBean:判断环境中没@ConditionalonProperty:判断配置文件中有应属性和值,才注册bean到IOC容器。

—————————————————————————————–

SpringBoot 底层原理

bean获取。

@singleton 默认容器内同名称的bean只有一个实例(单例)

@prototype 每次使用该bean时会创建新的实例(非单例)

@request 每个请求范围内会创建新的实例(web环境中,了解即可)

@session 每个会话范围内都会创建新的实例(web环境中,了解)

@application 每个应用范围内会创建新的实例(web环境中,了解)

@Scope 设置bean的作用域

@Lazy 延迟加载 会延迟到第一次使用的时候才会去加载

默认singleton的bean,在容器启动时被创建,可以使用aLazy注解来延迟初始化(延迟到第一次使用时)

prototype的bean,每一次使用该bean的时候都会创建一个新的实例。

实际开发当中,绝大部分的Bean是单例的,也就是说绝大部分Bean不需要配置scope属性

– 非单例是每次使用时会创建一个全新的bean

@Import 是 Java 中 Spring 框架(特别是 Spring Framework 和 Spring Boot)中用于配置类的一个注解。它主要用于导入其他配置类,使得当前的配置类能够复用其他配置类中的配置信息,从而避免重复的配置代码。

<font style="color:rgb(5, 7, 59);">@Conditional</font> 是 Spring Framework 中的一个注解,它用于在自动配置类(@Configuration 类)中或者通过 <font style="color:rgb(5, 7, 59);">@Bean</font> 方法定义 bean 时,根据特定的条件来决定是否创建某个 bean 或配置。这个注解使得 Spring 的自动配置更加灵活和强大,因为它允许开发者基于特定的条件(如类路径上的特定类、操作系统属性、环境变量等)来启用或禁用配置。

—————————————————————————————–

@Transactional注解书写位置:

  • 方法
    • 当前方法交给spring进行事务管理
    • 当前类中所有的方法都交由spring进行事务管理 (推荐)
  • 接口
    • 接口下所有的实现类当中所有的方法都交给spring 进行事务管理

—————————————————————————————–

  • @Data是lombok注解,可以生成getter/setter方法,tostring/hashcode/equals等方法重写

    @NoArgsConstructor /添加无参构造
    
    @AllArgsConstructor //添加全参构造
    

—————————————————————————————–

@ResponseBody: 将控制器方法返回值直接输出给前端,将java对象转换为json字符串输出给前端@RestController = @controller + @ResponseBody

标识了当前控制器类所有方法就都有了@ResponseBody

@Controller : spring框架的ioc注解,用于给当前类创建实例对象,也就是加入ioc容器中。

@Autowired :依赖注入注解:在运行时会从spring容器中找当前接口实现类对象并注入

@Qualifier(“Bean对象”):指定Bean别名这与对象

@Qualifier常与@Autowired一起使用

@0ptions(useGeneratedKeys = true,keyProperty =”id”)//需要获取数据库赋值的id属性 并赋值给对象的id

—————————————————————————————–

常用方法模板集合

@Test
1:JsONUtil.toJsonStr(paramMap)//将任意对象转换为json字符串形式
                     
//使用hutool工具类把BedDto类型转换成Bed实体类 类型
2:Bed bean = BeanUtil.toBean(bedDto, Bed.class);

//判断对象是否为null 如果为null 返回true
3:Objects.isNull(xx)

//判断对象是否不为null 如果不为null 返回true
4:Objects.nonNull(xx)
                  
//工具类Objects 专门用来解决空指针异常 意思 先判断s1!=null 在调用s1.equals(s2)
5:Objects.equals(s1,s2)

//强转方法
6:String.valueOf() 

//整个对象的转换方法
7:BeanUtils.copyProperties(user,userPojo) 

8:StringUtils.toStringArray(把括号中的内容转换为一个字符串类型数组)

//在java中,JSONOBject类中的get(String key)方法接收一个字符串参数作为键名,用于从JSON对象中获取对应的值,这种方法运行通过建模来检索特定的数据项
//当我们调用JSONOBject.get("acces_token")的时候,实际上是在告诉程序:请查找名为“access_token”的键,并返回其关键的值,
//如果找到匹配的键,则返回相应的值,如果没有找到,则返回null
9:JSONOBject.get("acces_token")
//建造者设计模式:利用各种组件(各种属性)随意组合生成对象,目的是创建对象更加灵活
//与直接调用构造函数对比
//类一般要提供很多构造函数才可以灵活构建对象,这种方式很麻烦
//建造者模式创建对象底层只需需要提供一个构造函数即可,在使用的时候想设置哪个属性就设置哪个属性,最终都调用同一个构造函数
@Data
@AllArgsConstructor
@NoArgsConstructor
@Builder给当前类添加建造者设计模式创建对象    这几个注解都要有
10:member = Member.builder()
.openId(openid)
.build();
阅读全文

git详细操作

2024/11/19

@Author yuan

Git作用

Git 作用

代码回溯 版本控制 多人协作 远程备份

Git 简介

Git 是一个分布式版本控制工具,通常用来对软件开发过程中的源代码文件进行管理。通过Git 仓库来存储和管理这些文件,Git 仓库分为两种:

本地仓库:开发人员自己电脑上的 Git 仓库

远程仓库:远程服务器上的 Git 仓库

commit:提交,将本地文件和版本信息保存到本地仓库

push:推送,将本地仓库文件和版本信息上传到远程仓库

pull:拉取,将远程仓库文件和版本信息下载到本地仓库

常用的 Git 代码托管服务

Git中存在两种类型的仓库,即本地仓库和远程仓库。那么我们如何搭建Git远程仓库呢?

我们可以借助互联网上提供的一些代码托管服务来实现,其中比较常用的有GitHub、码云、GitLab等。

获取Git 仓库-从远程仓库克隆

可以通过Git提供的命令从远程仓库进行克隆,将远程仓库克隆到本地命令形式:git clone【远程Git仓库地址】

工作区、暂存区、版本库 概念

版本库:前面看到的.git隐藏文件夹就是版本库,版本库中存储了很多配置信息、日志信息和文件版本信息等工作区:包含.git文件夹的目录就是工作区,也称为工作目录,主要用于存放开发的代码暂存区:.git文件夹中有很多文件,其中有一个index文件就是暂存区,也可以叫做stage。暂存区是一个临时保存修改文件的地方

Git工作区中文件的状态

Git工作区中的文件存在两种状态:untracked 未跟踪(未被纳入版本控制)

tracked 已跟踪(被纳入版本控制)

1.Unmodified 未修改状态

2.Modified 已修改状态

3.Staged 已暂存状态

本地仓库操作

本地仓库常用命令如下:

git status 查看文件状态

git add 将文件的修改加入暂存区

git reset 将暂存区的文件取消暂存或者是切换到指定版本

git commit 将暂存区的文件修改提交到版本库

git log 查看日志

Git项目克隆

在IDEA中克隆Git项目

首先配置Git

说明:如果Git安装在默认目录中(C:\Program Files\Git),则IDEA中无需再手动配置,直接就可以使用。

第一步:

第二步:

第三步:

第四步:

第五步:

第六步:

红色:未跟踪文件

绿色:已暂存文件

蓝色:已修改文件

黑色:未修改文件

标签

1.创建一个标签

确认标签名:

添加成功:

推送到远程仓库:

分支操作

分支操作:

查看分支,本质就是执行 gitbranch 命令

创建分支,本质就是执行 git branch 分支名

命令切换分支,本质就是执行 git checkout命令

将分支推送到远程仓库,本质就是执行 git push 命令

合并分支,本质就是执行 git merge 命令

IDEA中查看分支在右下角,如图

新建分支

新建分支推送

切换分支:

合并分支:

合并分支后需要推送到远程仓库同步

切换版本开发

切换版本:

在开发过程中,有a1到a5这几个版本的项目,现在需要重新基于a3去开发后续项目。基于这种情况,我们可以右键这个版本的项目,新建分支进行开发。

新建分支:

提交 && 拉取 && 冲突

1.提交

2.推送

3.拉取

4.提交推送2

5.拉取

操作冲突:多个人操作同一个文件,其中有人基于旧的版本修改,提交新版本会成功,但是推送到远程会失败,就是发送冲突。

冲突为什么发生:a和b同时修改c1文件,a先修改完成c1文件,提交推送到远程仓库,c1文件进行更新版本成为c2,提交推送后b也修改完成c1文件,提交的时候成功 推送则失败,因为远程仓库的文件已经进行了更新。b推送的时候没有拉取最新的文件进行修改,而是使用的过期版本,所以造成冲突。

解决冲突,拉取,合并,推送

操作冲突:

解决冲突操作:1

解决冲突操作:2

解决冲突操作:3

解决冲突操作:4

推送合并后的项目到远程仓库

阅读全文

若依-AI & 帝可得

2024/11/3

RuoYi-Vue

课程版本
JDK 11
MySQL 8
Redis 5
Maven 3.6
Node 16 (Vue3)
  • 技术选型:SpringBoot、SpringSecurity、MyBatis、Jwt、VUE3、Element-Plus
  1. 从VCS导入项目

    • 点击VCS菜单。

    • 选择Get from Version Control...选项。

    • URL:https://gitee.com/y_project/RuoYi-Vue.git
      Directory:C:\Users\Pluminary\Desktop\RuoYi-Vue
      
  2. 输入项目地址

    • 在弹出的窗口中,您可以看到不同的版本控制系统(例如Git, SVN等)。
    • 选择您要导入的项目所使用的版本控制系统。
    • 在接下来的窗口中,您需要输入项目的URL地址。这通常是项目的仓库地址,例如Git仓库的HTTPS或SSH链接。

① 若模块没有导入进去没有亮 就Maven → clean → package

C:\Users\Pluminary\Desktop\RuoYi-Vue\sql 导入Sql文件两个
sql/quartz.sql
sql/ry_20240629.sql

先gitwzs28150/RuoYi-Vue3: :tada: (RuoYi)官方仓库 基于SpringBoot,Spring Security,JWT,Vue3 & Vite、Element Plus 的前后端分离权限管理系统下载这里面的资料,在里面C:\Users\Pluminary\Desktop\itcast>code ./RuoYi-Vue3导入vscode里面,再安装依赖(要进入Vue3里面才能利用package.json去生成)npm install → 运行前端项目npm run dev

// npm install慢的话 就用中国镜像去下载
npm install --registr=https://registry.npmmirror.com

C:\Users\Pluminary\Desktop\itcast\RuoYi-Vue3>npm run dev

> ruoyi@3.8.8 dev
> vite


  VITE v5.3.2  ready in 2026 ms

  ➜  Local:   http://localhost:8080/
  ➜  Network: http://10.254.3.124:8080/
  ➜  Network: http://192.168.104.38:8080/
  ➜  Network: http://192.168.22.1:8080/
  ➜  Network: http://192.168.36.1:8080/
  ➜  press h + enter to show help

再导入课程管理.sql数据库 → 在若依的后台系统 → 系统工具 → 代码生成 → 配置好后下载代码 → 导入数据库

→ 导入RuoYi生成的前端代码C:\Users\Pluminary\Downloads\ruoyi\vue\apicourse导入vscode中 C:\Users\Pluminary\Desktop\itcast\RuoYi-Vue3\src\api\course && C:\Users\Pluminary\Downloads\ruoyi\vue\viewscourse导入vscode中 C:\Users\Pluminary\Desktop\itcast\RuoYi-Vue3\src\api\course

→ 导入RuoYi生成的后端代码C:\Users\Pluminary\Desktop\itcast\RuoYi-Vue3\src\views\coursecom 并且导入配置文件 && C:\Users\Pluminary\Downloads\ruoyi\main\resourcesmapper导入到idea的resource中

功能详解

权限控制
  • 若依内置了强大的权限控制系统,为企业级项目提供了通用的解决方案
    • **demo账号 (超级管理员)**,查看所有功能菜单
    • **zhangsan账号 (市场专员)**,查看线索菜单
    • **yueyue账号 (销售专员)**,查看商机、合同等菜单
  • RBAC (基于角色的控制访问) 是一种广泛使用的访问控制模型,通过角色来分配和管理用户的菜单权限
表名 说明
sys_dept 部门表
sys_post 岗(职)位信息表
sys_menu 菜单权限表
sys_role 角色信息表
sys_role_dept 角色和部门关联表
sys_role_menu 角色和菜单关联表
sys_user 用户信息表
sys_user_post 用户与岗位关联表
sys_user_role 用户和角色关联表

创建新用户小智并关联课研人员角色,仅限课程管理和统计分析菜单访问。

创建菜单
创建角色,并分配权限课研人员
创建用户,并关联角色xiaozhi

若依通过简单的功能配置实现RBC的权限管理
数据字典
  • 若依内置的数据字典,用于维护系统中常见的静态数据。例如:性别、状态
  • 功能包括:字典类型管理、字典数据管理
  • 表关系说明【一对多】
表名 说明
sys_dict_type 字典类型表
sys_dict_data 字典数据表
dict_id dict_name dict_type
1 用户性别 sys_user_sex
2 菜单状态 sys_show_hide
dict_code dict_sort dict_label dict_value dict_type
1 1 0 sys_user_sex
2 2 1 sys_user_sex
3 3 未知 2 sys_user_sex
  • 将一些不经常修改的数据(课程管理的学科字段)改为数据字典维护,以免占用大量空间

    • 添加字典类型和数据

      系统管理 → 字典管理 → 新增 → 添加字典类型
      字典名称:学科
      字典类型:course_subject
      第二页点进去添加字典数据
      javaEE → 0 → 1

    • 修改代码生成信息

      系统工具 → 代码生成 → 编辑课程管理 → subject课程学科 显示类型从文本框改成下拉框 → 字典类型 是学科 → 下载最新的代码
      只需要修改前端vue组件 因为只改了从前端文本框到下拉框 和一些数据字典

    • 下载代码,导入前端

      C:\Users\Pluminary\Downloads\ruoyi (1)\vue\views\course\course\index.vue去替换前端的index.vue 此时去若依前端查看课程管理→课程学科就会发现已经添加新的进去了

此时去课程管理里面找课程学科:JavaEE进行筛选不会出 因为若依底层
http://10.254.2.179/dev-api/course/course/list?pageNum=1&pageSize=10&subject=0
要从数据库里把JavaEE的subject改成0
优点:降低数据库的存储压力 提高磁盘利用率

其他功能
  • 参数设置:对系统中的参数进行动态维护

    系统管理 → 参数设置 → 验证码开关 → 修改 → 参数键值 → false
    还可以开启是否 用户注册功能 → 前端代码隐藏需要修改 → src/views/login.vue → 97行注册开关
    const register = ref(true) 此时登录界面就有立即注册 跳转注册

  • 通知公告(半成品):促进组织内部信息传递

    系统管理 → 通知公告 → 新增

  • 日志管理:轻松追踪用户行为和系统运行状况

    系统管理 → 通知公告 → 日志管理 → 操作日志

系统监控
  • 若依提供了一些列强大的监控工具,能够帮助开发者和运维快速了解应用程序的性能状态

    系统监控 → 在线用户 && 缓存列表
    数据监控【Druid Monitor】 → ruoyi && 123456

定时任务
  • 若依为定时任务功能提供方便友好的web界面,实现动态管理任务

    @Component
    public class MyTask{
      @Scheduled(cron = "0/5 *****?")
      public void showTime(){
         sout("定时任务开始执行:" + new Date());
      }
    }
    // 硬编码 改代码需要重新修改 重新编译 重新上传...
    
  • 每间隔5秒,控制台输出系统时间

    • 创建任务类

      创建一个类 C:\Users\Pluminary\Desktop\RuoYi-Vue\ruoyi-quartz\src\main\java\com\ruoyi\quartz\task\MyTask.java

      package com.ruoyi.quartz.task;
      
      import org.springframework.stereotype.Component;
      
      import java.util.Date;
      
      @Component
      public class MyTask {
          public void showTime(){
              System.out.println("定时任务开始执行:" + new Date());
          }
      }
      
    • 添加任务规则

      系统监控 → 定时任务 → 新增 →
      任务名称:输出时间 任务分组:默认
      调用方式:myTask.showTime()
      Cron表达式生成器:从0秒开始,每5秒执行一次 → 0/5 * * * * ?
      开启输出时间 状态打开!

      然后每间隔5秒就会向控制台输出时间

      定时任务开始执行:Wed Nov 06 18:12:30 CST 2024
      18:12:30.001 [quartzScheduler_Worker-4] DEBUG c.r.q.m.S.insertJobLog - [debug,135] - > Preparing: insert into sys_job_log( job_name, job_group, invoke_target, job_message, status, create_time )values( ?, ?, ?, ?, ?, sysdate() ) 18:12:30.002 [quartzScheduler_Worker-4] DEBUG c.r.q.m.S.insertJobLog - [debug,135] - –> Parameters: 输出时间(String), DEFAULT(String), myTask.showTime()(String), 输出时间 总共耗时:0毫秒(String), 0(String) 18:12:30.009 [quartzScheduler_Worker-4] DEBUG c.r.q.m.S.insertJobLog - [debug,135] - < Updates: 1 定时任务开始执行:Wed Nov 06 18:12:35 CST 2024 18:12:35.006 [quartzScheduler_Worker-5] DEBUG c.r.q.m.S.insertJobLog - [debug,135] - > Preparing: insert into sys_job_log( job_name, job_group, invoke_target, job_message, status, create_time )values( ?, ?, ?, ?, ?, sysdate() ) 18:12:35.006 [quartzScheduler_Worker-5] DEBUG c.r.q.m.S.insertJobLog - [debug,135] - –> Parameters: 输出时间(String), DEFAULT(String), myTask.showTime()(String), 输出时间 总共耗时:0毫秒(String), 0(String) 18:12:35.012 [quartzScheduler_Worker-5] DEBUG c.r.q.m.S.insertJobLog - [debug,135] - < Updates: 1

    • 启动任务

      官方有提供可以训练的模型

      在这里面:com/ruoyi/quartz/task/RyTask.java

表单构建
  • 通过表单构建工具,单独制作一个添加课程的表单页面
    • 制作表单并导出

      系统工具 → 表单构建 → 左侧行容器 → 拖入第一个单行文本 → 右侧可以改名字 如果想实现一行两个文本的化 就把表单栅格调小一点 → 选择性组件的下拉选择托到右面 → 修改字段名、标题、表单栅格…
      日期范围 → 课程有效期 → 选择型组件里的日期范围 命名为:课程有效期
      文件上传组件 → 课程封面
      评分 → 推荐指数
      多行文本 → 课程介绍

    • 复制到前端工程

      搞完后打开前端工程把add.vue它放在 src/views/course/course/中

    • 创建动态菜单

      系统管理 → 菜单管理 → 添加菜单 →
      菜单类型:菜单
      菜单名称:添加课程
      显示排序:1
      路由地址:course/add
      组件路径:course/course/add
      刷新界面 就会有菜单管理→添加课程了

系统工具

代码生成
  • 代码生成器,根据数据库表结构自动生成前后端CRUD代码
  • 提供三种生成模板:单表、树表、主子表(一对多)
  • 树表是一种展示层级数据的表格,能展开折叠,清晰呈现父子关系,便于管理

系统工具 → 代码生成 → 导入部门表 → 编辑 → 生成信息 → 生成模板:树表 →
树编码字段:dept_id:部门id
树父编码字段:parent_id:父部门id
树名称字段:dept_name:部门名称
提交后下载代码
导入后就是 系统管理/部门管理 的树型结构界面了

dept_id parent_id ancestors dept_name
100 0 0 若依科技
101 100 0,100 深圳总公司
102 100 0,100 长沙分公司
108 102 0,100,101 市场部门
109 102 0,100,102 财务部门
系统接口
  • Swagger,能够自动生成API的同步在线文档,并提供Web界面进行接口调用和测试

    系统工具 → 系统接口
    若依的Token在应用程序里 需要搞token进去
    测试:获取用户列表GET
    得到Token(F12后找应用程序 → Cookie若依 → 找到Admin-Token一定是当前ip地址的Token → 在Authorize中设置Token令牌 → 去后端改swagger的请求前缀pathMapping:/因为他的地址前面默认佩戴/dev-api

    # Swagger配置
    swagger:
      # 是否开启swagger
      enabled: true
      # 请求前缀
      pathMapping: /
    

    然后重启后台项目刷新浏览器打开接口再调用Token再去测试
    此时就操作成功了

    {
      "code": 200,
      "msg": "操作成功",
      "data": [
        {
          "userId": 1,
          "username": "admin",
          "password": "admin123",
          "mobile": "15888888888"
        },
        {
          "userId": 2,
          "username": "ry",
          "password": "admin123",
          "mobile": "15666666666"
        }
      ]
    }
    

若依常用功能?

权限控制
数据字典
定时任务
表单构建
代码生成

项目结构

表结构
前端代码分析
  • api/course/course.js 用于向后端发送Ajax请求的接口代码
  • views/course/course/index.vue用于在浏览器展示课程的视图组件
src\views\course\course\index.vue
<template>
  <div class="app-container">
    <!-- :model做双向绑定 将前端录入条件封装给响应对象  v-show控制搜索栏显示隐藏-->
    <el-form :model="queryParams" ref="queryRef" :inline="true" v-show="showSearch" label-width="68px">
      <el-form-item label="课程编码" prop="code">
        <!-- v-model双向绑定code(前端课程编码) clearable清理用户输入信息 @keyup键盘回车事件完成搜索-->
        <el-input
          v-model="queryParams.code"
          placeholder="请输入课程编码"
          clearable
          @keyup.enter="handleQuery"
        />
      </el-form-item>
      <el-form-item label="课程学科" prop="subject">
        <!-- v-for遍历课程学科的字典数据列表 :lable展示label :value提交value值-->
        <el-select v-model="queryParams.subject" placeholder="请选择课程学科" clearable>
          <el-option
            v-for="dict in course_subject"
            :key="dict.value"
            :label="dict.label"
            :value="dict.value"
          />
        </el-select>
      </el-form-item>
      <el-form-item label="课程名称" prop="name">
        <el-input
          v-model="queryParams.name"
          placeholder="请输入课程名称"
          clearable
          @keyup.enter="handleQuery"
        />
      </el-form-item>
      <el-form-item label="适用人群" prop="applicablePerson">
        <el-select v-model="queryParams.applicablePerson" placeholder="请选择适用人群" clearable>
          <el-option
            v-for="dict in course_applicable_person"
            :key="dict.value"
            :label="dict.label"
            :value="dict.value"
          />
        </el-select>
      </el-form-item>
      <el-form-item>
        <el-button type="primary" icon="Search" @click="handleQuery">搜索</el-button>
        <el-button icon="Refresh" @click="resetQuery">重置</el-button>
      </el-form-item>
    </el-form>

    <el-row :gutter="10" class="mb8">
      <el-col :span="1.5">
        <!-- @click点击新增按钮弹出新增 v-hasPermi自定义属性完成菜单显示/隐藏 -->
        <el-button
          type="primary"
          plain
          icon="Plus"
          @click="handleAdd"
          v-hasPermi="['course:course:add']"
        >新增</el-button>
      </el-col>
      <!-- :disabled表示这个框是否可用 -->
      <el-col :span="1.5">
        <el-button
          type="success"
          plain
          icon="Edit"
          :disabled="single"
          @click="handleUpdate"
          v-hasPermi="['course:course:edit']"
        >修改</el-button>
      </el-col>
      <el-col :span="1.5">
        <el-button
          type="danger"
          plain
          icon="Delete"
          :disabled="multiple"
          @click="handleDelete"
          v-hasPermi="['course:course:remove']"
        >删除</el-button>
      </el-col>
      <el-col :span="1.5">
        <el-button
          type="warning"
          plain
          icon="Download"
          @click="handleExport"
          v-hasPermi="['course:course:export']"
        >导出</el-button>
      </el-col>
      <!-- 点击会控制'搜索栏'显示隐藏 @queryTable重新加载表格展示数据 -->
      <right-toolbar v-model:showSearch="showSearch" @queryTable="getList"></right-toolbar>
    </el-row>
    <!-- v-loading展示表格的加载状态 遍历展示courseList 事件监听器监听选中行 -->
    <el-table v-loading="loading" :data="courseList" @selection-change="handleSelectionChange">
      <!-- 当用户勾选复选框 触发@selection-change -->
      <el-table-column type="selection" width="55" align="center" />
      <!-- 展示具体数据源数据 -->
      <el-table-column label="课程id" align="center" prop="id" />
      <el-table-column label="课程编码" align="center" prop="code" />
      <el-table-column label="课程学科" align="center" prop="subject">
 <!-- 通过scope拿到整个表格数据 通过:value="scope.row.subject"拿到字典值去匹配字典数据列表 找到该值对应的标签显示给前端-->
        <template #default="scope">
          <dict-tag :options="course_subject" :value="scope.row.subject"/>
        </template>
      </el-table-column>
      <el-table-column label="课程名称" align="center" prop="name" />
      <el-table-column label="价格" align="center" prop="price" />
      <el-table-column label="适用人群" align="center" prop="applicablePerson">
        <template #default="scope">
          <dict-tag :options="course_applicable_person" :value="scope.row.applicablePerson"/>
        </template>
      </el-table-column>
      <el-table-column label="课程介绍" align="center" prop="info" />
      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
<!-- 使用了模板插槽 -->
        <template #default="scope">
          <!-- @click="handleUpdate(scope.row)把当前行的数据传给当前方法  v-hasPermi自定义权限属性-->
          <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['course:course:edit']">修改</el-button>
          <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['course:course:remove']">删除</el-button>
        </template>
      </el-table-column>
    </el-table>
    
    <!-- 分页区域 -->
     <!-- v-show如果大于0条就显示反之隐藏 :total展示总条数 展示分页页码 @pagination="getList"换页后新数据的查询-->
    <pagination
      v-show="total>0"
      :total="total"
      v-model:page="queryParams.pageNum"
      v-model:limit="queryParams.pageSize"
      @pagination="getList"
    />

    <!-- 添加或修改课程管理对话框 -->
     <!-- el-dialog默认隐藏的 点击会显示 append-to-body默认将对话框在body上追加显示 :title动态绑定标题(新增和修改不一样)-->
    <el-dialog :title="title" v-model="open" width="500px" append-to-body>
      <!-- :model来双向绑定 :rules校验规则-->
      <el-form ref="courseRef" :model="form" :rules="rules" label-width="80px">
        <el-form-item label="课程编码" prop="code">
          <el-input v-model="form.code" placeholder="请输入课程编码" />
        </el-form-item>
        <el-form-item label="课程学科" prop="subject">
          <el-select v-model="form.subject" placeholder="请选择课程学科">
            <el-option
              v-for="dict in course_subject"
              :key="dict.value"
              :label="dict.label"
              :value="dict.value"
            ></el-option>
          </el-select>
        </el-form-item>
        <el-form-item label="课程名称" prop="name">
          <el-input v-model="form.name" placeholder="请输入课程名称" />
        </el-form-item>
        <el-form-item label="价格" prop="price">
          <el-input v-model="form.price" placeholder="请输入价格" />
        </el-form-item>
        <el-form-item label="适用人群" prop="applicablePerson">
          <el-select v-model="form.applicablePerson" placeholder="请选择适用人群">
            <el-option
              v-for="dict in course_applicable_person"
              :key="dict.value"
              :label="dict.label"
              :value="dict.value"
            ></el-option>
          </el-select>
        </el-form-item>
        <el-form-item label="课程介绍" prop="info">
          <el-input v-model="form.info" placeholder="请输入课程介绍" />
        </el-form-item>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <!-- 提交前先进行表单规则的校验:rules="rules"  -->
          <el-button type="primary" @click="submitForm">确 定</el-button>
          <el-button @click="cancel">取 消</el-button>
        </div>
      </template>
    </el-dialog>
  </div>
</template>

<script setup name="Course">
// 引入后端api接口文件
import { listCourse, getCourse, delCourse, addCourse, updateCourse } from "@/api/course/course";
// 获取当前实例代理对象,用于访问组件数据、方法
const { proxy } = getCurrentInstance();
// 获取课程学科的数据字典
const { course_applicable_person, course_subject } = proxy.useDict('course_applicable_person', 'course_subject');
// 列表数据
const courseList = ref([]);
// 是否显示弹框
const open = ref(false);
// 是否显示加载状态
const loading = ref(true);
// 是否显示搜索栏
const showSearch = ref(true);
// 复选框,被选中id的数组
const ids = ref([]);
// 复选框,是否单选,用于高亮修改、删除按钮
const single = ref(true);
// 复选框,是否多选,仅高亮删除按钮
const multiple = ref(true);
// 总(记录)条数
const total = ref(0);
// 用于区分新增、修改对话框标题
const title = ref("");
// 定义reactive响应式对象
const data = reactive({
  // 新增或修改表单数据
  form: {},
  // 搜索条件参数
  queryParams: {
    pageNum: 1,
    pageSize: 10,
    code: null,
    subject: null,
    name: null,
    applicablePerson: null,
  },
  // 表单校验规则
  rules: {
    code: [
      { required: true, message: "课程编码不能为空", trigger: "blur" }
    ],
    subject: [
      { required: true, message: "课程学科不能为空", trigger: "change" }
    ],
    name: [
      { required: true, message: "课程名称不能为空", trigger: "blur" }
    ],
    price: [
      { required: true, message: "价格不能为空", trigger: "blur" }
    ],
    applicablePerson: [
      { required: true, message: "适用人群不能为空", trigger: "change" }
    ],
    info: [
      { required: true, message: "课程介绍不能为空", trigger: "blur" }
    ],
  }
});
// 将data对象的三个属性,转换为ref响应式对象
const { queryParams, form, rules } = toRefs(data);

/** 查询课程管理列表 */
function getList() {
  loading.value = true;
  listCourse(queryParams.value).then(response => {
    courseList.value = response.rows;
    total.value = response.total;
    loading.value = false;
  });
}

// 取消按钮
function cancel() {
  open.value = false;
  reset();
}

// 表单重置
function reset() {
  form.value = {
    id: null,
    code: null,
    subject: null,
    name: null,
    price: null,
    applicablePerson: null,
    info: null,
    createTime: null,
    updateTime: null
  };
  proxy.resetForm("courseRef");
}

/** 搜索按钮操作 */
function handleQuery() {
  // 最新的从第一页开始 再发送请求
  queryParams.value.pageNum = 1;
  getList();
}

/** 重置按钮操作 */
function resetQuery() {
  proxy.resetForm("queryRef");
  handleQuery();
}

// 多选框选中数据
// 把选中的复选框对象传递过来
function handleSelectionChange(selection) {
  // 拿到对象调用map方法进行遍历取每个复选框的id
  // 封装给ids的响应式数组对象
  ids.value = selection.map(item => item.id);
  // 控制修改和删除按钮是否高亮可用的 23默认为true
  single.value = selection.length != 1;
  // 修改按钮只要大于0 就是false 那么修改按钮可用使用
  multiple.value = !selection.length;
}

/** 新增按钮操作 */
function handleAdd() {
  reset();
  open.value = true;
  title.value = "添加课程管理";
}

/** 修改按钮操作 */
// 拿到行对象 重置 取出当前行id或一个id
function handleUpdate(row) {
  reset();
  const _id = row.id || ids.value
  getCourse(_id).then(response => {
    form.value = response.data;
    open.value = true;
    title.value = "修改课程管理";
  });
}

/** 提交按钮 */
function submitForm() {
  // '修改课程'对表单进行校验 正则规则...是否必填
  proxy.$refs["courseRef"].validate(valid => {
    if (valid) {// 区分新增还是修改的操作
      if (form.value.id != null) {
        updateCourse(form.value).then(response => {
          proxy.$modal.msgSuccess("修改成功");
          open.value = false;
          getList();
        });
      } else {
        addCourse(form.value).then(response => {
          proxy.$modal.msgSuccess("新增成功");
          open.value = false;
          getList();
        });
      }
    }
  });
}

/** 删除按钮操作 */
function handleDelete(row) {
  // 一行或数组
  const _ids = row.id || ids.value;
  // 防止误操作
  proxy.$modal.confirm('是否确认删除课程管理编号为"' + _ids + '"的数据项?').then(function() {
    return delCourse(_ids);
  }).then(() => {
    getList();
    proxy.$modal.msgSuccess("删除成功");
  }).catch(() => {});
}

/** 导出按钮操作 */
function handleExport() {
  proxy.download('course/course/export', {
    ...queryParams.value
  }, `course_${new Date().getTime()}.xlsx`)
}

getList();
</script>
src\api\course\course.js
// 封装了请求和响应拦截器 下面return每个都调用请求
import request from '@/utils/request'

// 查询课程管理列表
// 接收用户输入参数 调用工具类把参数传过去 向后端发送请求完成课程列表的查询
// 然后返回前端并展示数据
export function listCourse(query) {
  return request({
    url: '/course/course/list',
    method: 'get',
    params: query
  })
}

// 查询课程管理详细
// 点击修改按钮时候根据id去查询 返回给前端
export function getCourse(id) {
  return request({
    url: '/course/course/' + id,
    method: 'get'
  })
}

// 新增课程管理
// 当点击确定按钮的时候 就把数据添加进来发送请求后返回前端
export function addCourse(data) {
  return request({
    url: '/course/course',
    method: 'post',
    data: data
  })
}

// 修改课程管理
// 修改完毕(根据id去查找数据库的)
export function updateCourse(data) {
  return request({
    url: '/course/course',
    method: 'put',
    data: data
  })
}

// 删除课程管理
// 批量/单体删除
export function delCourse(id) {
  return request({
    url: '/course/course/' + id,
    method: 'delete'
  })
}

再次熟悉:前+后端结构

若i18n乱码的情况下

file -> settings -> editor -> file ecoding -> default encoding for properties files:utf-8

后端代码分析

  • CourseController

  • ICourseService及实现类

  • CourseMapper及映射方法

  • Course

  • BaseController:web层通用数据处理

/**
     * 查询课程管理列表
     */
    @PreAuthorize("@ss.hasPermi('course:course:list')")
    @GetMapping("/list")
    public TableDataInfo list(Course course) {
        //1.开启分页
        startPage();
        //2.查询课程列表
        List<Course> list = courseService.selectCourseList(course);
        return getDataTable(list);
    }
// 在分页查询那块会附带着两个封装好的sql语句
// select * from tb_course where xxx 【逐步细分成两个小sql】
  1. select count(*) from tb_course where xxx      //总记录数
  2. select * from tb_course where xxx limit ?,?   //获取当前的数据列表

分页原理

AjaxResult:操作消息提醒

权限解读
  • @PreAuthorize注解是Spring Security框架中用来做权限检查的。
  • 它在运行方法前先验证权限,权限够就放行,不够就拦截
@RestController
@RequestMapping("/course/course")
public class CourseController extends BaseController {
    @Autowired
    private ICourseService courseService;

    /**
     * 查询课程管理列表
     */
// 问题:我怎么知道该用户有没有权限呢?基于RBC权限模型 
    @PreAuthorize("@ss.hasPermi('course:course:list')")
    @GetMapping("/list")
    public TableDataInfo list(Course course) {
        startPage();
        List<Course> list = courseService.selectCourseList(course);
        return getDataTable(list);
    }
}
菜单名称 排序 权限标识
课程管理 1 course:course:list
课程管理查询 1 course:course:query
课程管理新增 2 course:course:add
课程管理修改 3 course:course:edit
课程管理删除 4 course:course:remove
课程管理导出 5 course:course:export

PermissionService.java
源码解读在后期

/**
 * RuoYi首创 自定义权限实现,ss取自SpringSecurity首字母
 * 
 * @author ruoyi
 */
@Service("ss")
public class PermissionService
{
    /**
     * 验证用户是否具备某权限
     * 
     * @param permission 权限字符串
     * @return 用户是否具备某权限
     */
    public boolean hasPermi(String permission)
    {
        if (StringUtils.isEmpty(permission))
        {
            return false;
        }
        LoginUser loginUser = SecurityUtils.getLoginUser();
        if (StringUtils.isNull(loginUser) || CollectionUtils.isEmpty(loginUser.getPermissions()))
        {
            return false;
        }
        PermissionContextHolder.setContext(permission);
        return hasPermissions(loginUser.getPermissions(), permission);
    }

二次开发 —— 苍穹外卖

若依框架修改器
  • 若依框架修改器是一个可以一键修改RuoYi框架包名、项目名等的工具

E:\Java实例项目1-20套\2024-Java若依框架专题课\01-基础篇\资料\04-二次开发\若依框架修改器.exe

选择系列:RuoYi-Vue
目录名称: sky
项目名:sky
包名:com.sky
artifactId:sky
groupId:com.sky
站点名称:外卖管理系统


C:\Users\Pluminary\Desktop\20241108150415\sky
新建业务模块
  • 新建sky-merchant子模块商家管理

    • 新建子模块
      sky-merchant → Advanced Settings → GroupId:com.sky

    • 父工程版本锁定

      pom.xml(sky总)
      <!-- 商家管理-->
                  <dependency>
                      <groupId>com.sky</groupId>
                      <artifactId>sky-merchant</artifactId>
                      <version>${sky.version}</version>
                  </dependency>
      
      pom.xml(sky-merchant)
      <dependencies>
              <dependency>
                  <groupId>com.sky</groupId>
                  <artifactId>sky-framework</artifactId>
              </dependency>
          </dependencies>
      
    • sky-admin添加依赖

      pom.xml(sky-admin) 
      <dependency>
                  <groupId>com.sky</groupId>
                  <artifactId>sky-merchant</artifactId>
              </dependency>
      

菜品管理

  • 利用若依代码生成器(主子表模板),生成菜品管理的前后端代码
tb_dish【菜品管理】
id
name
price
image
description
status
create_time
update_time
tb_dish_flavor【菜品口味关系表】
id
dish_id
name
value
  • 准备SQL并导入数据库

    E:\Java实例项目1-20套\2024-Java若依框架专题课\01-基础篇\资料\04-二次开发\菜品管理

  • 配置代码生成信息【主子表的生成】

    在若依代码生成 → 导入表tb_dishtb_dish_flavor

  • 下载代码并导入项目

  • 修改代码 → 系统工具 → 代码生成 → 菜品管理(修改) →

    基本信息:实体类名称→Dish 作者→itheima

    字段信息参考页面原型生成 → 系统管理 → 字典管理 →

    字典名称:售卖状态
    字典类型:dish_status
    第二页点进去售卖状态的字典类型 dish_status → 新增

    数据标签:停售 + 起售
    数据键值:0 + 1
    显示排序:1 + 2

​ 回到系统工具 → 代码生成 → 菜品管理修改 → 售卖状态 → 显示类型:下拉框 → 字典类型:售卖状态

→ 代码生成 → 修改菜单配置信息 → 菜品

→ 上面的生成信息

生成模板:主子表
生成模块名:merchant
生成业务名:dish
关联子表的表名:tb_dish_flavor:菜品口味关系表
子表关联的外键名:dish_id:菜品

→ 在代码生成 → tb_dish_flavor菜品口味关系表 → 点击编辑

基本信息:
实体名:DishFalvor

→ 生成代码(main后端 + vue前端 + dishMenu.sql数据库动态菜单) → 动态sql导入进去 → 前端vue将merchant导入到./src/api中,将views导入到views中 → 在前端中将java和resources导入到merchant模块中

对菜品管理进行升级改造

主键隐藏掉 售价前缀¥ 修改时间时分秒
src\views\merchant\dish\index.vue 
把  <el-table-column label="主键" align="center" prop="id" />  打注释符
////////////////////////////////////////////////////////////////////////////
插入¥流程:
<el-table-column label="售价" align="center" prop="price" /> 修改为 →
<el-table-column label="售价" align="center" prop="price">
        <template #default="scope">
          <div>
            ¥{{ scope.row.price }}
          </div>
        </template>
      </el-table-column>
通义灵码操作:
先解析那行代码,然后输入使用vue3语法在售价前显示¥
////////////////////////////////////////////////////////////////////////////
生成年月日时分秒:
<el-table-column label="更新时间" align="center" prop="updateTime" width="180">
        <template #default="scope">
          <span>{{ parseTime(scope.row.updateTime, '{y}-{m}-{d}') }}</span>
        </template>
      </el-table-column>
修改为:

修改图片回显bug

新增毛血旺的图片若依默认保存在了本地服务器而不是阿里云上
/profile/upload/2024/11/10/毛血旺_20241110093243A001.jpg

C:\Users\Pluminary\Desktop\20241108150415\sky\sky-admin\src\main\resources\application.yml

# 项目相关配置
ruoyi:
  # 名称
  name: RuoYi
  # 版本
  version: 3.8.8
  # 版权年份
  copyrightYear: 2024
  # 文件路径 示例( Windows配置D:/ruoyi/uploadPath,Linux配置 /home/ruoyi/uploadPath)
  profile: D:/ruoyi/uploadPath
  # 获取ip地址开关
  addressEnabled: false
  # 验证码类型 math 数字计算 char 字符验证
  captchaType: math

D:\ruoyi\uploadPath\upload\2024\11\10
文件上传组件标签修改 增加&& item.indexOf("http") === -1

watch(() => props.modelValue, val => {
  if (val) {
    // 首先将值转为数组
    const list = Array.isArray(val) ? val : props.modelValue.split(",");
    // 然后将数组转为对象数组
    fileList.value = list.map(item => {
      if (typeof item === "string") {
        if (item.indexOf(baseUrl) === -1 && item.indexOf("http") === -1) {
          item = { name: baseUrl + item, url: baseUrl + item };
        } else {
          item = { name: item, url: item };
        }
      }
      return item;
    });
  } else {
    fileList.value = [];
    return [];
  }
},{ deep: true, immediate: true });
修改口味列表数组格式 改为下拉框的口味搭配
src\views\merchant\dish\index.vue
//------------------------------------------------
// 定义口味名称和口味列表静态数据
const dishFlavorListSelect = ref([
  {name:"辣度", value:["不辣","微辣","中辣","重辣"]},
  {name:"忌口", value:["不要葱","不要蒜","不要香菜","不要辣"]},
  {name:"甜味", value:["无糖","少糖","半糖"]}
]);
//------------------------------------------------
src\views\merchant\dish\index.vue
<template #default="scope">
      <!-- <el-input v-model="scope.row.name" placeholder="请输入口味名称" /> -->
               <!-- label是最终看到下拉框的内容 value是用户提交的内容 -->
           <el-select v-model="scope.row.name"  placeholder="请选择口味名称">
                <el-option
                  v-for="dishFlavor in dishFlavorListSelect"
                  :key="dishFlavor.name"
                  :label="dishFlavor.name" 
                  :value="dishFlavor.name"
                >
                </el-option>
            </el-select>
           </template>
       </el-table-column>
src\views\merchant\dish\index.vue【修改当选中辣度时候 后面的规格】
// 存储当前选中口味列表数组
const checkValueList = ref([]);
// 定义改变口味名称时更新当前选中的口味列表
function changeFlavorName(row){
  // 清空当前行的value
  row.value = [];
  // 根据选中的name去查找静态数据的value
  checkValueList.value = dishFlavorListSelect.value.find(item=>item.name==row.name).value;
}
//增加了一个 @change   注意:如果是多选框一定要 → multiple

<el-table-column label="口味名称" prop="name" width="150">
            <template #default="scope">
              <!-- <el-input v-model="scope.row.name" placeholder="请输入口味名称" /> -->
               <!-- label是最终看到下拉框的内容 value是用户提交的内容 -->
               <el-select v-model="scope.row.name"  placeholder="请选择口味名称"
               @change="changeFlavorName(scope.row)">
                <el-option
                  v-for="dishFlavor in dishFlavorListSelect"
                  :key="dishFlavor.name"
                  :label="dishFlavor.name" 
                  :value="dishFlavor.name"
                >
                </el-option>
               </el-select>
            </template>
          </el-table-column>
          <el-table-column label="口味列表" prop="value" width="150">
            <template #default="scope">
              <!-- <el-input v-model="scope.row.value" placeholder="请输入口味列表" /> -->
          <el-select v-model="scope.row.value"  placeholder="请选择口味列表" multiple>
                <el-option
                  v-for="value in checkValueList"
                  :key="value"
                  :label="value" 
                  :value="value"
                />
          </el-select>
          </template>
          </el-table-column>
//------------------------------------------------
// 定义口味名称和口味列表静态数据
const dishFlavorListSelect = ref([
  {name:"辣度", value:["不辣","微辣","中辣","重辣"]},
  {name:"忌口", value:["不要葱","不要蒜","不要香菜","不要辣"]},
  {name:"甜味", value:["无糖","少糖","半糖"]}
]);


// 存储当前选中口味列表数组
const checkValueList = ref([]);
// 定义改变口味名称时更新当前选中的口味列表
function changeFlavorName(row){
  // 根据选中的name去查找静态数据的value
  checkValueList.value =       dishFlavorListSelect.value.find(item=>item.name==row.name).value;
}
//------------------------------------------------
// 此时报错了 
11:59:00.441 [http-nio-8080-exec-64] ERROR c.s.f.w.e.GlobalExceptionHandler - [handleRuntimeException,100] - 请求地址'/merchant/dish',发生未知异常.
org.springframework.http.converter.HttpMessageNotReadableException: JSON parse error: Cannot deserialize value of type `java.lang.String` from Array value (token `JsonToken.START_ARRAY`); nested exception is com.fasterxml.jackson.databind.exc.MismatchedInputException: Cannot deserialize value of type `java.lang.String` from Array value (token `JsonToken.START_ARRAY`)
 at [Source: (org.springframework.util.StreamUtils$NonClosingInputStream); line: 1, column: 239] (through reference chain: com.sky.system.domain.Dish["dishFlavorList"]->java.util.ArrayList[0]->com.sky.system.domain.DishFlavor["value"])
    
/*
根据错误日志,问题出在请求地址 /merchant/dish 处理过程中,JSON 解析时出现了类型不匹配的问题。具体来说,Dish 对象中的 dishFlavorList 字段下的 DishFlavor 对象的 value 字段期望接收一个 String 类型的值,但实际接收到的是一个数组。
*/
解决序列化问题bug将口味列表中value通过JSON工具类转换为字符串

提交数据后是字符串 而不是数组

在331行
/** 提交按钮 */
function submitForm() {
  proxy.$refs["dishRef"].validate(valid => {
    if (valid) {
      form.value.dishFlavorList = dishFlavorList.value;
      // 将口味列表中value通过JSON工具类转换为字符串
      form.value.dishFlavorList.forEach(item=>item.value = JSON.stringify(item.value))
      if (form.value.id != null) {
        updateDish(form.value).then(response => {
          proxy.$modal.msgSuccess("修改成功");
          open.value = false;
          getList();
        });
      } else {
        addDish(form.value).then(response => {
          proxy.$modal.msgSuccess("新增成功");
          open.value = false;
          getList();
        });
      }
    }
  });
解决后端添加成功后 修改后口味列表无法回显再增加个非空判断

因为前端提交了字符串給后端,后端再回去修改的时候无法解析字符串 拿到字符串后返回JSON数组即可

/** 修改按钮操作 */
function handleUpdate(row) {
  reset();
  const _id = row.id || ids.value
  getDish(_id).then(response => {
    form.value = response.data;
    dishFlavorList.value = response.data.dishFlavorList;
    // 将口味列表的value字符串转成json数组
    if(dishFlavorList.value!=null){
      form.value.dishFlavorList.forEach(item=>item.value = JSON.parse(item.value))
    }
    open.value = true;
    title.value = "修改菜品管理";
  });
}

/** 提交按钮 */
function submitForm() {
  proxy.$refs["dishRef"].validate(valid => {
    if (valid) {
      form.value.dishFlavorList = dishFlavorList.value;
      // 将口味列表中value通过JSON工具类转换为字符串
      if(form.value.dishFlavorList!=null){
        form.value.dishFlavorList.forEach(item=>item.value = JSON.stringify(item.value))
      }
      if (form.value.id != null) {
        updateDish(form.value).then(response => {
          proxy.$modal.msgSuccess("修改成功");
          open.value = false;
          getList();
        });
      } else {
        addDish(form.value).then(response => {
          proxy.$modal.msgSuccess("新增成功");
          open.value = false;
          getList();
        });
      }
    }
  });
}
解决修改时无法修改口味列表的下拉框

給口味下拉框绑定一个获取焦点事件 再该事件内调用方法根据当前行的口味名称去查询静态数据 再赋值給静态数组

// 删除清除策略
// 定义口味列表获取焦点时更新当前选中的口味列表
function FocusFlavorName(row){
  // 根据选中的name去查找静态数据的value
  checkValueList.value = dishFlavorListSelect.value.find(item=>item.name==row.name).value;
}
// 加个 @focus
<el-table-column label="口味列表" prop="value" width="150">
            <template #default="scope">
              <!-- <el-input v-model="scope.row.value" placeholder="请输入口味列表" /> -->
              <el-select v-model="scope.row.value"  placeholder="请选择口味列表" multiple
              @focus = "FocusFlavorName(scope.row)">
                <el-option
                  v-for="value in checkValueList"
                  :key="value"
                  :label="value" 
                  :value="value"
                />
               </el-select>
            </template>
          </el-table-column>

二次开发——页面调整

将原有的页面,调整为外卖管理系统的项目标识
  • 浏览器标签页icon、标题
  • 系统页面中的logo、标题
  • 去除源码 & 文档
  • 主题和自定义图标
  • 登录页面中标题、背景图

icon:public中的favicon.ico 改为 favicon.ico.bak 就作废了
把新的图标复制进来改成favicon.ico

标题:最外层index.html
< title> 外卖管理系统 < /title>
但是改完没有效果 是因为运行环境給覆盖了
.env.development修改一下

logo放在了静态资源页面
src\assets\logo\logo.png

修改顶部源码和文档图标内容
src\layout\components\Navbar.vue

<!-- <el-tooltip content="源码地址" effect="dark" placement="bottom">
          <ruo-yi-git id="ruoyi-git" class="right-menu-item hover-effect" />
        </el-tooltip>

        <el-tooltip content="文档地址" effect="dark" placement="bottom">
          <ruo-yi-doc id="ruoyi-doc" class="right-menu-item hover-effect" />
        </el-tooltip> -->

iconfont-阿里巴巴矢量图标库
下载一个菜品管理的小图标
然后给它复制进去src\assets\icons\svg\菜品管理.svg
随后回到菜单管理修改图标就好

登录界面图片修改:
src\views\login.vue 修改一下标题
背景图 → src\assets\images\login-background1.jpg
通过css样式修改背景图
171行
background-image: url("../assets/images/login-background.jpg");
修改为
background-image: url("../assets/images/login-background1.jpg");

.login {
  display: flex;
  justify-content: right;
  align-items: center;
  height: 100%;
  background-image: url("../assets/images/login-background1.jpg");
  background-size: cover;
}
給外卖商家的员工创建角色和用户分配菜单的权限 登录后只能看到自己的功能

系统管理 → 角色管理 → 新增 →
角色名称:商家员工
权限字符串:merchant
角色顺序:4
菜单权限:父子联动
√ 菜品管理

再到用户管理 → 新增 → 添加用户 →
用户昵称:波妞
用户名称:boniu
用户密码:admin123
角色:商家员工

帝可得

  • 帝可得是一个基于物联网概念下的智能售货机运营管理系统

    • 物联网(IOT)

      让各种物品通过互联网链接起来,实现信息的交换和通信

      • 智能家居
      • 共享充电桩
      • 智能售货机
    • 智能售货机

      是物联网技术的一个典型应用

      • 物联网技术
      • 智能分析与推荐
      • 人员设备绑定管理
      • 线上线下融合
  • 售货机术语
    • 区域管理

      为了更高效地进行经营管理,公司将运营范围划分为若干个逻辑区域

    • 点位选择

      点位指的是智能售货机的具体放置位置

    • 售货机功能

      自动小店,摆满了各种零食

    • 货道设计

      售货机里的货道

角色与功能

  • 一个完整的售货机系统由**五端五角色**组成:
  • 管理员:对基础数据(区域、点位、设备、货道、商品等)进行管理
  • 运维人员:投放设备、撤除设备、维修设备。
  • 运营人员:补货。
  • 合作商:仅提供点位,坐收渔翁之利。
  • 消费者:在小程序或屏幕端下单购买商品。

帝可得在线功能文档_Docs

帝可得项目点击链接立即查看 https://codesign.qq.com/s/426304924036117

库表设计

  • 系统后台基础数据表关系说明:

AIGC

  • AI (Artificial Intelligence):即人工智能,是指通过计算机系统模拟人类思维行为一种技术

  • 它通过机器学习、深度学习等算法,使计算机具备对数据分析、理解、推理和决策的能力

  • AIGC (AI Generated content):是AI领域的一个应用分支,专注于利用AI技术自动生成内容

  • 国内常见的通用大模型(AGI)产品:

    • 文心一言
    • 讯飞星火
    • 通义千问
    • KIMI

Prompt的组成

  • 角色:给 AI 定义一个最匹配任务的角色,比如:「你是一位软件工程师」「你是一位小学老师」
  • 指示:对任务进行描述
  • 上下文:给出与任务相关的其它背景信息(尤其在多轮交互中)
  • 例子:必要时给出举例,[实践证明其对输出正确性有帮助]
  • 输入:任务的输入信息;在提示词中明确的标识出输入
  • 输出:输出的格式描述,以便后继模块自动解析模型的输出结果,比如(JSON、Java)

先定义角色,其实就是在开头把问题域收窄,减少二义性。

案例:

角色:你是一位专业的博客作者。

指示:撰写一篇关于最新AI技术发展的文章。

上下文:文章应该涵盖AI技术的当前状态和未来趋势。

例子:可以引用最近的AI技术突破和行业专家的见解。

输入:当前AI技术的相关信息和数据。

输出:一篇结构清晰、观点鲜明的文章草稿。
角色:你是一位资深的Java开发工程师。

指示:编写一个Java函数,该函数接收两个整数参数,并返回它们的和。

上下文:这个函数将被用于一个简单的数学应用程序,该程序帮助学生练习基本的算术运算。

例子:如果你调用函数 `addNumbers(3, 5)`,它应该返回 `8`。

输入:两个整数参数,分别为 `int a` 和 `int b`。

输出:返回这两个整数的和,类型为 `int`。

常见的编程相关的Prompt

表结构

你是一个软件工程师,帮我生成MySQL的表结构
需求如下:
    1,课程管理表,表名tb_course,字段有主键id、课程编码、课程学科、课程名称、课程价格、适用人群、课程介绍
其他要求:
    1,每个表中都有创建时间(create_time)、修改时间(date_time)、创建人(create_by)、修改人(update_by)、备注(remark)这些字段
    2,每个表的主键都是自增的
    3,课程价格是整型、课程编码是字符串
    4,请为每个字段都添加上comment
    5,帮我给生成的表中插入一些IT课程示例数据
        课程学科:Java、人工智能、大数据
        适用人群:小白学员、中级程序员

生成数据库说明文档

你是一个软件工程师,现在要根据数据库的sql脚本,编写数据库说明文档,sql脚本如下:
CREATE TABLE `tb_course` (
    `id` INT AUTO_INCREMENT COMMENT '主键ID',
    `course_code` VARCHAR(255) NOT NULL COMMENT '课程编码',
    `course_subject` VARCHAR(100) NOT NULL COMMENT '课程学科',
    `course_name` VARCHAR(255) NOT NULL COMMENT '课程名称',
    `course_price` INT COMMENT '课程价格',
    `target_audience` VARCHAR(100) COMMENT '适用人群',
    `course_introduction` TEXT COMMENT '课程介绍',
    `create_time` DATETIME COMMENT '创建时间',
    `update_time` DATETIME COMMENT '修改时间',
    `create_by` VARCHAR(64) COMMENT '创建人',
    `update_by` VARCHAR(64) COMMENT '修改人',
    `remark` VARCHAR(255) COMMENT '备注',
    PRIMARY KEY (`id`)
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='课程管理表';

输出要求是:
    1,每个表以及每个表的字段都要详细说明,包括,字段名称、类型、作用
    2,使用markdown的输出格式,字段的描述需要使用表格展示
    3,如果表之间有关系,需要描述清楚表之间的关系

生成代码

代码生成算是比较常规的方案,用的也比较多,分为了几种情况

  • 给出表生成代码(项目中常见)
    • 给出表结构的ddl,可以输出这个表的增删改查的所有代码
    • 给出表结构的dll,可以输出增删改查的接口文档
  • 补全代码
    • 例1-给出实体类,帮助编写getter、setter、toString、构造方法等等
    • 例2-给出一个controller,帮助编写swagger注解等
  • 提取结构(无含金量,费时间的编程)
    • 例1-根据接口文档提取dto类或者vo类

生成代码流程图

有一些比较复杂的业务流程,往往需要画出流程图,现在就可以使用ai协助我们画流程图

你是一个软件工程师,为了方便理解代码执行流程,需要给出代码执行的流程图,代码如下:
    // 创建工单
    @Transactional
    @Override
    public int insertTaskDto(TaskDto taskDto) {
    //1. 查询售货机是否存在
    VendingMachine vm = vendingMachineService.selectVendingMachineByInnerCode(taskDto.getInnerCode());
    if (vm == null) {
        throw new ServiceException("设备不存在");
    }
    //2. 校验售货机状态与工单类型是否相符
    checkCreateTask(vm.getVmStatus(), taskDto.getProductTypeId());
    //3. 校验这台设备是否有未完成的同类型工单,如果存在则不能创建
    hasTask(taskDto.getInnerCode(), taskDto.getProductTypeId());
    //4. 校验员工是否存在
    Emp emp = empService.selectEmpById(taskDto.getUserId());
    if (emp == null) {
        throw new ServiceException("员工不存在");
    }
    // 5. 校验非同区域下的工作人员不能接受工单
    if (emp.getRegionId() != vm.getRegionId()) {
        throw new ServiceException("非同区域下的工作人员不能接受工单");
    }
    //6. 保存工单信息
    Task task = new Task();
    BeanUtil.copyProperties(taskDto, task);// 属性赋值
    task.setCreateTime(DateUtils.getNowDate());// 创建时间
    task.setTaskCode(generateTaskCode());// 工单编号 202405150001
    task.setTaskStatus(DkdContants.TASK_STATUS_CREATE);// 创建工单
    task.setAddr(vm.getAddr());
    task.setRegionId(vm.getRegionId());
    task.setUserName(emp.getUserName());
    int taskResult = taskMapper.insertTask(task);
    //7. 如果是补货工单,向工单明细表插入记录
    if (task.getProductTypeId() == DkdContants.TASK_TYPE_SUPPLY) {
        if (CollUtil.isEmpty(taskDto.getDetails())) {
            throw new ServiceException("补货工单明细不能为空");
        }
        List<TaskDetails> taskDetailsList = taskDto.getDetails().stream().map(details -> {
            TaskDetails taskDetails = BeanUtil.copyProperties(details, TaskDetails.class);
            taskDetails.setTaskId(task.getTaskId());
            return taskDetails;
        }).collect(Collectors.toList());
        taskDetailsService.insertBatch(taskDetailsList);
    }

    return taskResult;
}

输出:请使用mermaid语言来描述这个方法的流程
graph TD;
    A[查询售货机] --> B(售货机不存在?)
    B -- 是 --> C[抛出异常: 设备不存在]
    B -- 否 --> D[校验售货机状态与工单类型]
    D --> E(校验失败?)
    E -- 是 --> F[抛出异常]
    E -- 否 --> G[检查未完成同类型工单]
    G --> H(存在未完成同类型工单?)
    H -- 是 --> I[抛出异常]
    H -- 否 --> J[查询员工]
    J --> K(员工不存在?)
    K -- 是 --> L[抛出异常: 员工不存在]
    K -- 否 --> M[校验员工区域与售货机区域是否一致]
    M --> N(不一致?)
    N -- 是 --> O[抛出异常: 非同区域工作人员不能接受工单]
    N -- 否 --> P[保存工单信息]
    P --> Q(保存成功?)
    Q -- 否 --> R[处理保存失败]
    Q -- 是 --> S(工单类型为补货?)
    S -- 是 --> T[处理补货工单明细]
    T --> U[补货明细处理结果?]
    U -- 失败 --> V[抛出异常: 补货工单明细不能为空]
    U -- 成功 --> END(结束)
    S -- 否 --> END

项目搭建(后端)

复制gitee克隆的地址https://gitee.com/Pluminary/dkd-parent.git,在新的idea中找到Get from Version Control在URL中导入xxx.git
若左列模块没有高亮 右侧找maven→clean→package
MySQL的配置和导入 → sql里的sql文件导入到Database
C:\Users\Pluminary\Desktop\dkd-parent\dkd-admin\src\main\resources\application-druid.yml
连接好数据库配置信息
Redis的配置(搞个密码)
为了方便学习我没有搞redis的password 项目里的是:root【application.yml的redis里面】
打开redis服务后就可以启动啦dkd-parent\dkd-admin\src\main\java\com\dkd\DkdApplication.java

项目搭建(前端)

通过vscode克隆源码,仓库地址:https://gitee.com/ys-gitee/dkd-vue.git\

  • 打开VS Code,并确保已经安装了Git。如果未安装Git,请先下载并安装。
  • 在VS Code左侧的活动栏中点击”Source Control”图标,或者按下Ctrl+Shift+G,打开Git集成面板。
  • 在Git集成面板上方的输入框中,选择并输入要克隆的Git仓库地址。可以是HTTP或SSH地址。
  • 点击Enter键,VS Code将连接到Git仓库并拉取最新的代码

在vscode中右上角第二个小框框 点击打开命令行 导入jar包 npm install
然后npm run dev打开项目
帝可得管理系统 http://10.254.1.228/index

在VSCode项目中运行npm install命令主要是用于安装项目所需的Node.js包依赖。以下是具体的作用和步骤:

安装依赖包:当你创建一个Node.js项目或者从版本控制系统中克隆一个项目到本地时,项目中通常会包含一个package.json文件。这个文件里列出了项目所有依赖的包及其版本号。运行npm install命令会读取这个文件,然后从npm(Node Package Manager)仓库下载并安装所有列出的依赖包到项目的node_modules目录。

确保环境一致性:通过npm install,可以确保在不同的开发环境和生产环境中,项目使用的是相同版本的依赖包,这有助于避免因为环境差异导致的bug。

项目初始化:如果你是第一次在一个项目中运行npm install,它还会运行每个依赖包的install脚本,这些脚本可能会进行一些设置工作,比如编译代码、生成必要的文件等。

脚本命令:在package.json中,除了依赖项,还可以定义一些脚本命令(scripts)。npm install会使得这些命令变为可用状态,你可以在项目目录下通过npm run <script-name>来执行这些脚本。

[npm 加速,命令行修改国内镜像源【附带国内最新几个镜像】超简约版_npm 修改国内镜像-CSDN博客](https://blog.csdn.net/m0_52172586/article/details/142930356#::text=1.查看目前的镜像源 >npm get registry 2.设置镜像源 >npm,config set registry https%3A %2F%2Fregistry.npmmirror.com 3.验证)

1.查看目前的镜像源
> npm get registry
2.设置镜像源
> npm config set registry https://registry.npmmirror.com
3.验证
> npm get registry
    
/////////////////////////////////////////
后端maven镜像就先设置好maven地址(非C盘)
然后再去配置maven里的文件设置镜像

点位管理

需求说明

业务场景: 假设我们的公司现在有一个宏伟的计划——在北京发展业务。首先,我们需要确定几个有潜力的区域,这些区域可能是人流量大、消费能力高的商业区或居民区。然后,我们要与这些区域内的潜在合作商进行洽谈,比如商场、写字楼、学校等地方的管理者或所有者。

一旦我们与合作商达成协议,确定了合作的细节和点位,我们就可以安排工作人员去投放智能售货机了。这些点位将成为我们智能售货机的“家”,为消费者提供便捷的购买服务。

点位管理主要涉及到三个功能模块,业务流程如下:

  1. 登录系统:后台管理人员登录后台系统
  2. 新增区域: 后台管理人员可以添加区域范围,区域范围与运维/运维人员挂钩,区域下可关联点位。
  3. 新增合作商: 管理人员可以添加合作商,合作商与点位进行关联。
  4. 新增区域点位: 后台管理人员可以在特定区域内新增点位,这些点位是放置智能售货机的具体位置。
graph TD
    A[登录系统] 
    A --> B[新增区域]
    B --> C[新增合作商]
    C --> D[新增区域点位]

库表设计

  • 参考页面原型和具体需求完成库表设计

区域表: 主键id、区域名称、备注说明
合作商表: 主键id、合作商名称、联系人、联系电话、分成比例、账号、密码
点位表: 主键id、点位名称、详细地址、商圈类型、区域外键、合作商外键

// 你是一位软件工程师,帮我生成MySQL的表结构
需求如下:
1,区域表,表名tb_region,字段有主键id、区域名称
2,合作商表,表名tb_partner,字段有主键id、合作商名称、联系人、联系电话、分成比例(int类型)、账号、密码
3,点位表,表名tb_node,字段有主键id、点位名称、详细地址、商圈类型(int类型)
    
其他要求:
1,每张表中都有创建时间(create_time)、修改时间(date_time)、创建人(create_by)、修改人(update_by)、备注(remark)这些字段
2,每张表的主键都是自增的
3,区域与点位是一对多的关系,合作商与点位是一对多的关系,请用字段表示出来,并建立外键约束
4,请为所有字段都添加上comment
5,帮我给生成的表中插入一些北京城市相关区域、点位、合作商的测试数据
CREATE TABLE `tb_region` (
  `id` INT AUTO_INCREMENT COMMENT '主键id' PRIMARY KEY,
  `region_name` VARCHAR(255) NOT NULL COMMENT '区域名称',
  `create_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
  `create_by` VARCHAR(64) COMMENT '创建人',
  `update_by` VARCHAR(64) COMMENT '修改人',
  `remark` TEXT COMMENT '备注'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='区域表';

-- 插入测试数据
INSERT INTO `tb_region` (`region_name`,`remark`) VALUES ('北京市朝阳区','北京市朝阳区'), ('北京市海淀区','北京市海淀区'), ('北京市东城区','北京市东城区');

CREATE TABLE `tb_partner` (
  `id` INT AUTO_INCREMENT COMMENT '主键id' PRIMARY KEY,
  `partner_name` VARCHAR(255) NOT NULL COMMENT '合作商名称',
  `contact_person` VARCHAR(64) COMMENT '联系人',
  `contact_phone` VARCHAR(15) COMMENT '联系电话',
  `profit_ratio` INT COMMENT '分成比例',
  `account` VARCHAR(64) COMMENT '账号',
  `password` VARCHAR(64) COMMENT '密码',
  `create_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
  `create_by` VARCHAR(64) COMMENT '创建人',
  `update_by` VARCHAR(64) COMMENT '修改人',
  `remark` TEXT COMMENT '备注'
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='合作商表';

-- 插入测试数据
INSERT INTO `tb_partner` (`partner_name`, `contact_person`, `contact_phone`, `profit_ratio`, `account`, `password`) VALUES
('合作商A', '张三', '13800138000', 30, 'a001', 'pwdA'),
('合作商B', '李四', '13912345678', 25, 'b002', 'pwdB');

CREATE TABLE `tb_node` (
  `id` INT AUTO_INCREMENT COMMENT '主键id' PRIMARY KEY,
  `node_name` VARCHAR(255) NOT NULL COMMENT '点位名称',
  `address` VARCHAR(255) NOT NULL COMMENT '详细地址',
  `business_type` INT COMMENT '商圈类型',
  `region_id` INT COMMENT '区域ID',
  `partner_id` INT COMMENT '合作商ID',
  `create_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
  `update_time` TIMESTAMP DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '修改时间',
  `create_by` VARCHAR(64) COMMENT '创建人',
  `update_by` VARCHAR(64) COMMENT '修改人',
  `remark` TEXT COMMENT '备注',
  FOREIGN KEY (`region_id`) REFERENCES `tb_region`(`id`) ON DELETE CASCADE ON UPDATE CASCADE,
  FOREIGN KEY (`partner_id`) REFERENCES `tb_partner`(`id`) ON DELETE CASCADE ON UPDATE CASCADE
) ENGINE=InnoDB DEFAULT CHARSET=utf8mb4 COMMENT='点位表';


-- 插入测试数据
-- 假设区域ID为1对应'北京市朝阳区',合作商ID为1对应'合作商A'
INSERT INTO `tb_node` (`node_name`, `address`, `business_type`, `region_id`, `partner_id`) VALUES
('三里屯点位', '北京市朝阳区三里屯路', 1, 1, 1),
('五道口点位', '北京市海淀区五道口', 2, 2, 2);

对于点位管理数据模型,下面是示意图:

  • 关系字段:region_id、partner_id

  • 数据字典:business_type

生成基础代码

使用若依代码生成器,生成区域管理、合作商管理、点位管理前后端基础代码,导入项目中:
  • 创建目录菜单
  • 添加数据字典
  • 配置代码生成信息
  • 下载代码并导入项目

系统管理 → 菜单管理 → 新增菜单
主类目、点位管理、2、node

→ 系统管理 → 字典管理
字典名称:商圈类型
字典类型:business_type
→ 在第二页找到商圈类型点进去
新增:旅游区
<u>数据标签:旅游区
数据键值:1
显示排序:1
→ 新增:商场写字楼、2、2;学校33、交通枢纽44

→ 系统工具 → 代码生成 → 导入表(tb_node、tb_partner、tb_region)
分别配置表的生成信息
→ 点击区域表字段信息
→ 根据新增区域弹出菜单显示 需要增加区域名称全打勾 备注说明除了查询全打勾 其余全×

→ 点击**合作商表** → 生成信息
包路径:com.dkd.manage、生成模块名:manage、生成业务名:region、生成功能名:区域管理、上级菜单:点位管理
→ 代码生成:Partner
→ 基本信息:
实体类名称:Partner
作者:itheima
→ 字段信息:见**帝可得后台管理系统.md**
→ 生成信息:
生成包路径:com.dkd.manage
生成模块名:manage
生成功能名:合作商管理
上级菜单:点位管理

→ 点击**点位表**
→ 生成信息:
生成包路径:com.dkd.manage
生成模块名:manage
生成功能名:点位管理
上级菜单:点位管理
→ 字段信息:见**帝可得后台管理系统.md**
→ 基本信息:
实体类名称:Node
表描述:点位表
作者:itheima

回到代码生成 选中三张表 生成!!
分别在前后端和数据库 导入java/manage、vue/manage、sql代码

细节:如果当你创建一个模块以后 src.main.java里面没有任何代码 resources里面也没有 它提交仓库的时候是默认空的 所以可以手动添加一个占位符.gitkeep虽然什么都不是,但是可以提交空项目模块

区域管理改造

基础页面

需求

  • 参考页面原型,完成基础布局展示改造
// 让前端页面自动排序
src\views\manage\region\index.vue
<el-table-column label="序号" type="index" width="50" align="center" prop="id" />
区域管理改造
  • 查看详情,需要显示所有区域下所有点位列表(稍后完成)

  • 在查询区域列表时,同时显示每个区域的点位数,还要新增查看详情

    修改后端要参考接口文档,修改前端要参考产品原型

  • 实现此功能方案:

    (1) 同步存储在区域表中有点位数的字段,当点位发生变化时候,同步区域表中的点位数(在tb_region里面新增一个node_count 方案可行考虑缺点:每次点位数据变化时都要更新区域表[增加了工作量],添加数据不一致也会)
    (2) 关联查询编写关联查询语句,在mapper层封装

SQL查询:先聚合统计每个区域的点位数,然后与区域表进行关联查询
[提前在idea中下面的通义灵码状态勾选 本地补全模型云端模型自动触发]
数据库返回的数据 要结合前端所需要的返回数据来写
比如接口文档需要返回remark id name nodeCount ==> select r.id,r.region_name,r.remark,ifnull(n.node_count,0) as node_count

-- 传统模式
-- 1.先聚合统计每个区域下的点位数
-- 确定查询表 tb_node
-- 确定分组字段 region_id
select region_id,count(*) as node_count from tb_node group by region_id;
-- 2.然后与区域表进行关联查询   内连接是两个表的交集
select r.id,r.region_name,r.remark,ifnull(n.node_count,0) as node_count 
   from tb_region r
left join (select region_id,count(*) as node_count 
   from tb_node group by region_id) n 
on r.id=n.region_id;

-- AI辅助编程模式
-- 查询区域表所有的信息,需要显示每个区域的点位数
SELECT r.*, COUNT(n.id) AS node_count FROM tb_region r LEFT JOIN tb_node n ON r.id = n.region_id GROUP BY r.id;
com/dkd/manage/domain/Region.java
package com.dkd.manage.domain;

import org.apache.commons.lang3.builder.ToStringBuilder;
import org.apache.commons.lang3.builder.ToStringStyle;
import com.dkd.common.annotation.Excel;
import com.dkd.common.core.domain.BaseEntity;

/**
 * 区域管理对象 tb_region
 * 
 * @author itheima
 * @date 2024-11-12
 */
public class Region extends BaseEntity
{
    private static final long serialVersionUID = 1L;

    /** 主键id */
    private Long id;

    /** 区域名称 */
    @Excel(name = "区域名称")
    private String regionName;

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

    public Long getId() 
    {
        return id;
    }
    public void setRegionName(String regionName) 
    {
        this.regionName = regionName;
    }

    public String getRegionName() 
    {
        return regionName;
    }

    @Override
    public String toString() {
        return new ToStringBuilder(this,ToStringStyle.MULTI_LINE_STYLE)
            .append("id", getId())
            .append("regionName", getRegionName())
            .append("createTime", getCreateTime())
            .append("updateTime", getUpdateTime())
            .append("createBy", getCreateBy())
            .append("updateBy", getUpdateBy())
            .append("remark", getRemark())
            .toString();
    }
}
com/dkd/manage/domain/vo/RegionVo.java
package com.dkd.manage.domain.vo;

import com.dkd.manage.domain.Region;
import lombok.Data;

@Data
public class RegionVo extends Region {
    // 点位数量
    private Integer nodeCount;

}
com/dkd/manage/mapper/RegionMapper.java
/**
     * 查询区域列表
     * @param regionVo
     * @return
     */
    public List<RegionVo> selectRegionVoList(RegionVo regionVo);
mapper/manage/RegionMapper.xml    
<select id="selectRegionVoList" resultType="com.dkd.manage.domain.vo.RegionVo">
        SELECT r.*, COUNT(n.id) AS node_count FROM tb_region r LEFT JOIN tb_node n ON r.id = n.region_id GROUP BY r.id
    </select>
思考:上面的xml中提取的字段是 node_count 而前端让返回的是驼峰式命名 nodeCount, 若依默认关闭了此功能需要手动开启此功能
dkd-parent → resources → mybatis → mybatis-config.xml
<!-- 使用驼峰命名法转换字段 -->
<setting name="mapUnderscoreToCamelCase" value="true"/>
Ctrl+F9是热部署 在不新增文件的时候可以直接部署

RegionMapper

/**
 * 查询区域管理列表
 * @param region
 * @return RegionVo集合
 */
public List<RegionVo> selectRegionVoList(Region region);

RegionMapper.xml

<select id="selectRegionVoList" resultType="com.dkd.manage.domain.vo.RegionVo">
select r.id,r.region_name,r.remark,ifnull(n.node_count,0) as node_count from tb_region r
    left join (select region_id,count(*) as node_count from tb_node group by region_id) n on r.id=n.region_id
    <where>
       <if test="regionName != null  and regionName != ''"> and r.region_name like concat('%', #{regionName}, '%')</if>
    </where>
</select> 

IRegionService

/**
 * 查询区域管理列表
 * @param region
 * @return RegionVo集合
 */
public List<RegionVo> selectRegionVoList(Region region);

RegionServiceImpl

/**
 * 查询区域管理列表
 * @param region
 * @return RegionVo集合
 */
@Override
public List<RegionVo> selectRegionVoList(Region region) {
    return regionMapper.selectRegionVoList(region);
}

RegionController

/**
 * 查询区域管理列表
 */
@PreAuthorize("@ss.hasPermi('manage:region:list')")
@GetMapping("/list")
public TableDataInfo list(Region region)
{
    startPage();
    List<RegionVo> voList = regionService.selectRegionVoList(region);
    return getDataTable(voList);
}

region/index.vue

<!-- 区域列表 -->
<el-table v-loading="loading" :data="regionList" @selection-change="handleSelectionChange">
  <el-table-column type="selection" width="55" align="center" />
  <el-table-column label="序号" type="index" width="50" align="center" prop="id" />
  <el-table-column label="区域名称" align="center" prop="regionName" />
  <el-table-column label="点位数" align="center" prop="nodeCount" />
  <el-table-column label="备注说明" align="center" prop="remark" />
  <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
    <template #default="scope">
      <el-button link type="primary"  @click="handleUpdate(scope.row)" v-hasPermi="['manage:region:edit']">修改</el-button>
      <el-button link type="primary"  @click="handleDelete(scope.row)" v-hasPermi="['manage:region:remove']">删除</el-button>
    </template>
  </el-table-column>
</el-table>
小结
  • 区域列表改造步骤
    • 确定关联查询方案并编写sql
    • 创建RegionVo
    • 在RegionMapper和xml中添加查询Vo方法和sql
    • 在RegionService接口和实现类中添加查询Vo方法
    • 修改RegionController查询方法
    • 修改前端视图组件

合作商改造-查看详情

密码是明文 改成密文
<el-form-item label="密码" prop="password">
   <el-input v-model="form.password" type="password" placeholder="请输入密码" />
</el-form-item>
隐藏修改时的账号密码id存在时隐藏不存在显示,因为修改和新增共用了一个对话框ui
<el-form-item label="账号" prop="account" v-if="form.id==null">
    <el-input v-model="form.account" placeholder="请输入账号" />
</el-form-item>
<el-form-item label="密码" prop="password" v-if="form.id==null">
    <el-input v-model="form.password" type="password" placeholder="请输入密码" />
</el-form-item>
前端需要返回创建时间因为数据返回时有,用v-if判断是否修改显示创建时间
 <el-form-item label="创建时间" prop="contactPhone" v-if="form.id!=null">
          {{form.createTime}}
        </el-form-item>
新增时保存的数据是以明文保存到了数据库此时新增的合作商就是密文了
com/dkd/manage/service/impl/PartnerServiceImpl.java  
/**
     * 新增合作商管理
     *
     * @param partner 合作商管理
     * @return 结果
     */
    @Override
    public int insertPartner(Partner partner) {
        // 使用SpringSecurity工具类,对前端传入的密码进行加密
        partner.setPassword(SecurityUtils.encryptPassword(partner.getPassword()));
        partner.setCreateTime(DateUtils.getNowDate());
        return partnerMapper.insertPartner(partner);
    }
合作商管理改造—合作商详情
  • 查看详情,需要显示合作商名称、联系人、联系电话、分成比例
  • 在查询合作商列表时,同时显示每个合作商的点位数
  • 重置密码,初始密码为123456
/** 借鉴修改流程
/** 修改按钮操作 */
function handleUpdate(row) {
  reset();
  const _id = row.id || ids.value
  getPartner(_id).then(response => {
    form.value = response.data;
    open.value = true;
    title.value = "修改合作商管理";
  });
}
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
  <template #default="scope">
    <el-button link type="primary" icon="Edit" @click="getPartnerInfo(scope.row)" v-hasPermi="['manage:partner:query']">查看详情</el-button>
  </template>
</el-table-column>
com/dkd/manage/controller/PartnerController.java
    /**
     * 获取合作商管理详细信息
     */
    @PreAuthorize("@ss.hasPermi('manage:partner:query')")
    @GetMapping(value = "/{id}")
    public AjaxResult getInfo(@PathVariable("id") Long id)
    {
        return success(partnerService.selectPartnerById(id));
    }
/** 查看合作商详情 **/
  const partnerInfoOpen = ref(false)
  function getPartnerInfo(row){
    reset();
    const _id = row.id
    getPartner(_id).then(response => {
      form.value = response.data;
      partnerInfoOpen.value = true;
    });
  }
    <!-- 查看合作商详情对话框 -->
<el-dialog title="合作商详情" v-model="partnerInfoOpen" width="500px" append-to-body>
    <!-- 使用el-descriptions组件以卡片形式展示信息,更加整洁 -->
    <el-descriptions :column="2" border>
        <el-descriptions-item label="合作商名称">{{ form.partnerName }}</el-descriptions-item>
        <el-descriptions-item label="联系人">{{ form.contactPerson }}</el-descriptions-item>
        <el-descriptions-item label="联系电话">{{ form.contactPhone }}</el-descriptions-item>
        <el-descriptions-item label="分成比例">{{ form.profitRatio }}%</el-descriptions-item>
    </el-descriptions>
</el-dialog>

合作商改造—列表查询点位管理→合作商管理→增加点位数

  • 实现此功能方案:

后端改造

关联查询编写关联查询语句,在mapper层封装

tb_node(点位表)的partner_id(合作商ID)
关联
tb_partner(合作商表)的id

-- 查询合作商表的所有信息,同时显示每个合作商的点位数
select p.*, count(n.id) as node_count
from tb_partner p
    left join tb_node n on p.id = n.partner_id
group by p.id

首先先创建一个需要查询新东西的方法增加

com/dkd/manage/domain/vo/PartnerVo.java
package com.dkd.manage.domain.vo;

import com.dkd.manage.domain.Partner;
import lombok.Data;

@Data
public class PartnerVo extends Partner {
    // 点位数量
    private Integer nodeCount;
}
com/dkd/manage/controller/PartnerController.java
 /**
     * 查询合作商管理列表
     */
    @PreAuthorize("@ss.hasPermi('manage:partner:list')")
    @GetMapping("/list")
    public TableDataInfo list(Partner partner) {
        startPage();
        List<PartnerVo> voList = partnerService.selectPartnerVoList(partner);
        return getDataTable(voList);
    }
com/dkd/manage/service/IPartnerService.java
/**
     * 查询合作商列表
     * @param partner
     * @return
     */
    public List<PartnerVo> selectPartnerVoList(Partner partner);
com/dkd/manage/service/impl/PartnerServiceImpl.java
/**
     * 查询合作商列表
     * @param partner
     * @return
     */
    @Override
    public List<PartnerVo> selectPartnerVoList(Partner partner) {
        return partnerMapper.selectPartnerVoList(partner);
    }
com/dkd/manage/mapper/PartnerMapper.java
/**
     * 查询合作商列表
     * @param partner
     * @return
     */
    public List<PartnerVo> selectPartnerVoList(Partner partner);
mapper/manage/PartnerMapper.xml
</select>
        <select id="selectPartnerVoList" resultType="com.dkd.manage.domain.vo.PartnerVo">
        select p.*, count(n.id) as node_count
        from tb_partner p
                 left join tb_node n on p.id = n.partner_id
        <where>
            <if test="partnerName != null  and partnerName != ''"> and partner_name like concat('%', #{partnerName}, '%')</if>
        </where>
        group by p.id
    </select>

前端改造

<el-table-column type="selection" width="55" align="center" />
      <el-table-column label="序号" type="index" width="50" align="center" prop="id" />
      <el-table-column label="合作商名称" align="center" prop="partnerName" />
      <el-table-column label="点位数" align="center" prop="nodeCount" />
      <el-table-column label="账号" align="center" prop="account" />
      <el-table-column label="分成比例" align="center" prop="profitRatio">
        <template #default="scope">
          {{ scope.row.profitRatio + '%' }}
        </template>
      </el-table-column>
      <el-table-column label="联系人" align="center" prop="contactPerson" />
      <el-table-column label="联系电话" align="center" prop="contactPhone" />
      <el-table-column label="操作" align="center" class-name="small-padding fixed-width">

合作商改造——重置密码

  • 查看详情,需要显示合作商名称、联系人、联系电话、分成比例
  • 在查询合作商列表时,同时显示每个合作商的点位数
  • 重置密码,初始密码为123456

后端部分

在PartnerController中

/**
 * 重置合作商密码
 */
@PreAuthorize("@ss.hasPermi('manage:partner:edit')")
@Log(title = "重置合作商密码", businessType = BusinessType.UPDATE)
@PutMapping("/resetPwd/{id}")
public AjaxResult resetpwd(@PathVariable Long id) {//1. 接收参数
    //2. 创建合作商对象
    Partner partner = new Partner();
    partner.setId(id);// 设置id
    partner.setPassword(SecurityUtils.encryptPassword("123456"));// 设置加密后的初始密码
    //3. 调用service更新密码
    return toAjax(partnerService.updatePartner(partner));
}

前端部分

manage/partner.js请求api中

// 重置合作商密码
export function resetPartnerPwd(id){
  return request({
    url: '/manage/partner/resetPwd/' + id,
    method: 'put'
  })
}

partner/index.vue视图组件中参考@click=”handleDelete”,此方法删除时弹出对话框

<el-table-column label="操作" align="center" class-name="small-padding fixed-width" width="300px">
    <template #default="scope">
        <el-button link type="primary" @click="resetPwd(scope.row)" v-hasPermi="['manage:partner:edit']">重置密码</el-button>
    </template>
</el-table-column>

<script>
    import { listPartner, getPartner, delPartner, addPartner, updatePartner,resetPartnerPwd } from "@/api/manage/partner";
    /* 重置合作商密码 */
    function resetPwd(row) {
        proxy.$modal.confirm('你确定要重置该合作商密码吗?').then(function () {
            return resetPartnerPwd(row.id);
        }).then(() => {
            proxy.$modal.msgSuccess("重置成功");
        }).catch(() => { });
    }
</script>

点位管理改造——基础布局

修改前端更简单一些 可以把前端一次性请求1w条数据 后端接口就可以重复调用

src\views\manage\node\index.vue视图组件中

import {listRegion} from "@/api/manage/region";

/* 查询所有条件对象 */
const loadAllParams=reactive({
  pageNum:1,
  pageSize:10000
})
/** 查询区域列表 **/
const regionList=ref([]);
function getRegionList() {
  listRegion(loadAllParams).then(response=>{
    regionList.value=response.rows;
  })
}
getRegionList();
getList();

src\api\manage\region.js

// 查询区域管理列表
export function listRegion(query) {
  return request({
    url: '/manage/region/list',
    method: 'get',
    params: query
  })
}
新增点位管理的时候想把合作商输入的改成自动获取的下拉框
  • 定义js代码向后台发送请求,将请求后的结果封装给合作商parnterList集合
  • 将文本框改成下拉框来遍历展示每个合作商的名称,提交时关联合作商的id
      <el-table-column label="详细地址" align="center" prop="address" show-overflow-tooltip/> 详细地址多出的部分隐藏只有鼠标移动到才会显示


import {listPartner} from "@/api/manage/partner"

<el-form-item label="合作商ID" prop="partnerId">
          <!-- <el-input v-model="form.partnerId" placeholder="请输入合作商ID" /> -->
           <el-select v-model="form.partnerId" placeholder="请选择合作商">
            <el-option
              v-for="item in partnerList"
              :key="item.id"
              :label="item.partnerName"
              :value="item.id">
            </el-option>
           </el-select>
</el-form-item>

/* 查询合作商列表 */
const partnerList=ref([]);
function getPartnerList(){
  listPartner(loadAllParams).then(response=>{
    partnerList.value = response.rows;
  })
}
getPartnerList();
避免每次都要写pageSize:10000 直接搞入js里面
src\api\page.js
/* 查询所有条件对象 */
// const loadAllParams=reactive({
//   pageNum:1,
//   pageSize:10000
// }) →

export const loadAllParams = reactive({
  pageNum: 1,
  pageSize: 10000,
});

点位管理改造点位中增加个查看详情(将单表查询改为多表查询咯)

  • 查看详情,需要显示当前点位下所有设备列表(稍后完成)
  • 在区域详情中,需要显示每个点位的设备数
  • 在点位列表查询中,关联显示区域、合作商等信息
  • 关联查询:对于设备数量的统计,我们需要执行关联查询,在mapper层封装
  • 关联实体对于区域和合作商的数据,我们会采用Mybatis提供的嵌套查询功能

点位管理改造表设计.png

<resultMap>......</resultMap>   #完成手动映射

#解决一对一 或 多对一 映射结果集只有一个对象时完成的ORM的映射封装
<association>......</association>  #点位和点位1对1  点位和合作商1对多

#解决一对多场景下来映射多个结果的集合 单个区域表+区域点位列表 映射的是集合!!
<collection>......</collection> 
# AI辅助编程模式
-- AI辅助编程模式
-- 你是一个软件开发工程师,现在要根据数据库的sql脚本,查询并显示点位表所有的字段信息,同时显示每个点位的设备数量,sql脚本如下:
create table tb_node
(
    id            int auto_increment comment '主键id'
        primary key,
    node_name     varchar(255)                        not null comment '点位名称',
    address       varchar(255)                        not null comment '详细地址',
    business_type int                                 null comment '商圈类型',
    region_id     int                                 null comment '区域ID',
    partner_id    int                                 null comment '合作商ID',
    create_time   timestamp default CURRENT_TIMESTAMP null comment '创建时间',
    update_time   timestamp default CURRENT_TIMESTAMP null on update CURRENT_TIMESTAMP comment '修改时间',
    create_by     varchar(64)                         null comment '创建人',
    update_by     varchar(64)                         null comment '修改人',
    remark        text                                null comment '备注',
    constraint tb_node_ibfk_1
        foreign key (region_id) references tb_region (id)
            on update cascade on delete cascade,
    constraint tb_node_ibfk_2
        foreign key (partner_id) references tb_partner (id)
            on update cascade on delete cascade
)
    comment '点位表';
    
create table tb_vending_machine
(
    id                   bigint auto_increment comment '主键'
        primary key,
    inner_code           varchar(15) default '000'                 null comment '设备编号',
    channel_max_capacity int                                       null comment '设备容量',
    node_id              int                                       not null comment '点位Id',
    addr                 varchar(100)                              null comment '详细地址',
    last_supply_time     datetime    default '2000-01-01 00:00:00' not null comment '上次补货时间',
    business_type        int                                       not null comment '商圈类型',
    region_id            int                                       not null comment '区域Id',
    partner_id           int                                       not null comment '合作商Id',
    vm_type_id           int         default 0                     not null comment '设备型号',
    vm_status            int         default 0                     not null comment '设备状态,0:未投放;1-运营;3-撤机',
    running_status       varchar(100)                              null comment '运行状态',
    longitudes           double      default 0                     null comment '经度',
    latitude             double      default 0                     null comment '维度',
    client_id            varchar(50)                               null comment '客户端连接Id,做emq认证用',
    policy_id            bigint                                    null comment '策略id',
    create_time          timestamp   default CURRENT_TIMESTAMP     not null comment '创建时间',
    update_time          timestamp   default CURRENT_TIMESTAMP     null comment '修改时间',
    constraint vendingmachine_VmId_uindex
        unique (inner_code),
    constraint tb_vending_machine_ibfk_1
        foreign key (vm_type_id) references tb_vm_type (id),
    constraint tb_vending_machine_ibfk_2
        foreign key (node_id) references tb_node (id),
    constraint tb_vending_machine_ibfk_3
        foreign key (policy_id) references tb_policy (policy_id)
)
    comment '设备表';
-- 查询并显示点位表所有的字段信息,同时显示每个点位的设备数量
SELECT
    n.id,
    n.node_name,
    n.address,
    n.business_type,
    n.region_id,
    n.partner_id,
    n.create_time,
    n.update_time,
    n.create_by,
    n.update_by,
    n.remark,
    COUNT(v.id) AS vm_count
FROM
    tb_node n
LEFT JOIN
    tb_vending_machine v ON n.id = v.node_id
GROUP BY
    n.id;
package com.dkd.manage.domain.vo;

import com.dkd.manage.domain.Node;
import com.dkd.manage.domain.Partner;
import com.dkd.manage.domain.Region;
import lombok.Data;

@Data
public class NodeVo extends Node {
    // 设备数量
    private Integer vmCount;
    // 区域信息
    private Region region;
    // 合作商信息
    private Partner partner;
}
com/dkd/manage/mapper/NodeMapper.java
/**
     *  查询点位管理列表
     * @param node
     * @return
     */
    public List<NodeVo> selectNodeVoList(Node node);
<!--
resultType="com.dkd.manage.domin.vo.NodeVo">...
这个是mybatis以前搞的自动映射封装直接把结果映射给了NodeVo的实体类了
嵌套查询就不能使用resultType自动映射 要改为resultMap做自动映射
<resultMap type="NodeVo" id="NodeVoResult">
多表查询一定要起别名噢 不然会报错没有指明where的子句是来自tb_node表的region_id  还是来自tb_vending_machine表的region_id
-->

<select id="selectNodeVoList" parameterType="Node" resultMap="NodeVoResult">
        SELECT
        n.id,
        n.node_name,
        n.address,
        n.business_type,
        n.region_id,
        n.partner_id,
        n.create_time,
        n.update_time,
        n.create_by,
        n.update_by,
        n.remark,
        COUNT(v.id) AS vm_count
        FROM
        tb_node n
        LEFT JOIN
        tb_vending_machine v ON n.id = v.node_id
        <where>
            <if test="nodeName != null  and nodeName != ''"> and n.node_name like concat('%', #{nodeName}, '%')</if>
            <if test="regionId != null "> and n.region_id = #{regionId}</if>
            <if test="partnerId != null "> and n.partner_id = #{partnerId}</if>
        </where>
        GROUP BY
        n.id
    </select>
多对一标签用association
<resultMap type="NodeVo" id="NodeVoResult">
        <result property="id"    column="id"    />
        <result property="nodeName"    column="node_name"    />
        <result property="address"    column="address"    />
        <result property="businessType"    column="business_type"    />
        <result property="regionId"    column="region_id"    />
        <result property="partnerId"    column="partner_id"    />
        <result property="createTime"    column="create_time"    />
        <result property="updateTime"    column="update_time"    />
        <result property="createBy"    column="create_by"    />
        <result property="updateBy"    column="update_by"    />
        <result property="remark"    column="remark"    />
        <result property="vmCount"    column="vm_count"    />
<!--
sql语法拿到region_id去执行区域当中的selectRegionById 方法执行的时候需要传递区域的id
原理:RegionMapper.java中的 public Region selectRegionById(Long id)把条件拿到手并封装给区域的Region对象 返回的Region对象最终映射给NodeVo.java的 private Region region;
怎么执行的映射呢?需要指定java属性名和执行的类型:
property="region" 
javaType="Region"
至此完成了mybatis的嵌套查询
-->
        <association property="region" javaType="Region" column="region_id" select="com.dkd.manage.mapper.RegionMapper.selectRegionById"/>
        <association property="partner" javaType="Partner" column="partner_id" select="com.dkd.manage.mapper.PartnerMapper.selectPartnerById"/>
    </resultMap>
<resultMap>......</resultMap>   // 完成手动映射 为了实现多表映射情况组合查询

// 解决一对一 或 多对一 映射结果集只有一个对象时完成的ORM的映射封装
<association>......</association>  #点位和点位1对1  点位和合作商1对多

// 解决一对多场景下来映射多个结果的集合 单个区域表+区域点位列表 映射的是集合!!
<collection>......</collection> 


/*
// <association> 标签允许你在查询结果中嵌套另一个对象。
 这样可以方便地在 NodeVo 对象中直接访问 Region 和 Partner 的属性,而不需要额外的查询
 
property:指定 NodeVo 类中的属性名称,该属性将引用关联的对象。
javaType:指定关联对象的 Java 类型。
column:指定用于关联查询的列名,通常是外键。
select:指定一个子查询的方法,用于根据外键查询关联对象
*/

NodeService

/**
 * 查询点位管理列表
 * @param node
 * @return NodeVo集合
 */
public List<NodeVo> selectNodeVoList(Node node);

NodeServiceImpl

/**
 * 查询点位管理列表
 *
 * @param node
 * @return NodeVo集合
 */
@Override
public List<NodeVo> selectNodeVoList(Node node) {
    return nodeMapper.selectNodeVoList(node);
}

NodeController

/**
 * 查询点位管理列表
 */
@PreAuthorize("@ss.hasPermi('manage:node:list')")
@GetMapping("/list")
public TableDataInfo list(Node node)
{
    startPage();
    List<NodeVo> voList = nodeService.selectNodeVoList(node);
    return getDataTable(voList);
}
com/dkd/manage/mapper/NodeMapper.java
/**
     *  查询点位管理列表
     * @param node
     * @return
     */
    public List<NodeVo> selectNodeVoList(Node node);

node/index.vue

<!-- 点位列表 -->
<el-table v-loading="loading" :data="nodeList" @selection-change="handleSelectionChange">
  <el-table-column type="selection" width="55" align="center" />
  <el-table-column label="序号" type="index" width="50" align="center" prop="id" />
  <el-table-column label="点位名称" align="center" prop="nodeName" />
  <el-table-column label="所在区域" align="center" prop="region.regionName" />
  <el-table-column label="商圈类型" align="center" prop="businessType">
    <template #default="scope">
      <dict-tag :options="business_type" :value="scope.row.businessType" />
    </template>
  </el-table-column>
  <el-table-column label="合作商" align="center" prop="partner.partnerName" />
  <el-table-column label="详细地址" align="center" prop="address" show-overflow-tooltip="true"/>
  <el-table-column label="操作" align="center" class-name="small-padding fixed-width">
    <template #default="scope">
      <el-button link type="primary" icon="Edit" @click="handleUpdate(scope.row)" v-hasPermi="['manage:node:edit']">修改</el-button>
      <el-button link type="primary" icon="Delete" @click="handleDelete(scope.row)" v-hasPermi="['manage:node:remove']">删除</el-button>
    </template>
  </el-table-column>
</el-table>

区域管理改造-地区详情新增查看详情

src\views\manage\region\index.vue
<el-table-column label="操作" align="center" class-name="small-padding fixed-width">
        <template #default="scope">
          <el-button link type="primary"  @click="getRegionInfo(scope.row)" v-hasPermi="['manage:region:list']">查看详情</el-button>
          <el-button link type="primary"  @click="handleUpdate(scope.row)" v-hasPermi="['manage:region:edit']">修改</el-button>
          <el-button link type="primary"  @click="handleDelete(scope.row)" v-hasPermi="['manage:region:remove']">删除</el-button>
        </template>
      </el-table-column>

<!-- 
template 插槽:
#default="scope":定义默认插槽,scope 是当前行的数据对象。

@click="getRegionInfo(scope.row)":点击按钮时调用 getRegionInfo 函数,并传入当前行的数据。
-->

...
...

/* 查看详情操作按钮 */
function getRegionInfo(row) {
  // 查询区域信息
  reset();
  const _id = row.id
  getRegion(_id).then(response => {
    form.value = response.data
  });
}

<!-- 
nodeList 变量:

const nodeList = ref([]):定义一个响应式数组 nodeList,用于存储点位列表。
getRegionInfo 函数:

reset():调用 reset 函数,可能用于重置表单或其他状态。
const _id = row.id:获取当前行的 id。
getRegion(_id).then(response => { form.value = response.data }):调用 getRegion 函数查询区域信息,并将返回的数据赋值给 form.value。
loadAllParams.regionId = row.id:设置 loadAllParams 对象的 regionId 属性为当前行的 id。
listNode(loadAllParams).then(response => { nodeList.value = response.rows }):调用 listNode 函数查询点位列表,并将返回的行数据赋值给 nodeList.value。
-->
区域管理里引入点位的api文件
import { listNode } from "@/api/manage/node";
import { loadAllParams } from "@/api/page";
...
/* 查看详情操作按钮 */
const nodeList = ref([]);
function getRegionInfo(row) {
  // 查询区域信息
  reset();
  const _id = row.id
  getRegion(_id).then(response => {
    form.value = response.data
  });
  // 查看点位列表
  loadAllParams.regionId=row_id
  listNode(loadAllParams).then(response => {
    nodeList.value = response.rows;
  });
}
添加区域管理对话框
 <!-- 添加或修改区域管理对话框 -->
    <el-dialog :title="title" v-model="open" width="500px" append-to-body>
      <el-form ref="regionRef" :model="form" :rules="rules" label-width="80px">
        <el-form-item label="区域名称" prop="regionName">
          <el-input v-model="form.regionName" placeholder="请输入区域名称" />
        </el-form-item>
        <el-form-item label="备注说明" prop="remark">
          <el-input v-model="form.remark" type="textarea" placeholder="请输入内容" />
        </el-form-item>
      </el-form>
      <template #footer>
        <div class="dialog-footer">
          <el-button type="primary" @click="submitForm">确 定</el-button>
          <el-button @click="cancel">取 消</el-button>
        </div>
      </template>
    </el-dialog>
   <!-- 查看详情对话框 -->
<el-dialog title="区域详情" v-model="regionInfoOpen" width="500px" append-to-body>
    <el-form-item label="区域名称" prop="regionName">
        <el-input v-model="form.regionName" disabled />
    </el-form-item>
    <label>包含点位:</label>
    <el-table :data="nodeList">
        <el-table-column label="序号" type="index" width="50" align="center" />
        <el-table-column label="点位名称" align="center" prop="nodeName" />
        <el-table-column label="设备数量" align="center" prop="vmCount" />
    </el-table>
</el-dialog>
/* 查看详情操作按钮 */
const nodeList = ref([]);
const regionInfoOpen=ref(false);
function getRegionInfo(row) {
  // 查询区域信息
  reset();
  const _id = row.id
  getRegion(_id).then(response => {
    form.value = response.data
  });
  // 查看点位列表
  loadAllParams.regionId=row_id
  listNode(loadAllParams).then(response => {
    nodeList.value = response.rows;
  });
  regionInfoOpen.value=true;
}
数据完整性
  • 在删除区域或合作商数据时,关联的点位数据该如何处理?

tb_region(区域表) tb_node(点位表) tb_partner(合作商表)
id ←region_id:id ← region_id id
partner_id partner_id:id →→→↑

找到设置外键约束(取消约束)dkd → tb_node → Modify Table(old) 找到Foreign Keys 双击打开后将Update ruleDelete rule修改为:no action

**CASCADE(级联操作):**当父表中的某行记录被删除或更新时,与其关联的所有子表中的匹配行也会自动被删除或更新。这种方式适用于希望保持数据一致性的场景,即父记录不存在时,相关的子记录也应该被移除。

**SET NULL(设为空):**若父表中的记录被删除或更新,子表中对应的外键字段会被设置为NULL。选择此选项的前提是子表的外键列允许为NULL值。这适用于那些子记录不再需要明确关联到任何父记录的情况。

**RESTRICT(限制):**在尝试删除或更新父表中的记录之前,数据库首先检查是否有相关联的子记录存在。如果有,则拒绝执行删除或更新操作,以防止意外丢失数据或破坏数据关系的完整性。这是一种保守策略,确保数据间的引用完整性。

**NO ACTION(无操作):**在标准SQL中,NO ACTION是一个关键字,它要求数据库在父表记录被删除或更新前,检查是否会影响子表中的相关记录。在MySQL中,NO ACTION的行为与RESTRICT相同,即如果子表中有匹配的行,则禁止执行父表的删除或更新操作。这意味着如果存在依赖关系,操作将被阻止,从而保护数据的参照完整性。

修改完毕后,如果你尝试进行删除操作,会发现数据库的完整性约束生效了,它会阻止删除操作并给出错误提示。但是,这个错误提示信息可能对于用户来说不够友好,可能会让用户感到困惑。

SQLIntegrityConstraintViolationException是Java中的一个异常类,这个类通常用于表示SQL数据库操作中的完整性约束违反异常

例如:外键约束、唯一约束等。当数据库操作违反了这些约束时,就会抛出这个异常。

这个错误是由于外键约束导致的。它表明在删除或更新父表的行时,存在外键约束,子表中的相关行会受到影响。

是因为在删除tb_region表中的行时,tb_node表中的region_id外键约束会阻止操作。

如果你在使用Spring框架进行数据库操作,可能会先遇到DataIntegrityViolationException,它是对SQLIntegrityConstraintViolationException的一个更高层次的抽象,旨在提供一种更加面向应用的错误表示。

而SQLIntegrityConstraintViolationException是更底层的异常,直接来源于数据库驱动,包含更多底层数据库相关的细节。

在实际开发中,推荐捕获并处理DataIntegrityViolationException,因为它更符合Spring应用的异常处理模式,同时也可以通过其内部的cause(原因)属性来获取具体的SQLIntegrityConstraintViolationException,进而获取详细的错误信息。

为了提升用户体验,我们可以使用Spring Boot框架的全局异常处理器来捕获这些错误信息,并返回更友好的提示信息给用户。这样,当用户遇到这种情况时,他们将收到一个清晰、易懂的提示,告知他们操作无法完成的原因。

com/dkd/framework/web/exception/GlobalExceptionHandler.java
/**
     * 数据完整性异常
     */
    @ExceptionHandler(DataIntegrityViolationException.class)
    public AjaxResult handleDataIntegrityViolationException(DataIntegrityViolationException e) {
        log.error(e.getMessage(), e);
        if (e.getMessage().contains("foreign")) {
            return AjaxResult.error("无法删除该数据,有其他数据引用");
        }
        return AjaxResult.error("数据完整性异常,请联系管理员");
    }

16

阅读全文

Java面试专项

2024/10/31

Redis篇

我看你做的项目中,都用到了redis,你在最近的项目中哪些场景使用了redis呢?
  • 验证你项目场景的真实性,二是为了深入发问的切入点
  • 缓存 缓存三兄弟(穿透、击穿、雪崩)、双写一致、持久化、数据过期策略、数据淘汰策略
  • 分布式锁 setnx、redisson
  • 消息队列、延迟队列 何种数据类型

==缓存穿透==:查询一个不存在的数据,mysql查询不到数据也不会直接写入缓存,就会导致每次请求都查询数据库(可能原因是数据库被攻击了 发送了假的/大数据量的请求url)

  • 解决方案一缓存空数据,查询返回的数据为空,仍把这个空结果进行缓存 {key:1, value:null}
    优点:简单
    缺点:消耗内存,可能会发生不一致的问题

  • 解决方案二布隆过滤器 (拦截不存在的数据)

    在缓存预热时,要预热布隆过滤器。根据id查询文章时查询布隆过滤器如果不存在直接返回

    bitmap(位图):相当于一个以bit位为单位的数组,数组中每个单元只能存储二进制数0或1

    布隆过滤器作用:可以用于检索一个元素是否在集合中

    • 存储数据:id为1的数据,通过多个hash函数获取hash值,根据hash计算数组对应位置改为1
    • 查询数据:使用相同hash函数获取hash值,判断对应位置是否都为1

    存在误判率:数组越小 误判率越大

    bloomFilter.tryInit(size, 0.05) //误判率5%
    

==缓存击穿==:给某一个key设置了过期时间,当key过期的时候,恰好这个时间点对这个key有大量的并发请求过来,这些并发请求可能一瞬间把DB击穿

  • 解决方案一互斥锁【数据强一致性 性能差 (银行)】

    1.查询缓存,未命中 → 2.获取互斥锁成功 → 3.查询数据库重建缓存数据 → 4.写入缓存 → 5.释放锁

    1.查询缓存,未命中 → 2.获取互斥锁失败 → 3.休眠一会再重试 → 4.写入缓存重试 → 5.缓存命中

  • 解决方案二逻辑过期[不设置过期时间] 【高可用 性能优 不能保证数据绝对一致 (用户体验)】
    也可以搞个永不过期 具体是先在业务里写好某种情况下 某些时候不会过期 比如疫情卖口罩时期

    在数据库一条数据里面添加一个 “expire”: 153213455

    1.查询缓存,发现逻辑时间已过期 → 2.获取互斥锁成功 → 3.开启线程 ↓→ 4.返回过期数据

    ​ 【在新的线程】→ 1.查询数据库重建缓存数据 → 2.写入缓存,重置逻辑过期时间 → 3.释放锁
    1.查询数据缓存,发现逻辑时间已过期 → 2.获取互斥锁失败 → 3.返回过期数据

==缓存雪崩==:在同一个时段内大量的缓存key同时失效或者Redis服务宕机,导致大量请求到达数据库,带来压力

  • 解决方案一:给不同的key的TTL(过期时间)添加随机值
  • 解决方案二:利用Redis集群提高服务的可用性 【哨兵模式、集群模式】
  • 解决方案三:给缓存业务添加降级限流策略【nginx、springcloud、gateway】
  • 解决方案四:给业务添加多级缓存 【Guava(做一级缓存 然后Redis是二级缓存)或Caffeine】
《缓存三兄弟》
穿透无中生有key,布隆过滤null隔离。
缓存击穿过期key,锁与非期解难题。
雪崩大量过期key,过期时间要随机。
面试必考三兄弟,可用限流来保底。
redis作为缓存,mysql的数据如何与redis进行同步呢?(双写一致性)

先插入数据库
更新先更新数据库 更新数据库成功但redis不成功 影响不大 因为后面会有过期删除 最终会一致,更新mysql后缓存可以删除也可以修改
更新完数据库直接删除缓存了 有过期时间兜底 最终会保持一致 我们项目中对数据敏感性一致性不高 我们追求实时性
如果是最终保持一致性的就MQ 我们对实时性不高 对数据敏感性 一致性高
删除问题不大 哪里都行!
读多写少的可以上缓存
mysql保存购物车表 但是再页面操作的时候 只操作redis 用mq给到消费者修改或定时任务 更新数据到mysql,MQ问题:我们对数据实时性要求不高 只需要保存最终一致性就行

你如果只写redis 万一丢了数据怎么办
购物车丢点订单无影响 数据安全性要求不太高 mysql尽量不要搞购物车的表 都在redis的表 丢就丢了呗。
或者异步同步/定时任务
实时性要求 安全性要求 → MySQL
电商一般数据库和mysql都要存 → 读多写少

一定、一定、一定要设置前提,介绍自己的业务背景 (一致性要求高?允许延迟一致?)

① 介绍自己简历上的业务,我们当时是把文章的热点数据存入到了缓存中,虽然是热点数据,但是实时要求性并没有那么高,所以我们采用的是异步的方案同步的数据

② 我们当时是把抢卷的库存存入到了缓存中,这个需要实时的进行数据同步,为了保证数据的强一致性,我们当时采用的是redission提供的读写锁来保证数据的同步

双写一致性:当修改了数据库的数据也要同时更新缓存的数据,缓存和数据库的数据要保持一致

  • 读操作:缓存命中,直接返回;缓存未命中查询数据库,写入缓存,设定超时时间

  • 写操作:延迟双删 [因为无论先删除缓存还是先删除数据库都可能会出数据不一致问题 有脏数据]

  • ==基于redisson互斥锁:==[放入缓存中的数据 读多写少] 【强一致性业务 性能低】

    • 共享锁:读锁readLock,加锁之后,其他线程可以共享读操作,但**不允许写操作**
    • 排他锁:独占锁writeLock也叫,加锁之后,阻塞其他线程读写操作(只允许一个用户或进程独占地对数据进行读取和写入操作)排他锁确保了写操作的原子性和一致性
    • 读数据的时候添加共享锁(读不互斥、写互斥)
    • 写数据的时候添加排他锁(阻塞其他线程的读写 因为读多写少)

    redissionClient.getReadWriteLock(“xxxx”);

  • ==异步通知==: 异步通知保证数据的最终一致性(需要保证MQ的可靠性)需要在Redis中更新数据的同时,通知另一个服务进行某些操作。

    • 使用场景
      • 缓存与数据库双写: 当应用需要同时更新Redis缓存和数据库时,可以先将数据写入Redis,然后通过异步通知机制触发数据库的更新操作。
      • 跨地域数据复制: 在跨地域部署的服务中,为了实现数据的最终一致性,可以在一个地域写入数据后,通过异步通知机制在另一个地域进行数据复制。
      • 系统间数据同步: 在微服务架构中,不同的服务可能有自己的数据存储。当一个服务更新了数据后,可以通过异步通知机制告知其他相关服务进行数据同步。
  • ==基于Canal的异步通知==:监听mysql的binlog

    • 使用MQ中间件,更新数据之后,通知缓存删除
    • 利用canal中间件,不需要修改业务代码,伪装为mysqls的一个从节点,canal通过读取binlog数据更新缓存
Redis作为缓存,数据的持久化是怎么做的?

Redis持久化:RDB全称Redis Database Backup file(Redis数据备份文件),也被叫做Redis数据快照,简单来说就是把内存中的所有数据都记录到磁盘中。当Redis实例故障重启后,从磁盘读取快照文件,数据恢复。

[root@localhost ~]# redis-cli
127.0.0.1:6379> save          #由Redis主进程来执行RDB,会阻塞所有命令
ok

127.0.0.1:6379> bgsave        #开启子进程执行RDB,避免主进程受到影响
Background saving started

Redis内部有触发RDB的机制,可以在redis.conf文件中找到,格式如下:

// 900秒内,如果至少有1个key被修改,则执行bgsave
save 900 1
save 300 10
save 60 10000

==RDB的执行原理?==数据完整性高用RDB

bgsave开始时会fork主进程得到子进程,子进程共享主进程的内存数据,完成fork后读取内存数据并写入RDB文件

在LInux中主进程并无法直接读取物理内存,它只能通过虚拟内存去读。因此有页表(记录虚拟地址与物理地址的映射关系)去执行操作 同时 主进程也会fork(复制页表) 成为一个新的子进程(携带页表) → 写新RDB文件替换旧的RDB文件 → 磁盘

fork采用的是copy-on-write技术:

  • 当主进程执行读操作时,访问共享内存
  • 当主进程执行写操作时,则会拷贝一份数据,执行写操作

优点:二进制数据重启后 Redis无需过多解析 直接恢复

==AOF==对数据不敏感要求不高

AOF全称为Append Only File(追加文件)底层硬盘顺序读写。Redis处理的每个写命令都会记录在AOF,可以看作是命令日志文件
AOF默认是关闭的,需要修改redis.conf配置文件来开启AOF

# 是否开启AOF功能,默认是no
appendonly yes
# AOF文件的名称
appendfilename "appendonly.aof"

AOF的命令记录的频率也可以通过redis.conf文件来配

# 表示每执行一次写命令,立即记录到AOF文件
appendfsync always
# 写命令执行完毕先放入AOF缓冲区,然后表示每隔一秒将缓冲区数据写到AOF文件,是默认方案
appendfsync everysec
# 写命令执行完先放入AOF缓冲区,由操作系统决定何时将缓冲区内容写回磁盘
appendfsync no
配置项 刷盘时机 优点 缺点
Always 同步刷盘 可靠性高,几乎不丢数据 性能影响大
everysec 每秒刷盘 性能适中 最多丢失1秒数据
no 操作系统控制 性能最好 可靠性较差,可能丢失大量数据

因为是记录命令,AOF文件会比RDB文件大的多。而且AOF会记录对同一个key的多次写操作,但只有最后一次写操作才有意义通过执行bgrewriteaof命令,可以让AOF文件执行重读功能,用最少的命令达到相同效

Redis会在出发阈值时自动重写AOF文件。阈值也可以在redis.conf中配置

# AOF文件比上次文件 增多超过多少百分比则触发重写
auto-aof-rewrite-percentage 100
# AOF文件体积最小多大以上才触发重写
auto-aof-rewrite-min-size 64mb

★★★★★★★★ RDB与AOF对比 ★★★★★★★★

RDB和AOF各有优缺点,如果对数据安全性要求较高,在实际开发中往往会结合两者来使用
RDB是二进制文件,在保存时体积较小恢复较快,但也有可能丢失数据,我们通常在项目中使用AOF来恢复数据,虽然慢但丢失数据风险小,在AOF文件中可以设置刷盘策略(每秒批量写入一次命令)

RDB AOF
持久化方式 定时对整个内存做快照哦 记录每一次执行的命令
数据完整性 不完整,两次备份之间会丢失 相对完整,取决于刷盘策略
文件大小 会有压缩,文件体积小 记录命令,文件体积大
宕机恢复速度 很快
数据恢复优先级 低,因为数据完整性不如AOF 高,因为数据完整性更高
系统资源占用 高,大量CPU和内存消耗 低,主要是磁盘IO资源
但AOF重写时会占用大量CPU和内存资源
使用场景 可以容忍数分钟的数据丢失,追求更快的启动速度 对数据安全性要求较高常见
假如Redis的key过期之后,会立即删除吗

Redis对数据设置数据的有效时间,数据过期以后就需要将数据从内存中删除掉。可以按照不同的规则进行删除,这种删除规则就被称之为数据的删除策略(数据过期策略)

==Redis数据删除策略-惰性删除==

惰性删除:设置该key过期时间后,我们不去管它,当需要该key时,我们在检查其是否过期,如果过期,我们就删掉它,反之返回该key

set name zhangsan 10
get name # 发现name过期了,直接删除key

优点:对CPU友好,只会在使用该key时才会进行过期检查,对于很多用不到的key不会浪费时间进行过期检查
缺点:对内存不友好,如果一个key已经过期,但是一直没有使用,那么该key就会一直存在内存中,内存永远不会释放

==Redis数据删除策略-定期删除==

定期删除:每隔一段时间,我们就会对一些key进行检查,删除里面过期的key (从一定数量的数据库中取出一定数量的随机key进行检查,并删除其中的过期key)

定期清理的两种模式:

  • SLOW模式是定时模式,执行频率默认为10hz,每次不超过25ms,以通过修改配置文件redis.conf的hz选项来调整这个次数
  • FAST模式执行频率不固定,但两次间隔不低于2ms,每次耗时不超过1ms

优点:可以通过限制删除操作执行的时长和频率来减少删除操作对CPU的影响。另外定期删除,也能有效释放过期键占用的内存
难点:难以确定删除操作执行的时长和频率

Redis过期删除策略: 惰性删除 + 定期删除 两种策略进行配合使用

假如缓存过多,内存是有限的,内存被占满了怎么办?

==数据淘汰策略==

当Redis中的内存不够用时,此时在向Redis中添加新的key,那么Redis就会按照某一种规则将内存中的数据制除掉,这种数据的制除规则被称之为内存的淘汰策略

Redis支持8种不同策略来选择要删除的key:

  • noeviction:不淘汰任何key,但是内存满时不允许写入新数据,默认就是这种策略

    maxmemory-policy noeviction
  • volatile-ttl:对设置了TTL的key,比较key的剩余TTL值,TTL越小越先被淘汰 (TTL:过期时间的key)

  • allkeys-random:对全体key,随机进行淘汰

  • volatile-random:对设置了TTL的key,随机进行淘汰

  • allkeys-lru:对全体key,基于LRU算法进行淘汰

    LRU(Least Recently Used):最近最少使用,用当前时间减去最后一次访问时间,这个值越大测淘汰优先级越高 [逐出访问时间最少的]
    LFU(Least Frequently Used):最少频率使用。会统计每个key的访问频率,值越小淘汰优先级越高。[逐出频率最低的] 【电商会应用】

  • allkeys-lfu:对全体key,基于LFU算法进行淘汰

  • volatile-lfu:对设置了TTL的key,基于LFU算法进行淘汰

淘汰策略 - 使用建议

1.优先使用 allkeys-lru 策略。充分利用LRU算法的优势,把最近最常访问的数据留在缓存中,如果业务有明显的冷热数据区分,建议使用。
2.如果业务中数据访问频率差别不大,没有明显冷热数据区分,建议使用allkeys-random,随机选择淘汰
3.如果业务中有置顶的需求,可以使用volatile-lru策略,同时置顶数据不设置过期时间,这些数据就一直不会被删除,会淘汰其他设置过期时间的数据
4.如果业务中有短时高频访问的数据,可以使用allkeys-lfuvolatile-lfu策略

数据库有1000万数据,Redis只能缓存20w数据,如何保证Redis中的数据都是热点数据?
  • 使用allkeys-lru(挑选最近最少使用的数据淘汰) 淘汰策略,留下来的都是经常访问的热点数据
Redis的内存用完了会发生什么?
  • 主要看数据淘汰策略是什么?如果是默认的配置(noeviction),会直接报错
redis分布式锁,是如何实现的?

需要结合项目中的业务进行回答,通常情况下,分布式锁的使用场景:
集群情况下的定时任务、抢单、幂等性场景
如果使用互斥锁的话 那么在集群项目有多个服务器就会出现问题

==Redis分布式锁==

Redis实现分布式锁主要利用Redis的setnx命令,setnx是**SET if not exists**(如果不存在,则SET)的简写

  • 获取锁

    添加锁,NX是互斥、EX是设置超时时间
    SET lock value NX EX 10

  • 释放锁

    释放锁,删除即可
    DEL key

Redis实现分布式锁如何合理的控制锁的有效时长?
  • 根据业务执行时间预估
  • 给锁续期

==redisson实现分布式锁 - 执行流程==

加锁 ↓→ 加锁成功 → Watch dog(看门狗)每隔(releaseTime/3的时间做一次续期) → Redis
↓ 操作redis → Redis
↓→→ 释放锁↑ → 通知看门狗无需继续监听 → Redis

加锁 → → → 是否加锁成功?→→→ ↓
↑←←while循环不断尝试获取锁←←←↓

public void redisLock() throws InterruptedException{
    RLock lock = redissonClient.getLock("heimalock");
 // boolean isLock = lock.tryLock(10, 30, TimeUnit.SECONDS);
// 如果不设置中间的过期时间30 才会触发看门狗
// 加锁,设置过期时间等操作都是基于lua脚本完成的[调用redis命令来保证多条命令的原子性]
    boolean isLock = lock.tryLock(10, TimeUnit.SECONDS);
    if(isLock){
        try{
            sout("执行业务");
        } finally{
            lock.unlock();
        }
    }
}
要加依赖
<dependency>
    <groupId>redis.clients</groupId>
    <artifactId>jedis</artifactId>
    <version>3.7.0</version>
</dependency>

==redisson实现分布式锁 - 可重入==

redis实现分布式锁是不可重入的 但是 redisson实现分布式锁是可以重入的
可重入原理:它俩是同一个线程 每个线程都有唯一的线程id 根据线程id唯一标识做判断 判断之前获取锁是不是同一个线程
利用hash结构记录线程id重入次数

KEY VALUE VALUE
field value
heimalock thread1 0
public void add1(){
  RLock lock = redissonClient.getLock("heimalock");
  boolean isLock = lock.tryLock();
// 执行业务
  add2();
// 释放锁
  lock.unlock();
}
public void add2(){
  RLock lock = redissonClient.getLock("heimalock");
  boolean isLock = lock.tryLock();
// 执行业务
// 释放锁 锁次数-1不完全释放
  lock.unlock();
}

==redisson实现分布式锁 - 主从一致性==

Redis Master主节点:主要负责写操作(增删改) 只能写
Redis Slave从节点:主要负责读操作只能读

当RedisMaster主节点突然宕机后 Java应用会去格外获取锁 这时两个线程就同时持有一把锁 容易出现脏数据
怎么解决呢?

  • RedLock(红锁):不能只在一个redis实例上创建锁,应该是在多个redis实例上创建锁(n/2+1),避免在一个redis实例【实现复杂、性能差、运维繁琐】怎么解决?→ CP思想zookeeper
Redis集群有哪些方案?
  • ==主从复制==

    单节点Redis的并发能力是有上限的,要进一步提高Redis的并发能力,就需要搭建主从集群,实现读写分离
    主节点写操作→增删改 从节点读操作→查

    介绍一下redis的主从同步

    单节点Redis的并发能力是有上限的,要进一步提高Redis的并发能力,就要搭建主从集群,实现读写分离。一般都是一主多从,主节点负责写数据,从节点负责读数据

    主从数据同步原理:

    • 主从全量同步

    slave从节点执行replicaof命令建立链接 → 请求master主节点数据同步(replid+offset) → master判断是否是第一次同步(判断replid是否一致) → 是第一次, 返回master的数据版本信息(replid+offset) → slave保存版本信息 → master执行bgsave, 生成RDB → 发送RDB文件给slave → slave清空本地数据加载RDB数据 → 此时master记录RDB期间所有命令repl_balklog → 发送repl_backlog中的命令 → slave执行接收到的命令

    Replication ld: 简称replid,是数据集的标记,id一致则说明是同一数据集。每一个master都有唯一的replid,slave则会继承master节点的replid
    offset: 偏移量,随着记录在repl baklog中的数据增多而逐渐增大。save完成同步时也会记录当前同步的ofset,如果slave的offset小于master的offset,说明slave数据落后于master,需要更新。

    简述全量同步的流程?

    • slave节点请求增量同步

    • master节点判断replid,发现不一致,拒绝增量同步

    • master将完整内存数据生成RDB,发送RDB到slave

    • slave清空本地数据,加载master的RDB

    • master将RDB期间的命令记录在repl_baklog,并持续将log中的命令发送给slave

    • slave执行接收到的命令,保持与master之间的同步

    能说一下,主从同步数据的流程吗?

    全量同步

    1.从节点请求主节点同步数据(replication id、offset)
    2.主节点判断是否为第一次请求,是第一次就与从节点同步版本信息(replication id和offset)
    3.主节点执行bgsave, 生成RDB文件后, 发送给从节点去执行
    4.在RDB生成执行期间, 主节点会从命令的方式记录到缓冲区(日志文件)

    • 主从增量同步
      主从增量同步(slave重启或后期数据变化)

    ① slave重启后 → 携带(replid+offset)找master → master判断请求replid是否一致 → 是第一次, 返回主节点replid和offset → 保存版本信息
    ② slave重启后 → 携带(replid+offset)找master → master判断请求replid是否一致 → 不是第一次, 回复continue向slave → master 去repl_baklog中获取offset后的数据 → 发送offset后的命令给slave → 执行命令

    增量同步

    1.从节点请求主节点同步数据,主节点判断不是第一次请求,不是第一次就获取从节点的offset值
    2.主节点从命令日志中获取offset值后的数据,发送给节点进行数据同步

简述全量同步和增量同步区别?

•全量同步:master将完整内存数据生成RDB,发送RDB到slave。后续命令则记录在repl_baklog,逐个发送给slave。

•增量同步:slave提交自己的offset到master,master获取repl_baklog中从offset之后的命令给slave

什么时候执行全量同步?

•slave节点第一次连接master节点时

•slave节点断开时间太久,repl_baklog中的offset已经被覆盖时

什么时候执行增量同步?

•slave节点断开又恢复,并且在repl_baklog中能找到offset时

  • ==哨兵模式==~~搭过集群,具体多少个节点是组长那边,不太清楚[并发量不是太多 搭哨兵可以节省一点资源]
    Redis提供了哨兵(Sentinel)机制来实现主从集群的自动故障恢复

    • 监控:Sentinel会不断检查您的master和slave是否按预期工作
    • 自动故障恢复:如果master故障,Sentinel会将一个slave提升为一个master。当故障实例恢复后也以新的master为主
    • 通知:Sentinel充当Redis客户端的服务发现来源,当集群发生故障转移时,会将最新信息推送给Redis的客户端

    服务状态监控
    Sentinel基于心跳机制监测服务状态,每隔1秒向集群的每个实例发送ping命令 期待回复pong

    • 主观下线:如果某sentinel节点发现或某实例未在规定时间相应,则认为该实例主观下线
    • 客观下线:若超过指定数量(quorum)的sentinel都认为该实例主观下线,则该实例客观下线,quorum值最好超过Sentinel实例数量的一半

    哨兵选主规则

    • 首先判断主与从节点断开时间长短,如超过指定值就排该从节点
    • 然后判断从节点的slave-priority值,越小优先级越高
    • 如果slave-priority值一样,则判断slave节点的offset值,越大优先级越高 (数据是最全的)
    • 最后是判断slave节点的运行id大小,越小优先级越高

    redis集群(哨兵模式) 脑裂
    因网络问题 主节点和从节点分别在不同的网络分区 这样sentinel只会监控到一部分从节点网络分区 导致RedisClient继续写主节点的数据,这时网络恢复了,哨兵会将老的master强制降级到slave(携带着脑裂前的最新数据),这个时候slave就会把自己数据清空去同步master数据,这时就存在真正的数据丢失了

    怎么解决

    redis中有两个配置参数:【若不能达成就拒绝客户端请求 这样就会避免大量数据丢失】
    min-replicas-to-write 1 表示最少的salve节点为1
    min-replicas-max-lag 5 表示数据复制和同步的延迟不能超过5秒

    怎么保证Redis的高并发高可用呢?

    哨兵模式:实现主从集群的自动故障恢复(监控、自动故障恢复、通知)

    你们使用redis是单点还是集群,哪种集群?

    主从(1主1从) + 哨兵就可以了。单节点不超过10G内存,如果Redis内存不足则可以给不同服务分配独立的Redis主从节点

    redis集群脑裂,该怎么解决?

    集群脑裂是由于主节点和从节点和sentinel处于不同网络分区,使得sentinel没有能够心跳感知到主节点,所以通过选举的方式提升了一个从节点为主,这样就存在了两个master,就像大脑分裂了一样,这样会导致客户端还在老的主节点那里写入数据,新节点无法同步数据,当为网络恢复后,sentinel会将老的主节点降为从节点,此时再从新master同步数据,就会导致数据丢失
    解决:我们可以修改redis的配置,可以设置最少的从节点数量以及缩短主从数据同步的延迟时间,达不到要求就拒绝请求,这样就会避免大量数据丢失。

  • ==分片集群==

    主从和哨兵可以解决高可用、高并发读的问题,但是依然有两个问题没有解决:

    • 海量数据存储问题
    • 高并发写的问题

    使用分片集群可用解决上述问题,分片集群特征:

    • 集群中有多个master,每个master保存不同数据
    • 每个master都可用有多个slave节点
    • master之间通过ping监测彼此健康状态
    • 客户端请求可用访问集群任意节点,最终都会被转发到正确节点

    分片集群结果 - 数据读写

    Redis分片集群引入了哈希槽的概念,Redis集群有16384个哈希值,每个key通过CRC16校验后对16384取模来决定放置哪个槽,集群的每个节点负责一部分hash槽

    存数据流程:
    set name itheima → CRC16计算name的hash值(666666) → 666666%16384=11306 → 根据11306找寻所对应哈希槽的范围 并且插入数据

    redis的分片集群有什么用?

    • 集群中有多个master,每个master保存不同数据。(解决高并发的问题)
    • 每个master都可以有多个slave节点。(解决高并发的问题)
    • master之间通过ping监测彼此健康状态
    • 客户端请求可用访问集群任意节点,最终都会被转发到正确节点

    redis的分片集群中数据是怎么存储和读取的?

    • Redis 分片集群引入了哈希槽的概念,Redis 集群有16384个哈槽
    • 将16384个插槽分配到不同的实例
    • 读写数据:根据key的**有效部分**计算哈希值,对16384取余(有效部分,如果key前面有大括号,大括号的内容就是有效部分,如果没有,则以key本身做为有效部分)余数做为播槽,寻找插所在的实例

Redis是单线程的,但是为什么还那么快

  • Redis是纯内存操作,执行速度非常快
  • 采用单线程,避免不必要的上下文切换可竞争条件,多线程还要考虑线程安全问题
  • 使用I/O多路复用模型,非阻塞IO

解释一下I/O多路复用模型?

Redis是纯内存操作,执行速度非常快,它的性能瓶颈是网络延迟而不是执行速度,I/O多路复用模型主要就是实现了高效的网络请求

  • 是指利用单个线程来同时监听多个Socket ,并在某个Socket可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源,目前的I/O多路复用都是采用的epol模式实现,它会在通知用户进程Socket就绪的同时,把已就绪的Socket写入用户空间,不需要换个历Socket来判断是否就绪,提升了性能

  • Redis网络模型:

    就是使用I/O多路复用结合事件的处理器来应对多个Socket请求

    • 连接应答处理器

    • 命令回复处理器,在Redis6.0之后,为了提升更好的性能,使用了多线程来处理回复事件

    • 命令请求处理器,在Redis6.0之后,将命令的转换使用了多线程,增加命令转换速度,在命令执行的时候,依然是单线程

  • ==用户空间和内核空间==

    • Linux系统中一个进程使用的内存情况划分两部分:内核空间、用户空间
    • 用户空间只能执行受限的命令RIng3,而且不能直接调用系统资源必须通过内核提供的接口来访问
    • 内核空间可以执行特权命令Ring0,调用一切系统资源

    Linux系统为了提高IO效率,会在用户空间和内核空间都加入缓冲区

    • 写数据时,要把用户缓冲数据拷贝到内核缓冲区,然后写入设备
    • 读数据时,要从设备读取数据到内核缓冲区,然后拷贝到用户缓冲区
  • 常见的IO模型

    • ==阻塞IO==

      阻塞IO就是两个阶段都必须阻塞等待:
      阶段一:

      • 用户进程尝试读取数据(网卡数据等)
      • 此时数据尚未到达,内核需要等待数据
      • 此时用户进程也处于阻塞状态

      阶段二:

      • 数据到达并拷贝到内核缓冲区,代表已就绪
      • 将内核数据拷贝到用户缓冲区
      • 拷贝过程中,用户进程依然阻塞等待
      • 拷贝完成,用户进程解除阻塞,处理数据
    • ==非阻塞IO==

      阶段一

      • 用户进程尝试读取数据(比如网卡数据)
      • 此时数据尚未到达,内核需要等待数据
      • 返回异常给用户进程
      • 用户进程拿到error后,再次尝试读取
      • 循环往复,直到数据就绪

      阶段二:

      • 将内核数据拷贝到用户缓冲区
      • 拷贝过程中,用户进程依然阻塞等待
      • 拷贝完成,用户进程解除阻塞,处理数据
    • ==IO多路复用==

      是利用单个线程来同时监听多个Socket,并在某个Socket可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源

      IO多路复用是利用单个线程来同步监听多个Socket,并在某个Socket可读、可写时得到通知,从而避免无效的等待,充分利用CPU资源。不过监听Socket的方式、通知的方式又有多种实现

      • select
      • poll
      • epoll

      差异:
      ★ select和polI只会通知用户进程有Socket就绪,但不确定具体是哪个Socket,需要用户进程逐个历Socket来确认
      ★ epoll则会在通知用户进程Socket就绪的同时,把已就绪的Socket写入用户空间,

      阶段一:

      • 用户进程调用select,指定要监听的Socket集合
      • 内核监听对应的多个socket
      • 任意一个或多个sacket数据就绪则返回readable
      • 此过程中用户进程阻塞

      阶段二:

      • 用户进程找别就格的socket
      • 依次调用recvfrom读取数据
      • 内核将数据拷贝到用户空间
      • 用户进程处理数据
  • Redis网络模型

MySQL篇

在MySQL中,如何定位慢查询?

1.介绍一下当时产生问题的场景(我们当时的一个接口测试的时候非常的慢,压测的结果大概5秒钟)
2.我们系统中当时采用了运维工具(Skywalking),可以监测出哪个接口,最终因为是sql的问题
3.在mysql中开启了慢日志查询,我们设置的值就是2秒,一旦sql执行超过2秒就会记录到日志中(调试阶段)

产生原因:

  • 聚合查询
  • 多表查询
  • 表数据量过大查询
  • 深度分页查询

方案一:==开源工具==[调试阶段才会开启 生产阶段不会开启]

  • 调试工具Arthas
  • 运维工具:Prometheus、SKywalking(接口访问时间)

方案二:==MySQL自带慢日志==

慢查询日志记录了所有执行时间超过指定参数(long_query_time, 单位:秒,默认10秒)的所有SQL语句的日志,如果要开启慢查询日志,需要在MySQL的配置文件(/etc/my.cnf)中配置信息:

# 开启MySQL慢日志查询开关
slow_query_log = 1
# 设置慢日志的时间为2秒,SQL语句执行时间超过2秒,就会被视为慢查询,记录慢查询日志
long_query_time = 2

那这个SQL语句执行很慢,如何分析呢?

可以采用MySQL自带的分析工具 explain

  • 通过keykey_len检查是否命中了索引(索引本身存在是否有失效的情况)
  • 通过type字段查看sql是否有进一步的优化空间,是否存在全索引扫描全盘扫描
  • 通过extra建议判断,是否出现了回表的情况,如果出现了,可以尝试添加索引或修改返回字段来修复

产生原因:

  • 聚合查询 → 新增临时表的数据
  • 多表查询 → 优化SQL语句结构
  • 表数据量过大查询 → 添加索引
  • 深度分页查询
一个SQL语句执行很慢,如何分析?

可以采用EXPLAIN或者DESC命令获取MySQL如何执行SELECT语句的信息

# 直接在select语句之前加上关键字 explain/desc
EXPLAIN SELECT 字段列表 FROM 表名 WHERE 条件;

mysql > explain select * from t_user where id = ‘1’

id select_type table partitions type possible_keys key key_len ref rows filtered Extra
1 SIMPLE t_user NULL const PRIMARY PRIMARY 98 const 1 100.00 NULL
  • possible_key:当前sql可能会使用到的索引
  • key:当前sql实际命中的索引 通过它俩查看是否可能会命中索引
  • key_len索引占用的大小 通过它俩查看是否可能会命中索引
  • Extra:额外的优化建议 看是否走过覆盖索引或回表查询
Extra 含义
Using where; Using Index 查找使用了索引,需要的数据都在索引列中能找到,不需要回表查询数据
Using index condition 查找使用了索引,但是需要回表查询数据
  • type:这条sql的连接的类型,性能由好到差为
    • NULL
    • system:查询系统中的表
    • const:根据主键查询
    • eq_ref:主键索引查询或唯一索引查询
    • ref:索引查询
    • range:范围查询
    • index:索引树扫描
    • all:全盘扫描

了解过索引吗?(什么是索引)

索引(index)是帮助MySQL高效获取数据的数据结构(有序),在数据之外,数据库系统还维护着满足特定查找算法的数据结构**(B+树)**,这些数据结构以某种方式引用(指向)数据,这样就可以在这些数据结构上实现高级查找算法,这种数据结构就是索引

  • 索引(index)是帮助MySQL高效获取数据的数据结构(有序)
  • 提高数据检索的效率,降低数据库的IO成本(不需要全表扫描)
  • 通过索引列对数据进行排序,降低数据排序的成本,降低了CPU的消耗

索引的底层数据结构了解过吗?

MySQL的InnoDB引擎采用的B+树的数据结构来存储索引

  • 阶数更多,路径更短
  • 磁盘读写代价B+树更低,非叶子节点只存储指针,叶子阶段存储数据
  • B+树便于扫库和区间查询,叶子节点是一个双向链表

**MySQL默认使用的索引底层数据结构是B+树**。再聊B+树之前,先来聊聊二叉树和B树

==B Tree(矮胖树)==,B树是一种多叉路衡查找树,相对于二叉树,B树每个节点可以有多个分支,即多叉。以一颗最大度数(max-degree)为5(5阶)的b-tree为例,那这个B树每个节点最多存储4个key

==B+Tree== 是再BTree基础上的一种优化,使其更适合实现外存储索引结构,InnoDB存储引擎就是B+Tree实现其索引结构

B树与B+树对比

  • 磁盘读写代价B+树更低
  • 查询效率B+树更加稳定
  • B+树便于扫库和区间查询

B树要找12 首先找38 左面小 再去缩小范围16和29 找到12 → 但是我们只想要12的数据 B树会额外的把38,16,29的数据全查一遍最后才到12的数据

B+树是在叶子节点才会存储数据,在非叶子节点全是指针,这样就没有其他乱七八糟的数据影响 。且查找路径是差不多的,效率较稳定

便于扫库:比如我们要查询6-34区间的数据,先去根节点扫描一次38 → 16-29 → 由于叶子节点之间有双向指针,就可以一次性把所有数据都给拿到[无需再去根节点找一次]

什么是聚簇索引?什么是非聚簇索引(二级索引)?什么是回表?

  • 聚簇索引(聚集索引)数据索引放到一块,B+树的叶子节点保存了整行数据,有且只有一个
  • 非聚簇索引(二级索引)数据索引分开存储,B+树的叶子节点保存对应的主键,可以有多个
  • 回表查询:通过二级索引找到对应的主键值,到聚集索引中查找正行数据,这个过程就是回表
分类 含义 特点
==聚集索引(Clustered Index)== 将数据存储与索引放到了一块,索引结构的叶子节点保存了行数据 必须有, 而且只有一个
==二级索引(Secondary Index)== 将数据与索引分开存储,索引结构的叶子节点关联的是对应的主键 可以存在多个

聚集索引选取规则:

  • 如果存在主键,主键索引就是聚集索引
  • 如果不存在主键,将使用第一个唯一 (UNIQUE) 索引作为聚集索引
  • 如果表没有主键,或没有合适的唯一索引,则InnoDB会自动生成一个rowid作为隐藏的聚集索引

==回表查询==

select * from user where name = 'Arm';

知道什么叫覆盖索引吗?

覆盖索引是指查询使用了索引,返回的列,必须在索引中全部能够找到

  • 使用id查询,直接走聚集索引查询,一次索引描述,直接返回数据,性能高
  • 如果返回的列中没有创建索引,有可能会触发回表查询,尽量避免使用 **select *** [除非用的聚簇索引(主键)]

==覆盖索引==是指查询使用了索引,并且需要返回的列,在该索引中已经全部能够找到

id name gender createdate
2 Arm 1 2021-01-01
3 Lily 0 2021-05-04
5 Rose 0 2021-04-21
6 Zoo 1 2021-07-31
8 Doc 1 2021-02-26
11 Lee 1 2021-09-11
  • id为主键,默认是主键索引
  • name字段为普通索引
select * from tb_user where id = 1;                     【覆盖索引】
select id, name from tb_user where name = 'Arm'         【覆盖索引】
select id, name, gender from tb_user where name = 'Arm' 【非覆盖索引】(需要回表查询)

MySQL超大分页怎么处理?

问题:再数据量比较大时,limit分页查询,需要对数据进行排序,效率低
解决方案:可以用覆盖索引 + 子查询处理
[我们先分页查询获取表中的id 并且对表的id进行排序 就能筛选出分页后的id集合(因为id是覆盖索引效率高) 最后再根据id集合到原来的表中做关联查询就可以得到提升了]

在数据量比较大时,如果用limit分页查询,在查询时,越往后,分页查询效率越低

mysql > select * from tb_sku limit 0,10;
10 rows in set (0.00 sec)

mysql > select * from tb_sku limit 9000000,10;
10 rows in set (11.05 sec)

因为,当在进行分页查询时,如果执行 limit 9000000,10,此时需要MySQL排序前9000010记录,仅仅返回9000000 - 9000010 的记录,其他记录丢失,查询排序的代价非常大。

==MySQL超大分页查询优化思路==:一般分页查询时,通过创建覆盖索引能够比较好地提高性能,可以通过覆盖索引子查询形式进行优化

# 超大分页处理:先通过覆盖索引找到符合条件的id,再通过这个id的覆盖索引查询到所有的列
select * 
from tb_sku t,
(select id from tb_sku order by id limit 9000000,10) a
where t.id = a.id

# 10 rows in set (7.15 sec)

索引创建原则有哪些?

数据量较大,且查询比较频繁的表
常作为查询条件、排序、分组的字段
③ 字段内容区分度高
④ 内容较长,使用前缀索引
尽量联合索引
要控制索引的数量
⑦ 如果索引列不能存储NULL值,请在创建表时使用NOT NULL约束它

  • 先陈述自己再实际工作中是怎么用的
  • 主键索引
  • 唯一索引
  • 根据业务创建的索引(复合索引)
创建索引的方式1
① SQL的方式
ALTER TABLE user_innodb ADD INDEX idx_name(name)

② 在建表的时候 去指定索引
...
PRIMARY KEY('id'),
KEY 'idx_name' ('name') USING HASH

③ 通过界面化工具去指定索引
字段旁边有个`索引` 可以去添加

=============================================
单个字段的索引 → 单列索引
多个字段的索引 → 联合索引
索引的类型

索引可以增加查询速度 同时也增加了更新/修改速度因为更新的第一步就是查询

普通索引 经过特殊设计的数据结构
唯一索引 唯一约束
[索引必须是唯一的 比如name就不行 因为名字可以很多建立普通索引]
主键索引 在主键索引上添加了非空约束
全文索引 一般使用搜索引擎,因为对中文的搜索不太友好美国英文开发的
[特殊的sql:select * from 表名 where match(字段名) against(‘马士兵教育’ IN NATURAL LANGUAGE MODE);]

AVL树 右右型左旋 左子树与右子树的深度差绝对值不超过1
树的节点里应该放:键值+Value值+左右子树的地址left+right
Innodb一次会加载16k(16384字节=Redis的槽位) 内存到内存
不选红黑树是因为它是二叉的,我们需要多叉树
要用==B+树==全盘扫描能力更强 叶子节点是双向链表
因为稳定性比较好 B树非所见所得 B+树是稳定几层的查找数据因为数据都在最后一层叶子节点上
Innodb的索引方法是BTREE 不能改成HASH

**数据结构可视化网**:Data Structure Visualization

  • 针对数据量较大,且查询比较频繁的表建立索引。单表超过10万数据(增加用户体验)
  • 针对常作为查询条件(where)、排序(order by)、分组(group by) 操作的字段建立索引
  • 尽量选择区分度高的列作为索引,尽量建立唯一索引,区分度越高,使用索引的效率越高 (比如address都在北京市)
  • 如果是字符串类型的字段,字段的长度越长(描述信息…),可以针对于字段的特点,建立前缀索引
  • 尽量使用联合索引,减少单列索引,查询时,联合索引很多时候可以覆盖索引(避免回表),节省存储空间,提高查询效率
  • 要控制索引的数量,索引并不是多多益善,索引越多,维护索引结构的代价也就越大,会影响增删改的效率
  • 如果索引列不能存储NULL值,请在创建表时使用NOT NULL约束它。当优化器知道每列是否包含NULL值时,它可以更好地确定哪个索引最有效地用于查询。

什么情况下索引会失效?

  • 违反最左前缀法则
  • 范围查询右边的列,不能使用索引
  • 不要在索引列上进行运算操作,索引将失效
  • 字符串不加单引号,造成索引失效。(类型转换)
  • 以**%开头的Like模糊查询**,索引失效
    [不影响正常查询业务 但未运用超大分页查询优化 会导致索引失效]

怎么哪块读判断索引是否失效了呢

# 执行计划explain

【2024最新版MySQL索引讲解!一个视频带你彻底搞懂MySQL索引!!【马士兵】】https://www.bilibili.com/video/BV17z421i7Kb?vd_source=5966d6c3cf3709c10b3c53b278b0f4d3

什么情况下索引会失效?
如果索引了多列,要遵守最左前缀法则。指的是查询从索引的最左前列开始,并且不跳过索引中的列。匹配最左前缀法则,走索引:

谈谈你对sql的优化经验?

  • 表的设计优化,数据类型的选择
  • 索引优化,索引创建原则
  • sql语句优化,避免索引失效,避免使用select
  • 主从复制、读写分离,不让数据的写入,影响读操作
  • 分库分表
  • 表的设计优化(参考阿里开发手册《嵩山版》)

    • 比如设置合适的数值(tinyint、int、bigint) ,要根据实际情况选择
    • 比如设置合适的字符串类型(char和varchar) char定长效率高,varchar可变长度,效率低

    候选人: 这个我们主要参考的阿里出的那个开发手册《嵩山版》,就比如,在定义字段的时候需要结合字段的内容来选择合适的类型,如果是数值的话,像tinyint、int、bigint这些类型,要根据实际情况选择。如果是字符串类型,也是结合存储的内容来选择char和varchar或者text类型

  • 索引优化(参考优化创建原则和索引失效)

  • SQL语句优化

    • SELECT语句务必指明字段名称 (避免直使用select *)回表

    • SQL语句要避免造成索引失效的写法

    • 尽量使用union all代替union,union(不会重复)会多一次过滤, 效率低

      select * from t_user where id > 2
      union all | union
      select * from t_user where id < 5
      
    • 避免在where子句中对字段进行表达式操作

    • join优化 能用inner join 就不用left join, right 如必须使用 一定要以小表为驱动;内链接会对两个表进行优化,优先把小表放到外边,把大表放到里边。left join 或 right join,不会重新调整顺序

      for(int i = 0; i < 3; i++){ //只链接查询3次
       for(int j = 0; j < 1000; j++){
      
       }  
      }
      
  • 主从复制、读写分离(在生产环境下一般会搭建主库和从库 分开读操作和写操作)

    如果数据库的使用场景读的操作比较多的时候,为了避免写的操作所造成的性能影响 可以采用读写分离的架构。读写分离解决的是,数据库的写入,影响了查询的效率。[Master(写) 和 Slave(读)]

  • 分库分表(后面有介绍)

事务的特性是什么?可以详细的说一下吗?【ACID】

事务是一组操作的集合,它是一个不可分割的工作单位,事务会把所有的操作作为一个整体一起向系统提交或撤销操作请求,即这些操作要么同时成功,要么同时失败。

候选人:嗯,这个比较清楚,ACID,分别指的是:原子性、一致性、隔离性、持久性;
我举个例子:A向B转账500,转账成功,A扣除500元,B增加500元,原子操作体现在要么都成功,要么都失败
在转账的过程中,数据要一致,A扣除了500,B必须增加500
在转账的过程中,隔离性体现在A像B转账,不能受其他事务干扰
在转账的过程中,持久性体现在事务提交后,要把数据持久化(可以说是落盘操作)

  • **原子性(**Atomicity):事务是不可分割的最小操作单元,要么全部成功,要么全部失败。
  • 一致性(Consistency):事务完成时,必须使所有的数据都保持一致状态。
  • 隔离性(lsolation):数据库系统提供的隔离机制,保证事务在不受外部并发操作影响的独立环境运行
  • 持久性(Durabiity):事务一旦提交或回滚,它对数据库中的数据的改变就是永久的。

并发事务带来哪些问题?怎么解决这些问题?MySQL默认隔离级别是?

  • ==并发事务问题==:脏读、不可重复读、幻读
  • ==隔离级别==:读未提交、读已提交、可重复读、串行化
问题 描述
脏读 一个事务读到另外一个事务还没有提交的数据
不可重复读 一个事务先后读取同一条事务,但两次读取的数据不同,称之为不可重复读
幻读 一个事务按照条件查询数据时,没有对应的数据行,这同时另一个事务B(insert且commit)了事务,此时事务A在插入数据时候,又发现这行数据已经存在了,好像出现了”幻影“
怎么解决并发事务的问题呢??

对事务进行隔离 (× 是代表可以解决此问题)

隔离级别 脏读 不可重复读 幻读
Read uncommitted 未提交读
Read committed 读已提交 ×
==Repeatable Read(默认) 可重复读== × ×
Serializable 串行化 × × ×

注意:**事务隔离级别越高,数据越安全,但是性能越低**

undo log 和 redo log的区别?

redo log:记录的是数据页的物理变化,服务宕机可用来同步数据
undo log:记录的是逻辑日志,当事务回滚时,通过逆操作恢复原来的数据
redo log 保证了事务的持久性,undolog保证了事务的原子性和一致性

  • 缓冲池(buffer pool):主内存中的一个区域,里面可以缓存磁盘上经常操作的真实数据,在执行增删改査操作时,先操作缓冲池中的数据(若缓冲池没有数据,则从磁盘加载并缓存),以一定频率刷新到磁盘,从而减少磁盘IO,加快处理速度
  • 数据页(page):是InnoD8 存储引擎磁盘管理的最小单元,每个页的大小默认为 16KB。页中存储的是行数据

==redo log==

重做日志,记录的是事务提交时数据页的物理修改,是用来实现事务的持久性
该日志文件由两部分组冲:重做日志缓冲(redo log buffer) 以及 **重做日志文件(redo log file)**,前者是在内存中,后者是在磁盘中。当事务提交之后会把所有修改信息都保存到该日志文件中,用于在刷新脏页到磁盘,发生错误时,进行数据恢复使用。

==undo log==

回滚日志,用于记录数据被修改前的信息,作用包含两个:提供回滚MVCC(多版本并发控制)。undolog 和 redolog记录物理日志不一样,它是逻辑日志

  • **可以认为当delete一条记录时,undo log中会记录一条对应的insert记录**,反之亦然
  • 当update一条记录时,它记录一条对应相反的update记录。当执行rolback时,就可以从undolog中的逻辑记录读取到相应的内容并进行回滚。

undo log可以实现事务的一致性和原子性

事务中的隔离性是如何保证的呢?

排他锁 (如果一个事务获取到了一个数据行的排他锁,其他事务就不能再获取该行的其他锁)
mvcc: 多版本并发控制 让MySQL中的多版本并发控制。指维护一个数据的多个版本,使得读写操作没有冲突
隐藏字段:
① trx _id(事务id),记录每一次操作的事务id,是自增的
② roll _pointer(回滚指针),指向上一个版本的事务版本记录地址

undo log:
① 回滚日志,存储老版本数据
② 版本链:多个事务并行操作某一行记录,记录不同事务修改数据的版本,通过rollpointer指针形成一个链表

readView:解决的是一个事务查询选择版本的问题
根据readView的匹配规则和当前的一些事务id判断该访问那个版本的数据》不同的隔离级别快照读是不一样的,最终的访问的结果不一样RC:每一次执行快照读时生成ReadView
RR:仅在事务中第一次执行快照读时生成ReadView,后续复用

面试官: 事务中的隔离性是如何保证的呢?(你解释一下MVCC)
候选人: 事务的隔离性是由锁和mvcc实现的。
其中mnvcc的意思是多版本并发控制。指维护一个数据的多个版本,使得读写操作没有冲突,它的底层实现主要是分为了三个部分,第一个是隐藏字段,第二个是undolog日志,第三个是readView读视图
隐藏字段是指:在mysq!中给每个表都设置了隐藏字段,有一个是x_id(事务id),记录每一次操作的事务id,是自增的;另一个字段是roll-pointer(回滚指针),指向上一个版本的事务版本记录地址
undolog主要的作用是记录回滚日志,存储老版本数据,在内部会形成一个版本链,在多个事务并行探作某一行记录,记录不同事务修改数据的版本,通过roll_pointer指针形成一个链表
readview解决的是一个事务查询选择版本的问题,在内部定义了一些匹配规则和当前的一些事务id判断该访问那个版本的数据,不同的隔离级别快照读是不一样的,最终的访问的结果不一样。如果是rc隔离级别,每一次执行快照读时生成ReadView,如果是r隔离级别仅在事务中第一次执行快照读时生成ReadView,后续复用

解释一下MVCC?

全程 Multi-Version Concurrency Control,多版本并发控制。指维护一个数据的多个版本,使得读写操作没有冲突

问题的来源:(橙色的)查询的是哪个事务版本的记录?
事务2 事务3 事务4 事务5
开始事务 开始事务 开始事务 开始事务
修改id为30记录, age改为3 查询id为30的记录
提交事务
修改id为30记录, name改为A3
查询id为30的记录
提交文件 修改id为30的记录, age改为10
查询id为30的记录 查询id为30的记录
提交事务
MVCC-实现原理
  • 记录中的隐藏字段
id age name DB_TRX_ID DB_ROLL_PTR DB_ROW_ID
  • DB_TRX_ID:最近修改事务ID,记录插入这条记录或最后一次修改该记录的事务ID
  • DB_ROLL_PTR:回滚指针,指向这条记录的上一个版本,用于配合undo log, 指向上一个版本
  • DB_ROW_ID:隐藏主键,如果表结构没有指定主键,将会生成该隐藏字段
undo log
  • 回滚日志,在insert、update、delete的时候产生的便于数据回滚的日志。
  • 当insert的时候,产生的undolog日志只在回滚时需要,在事务提交后,可被立即删除。
  • 而update、delete的时候,产生的undo log日志不仅在回滚时需要,mvcc版本访问也需要,不会立即被删除。
undo log版本链

不同事务或相同事务对同一条记录进行修改,会导致该记录的undolog生成一条记录版本链表,链表的头部是最新的旧记录,链表尾部是最早的旧记录

  • readview

ReadView(读视图) 是 快照读 SQL执行时MVCC提取数据的依据,记录并维护系统当前活跃的事务(未提交的)id

ReadView中包含了四个核心字段

字段 含义
m_ids 当前活跃的事务ID集合
min_trx_id 最小活跃事务ID
max_trx_id 预分配事务ID, 当前最大事务ID+1 (事务ID是自增的)
creator_trx_id ReadView创建者的事务ID
  • 当前读

读取的是记录的最新版本,读取时还要保证其他并发事务不能修改当前记录,会对读取的记录进行加锁。对于我们日常的操作,如:select .. lock in share mode(共享锁),select .. for update、update、insert、delete(排他锁)都是一种当前读。

  • 快照读

简单的select(不加锁)就是快照读,快照读,读取的是记录数据的可见版本,有可能是历史数据,不加锁,是非阻塞读。
Read Committed:每次select,都生成一个快照读。
Repeatable Read:开启事务后第一个select语句才是快照读的地方。

MySQL主从同步原理?

MySQL主从复制的核心就是二进制日志binlog[DDL(数据定义语言)语句DML(数据操纵语言)语句]
主库在事务提交时,会把数据变更记录在二进制日志文件 Binlog 中。
从库读取主库的二进制日志文件 Binlog,写入到从库的中继日志 Relay Log。
从库重做中继日志中的事件,将改变反映它自己的数据。

MySQL主从复制的核心就是二进制日志

二进制文件(BINLOG) 记录了所有的DDL(数据定义语言)语句DML(数据操纵语言)语句,但不包括数据查询(SELECT、SHOW)语句

复制分成三步:
  • Master主库在事务提交时,会把数据变更记录在二进制日志文件Binlog中
  • 从库读取主库的二进制日志文件Binlog,写入到从库的中继日志Relay Log
  • slave重做中继日志中的事件,将改变反应他自己的数据

你们项目用过分库分表吗?

  • 业务介绍
    1,根据自己简历上的项目,想一个数据量较大业务(请求数多或业务累积大)
    2,达到了什么样的量级(单表1000万或超过20G)

  • 具体拆分策略
    1,水平分库,将一个库的数据拆分到多个库中,解决海量数据存储和高并发的问题
    2,水平分表,解决单表存储和性能的问题
    3,垂直分库,根据业务进行拆分,高并发下提高磁盘I0和网络连接数
    4,垂直分表,冷热数据分离,多表互不影响

分担了访问压力、解决存储压力

分库分表的时机:

前提:项目业务数据逐渐增多,业务发展比较迅速【单表数据量达1000W或20G以后】
② 优化解决不了性能问题(主从读写分离、查询索引)
IO瓶颈(磁盘IO、网络IO)、CPU瓶颈(聚合查询、连接数太多)

拆分策略【垂直 ≈ 微服务、水平 ≈ 分配数值】

  • ==垂直拆分==
    • 垂直分库:以表为依据,根据业务将不同表拆分到不同库中
      (特点:按业务对数据分级管理、维护、监控、扩展;在高并发下,提高磁盘IO和数据量连接数)
      • tb_user → 用户微服务
      • tb_order → 订单微服务
      • tb_sku → 商品微服务
    • 垂直分表:以字段为依据,根据字段属性将不同字段拆分到不同表中
      (把不常用的字段单独放在一张表;把text, blob等大字段[描述]拆分出来放在附表中)
      (特点:冷热数据分离、减少IO过渡争抢,两表互不影响)
  • ==水平拆分==
    • 水平分库:将一个库的数据拆分到多个库中
      (解决了单库大数量,高并发的性能瓶颈问题;提高了系统的稳定性和可用性)
      路由规则
      • 根据id节点取模
      • 按id也就是范围路由,节点1(1-100万),节点2(100万-200万)
    • 水平分表:将一个库的数据拆分到多个表中(可以在同一个库内)
      (优化单一表数据量过大而产生的性能问题;避免IO争抢并减少锁表的几率)
分库后的问题:↓↓
  • 分布式事务一致性问题
  • 跨节点关联查询
  • 跨节点分页、排序函数
  • 主键避重
使用分库分表中间件
  • sharding-sphere
  • mycat

Spring框架中的单例bean是线程安全的吗?

不是线程安全的,是这样的

当多用户同时请求一个服务时,容器会给每一个请求分配一个线程,这是多个线程会并发执行该请求对应的业务逻辑(成员方法),如果该处理逻辑中有对该单列状态的修改(体现为该单例的成员属性),则必须考虑线程同步问题。

Spring框架并没有对单例bean进行任何多线程的封装处理。关于单例bean的线程安全和并发问题需要开发者自行去搞定。
比如:我们通常在项目中使用的Springbean都是不可可变的状态(比如Service类和DAO类),所以在某种程度上说Spring的单例bean是线程安全的。

如果你的bean有多种状态的话(比如 View Model对象),就需要自行保证线程安全。最浅显的解决办法就是将多态bean的作用由“singleton”变更为“prototype”。

Spring框架中的bean是单例的

@Service
@Scope("singleton")
public class UserServiceImpl implements UserService{

}
  • singleton:bean在每个Spring IOC容器中只有一个实例
  • prototype:一个bean的定义可以有多个实例

Spring bean并没有可变的状态(比如Service类和DAO类), 所以在某种程度上说Spring的单例bean是线程安全的。但要尽可能的少创造可变参数比如count

@Controller
@RequeestMapping("/user")
public class UserController{
    private int count; //成员方法需要考虑线程安全问题

    @Autowired
    private UserService userService;

    @GetMapping("/getById/{id}")
    public User getById(@PathVariable("id") Integer id){
        count++;
        sout(count);
        return userService.getById(id);
    }
}

什么是AOP,你们项目中有没有用到AOP?

AOP称为面向切面编程,用于将那些与业务无关,但却对多个对象产生影响的公共行为和逻辑,抽取并封装为一个可重用的模块,这个模块被命名为“切面”(Aspect),减少系统中的重复代码,降低了模块间的耦合度,同时提高了系统的可维护性。

常见AOP使用场景:
  • 拒绝策略记录操作日志

    nginx → 新增用户 → @Around(“pointcut()”) 环绕通知

  • 缓存处理

  • Spring中内置的事务处理

Spring中的事务是如何实现的
Spring支持 编程式事务管理声明式事务 管理两种方式

  • 编程式事务控制:需使用TransactionTemplate来进行实现,对业务代码有侵入性,项目中很少使用
  • 声明式事务管理:声明式事务管理建立在AOP之上的。其本质是通过AOP功能,对方法前后进行拦截,将事务处理的功能编织到拦截的方法中,也就是在目标方法开始之前加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。

Spring中事务失效的场景有哪些?

异常捕获处理,自己处理了异常,没有抛出,解决:手动抛出
抛出检查异常,配置rollbackFor属性为Exception
非public方法导致的事务失效,改为public

考察对spring框架的深入理解、复杂业务的编码经验

  • ==异常捕获处理==

    原因:事务通知只有捉到了目标抛出的异常,才能进行后续的回滚处理,如果目标自己处理掉异常,事务通知无法知悉

    解决在catch块添加throw new RuntimeException(“转账失败”) 抛出

  • ==抛出检查异常==

    原因:Spring默认只会回滚非检查异常

    @Transactional
    public void update(...) throw FileNotFoundException{
        ...
        new FileInputStream("dddd")
        ...
    }
    

    解决:配置rollbackFor属性

    @Transcational(rollbackFor=Exception.class)
    
  • ==非public方法==

    @Transcational(rollbackFor=Exception.class)
    void update(...) throw FileNotFoundException{
        ...
        new FileInputStream("dddd")
        ...
    }
    

    原因:Spring为方法创建代理、添加事务通知、前提条件都是该方法是public
    解决:把方法改为public

Spring的bean的生命周期?

Spring容器是如何管理和创建bean实例
方便调试和解决问题

① 通过BeanDefinition获取bean的定义信息
② 调用构造函数实例化bean
③ bean的依赖注入
④ 处理Aware接囗(BeanNameAware、BeanFactoryAware、ApplicationContextAware)
⑤ Bean的后置处理器BeanPostProcessor-前置
⑥ 初始化方法(InitializingBean、init-method)
⑦ Bean的后置处理器BeanPostProcessor-后置
⑧ 销毁bean

BeanDefinition

Spring容器在进行实例化时,会将xml配置的< bean >的信息封装成一个BeanDefinition对象,Spring根据BeanDefinition来创建Bean对象,里面有很多的属性来描述Bean

<bean id="userDao" class="com.itheima.dao.impl.UserDaolmpl" lazy-init="true"/><bean id="userService" class="com.itheima.service.UserServicelmpl" scope="singleton">
  <property name="userDao" ref="userDao"></property>
</bean>

Spring中的循环引用?

循环依赖:循环依赖其实就是循环引用, 也就是两个或两个以上的bean互相持有对方,最终形成闭环。比如A依赖于B,B依赖于A
★ 循环依赖在spring中是允许存在,spring框架**依据三级缓存已经解决了大部分的循环依赖**
一级缓存:单例池,缓存已经经历了完整的生命周期,已经初始化完成的bean对象
二级缓存:缓存早期的bean对象(生命周期还没走完)
三级缓存:缓存的是ObjectFactory,表示对象工厂,用来创建某个对象的

构造方法出现了循环依赖怎么解决?

A依赖于B,B依赖于A,注入的方式是构造函数
原因:由于bean的生命周期中构造函数是第一个执行的,spring框架并不能解决构造函数的的依赖注入
解决方案:使用**@Lazy进行懒加载**,什么时候需要对象再进行bean对象的创建

public A(@Lazy B b){
sout(“A的构造方法执行了”);
this.b=b;
}

@Component @Component
public class A{ → ← public class B{
@Autowired ↑ ↑ @Autowired
private B b; →↑ ↑← private A a;
} }

什么是Spring的循环依赖??

==一级缓存==作用:限制bean在beanFactory中只存一份,即实现singleton scope,解决不了循环依赖

如果想打破循环依赖,就需要一个中间人的参与,这个中间人就是==二级缓存==如果一个对象是代理对象(被增强了)就不行

针对如果是代理对象的话如何解决呢? → ==三级缓存==

那如果构造方法出现了循环依赖怎么解决?

@Component @Component
public class A{ → ← public class B{
private B b; ↑ ↑ private A a;
public A(B c){ →↑ ↑← public B(A c){
sout(“A的构造方法执行了”) sout(“B的构造方法执行了”)
this.b=b; this.b=b;
} }
} }

报错信息:Is there an unresolvable circular reference?
解决:@Lazy 延迟加载→什么时候需要对象的时候什么时候实例化对象

public A(@Lazy B b){
 sout("A的构造方法执行了");
 this.b=b;
}

Spring解决循环依赖是通过三级缓存

// 单实例对象注册器
public class DefaultSingletonBeanRegistry extends SimpleAliasRegistry implements SingletonBeanRegistry {
private static final int SUPPRESSED EXCEPTIONS LIMIT= 100;  
private final Map<String, Object>singletonObjects = new ConcurrentHashMap(256); 一级缓存
private final Map<String, ObjectFactory<?>> singletonFactories = new HashMap(16); 三级缓存
private final Map<String, Object> earlySingletonObjects = new ConcurrentHashMap(16); 二级缓存 
}
缓存名称 源码名称 作用
一级缓存 singletonObject 单例池,缓存已经经历了完整的生命周期,已经初始化完成的bean对象
二级缓存 earlySingletonObjects 缓存早期的bean对象(生命周期还没走完)
三级缓存 singletonFactories 缓存的是ObjectFactory,表示对象工厂,用来创建某个对象的

SpringMVC的执行流程知道吗?

  • 视图阶段(老旧JSP等)
  • 前后端分离阶段(接口开发,异步)

==视图阶段(jsp)==

  • 用户发送出请求到前端控制器DispatcherServlet
  • DispatcherServlet收到请求调用HandlerMapping(处理器映射器)
  • HandlerMapping找到具体的处理器,生成处理器对象及处理器拦截器(如果有)
  • DispatcherServlet调用HandlerAdapter(处理器适配器)HandlerAdapter经过适配调用具体的处理器(Handler/Controller)Controller执行完成返回
  • ModelAndView对象HandlerAdapter将Controller执行结果ModelAndView返回给DispatcherServlet
  • DispatcherServlet将ModelAndView传给ViewReslover(视图解析器)
  • ViewReslover解析后返回具体View(视图)
  • DispatcherServlet根据View进行渲染视图(即将模型数据填充至视图中)
  • DispatcherServlet响应用户

==前后端分离阶段(接口开发,异步请求)==

  • 用户发送出请求到前端控制器DispatcherServlet
  • DispatcherServlet收到请求调用HandlerMapping(处理器映射器)
  • HandlerMapping找到具体的处理器,生成处理器对象及处理器拦截器(如果有),再一起返回给DispatcherServlet
  • DispatcherServlet调用HandlerAdapter(处理器适配器)
  • HandlerAdapter经过适配调用具体的处理器(Handler/Controller)

SpringBoot自动配置原理?

SpringBoot中最高频的一道面试题,也是框架最核心的思想
==@SpringBootConfiguration==:该注解与 @Configuration 注解作用相同,用来声明当前也是一个配置类
==@ComponentScan==:组件扫描,默认扫描当前引导类所在包及其子包
==@EnableAutoConfiguration==:SpringBoot实现自动化配置的核心注解

1,在Spring Boot项目中的引导类上有一个注解@SpringBootApplication,这个注解是对三个注解进行了封装,分别是:

  • @SpringBootConfiquration
  • @EnableAutoConfiquration
  • @ComponentScan

2,其中@EnableAutoConfiguration是实现自动化配置的核心注解。该注解通过@Import注解导入对应的配置选择器。内部就是读取了该项目和该项目引用的jar包的classpath路径下META-INF/spring.factories文件中的所配置的类的全类名。在这些配置类中所定义的Bean会根据条件注解所指定的条件来决定是否需要将其导入到Spring容器中

3,条件判断会有像@ConditionalOnClass这样的注解,判断是否有对应的class文件,如果有则加载该类,把这个配置类的所有的Bean放入spring容器中使用。

package com.itheima;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

// SpringBoot的启动类
// 注意: 我们写的代码要在启动类的包或者子包中
// @SpringBootApplication注解中包含了 @ComponentScan,没有指定扫描哪个包,默认扫描当前类所在的包和子包
@SpringBootApplication
public class Day15TliasManagement01IocDiApplication {
    // 启动项目, 内嵌的Tomcat会启动, 把项目部署到这个内嵌Tomcat中
    public static void main(String[] args) {
        SpringApplication.run(Day15TliasManagement01IocDiApplication.class, args);
    }
}

按住ctrl+左键点击@SpringBootApplication会弹到SpringBootApplication.class界面

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@SpringBootConfiguration
@EnableAutoConfiguration
@ComponentScan(
    excludeFilters = {@Filter(
    type = FilterType.CUSTOM,
    classes = {TypeExcludeFilter.class}
), @Filter(
    type = FilterType.CUSTOM,
    classes = {AutoConfigurationExcludeFilter.class}
)}
)

按住ctrl+左键点击@EnableAutoConfiguration会弹到EnableAutoConfiguration.class界面

# @Import({AutoConfigurationImportSelector.class})
# AutoConfigurationImportSelector是自动配置的选择器 
# 会加载META-INF中的spring.factories文件的自动配置类...AutoConfiguration...
//
// Source code recreated from a .class file by IntelliJ IDEA
// (powered by FernFlower decompiler)
//

package org.springframework.boot.autoconfigure;

import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import org.springframework.context.annotation.Import;

@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
@Inherited
@AutoConfigurationPackage
@Import({AutoConfigurationImportSelector.class})
public @interface EnableAutoConfiguration {
    String ENABLED_OVERRIDE_PROPERTY = "spring.boot.enableautoconfiguration";

    Class<?>[] exclude() default {};

    String[] excludeName() default {};
}

Spring框架常见的注解有哪些?

注解 说明
@Component、@Controller、@Service、@Repository 使用在类上用于实例化Bean
@Autowired 使用在字段上用于根据类型依赖注入
@Qualifier 结合@Autowired一起使用用于根据名称进行依赖注入
@Scope 标注Bean的作用范围默认单例的
@Configuration 指定当前类是一个Spring配置类,当创建容器时会从该类上加载注解
@ComponentScan 用于指定Spring在初始化容器时要扫描的包
@Bean 使用在方法上,标注将该方法的返回值存储到Spring容器中
@Import 使用@Import导入的类会被Spring加载到IOC容器中
@Aspect、@Before、@After、@Around、@Pointcut 用于切面编程(AOP)

SpringMVC框架常见的注解有哪些?

注解 说明
@RequestMapping 用于映射请求路径,可以定义在类上和方法上。用于类上,则标识类中的所有的方法都是以该地址作为父路径
@RequestBody 注解实现接收http请求的json数据,将json转换为java对象
@RequestParam 指定请求参数的名称
@PathViriable 从请求路径中获取请求参数(/user/{id}),传递给方法的形式参数
@ResponseBody 注解实现将Controller方法返回对象转换成json对象响应给客户端
@RequestHeader 获取指定的请求头数据
@RestController @Controller + @

SpringBoot常见的注解有哪些?

注解 说明
@SpringBootConfiguration 组合了 -@Configuration注解,实现配置文件的功能
@EnableAutoConfiguration 打开自动配置的功能,也可以关闭某个自动配置的选项
@ComponentScan Spring组件扫描

MyBatis执行流程?

  • 读取MyBatis配置文件:mybatis-config.xml加载运行环境和映射文件
  • 构造会话工厂SqlSessionFactory
  • 会话工厂创建SqlSession对象(包含了执行SQL语句的所有方法)
  • 操作数据库的接口,Executor执行器,同时负责查询缓存的维护
  • Executor接口的执行方法中有一个MappedStatement类型的参数,封装了映射信息
  • 输入参数映射
  • 输出结果映射
  • 理解了各个组件的关系
  • Sql的执行过程(参数映射、sql解析、执行和结果处理)

MyBatis是否支持延迟加载?

  • 延迟加载的意思是:就是在需要用到数据时才进行加载,不需要用到数据时就不加载数据。
  • Mybatis支持一对一关联对象和一对多关联集合对象的延迟加载
  • Mybatis配置文件中,可以配置是否启用延迟加载 lazyLoadingEnabled=true/false,默认是关闭的

延迟加载的底层原理知道吗?

  • 使用CGLIB创建目标对象的代理对象
  • 当调用目标方法时,进入拦截器invoke方法,发现目标方法是nul值,执行sql查询
  • 获取数据以后,调用set方法设置属性值,再继续查询目标方法,就有值了

查询用户的时候,把用户所属的订单数据也查询出来,这个是==立即加载==
查询**用户的(sql)时候,暂时不查询订单数据,当需要订单的时候,再查询订单(sql)**,这个就是==延迟加载==

延迟加载的实现步骤:

  1. 配置开启延迟加载: 在MyBatis的配置文件中(通常是mybatis-config.xml),需要设置两个属性:
    • lazyLoadingEnabled=true:开启延迟加载。
    • aggressiveLazyLoading=false:关闭积极的延迟加载,即访问对象的时候不会立即加载其所有属性。
  2. 映射文件配置: 在对应的Mapper映射文件中,对于需要延迟加载的关联查询,使用select标签定义延迟加载的SQL语句,并通过fetchType="lazy"属性明确指定使用延迟加载。
  3. 创建代理对象: 当执行查询操作时,MyBatis不会立即执行关联查询的SQL,而是返回一个代理对象。这个代理对象是使用CGLIB库创建的,它继承自目标对象。
  4. 拦截器方法调用: 当我们首次访问这个代理对象的某个方法(比如访问订单详情)时,实际上会调用CGLIB生成的代理对象的拦截器方法(intercept方法)。在拦截器方法中,会判断当前要访问的属性是否已经被加载:
    • 如果属性已经被加载,则直接返回属性值。
    • 如果属性未被加载,则会执行之前定义好的延迟加载SQL语句,从数据库中查询数据。
  5. 设置属性值: 查询得到数据后,MyBatis会将这些数据设置到目标对象的相应属性上,这样下次访问该属性时,就不需要再次查询数据库了。

底层原理:

  • CGLIB代理:MyBatis使用CGLIB库创建目标对象的代理,当调用目标方法时,实际上会进入拦截器(Interceptor)的intercept方法。
  • 拦截器逻辑:在拦截器中,会判断当前调用的方法是否需要触发延迟加载。如果需要,则执行延迟加载的SQL查询。
  • 结果处理:查询结果会被处理并设置到目标对象的属性上,这样目标对象的相关属性就持有了数据,后续访问将直接返回这些数据,而无需再次查询。MyBatis在执行完延迟加载的SQL查询后,会获取查询结果,并将这些结果映射到目标对象的相应属性中

示例说明:

假设有一个用户User和订单Order的关系,在查询用户时,通常不会立即加载其订单信息,而是当需要时再加载。以下是简化的代码示例:

<!-- UserMapper.xml -->
<resultMap id="userMap" type="User">
  <id property="id" column="id"/>
  <result property="name" column="name"/>
  <!-- 延迟加载订单信息 -->
  <collection property="orders" column="id" ofType="Order" select="selectOrdersForUser" fetchType="lazy"/>
</resultMap>

<select id="selectUser" resultMap="userMap">
  SELECT * FROM user WHERE id = #{id}
</select>

<select id="selectOrdersForUser" resultType="Order">
  SELECT * FROM order WHERE user_id = #{id}
</select>

在上述配置中,当调用selectUser查询用户信息时,不会立即查询订单信息。只有当程序中访问User对象的orders属性时,才会执行selectOrdersForUser查询,这就是延迟加载的具体实现。

MyBatis的一级、二级缓存用过吗?

  • 一级缓存:基于 PerpetualCache 的 HashMap 本地缓存,其存储作用域为 Session,当Session进行flush或close之后,该Session中的所有Cache就将清空,默认打开一级缓存
  • 二级缓存是基于namespace和mapper的作用域起作用的,不是依赖于SQLsession,默认也是采用PerpetualCache,HashMap 存储。需要单独开启,一个是核心配置,一个是mapper映射文件

MyBatis的二级缓存什么时候会清理缓存中的数据?

  • 当某一个作用域(一级缓存 Session/二级缓存Namespaces)的进行了新增、修改、删除操作后,默认该作用域下所有 select 中的缓存将被 clear。
  • 本地缓存,基于PerpetualCache,本质是一个HashMap
  • 一级缓存:作用域是session级别
    • 基于 PerpetualCache 的 HashMap 本地缓存,其存储作用域为 Session,当Session进行flush或close之后,该Session中的所有Cache就将清空,默认打开一级缓存
  • 二级缓存:作用域是namespace和mapper的作用域,不依赖于session
    • 二级缓存是基于namespace和mapper的作用域起作用的,不是依赖于SQLsession,默认也是采用 PerpetualCache
      HashMap 存储

注意事项
  • 对于缓存数据更新机制,当某一个作用域(一级缓存 Session/二级缓存Namespaces)的进行了新增、修改、删除操作后,默认该作用域下所有 select 中的缓存将被 clear
  • 二级缓存需要缓存的数据实现Serializable接口
  • 只有会话提交或者关闭以后,一级缓存中的数据才会转移到二级缓存中

SpringCloud篇

SpringCloud 5大组件有哪些?

回答原则:简单的问题不能答错

通常情况 SpringCloudAlibba
Eureka:注册中心 Nacos:注册中心/配置中心
Ribbon:负载均衡 Ribbon:负载均衡
Feign:远程调用 Feign:远程调用
Hystrix:服务熔断 sentinel:服务保护
Zuul/Gateway:网关 Gateway:服务网关

服务注册和发现是什么意思? SpringCloud 如何实现服务注册发现?

  • 我们当时项目采用的eureka作为注册中心,这个也是SpringCloud体系的一个核心组件
  • 服务注册:服务提供者需要把自己的信息注册到eureka来保存这些信息,比如**服务名称、ip、端口**等等
  • 服务发现:消费者向eureka拉取服务列表信息,如果服务提供者有集群,则消费者利用负载均衡算法,选择一个发起调用
  • 服务监控:服务提供者会每隔30秒向eureka发送心跳,报告健康状态,如果eureka服务90秒没有收到心跳,从eureka中剔除
  • 微服务中必须要使用的组件,考虑我们使用微服务的程度
  • 注册中心的核心作用是:服务注册和发现
  • 常见的注册中心:eureka、nocas、zookeeper

请你说一下nacos与eureka的区别?

  • Nacos与Eureka的共同点 (注册中心)
    • 都支持服务注册和服务拉取
    • 都支持服务者心跳方式做健康检测
  • Nacos与Eureka的区别 (注册中心)
    • Nacos支持服务端主动检测提供者状态:临时实例采用心跳模式,非临时实例采用主动检测模式
    • 临时实例心跳不正常会被剔除,非临时实例则不会被提出
    • Nacos支持服务列表变更的消息推送模式,服务列表更新更及时
    • Nacos集群默认采用AP方式高可用模式,当集群中存在非临时实例时,采用CP模式;Eureka采用AP方式
  • Nacos还支持了配置中心,Eureka只有注册中心,也是选择选用nacos的一个重要原因

把RestTemplate替换成OpenFeign后它们的底层还是一样的吗?OpenFeign是远程调用

OpenFeign的底层原理也是根据服务名称,首先去远程注册中心拉取服务列表,底层也会在本地缓存一份,也会根据负载均衡选出一个实例,又运用了jdk的动态代理生成代理类,也会涉及到反射机制,最终拼出完整的url,发起http远程调用

@FeignClient(name = "service-provider")
public interface ServiceProviderClient {
    // 定义接口方法,映射到服务提供者的具体API
    @GetMapping("/api/resource")
    String getResource();
}

你们项目负载均衡如何实现的?图1.1

微服务的负载均衡主要使用了一个组件Ribbon,比如,我们再使用feign远程调用的过程中,底层的负载均衡就是使用了Ribbon

  • 负载均衡Ribbon,发起远程调用feign就会使用Ribbon
  • Ribbon负载均衡策略有哪些
  • 如果想自定义负载均衡策略如何实现?
Ribbon已经进入维护模式,Netflix不再积极开发新功能。而Spring Cloud LoadBalancer作为替代,不仅提供了Ribbon的核心功能,还引入了一些新特性和改进

Ribbon负载均衡策略有哪些?

  • RoundRobinRule:简单轮询服务列表来选择服务器
  • WeightedResponseTimeRule:按照权重来选择服务器,响应时间越长,权重越小
  • RandomRule随机选择一个可用的服务器
  • BestAvaliableRule:忽略那些短路的服务器,并选择并发数较低的服务器
  • RetryRule:重试机制的选择逻辑
  • AvaliabilityFilteringRule:可用性敏感策略,先过滤非健康的,再选择连接数较小的实例
  • ZoneAvoidanceRule:以区域可用的服务器为基础进行服务器的选择。使用Zone对服务器进行分类,这个Zone可用理解为一个机房、一个机架等。而后再对Zone内的多个服务做轮询

如果想自定义负载均衡策略如何实现?图1.2

  • 创建类实现IRule接口,可以指定负载均衡策略(全局)
  • 在客户端的配置文件中,可以配置某一个服务调用的负载均衡(局部)

首先,你需要创建一个类来实现 IRule 接口,这样就能自定义负载均衡的策略。

java复制编辑import com.netflix.loadbalancer.IRule;
import com.netflix.loadbalancer.Server;
import com.netflix.loadbalancer.ZoneAwareLoadBalancer;
import com.netflix.loadbalancer.RandomRule;

import java.util.List;

public class CustomLoadBalancerRule implements IRule {

 private IRule delegate = new RandomRule();  // 默认策略

 @Override
 public Server choose(Object key) {
     // 在这里实现自己的负载均衡算法
     // 比如,你可以使用 RoundRobin、Random 或者基于健康检查的策略
     return delegate.choose(key);
 }

 @Override
 public void setLoadBalancer(ZoneAwareLoadBalancer<?> lb) {
     delegate.setLoadBalancer(lb);
 }

 @Override
 public ZoneAwareLoadBalancer<?> getLoadBalancer() {
     return delegate.getLoadBalancer();
 }
}

然后,你需要在 Spring 配置类或者启动类上注入该自定义的负载均衡策略。

java复制编辑import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RibbonConfig {

 @Bean
 public IRule customLoadBalancerRule() {
     return new CustomLoadBalancerRule();
 }
}

SpringCloud中什么是服务雪崩,怎么解决这个问题?

  • 什么是==服务雪崩==?一个服务失败,导致整条链路的服务都失败的情形
  • 服务雪崩:一个服务失败,导致整条链路的服务都失败的情形

  • ==熔断降级== && ==服务熔断==(解决)Hystix 服务熔断降级

服务降级部分服务不可用:服务自我保护的一种方式,或者保护下游服务的一种方式,用于确保服务不会受请求突增影响变得不可用,确保服务不会崩溃,一般在实际开发中与Feign接口整合,编写降级逻辑

服务熔断整个服务不可用:默认关闭,需要手动打开,如果监测到10秒内请求的失败率超过50%,就触发熔断机制。之后每隔5秒重新尝试请求微服务,如果微服务不能响应,继续走熔断机制。如果微服务可达,则关闭熔断机制,恢复正常请求

  • 限流(预防)微服务限流(漏桶算法、令牌桶算法)

你们项目中有没有做到限流?怎么做的?&& 限流常见的算法有哪些??

① 先来介绍一下业务,什么情况下去做限流,需要说明QPS具体是多少

  • 我们有一个活动,到了假期就会抢购优惠券,QPS最高可以达到2000,平时10-50之间,为了应对突发流量,需要做限流
  • 常规限流,为了防止恶意攻击,保护系统正常运行,我们当时系统能够承受最大的QPS是多少(压测结果)

nginx限流

  • 控制速率(突发流量),使用的漏桶算法来实现过滤,让请求以固定的速率处理请求,可以应对突发流量
  • 控制并发数,限制单个ip的连接数和并发链接的总数

网关限流

  • 在SpringCloudGateway中支持局部过滤器RequestRateLimiter来做限流,使用的是令牌桶算法
  • 可以根据ip或路径进行限流,可以设置每秒填充平均速率,和令牌桶总容量

解释原理:

QPS(Queries Per Second,每秒查询率)是衡量一个系统处理请求能力的指标,它表示服务器在一秒钟内能够处理的查询数量。这个指标常用于数据库和web服务器等应用,以评估系统在高并发情况下的性能。
以下是对您提到的两句话的分析:

  1. 活动期间的高并发处理:
  • 背景知识: 在电子商务等应用中,促销活动往往会引起用户的大量点击和购买行为,导致短时间内流量剧增。
  • 限流原理: 为了应对这种突发的高流量,系统需要实施限流措施。限流是为了保护系统资源不被过度消耗,确保系统的稳定性和可靠性。常见的限流算法有固定窗口、滑动窗口、令牌桶和漏桶等。

固定窗口: 假设每 1 分钟允许 100 次请求,10:00:00 到 10:01:00 期间的 100 次请求被允许,超出 100 次则会被限流,10:01:00 到 10:02:00 则重新开始计算。

滑动窗口: 每 60 秒内最多允许 100 次请求,滑动窗口的时间长度为 60 秒,窗口内的请求数会随着时间滑动更新,防止请求在时间边界上积压

令牌桶:假设每秒生成 10 个令牌,令牌桶的容量为 100 个令牌。如果 1 秒内有 15 个请求到达,则前 10 个请求能获得令牌并继续执行,剩余的 5 个请求需要等到下一个时间窗口令牌生成后再执行。

漏桶:假设每秒钟流出 10 个请求,漏桶的容量为 100 个请求。如果 1 秒钟内接收了 30 个请求,系统只会处理 10 个请求,剩余的 20 个请求被丢弃,直到下一个时间点。

  • 实施方式: 在您提到的情况下,可以采用以下策略:
    • 预判性扩容: 根据历史数据和活动规模预测流量,提前进行服务器资源的扩容。
    • 动态限流: 在活动期间,根据实时监控的QPS数据动态调整限流阈值,保证系统平稳运行。
    • 排队处理: 对于超出系统处理能力的请求,可以采用队列进行缓冲,分批次处理。
  1. 常规限流与系统最大承受QPS:
  • 背景知识: 常规限流是为了在日常运行中防止恶意攻击(如DDoS攻击)和保护系统资源不被滥用。
  • 压测结果: 系统的最大承受QPS是通过压力测试得出的。压力测试(也称为负载测试)是通过模拟高并发访问来测试系统的极限性能,以确定系统在保证稳定运行的前提下能够承受的最大QPS。
  • 原理分析:
    • 保护系统: 通过设定一个QPS上限,可以防止系统过载,保障系统的正常运行。
    • 资源分配: 了解系统的最大承受QPS有助于合理分配资源,如数据库连接、内存和CPU等。
    • 用户体验: 适当的限流可以保证用户的体验,避免因系统过载导致的响应缓慢或服务不可用。
      在实施限流策略时,还需要考虑以下因素:
  • 业务优先级: 对于不同的业务请求,可能需要有不同的限流策略,优先保证核心功能的可用性。
  • 用户体验: 限流策略应尽量减少对用户体验的影响,例如通过友好的错误提示或降级方案。
  • 数据监控: 实时监控系统的QPS和其他关键指标,以便快速响应并调整限流策略。
    综上所述,限流是确保系统在高并发情况下稳定运行的重要措施,而了解系统的最大承受QPS是制定合理限流策略的基础。

为什么要限流?

  • 并发业务量大(突发流量)
  • 防止用户恶意刷接口
限流的实现方式:
  • ==Tomcat==单体项目可以,分布式不行:可以设置最大连接数 <Connector port="8080"...maxThreads="150"...>

  • ==Nginx==:漏桶算法固定速率露出(平滑)

    控制速率(突发流量)

  • ==网关==:令牌桶算法

  • 自定义拦截器

你们的微服务是怎么监控的?

我们项目中采用的skywalking进行监控的

  • skywalking主要可以监控接口、服务、物理实例的一些状态。特别是在压测的时候可以看到众多服务中哪些服务和接口比较慢,我们可以针对性的分析和优化。
  • 我们还在skywalking设置了告警规则,特别是在项目上线以后,如果报错,我们分别设置了可以给相关负责人发短信和发邮件,第一时间知道项目的bug情况,第一时间修复
skywalking

一个分布式系统的应用程序性能监控工具(Application Performance Management), 提供了完善的链路追踪能力,apache的顶级项目(前华为产品经理吴晟主导开源)

解释一下CAP和BASE分布式系统理论

  • CAP 定理(一致性、可用性、分区容错性)
  1. 分布式系统节点通过网络连接,一定会出现分区问题(P)
  2. 当分区出现时,系统的一致性(C)和可用性(A)就无法同时满足
  • BASE理论
  1. 基本可用
  2. 软状态
  3. 最终一致
  • 解决分布式事务的思想和模型
  1. 最终一致思想:各分支事务分别执行并提交,如果有不一致的情况,再想办法恢复数据(AP)
  2. 强一致思想:各分支事务执行完业务不要提交,等待彼此结果。而后统一提交或回滚(CP)
  • 分布式事务方案的指导
  • 分布式系统设计方向
  • 根据业务指导使用正确的技术选择
==CAP定理==分布式系统无法同时满足三个指标
  • ==Consistency==(一致性):用户访问分布式系统中的任意节点,得到的数据必须一致。主从一致
  • ==Availability==(可用性):用户访问集群中的任意健康节点,必须能得到响应,而不是超时或拒绝
  • ==Partition tolerance==(分区容错性):当出现网络分区现象后,系统能够继续运行
    • Partition(分区):因为网络故障或其他原因导致分布式系统中的部分节点与其他节点失去链接,形成独立分区
    • Tolerance(容错):在集群出现分区时,整个系统也要持续对外提供服务

结论:

  • 分布式系统节点之间肯定是需要网络链接的,分区 (P) 必然存在
  • 如果保证访问的高可用性(A)可以持续对外提供服务,但不能保证数据的强一致性 AP
  • 如果保证访问的数据强一致性(C)就要放弃高可用性 CP
==BASE理论==

BASE理论是对CAP的一种解决思路,包含三个思想:

  • ==Basically Avaliable==(基本可用):分布式系统在出现故时,允许损失部分可用性,即保证核心可用
  • ==Soft State==(软状态):在一定时间内,允许出现中间状态,比如临时的不一致状态
  • ==Eventually Consistent==(最终一致性):虽然无法保证强一致性,但是在软状态结束后,最终达到数据一致性

CAP如何选择?
  • CP[支付宝]或者AP[超级跑跑系统维护]
  • 在什么场合,可用性高于一致性?
    • 网页必须要保障可用性(一定能看到最重要 是不是最新的不重要)和分区容错
    • 支付的时候一定要保障一致性(我可以保证不可用 但我不允许余额不一致)和分区容错
  • 合适的才是最好的

你们采用哪种分布式事务解决方案?

● 简历上写的微服务,只要是发生了多个服务之间的写操作,都需要进行分布式事务控制

● 描述项目中采用的哪种方案(seataMQ)
⚪ seata的XA模式,CP,需要互相等待各个分支事务提交,可以保证强一致性,性能差 (银行业务 )
⚪ seata的AT模式,AP,底层使用undolog 实现,性能好 (互联网业务 )
⚪ seata的TCC模式,AP,性能较好,不过需要人工编码实现 (银行业务 )
⚪ MQ模式实现分布式事务,在A服务写数据的时候,需要在同一个事务内发送消息到另外一个事务异步,性能最好 (互联网业务 )

  • Seata框架(XA、AT、TCC)
  • MQ

Seata架构

  • TC(Transaction Coordinator) - 事务协调者维护全局和分支事务的状态,协调全局事务提交或回滚
  • TM(Transaction Manager) - 事务管理器:定义全局事务的范围、开启全局事务、提交或回滚全局事务
  • RM(Resource Manager) - 资源管理器:管理分支事务处理的资源,与TC交谈以注册分支事务和报告分支事务的状态,并驱动分支事务提交或回滚

分布式服务的接口幂等性如何设计?

  • 幕等: 多次调用方法或者接口不务状态,可以保证重复调用的结果和单次调用的结果一致
  • 如果是**新增数据**,可以使用数据库的唯一索引
  • 如果是**新增或修改数据**
    • 分布式锁,性能较低
    • 使用token+redis来实现,性能较好
      ● 第一次请求,生成一个唯一token存入redis,返回给前端
      ● 第二次请求,业务处理,携带之前的token,到redis进行验证,如果存在,可以执行业务,删除token; 如果不存在,则直接返回,不处理业务

幂等多次调用方法或者接口不会改变业务状态,可以保证重复调用的结果和单次调用的结果一致

需要幂等场景
  • 用户重复点击(网络波动)
  • MQ消息重复
  • 应用使用失败或超时
请求方式 说明
GET 查询操作,天然幂等
POST 新增操作,请求一次与请求多次造成的结果不同,不是幂等的
PUT 更新操作,如果是以绝对值更新,则是幂等的。如果是通过增量的方式更新,则不是幂等的
DELETE 删除操作,根据唯一值删除,是幂等的
update a set money = 500 where id = 1 【幂等】
update a set money = money + 500 where id = 1; 【非幂等】
  • 数据库唯一索引【新增】

  • ==token + redis== 【新增+修改】**AND** ==分布式锁== 【新增+修改】

    创建商品、提交订单、转账、支付等操作

你们项目中使用了什么分布式任务调度

xxl-job 是一个分布式任务调度平台,它致力于解决分布式场景下的任务调度问题,主要由调度中心和执行器两部分组成。调度中心负责统一管理任务调度,而执行器则是负责接收调度并执行任务逻辑的客户端。

  • xxl-job路由策略有哪些?

    xxl-job提供了很多的路由策略,我们平时用的较多的就是:轮询、故障转移、分片广播

  • xxl-job任务执行失败怎么解决?

    • 路由策略选择故障转移,使用健康的实例来执行任务
    • 设置重试次数
    • 查看日志+邮件警告来通知相关负责人解决
  • 如果有大数据量的任务同时都现需要执行,怎么解决?

    • 让多个实例一块去执行(部署集群),路由策略分片广播
    • 在任务执行的代码中可以获取分片总数和当前分片,按照取模的方式分摊到各个实例执行
xxl-job解决的问题
  • 解决集群任务的重复执行问题 xxl-job路由策略有哪些?
  • cron表达式定义灵活在页面上 xxl-job任务执行失败怎么解决?
  • 定时任务失败了,重试和统计 如果有大数据量的任务同时都需要执行,怎么解决?
  • 任务量大,分片执行

场景 1: 定时处理过期订单

假设用户下单后如果订单超过了某个时间没有支付,平台需要自动取消该订单并释放库存。这个任务需要在每天的某个固定时间(比如凌晨 2 点)运行。

解决的问题:
  1. 定时任务调度:XXL-Job 可以轻松管理该任务的执行时间和周期,确保每天准时执行,不需要开发者手动触发。
  2. 任务失败重试:如果该任务因为某些原因执行失败,XXL-Job 可以自动进行重试,并设置重试次数,确保任务最终被执行。
  3. 分布式执行:假设电商平台是一个分布式系统,订单数据存储在多个数据库中,XXL-Job 可以通过分布式执行确保每个数据库的订单都被正确处理

假设每晚 2 点有一个任务需要取消未支付的订单

public class OrderJob {
    @JobHandler("orderCancelJobHandler")
    public void cancelUnpaidOrders() {
        // 查询所有未支付的订单
        List<Order> unpaidOrders = orderService.findUnpaidOrders();
        for (Order order : unpaidOrders) {
            if (order.isExpired()) {
                orderService.cancelOrder(order);
                inventoryService.releaseStock(order.getProductId(), order.getQuantity());
                // 发送订单取消通知给用户
                notificationService.sendOrderCancelledNotification(order.getUserId());
            }
        }
    }
}

场景 2: 定时更新商品库存

假设电商平台上销售的是一些有时效性的商品,商家需要定期更新商品的库存状态(例如,库存数量达到一定阈值时,自动下架商品,或者增加库存数量)。这个任务同样需要定时执行。

解决的问题:
  1. 任务分片:在商品很多的情况下,XXL-Job 可以通过任务分片的方式并行处理不同商品的库存更新,提升任务的执行效率。
  2. 任务优先级:根据不同商品的重要程度,XXL-Job 可以设置任务的优先级,确保关键商品的库存更新优先执行。
public class InventoryJob {
    @JobHandler("inventoryUpdateJobHandler")
    public void updateProductInventory() {
        // 获取需要更新库存的商品
        List<Product> productsToUpdate = productService.findProductsForInventoryUpdate();
        for (Product product : productsToUpdate) {
            inventoryService.updateInventory(product);
            if (product.getStockQuantity() <= product.getLowStockThreshold()) {
                productService.deactivateProduct(product);
                // 发送商品下架通知
                notificationService.sendOutOfStockNotification(product.getId());
            }
        }
    }
}

场景 3: 定时发送促销活动通知

假设电商平台有一个促销活动,每个活动的开始和结束时间都由后台系统控制。需要在活动开始前 1 小时、活动结束时发送通知给用户。这些通知可以是短信、邮件或 APP 推送通知。

解决的问题:
  1. 定时任务管理:XXL-Job 可以定时触发通知任务,确保用户在活动前后及时收到通知。
  2. 高并发支持:在促销活动开始或结束时,平台可能会有大量的通知需要发送,XXL-Job 支持任务的并行处理,可以帮助我们高效地分发通知,避免性能瓶颈。
  3. 任务状态监控:XXL-Job 提供任务的实时监控功能,平台可以随时查看任务的执行情况,确保通知任务按时执行。
public class PromotionJob {
    @JobHandler("promotionNotifyJobHandler")
    public void sendPromotionNotifications() {
        // 获取当前正在进行的促销活动
        List<Promotion> activePromotions = promotionService.findActivePromotions();
        for (Promotion promotion : activePromotions) {
            if (promotion.isStartingSoon()) {
                notificationService.sendStartNotification(promotion);
            } else if (promotion.isEndingSoon()) {
                notificationService.sendEndNotification(promotion);
            }
        }
    }
}
xxl-job路由策略有哪些?

实例找任务项执行任务 这种找机器的方式就是路由策略

消息中间件RabbitMQ+Kafka

消息中间件提供了服务与服务之间的异步调用,还可以服务与服务之间解耦

RabbitMQ:**消息不丢失、消息重复消费、消息堆积、延迟队列、死信队列、高可用机制**
Kafka:**消息不丢失、消息重复消费、高可用机制、高性能设计吞吐量达到百万级数据存储和清理**

RabbitMQ-如何保证消息不丢失?

  • 开启生产者确认机制,确保生产者的消息能到达队列
    confirm到交换机ack 不到nack 和 return没到返回nack机制保证生产者把消息发过去

    达到队列成功返回ack,失败返回nacknegative acknowledgment

    1. 生产者发送消息到交换机。
    2. 交换机收到消息后,根据绑定规则(是否有匹配的队列)决定消息是否被正确路由。
    3. 如果消息成功路由到队列,交换机会向生产者返回 ack 确认。
    4. 如果消息没有成功路由到任何队列,交换机会通过 return 将消息退回给生产者。
    5. 生产者收到 acknack,可以处理消息确认或重试逻辑。
  • 开启持久化功能,确保消息未消费前在队列中不会丢失 durable = True
    万一broker挂掉就惨了 保证至少成功一次消费
    MQ是默认内存存储信息,开启持久化功能可以确保缓存在MQ中的消息不丢失[把数据存在磁盘上]

    # 声明持久化交换器
    channel.exchange_declare(exchange='exchange_name', durable=True)
    
    # 声明持久化队列
    channel.queue_declare(queue='queue_name', durable=True)
    
    # 发送持久化消息
    channel.basic_publish(exchange='exchange_name',
                          routing_key='routing_key',
                          body='Hello World!',
                          properties=pika.BasicProperties(
                             delivery_mode=2,  # 使消息持久化
                          ))
    
  • 开启消费者确认机制为auto,由spring确认消息处理成功后完成ack
    消费者三种机制:

    RabbitMQ支持消费者确认机制,即:**消费者处理消息后可以向MQ发送ack回执,MQ收到ack回执后才会删除该消息**,而Spring AMQP则允许配置三种确认模式:

    • manual:手动ack,需要在业务代码结束后,调用api发送ack。

    • auto:自动ack,由spring监测listener代码是否出现异常,没有异常则返回ack;抛出异常则返回nack

    • none:关闭ack,MQ假定消费者获取消息后会成功处理,因此消息投递后立即被删除

  • 开启消费者失败重试机制,多次重试失败后将消息投递到异常交换机,交由人工处理

    在开启重试模式后,重试次数耗尽,如果消息依然失败,则需要有MessageRecoverer接口来处理,它包含三种不同的实现:

    • RejectAndDontRequeueRecoverer:重试耗尽后,直接reject,丢弃消息。默认就是这种方式

    • ImmediateRequeueMessageRecoverer:重试耗尽后,返回nack,消息重新入队

    • RepublishMessageRecoverer:重试耗尽后,将失败消息投递到指定的交换机

  • 异步发送(验证码、短信、邮件)
  • MySQL和Redis,ES之间的数据同步
  • 分布式事务
  • 削峰填谷
==生产者确认机制==

RabbitMQ提供了publisher confirm机制来避免消息发送到MQ过程中丢失。消息发送到MQ以后,会返回一个结果给发送者,表示消息是否处理成功

RabbitMQ消息的重复消费问题如何解决的?

我们当时消费者是设置了自动确认机制,当服务还没来得及给MQ确认的时候,服务宕机了,导致服务重启之后,又消费了一次消息,这样就重复消费了
因为我们当时处理的支付(订单|业务唯一标识),它有一个业务的唯一标识,我们再处理消息时,先到数据库查询一下,这个数据是否存在,如果不存在,说明没有处理过,这个时候就可以正常处理这个消息了。如果已经存在这个数据了,就说明消息重复消费了,我们就不需要再消费了

原因
  • 网络抖动
  • 消费者挂了

解决方案:适用于任何MQ(Kafka,RabbitMQ,RocketMQ)

  • 每条消息设置一个唯一的标识id
  • 幂等方案:【分布式锁、数据库锁(悲观锁、乐观锁)】

RabbitMQ中死信交换机?(RabbitMQ延迟队列有了解过吗)

如果用原来的定时任务 也可以但是 可能会有订单空窗期 如果没人消费的时候 它内部还是回去sql查询已下单 +(now()-下单时间)?15min : true, false

  • 我们当时一个什么业务使用到了延迟队列(超时订单、限时优惠、定时发布)
  • 其中延迟队列就用到了死信交换机TTL(消息存活时间)实现的
  • 消息超时未消费就会变成死信(死信的其他情况:拒绝被消费,队列满了

延迟队列插件实现延迟队列DelayExchange

  • 声明一个交换机,添加delayed属性为true
  • 发送消息时,添加x-delay头,值为超过时间
什么样的消息会成为死信?
★ 消费者返回reject或者nack,且requeue参数设置为false【消息被拒绝并且不重入队列】
★ 消息超时未消费
★ 队列满了

如何给队列绑定死信交换机?
★ 给队列设置dead-letter-exchange属性,指定一个交换机
★ 给队列设置dead-letter-routing-key属性,设置死信交换机与死信队列的RoutingKey

------------------------------------------------------------------------
★ ★ ★ 使用 Spring AMQP 配置 ★ ★ ★ 
import org.springframework.amqp.core.Queue;
import org.springframework.amqp.core.Exchange;
import org.springframework.amqp.core.TopicExchange;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class RabbitMQConfig {

    // 定义普通队列
    @Bean
    public Queue normalQueue() {
        return new Queue("normalQueue", true, false, false, 
                Map.of("x-dead-letter-exchange", "dlx_exchange", 
                       "x-dead-letter-routing-key", "dlx_routing_key"));
    }

    // 定义死信队列
    @Bean
    public Queue dlxQueue() {
        return new Queue("dlxQueue", true);
    }

    // 定义普通交换机
    @Bean
    public Exchange normalExchange() {
        return new TopicExchange("normal_exchange");
    }

    // 定义死信交换机
    @Bean
    public Exchange dlxExchange() {
        return new TopicExchange("dlx_exchange");
    }

    // 将普通队列与交换机绑定
    @Bean
    public Binding bindNormalQueue() {
        return BindingBuilder.bind(normalQueue()).to(normalExchange()).with("normal.routing.key").noargs();
    }

    // 将死信队列与死信交换机绑定
    @Bean
    public Binding bindDLXQueue() {
        return BindingBuilder.bind(dlxQueue()).to(dlxExchange()).with("dlx_routing_key").noargs();
    }
}
------------------------------------------------------------------------
如果你希望将死信队列配置成带有过期时间或其他特殊属性的队列,可以在定义 dlxQueue 时增加更多的设置,例如 TTL(过期时间)。
例如,设置死信队列的 TTL:

@Bean
public Queue dlxQueue() {
    return QueueBuilder.durable("dlxQueue")
            .withArgument("x-message-ttl", 60000) // 设置TTL为60秒
            .build();
}
延迟队列 = 死信交换机 + TTL (生存时间)
  • 延迟队列:进入队列的消息会被延迟消费的队列
  • 场景:超时订单、限时优惠,定时发布
死信交换机

当一个队列中的消息满足下列情况之一时,可以成为死信(dead letter):

  • 消费者使用basic.reject 或 basic.nack声明消费失败,并且信息的requeue参数设置为false
  • 消息是一个过期消息,超时无人消费
  • 要投递的队列消息堆积满了,最早的消息可能成为死信

如果该队列配置了dead-letter-exchange属性,指定了一个交换机,那么队列中的死信就会投递到这个交换机中,而这个交换机称为死信交换机(Dead Letter Exchange,简称DLX)

@Bean
public QUeue ttlQueue(){
    return QueueBuilder.durable("simple.queue") // 指定队列名称,并持久化
        .ttl(10000) // 设置队列的超时时间 10秒
        .deadLetterExchange("dl.direct") // 指定死信交换机
        .build();
}
TTL

TTL(Time-To-Live)。如果一个队列中的消息TTL结束仍未消费,则会变成死信,ttl超时分为两种情况:

  • 消息所在的队列设置了存活时间
  • 消息本身设置了存活时间

死信图片

RabbitMQ如果有100万消息堆积在MQ,如何解决(消息堆积怎么解决)?

**解决消息堆积有三种思路 **

  • 增加更多消费者,提高消费速度
  • 在消费者内开启线程池加快消息处理速度
  • 扩大队列容积,提高堆积上限
    • 在声明队列的时候可以设置x-queue-model为lazy,即为惰性队列
    • 基于磁盘存储,消息上限高
    • 性能比较稳定,但基于磁盘存储,受限于磁盘IO,时效性会降低

生产者发送消息的速度超过了消费者处理消息的速度,就会导致队列中的消息堆积,直到队列存储消息达到上限。之后发送的消息就会成为死信,可能会被丢弃,这就是堆积问题

**解决消息堆积有三种思路 **

  • 增加更多消费者,提高消费速度
  • 在消费者内开启线程池加快消息处理速度
  • 扩大队列容积,提高堆积上限
达到上限发送的消息会变成死信,那我为什么不搞个死信交换机 而是用了上面的三种思路??

使用死信交换机(DLX, Dead Letter Exchange)是另一种处理消息堆积的方式,但它的作用更偏向于“消息过期”或“处理失败”的情况下将消息转发到另一个队列。并不直接解决生产者发送消息过快或消费者处理速度过慢的问题。通过死信交换机,你可以将无法处理的消息转发到其他队列,方便你后续进行分析或处理,但它并不能提高消费者处理消息的速度。

针对消息堆积的本质问题,解决方式更多的是优化消费者处理能力,而不是仅依赖死信交换机。具体而言,死信交换机和你的三种思路的关系如下:

  1. 死信交换机(DLX):当消息达到队列上限或无法消费时,消息被转发到死信队列。你可以分析死信队列中的消息,了解原因,并决定是重试、丢弃还是进行其他处理。它的作用是不丢失消息,但并不能帮助消除堆积。
  2. 增加消费者:这是直接针对堆积的根本解决方案,通过增加消费者数量来加速消息的处理。死信交换机无法直接解决消费者处理能力不足的问题。
  3. 开启消费者线程池:在单个消费者上开启线程池,可以提高消费者的处理能力,减少堆积。死信交换机并不能增加消息处理速度,它只是用来应对消费失败的情况。
  4. 扩大队列容量并使用惰性队列:惰性队列可以将消息存储在磁盘上,而非内存中,减轻内存压力,但这也会降低时效性,并不能解决生产者生产过快或消费者消费过慢的问题。死信交换机同样无法直接解决这一点。

总结来说,死信交换机是处理消息丢失或无法消费的方式,它和通过增加消费者、线程池、队列优化这些手段并不冲突,但也无法替代这些更直接的解决方案。你可以结合这两者,使用死信交换机来保障消息不丢失,同时采取上述方法来提高消息消费速度。

惰性队列

惰性队列特征如下:

  • 接收到消息后直接存入磁盘而非内存
  • 消费者要消费消息时才会从磁盘中读取并加载到内存
  • 支持数百万条的消息存储
@Bean
public Queue lazyQueue(){
    return QueueBuilder
            .durable("lazy.queue")
            .lazy()
            .build();
}
@RabbitListener(queuesToDeclare = @Queue){
    name = "lazy.queue",
    durable = "true",
    arguments = @Argument(name = "x-queue-mode"), value="lazy"
}
public void listenLazyQUeue(String msg){
    log.info("接收到lazy.queue的消息:{}",msg);
}

RabbitMQ高可用机制有了解过吗? &&  请描述 RabbitMQ 镜像队列的工作原理及其在高可用性场景下的优缺点

我们当时的项目在生产环境下,采用的是镜像模式搭建的集群,共有3个节点
镜像队列结构是一主多从(从就是镜像),所有(写)操作都是主节点完成,然后同步给镜像节点
主宕机后,镜像节点会代替成为新的主(如果在主从同步完成前,主就已经宕机,可能出现数据丢失)

那出现丢数据怎么解决呢?

我们可以采用仲裁队列,与镜像队列一样,都是主从模式,支持主从数据同步,主从同步基于Raft协议,强一致性,并且使用起来也非常简单,不需要格外的配置,在声明队列的时候只需要指定这个是仲裁队列即可

  • 在生产环境下,使用集群来保证高可用性
  • 普通集群、镜像集群、仲裁队列
普通集群

普通集群,或者叫标准集群(classic cluster)

  • 会在集群的各个节点间共享部分数据,包括:交换机、队列元信息。不包含队列中的信息
  • 当访问集群某节点时,如果队列不在该节点,会从数据所在节点传递到当前节点并返回
  • 队列所在节点宕机,队列中的消息就会丢失
镜像集群

镜像集群:本质是主从模式,具备下面的特征:

  • 交换机、队列、队列中的消息会在各个mq的镜像节点之间同步备份
  • 创建队列的节点被称为该队列的主节点,备份到的其他节点叫做该队列的镜像节点
  • 一个队列的主节点可能是另一个队列的镜像节点
  • 所有操作都是主节点完成,然后同步给镜像节点
  • 主宕机后,镜像节点会替代成新的主
仲裁队列:.quorum()

仲裁队列是3.8版本以后才有的新功能,用来替代镜像队列

  • 与镜像队列一样,都是主从模式,支持主从数据同步
  • 使用非常简单,没有复杂的配置
  • 主从同步基于Raft协议,强一致性

仲裁队列的工作原理如下:

  1. 主从模式:仲裁队列也是主从模式,支持主从数据同步。
  2. Raft 协议:主从同步基于 Raft 协议,确保数据的一致性和可靠性。
  3. 强一致性:所有写操作必须得到大多数节点的确认后才能完成,避免了数据丢失。

仲裁队列通过以下机制保证数据不丢失:

  • 多数派确认每次写操作需要得到大多数节点的确认,确保数据已经成功复制到多个节点
  • 自动故障转移:如果主节点宕机,Raft 协议会自动选举新的主节点,确保服务的连续性。
  • 数据一致性Raft 协议保证了数据的强一致性,即使在网络分区或节点宕机的情况下,也不会出现数据不一致的问题。

仲裁队列的优点是配置简单、数据强一致,但需要至少 3 个节点,并且在写操作上的延迟和资源消耗可能会比镜像队列高。

@Bean
public Queue quorumQueue(){
    return QueueBuilder
            .durable("quorum.queue") // 持久化
            .quorum() // 仲裁队列
            .build();
}

Kafka是如何保证消息不丢失?

需要从三个层面去解决这个问题

  • 生产者发送消息到Brocker丢失

    • 设置异步发送,发送失败使用回调进行记录或重发
    • 失败重试,参数配置,可以设置重试次数消息
  • 在Brocker中存储丢失

    发送确认acks,选择all,让所有的副本都参与保存数据后确认

  • 消费者从Brocker接收消息丢失

    • 关闭自动提交偏移量,开启手动提交偏移量
    • 提交方式:最好是同步+异步提交

使用Kafka在消息的收发过程中都会出现消息丢失,Kafka分别给出了解决方案

  • 生产者发送消息到Brocker丢失
  • 消息在Brocker中存储丢失
  • 消费者从Brocker接收消息丢失

kafka-高产出的分布式消息系统(A high-throughput distributed messaging system)。

Kafka是一个高吞吐、分布式、基于发布订阅的消息系统,利用Kafka技术可以在廉价的PC Server上搭建起大规模消息系统。

Kafka的特性:
  • 高吞吐量、低延迟:kafka每秒可以处理几十万条消息,它的延迟最低只有几毫秒,每个topic可以分多个partition, consumer group 对partition进行consume操作;
  • 可扩展性:kafka集群支持热扩展;
  • 持久性、可靠性:消息被持久化到本地磁盘,并且支持数据备份防止数据丢失;
  • 容错性:允许集群中节点失败(若副本数量为n,则允许n-1个节点失败);
  • 高并发:支持数千个客户端同时读写;
  • 支持实时在线处理和离线处理:可以使用Storm这种实时流处理系统对消息进行实时进行处理,同时还可以使用Hadoop这种批处理系统进行离线处理;

Kafka和其他组件比较,具有消息持久化、高吞吐、分布式、多客户端支持、实时等特性,适用于离线和在线的消息消费,如常规的消息收集、网站活性跟踪、聚合统计系统运营数据(监控数据)、日志收集等大量数据的互联网服务的数据收集场景。

  1. 日志收集:一个公司可以用Kafka可以收集各种服务的log,通过kafka以统一接口服务的方式开放给各种consumer,例如Hadoop、Hbase、Solr等;
  2. 消息系统:解耦和生产者和消费者、缓存消息等;
  3. 用户活动跟踪:Kafka经常被用来记录web用户或者app用户的各种活动,如浏览网页、搜索、点击等活动,这些活动信息被各个服务器发布到kafka的topic中,然后订阅者通过订阅这些topic来做实时的监控分析,或者装载到Hadoop、数据仓库中做离线分析和挖掘;
  4. 运营指标:Kafka也经常用来记录运营监控数据。包括收集各种分布式应用的数据,生产各种操作的集中反馈,比如报警和报告;
  5. 流式处理:比如spark streaming和storm;
  6. 事件源;
  7. kafka在FusionInsight中的位置:

Kafka是如何保证消费的顺序性?

问题原因:
一个topic的数据可能存储在不同的分区中 ,每个分区都有一个按照顺序的存储的偏移量,如果消费者关联了多个分区不能保证顺序性

==解决方案:==

  • 发送消息时指定分区号
  • 发送消息时按照相同的业务设置相同的key

应用场景:

  • 即时消息中的单对单聊天和群聊,保证发送方消息发送顺序与接收方的顺序一致
  • 充值转账两个渠道在同一个时间进行金额变更,短信通知必须要有顺序

承接上图消费者从Brocker接收消息丢失
如何做?→ topic分区中消息只能由消费者组中的唯一一个消费者处理,所以消息肯定是按照先后顺序进行处理的。但是它也仅仅是保证Topic的一个分区顺序处理,不能保证跨分区的消息先后处理顺序。所以,如果你想要顺序的处理Topic的所有消息,那就只提供一个分区。

// 指定分区
kafkaTemplate.sent("springboot-kafka-topic",0,"key-001","value-001");
// 相同的业务key
kafkaTemplate.sent("springboot-kafka-topic","key-001","value-001");

会计算key的hashcode值推断出它在哪个分区,如果要求有顺序性 就可以设置同一个key,此时hash值都是一样的 就可以在同一个分区存储

Kafka的高可用机制有了解过吗?

  • ==集群模式==

    一个kafka集群由多个broker实例组成,即使某一台宕机,也不会耽误其他broker继续对外提供服务

  • ==分区备份机制==

    • 一个topic有多个分区,每个分区有多个副本,有一个leader,其余的是follower,副本存储在不同的broker中
    • 所有的分区副本的内容都是相同的,如果leader发生故障时,会自动将其中一个follower提升为leader,保证了系统的容错性、高可用性

解释一下复制机制中的ISR?

ISR (in-sync replica) 需要同步复制保存的follower
分区副本分为了两类,一个是ISR,与leader副本同步保存数据,另外一个普通的副本,是异步同步数据,当leader挂掉后,会优先从ISR副本列表中选取一个作为leader

// 一个topic默认分区的replication个数,不能大于集群中broker的个数。默认为1
default.replication.factor=3
// 最小的ISR副本个数
min.insync.replicas=2

Kafka数据清理机制了解过吗?

  • kafka文件存储机制
    • Kafka中topic的数据存储在分区上,分区如果文件过大会分段存储segment
    • 每个分段都在磁盘上以索引(xxxx.index)和日志文件(xxx.log)的形式存储
    • 分段的好处是,第一能够减少单个文件内容的大小,查找数据方便,第二方便kafka进行日志清理
  • 数据清理机制
    • 根据消息的保留时间,当消息保存的时间超过了指定的时间,就会触发清理,默认168小时(7天)
    • 根据topic存储的数据大小,当topic所占的日志文件大小大于一定的阈值,则开始删除最久的消息(默认关闭)

Kafka中实现高性能的设计有了解过吗?

  • 消息分区:不受单台服务器的限制,可以不受限的处理更多的数据
  • 顺序读写:磁盘顺序读写,提升读写效率
  • 页缓存:把磁盘中的数据缓存到内存中,把对磁盘的访问变为对内存的访问
  • 零拷贝:减少上下文切换及数据拷贝
  • 消息压缩:减少磁盘IO和网络IO
  • 分批发送:将消息打包批量发送,减少网络开销
零拷贝

集合面试篇

算法复杂度分析

什么是算法时间复杂度?

  • 时间复杂度表示了算法的执行时间数据规模之间的增长关系

常见的时间复杂度有哪些?口诀:常对幂指阶

  • O(1)、O(n)、O(n^2)、O(logn)

什么是算法的空间复杂度?

  • 表示算法占用的额外存储空间数据规模之间的增长关系
    常见的空间复杂度:O(1)、O(n)、O(n^2)
为什么要进行复杂度分析?
  • 指导你编写出性能更优的代码
  • 评判别人写的代码的好坏
时间复杂度分析:来评估代码的执行耗时的
  • 大O表示法:不具体表示代码真正的执行时间,而是表示代码执行时间随数据规模增长的变化趋势

  • 只要代码的执行时间**不随着n的增大而增大,这样的代码复杂度都是O(1)**

  • 复杂度分析就是要弄清楚代码的执行次数数据规模n之间的关系

时间复杂度:全称是渐进空间复杂度,表示算法占用的额外存储空间数据规模之间的增长关系

List相关面试题

  • 数组是一种用连续的内存空间存储相同数据类型数组的线性数据结构

  • 数组下标为什么从0开始

    寻址公式是:baseAddress + i * data TypeSize 计算下标的内存地址效率较高

  • 查找的时间复杂度

    • 随机(通过下标)查询的时间复杂度是O(1)
    • 查找元素(未知下标)的时间复杂度是O(n)
    • 查找元素(未知下标但排序)通过二分查找的时间复杂度是O(logn)
  • 插入和删除时间复杂度

    插入和删除的时候,为了保证数组的内存连续性,需要挪动数组元素,平均复杂度为O(n)

底层实现
  • 数据结构—数组
  • ArrayList源码分析
面试问题
  • ArrayList底层的实现原理是什么
  • ArrayList list = new ArrayList(10)中的list扩容几次
  • 如何实现数组和List之间的转换
  • ArrayList和LinkedList的区别是什么

ArrayList源码分析

List< Integer > list = new ArrayList< Integer >();
list.add(1)

public class ArrayList<E> extends AbstractList<E>
        implements List<E>, RandomAccess, Cloneable, java.io.Serializable{

    private static final long serialVersionUID = 8683452581122892189L;

    /**
     * Default initial capacity.
     */
    private static final int DEFAULT_CAPACITY = 10;

    /**
     * 用于空实例的共享空数组实例
     */
    private static final Object[] EMPTY_ELEMENTDATA = {};

    /**
     * 用于默认大小的空实例的共享空数组实例
     * 与上面的区分开,以了解添加第一个元素时要膨胀多少
     */
    private static final Object[] DEFAULTCAPACITY_EMPTY_ELEMENTDATA = {};
  
    /* 存储ArrayList元素的数组缓冲区,ArrayList的容量就是这个数组缓冲区的长度 */
    transient Object[] elementData; // non-private to simplify nested class access

    /**
     * ArrayList的大小(包含的元素数量)
     * @serial
     */
    private int size;
...
}

--------------------------------------------------------------------------------

public ArrayList(int initialCapacity) {
        if (initialCapacity > 0) {
       // 创建一个真正存储集合位置的数组
            this.elementData = new Object[initialCapacity];
        } else if (initialCapacity == 0) {
    // 如果容量是0则创建一个新的数组给elementData
            this.elementData = EMPTY_ELEMENTDATA;
        } else {
            throw new IllegalArgumentException("Illegal Capacity: "+
                                               initialCapacity);
        }
    }
    public ArrayList() { 
         // 无参构造函数,默认创建空集合
        this.elementData = DEFAULTCAPACITY_EMPTY_ELEMENTDATA;
    }

-------------------------------------------------------------------------------
// Collection是所有单列集合的父接口
// 将 Collection 对象转换成数组,然后将数组的地址赋给 elementData
 public ArrayList(Collection<? extends E> c) {
        elementData = c.toArray();
        if ((size = elementData.length) != 0) {
            // 判断集合类型是否为不为object[] 
            // 在其他jdk此处是 ?? == ArrayList.class
            if (elementData.getClass() != Object[].class)
                // 不是的话就拷贝到数组elementData中
                elementData = Arrays.copyOf(elementData, size, Object[].class);
        } else {
            // replace with empty array.
            this.elementData = EMPTY_ELEMENTDATA;
        }
    }
ArrayList源码分析-添加和扩容操作(第1次添加数据)

ArrayList底层的实现原理是什么

  • ArrayList底层是用动态数组实现的
  • ArrayList初始容量为0,当第一次添加数据的时候才会初始化容量为10
  • ArrayList在进行扩容的时候是原来容量的1.5倍,每次扩容都需要拷贝数组
  • ArrayList在添加数据的时候
    • 确保数组已使用长度(size)加1之后足够存下下一个数据
    • 计算数组的容量,如果当前数组已使用长度+1后的大于当前的数组长度,则调用grow方法扩容(原来的1.5倍)
    • 确保新增的数据有地方存储之后,则将新元素添加到位于size的位置上
    • 返回添加成功布尔值

ArrayList list = new ArrayList(10)中的list扩容几次

  • 该语句只是声明和实例了一个ArrayList,指定了容量为10,未扩容

如何实现数组和List之间的转换

  • 数组转List,使用JDK中java.util.Arrays工具类的asList方法
  • List转数组,使用List的toArray方法,无参toArray方法返回Object数组,传入初始化长度的数组对象,返回该对象数组

使用 Hutool 工具库可以非常方便地实现数组和 List 之间的转换。Hutool 提供了 ArrayUtilCollUtil 工具类来处理数组和集合之间的转换。

问:我不能用BeanUtil吗?
答:BeanUtil 是 Hutool 工具库中用于 Java Bean 操作的工具类,主要用于 对象属性拷贝Bean 转 MapMap 转 Bean 等操作。它并不适用于 数组和 List 之间的转换

如果你误以为 BeanUtil 可以用于数组和 List 的转换,可能是因为它的名字容易让人误解。实际上,数组和 List 的转换应该使用 ArrayUtilCollUtil

正确的工具类选择

  1. 数组转 List:使用 ArrayUtil.toList
  2. List 转数组:使用 CollUtil.toArray
  3. Bean 属性拷贝:使用 BeanUtil.copyProperties
  4. Bean 转 Map:使用 BeanUtil.beanToMap
  5. Map 转 Bean:使用 BeanUtil.fillBeanWithMap
更多的Hutool工具使用高能预警
1. 字符串工具类:StrUtil
  • 功能:字符串操作,如判空、格式化、截取、替换等。
  • 常用方法
    • StrUtil.isEmpty():判断字符串是否为空。
    • StrUtil.format():格式化字符串。
    • StrUtil.split():拆分字符串。
    • StrUtil.join():连接字符串。

2. 日期时间工具类:DateUtil
  • 功能:日期和时间的格式化、解析、计算等。
  • 常用方法
    • DateUtil.now():获取当前时间。
    • DateUtil.format():格式化日期。
    • DateUtil.parse():解析字符串为日期。
    • DateUtil.offsetDay():日期加减。

3. 文件工具类:FileUtil
  • 功能:文件和目录的操作,如读写、复制、删除等。
  • 常用方法
    • FileUtil.readUtf8String():读取文件内容为字符串。
    • FileUtil.writeUtf8String():将字符串写入文件。
    • FileUtil.copy():复制文件或目录。
    • FileUtil.del():删除文件或目录。

4. JSON 工具类:JSONUtil
  • 功能:JSON 的解析和生成。
  • 常用方法
    • JSONUtil.parseObj():将 JSON 字符串解析为 JSON 对象。
    • JSONUtil.parseArray():将 JSON 字符串解析为 JSON 数组。
    • JSONUtil.toJsonStr():将对象转换为 JSON 字符串。

5. 集合工具类:CollUtil
  • 功能:集合操作,如创建集合、判空、过滤、分组等。
  • 常用方法
    • CollUtil.newArrayList():快速创建 ArrayList。
    • CollUtil.isEmpty():判断集合是否为空。
    • CollUtil.filter():过滤集合。
    • CollUtil.group():对集合进行分组。

6. 反射工具类:ReflectUtil
  • 功能:反射操作,如调用方法、获取字段、创建对象等。
  • 常用方法
    • ReflectUtil.invoke():调用方法。
    • ReflectUtil.getFieldValue():获取字段值。
    • ReflectUtil.newInstance():创建对象实例。

7. HTTP 工具类:HttpUtil
  • 功能:HTTP 请求的发送和响应处理。
  • 常用方法
    • HttpUtil.get():发送 GET 请求。
    • HttpUtil.post():发送 POST 请求。
    • HttpUtil.downloadFile():下载文件。

8. 加密解密工具类:SecureUtil
  • 功能:常见的加密解密操作,如 MD5、SHA、AES 等。
  • 常用方法
    • SecureUtil.md5():计算 MD5 值。
    • SecureUtil.sha256():计算 SHA-256 值。
    • SecureUtil.aes():AES 加密解密。

9. IO 工具类:IoUtil
  • 功能:IO 流操作,如读写、关闭流等。
  • 常用方法
    • IoUtil.read():读取流内容。
    • IoUtil.write():写入流内容。
    • IoUtil.close():关闭流。

10. 随机工具类:RandomUtil
  • 功能:生成随机数、随机字符串等。
  • 常用方法
    • RandomUtil.randomInt():生成随机整数。
    • RandomUtil.randomString():生成随机字符串。
    • RandomUtil.randomEle():从集合中随机选择一个元素。

11. 验证工具类:Validator
  • 功能:数据验证,如邮箱、手机号、身份证等。
  • 常用方法
    • Validator.isEmail():验证是否为邮箱。
    • Validator.isMobile():验证是否为手机号。
    • Validator.isCitizenId():验证是否为身份证号。

12. 缓存工具类:CacheUtil
  • 功能:简单的缓存操作。
  • 常用方法
    • CacheUtil.newTimedCache():创建定时缓存。
    • CacheUtil.put():添加缓存。
    • CacheUtil.get():获取缓存。

13. 线程工具类:ThreadUtil
  • 功能:线程操作,如睡眠、创建线程池等。
  • 常用方法
    • ThreadUtil.sleep():线程睡眠。
    • ThreadUtil.newExecutor():创建线程池。

14. Excel 工具类:ExcelUtil
  • 功能:Excel 文件的读写操作。
  • 常用方法
    • ExcelUtil.getReader():读取 Excel 文件。
    • ExcelUtil.getWriter():写入 Excel 文件。

15. 压缩工具类:ZipUtil
  • 功能:文件或目录的压缩和解压缩。
  • 常用方法
    • ZipUtil.zip():压缩文件或目录。
    • ZipUtil.unzip():解压缩文件。

16. 日志工具类:Log
  • 功能:简化日志操作。
  • 常用方法
    • Log.get():获取日志对象。
    • Log.info():输出日志信息。

17. 数学工具类:MathUtil
  • 功能:数学计算,如四舍五入、最大值、最小值等。
  • 常用方法
    • MathUtil.round():四舍五入。
    • MathUtil.max():获取最大值。
    • MathUtil.min():获取最小值。

18. 网络工具类:NetUtil
  • 功能:网络相关操作,如获取本机 IP、Ping 等。
  • 常用方法
    • NetUtil.getLocalhost():获取本机 IP。
    • NetUtil.ping():Ping 测试。
  • Arrays.asList转List后,如果修改了数组内容,list受影响吗
  • List用toArray转数组后,如果修改了List内容,数组受影响吗
再答:
  • Arrays.asList转换list之后,如果修改了数组的内容,list会受影响,因为它的底层使用的Arrays类中的一个内部类ArrayList来构造的集合,在这个集合的构造器中,把我们传入的这个集合进行了包装而已,最终指向的都是同一个内存地址
  • list用了toArray转数组后,如果修改了list内容,数组不会受影响,当调用了toArray以后,在底层是它进行了数组拷贝,跟原来的元素就没啥关系了,所以即使list修改了以后,数组也不受影响

LinkedList的数据结构—链表

单向链表

  • 链表中的每一个元素称之为结点(Node)
  • 物理存储单元上,非连续、非顺序的存储结构
  • 单向链表:每个结点包括两个部分:一个是存储数据元素的数据域,另一个是存储下一个结点地址的指针域。记录下个结点地址的指针叫作后继指针 next
1.单向链表和双向链表的区别是什么
  • 单向链表只有一个方向,结点只有一个后继指针 next。
  • 双向链表它支持两个方向,每个结点不止有一个后继指针next指向后面的结点,还有一个前驱指针prev指向前面的结点
2.链表操作数据的时间复杂度是多少
查询 新增删除
单向链表 头O(1), 其他O(n) 头O(1), 其他O(n)
双向链表 头尾O(1), 其他O(n), 给定节点O(1) 头尾O(1), 其他O(n), 给定节点O(1)

ArrayList和LinkedList的区别

  • 底层数据结构
    • ArrayList 是动态数组的数据结构实现
    • LinkedList 是双向链表的数据结构实现
  • 操作数组效率

    • ArrayList 按照下标查询的时间复杂度O(1);【内存是连续的,根据寻址公式】,LinkedList不支持下标查询

    • 查找(未知索引):ArrayList需要遍历,链表也需要遍历,时间复杂度都是O(n)

    • 新增删除

      • ArrayList尾部插入和删除,时间复杂度是O(1);其他部分增删需要挪动数组,时间复杂度是O(n)
      • LinkedList 头尾节点增删时间复杂度是O(1),其他都需要遍历链表,时间复杂度是O(n)
  • 内存空间占用

    • ArrayList 底层是数组,内存连续,节省内存
    • LinkedList 是双向链表需要存储数据,和两个指针,更占用内存
  • 线程安全

    • ArrayList和LinkedList都不是线程安全的

    • 如果要保证线程安全,有两种方法

      • 在方法内使用,局部变量则是线程安全的

      • 使用线程安全的ArrayList和LinkedList

        List<Object> syncArrayList = Collections.synchronizedList(new ArrayList<>());
        List<Object> syncLinkedList = Collections.synchronizedList(new LinkedList<>());
        

HashMap相关面试题

  • 二叉树

    • 满二叉树

    • 完全二叉树

    • 二叉搜索树

      二叉搜索树又名二叉查找树,有序二叉树或者排序二叉树,是二叉树中比较常用的一种类型二叉查找树要求,在树中的任意一个节点,其左子树中的每个节点的值,都要小于这个节点的值,而右子树节点的值都大于这个节点的值

    • 红黑树

  • 红黑树

  • 散列表

数据结构—红黑树 什么是红黑树?

  • 红黑树:也是一种自平衡的二叉搜索树(BST)
  • 所有的红黑规则都是希望红黑树能够保证平衡
  • 红黑树的时间复杂度:查找、添加、删除都是O(logn)

散列表

什么是散列表?

  • 散列表(Hash Table)又叫哈希表/Hash表
  • 根据键(Key)直接访问再内存存储位置值(Value)的数据结构
  • 由数组演化而来的,利用了数组支持按照下标进行随机访问数据

散列冲突

  • 散列冲突又成为哈希冲突,哈希碰撞
  • 指多个key映射到同一个数组下标位置

散列冲突—链表法(拉链)

  • 数组的每个下标位置称之为(bucket) 或者 (slot)
  • 每个桶(槽)会对应一条链表
  • hash冲突后的元素都放到相同槽位对应的链表中或红黑树中

HashMap中的最重要的一个数据结构就是散列表,在散列表中又用到了红黑树链表
散列表(Hash Table)又名为哈希表/Hash表,是根据键(Key)直接访问在内存存储位置值(value)的数据结构,它是由数组演化而来的,利用了数组支持按照下标进行随机访问数据的特性[根据寻址公式,时间复杂度O(1)]

说一下HashMap的实现原理

1.说一下HashMap的实现原理

  • HashMap的数据结构:底层使用hash表数据结构,即数组和链表或红黑树

  • 添加数据时,计算key的值确定元素在数组中的下标

    • key相同则替换
    • 不同则存入链表或红黑树中

    获取数据通过key的hash计算数组下标获取元素

2.HashMap的jdk1.7和jdk1.8有什么区别

  • JDK1.8之前采用的拉链法,数组+链表
  • JDK1.8之后采用数组+链表+红黑树
    链表长度大于8且数组长度大于64则会从链表转化为红黑树

当我们往HashMap中put元素时,利用key的hashCode重新hash计算

出当前对象的元素在数组中的下标

HashMap的put方法的具体流程

1.判断键值对数组table是否为空或为null,否则执行resize()进行扩容 [初始化]
2.根据键值key计算hash值得到数组索引
3.判断table[i] == null,条件成立,直接新建节点添加
4.如果table[i] == null,不成立
4.1 判断table[i]的首个元素是否和key一样,如果相同直接覆盖value
4.2 判断table[i]是否为treeNode,即table[i]是否是红黑树,如果是红黑树,则直接在树中插入键值对
4.3 遍历table[i],链表的尾部插入数据,然后判断链表长度是否大于8,大于8的话把链表转换为红黑树,在红黑树中执行插入操作,遍历过程中若发现key已经存在直接覆盖value

讲一下HashMap的扩容机制

HashMap源码分析

桶下标是hash值取模数组(长度)下标 capacity

HashMap的寻址算法

Hash值右移16位后与原来的hash值进行异或运算【扰动算法hash值更加均匀,减少hash冲突
数组长度必须是2的n次幂 按位与运算的效果才能代替取模

HashMap在1.7情况下的多线程死循环问题

jdk7的数据结构是:数组+链表
在数组进行扩容的时候,因为链表是头插法,在进行数据迁移的过程中,有可能导致死循环

进程和线程的区别?

两者对比:
  • 进程是整个在运行程序的实例,进程中包含了线程,每个线程执行不同的任务
  • 不同的进程使用不同的内存空间,在当前进程下的所有线程可以共享内存空间
  • 线程更轻量,线程上下文切换成本一般上要比进程上下文切换低(上下文切换指的是从一个线程切换到另一个线程)

程序由指令数据组成,但这些指令要运行,数据要读写,就必须将指令加载至CPU,数据加载至内存。在指令运行过程中还需要用到磁盘、网络等设备,进程就是用来加载指令、管理内存、管理IO的。
当一个程序被运行,从磁盘加载这个程序的代码至内存,这时就开启了一个**进程**

一个线程就是一个指令流,将指令流中的一条条指令以一定的顺序交给CPU执行。一个进程之内可以分为一到多个线程

core → 线程1[指令1,指令2,指令3…] 线程2[指令1,指令2,指令3…]

并行和并发的区别?

现在都是多核CPU,在多核CPU下

  • 并发是同一时间应对多件事情的能力,多个线程轮流使用一个或多个CPU
  • 并行是同一时间动手做多件事的能力,4核CPU同时执行4个线程

==单核CPU== → 单核CPU下线程实际还是串行执行的

  • 操作系统中有一个组件叫做任务调度器,将cpu的时间片(windows下时间片最小约为15ms)分给不同的程序使用,只是由于cpu在线程间(时间片很短)的切换非常快,人类感觉是同时运行的
  • 每个时间片只能用有一个线程被执行
  • 总结一句话:**微观串行,宏观并行**
  • 一般会将这种线程轮流使用CPU的做法称为并发(concurrent)
CPU 时间片1 时间片2 时间片3
core 线程1 线程2 线程3

==多核CPU== → 每个核(core)都可以调度运行线程,这个时候线程是可以并行的

CPU 时间片1 时间片2 时间片3 时间片4
core1 线程1 线程2 线程3 线程3
core2 线程2 线程4 线程2 线程4

并发 (concurrent) 是同一时间应对 (dealing with) 多件事情的能力
并行 (parallel) 是同一时间动手做 (doing) 多件事情的能力

  • 家庭主妇做饭、打扫卫生、给孩子喂奶,她一个人轮流交替做这么多件事,这就是并发【单核CPU】
  • 家庭主妇雇了个保姆,她们一起做这些事,这时既有并发,也有并行【会产生竞争,例如锅只有一个,一个人用锅时,另一个人就要等待】
  • 雇了3个保姆,一个专门做饭,一个专门打扫卫生,一个专门喂奶,互不干扰,这就是并行

创建线程的方式有哪些?

  • 继承Thread类,重写run方法
public class MyThread extends Thread{
    @Override
    public void run(){
        sout("MyThread...run...");
    }
    public static void main(String[] args){
        // 创建MyThread对象
        MyThread t1 = new MyThread();
        MyThread t2 = new MyThread();
        
        // 调用start方法启动线程
        t1.start();
        t2.start();
    }
}
  • 实现Runnable接口,重写run方法
public class MyRunnable implements Runnable {

    @Override
    public void run() {
        // 在这里编写要执行的任务
        System.out.println("线程正在执行任务...");
    }
    public static void main(String[] args) {
        // 创建MyRunnable实例
        MyRunnable myRunnable = new MyRunnable();
        
        // 创建线程并启动
        Thread t1 = new Thread(myRunnable);
        Thread t2 = new Thread(myRunnable);
        // 调用start方法启动线程
        t1.start();
        t2.start();
    }
}
  • 实现Callable< T >接口,重写call方法泛型和重写方法一致
public class MyCallable implements Callable<String> {
 @Override
    public String call() throws Exception {
        sout(Thread.currentThread().getName());
        return "ok";
    }
     public static void main(String[] args) {
        // 创建MyCallable实例
        MyCallable myCallable = new MyCallable();
        
        // 使用FutureTask来包装Callable对象
        FutureTask<String> ft = new FutureTask<String>(myCallable);
        
        // 创建并启动线程
        Thread t1 = new Thread(ft);
        t1.start();
        // 调用ft的get方法获取执行结果
        String result = ft.get();
        sout(result)
    }
}
  • 线程池创建线程 (项目中使用的方式)
public class MyExecutors implements Runnable{
    @Override
    public void run(){
        sout("MyRunnable...run...");
    }
    public static void main(String[] args){
        // 创建线程池对象
        ExecutorService threadPool = Executors.newFixedThreadPool(3);
        threadPool.submit(new MyExecutors()); 
        //submit用来提交线程
        
        // 关闭线程池
        threadPool.shutdown();
    }
}
刚刚你说过,使用runnable和callable都可以创建线程,它们有什么区别呢?
  • Runnable接口run方法没有返回值
  • Callable接口call方法有返回值,要结合FutureTask配合可以用来获取异步执行的结果

FutureTaskFuture 的实现类,它可以包装一个 CallableRunnable 对象,并允许我们在任务执行完毕后获取执行结果或取消任务。

FutureTask 可以在子线程中异步执行任务,而主线程可以通过调用 FutureTask.get() 方法获取任务执行的结果。

  • Callable接口的call()方法允许抛出异常;而Runnabble接口的run()方法的异常只能在内部消化,不能继续上抛
import java.util.concurrent.Callable;
import java.util.concurrent.FutureTask;

public class CallableExample {

    public static void main(String[] args) throws Exception {
        // 创建一个Callable任务
        Callable<Integer> task = new Callable<Integer>() {
            @Override
            public Integer call() throws Exception {
                System.out.println("Task is running in the background...");
                // 模拟耗时操作
                Thread.sleep(2000);
                return 42; // 返回计算结果
            }
        };

        // 创建FutureTask对象,包装Callable任务
        FutureTask<Integer> futureTask = new FutureTask<>(task);

        // 启动线程执行FutureTask
        Thread thread = new Thread(futureTask);
        thread.start();

        // 主线程可以做一些其他工作
        System.out.println("Main thread is doing something else...");

        // 获取异步执行结果,阻塞直到任务完成
        Integer result = futureTask.get(); // 这会阻塞主线程直到获取到结果
        System.out.println("Task result: " + result); // 打印任务执行结果
    }
}

在启动线程的时候,可以使用run方法吗?run()和start()有什么区别?

start()是开启一个线程 run()跟开启普通方法一样

start():用来启动线程,通过该线程调用run方法执行run方法中所定义的逻辑代码。start方法只能被调用一次
run():封装了要被线程执行的代码,可以被调用多次

线程包括哪些状态,状态之间是如何变化的?

状态:
新建New、可运行Runnable、阻塞Blocked、等待Waiting、时间等待Timed_waiting、终止Terminated

线程状态之间如何变化:

  • 创建线程对象是新建状态
  • 调用了start()方法转变为可执行状态
  • 线程获取到了CPU的执行权,执行结束是终止状态
  • 在可执行状态的过程中,如果没有获取CPU的执行权,可能会切换其他状态
    • 如果没有获取锁(synchronized或lock) 进入阻塞状态,获得锁再切换为可执行状态
    • 如果线程调用了wait()方法进入等待状态,其他线程调用notify()唤醒后可转换为可执行状态
    • 如果线程调用了sleep(50)方法,进入计时等待状态,到时间后可切换为可执行状态
Thread.java
public enum State {
    /**
     * 新建状态。线程已经被创建,但尚未启动。
     */
    NEW,

    /**
     * 可运行状态。线程在JVM中是可运行的,这并不意味着它一定在运行,它可能在等待其他线程或操作系统的资源。
     */
    RUNNABLE,

    /**
     * 阻塞状态。线程正在等待监视器锁,以进入一个同步块/方法,或者在调用Object.wait后等待重新进入同步块/方法。
     */
    BLOCKED,

    /**
     * 等待状态。线程在等待另一个线程执行特定操作。例如,一个线程调用了Thread.join,它在等待指定的线程终止。
     */
    WAITING,

    /**
     * 超时等待状态。线程在等待另一个线程执行特定操作,但它设置了超时时间。如果线程在指定时间内没有等待到所需条件,它将自动返回。
     */
    TIMED_WAITING,

    /**
     * 终止状态。线程已经完成了执行。
     */
    TERMINATED;
}

新建T1、T2、T3三个线程,如何保证它们按顺序执行?

可以使用线程中的join方法解决
join() 等待线程运行结束

t.join() 阻塞调用此方法的线程进入timed_waiting 直到线程t执行完毕后,此线程再继续执行
Thread t1 = new Thread(()->{
    sout("t1");
});
Thread t2 = new Thread(()->{
    try{
        t1.join();
    }catch(InterruptedException e){
        e.printStackTrance();
    }
    sout("t2");
})
Thread t3 = new Thread(()->{
    try{
        t2.join();
    }catch(InterruptedException e){
        e.printStackTrance();
    }
    sout("t3");
});
// 启动线程
t1.start();
t2.start();
t3.start();

notify() 和 notifyAll() 有什么区别?

  • notifyAll:唤醒所有wait的线程
  • notify:只随机唤醒一个wait线程

java中wait和sleep方法有什么区别?wait要和synchronized一起使用

共同点

wait(),wait(long)和sleep(long)的效果都是让当前线程暂时放弃CPU的使用权,进入阻塞状态

不同点
  • 方法归属不同

    • sleep(long)是Thread的静态方法
    • 而wait(),wait(long)都是Object的成员方法,每个对象都有
  • 醒来时机不同

    • 执行sleep(long)和wait(long)的线程都会在等待相应毫秒后醒来
    • wait(long)和wait()还可以被notify唤醒,wait()如果不唤醒就一直等下去
    • 它们都可以被打断唤醒
  • 锁特性不同【重点】

    • wait方法的调用必须先获取wait对象的锁,而sleep则无此限制
    • wait方法执行后会释放锁对象,允许其他线程获得该锁对象 (我放弃cpu,但你们还可以用)
    • 而sleep如果在synchronized代码块中执行,并不会释放锁对象 (我放弃cpu,你们也用不了)

如何停止一个正在运行的线程?

有三种方式可以停止线程
  • 使用退出标志,使线程正常退出,也就是当run方法完成后线程终止
  • 使用stop方法强行终止(不推荐,方法已作废)
  • 使用interrupt方法中断线程
    • 打断阻塞的线程(sleep, wait, join)的线程,线程会抛出InterruptedException异常
    • 打断正常的线程,可以根据打断状态来标记是否退出线程

synchronized关键字的底层原理?底层:Monitor

  • synchronized【对象锁】采用互斥的方式让同一时刻至多只有一个线程能持有【对象锁】
  • 它的底层由monitor实现的,monitor**是jvm级别的现象(C++实现)**,线程获得锁需要使用对象(锁)关联monitor
  • 在monitor内部有三个属性,分别是owner、entrylist、waitset
    • owner是关联的获得锁的线程,并且只能关联一个线程;
    • entrylist关联的是处于阻塞状态的线程;
    • waitset关联的是处于Waiting状态的线程;

synchronized关键字的底层原理—进阶

Monitor实现的锁属于重量级锁,你了解过锁升级吗?

一旦锁发生了竞争,都会升级为重量级锁

Java中的synchronized有偏向锁、轻量级锁、重量级锁三种形式,分别对应了锁只被一个线程持有、不同线程交替持有锁、多线程竞争锁三种情况。

描述
重量级锁 底层使用的Monitor实现,里面涉及到了用户态和内核态的切换、进程的上下文切换,成本较高,性能比较低 【有多个线程来抢】
轻量级锁 线程加锁的时间是错开的(也就是没有竞争)可以使用轻量级锁来优化。轻量级修改了对象头的锁标志,相对重量级锁性能提升很多。每次修改都是CAS操作,保证原子性
偏向锁 一段很长的时间内都只被一个线程使用锁,可以使用了偏向锁,在第一次获得锁时,会有一个CAS操作,之后该线程再获取锁,只需要判断mark word中是否是自己的线程id即可,而不是开销相对较大的CAS命令
  • Monitor实现的锁属于重量级锁,里面涉及到了用户态权限低和内核态权限高的切换、进程的上下文切换,成本较高,性能比较低
  • 在JDK1.6引入了两种新型锁机制:偏向锁和轻量级锁,它们的引入是为了解决在没有多线程竞争或基本没有竞争的场景下使用传统锁机制带来的性能开销问题
Monitor重量级锁

每个Java对象都可以关联一个Monitor对象,如果使用 synchronized 给对象上锁(重量级)之后,该对象头的Mark Word中就被设置指向Monitor对象的指针

加锁流程
  • 在线程栈中创建一个Lock Record,将其obj字段指向锁对象。
  • 通过CAS指令将Lock Record的地址存储在对象头的mark word中,如果对象处于无锁状态则修改成功,代表该线程获得了轻量级锁。
  • 如果是当前线程已经持有该锁了,代表这是一次锁重入。设置Lock Record第一部分为nul,起到了一个重入计数器的作用。
  • 如果CAS修改失败,说明发生了竞争,需要膨胀为重量级锁。
解锁过程
  • 遍历线程栈,找到所有obj字段等于当前锁对象的Lock Record.
  • 如果Lock Record的Mark Word为null,代表这是一次重入,将obj设置为null后continue。
  • 如果Lock Record的 Mark Word不为nul,则利用CAS指令将对象头的mark word恢复成为无锁状态。如果失败则膨胀为重量级锁。
偏向锁性能比轻量级锁好
  • 轻量级锁在没有竞争时(就自己这个线程)每次重入仍然需要执行 CAS 操作。
  • Java6 中引入了偏向锁来做进一步优化:只有第一次使用 CAS 将线程 ID 设置到对象的 Mark Word 头,之后发现这个线程 ID 是自己的就表示没有竞争,不用重新 CAS。以后只要不发生竞争,这个对象就归该线程所有

你谈谈JMM (Java内存模型)

Java内存模型
  • JMM(Java Memory Model)Java内存模型,定义了共享内存多线程程序读写操作的行为规范,通过这些规则来规范对内存的读写操作从而保证指令的正确性
  • JMM把内存分为两块,一块是私有线程的工作区域(工作内存),一块是所有线程的共享区域(主内存)
  • 线程跟线程之间是相互隔离,线程跟线程相互需要通过主内存

CAS你知道吗?

  • CAS全称是:Compare And Swap(比较再交换),它体现的一种乐观锁的思想,在无锁情况下保证线程操作共享数据的原子性。
  • CAS使用到的地方很多:AQS框架、AtomicXXX类
  • 在操作共享变量的时候使用自旋锁,效率上更高一些
  • CAS的底层是调用的Unsafe类中的方法,都是操作系统提供的,其他语言实现

在JUC(java.util.concurrent)包下实现的很多类都用到了CAS操作

  • AbstractQueuedSynchronizer (AQS框架)
  • AtomicXXX类

乐观锁和悲观锁的区别?

谈一谈你对volatile的理解?

一旦一个共享变量(类的成员变量、类的静态成员变量)被volatile修饰之后,那么就具备了两层语义:

  • 保证线程间的可见性

    用volatile修饰共享变量,能够防止编译器等优化发生,让一个线程对共享变量的修改对另一个线程可见

  • 禁止进行指令重排序

    用volatile修饰共享变量会在读、写共享变量时加入不同的屏障,阻止其他读写操作越过屏障,从而达到阻止重排序的效果

什么是AQS?

  • 是多线程中的队列同步器。是一种锁机制,它是做为一个基础框架使用的,像ReentrantLock、Semaphore都是基于AQS实现的
  • AQS内部维护了一个**先进先出的双向队列**,队列中存储的排队的线程
  • 在AQS内部还有一个属性state,这个state就相当于是一个资源,默认是0(无所状态),如果队列中有一个线程修改成功了state为1,则当前线程就相当于获取了资源。
  • 在对state修改的时候使用CAS(compare and swap)操作,保证多个线程修改的情况下原子性

AQS(AbstractQueuedSynchronizer),即抽象队列同步器。它是构建锁或者其他同步组件的基础框架

AQS与Synchronized的区别
synchronized AQS
关键字,C++语言实现 java语言实现
悲观锁,自动释放锁 悲观锁,手动开启和关闭
锁竞争激励都会升级为重量级锁,性能差 锁竞争激烈的情况下,提供了多种解决方案
AQS常见的实现类
  • ReentrantLock 阻塞式锁
  • Semaphore 信号量
  • CountDownLatch 倒计时锁

ReentrantLock [rɪ’entrənt]lock 的实现原理?[关联HashMap线程不安全需加锁(synchronized或ReentrantLock)]

ReentrantLock主要利用CAS+AQS队列CompareAndSwap+AbstractQueuedSynchronized来实现。**它支持公平锁和非公平锁,两者的实现类似构造方法接受一个可选的公平参数(默认非公平锁),当设置为true时,表示公平锁,否则为非公平锁**。公平锁的效率往往没有非公平锁的效率高,在许多线程访问的情况下,公平锁表现出较低的吞吐量。

ReentrantLock翻译过来是可重入锁,相对于synchronized它具备以下特点:

  • 可中断synchronized不可中断
  • 可设置超时时间没有获得锁时只能进入等待[没有获取锁可以放弃锁]
  • 可以设置公平锁synchronized只有非公平锁[也支持非公平锁]
  • 支持多个条件变量
  • 与synchronized一样,都支持重入

synchronized和Lock有什么区别?

  • 语法层面

synchronized是关键字,源码在jvm中,用c++语言实现
Lock是接口,源码由jdk提供,用java语言实现
使用synchronized时,退出同步代码块锁会自动释放,而使用Lock时,需要手动调用unlock方法释放锁

  • 功能层面

二者均属于悲观锁、都具备基本的互斥、同步、锁重入功能
Lock提供了许多synchronized不具备的功能,例如公平锁、可打断、可超时、多条件变量
Lock有适合不同场景的实现,如ReentrantLock、ReentrantReadWeiteLock(读写锁)

死锁产生的条件是什么

死锁:一个线程需要同时获取多把锁,这时就容易发生死锁

如何进行死锁诊断 ?

当程序出现了死锁现象,我们可以使用jdk自带的工具:jpsjstack

  • jps:输出JVM中运行的进程状态信息
  • jstack:查看java进程内线程的堆栈信息

JVM中也有死锁,jvm没有超时机制不会解决 可以查看命令打印堆栈信息可以查看哪里产生死锁

你可以使用jstack命令来打印指定进程ID的Java堆栈跟踪信息。这个命令可以帮助你分析线程的状态

  1. 首先,找到你的Java进程ID(PID)。你可以使用jps命令来列出所有正在运行的Java进程及其PID。

    jps
    
  2. 使用jstack命令打印出该Java进程的堆栈跟踪。

    jstack -l <PID>
    

    <PID>替换为实际的进程ID。

  3. 查找堆栈跟踪中的”DEADLOCK”关键字。jstack会自动检测死锁并在输出中报告。

其他解决工具,可视化工具
  • jconsole

用于对jvm的 内存,线程,类 的监控,是一个基于jmx的GUI性能监控工具
打开方式:java安装目录 bin目录下 直接启动 jconsole.exe就行

  • VisualVM:故障处理工具

能够监控线程,内存情况,查看方法的cpu时间和内存中的对象,已被GC的对象,反向查看分配的堆栈
打开方式:java安装目录 bin目录下 直接启动 jvisualvm.exe就行

死锁:两个线程争夺两个资源的时候 1线程拿到a 想拿b 2线程拿到了b 想拿a
四个原因互斥条件 请求保持 不可剥夺 循环等待
产生死锁的四个因素 同时满足才会死锁 想要解决死锁 需要打破其中一个原因就行

  1. 互斥条件(Mutual Exclusion):资源不能被多个线程同时使用。即某个资源在一段时间内只能由一个线程占用,其他线程必须等待该资源被释放后才能使用。
  2. 持有和等待条件(Hold and Wait):线程至少持有一个资源,并且正在等待获取额外的资源,而该资源又被其他线程持有。
  3. 非抢占条件(No Preemption):已经分配给某个线程的资源在该线程完成任务前不能被抢占,即只能由线程自己释放。
  4. 循环等待条件(Circular Wait):存在一种线程资源的循环等待链,每个线程都在等待下一个线程所持有的资源。

在实际操作中,以下是一些打破死锁的具体方法:银行家算法可以避免死锁

  • 资源分配图:使用资源分配图来检测循环等待条件,并在检测到循环时采取措施。
  • 锁排序:确保所有线程以相同的顺序获取锁,从而避免循环等待。
  • 超时机制:线程在请求资源时设置超时时间,如果超过时间未获得资源,则放弃当前任务并释放已持有的资源。
  • 死锁检测算法:运行死锁检测算法,如银行家算法,来检测系统中的死锁,并在必要时采取措施。
  • 线程中断:允许系统或其他线程中断正在等待资源的线程。
  • 回滚操作:如果检测到死锁,可以让某些线程回滚它们的工作,并释放资源,从而打破死锁。

MySQL是不会有死锁的 自身会检测 [让后面的超时释放回滚]
在分布式事务 线程1拿着资源a是数据库1 线程2拿着资源b是数据库2
JVM中也有死锁,jvm没有超时机制不会解决 可以查看命令打印堆栈信息可以查看哪里产生死锁

你可以使用jstack命令来打印指定进程ID的Java堆栈跟踪信息。这个命令可以帮助你分析线程的状态

  1. 首先,找到你的Java进程ID(PID)。你可以使用jps命令来列出所有正在运行的Java进程及其PID。

    jps
    
  2. 使用jstack命令打印出该Java进程的堆栈跟踪。

    jstack <PID>
    

    <PID>替换为实际的进程ID。

  3. 查找堆栈跟踪中的”DEADLOCK”关键字。jstack会自动检测死锁并在输出中报告。

聊一下ConcurrentHashMap

ConcurrentHashMap是一种线程安全的高效Map集合
底层数据结构

  • JDK1.7底层采用分段的数组+链表实现

  • JDK1.8采用的数数据结构跟HashMap1.8的结构一样,数组+链表/红黑二叉树

    在JDK1.8中,放弃了Segment臃肿的设计,数据结构跟HashMap的数据结构是一样的:数组+红黑树+链表,采用CAS + Synchronized来保证并发安全进行实现

    • CAS控制数组节点的添加
    • synchronized只锁定当前链表或红黑二叉树的首节点,只要hash不冲突,就不会产生并发的问题,效率得到提升

加锁的方式

  • JDK1.7采用Segment分段锁,底层使用的是ReentrantLock
  • JDK1.8采用CAS自旋锁添加新节点,采用synchronized锁定链表或红黑二叉树的首节点,相对Segment分段锁粒度更细,性能更好

导致并发程序出现问题的根本原因是什么 (Java程序中怎么保证多线程的执行安全)

Java并发编程三大特性
  • 原子性synchronized、lock:一个线程在CPU中操作不可暂停,也不可中断,要么执行完成,要么不执行
int ticketNum = 10;
public void getTicket(){
    if(ticketNum <= 0){
        return;
    }
    sout(Thread.currentThread().getName() + "抢到一张票,剩余:" + ticketNum);
    // 非原子性操作
    ticketNum--;
}
main{
    TicketDemo demo = new TicketDemo();
    for(int i = 0; i < 20; i++){
        new Thread(demo::getTicket).start();
    }
}
不是原子操作,怎么保证原子操作呢?
  1. synchronized:同步加锁
  2. JUC里面的lock:加锁

  • 可见性volatile、synchronized、lock
内存可见性:让一个线程对共享变量的修改对另一个线程可见
public class VolatileDemo{
    private static boolean flag = false;
    public static void main(String[] args) throws InterruptedException{
        new Thread(()->{
            while(!flag){
                sout("第一个线程执行完毕...");
            }
        }).start();
        Thread.sleep(100);
        new Thread(()->{
            flag = true;
            sout("第二个线程执行完毕...");
        }).start();
    }
}

解决方案:synchronized、volatile、LOCK

volatile:加在共享变量上面即可 → private static volatile boolean flag = false;

  • 有序性volatile

指令重排:处理器为了提高程序运行效率,可能会对输入代码进行优化,它不保证程序中各个语句的执行先后顺序同代码中的顺序一致,但是它会保证程序最终执行结果和代码顺序执行的结果是一致的

int x;
int y;
@Actor
public void actor1(){
    x = 1;
    y = 1;
}
@Actor
public void actor2(II_Result r){
    r.r1 = y;
    r.r2 = x;
}

解决办法:在前面加上volatile

说一下线程池的核心参数

为什么要创建线程池 因为每次创建线程的时候就要占用一定的内存空间 无限创建线程回浪费内存严重会导致内存溢出
CPU有限的同一时刻只能同时处理一个线程 大量线程来的话就没有线程权 会造成线程等待 造成大量线程在之间切换也会导致性能变慢

在这个例子中,我们创建了一个线程池,核心线程数为5,最大线程数为10,如果线程池中的线程数大于核心线程数,则空闲线程在60秒后会被终止。工作队列使用ArrayBlockingQueue,其容量为100。

import java.util.concurrent.ArrayBlockingQueue;
import java.util.concurrent.ThreadPoolExecutor;
import java.util.concurrent.TimeUnit;

public class ThreadPoolExample {
    public static void main(String[] args) {
        // 核心线程数
        int corePoolSize = 5;
        // 最大线程数 = (核心线程 + 救急线程的最大数目)
        int maximumPoolSize = 10;
        // 线程池中超过 corePoolSize 数量的空闲线程最大存活时间
        long keepAliveTime = 60L;
        // 时间单位 - 救急线程的生存时间单位,如秒、毫秒等
        TimeUnit unit = TimeUnit.SECONDS;
        // 工作队列,用于存放提交的任务 当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务
        ArrayBlockingQueue<Runnable> workQueue = new ArrayBlockingQueue<>(100);
        // 线程工厂 - 可以定制线程对象的创建,例如设置线程名字、是否是守护线程等
        ThreadFactory threadFactory = new ThreadFactory;
        // 拒绝策略 - 当所有线程都繁忙,workQueue也繁忙时,会触发拒绝策略
        RejectedExecutionHandler handler = new RejectedExecutionHandler;
        
        // 创建线程池
        ThreadPoolExecutor threadPoolExecutor = new ThreadPoolExecutor(
                corePoolSize,
                maximumPoolSize,
                keepAliveTime,
                unit,
                workQueue
        );

        // 示例:向线程池提交任务  threadPoolExecutor.submit()/.execute()
        for (int i = 0; i < 20; i++) {
            int taskNumber = i;
            threadPoolExecutor.execute(() -> {
                System.out.println("Executing task " + taskNumber);
                // 模拟任务执行时间
                try {
                    Thread.sleep(1000);
                } catch (InterruptedException e) {
                    e.printStackTrace();
                }
            });
        }

        // 关闭线程池
        threadPoolExecutor.shutdown();
    }
}

一开始new的时候没有 是空的。先当一个任务提交给线程池时,线程池首先检查当前运行的线程数是否达到核心线程数。如果没有达到核心线程数,线程池会创建一个新的线程来执行任务。如果已经达到核心线程数,线程池会将任务放入工作队列中等待执行。如果工作队列满了,并且当前运行的线程数小于最大线程数,,线程池会创建新的线程来执行任务。如果工作队列满了,并且当前运行的线程数等于最大线程数,线程池会根据拒绝策略

  • 丢弃任务抛出异常
  • 丢弃任务不抛弃异常
  • 丢弃队列最前面的任务,然后重新提交被拒绝的任务、
  • 由主线程处理该任务来处理无法执行的任务。【线程池无法起到异步问题】
    • 问题:想继续异步且不丢弃任务怎么办?
    • 把这个业务先存到别的地方 ↓↓↓
  • 自定义拒绝策略 自己写实现类实现拒绝策略 可以先存到mysql到时候再慢慢搞

线程池中有哪些常见的阻塞队列

线程工厂可以设置创建的属性
守护线程:主线程(main)一天不死 守护线程不死 [同生共死]
非守护线程:new一个就是 [不是同生共死]

workQueue - 阻塞队列常用的队列:当没有空闲核心线程时,新来任务会加入到此队列排队,队列满会创建救急线程执行任务

  1. ArrayBlockingQueue: 基于数组结构的有界阻塞队列,此队列按照先进先出(FIFO)的原则对元素进行排序。创建时需要指定容量。【底层是数组 随机读写的 **时间复杂度O(1)**】
    • 开辟新空间创建新数组 把旧数组的数据迁移过去 new ArrayList为空 需要add才可以 扩容是+10 取1.5倍
    • 高并发不会超过某个值 数组不会涉及到扩容 性能会好一些【比较稳定能预估】
    • new的时候不用指定长度
  2. LinkedBlockingQueue: 基于链表结构的有界阻塞队列(如果不指定容量,则默认为Integer.MAX_VALUE,即视为无界)。按照先进先出的原则排序元素。【随机读写的 时间复杂度O(n) 随机读写快 查询慢 是通过二分查找定位到下标元素(通过下标访问数组和链表) 只会走一次二分查找】
    • 读中间的慢 读头尾快
    • 新增元素不涉及到数组的迁移
    • 一般情况下高并发推荐使用,因为队列高级数据结构(可以用数组和链表的实现 由于底层数据结构不同)的特性是先进先出,链表不涉及到数组的扩容 末尾的最快是O(1)【不稳定】
    • new的时候可指定长度是最大链表的长度
    • 不可指定长度 [有界队列&无界队列] → 可能产生JVM的OOM
  3. DelayedWorkQueue:是一个优先级队列,它可以保证每次出队的任务都是当前队列中时间最靠前的
  4. SynchronousQueue:不存储元素的阻塞队列,每个插入操作都必须等待一个移出操作
ArrayBlockingQueue LinkedBlockingQueue不给值默认最大值
强制有界 默认无界,支持有界
底层是数组 底层是链表
提前初始化Node数组 是懒惰的,创建节点的时候添加数据
Node需要是提前创建好的 入队会生成新Node
一把锁 两把锁(头尾)可以一边入队,一边出队

如何确定核心线程数

① 高并发、任务执行时间短 → (CPU核数 + 1),减少线程上下文的切换
② 并发不高、任务执行时间长

  • IO密集型任务 → (CPU核数 * 2 + 1)
  • 计算密集型任务 → (CPU核数 + 1)

并发高、业务执行时间长,解决这种类型任务的关键不在于线程池而在于整体架构的设计,看看这些业务里面某些数据是否能做缓存是第一步,增加服务器是第二步,至于线程池的设置参考②

  • IO密集型任务:文件读写、DB读写、网络请求等 核心线程数大小设置为2N+1
  • CPU密集型任务:计算型代码、Bitmap转换、Gson转换等 核心线程数大小设置为N+1
// 查看机器的CPU核数
public static void main(String[] args){
    // 查看机器的CPU核数
    System.out.println(Runtime.getRuntime().avaliableProcessors());
}

线程池的种类有哪些

java.util.concurrent.Executors类中提供了大量创建线程池的静态方法,常见的有四种

① 创建使用固定线程数的线程池

适用于任务已知,相对耗时的任务

public static ExecutorService newFixedThreadPool(int nThreads){
    return new ThreadPoolExecutor(nThreads, nThreads,0L,TimeUnit.MILLISECONDS.new LinkedBlockingQueue<Runnable>)
}
  • 核心线程数与最大线程数一样,没有救急线程 = 最大线程数 - 核心线程数
  • 阻塞队列是LinkedBlockingQueue,最大容量为Integer.MAX_VALUE
② 单线程化的线程池它只会用唯一的工作线程来执行任务,保证所有任务按照指定顺序(FIFO)执行→[先进先出]

适用于按照顺序执行的任务

public static ExecutorService newSingleThreadExecutor(){
    return new FinalizableDelegatedExecutorService(new ThreadPoolExecutor(1,1,0L,TimeUnit.MILLISECONDS,new LinkedBlockingQueue<Runnable>()));
}
  • 核心线程数和最大线程数都是1
  • 阻塞队列是LinkedBlockingQueue,最大容量为Integer.MAX_VALUE
③ 可缓存线程池
public static ExecutorService newCachedThreadPool(){
    return new ThreadPoolExecutor(0, Integer.MAX_VALUE,60L,TimeUnit.SECONDS,new SynchronousQueue<Runnable>());
}
  • 核心线程数为0
  • 最大线程数是Integer.MAX_VALUE
  • 阻塞队列是SynchronousQueue: 不存储元素的阻塞队列,每个插入操作都必须等待一个移出操作
④ 提供了 延迟周期执行 功能的ThreadPoolExecutor
public ScheduledThreadPoolExecutor(int corePoolSize){
    super(corePoolSize, Integer.MAX_VALUE,0,NANOSECONDS,new DelayedWorkQueue());
}

为什么不建议使用Executors创建线程池?

参考阿里开发手册

【强制】 线程池不允许使用Executors去创建,而是通过ThreadPoolExecutor的方式,这样的处理方式让写的同学更加明确线程池的运行规则,规避资源耗尽的风险。
说明:Executors返回的线程池对象的弊端如下:
1. FixedThreadPool 和 SingleThreadPool:
允许的请求队列长度为 Integer.MAX_VALUE,可能会堆积大量的请求,从而导致OOM
2. CachedThreadPool:
允许的创建线程数量为 Integer.MAX_VALUE,可能会创建大量的线程,从而导致OOM

线程池的使用场景①:ES数据批量导入

CountDownLatch

CountDownLatch(闭锁/倒计时锁)用来进行线程同步协作,等待所有线程完成倒计时时(一个或多个线程,等待其他多个线程完成某件事情之后才能执行)

  • 其中构造参数用来初始化等待计数值
  • await()用来等待计数归零
  • countDown()用来让计数减一
多线程使用场景一 (es数据批量导入)

在我们项目上线之前,我们需要把数据库中的数据一次性的同步到es索引库中,但是当时的数据好像是1000万左右一次性读取数据肯定不行(oom异常),当时我就想到可以使用线程池的方式导入,利用CountDownLatch来控制就能避免一次性加载过多,防止内存溢出

DB(一千万) → 线程池(CountDownLatch) → Elasticearch

       批量导入  →  查询总条数   →       DB
                      ↓               ↑          批量导入到ES中     →   ES
(固定每页2000条)        计算总页数            ↑  (countDownLatch.countDown())
                        ↓               ↑                ↑
(总页数)         CountDownLatch        ↑                ↑
                        ↓               ↑                ↑
                分页查询文章数据 → [查询当前页的文章 → 创建任务批量导入ES → 提交到线程池执行]循环
                                             (文章列表, countDownLatch)
                                                          ↓
                                                countDownLatch.await()

线程池的使用场景②:数据汇总

  • 在一个电商网站中,用户下单之后,需要查询数据,数据包含了三部分:订单信息、包含的商品、物流信息;这三块信息都在不同的微服务中进行实现的,我们如何完成这个业务呢?
    • 在实际开发的过程中,难免需要调用多个接口来汇总数据,如果所有接口(或部分接口)的没有依赖关系,就可以使用线程池+future来提升性能
      [统计的图文发布量、点赞数量、收藏数量、评论数量若不在同一台微服务下 或者 部分没有依赖关系]

线程池的使用场景③:异步调用

为了避免下一级方法影响上一级方法(性能考虑),可使用异步线程调用下一个方法(不需要下一级方法返回值),可以提升方法相应时间

如何控制某个方法允许并发访问线程的数量

Semaphore信号量,是JUC包下的一个工具类,底层是AQS,我们可以通过其限制执行的线程数量
适用场景
通常用于那些资源有明确访问数量限制的场景,常用于限流

Semaphore使用步骤
  • 创建Semaphore对象,可以给一个容器
  • semaphore.acquire():请求一个信号量,这时候的信号量个数 -1 (一旦没有可使用的信号量,也即信号量个数变为负数时,再次请求的时候就会阻塞,直到其他线程释放了信号量)
  • semaphore.release():释放一个信号量,此时信号量个数 +1

谈一谈你对ThreadLocal的理解

  • ThreadLocal 可以实现【资源对象】的线程隔离,让每个线程各用各的【资源对象】避免争用引发的线程安全问题
  • ThreadLocal 同时实现了线程内的资源共享
  • 每个线程内有一个 ThreadLocalMap 类型的成员变量,用来存储资源对象
    • 调用 set 方法,就是以 ThreadLocal 自己作为 key,资源对象作为 value,放入当前线
      程的 ThreadLocalMap 集合中
    • 调用 get 方法,就是以 ThreadLocal 自己作为 key,到当前线程中査找关联的资源值
    • 调用remove 方法,就是以 ThreadLocal 自己作为 key,移除当前线程关联的资源值
  • ThreadLocal内存泄漏问题ThreadLocalMap 中的key是弱引用,值为强引用; key会被Gc释放内存,关联 value的内存并不会释放。建议主动remove 释放 key,value
ThreadLocal概述

ThreadLocal是多线程中对于解决线程安全的一个操作类,它会为每个线程都分配一个独立的线程副本从而解决了变量并发访问冲突的问题。ThreadLocal同时实现了线程内的资源共享

案例:使用JDBC操作数据库时,会将每一个线程的Connection放入各自的ThreadLocal中,从而保证每个线程都在各自的 Connection 上进行数据库的操作,避免A线程关闭了B线程的连接。

ThreadLocal基本使用

  • set(value) 设置值
  • get() 获取值
  • remove() 清除值
ThreadLocal的实现原理 & 源码解析

ThreadLocal本质来说就是一个线程内部存储类,从而让多个线程只操作自己内部的值,从而实现线程数据隔离

JVM相关面试题

什么是JVM?

JVM = Java Virtual Machine 是java程序的运行环境
JVM是运行在操作系统中的 屏蔽了操作系统的差异

好处

  • 一次编码,到处运行
  • 自动内存管理,垃圾回收机制

什么是程序计数器?

程序计数器:线程私有的,内部保存的字节码的行号。用于记录正在执行的字节码指令的地址

javap -v xx.class:打印堆栈大小,局部变量的数量和方法的参数

找到Application的class文件后 → Build → Rebuild Project编译一下 → 找到该Application的class文件黄色的 → Open in → Terminal → javap -v Application.class

你能给我详细介绍Java堆吗?

线程共享的区域:主要用来保存对象实例、数组等,当堆中没有内存空间可分配给实例,也无法再扩展,则抛出OutOfMemoryError异常

  • 组成:年轻代 + 老年代
    • 年轻代被划分为三部分,Eden区和两个大小严格相同的Survivor区幸存者区
    • 老年代主要保存生命周期长的对象,一般是一些老的对象
  • jdk1.7和1.8的区别
    • 1.7中有一个永久代,存储的是类信息、静态变量、常量、编译后的代码
    • 1.8移除了永久代,把数据存储到了本地内存的元空间中,防止内存溢出

什么是虚拟机栈?

Java Virtual machine Stacks(Java虚拟机栈)

  • 每个线程运行时所需要的内存,称为虚拟机栈,先进后出
  • 每个栈由多个栈帧(frame)组成,对应着每次方法调用时所占用的内存
  • 每个线程只能有一个活动栈帧,对应着当前正在执行的那个方法

垃圾回收是否涉及栈内存?

不涉及,因为垃圾回收主要指的是堆内存
这里当栈帧弹栈后,内存就会释放

栈内存分配越大越好吗?

未必,默认的栈内存通常为1024k
栈帧过大会导致线程数变少,例如,机器总内存为512m,目前能活动的线程数则为512个,如果把栈内存改为2048k,那么能活动的栈帧就会减半

方法内的局部变量是否线程安全?

如果方法内局部变量没有逃离方法的作用范围,它是线程安全的
如果是局部变量引用了对象,并逃离方法的作用范围,需要考虑线程安全

什么情况下会导致栈内存溢出?

栈帧过多导致栈内存溢出,经典问题:递归调用
栈帧过大导致栈内存溢出

堆栈的区别是什么?

栈内存一般会用来存储局部变量和方法调用,但堆内存是用来存储Java对象和数组的的。堆会GC垃圾回收,而栈不会
栈内存是线程私有的,而堆内存是线程共有的。
两者异常错误不同,但如果栈内存或者堆内存不足都会抛出异常
栈空间不足:java.lang.StackOverFlowError。
堆空间不足:java.ang.OutOfMemoryError。

能不能解释一下方法区

  • 方法区(Method Area)是各个线程共享的内存区域
  • 主要存储类的信息、运行时常量池
  • 虚拟机启动的时候创建,关闭虚拟机时释放
  • 如果方法区域中的内存无法满足分配请求,则会抛出OutOfMemoryError: Metaspace
常量池

可以看作是一张表,虚拟机指令根据这张常量表找到要执行的类名、方法名、参数类型、字面量等信息
Terminal中执行:javap -v Application.class
可以查看字节码结构 (类的基本信息、常量池、方法定义)
当类被加载,它的常量池信息就会放入运行时常量池,并把里面的符号地址变为真实地址

你听过直接内存吗?

直接内存:并不属于JVM中的内存结构,不由JVM进行管理。是虚拟机的系统内存,常见于NIO操作时,用于数据缓冲区,它分配回收成本较高,但读写能力高。[平时的是BIO]

直接内存并不属于JVM中的内存结构,不由VM进行管理。是虚拟机的系统内存常见于 NIO 操作时,用于数据缓冲区,分配回收成本较高,但读写性能高,不受 JVM 内存回收管理

什么是类加载器,类加载器有哪些?

类加载器

JVM只会运行二进制文件,类加载器的作用就是将字节码文件加载到JVM中,从而让Java程序能够启动起来

  1. 引导类加载器(Bootstrap ClassLoader)加载JAVA_HOME/jre/lib目录下的库
    • 这是最顶层的类加载器,它用于加载Java的核心库,这些库位于<JAVA_HOME>/jre/lib目录(比如rt.jarresources.jar等),或者被-Xbootclasspath参数指定的路径中。
    • 引导类加载器是用原生代码(如C/C++)实现的,它属于JVM的一部分。
    • 它并不继承自java.lang.ClassLoader,而是由JVM自身实现。
  2. 扩展类加载器(Extension ClassLoader)加载JAVA_HOME/jre/lib/ext目录中的类
    • 它负责加载<JAVA_HOME>/lib/ext目录中,或者由系统属性java.ext.dirs指定的路径中的类库。
    • 它是sun.misc.Launcher$ExtClassLoader类的实例。
  3. 系统类加载器(System ClassLoader)用于加载classPath下的类
    • 也称为应用类加载器(Application ClassLoader),它负责加载用户类路径(Classpath)上的所有类库。
    • 系统类加载器是sun.misc.Launcher$AppClassLoader类的实例。
    • 它是程序中默认的类加载器,可以通过ClassLoader.getSystemClassLoader()方法获取。
  4. 自定义加载器(CustomizeClassLoader)自定义继承ClassLoader,实现自定义类加载规则
    • 用户还可以自定义类加载器。自定义类加载器通过继承java.lang.ClassLoader类并重写相应的方法来实现。自定义类加载器可以用于特定的需求,例如在Web容器中加载类,或者在运行时从网络或其他地方动态加载类。

什么是双亲委派模型?

加载某一个类,先委托上一级的加载器进行加载,如果上级加载器也有上级,则会继续向上委托,如果该类委托上级没有被加载,子加载器尝试加载该类

JVM为什么采用双亲委派机制?
  • 通过双亲委派机制可以避免某一个类被重复加载,当父类已经加载后则无需重复加载,保证唯一性
  • 为了安全,保证类库API不会被修改

说一下类装载的执行过程?

加载:查找和导入class文件
验证:保证加载类的准确性
准备:为类变量分配内存并设置类变量初始值
解析:把类中的符号引用转换为直接引用
初始化:对类的静态变量,静态代码块执行初始化操作
使用:JVM 开始从入口方法开始执行用户的程序代码
卸载:当用户程序代码执行完毕后,JVM便开始销毁创建的Class对象

类从加载到虚拟机中开始,直到卸载为止,它的整个生命周期包括了:加载、验证、准备、解析、初始化、使用卸载这7个阶段。其中,验证、准备和解析这三个部分统称为连接(linking)

  • 通过类的全名,获得类的二进制数据流
  • 解析类的二进制数据流为方法区内的数据结构(Java类模型)
  • 创建java.lang.Class类的实例,表示该类型。作为方法区这个类的各种数据的访问入口

对象什么时候可以被垃圾器回收

如果一个或多个对象没有任何的引用指向它了,那么这个对象现在就是垃圾,如果定位了垃圾,则有可能会被垃圾回收器回收

怎么确定什么是垃圾?
  • 引用计数法

    一个对象被引用了一次,在当前的对象头上递增一次引用次数,如果这个对象的引用次数为0,代表这个对象可回收

  • 可达性分析算法

    采用的都是通过可达性分析算法来确定哪些内容是垃圾

JVM垃圾回收算法有哪些?

  • 标记清除算法

    是将垃圾回收分为2个阶段,分别为标记清除

    • 根据可达性分析算法得出的垃圾进行标记
    • 对这些标记为可回收的内容进行垃圾回收
  • 复制算法

    将原有的内存空间一分为二,每次只用其中的一块,正在使用的对象复制到另一个内存空间中,然后将该内存空间清空,交换两个内存的角色,完成垃圾的回收;无碎片,内存使用率低

  • 标记清理算法一般用于老年代

    标记清除算法一样,将存活对象都向内存另一端移动,然后清理边界以外的垃圾,无碎片,对象需要移动,效率低

JVM的分代回收是什么?

分代收集算法

在java8时,堆被分为了两份:新生代和老年代[1:2]
对于新生代,内部又分为了三个区域,Eden区,幸存者区survivor(分成from和to)【8:1:1】

MinorGC、MixedGC、FullGC的区别是什么
  • MinorGC(youngGC)发生在新生代的垃圾回收,暂停时间短(STW)
  • MixedGC:新生代 + 老年代 部分区域的垃圾回收,G1收集器特有
  • FullGC:新生代 + 老年代 完整垃圾回收,暂停时间长(STW),应尽力避免

STW(Stop-The-World)暂停所有应用程序线程,等待垃圾回收的完成

JVM有哪些垃圾回收器?

在jvm中,实现了多种垃圾收集器,包括:

  • 串行垃圾收集器

    SerialSerial Old串行垃圾收集器,是指使用单线程进行垃圾回收,堆内存较小,适合个人电脑

    • Serial 作用于新生代,采用复制算法
    • Serial Old 作用于老年代,采用标记-整理算法垃圾回收时,只有一个线程在工作,并且java应用中的所有线程都要暂停(STW),等待垃圾回收的完成
  • 并行垃圾收集器

    Parallel New和Parallel Old是一个并行垃圾回收器,JDK8默认使用此垃圾回收器

    • Parallel New作用于新生代,采用复制算法

    • Parallel Old作用于老年代,采用标记-整理算法

      垃圾回收时,多个线程在工作,并且java应用中的所有线程都要暂停(STW),等待垃圾回收的完成。

  • CMS(并发)垃圾收集器

    CMS全称 Concurrent Mark Sweep,是一款并发的、使用标记-清除算法的垃圾回收器,该回收器是针对老年代垃圾回收的,是一款以获取最短回收停顿时间为目标的收集器,停顿时间短,用户体验就好。其最大特点是在进行垃圾回收时,应用仍然能正常运行

  • G1垃圾收集器

    作用在新生代和老年代

详细聊一下G1垃圾回收器

  • 应用于新生代和老年代,在JDK9之后默认使用G1
  • 划分成多个区域,每个区域都可以充当eden,survivor,old,humongous,其中humongous专为大对象准备
  • 采用复制算法
  • 响应时间与吞吐量兼顾
  • 分成三个阶段:新生代回收(STW)、并发标记(重新标记STW)、混合收集
  • 如果并发失败(即回收速度赶不上创建新对象速度),就会触发Full GC

强引用、软引用、弱引用、虚引用的区别

强引用:只要所有 GC Roots 能找到,就不会被回收
软引用:需要配合SoftReference使用,当垃圾多次回收,内存依然不够时候会回收软引用对象
弱引用:需要配合WeakReference使用,只要进行了垃圾回收,就会把引用对象回收
虚引用:必须配合引用队列使用,被引用对象回收时,会将虚引用入队由 Reference Handler 线程调用虚引用相关方法释放直接内存

  • 强引用:只有所有 GCRoots 对象都不通过【强引用】 引用该对象,该对象才能被垃圾回收
User user = new User();

GC Root → User对象

  • 软引用:仅有软引用引用该对象时,在垃圾回收后,内存仍不足时会再次触发垃圾回收
User user = new User();
SoftReference softReference = new SoftReference(user);

GC Root → SoftReference对象 →→虚线 User对象
一开始并不会对User对象进行回收 此时User对象就是软引用 如果内存还是不够 马上又再次进行了垃 圾回收 此时软引用的User就会被回收

  • 弱引用:仅有弱引用引用该对象时,在垃圾回收时,无论内存是否充足,都会回收弱引用对象
User user = new User();
WeakReference weakReference = new WeakReference(user)

GC Root → WeakReference对象 →→虚线 User对象

延申话题:ThreadLocal内存泄露问题

static class Entry extends WeakReference<ThreadLocal<?>>{
    Object value;
 Entry(ThreadLocal<?>k, Object v){
     super(k); // k是弱引用
     value = v; // 强引用,不会被回收
 }
}
  • 虚引用:必须配合引用队列使用,被引用对象回收时,会将虚引用入队,由 Reference Handler 线程调用虚引用相关方法释放直接内存
User user = new User();
ReferenceQueue referenceQueue = new ReferenceQueue();
PhantomReference phantomReference = new PhantomReference(user, queue);

JVM调优的参数可以在哪里设置?

  • war包部署在tomcat中设置

    修改 TOMCAT_HOME/bin/catalina.sh 文件
    D:\apache-tomcat-8.5.93\bin\catalina.sh

    # OS specific support.  $var _must_ be set to either true or false.
    JAVA_OPTS="-Xms512m -Xmx1024m"
    cygwin=false
    darwin=false
    os400=false
    hpux=false
    
  • jar包部署在启动参数设置

    通常在linux系统下直接加参数启动SpringBoot项目—VM

    nohup java -Xms512m -Xmx1024n -jar xxxx.jar --spring.profiles.active=prod &

nohup:用于在系统后台不挂断地运行命令,退出终端不会影响程序的运行
**参数&**:让命令在后台执行,终端退出后命令仍然执行

JVM调优的参数都有哪些?

对于JVM调优,主要就是调整 年轻代、老年代、元空间 的内存大小及使用的垃圾回收器类型

  • 设置堆空间大小

    设置堆的初始大小和最大大小,为了防止垃圾收集器在初始大小、最大大小之间收缩堆而产生额外的时间,通常把最大、初始大小设置为相同的值

    -Xms: 设置堆的初始化大小
    -Xmx: 设置堆的最大大小
    // 不指定单位默认为字节
    -Xms:1024
    -Xms:1024k
    

    堆内存设置多少合适?

    • 最大大小的默认值是物理内存的1/4,初始大小是物理内存的1/64【不设置的情况下】
    • 堆太小,可能会频繁的导致年轻代和老年代的垃圾回收,会产生STW,暂停用户线程
    • 堆内存大肯定是好的,存在风险,假如发生了fullgc,它会扫描整个堆空间,暂停用户线程的时间长
  • 虚拟机栈的设置

    虚拟机栈的设置:每个线程默认会开启1M的内存,用于存放栈帧、调用参数、局部变量等,但一般256K就够用。通常减少每个线程的堆栈,可以产生更多的线程,但这实际上还受限于操作系统。

    -Xss 对每个线程stack大小的调整,-Xss128k

  • 年轻代中Eden区和两个Survivor区的大小比例

    设置年轻代中Eden区和两个Survivor区的大小比例。该值如果不设置,则默认比例为8:1:1。通过增大Eden区的大小来减少YGC发生的次数,但有时我们发现,虽然次数减少了,但Eden区满的时候,由于占用的空间较大,导致释放缓慢,此时STW的时间较长,因此需要按照程序情况去调优。

    -XXSurvivorRatio=8,表示年轻代中的分配比率:survivor:eden = 2:8

  • 年前代晋升老年代阈值【默认值为15,取值范围0-15】

    -XX:MaxTenuringThreshold=threshold

  • 设置垃圾回收收集器

    通过增大吞吐量提高系统性能,可以通过设置并行垃圾回收收集器

    -XX:+UseParallelGC
    -XX:+UseParallelOldGC

    -XX:+UserG1GC

JVM调优的参数都有哪些?

  • 命令工具

    • jps 进程状态信息

    • jstack 查看进程内线程的堆栈信息产生死锁可以查看

    • jmap 查看堆栈信息[生成堆转内存快照,内存使用信息]

      jmap -head pid 显示Java堆的信息
      jmap -dump:format=b,file=heap.hprof pid
      
      • format=b 表示以hprof二进制格式存储Java堆的内存

      • file=< filename > 用于指定快照dump文件的文件名

        dump:它是我们都可以通过工个进程或系统在某一给定的时间的快照。比如在进程崩溃时,甚至是任何时候,具将系统或某进程的内存备份出来供调试分析用,dump文件中包含了程序运行的模块信息、线程信息、堆调用信息、异常信息等数据,方便系统技术人品进行错误排查

    • jhat 堆转储快照分析工具

    • jstat JVM统计监测工具[可以用来显示垃圾回收信息、类加载信息、新生代统计信息等]

      • 总结垃圾回收统计:jstat -gcutil pid
      • 垃圾回收统计:jstat -gc pid
  • 可视化工具

    • jconsole 用于对jvm的内存,线程,类的监控, 是一个可视化工具
      D:\java\jdk-11.0.20\bin\jconsole.exe
    • VisualVM 能够监控线程,内存情况只有jdk1.8有
      D:\java\jdk1.8.0_181\bin\jvisualvm.exe

Java内存泄露的排查思路?

内存泄漏通常是指堆内存,通常是指一些大对象不被回收的情况
1、通过jmap或设置jvm参数获取堆内存快照dump
2、通过工具,VisualVM去分析dump文件,VisualVM可以加载离线的dump文件
3、通过查看堆信息的情况,可以大概定位内存溢出是哪行代码出了问题
4、找到对应的代码,通过阅读上下文的情况,进行修复即可

JVM Stacks 虚拟机栈StackOverFlowError
Heap OutOfMemoryError:java heap space
Method Are/ MateSpace 方法区/元空间OutOfMemoryError: Metaspace

模拟堆空间溢出场景:-VM设置参数 → -Xmx10m

List<String> list = new ArrayList<>();
while(true){
    list.add("北京");
}
-------------------------------------------
// OutOfMemoryError:java heap space
如何排查启动闪退、运行一段时间宕机
  • 获取堆内存快照dump

    • 使用jmap命令获取运行中程序的dump文件【只有在项目运行时候才可以用】
    jmap -head pid 显示Java堆的信息
    jmap -dump:format=b,file=heap.hprof pid 【只有在项目运行时候才可以用】
    
    • 使用vm参数获取dump文件

      有的情况是内存溢出之后程序则会直接中断,而jmap只能打印在运行中的程序,所以建议通过参数的方式生成dump文件

    -XX:+HeapDumpOnOutOfMemoryError
    -XX:HeapDumpPath=/home/app/dumps/
    
  • VisualVM区分析dump文件

  • 通过查看堆内存的信息,定位内存溢出问题

CPU飙高排查方案与思路?

1.使用top命令查看占用cpu的情况
2.通过top命令查看后,可以查看是哪一个进程占用cpu较高
3.使用ps命令查看进程中的线程信息
4.使用jstack命令查看进程中哪些线程出现了问题,最终定位问题

  • 使用top命令查看占用cpu的情况哪个进程占用的cpu最高

    finalShell中输入 top

  • 查看进程中的线程信息 ps H -eo pid,tid,%cpu | gerp pid

  • jstack 查看进程内线程的堆栈信息产生死锁可以查看

    因为是十六进程所以要十进程转换十六进程
    直接linux输入 printf "%x\n" Pid
    然后就可以根据十六进制的去找哪个线程cpu占用
    之后查看文件是cat xxx

设计模式

框架中的设计模式 + 项目中的设计模式

简单工厂模式

简单工厂包含如下角色

  • 抽象产品:定义了产品的规范,描述了产品的主要特性和功能。
  • 具体产品 :实现或者继承抽象产品的子类
  • 具体工厂:提供了创建产品的方法,调用者通过该方法来获取产品。

需求:设计一个咖啡店点餐系统。
设计一个咖啡类(Coffee),并定义其两个子类(美式咖啡【AmericanCofee】和拿铁咖啡【LatteCoffee】); 再设计一个咖啡店类(CoffeeStore),咖啡店具有点咖啡的功能。

工厂方法模式完全遵循开闭原则

方法模式的主要角色:
抽象工厂(Abstract Factory):提供了创建产品的接口,调用者通过它访问具体工厂的工厂方法来创建产品。
具体工厂(ConcreteFactory):主要是实现抽象工厂中的抽象方法,完成具体产品的创建。
抽象产品(Product):定义了产品的规范,描述了产品的主要特性和功能。
具体产品(ConcreteProduct):实现了抽象产品角色所定义的接口,由具体工厂来创建,它同具体工厂之间一 一对应。

抽象工厂模式

工厂方法模式只考虑生产同等级的产品,抽象工厂可以处理等级产品的生产
抽象工厂模式是工厂方法模式的升级版本,工厂方法模式只生产–个等级的产品,而抽象工厂模式可生产多个等级的产品。一个超级工厂创建其他工厂。该超级工厂又称为其他工厂的工厂

策略模式

  • 该模式定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户
  • 它通过对算法进行封装,把使用算法的责任和算法的实现分割开来,并委派给不同的对象对这些算法进行管理

策略模式的主要角色如下:
抽象策略(Strategy)类:这是一个抽象角色,通常由一个接口或抽象类实现。此角色给出所有的具体策略类所需的接口
具体策略(Concrete Strategy)类:实现了抽象策略定义的接口,提供具体的算法实现或行为。
环境(Context)类:持有一个策略类的引用,最终给客户端调用。

策略模式—登录案例 (工厂模式 + 策略模式)

  • 什么是策略模式

    • 策略模式定义了一系列算法,并将每个算法封装起来,使它们可以相互替换,且算法的变化不会影响使用算法的客户

    • 一个系统需要动态地在几种算法中选择一种时,可将每个算法封装到策略类中

  • 案例(工厂方法+策略)

    • 介绍业务(登录、支付、解析excel、优惠等级…)
    • 提供了很多种策略,都让spring容器管理
    • 提供一个工厂:准备策略对象,根据参数提供对象

一句话总结:只要代码中有冗长的if-else 或switch 分支判断都可以采用策略模式优化

举一反三

  • 订单的支付策略(支付宝、微信、银行卡..)
  • 解析不同类型excel(xls格式、xlsx格式)
  • 打折促销(满300元9折、满500元8折、满1000元7折..)
  • 物流运费阶梯计算(5kg以下、5-10kg、10-20kg、20kg以上)

策略模式和工厂方法模拟.png

责任链模式—概述及案例

责任链模式:为了避免请求发送者与多个请求处理者耦合在一起,将所有请求的处理者通过前一对象记住其下一个对象的引用而连成一条链;当有请求发生时,可将请求沿着这条链传递,直到有对象处理它为止。

  • 抽象处理者(Handler)角色:定义一个处理请求的接口,包含抽象处理方法和一个后继连接。
  • 具体处理者(Concrete Handler)角色:实现抽象处理者的处理方法,判断能否处理本次请求,如果可以处理请求则处理,否则将该请求转给它的后继者。
  • 客户类(Cient)角色:创建处理链,并向链头的具体处理者对象提交请求,它不关心处理细节和请求的传递过程。

举一反三

  • 内容审核(视频、文章、课程)
  • 订单创建
  • 简易流程审批

常见技术场景题

单点登录这块怎么实现的?

单点登录的英文名:Single Sign On (SSO),只需要登录一次,就可以访问所有信任的应用系统

① 先解释什么是单点登录:单点登录的英文名叫做:Single SignOn(简称SSO)
② 介绍自己项目中涉及到的单点登录(即使没涉及过,也可以说实现的思路)
③ 介绍单点登录的解决方案,以JWT为例
用户访问其他系统,会在网关判断token是否有效
如果token无效则会返回401(认证失败)前端跳转到登录页面
用户发送登录请求,返回浏览器一个token,浏览器把token保存到cookie
再去访问其他服务的时候,都需要携带token,由网关统一验证后路由到目标服务

权限认证是如何实现的?

后台的管理系统,更注重权限控制,最常见的就是RBAC模型来指导实现权限
RBAC(Role-Based Access Control)基于角色的访问控制

  • 3个基础部分组成:用户、角色、权限
  • 具体实现:
    • 5张表:用户表、角色表、权限表、用户角色中间表、角色权限中间表
    • 7张表:用户表、角色表、权限表、菜单表、用户角色中间表、角色权限中间表、权限菜单中间表

张三具有什么权限呢?
流程:张三登录系统 → 查询张三拥有的角色列表 → 再根据角色查询拥有的权限

权限框架:Apache shiroSpring Security(推荐)

上传数据的安全性你们怎么控制?

主要说的是数据在网络上传输如何保证安全

使用**非对称加密(或对称加密)**,给前端一个公钥让他把数据加密后传到后台,后台负责解密后处理数据

对称加密

文件加密和解密使用相同的密钥,即加密密钥也可以用作解密密钥

你负责项目的时候遇到了哪些比较棘手的问题?怎么解决的?1+3

其次你也可以说说aop的实现,比如你们操作日志记录等,利用aop切面思想,通过环绕通知等但需封装出出个切面工具类。建议你们说说sql调优,比如商品列表页需要分页查询,但是几百万商品导致查询慢,如何优化的,这是一个

① 设计模式在项目中的应用

是为了遵循一系列的开发原则【工厂、策略、责任链】

  • 什么背景[技术问题] → 登录的例子
  • 过程[解决问题的过程]
  • 最终落地方案
② 线上BUGJVM+多线程
  • CPU飙高
  • 内存泄露
  • 线程死锁
③ 调优
  • 慢接口
  • 慢SQL
  • 缓存方案

④ 组件封装

  • 分布式锁
  • 接口幂等
  • 分布式事务
  • 支付通用
你们项目中日志怎么采集的?

我们搭建了ELK日志采集系统
介绍**ELK**的三个组件:
Elasticsearch是全文搜索分析引擎,可以对数据存储、搜索、分析
Logstash是一个数据收集引擎,可以动态收集数据,可以对数据进行过滤、分析,将数据存储到指定的位置
Kibana是一个数据分析和可视化平台,配合Elasticsearch对数据进行搜索,分析,图表化展示

  • 为什么要采集日志

日志是定位系统问题的重要手段,可以根据日志信息快速定位系统中的问题

  • 采集日志的方式有哪些
    • ELK:即ElasticSearch、LogStash、Kibanna三个软件的首字母
    • 常规采集:按天保存到一个日志文件

查看日志的命令?查看是否在线查看过日志
  • 实时监控日志的变化
    实时监控某一个日志文件的变化:tail -f xx.log
    实时监控日志文件最后100行的变化:tail -n 100 -f xx.log

  • 按照行号查询
    查询日志尾部最后100行日志:tail -n 100 xx.log
    查询日志头部开始100行日志:head -n 100 xx.log
    查询某一个日志行号区间:cat -n xx.log | tail -n +100 | head -n 100(查询100行至200行的日志)

  • 按照关键字找日志的信息
    查询日志文件中包含debug的日志行号:cat -n xx.log | grep "debug"

  • 按照日期查询日期必须在日志中出现过

    sed -n '/2025-01-14 14:22:31.070/,/ 2025-01-14 14:27:18.158/p' xx.log

  • 日志太多,处理方式

    • 分页查询日志信息:cat -n xx.log | grep "debug" | more
    • 筛选过滤后,输出到一个文件:cat -n xx.log | grep "debug" > debug.txt

上线的项目远程Debug —— 生产问题怎么排查?本地调试远程代码

已经上线的bug排查的思路:

  • 先分析日志,通常在业务中都会有日志的记录,或者查看系统日志,或者查看日志文件,然后定位问题
  • 远程debug(通常公司的正式环境(生产环境)是不允许远程debug的。一般远程debug都是公司的测试环
    境,方便调试代码)

远程debug

前提条件:远程的代码和本地的代码要保持一致

远程代码需要配置启动参数,把项目打包放到服务器后启动项目的参数:

java -jar -agentlib:jdwp=transport=dt_socket,server=y,suspend=n,address=5005 project-1.0-SNAPSHOT.jar

idea中设置远程debug,找到idea中的Edit Configurations... → 添加一个Remote JVM debug → 右侧要配置Configuration的Host → 添加上面的代码…

在项目中点debug(绿色小虫子)

访问远程服务器,在本地代码中打断点即可调试远程

怎么快速定位系统的瓶颈?

  • 压测(性能测试),项目上线之前测评系统的压力

    • 压测目的:给出系统当前的性能状况;定位系统性能瓶颈或潜在性能瓶颈
    • 指标:响应时间、QPS、并发数、吞吐量、CPU利用率、内存使用率、磁盘IO、错误率
    • 压测工具:LoadRunner、Apache Jmeter …
    • 后端工程师:根据压测的结果进行解决或调优(接口、代码报错、并发达不到要求.)
  • 监控工具、链路追踪工具,项目上线之后监控

    • 监控工具:Prometheus+Grafana
    • 链路追踪工具:skywalking、Zipkin
  • 线上诊断工具Arthas(阿尔萨斯),项目上线之后监控、排查

    • 官网:https://arthas.aliyun.com/

    • 核心功能:Arthas 是 Alibaba 开源的 Java 诊断工具,深受开发者喜爱。
      当你遇到以下类似问题而束手无策时,Arthas 可以帮助你解决:

      • 这个类从哪个jar 包加载的?为什么会报各种类相关的 Exception?

      • 我改的代码为什么没有执行到?难道是我没 commit?分支搞错了?

      • 遇到问题无法在线上 debug,难道只能通过加日志再重新发布吗?

      • 线上遇到某个用户的数据处理有问题,但线上同样无法 debug,线下无法重现!

      • 是否有一个全局视角来查看系统的运行状况?

      • 有什么办法可以监控到 JVM 的实时运行状态?

      • 怎么快速定位应用的热点,生成火焰图?

      • 怎样直接从 JVM 内查找某个类的实例?

怎么解决cpu飙高?

使用top命令查看占用cpu的情况
通过top命令查看后,可以查看是哪一个进程占用cpu较高
使用ps命令查看进程中的线程信息 使用top -H -p 进程Id [找线程哪个使用多]
记住要打印%X十六进制
使用jstack命令查看进程中哪些线程出现了问题,最终定位问题
jstack 进程PID | grep 16进制线程PID -A 20

  • 使用top命令查看占用cpu的情况哪个进程占用的cpu最高

    finalShell中输入 top

  • 查看进程中的线程信息 ps H -eo pid,tid,%cpu | gerp pid

  • jstack 查看进程内线程的堆栈信息产生死锁可以查看

    因为是十六进程所以要十进程转换十六进程
    直接linux输入 printf "%x\n" Pid
    然后就可以根据十六进制的去找哪个线程cpu占用
    之后查看文件是cat xxx

2025/1/14 20:35 地点广州 完结撒花

项目难点?四方保险——day11-数据中心:时序数据库、看板展示【实战】

技术上的难点:时序数据库看板展示

秒杀系统如何优化?

痛点描述:
  • 瞬时并发量大
    • 大量用户会在同一时间进行抢购
    • 网站瞬时访问流量激增
  • 库存少
    • 访问请求数量远远大于库存数量
    • 只有少部分用户能够秒杀成功

Ⅰ. 访问层 — 商品页

  • 可以将静态秒杀页面放在cdn上[用户访问速度↑ 减轻服务器压力++]

Ⅱ. 访问层 — 秒杀按钮

  • 活动前禁用按钮、点击后禁用按钮、滑动验证码[防羊毛党]、排队体验[提升用户体验]

Ⅲ. 中间转换层 — 多级负载均衡 & 限流 & 自动伸缩

  • 通常会通过Nginx来进行负载均衡【单台Ng处理的并发量是两三万左右】
  • 在它上层要做到硬件级别的隔离器 【F5/LVS】
  • 通过Ng负载均衡到网关之后 通过客户端的负载均衡器Ribbon
  • 4级的负载均衡 可以处理每秒上10W以上的QPS并发量
  • 通过DockerK8S来进行云服务器的动态伸缩的部署[秒杀开始自动扩容 秒杀结束自动缩减]
  • 注意要在Ng上做好限流 防止一些绕过了我们前端的DDOS攻击 还需要在网关层通过Sentinel对不同的服务节点去设置限流以及熔断的机制
  • 可以在秒杀中通过MQ做削锋填股 通过MQ可以减轻下游的压力 防止激增流量打垮下游数据库

Ⅳ. 服务端 — 用Redis做缓存减轻数据库压力

  • 秒杀商品信息预热到Redis中 防止Redis被击穿我们的数据库
  • 通过Redis的Lua脚本[保证多个操作的原子性]操作库存
  • 防重 可以通过 redis的SETNX → 用 Token + 商品URL // IP + 商品URL 只能有一个有效
  • 分布式锁保证请求的原子性 → Redisson的分布式锁

**Ⅴ. 数据库 — 读写分离 **

  • 数据量很大就分库分表

订单超时自动取消是怎么实现的?

① JDK自带的延时队列

优点:简单,不需要借助其他第三方组件,成本低。
缺点:所有超时处理订单都要加入到DelayQueue中,占用内存大,没办法做到分布式处理,之恶能在集群中挑选一台leader专门处理,效率低
不适合订单量比较大的

② 基于RocketMQ的定时消息 — 延时消息

优点:使用简单,和使用普通消息一样,支持分布式。精度高,支持任意时刻

缺点使用限制:定时时长最大值24小时。
成本高:每个订单需要新增一个定时消息,且不会马上消费,给MQ带来很大的存储成本。
同一个时刻大量消息会导致消息延迟:定时消息的实现逻辑需要先经过定时存储等待触发,定时时间到达后才会被投递给消费者。因此,如果将大量定时消息的定时时间设置为同一时刻,则到达该时刻后会有大量消息同时需要被处理,会造成系统压力过大,导致消息分发延迟,影响定时精度。

③ 基于Redis的过期监听

设置过期时间:24小时内没有支付就会自动取消
缺点:(也是所有中间件的缺点)

  • 不可靠 Redis在过期通知的时候,如果应用正好重启了,那么就有可能通知事件就丢了,会导致订单一直无法关闭,有稳定性问题。如果一定要使用Redis过期监听方案,建议再通过定时任务做补偿机制。
  • 如果订单量大需要占用中间件大量的存储空间,需要额外维护成本。
④ 定时任务分布式处理【要按照成本思维的思考方式】

通过定时任务(任务调度)的批量处理 → 一次性把所有超时的订单全部捞出来 处理完再全部执行更新
如果使用中间件都要单独存储那些数据,如果存储压力大就要涉及到集群

如果对于超时精度比较高,超时时间在24小时内,且不会有峰值压力的场景下,推荐使用RocketMQ的定时消息解决方案
在电商业务下,许多订单超时场景都在24小时以上,对于超时精度没那么敏感,并且有海量订单需要批处理,推荐使用基于定时任务的跑批解决方案。

如何防止重复下单?

方案一:提交订单按钮置灰 [防止用户无意点击多次]
方案二:后端采用redis的setnx 来保证它的唯一幂等性

setnx:当我们调用setnx来去保存一个key和value的时候,如果这个value没有值的话,那么就会返回true保存成功;如果有值就会返回false → 保证多次存储只能存储一个值

怎么防止刷单?【人肉机刷单!!】

业务风控

提高羊毛门槛:实名认证、消费门槛、随机优惠
限制用户参与、中奖、奖励次数
根据用户的历史行为和忠诚度,提供不同层次的优惠,优待忠实用户
奖池(优惠券数量)限制上限

分布式集群架构下怎么保证并发安全?

让你设计一个扫码登录怎么实现?

生成二维码

请求登录页生成二维码,PC端请求后端生成一个二维码,此时在后端就会生成一个全局唯一的二维码ID,主要保存二维码的状态[二维码ID, NEW],状态设置到Redis设置过期时间,然后把当前的二维码ID返回给前端,然后生成二维码 【前后端都可以生成 → 返回Base64的编码给前端】此时的二维码就绑定了用户的ID让用户扫描。

扫码

PC端和后端会建立一个轮询的请求,不断的根据二维码ID去查询二维码状态,一旦状态改变页面也会改变。也可以通过长连接WebSocket获取状态 淘宝用的轮询、抖音用的长连接,此时就可以扫码。
扫码前保证手机是登录状态 没有登录肯定是不能扫码的,登录后进行扫码就会携带手机端的用户token以及二维码的ID在后端去校验请求Token,如果校验成功就代表手机可以登录,此时可以变更二维码状态为扫描。前端就可以根据这个把页面变为待确认状态

如何设计分布式日志存储架构?

使用redis出现缓存三兄弟如何解决?减轻数据库的压力

你在项目中用到了Redis对吧 介绍一下有没有遇到关于redis的什么问题?

暂时还没看!
12.使用redis出现缓存击穿雪崩穿透怎么解决_哔哩哔哩_bilibili

如何使用Redis记录用户连续登录了多少天?放在数据库里不合适

放在数据库不合适因为你要创建一个表 记录用户哪一天进行了签到 如果用户量很多就会很大的量

给你一亿个Redis keys统计双方的共同好友?

如何做一亿用户实时积分排行榜?

内存200M读取1G文件并统计内容重复次数?内存受限时

一次性读取肯定会OOM
可以根据缓冲区分块读取

查询200条数据耗时200ms,怎么在500ms内查询1000条数据?

SpringBoot如果有百万数据插入怎么优化?

SpringBoot可以同时处理多少请求?

volatile的应用场景?

为了保证我们并发编程的可见性有序性

SQL的执行流程

单表最多数据量需要多大才涉及到分表?

Mysql引擎层BufferPool工作过程原理?

什么是聚集索引和非聚集索引?

count(*)、count(1)、count(字段) 谁更快?有什么区别?

tb_user
id name
1 潘春尧
2 NULL
3 张三
Ⅰ. SELECT count(*) FROM tb_user;                → 3
Ⅱ. SELECT count(1) FROM tb_user;                → 3
Ⅲ. SELECT count(name) FROM tb_user;             → 2

在功能上没有区别 Ⅲ.如果你统计的数据需要排除NULL 就可以用count(指定字段)
在性能上没有任何区别 非要比较就是count(1)更胜一筹 因为不需要mysql在底层做任何的sql优化

SQL语句中使用了前模糊会导致索引失效?

分库分表id冲突解决方案?

深分页为什么慢,怎么优化?

MySQL的隔离级别实现原理MVCC ?

  • **MVCC ** (Multiversion Concurrency Control) 多版本并发控制器

    它是事务隔离级别的无锁的实现方式,用于提高事务的并发性能

事务隔离级别 (isolation)

用来解决并发事务所产生一些问题:
并发:同一个时间,多个线程同时进行请求。
什么时候会发生并发问题:在并发情况下,对同一个数据(变量、对象)进行读写操作才会产生并发问题
并发会产生什么问题?
1.脏读一读已提交(行锁,读不会加锁)
2.不可重复度–重复读(行锁,读和写都会上锁)
3.幻影读–串行化(表锁)概念: 通过设置隔离级别可解决在并发过程中产生的那些问题

用户忘记密码,系统为什么不直接提供原密码,而让改密码

因为系统它也不知道我们的原密码是什么

服务端在保存密码的时候绝不会明文存到数据库,怕有数据库权限的人或者黑客恶意利用
必须用不可逆的加密算法

MD5
  • 只能加密不可解密 但是它是hash算法 可能会有哈希冲突 至少加密2^128次方才有可能发生哈希碰撞
  • 每次生成的密文都一样,不管加密多少次生成的密文都是一样的 可以通过暴力破解
    • 解决暴力破解 就要在里面加 每次加密 解密 都要加入
HS256
  • 增加加密字符长度 目前没有碰撞性
  • 最好加入随机盐
BCrypt → 加入spring-security-core依赖
  • 盐是随机的
  • 无法通过暴力破解

Git怎么修复线上的突发BUG线上突发Bug要修复,本地正在开发新需求

在git里我们通常会用一个单独的分支来进行管理
本地开发也会有一个单独的分支
可以将线上代码的分支签出来单独进行修复

  • 正在开发的代码 → 暂存dev分支
  • 严重故障:回滚上一个版本
    非严重故障:在fix分支修复紧急Bug
  • 非常紧急:直接合并master分支上线
  • 一般急:合并release分支,走测试、上线流程
  • 非紧急:合并dev分支,走测试、线上流程

RestTemplate如何优化连接池

默认是没有连接池的
要用框架HTTPClientOKHTTP

Synchronized怎么提升性能

开发中有没有用到设计模式?怎么用的

策略模式 + 简单工厂 + 模板方法

SpringBoot如何防止反编译

有没有出现过Spring正常SpringBoot报错的情况?

SpringBoot配置文件敏感信息如何加密?

一个需求来了怎么办
首先看这个需求 进行一个分析 分析这个需求跟哪些功能有关联 比如说在我做过的xxx里面 和什么关联 要思考怎么去做这个关联 数据库 代码层面 思考好之后 再去ER画图 写接口文档 再去开始写代码

阅读全文

SpringBoot趣味实战课

2024/10/21

Swagger + Mariadb + Hibernate 实现极简CRUD

application.yaml
spring:
  application:
    name: Pluminary
  datasource:
    driver-class-name: org.mariadb.jdbc.Driver
    url: jdbc:mariadb://localhost:3306/pcy?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&nullCatalogMeansCurrent=true
    username: root
    password: root
  jpa:
    hibernate:
      ddl-auto: update
    properties:
      hibernate:
        dialect: org.hibernate.dialect.MariaDB103Dialect
  springdoc:
    api-docs:
      path: /v3/api-docs
    swagger-ui:
      path: /swagger-ui.html

  server:
    port: 8080
    servlet:
      context-path: /springboot
      session:
        timeout: 60
  debug: true
com/pcy/Swagger/SwaggerConfig.java
package com.pcy.Swagger;

import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import org.springdoc.core.models.GroupedOpenApi;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;

@Configuration
public class SwaggerConfig {

    @Bean
    public GroupedOpenApi createRestApi() {
        return GroupedOpenApi.builder()
                .group("Spring Boot 实战")
                .pathsToMatch("/users/**") //这里是扫描包
                .build();
    }

    @Bean
    public OpenAPI customOpenAPI() {
        return new OpenAPI()
                .info(new Info()
                        .title("Spring Boot 实战")
                        .version("1.0")
                        .description("Spring Boot 实战的 RESTFul 接口文档说明")
                        .contact(new Contact()
                                .name("Pluminary")
                                .url("https://github.com/P-luminary")
                                .email("390415030@qq.com")));
    }
}
com/pcy/service/UserRepository.java //【这个是持久化接口 实现CRUD】
package com.pcy.service;

import com.pcy.dao.User;
import org.springframework.data.jpa.repository.JpaRepository;

public interface UserRepository extends JpaRepository<User,Integer> {
}
com/pcy/controller/UserController.java
package com.pcy.controller;

import com.pcy.dao.User;
import com.pcy.service.UserRepository;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.responses.ApiResponse;
import io.swagger.v3.oas.annotations.responses.ApiResponses;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/users")
@Tag(name = "User Controller", description = "用户相关操作")
public class UserController {

    @Autowired
    private UserRepository userRepository;

    @Operation(summary = "根据ID获取用户信息", description = "通过用户ID获取用户详细信息")
    @ApiResponses(value = {
            @ApiResponse(responseCode = "200", description = "成功获取用户信息"),
            @ApiResponse(responseCode = "404", description = "未找到用户")
    })
    @GetMapping("/{id}")
    public User get(@PathVariable int id) {
        return userRepository.findById(id).orElse(null);
    }

    @Operation(summary = "创建用户", description = "创建一个新的用户")
    @ApiResponses(value = {
            @ApiResponse(responseCode = "200", description = "成功创建用户"),
            @ApiResponse(responseCode = "400", description = "无效的输入")
    })
    @PostMapping
    public User create(@RequestBody User user) {
        return userRepository.save(user);
    }

    @Operation(summary = "更新用户", description = "更新用户信息")
    @ApiResponses(value = {
            @ApiResponse(responseCode = "200", description = "成功更新用户信息"),
            @ApiResponse(responseCode = "404", description = "未找到用户")
    })
    @PutMapping
    public User update(@RequestBody User user) {
        return userRepository.save(user);
    }

    @Operation(summary = "删除用户", description = "根据ID删除用户")
    @ApiResponses(value = {
            @ApiResponse(responseCode = "200", description = "成功删除用户"),
            @ApiResponse(responseCode = "404", description = "未找到用户")
    })
    @DeleteMapping("/{id}")
    public void delete(@PathVariable int id) {
        userRepository.deleteById(id);
    }
}
pom.xml
<?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>3.3.2</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.pcy</groupId>
    <artifactId>Pluminary</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>Pluminary</name>
    <description>Pluminary</description>
    <properties>
        <java.version>17</java.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-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

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

        <dependency>
            <groupId>io.swagger.core.v3</groupId>
            <artifactId>swagger-annotations</artifactId>
            <version>2.2.15</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
<!--        <dependency>-->
<!--            <groupId>mysql</groupId>-->
<!--            <artifactId>mysql-connector-java</artifactId>-->
<!--            <version>8.0.33</version>-->
<!--        </dependency>-->
        <dependency>
            <groupId>org.mariadb.jdbc</groupId>
            <artifactId>mariadb-java-client</artifactId>
            <version>3.0.3</version>
        </dependency>

        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
            <version>2.1.0</version>
        </dependency>
        <dependency>
            <groupId>org.mariadb.jdbc</groupId>
            <artifactId>mariadb-java-client</artifactId>
            <version>2.7.4</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-core</artifactId>
            <version>6.1.7.Final</version> <!-- 选择与 Spring Boot 3.3.2 兼容的版本 -->
        </dependency>
    </dependencies>

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

增加分页、排序

com/pcy/controller/UserController.java
@Operation(summary = "获取用户列表", description = "获取用户列表")
    @GetMapping
    public Page<User> list(@RequestParam(defaultValue = "id") String property,
 @RequestParam(defaultValue = "ASC")Sort.Direction direction,
 @RequestParam(defaultValue = "0") Integer page,
 @RequestParam(defaultValue = "10") Integer pageSize) {
    Pageable pageable = PageRequest.of(page, pageSize, direction, property);
        return userRepository.findAll(pageable);
    }

根据姓名查用户

com/pcy/controller/UserController.java    
    @Operation(summary = "根据姓名查用户",description = "根据姓名查用户")
    @GetMapping("/name")
    public List<User> getByName(String name){
        return userRepository.findByNameContaining(name);
    }
com/pcy/service/UserRepository.java
package com.pcy.service;

import com.pcy.dao.User;
import org.springframework.data.jpa.repository.JpaRepository;

import java.util.List;

public interface UserRepository extends JpaRepository<User,Integer> {
    List<User> findByNameContaining(String name);
}

根据生日查用户、删除User表

com/pcy/controller/UserController.java
@Operation(summary = "根据生日获取用户信息①",description = "根据生日获取用户信息①")
    @GetMapping("/birthdayOne")
    public List<User> getBirthDayOne(LocalDate birthDay){
        return userRepository.findByBirthDay(birthDay);
    }

    @Operation(summary = "根据生日获取用户信息②",description = "根据生日获取用户信息②")
    @GetMapping("/birthdayTwo")
    public List<User> getBirthDayTwo(LocalDate birthDay){
        return userRepository.findByBirthDayNative(birthDay);
    }

    @Operation(summary = "删除User",description = "删除User")
    @GetMapping("/delete")
    public void delete(){
        userRepository.delete();
    }
com/pcy/service/UserRepository.java
@Query("SELECT u FROM User u WHERE u.birthday=?1")
    List<User> findByBirthDay(LocalDate birthDay);

    @Query(value = "SELECT * FROM user WHERE birth_day =:birthDay",nativeQuery = true)
    List<User> findByBirthDayNative(LocalDate birthDay);

    @Modifying
    @Transactional
    @Query(value = "DELETE FROM User")
    int delete();

增加审计

com/pcy/MallApplication.java //【增加@EnableJpaAuditing】
package com.pcy;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.data.jpa.repository.config.EnableJpaAuditing;

@EnableJpaAuditing
@SpringBootApplication
public class MallApplication {
    public static void main(String[] args) {
        SpringApplication.run(MallApplication.class, args);
    }
}
com/pcy/dao/BaseEntity.java //【没有必要为每个实体类都编写 直接封装导一个类 User去继承】
package com.pcy.dao;

import jakarta.persistence.Column;
import jakarta.persistence.EntityListeners;
import jakarta.persistence.MappedSuperclass;
import lombok.Data;
import org.springframework.data.annotation.CreatedBy;
import org.springframework.data.annotation.CreatedDate;
import org.springframework.data.annotation.LastModifiedBy;
import org.springframework.data.annotation.LastModifiedDate;
import org.springframework.data.jpa.domain.support.AuditingEntityListener;

import java.time.LocalDateTime;

@Data
@MappedSuperclass
//该注解用于监听实体类,在save、update之后的状态
@EntityListeners(AuditingEntityListener.class)
public abstract class BaseEntity {
    @CreatedBy
    @Column(updatable = false)
    private String creator;

    @LastModifiedBy
    private String modifier;

    @CreatedDate
    @Column(updatable = false) //不可修改的
    private LocalDateTime createTime;

    @LastModifiedDate
    private LocalDateTime updateTime;
}
com/pcy/dao/User.java //【增加@EqualsAndHashCode 与 extends BaseEntity】
@Data
@Entity
@EqualsAndHashCode(callSuper = true)
//@Schema(name="用户信息")
@Table(indexes = {@Index(name = "uk_email",columnList = "email",unique = true)})
public class User extends BaseEntity{
    @Id
//    @Schema(description = "用户ID")
//    @NotBlank(message = "Id不能为空")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;
    ...
}
com/pcy/service/impl/AuditorAwareImpl.java
package com.pcy.service.impl;

import org.springframework.data.domain.AuditorAware;
import org.springframework.stereotype.Component;

import java.util.Optional;

@Component
public class AuditorAwareImpl implements AuditorAware<String> {

    @Override
    public Optional<String> getCurrentAuditor() {
        // 添加一个随机数
        return Optional.of("管理员"+(int)(Math.random()));
    }
}

引入Mybatis-Plus + FreeMarker

pom.xml
<dependency>
  <groupId>com.baomidou</groupId>
  <artifactId>mybatis-plus-boot-starter</artifactId>
  <version>3.4.2</version>
</dependency>
<dependency>
  <groupId>org.springframework.boot</groupId>
  <artifactId>spring-boot-starter-freemarker</artifactId>
</dependency>
//根据你提供的实体类BaseEntity和User,我为你设计了一个基于MyBatis-Plus 3.5.x版本的代码生成器MysqlGenerator,它将自动生成与这些实体类相关的代码,如Mapper、Service、Controller等。以下是生成器的代码示例
【仅供查看学习 实际代码爆红无法导入】
import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.core.toolkit.StringPool;
import com.baomidou.mybatisplus.generator.AutoGenerator;
import com.baomidou.mybatisplus.generator.config.*;
import com.baomidou.mybatisplus.generator.config.builder.*;
import com.baomidou.mybatisplus.generator.config.po.TableInfo;
import com.baomidou.mybatisplus.generator.engine.FreemarkerTemplateEngine;
import com.baomidou.mybatisplus.generator.fill.Property;
import com.baomidou.mybatisplus.generator.keywords.MySqlKeyWordsHandler;

import java.util.Collections;

public class MysqlGenerator {

    // 项目路径
    private static final String PROJECT_PATH = System.getProperty("user.dir");
    // 输出路径
    private static final String OUTPUT_DIR = PROJECT_PATH + "/src/main/java";
    // 作者
    private static final String AUTHOR = "YourName";
    // 包名
    private static final String BASE_PACKAGE = "com.pcy";
    // 数据源配置
    private static final String DATABASE_URL = "jdbc:mysql://localhost:3306/your_database";
    private static final String DATABASE_USERNAME = "root";
    private static final String DATABASE_PASSWORD = "password";
    private static final String DATABASE_DRIVER = "com.mysql.cj.jdbc.Driver";

    public static void main(String[] args) {
        // 1. 全局配置
        GlobalConfig.Builder globalConfig = new GlobalConfig.Builder()
            .outputDir(OUTPUT_DIR)
            .author(AUTHOR)
            .enableSwagger()
            .fileOverride()
            .disableOpenDir(); // 不自动打开输出目录

        // 2. 数据源配置
        DataSourceConfig.Builder dataSourceConfig = new DataSourceConfig.Builder(DATABASE_URL, DATABASE_USERNAME, DATABASE_PASSWORD)
            .dbQuery(new MySqlQuery())
            .schema("public")
            .dbType(DbType.MYSQL)
            .keyWordsHandler(new MySqlKeyWordsHandler())
            .driverName(DATABASE_DRIVER);

        // 3. 包配置
        PackageConfig.Builder packageConfig = new PackageConfig.Builder()
            .parent(BASE_PACKAGE)
            .entity("dao")
            .mapper("mapper")
            .service("service")
            .controller("controller");

        // 4. 策略配置
        StrategyConfig.Builder strategyConfig = new StrategyConfig.Builder()
            .addInclude("user") // 生成指定表
            .addTablePrefix("t_") // 去掉表前缀
            .entityBuilder()
                .superClass(BaseEntity.class)
                .enableLombok()
                .addSuperEntityColumns("id", "creator", "modifier", "create_time", "update_time")
                .logicDeleteColumnName("deleted")
                .addTableFills(new Property("create_time", FieldFill.INSERT))
                .addTableFills(new Property("update_time", FieldFill.INSERT_UPDATE))
                .enableActiveRecord()
                .naming(NamingStrategy.underline_to_camel)
                .columnNaming(NamingStrategy.underline_to_camel)
            .controllerBuilder()
                .enableRestStyle()
                .enableHyphenStyle()
            .serviceBuilder()
                .formatServiceFileName("%sService")
                .formatServiceImplFileName("%sServiceImpl")
            .mapperBuilder()
                .enableBaseResultMap()
                .enableBaseColumnList();

        // 5. 模板配置
        TemplateConfig.Builder templateConfig = new TemplateConfig.Builder();

        // 6. 自定义配置
        InjectionConfig.Builder injectionConfig = new InjectionConfig.Builder()
            .beforeOutputFile((tableInfo, objectMap) -> objectMap.put("parent", BASE_PACKAGE));

        // 7. 整合配置
        AutoGenerator autoGenerator = new AutoGenerator(dataSourceConfig.build())
            .global(globalConfig.build())
            .packageInfo(packageConfig.build())
            .strategy(strategyConfig.build())
            .template(templateConfig.build())
            .injection(injectionConfig.build())
            .templateEngine(new FreemarkerTemplateEngine()); // 选择模板引擎

        // 8. 执行
        autoGenerator.execute();
    }
}
/*
关键配置说明:
GlobalConfig:设置代码生成的全局配置,包括作者、输出目录、是否覆盖已有文件等。
DataSourceConfig:配置数据库连接信息,使用MySQL数据库。
PackageConfig:指定生成的代码所在的包路径。
StrategyConfig:配置生成策略,包括实体类的继承关系、使用Lombok、Rest风格的控制器等。
TemplateConfig:模板配置,可定制生成的模板。
InjectionConfig:自定义配置,用于在生成文件前注入自定义的变量或逻辑。
AutoGenerator:整合所有配置并执行代码生成。

生成的文件包括:
实体类:根据数据库表生成实体类,并继承BaseEntity。
Mapper接口:生成Mapper接口用于数据库操作。
Service接口和实现类:生成Service接口及其实现类。
Controller类:生成Rest风格的控制器类。

使用方法:
修改数据库连接信息(DATABASE_URL、DATABASE_USERNAME、DATABASE_PASSWORD)。
配置需要生成代码的表名(addInclude("user"))。
运行MysqlGenerator.java的main方法,代码将会生成在指定的输出目录中。
*
//【以下都是自动生成的代码】
com/pcy/mapper/UserMapper.java
package com.pcy.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.pcy.entity.User;
import org.apache.ibatis.annotations.Mapper;
@Mapper
public interface UserMapper extends BaseMapper<User> {
}
com/pcy/service/UserService.java
package com.pcy.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.pcy.entity.User;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

/**
 * <p>
 * 用户表 服务类
 * </p>
 */
public interface UserService extends IService<User> {
// 在Spring中使用事务
    @Transactional(propagation = Propagation.REQUIRED)
    void addWithRequired(User user);

    @Transactional(propagation = Propagation.REQUIRED)
    void addWithRequiredAndException(User user);

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    void addWithRequiredNew(User user);

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    void addWithRequiredNewAndException(User user);

    @Transactional(propagation = Propagation.NESTED)
    void addWithNested(User user);

    @Transactional(propagation = Propagation.NESTED)
    void addWithNestedAndException(User user);
}
com/pcy/service/impl/UserServiceImpl.java
package com.pcy.service.impl;


import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.pcy.entity.User;
import com.pcy.mapper.UserMapper;
import com.pcy.service.UserService;
import com.pcy.mapper.UserMapper;
import com.pcy.service.UserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Propagation;
import org.springframework.transaction.annotation.Transactional;

/**
 * <p>
 * 用户表 服务实现类
 * </p>
 */
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {

    @Autowired
    private UserMapper mapper;
    
    @Override
    @Transactional(propagation = Propagation.REQUIRED)
    public void addWithRequired(User user) {
        mapper.insert(user);
    }

    @Override
    @Transactional(propagation = Propagation.REQUIRED)
    public void addWithRequiredAndException(User user) {
        mapper.insert(user);
        throw new RuntimeException();
    }

    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void addWithRequiredNew(User user) {
        mapper.insert(user);
    }

    @Override
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void addWithRequiredNewAndException(User user) {
        mapper.insert(user);
        throw new RuntimeException();
    }

    @Override
    @Transactional(propagation = Propagation.NESTED)
    public void addWithNested(User user) {
        mapper.insert(user);
    }

    @Override
    @Transactional(propagation = Propagation.NESTED)
    public void addWithNestedAndException(User user) {
        mapper.insert(user);
        throw new RuntimeException();
    }
}
resources/mapper/UserMapper.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.pcy.mapper.UserMapper">

    <!-- 通用查询映射结果 -->
    <resultMap id="BaseResultMap" type="com.pcy.entity.User">
    <result column="id" property="id" />
    <result column="creator" property="creator" />
    <result column="modifier" property="modifier" />
    <result column="create_time" property="createTime" />
    <result column="update_time" property="updateTime" />
        <result column="name" property="name" />
        <result column="email" property="email" />
        <result column="birth_day" property="birthDay" />
    </resultMap>

    <!-- 通用查询结果列 -->
    <sql id="Base_Column_List">
        id,
        creator,
        modifier,
        create_time,
        update_time,
        name, email, birth_day
    </sql>

</mapper>
//【提问:爆bug  "Could not autowire. No beans of 'UserMapper' type found"】 深度解析
1. @MapperScan 注解的原理 //启动类里面的 @MapperScan("com.pcy.mapper")
@MapperScan 是 MyBatis-Spring 提供的一个注解,用于指定要扫描的 Mapper 接口所在的包路径。它的作用是告诉 Spring 框架应该在哪些包路径下寻找 Mapper 接口,并将它们注册为 Spring 的 Bean。
扫描 Mapper 接口:Spring Boot 在启动时,会扫描你指定的包路径下的所有接口,并检测这些接口是否包含 MyBatis 的 Mapper 注解或者继承了 BaseMapper 等相关接口。
注册为 Bean:一旦找到这些接口,Spring 会自动为这些接口生成一个实现类,并将它们注册为 Spring 容器中的 Bean,这样你就可以通过 @Autowired 注入这些 Mapper。

2. @Mapper 注解的原理
@Mapper 是 MyBatis 提供的一个注解,用于标记一个接口为 MyBatis 的 Mapper 接口。被标记为 @Mapper 的接口会被 MyBatis-Spring 扫描到,并且 MyBatis 会为该接口生成一个实现类,负责执行 SQL 语句。
当你在 UserMapper 接口上添加 @Mapper 注解时,即使没有使用 @MapperScan,MyBatis 也会知道这个接口是一个 Mapper 接口,并将其注册为一个 Bean。这使得你可以在 UserServiceImpl 中通过 @Autowired 注入它。

3. 为什么使用 @MapperScan 和 @Mapper 不会报错
自动注册 Bean:@MapperScan 会自动扫描指定包路径下的所有 Mapper 接口,并将它们注册为 Spring 容器中的 Bean。这意味着在 UserServiceImpl 中,当你使用 @Autowired 注入 UserMapper 时,Spring 可以找到对应的 Bean,从而避免 Could not autowire 错误。
手动注册 Bean:当你在 Mapper 接口上直接使用 @Mapper 注解时,Spring 也会将该接口注册为一个 Bean,这样你同样可以通过 @Autowired 进行注入,而不会出现 Bean 找不到的问题。
//【提问:MysqlGenerator 逆向生成那些包的原理】
MyBatis-Plus 提供的 MyBatis-Plus Generator 是一个非常强大的代码生成工具,可以通过数据库表结构生成对应的 Java 代码,包括实体类、Mapper 接口、Mapper XML 文件、Service 类、Controller 类等。这个过程通常被称为“逆向工程”或“代码生成”。
1. MyBatis-Plus Generator 的工作原理
 1.1 读取数据库表结构
数据源配置:首先,MyBatis-Plus Generator 通过配置的数据源连接到指定的数据库。它会读取数据库中的表结构信息,包括表名、字段名、数据类型、主键、外键、索引等信息。
> DataSourceConfig dsc = new DataSourceConfig.Builder(DATABASE_URL, DATABASE_USERNAME, DATABASE_PASSWORD)
    .driverName(DATABASE_DRIVER)
    .build();

元数据解析:MyBatis-Plus Generator 通过 JDBC 获取数据库的元数据 (Metadata),并解析每个表的结构,将其转换为可以用于代码生成的数据结构。

 1.2 生成代码
代码生成器:AutoGenerator 是核心的代码生成器类。它根据从数据库中获取的表结构信息,生成相应的 Java 类文件。
> AutoGenerator generator = new AutoGenerator(dsc);

模板引擎:MyBatis-Plus Generator 使用模板引擎(例如 Freemarker)来渲染代码模板。通过模板和解析后的元数据,生成代码文件。每个生成的 Java 类文件都对应着一个模板文件,模板文件中包含了如何生成特定类型文件的逻辑。
> generator.templateEngine(new FreemarkerTemplateEngine());

 1.3 生成的包和文件
实体类 (entity):根据表结构生成对应的 Java 实体类。每个实体类与数据库表一一对应,包含表中字段的定义。
> strategyConfig.entityBuilder().enableLombok().naming(NamingStrategy.underline_to_camel);

Mapper 接口 (mapper):生成的 Mapper 接口用于与数据库交互,执行基本的增删改查操作。Mapper 接口通常继承自 BaseMapper,提供基本的 CRUD 操作。
> strategyConfig.mapperBuilder().enableBaseResultMap().enableBaseColumnList();
Mapper XML 文件 (mapper.xml):生成的 Mapper XML 文件包含了 Mapper 接口中对应的方法的 SQL 语句。这些 XML 文件用于定义复杂的查询、更新语句等。

Service 接口和实现类 (service, service.impl):Service 层是业务逻辑层。生成的 Service 接口提供了业务操作的定义,Service 实现类则实现这些业务操作。
> strategyConfig.serviceBuilder().formatServiceFileName("%sService");

Controller 类 (controller):生成的 Controller 类用于处理 HTTP 请求,调用 Service 层的方法进行业务处理,然后返回结果。Controller 通常与前端交互,处理用户请求。
> strategyConfig.controllerBuilder().enableRestStyle().enableHyphenStyle();


2. MyBatis-Plus Generator 如何生成这些包和文件
 2.1 代码生成策略 (StrategyConfig)
StrategyConfig 类用于配置代码生成的策略,如生成哪些表,生成哪些类,类的命名规则,是否使用 Lombok 等。
StrategyConfig strategyConfig = new StrategyConfig.Builder()
    .addInclude("user") // 生成指定表
    .entityBuilder().enableLombok() // 实体类配置
    .mapperBuilder().enableBaseResultMap() // Mapper 配置
    .serviceBuilder().formatServiceFileName("%sService") // Service 配置
    .controllerBuilder().enableRestStyle() // Controller 配置
    .build();

 2.2 模板文件
MyBatis-Plus Generator 使用的模板文件可以自定义,通常位于 resources/templates 目录下。每个模板文件对应一个需要生成的 Java 文件类型,例如 entity.java.ftl 对应实体类,mapper.java.ftl 对应 Mapper 接口。
     
模板文件中可以使用变量和逻辑来决定生成的代码内容。例如,${className} 会被替换为实际的类名,<#if useLombok> @Data </#if> 会根据条件生成代码。
     
 2.3 文件输出配置 (InjectionConfig 和 FileOutConfig)
通过 InjectionConfig 和 FileOutConfig,可以控制生成文件的路径、名称、以及自定义生成的文件内容。例如,可以指定某个表的实体类生成到特定的包下,或者将 XML 文件输出到特定的路径。
InjectionConfig cfg = new InjectionConfig.Builder()
    .beforeOutputFile((tableInfo, objectMap) -> {
        // 自定义处理逻辑
    })
    .build();

用MyBatis Plus的分页

pom.xml
<?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>3.3.2</version>
        <relativePath/> <!-- lookup parent from repository -->
    </parent>
    <groupId>com.pcy</groupId>
    <artifactId>Pluminary</artifactId>
    <version>0.0.1-SNAPSHOT</version>
    <name>Pluminary</name>
    <description>Pluminary</description>
    <properties>
        <java.version>17</java.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-test</artifactId>
            <scope>test</scope>
        </dependency>

        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
        </dependency>

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

        <dependency>
            <groupId>io.swagger.core.v3</groupId>
            <artifactId>swagger-annotations</artifactId>
            <version>2.2.15</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-jpa</artifactId>
        </dependency>
<!--        <dependency>-->
<!--            <groupId>mysql</groupId>-->
<!--            <artifactId>mysql-connector-java</artifactId>-->
<!--            <version>8.0.33</version>-->
<!--        </dependency>-->
        <dependency>
            <groupId>org.mariadb.jdbc</groupId>
            <artifactId>mariadb-java-client</artifactId>
            <version>3.0.3</version>
        </dependency>

        <dependency>
            <groupId>org.springdoc</groupId>
            <artifactId>springdoc-openapi-starter-webmvc-ui</artifactId>
            <version>2.1.0</version>
        </dependency>
        <dependency>
            <groupId>org.hibernate</groupId>
            <artifactId>hibernate-core</artifactId>
            <version>6.1.7.Final</version> <!-- 选择与 Spring Boot 3.3.2 兼容的版本 -->
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-boot-starter</artifactId>
            <version>3.5.5</version>
            <exclusions>
                <exclusion>
                    <artifactId>mybatis-spring</artifactId>
                    <groupId>org.mybatis</groupId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.mybatis</groupId>
            <artifactId>mybatis-spring</artifactId>
            <version>3.0.3</version>
        </dependency>
        <dependency>
            <groupId>com.baomidou</groupId>
            <artifactId>mybatis-plus-generator</artifactId>
            <version>3.5.5</version> <!-- 版本对齐 -->
        </dependency>

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

    </dependencies>

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

</project>
//【添加MyBatis-Plus的分页插件】
com/pcy/utils/MyBatisPlusConfig.java
package com.pcy.utils;

import com.baomidou.mybatisplus.annotation.DbType;
import com.baomidou.mybatisplus.extension.plugins.MybatisPlusInterceptor;
import com.baomidou.mybatisplus.extension.plugins.inner.PaginationInnerInterceptor;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
//@Configuration 用于定义配置类,被注解的类内部包含有一个或多个被@Bean注解的方法
// 用于构建bean定义,初始化Spring容器
@Configuration
public class MyBatisPlusConfig {
    @Bean
    public MybatisPlusInterceptor mybatisPlusInterceptor(){
        MybatisPlusInterceptor interceptor = new MybatisPlusInterceptor();
        interceptor.addInnerInterceptor(new PaginationInnerInterceptor(DbType.MARIADB));
        return interceptor;
    }
}
com/pcy/controller/UserController.java //【增加listPage】
package com.pcy.controller;

import com.pcy.entity.User;
import com.pcy.service.UserRepository;
import com.pcy.service.UserService;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.beans.factory.annotation.Autowired;
import com.baomidou.mybatisplus.extension.plugins.pagination.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.data.domain.Pageable;
import org.springframework.data.domain.Sort;
import org.springframework.web.bind.annotation.*;

import java.time.LocalDate;
import java.util.List;

@RestController
@RequestMapping("/users")
@Tag(name = "User Controller", description = "用户相关操作")
public class UserController {

    @Autowired
    private UserRepository userRepository;

    @Autowired
    private UserService userService;

    @Operation(summary = "根据ID获取用户信息", description = "通过用户ID获取用户详细信息")
    @GetMapping("/{id}")
    public User get(@PathVariable int id) {
        return userRepository.findById(id).orElse(null);
    }

    @Operation(summary = "创建用户", description = "创建一个新的用户")
    @PostMapping
    public User create(@RequestBody User user) {
        return userRepository.save(user);
    }

    @Operation(summary = "更新用户", description = "更新用户信息")
    @PutMapping
    public User update(@RequestBody User user) {
        return userRepository.save(user);
    }

    @Operation(summary = "删除用户", description = "根据ID删除用户")
    @DeleteMapping("/{id}")
    public void delete(@PathVariable int id) {
        userRepository.deleteById(id);
    }

    @Operation(summary = "获取用户列表", description = "获取用户列表")
    @GetMapping("/list")
    public org.springframework.data.domain.Page<User> list(@RequestParam(defaultValue = "id") String property,
                                                           @RequestParam(defaultValue = "ASC") Sort.Direction direction,
                                                           @RequestParam(defaultValue = "0") Integer page,
                                                           @RequestParam(defaultValue = "10") Integer pageSize) {
        Pageable pageable = PageRequest.of(page, pageSize, direction, property);
        return userRepository.findAll(pageable);
    }

    @Operation(summary = "根据生日获取用户信息①", description = "根据生日获取用户信息①")
    @GetMapping("/birthdayOne")
    public List<User> getBirthDayOne(@RequestParam LocalDate birthDay) {
        return userRepository.findByBirthDay(birthDay);
    }

    @Operation(summary = "根据生日获取用户信息②", description = "根据生日获取用户信息②")
    @GetMapping("/birthdayTwo")
    public List<User> getBirthDayTwo(@RequestParam LocalDate birthDay) {
        return userRepository.findByBirthDayNative(birthDay);
    }

    @Operation(summary = "删除所有用户", description = "删除所有用户")
    @DeleteMapping("/deleteAll")
    public void deleteAll() {
        userRepository.deleteAll();
    }

    @Operation(summary = "分页查询用户列表", description = "分页查询用户列表")
    @GetMapping("/page")
    public Page<User> listPage(@RequestParam(defaultValue = "1") Integer page,
                               @RequestParam(defaultValue = "10") Integer pageSize) {
        return userService.page(new Page<>(page, pageSize));
    }
}

高级SQL语句(Lambda)

wrapper.lambda().like(user -> user.getName(), "p");
/*
Lambda 表达式:

user -> user.getName() 是一个 Lambda 表达式。
user 是 User 类的一个实例,作为 Lambda 表达式的输入参数。
user.getName() 是对 user 对象的 getName() 方法的调用,返回 name 字段的值。
作用:

这行代码告诉 MyBatis-Plus:在生成的 SQL 查询中,查找 name 字段值中包含 "p" 的所有记录。
wrapper.lambda() 返回一个 LambdaQueryWrapper<User> 对象,支持使用 Lambda 表达式进行条件构建。
.like() 方法添加了一个 LIKE 条件,表示在 SQL 查询中进行模糊匹配。
*/


wrapper.lambda().like(User::getName, "p");
/*
方法引用:

User::getName 是一种方法引用,它引用了 User 类的 getName() 方法。
方法引用是对 Lambda 表达式的一种简写。它表示将某个方法作为函数式接口的实现。
作用:

这行代码与第一行代码的作用相同,都是在生成的 SQL 查询中查找 name 字段值中包含 "p" 的所有记录。
User::getName 告诉 MyBatis-Plus:使用 User 类中的 getName() 方法来获取要参与条件判断的字段。
*/
com/pcy/controller/UserController.java
@Operation(summary = "自定义查询", description = "自定义查询")
    @GetMapping("/Dingyi")
    public List<User> getWrapper() { //类型List<User> 可以返回数据库列表
        QueryWrapper<User> wrapper = new QueryWrapper<>();
//        wrapper.eq("name", "潘春尧");
//        wrapper.lambda().ge(User::getBirthDay, LocalDate.parse("2011-01-01"));
//        wrapper.between(User::getBirthDay, "2011-01-01", "2011-12-31");
        wrapper.lambda().like(User::getName, "string");

//      wrapper.lambda().like(user -> user.getName(), "p");
//        wrapper.select("name,count(*)").groupBy("name");
//        return (QueryWrapper<User>) userMapper.selectList(wrapper);
//        wrapper.in(CollectionUtils.isNotEmpty(nameList), User::getName, nameList);
        return userMapper.selectList(wrapper);
    }

自动填充、填充实现策略

com/pcy/utils/MyMetaObjectHandler.java
package com.pcy.utils;

import com.baomidou.mybatisplus.core.handlers.MetaObjectHandler;
import org.apache.ibatis.reflection.MetaObject;

import java.time.LocalDateTime;

public class MyMetaObjectHandler implements MetaObjectHandler {
    @Override
    public void insertFill(MetaObject metaObject) {
        this.strictInsertFill(metaObject, "createTime", LocalDateTime::now, LocalDateTime.class);
        this.strictInsertFill(metaObject, "updateTime", LocalDateTime::now, LocalDateTime.class);
        this.strictInsertFill(metaObject, "creator", this::getCurrentUser, String.class);
        this.strictInsertFill(metaObject, "modifier", this::getCurrentUser, String.class);
    }

    @Override
    public void updateFill(MetaObject metaObject) {
        this.strictUpdateFill(metaObject, "updateTime", LocalDateTime::now, LocalDateTime.class);
        this.strictUpdateFill(metaObject, "modifier", this::getCurrentUser, String.class);
    }
    
    // 模拟获取当前用户
    private String getCurrentUser(){
        return "管理员" + (int) (Math.random() * 10);
    }
}
// 这是自动填充的原理
default MetaObjectHandler strictFillStrategy(MetaObject metaObject, String fieldName, Supplier<?> fieldVal) {
        if (metaObject.getValue(fieldName) == null) {
            Object obj = fieldVal.get();
            if (Objects.nonNull(obj)) {
                metaObject.setValue(fieldName, obj);
            }
        }
        return this;
    }

强大的Druid

pom.xml
        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.5</version>
        </dependency>
// Druid和MariaDB是两种不同类型的数据库系统
1、类型和用途:
Druid:Druid是一种分布式的实时分析数据库,主要用于处理高吞吐量的时间序列数据或事件数据。它专为快速查询和分析大规模数据而设计,常用于数据仓库、在线分析处理(OLAP)以及实时数据分析等场景。
MariaDB:MariaDB是一种关系型数据库管理系统(RDBMS),它是MySQL的一个分支,广泛用于常规的事务处理、数据存储和管理。MariaDB通常用于传统的OLTP(在线事务处理)场景,如web应用、内容管理系统等。

2、适用场景:
Druid:适合用于实时数据分析、日志分析、时间序列分析、用户行为分析等需要快速响应的场景。
MariaDB:适合传统的数据库应用,如电子商务系统、内容管理系统、ERP、CRM等需要强事务处理能力的场景。
    
总结来说,Druid和MariaDB各自适用于不同的数据处理需求,Druid更侧重于实时分析和大规模数据处理,而MariaDB更侧重于事务处理和关系型数据管理

Spring Data JPA与MyBatis-Plus的区别并且简单举例说明

Spring Data JPA: //【实现接口】

@Entity
public class User {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;
    private String name;
    // Other fields, getters, and setters
}

public interface UserRepository extends JpaRepository<User, Long> {
    List<User> findByName(String name);
}
Spring Data JPA: //【实现控制类】
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/jpa/users")
public class UserJpaController {

    @Autowired
    private UserRepository userRepository;

    @GetMapping
    public List<User> getAllUsers() {
        return userRepository.findAll();
    }

    @GetMapping("/{id}")
    public User getUserById(@PathVariable Long id) {
        return userRepository.findById(id)
                             .orElseThrow(() -> new ResourceNotFoundException("User not found with id: " + id));
    }

    @PostMapping
    public User createUser(@RequestBody User user) {
        return userRepository.save(user);
    }
    @PutMapping("/{id}")
    public User updateUser(@PathVariable Long id, @RequestBody User userDetails) {
        User user = userRepository.findById(id)
                                  .orElseThrow(() -> new ResourceNotFoundException("User not found with id: " + id));

        user.setName(userDetails.getName());
        // Update other fields here
        return userRepository.save(user);
    }

    @DeleteMapping("/{id}")
    public void deleteUser(@PathVariable Long id) {
        User user = userRepository.findById(id)
                                  .orElseThrow(() -> new ResourceNotFoundException("User not found with id: " + id));

        userRepository.delete(user);
    }

    @GetMapping("/search")
    public List<User> searchUsersByName(@RequestParam String name) {
        return userRepository.findByName(name);
    }
}


MyBatis Plus: //【实现接口】

@TableName("user")
public class User {
    private Long id;
    private String name;
    // Other fields, getters, and setters
}

public interface UserMapper extends BaseMapper<User> {
    // Custom SQL
    @Select("SELECT * FROM user WHERE name = #{name}")
    List<User> selectByName(@Param("name") String name);
}
MyBatis-Plus: //【实现控制类】
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/mybatis/users")
public class UserMyBatisController {

    @Autowired
    private UserMapper userMapper;

    @GetMapping
    public List<User> getAllUsers() {
        return userMapper.selectList(null);
    }

    @GetMapping("/{id}")
    public User getUserById(@PathVariable Long id) {
        return userMapper.selectById(id);
    }

    @PostMapping
    public void createUser(@RequestBody User user) {
        userMapper.insert(user);
    }

    @PutMapping("/{id}")
    public void updateUser(@PathVariable Long id, @RequestBody User userDetails) {
        User user = userMapper.selectById(id);
        if (user == null) {
            throw new ResourceNotFoundException("User not found with id: " + id);
        }

        user.setName(userDetails.getName());
        // Update other fields here
        userMapper.updateById(user);
    }

    @DeleteMapping("/{id}")
    public void deleteUser(@PathVariable Long id) {
        User user = userMapper.selectById(id);
        if (user == null) {
            throw new ResourceNotFoundException("User not found with id: " + id);
        }

        userMapper.deleteById(id);
    }

    @GetMapping("/search")
    public List<User> searchUsersByName(@RequestParam String name) {
        return userMapper.selectByName(name);
    }
}

////////////////////////////////////////////////////////////////////////////////////////////////////////////////////

// 构建查询条件的包装类,它使用 Lambda 表达式避免了手写字符串可能导致的字段错误。
// 这种方式非常适合需要根据多个条件动态生成SQL查询的场景,使用LambdaQueryWrapper不仅能提高代码的可读性,还能减少由于硬编码字符串导致的错误。
import com.baomidou.mybatisplus.core.conditions.query.LambdaQueryWrapper;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

import java.util.List;

@RestController
@RequestMapping("/api/mybatis/users")
public class UserMyBatisController {

    @Autowired
    private UserMapper userMapper;

    @GetMapping("/search")
    public List<User> searchUsersByName(@RequestParam String name) {
        // 使用 LambdaQueryWrapper 构建模糊查询条件
        LambdaQueryWrapper<User> queryWrapper = new LambdaQueryWrapper<>();
        queryWrapper.like(User::getName, name); // 类似于 SQL 中的 "WHERE name LIKE '%name%'"

        // 执行查询并返回结果
        return userMapper.selectList(queryWrapper);
    }
    // 其他CRUD方法与前面的示例相同
}

Junit

经过单元测试,观察日志输出,就会发现没有进行数据库查询,对数据库的交互逻辑不是Service层的单元测试需要关心的事情,而是Dao层的单元测试需要考虑的。Service层的单元测试是假定Dao层全部正确的基础上写的,我们只需要关注Service层是正确即可。
pom.xml
          <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
com/pcy/service/impl/UserServiceImpl.java
@Service
public class UserServiceImpl extends ServiceImpl<UserMapper, User> implements UserService {
    private static final Logger logger = LoggerFactory.getLogger(UserServiceImpl.class);
    @Autowired
    private UserMapper mapper;

    public User getById(int id) {
        logger.info("id为:",id);
        return mapper.selectById(id);
    }
......
}
这是测试Service
test/java  com/pcy/service/impl/UserServiceImplTest.java //【用Mock改造 + log4j】
// 检查 UserServiceImpl 是否在测试中被 @MockBean 或其他方式替换为Mock对象。如果使用了Mock对象,测试时不会真正访问数据库,而是使用模拟数据。
package com.pcy.service.impl;

import com.pcy.entity.User;
import com.pcy.mapper.UserMapper;
import org.junit.jupiter.api.Assertions;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;

@SpringBootTest
class UserServiceImplTest {
    @InjectMocks
    UserServiceImpl userService;

    @Mock
    UserMapper userMapper;

    @Test
    @DisplayName("Test Service getById")
    void getById() {
        // 模拟userMapper的selectById方法返回一个User对象
        User mockUser = new User().setId(1).setName("qwe").setEmail("1234@qq.com");
        Mockito.when(userMapper.selectById(1)).thenReturn(mockUser);

        // 调用userService的getById方法,并验证返回结果
        User user = userService.getById(1);

        System.out.println(user);
        Assertions.assertEquals("qwe", user.getName());
    }
}
=====================================================================
Java HotSpot(TM) 64-Bit Server VM warning: Sharing is only supported for boot loader classes because bootstrap classpath has been appended
2024-08-12T21:18:07.805+08:00  INFO 31512 --- [Pluminary] [           main] com.pcy.service.impl.UserServiceImpl     : id为:
User(id=1, name=qwe, age=0, email=1234@qq.com, birthDay=null)
com/pcy/entity/User.java
//你的 User 类同时使用了 Lombok 注解 (@Data, @Accessors(chain = true)) 和手动定义的 getter/setter 方法。由于 Lombok 已经生成了这些方法,手动定义的 getter/setter 方法会覆盖 Lombok 自动生成的方法,这可能导致链式调用的 setEmail 和其他类似方法无法正确解析。
package com.pcy.entity;

import jakarta.persistence.*;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;

import java.time.LocalDate;

@Data
@Entity
@EqualsAndHashCode(callSuper = true)
//@Schema(name="用户信息")
@Table(indexes = {@Index(name = "uk_email",columnList = "email",unique = true)})
@Accessors(chain = true) // 允许链式调用
public class User extends BaseEntity{
    @Id
//    @Schema(description = "用户ID")
//    @NotBlank(message = "Id不能为空")
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Integer id;

//    @Schema(description = "用字")
//    @NotBlank(message = "名字不能为空")
    @Column(nullable = false, columnDefinition = "varchar(20) comment '姓名'")
    private String name;

//    @Transient //注解修饰
//    @Schema(description = "年龄")
//    @Min(value = 1, message = "年龄不能小于1")
    private int age;

//    @Schema(description = "邮箱")
//    @Email(message = "E-mail格式不正确")
    @Column(nullable = false, length = 50)
    private String email;

//    @Schema(description = "生日")
//    @Past(message = "生日必须为过去的时间")
    private LocalDate birthDay;
}
这是测试Controller
test/java  com/pcy/controller/UserControllerTest.java
// Controller层的单元测试需要用到一个特定的类——MockMvc 专门为SpringMVC提供支持的
package com.pcy.controller;

import com.pcy.entity.User;
import com.pcy.service.UserService;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.annotation.Before;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.DisplayName;
import org.junit.jupiter.api.Test;
import org.mockito.BDDMockito;
import org.mockito.InjectMocks;
import org.mockito.Mock;
import org.mockito.Mockito;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.test.web.servlet.MockMvc;
import org.springframework.test.web.servlet.request.MockMvcRequestBuilders;
import org.springframework.test.web.servlet.result.MockMvcResultHandlers;
import org.springframework.test.web.servlet.result.MockMvcResultMatchers;
import org.springframework.test.web.servlet.setup.MockMvcBuilders;

import static org.junit.jupiter.api.Assertions.*;
@Slf4j
@SpringBootTest
class UserControllerTest {
    MockMvc mockMvc;

    @Mock
    UserService userService;

    @InjectMocks
    UserController userController;

    @BeforeEach
    void setUp(){
        mockMvc = MockMvcBuilders.standaloneSetup(userController).build();
    }

    @Test
    @DisplayName("Test Controller get")
    void get() throws Exception {
        Mockito.when(userService.getById(1)).thenReturn(new User().setName("刘水镜").setEmail("liushuijing@mail.com"));
        BDDMockito.given(userService.getById(1)).willReturn(new User().setName("刘水镜").setEmail("liushuijing@mail.com"));
        mockMvc.perform(MockMvcRequestBuilders.get("/user/{id}", 1)
                        .accept("application/json;charset=UTF-8")
                        .contentType("application/json;charset=UTF-8"))
                .andExpect(MockMvcResultMatchers.status().isOk())
                .andExpect(MockMvcResultMatchers.jsonPath("$.name").value("刘水镜"))
                .andDo(MockMvcResultHandlers.print())
                .andReturn();
        log.info("Test Controller get");
    }
}

全局异常处理

/*
一、@RestControllerAdvice 注解的作用
@RestControllerAdvice 是 Spring Framework 为我们提供的一个复合注解,它是 @ControllerAdvice 和 @ResponseBody 的结合体。

@ControllerAdvice:该注解标志着一个类可以为所有的 @RequestMapping 处理方法提供通用的异常处理和数据绑定等增强功能。当应用到一个类上时,该类中定义的方法将在所有控制器类的请求处理链中生效。

@ResponseBody:表示方法的返回值将被直接写入 HTTP 响应体中,通常配合 Jackson 或 Gson 等 JSON 库将对象转换为 JSON 格式的响应。

因此,@RestControllerAdvice 就是专门为 RESTful 控制器设计的全局异常处理器,它的方法返回值将自动转换为响应体。
*/
“全球”异常
com/pcy/controller/UserController.java
@Operation(summary = "异常查询", description = "异常查询")
    @GetMapping(value = "/{id}")
    public Result<User> get(@PathVariable Integer id) {
        User user = userService.getById(id);
        if (user == null){
            throw new RuntimeException("找不到id信息" + id);
        }
        return Result.success(userService.getById(id));
    }
/*
当输入id信息错误的时候
{
  "code": 200,
  "message": "操作成功",
  "data": {
    "creator": null,
    "modifier": null,
    "createTime": null,
    "updateTime": null,
    "id": 1,
    "name": "潘春尧",
    "age": 1,
    "email": "390@qq.com",
    "birthDay": "2024-08-10"
  }
}


当输入id信息错误的时候
{
  "code": 500,
  "message": "找不到id信息3323",
  "data": null
}
*/
com/pcy/utils/GlobalExceptionHandler.java
package com.pcy.utils;

import com.pcy.entity.MessageEnum;
import com.pcy.entity.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;

@Slf4j
@RestControllerAdvice
public class GlobalExceptionHandler {
//@ExceptionHandler注解用于在Spring MVC控制器中处理特定类型的异常。它可以应用于方法上
//当控制器方法抛出指定类型的异常时,@ExceptionHandler注解的方法将被调用来处理该异常
    @ExceptionHandler(Exception.class)
    public Result<Boolean> globalException(Exception e){
        Result<Boolean> result = new Result<>();
        result.setCode(MessageEnum.ERROR.getCode());
        result.setMessage(e.getMessage() == null ? MessageEnum.ERROR.getMessage() : e.getMessage());
        log.error(e.getMessage(), e);
        return result;
    }
}
com/pcy/entity/MessageEnum.java
package com.pcy.entity;

import lombok.Getter;

@Getter
public enum MessageEnum {
    SUCCESS(200, "操作成功"),
    ERROR(500, "操作失败");

    private final Integer code;
    private final String message;
    MessageEnum(Integer code, String message){
        this.code = code;
        this.message = message;
    }
}
com/pcy/entity/Result.java
package com.pcy.entity;

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

@Data
@NoArgsConstructor
@AllArgsConstructor
public class Result<T> {
   private Integer code;
   private String message;
   private T data;
   // 用于生成一个没有具体数据内容的成功响应
   public static <T> Result<T> success(){
       return success(null);
   }
   // 用于生成包含数据的成功响应
    public static <T> Result<T> success(T data){
        return new Result<>(MessageEnum.SUCCESS.getCode(), MessageEnum.SUCCESS.getMessage(), data);
    }
    // 用于生成一个没有具体错误信息的默认错误响应
    public static<T> Result<T> error(){
        return error(MessageEnum.ERROR);
    }
    // 用于生成带有特定错误信息的错误响应,MessageEnum 是一个枚举类型,包含了不同的错误信息和代码。
    public static<T> Result<T> error(MessageEnum messageEnum){
        return new Result<>(messageEnum.ERROR.getCode(), messageEnum.getMessage(), null);
    }
    // 用于生成包含自定义错误信息的错误响应
    public static <T> Result<T> error(String message) {
        return error(message, MessageEnum.ERROR.getCode());
    }
    // 用于生成包含自定义错误信息和自定义状态码的错误响应
    protected static <T> Result<T> error(String message, Integer code) {
        return new Result<>(code, message, null);
    }
}
写个小异常
com/pcy/controller/ExceptionController.java
package com.pcy.controller;

import com.pcy.entity.Result;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/exception")
@Tag(name = "Exception", description = "异常操作")
public class ExceptionController {
    @GetMapping("/runtimeexception")
    public Result<Boolean> runtimeException(){
        throw new RuntimeException();
    }
}
/*
开启全局异常处理的返回值
{
  "code": 500,
  "message": "操作失败",
  "data": null
}

没有全局异常处理的错误返回值
{
  "timestamp": "2024-08-13T08:21:43.192+00:00",
  "status": 500,
  "error": "Internal Server Error",
  "path": "/exception/runtimeexception"
}
*/
//在SwaggerConfig中添加扫描路径 "/exception/**"  不然接口无法获取
package com.pcy.Swagger;

import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import org.springdoc.core.models.GroupedOpenApi;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
// http://localhost:8080/swagger-ui/index.html
@Configuration
public class SwaggerConfig {

    @Bean
    public GroupedOpenApi createRestApi() {
        return GroupedOpenApi.builder()
                .group("Spring Boot 实战")
                .pathsToMatch("/users/**", "/exception/**")
                // .addPathsToMatch("/exception/**")
                .build();
    }

    @Bean
    public OpenAPI customOpenAPI() {
        return new OpenAPI()
                .info(new Info()
                        .title("Spring Boot 实战")
                        .version("1.0")
                        .description("Spring Boot 实战的 RESTFul 接口文档说明")
                        .contact(new Contact()
                                .name("Pluminary")
                                .url("https://github.com/P-luminary")
                                .email("390415030@qq.com")));
    }
}
你提到的 GlobalExceptionHandler 和 ExceptionController 是用于统一处理 Spring MVC 控制器中的异常。让我逐步分析它们的作用,以及为什么在某些情况下它返回错误值。

//1. GlobalExceptionHandler 的作用
@RestControllerAdvice:这个注解用来全局处理控制器层的异常。它会拦截所有抛出的异常,并根据异常类型调用相应的 @ExceptionHandler 方法。

@ExceptionHandler(Exception.class):这个注解标注的方法会在控制器抛出 Exception 或其子类时执行。它用来捕获并处理全局的异常,比如你代码中的 RuntimeException。

globalException(Exception e):这是一个全局异常处理方法。当控制器中出现 Exception 时,这个方法会被调用。它将返回一个带有错误状态码的 Result<Boolean> 对象,并且会将错误信息记录到日志中。

//2. ExceptionController 的作用
@RestController:声明这个类是一个 Spring MVC 控制器,处理 Web 请求并返回数据。

runtimeException() 方法:在这个方法中,你手动抛出了一个 RuntimeException,这会触发 GlobalExceptionHandler 中的 globalException 方法,并返回一个包含错误信息的 Result<Boolean> 对象。

//3. 为什么只有引用 runtimeException() 才返回错误值
runtimeException() 方法直接抛出了一个 RuntimeException,因此会被 GlobalExceptionHandler 捕获并处理。这就是为什么在访问 /exception/runtimeexception 时,你会看到返回的是错误信息。

//4. 在 get() 方法中返回 200 状态码的原因
在 get() 方法中,如果你传入的 id 是无效的,返回的 Result<User> 仍然会是 Result.success(userService.getById(id)),即使 userService.getById(id) 返回的是 null。这种情况下,你的 Result.success(null) 仍然会返回状态码 200,因为 Result.success() 的设计是用于表示成功状态的,且你没有抛出任何异常。

//5. 如何让 get() 方法在出错时返回错误信息
你可以通过以下方法来确保在 get() 方法中传入无效的 id 时,抛出异常并触发全局异常处理器:

手动抛出异常:

java
复制代码
@GetMapping(value = "/{id}")
public Result<User> get(@PathVariable Integer id) {
    User user = userService.getById(id);
    if (user == null) {
        throw new RuntimeException("User not found with id: " + id);
    }
    return Result.success(user);
}
在 userService.getById(id) 方法中抛出异常:如果你的业务逻辑要求在找不到用户时抛出异常,那么可以在 userService.getById(id) 方法中实现这个逻辑。

//6. 总结
GlobalExceptionHandler 用于捕获和处理全局异常。
当你手动抛出 RuntimeException 或其他异常时,它会捕获并返回带有错误信息的 Result。
在 get() 方法中,如果你想要在找不到用户时返回错误信息,需要手动抛出异常,这样才能触发 GlobalExceptionHandler。

日志级别

方法一:直接编写使用
// [配置方法:一种是直接在application.yml文件中配置、另一种是在外置logback-spring.xml文件中配置]
logging:
  pattern:
    console: "%d - %m%n"
方法二:引用外置xml文件
resources/pom.xml               <引用外部的配置>
<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-logging</artifactId>
</dependency>

resources/application.yaml
logging:
  config: classpath:logback-spring.xml
               <如果你有更多样的配置需求,就需要使用外置XML文件的配置方式>
<?xml version="1.0" encoding="UTF-8" ?>

<configuration>

    <!--    日志文件存放路径-->
    <property name="PATH" value="C:/Users/Pluminary/Desktop/log"/>

    <!-- 彩色日志依赖的渲染类 -->
    <conversionRule conversionWord="clr" converterClass="org.springframework.boot.logging.logback.ColorConverter"/>
    <conversionRule conversionWord="wex"
                    converterClass="org.springframework.boot.logging.logback.WhitespaceThrowableProxyConverter"/>
    <conversionRule conversionWord="wEx"
                    converterClass="org.springframework.boot.logging.logback.ExtendedWhitespaceThrowableProxyConverter"/>
    <!-- 彩色日志格式 -->
    <property name="CONSOLE_LOG_PATTERN"
              value="${CONSOLE_LOG_PATTERN:-%clr(%d{yyyy-MM-dd HH:mm:ss.SSS}){faint} %clr(${LOG_LEVEL_PATTERN:-%5p}) %clr(${PID:- }){magenta} %clr(---){faint} %clr([%15.15t]){faint} %clr(%-40.40logger{39}){cyan} %clr(:){faint} %m%n${LOG_EXCEPTION_CONVERSION_WORD:-%wEx}}"/>
    <!-- 文件日志格式 -->
    <property name="FILE_LOG_PATTERN"
              value="%d{yyyy-MM-dd HH:mm:ss.SSS} [%thread] %-5level %logger{36} -%msg%n"/>

    <!-- 控制台输出配置-->
    <appender name="console" class="ch.qos.logback.core.ConsoleAppender">
        <!--日志输出格式-->
        <layout class="ch.qos.logback.classic.PatternLayout">
            <pattern>
                ${CONSOLE_LOG_PATTERN}
            </pattern>
        </layout>
    </appender>

    <!-- INFO 级别日志文件输出配置-->
    <appender name="info" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!--按级别过滤日志,只输出 INFO 级别-->
        <filter class="ch.qos.logback.classic.filter.LevelFilter">
            <level>INFO</level>
            <onMatch>ACCEPT</onMatch>
            <onMismatch>DENY</onMismatch>
        </filter>
        <!--当天日志文件名-->
        <File>${PATH}/info.log</File>
        <!--按天分割日志文件-->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!--历史日志文件名规则-->
            <fileNamePattern>${PATH}/info.log.%d{yyyy-MM-dd}.%i</fileNamePattern>
            <!--按大小分割同一天的日志-->
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>100MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--日志文件保留天数-->
            <maxHistory>30</maxHistory>
        </rollingPolicy>
        <!--日志输出格式-->
        <layout class="ch.qos.logback.classic.PatternLayout">
            <Pattern>${FILE_LOG_PATTERN}</Pattern>
        </layout>
    </appender>

    <!-- ERROR 级别日志文件输出配置-->
    <appender name="error" class="ch.qos.logback.core.rolling.RollingFileAppender">
        <!--按级别过滤日志,只输出 ERROR 及以上级别-->
        <filter class="ch.qos.logback.classic.filter.ThresholdFilter">
            <level>ERROR</level>
        </filter>
        <!--当天日志文件名-->
        <File>${PATH}/error.log</File>
        <!--按天分割日志文件-->
        <rollingPolicy class="ch.qos.logback.core.rolling.TimeBasedRollingPolicy">
            <!--历史日志文件名规则-->
            <fileNamePattern>${PATH}/error.log.%d{yyyy-MM-dd}.%i</fileNamePattern>
            <!--按大小分割同一天的日志-->
            <timeBasedFileNamingAndTriggeringPolicy class="ch.qos.logback.core.rolling.SizeAndTimeBasedFNATP">
                <maxFileSize>100MB</maxFileSize>
            </timeBasedFileNamingAndTriggeringPolicy>
            <!--日志文件保留天数-->
            <maxHistory>30</maxHistory>
        </rollingPolicy>
        <!--日志输出格式-->
        <layout class="ch.qos.logback.classic.PatternLayout">
            <Pattern>${FILE_LOG_PATTERN}</Pattern>
        </layout>
    </appender>

    <!--日志级别-->
    <root level="info">
        <appender-ref ref="console"/>
        <appender-ref ref="info"/>
        <appender-ref ref="error"/>
    </root>
</configuration>
                   
                   
<
Logback 能够精确区分并输出特定日志级别的错误,是通过 Appender 配置中的 Filter 机制实现的。在你的 Logback 配置文件中,RollingFileAppender 使用了不同的 Filter 来确保只有指定级别的日志信息会被记录到特定的日志文件中。

工作原理
LevelFilter 和 ThresholdFilter:

LevelFilter: 这个过滤器允许你指定只接受特定日志级别的日志。例如,LevelFilter 被配置为只接受 INFO 级别的日志,而拒绝其他级别的日志。<level>INFO</level> 表示只记录 INFO 级别的日志。
ThresholdFilter: 这个过滤器允许你指定一个日志级别的下限,只有高于或等于这个级别的日志才会被记录。例如,ThresholdFilter 被配置为只接受 ERROR 级别及以上的日志(例如 ERROR 和 FATAL)。
日志级别的传递:

日志框架从最底层(比如 TRACE)开始逐级向上检查日志的级别,直到它与 Appender 中配置的 Filter 级别匹配。例如,如果一个 ERROR 级别的日志被触发,RollingFileAppender 的 ThresholdFilter 将检测到这个日志并允许它通过,然后将日志写入指定的 error.log 文件。
日志级别匹配:

当应用程序运行时,它会生成不同级别的日志信息(如 DEBUG、INFO、WARN、ERROR 等)。每个 Appender 都会根据它的 Filter 规则检查这些日志条目。只有符合条件的日志条目才会被记录到相应的日志文件中。
>

AOP切面

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-aop</artifactId>
        </dependency>
com/pcy/controller/AspectController.java
package com.pcy.controller;

import com.pcy.entity.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
@RequestMapping("/aspect")
public class AspectController {
    @GetMapping
    public Result aspect(String message){
        log.info("aspect controller");
        return Result.success(message);
    }
}
com/pcy/Swagger/SwaggerConfig.java //【增加"/aspect/**"】
package com.pcy.Swagger;

import io.swagger.v3.oas.models.OpenAPI;
import io.swagger.v3.oas.models.info.Contact;
import io.swagger.v3.oas.models.info.Info;
import org.springdoc.core.models.GroupedOpenApi;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
// http://localhost:8080/swagger-ui/index.html
@Configuration
public class SwaggerConfig {

    @Bean
    public GroupedOpenApi createRestApi() {
        return GroupedOpenApi.builder()
                .group("Spring Boot 实战")
                .pathsToMatch("/users/**", "/exception/**","/aspect/**")
                // .addPathsToMatch("/exception/**")
                .build();
    }

    @Bean
    public OpenAPI customOpenAPI() {
        return new OpenAPI()
                .info(new Info()
                        .title("Spring Boot 实战")
                        .version("1.0")
                        .description("Spring Boot 实战的 RESTFul 接口文档说明")
                        .contact(new Contact()
                                .name("Pluminary")
                                .url("https://github.com/P-luminary")
                                .email("390415030@qq.com")));
    }
}
com/pcy/utils/WebAspect.java
package com.pcy.utils;

import com.pcy.entity.Result;
import jakarta.servlet.http.HttpServletRequest;
import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.JoinPoint;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.aspectj.lang.reflect.MethodSignature;
import org.springframework.stereotype.Component;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;

import java.util.HashMap;
import java.util.Map;

@Slf4j
@Aspect
@Component
public class WebAspect {
// ★★★★★★★★★★★ 一定要注意这个AOP切面扫描的包 ★★★★★★★★★★★
    @Pointcut("execution(public * com.pcy.controller.*.*(..))")
    public void pointCut() {
    }

    @Before(value = "pointCut()")
    public void before(JoinPoint joinPoint) {
        System.out.println("======================================== 这是@Before ========================================");
        String methodName = joinPoint.getSignature().getName();
        String className = joinPoint.getTarget().getClass().getName();
        Object[] args = joinPoint.getArgs();
        String[] parameterNames = ((MethodSignature) joinPoint.getSignature()).getParameterNames();

        HttpServletRequest request = ((ServletRequestAttributes) RequestContextHolder.getRequestAttributes()).getRequest();

        Map<String, Object> paramMap = new HashMap<>();
        for (int i = 0; i < parameterNames.length; i++) {
            paramMap.put(parameterNames[i], args[i]);
        }

        log.info("before path:{}",request.getServletPath());
        log.info("before class name:{}",className);
        log.info("before method name:{}",methodName);
        log.info("before args:{}",paramMap.toString());
    }

    @After(value = "pointCut()")
    public void after(JoinPoint joinPoint) {
        System.out.println("======================================== 这是@After =========================================");
        log.info("{} after", joinPoint.getSignature().getName());
    }

    @AfterReturning(value = "pointCut()", returning = "returnVal")
    public void afterReturning(JoinPoint  joinPoint, Object returnVal) {
        System.out.println("==================================== 这是@AfterReturning ====================================");
        log.info("{} after return, returnVal: {}", joinPoint.getSignature().getName(), returnVal);
    }
}

/*
2024-08-14 18:28:20.249  INFO 3296 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
2024-08-14 18:28:20.249  INFO 3296 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2024-08-14 18:28:20.250  INFO 3296 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 1 ms
2024-08-14 18:28:20.261  INFO 3296 --- [nio-8080-exec-1] c.pcy.HandlerInterceptor.LogInterceptor  : preHandle开始时间:18:28:20:261 毫秒
======================================== 这是@Before ========================================
2024-08-14 18:28:20.278  INFO 3296 --- [nio-8080-exec-1] com.pcy.utils.WebAspect                  : before path:/aspect
2024-08-14 18:28:20.278  INFO 3296 --- [nio-8080-exec-1] com.pcy.utils.WebAspect                  : before class name:com.pcy.controller.AspectController
2024-08-14 18:28:20.278  INFO 3296 --- [nio-8080-exec-1] com.pcy.utils.WebAspect                  : before method name:aspect
2024-08-14 18:28:20.278  INFO 3296 --- [nio-8080-exec-1] com.pcy.utils.WebAspect                  : before args:{message=www}
2024-08-14 18:28:20.278  INFO 3296 --- [nio-8080-exec-1] com.pcy.controller.AspectController      : aspect controller
==================================== 这是@AfterReturning ====================================
2024-08-14 18:28:20.279  INFO 3296 --- [nio-8080-exec-1] com.pcy.utils.WebAspect                  : aspect after return, returnVal: Result(code=200, message=操作成功, data=www)
======================================== 这是@After =========================================
2024-08-14 18:28:20.280  INFO 3296 --- [nio-8080-exec-1] com.pcy.utils.WebAspect                  : aspect after
2024-08-14 18:28:20.308  INFO 3296 --- [nio-8080-exec-1] c.pcy.HandlerInterceptor.LogInterceptor  : postHandle结束时间:18:28:20:308 毫秒
2024-08-14 18:28:20.308  INFO 3296 --- [nio-8080-exec-1] c.pcy.HandlerInterceptor.LogInterceptor  : afterCompletion
2024-08-14 18:28:20.309  INFO 3296 --- [nio-8080-exec-1] c.pcy.HandlerInterceptor.LogInterceptor  : 接口运行时间:47 毫秒

*/
若是调用UserController的get接口
com/pcy/controller/UserController.java
...
    @Operation(summary = "根据ID获取用户信息", description = "通过用户ID获取用户详细信息")
    @GetMapping("/user/{id}")
    public User get(@PathVariable int id) {
        return userRepository.findById(id).orElse(null);
    }
...

Console控制台的报错信息:
/*
2024-08-14 18:33:22.383  INFO 3296 --- [nio-8080-exec-9] c.pcy.HandlerInterceptor.LogInterceptor  : preHandle开始时间:18:33:22:383 毫秒
======================================== 这是@Before ========================================
2024-08-14 18:33:22.385  INFO 3296 --- [nio-8080-exec-9] com.pcy.utils.WebAspect                  : before path:/users/user/2
2024-08-14 18:33:22.385  INFO 3296 --- [nio-8080-exec-9] com.pcy.utils.WebAspect                  : before class name:com.pcy.controller.UserController
2024-08-14 18:33:22.385  INFO 3296 --- [nio-8080-exec-9] com.pcy.utils.WebAspect                  : before method name:get
2024-08-14 18:33:22.385  INFO 3296 --- [nio-8080-exec-9] com.pcy.utils.WebAspect                  : before args:{id=2}
==================================== 这是@AfterReturning ====================================
2024-08-14 18:33:22.425  INFO 3296 --- [nio-8080-exec-9] com.pcy.utils.WebAspect                  : get after return, returnVal: User(id=2, name=we2, age=2, email=2, birthDay=2024-08-10)
======================================== 这是@After =========================================
2024-08-14 18:33:22.426  INFO 3296 --- [nio-8080-exec-9] com.pcy.utils.WebAspect                  : get after
2024-08-14 18:33:22.428  INFO 3296 --- [nio-8080-exec-9] c.pcy.HandlerInterceptor.LogInterceptor  : postHandle结束时间:18:33:22:428 毫秒
2024-08-14 18:33:22.428  INFO 3296 --- [nio-8080-exec-9] c.pcy.HandlerInterceptor.LogInterceptor  : afterCompletion
2024-08-14 18:33:22.428  INFO 3296 --- [nio-8080-exec-9] c.pcy.HandlerInterceptor.LogInterceptor  : 接口运行时间:45 毫秒
*/
异常善后处理
com/pcy/controller/AspectController.java //【浏览exception接口的时候会报错】
package com.pcy.controller;

import com.pcy.entity.Result;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@Slf4j
@RestController
@RequestMapping("/aspect")
public class AspectController {
    @GetMapping
    public Result aspect(String message){
        log.info("aspect controller");
        return Result.success(message);
    }
    @GetMapping("/exception")
    public Result exception(){//抛出异常
        throw new RuntimeException("runtime exception");
    }
}


/*
======================================== 这是@Before ========================================
2024-08-15 15:09:20.586  INFO 4200 --- [nio-8080-exec-4] com.pcy.utils.WebAspect                  : before path:/aspect/exception
2024-08-15 15:09:20.586  INFO 4200 --- [nio-8080-exec-4] com.pcy.utils.WebAspect                  : before class name:com.pcy.controller.AspectController
2024-08-15 15:09:20.586  INFO 4200 --- [nio-8080-exec-4] com.pcy.utils.WebAspect                  : before method name:exception
2024-08-15 15:09:20.587  INFO 4200 --- [nio-8080-exec-4] com.pcy.utils.WebAspect                  : before args:{}
2024-08-15 15:09:20.587  INFO 4200 --- [nio-8080-exec-4] com.pcy.utils.WebAspect                  : exception after throwing, message: runtime exception
======================================== 这是@After =========================================
2024-08-15 15:09:20.587  INFO 4200 --- [nio-8080-exec-4] com.pcy.utils.WebAspect                  : exception after
2024-08-15 15:09:20.588 ERROR 4200 --- [nio-8080-exec-4] com.pcy.utils.GlobalExceptionHandler     : runtime exception
*/
com/pcy/utils/WebAspect.java
@AfterThrowing(value = "pointCut()", throwing = "e")
    public void afterThrowing(JoinPoint  joinPoint, Exception e) {
        log.info("{} after throwing, message: {}", joinPoint.getSignature().getName(), e.getMessage());
    }
综上所述:after方法不关心方法是否成功,当方法执行完成之后就会被执行;afterReturning方法必须在目标方法成果return之后才会被执行;afterThrowing方法则会在目标方法抛出异常后被执行
性能统计

Around可以囊括以上所有能力

com/pcy/controller/AspectController.java
@Slf4j
@RestController
@RequestMapping("/aspect")
public class AspectController {
  @GetMapping("/sleep/{time}")
    public Result sleep(@PathVariable("time") long time) {
        log.info("sleep");
        try {
            Thread.sleep(time);
        } catch (InterruptedException e) {
           log.error("error", e);
        }
        if (time == 1000) {
            throw new RuntimeException("runtime exception");
        }
        log.info("wake up");
        return Result.success("wake up");
    }
}
@Around("pointCut()")
    public Object around(ProceedingJoinPoint joinPoint) {
        log.info("around start");
        long startTime = System.currentTimeMillis();
        Object result = null;
        try {
            result = joinPoint.proceed();
        } catch (Throwable e) {
            log.error("around error",e);
        }
        long endTime = System.currentTimeMillis();
        log.info("execute time:{} ms",endTime - startTime);
        return result;
    }
//【当输入time值为2004时】
2024-08-15 15:27:21.987  INFO 10844 --- [nio-8080-exec-6] c.pcy.HandlerInterceptor.LogInterceptor  : preHandle开始时间:15:27:21:987 毫秒
2024-08-15 15:27:21.990  INFO 10844 --- [nio-8080-exec-6] com.pcy.utils.WebAspect                  : around start
======================================== 这是@Before ========================================
2024-08-15 15:27:21.991  INFO 10844 --- [nio-8080-exec-6] com.pcy.utils.WebAspect                  : before path:/aspect/sleep/2004
2024-08-15 15:27:21.991  INFO 10844 --- [nio-8080-exec-6] com.pcy.utils.WebAspect                  : before class name:com.pcy.controller.AspectController
2024-08-15 15:27:21.991  INFO 10844 --- [nio-8080-exec-6] com.pcy.utils.WebAspect                  : before method name:sleep
2024-08-15 15:27:21.991  INFO 10844 --- [nio-8080-exec-6] com.pcy.utils.WebAspect                  : before args:{time=2004}
2024-08-15 15:27:21.991  INFO 10844 --- [nio-8080-exec-6] com.pcy.controller.AspectController      : sleep
2024-08-15 15:27:23.996  INFO 10844 --- [nio-8080-exec-6] com.pcy.controller.AspectController      : wake up
==================================== 这是@AfterReturning ====================================
2024-08-15 15:27:23.997  INFO 10844 --- [nio-8080-exec-6] com.pcy.utils.WebAspect                  : sleep after return, returnVal: Result(code=200, message=操作成功, data=wake up)
======================================== 这是@After =========================================
2024-08-15 15:27:23.997  INFO 10844 --- [nio-8080-exec-6] com.pcy.utils.WebAspect                  : sleep after
2024-08-15 15:27:23.997  INFO 10844 --- [nio-8080-exec-6] com.pcy.utils.WebAspect                  : execute time:2007 ms
2024-08-15 15:27:23.999  INFO 10844 --- [nio-8080-exec-6] c.pcy.HandlerInterceptor.LogInterceptor  : postHandle结束时间:15:27:23:999 毫秒
2024-08-15 15:27:23.999  INFO 10844 --- [nio-8080-exec-6] c.pcy.HandlerInterceptor.LogInterceptor  : afterCompletion
2024-08-15 15:27:23.999  INFO 10844 --- [nio-8080-exec-6] c.pcy.HandlerInterceptor.LogInterceptor  : 接口运行时间:12 毫秒


//【当输入time值为1000时】
2024-08-15 15:28:19.596  INFO 10844 --- [nio-8080-exec-9] c.pcy.HandlerInterceptor.LogInterceptor  : preHandle开始时间:15:28:19:596 毫秒
2024-08-15 15:28:19.597  INFO 10844 --- [nio-8080-exec-9] com.pcy.utils.WebAspect                  : around start
======================================== 这是@Before ========================================
2024-08-15 15:28:19.597  INFO 10844 --- [nio-8080-exec-9] com.pcy.utils.WebAspect                  : before path:/aspect/sleep/1000
2024-08-15 15:28:19.597  INFO 10844 --- [nio-8080-exec-9] com.pcy.utils.WebAspect                  : before class name:com.pcy.controller.AspectController
2024-08-15 15:28:19.597  INFO 10844 --- [nio-8080-exec-9] com.pcy.utils.WebAspect                  : before method name:sleep
2024-08-15 15:28:19.597  INFO 10844 --- [nio-8080-exec-9] com.pcy.utils.WebAspect                  : before args:{time=1000}
2024-08-15 15:28:19.597  INFO 10844 --- [nio-8080-exec-9] com.pcy.controller.AspectController      : sleep
2024-08-15 15:28:20.607  INFO 10844 --- [nio-8080-exec-9] com.pcy.utils.WebAspect                  : sleep after throwing, message: runtime exception
======================================== 这是@After =========================================
2024-08-15 15:28:20.607  INFO 10844 --- [nio-8080-exec-9] com.pcy.utils.WebAspect                  : sleep after
2024-08-15 15:28:20.607 ERROR 10844 --- [nio-8080-exec-9] com.pcy.utils.WebAspect                  : around error

java.lang.RuntimeException: runtime exception
    at com.pcy.controller.AspectController.sleep(AspectController.java:32)
    at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:104)
    at java.base/java.lang.reflect.Method.invoke(Method.java:578)
......
同一切面内的执行顺序

先执行before方法,再执行afterReturning / afterThrowing方法,最后执行after方法
要验证的关键点是around方法和它们之间的先后关系

around方法早于before方法开始执行,并且晚于after方法结束执行,刚好将其他同志完全包裹了起来

//【注释掉WebAspect.java里面的代码不然会叠叠乐累加】
com/pcy/utils/AspectOne.java
package com.pcy.utils;

import lombok.extern.slf4j.Slf4j;
import org.aspectj.lang.ProceedingJoinPoint;
import org.aspectj.lang.annotation.*;
import org.springframework.stereotype.Component;

@Slf4j
@Aspect
@Component
public class AspectOne {
    @Pointcut("execution(public * com.pcy.controller.*.*(..))")
    public void pointCut(){}
    @Before(value = "pointCut()")
    public void before(){
        log.info("before one");
    }
    @After(value = "pointCut()")
    public void after(){
        log.info("after one");
    }
    @AfterReturning(value = "pointCut()")
    public void afterReturning(){
        log.info("afterReturning one");
    }

    @Around(value = "pointCut()")
    public Object around(ProceedingJoinPoint joinPoint) {
        log.info("around one start");
        Object result = null;
        try {
            result = joinPoint.proceed();
        } catch (Throwable e) {
            log.error("around error", e);
        }
        log.info("around one end");
        return result;
    }
}

/*
2024-08-15 16:12:12.819  INFO 28788 --- [nio-8080-exec-1] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring DispatcherServlet 'dispatcherServlet'
2024-08-15 16:12:12.819  INFO 28788 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Initializing Servlet 'dispatcherServlet'
2024-08-15 16:12:12.820  INFO 28788 --- [nio-8080-exec-1] o.s.web.servlet.DispatcherServlet        : Completed initialization in 0 ms
2024-08-15 16:12:12.839  INFO 28788 --- [nio-8080-exec-1] c.pcy.HandlerInterceptor.LogInterceptor  : preHandle开始时间:16:12:12:839 毫秒
2024-08-15 16:12:12.864  INFO 28788 --- [nio-8080-exec-1] com.pcy.utils.AspectOne                  : around one start
2024-08-15 16:12:12.864  INFO 28788 --- [nio-8080-exec-1] com.pcy.utils.AspectOne                  : before one
2024-08-15 16:12:12.864  INFO 28788 --- [nio-8080-exec-1] com.pcy.controller.AspectController      : aspect controller
2024-08-15 16:12:12.865  INFO 28788 --- [nio-8080-exec-1] com.pcy.utils.AspectOne                  : afterReturning one
2024-08-15 16:12:12.865  INFO 28788 --- [nio-8080-exec-1] com.pcy.utils.AspectOne                  : after one
2024-08-15 16:12:12.865  INFO 28788 --- [nio-8080-exec-1] com.pcy.utils.AspectOne                  : around one end
2024-08-15 16:12:12.908  INFO 28788 --- [nio-8080-exec-1] c.pcy.HandlerInterceptor.LogInterceptor  : postHandle结束时间:16:12:12:908 毫秒
2024-08-15 16:12:12.909  INFO 28788 --- [nio-8080-exec-1] c.pcy.HandlerInterceptor.LogInterceptor  : afterCompletion
2024-08-15 16:12:12.909  INFO 28788 --- [nio-8080-exec-1] c.pcy.HandlerInterceptor.LogInterceptor  : 接口运行时间:69 毫秒
*/
不同切面间的执行顺序

将AspectOne复制两份命名AspectTwo和AspectThree [执行后是One→Three→Two]
在Spring中的加载顺序是根据类名升序排列的,Three字母排序排在Two前面
那如何指定执行顺序按照One Two Three?
分别为AspectOne/Two/Three加上@Order(1),@Order(2),@Order(3)

Redis

集成
pom.xml
       <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-data-redis</artifactId>
            <exclusions>
                <exclusion>
                    <groupId>io.lettuce</groupId>
                    <artifactId>lettuce-core</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>redis.clients</groupId>
            <artifactId>jedis</artifactId>
        </dependency>
spring:
  application:
    name: Pluminary
  datasource:
    driver-class-name: org.mariadb.jdbc.Driver
    url: jdbc:mariadb://localhost:3306/pcy?useUnicode=true&characterEncoding=UTF-8&autoReconnect=true&nullCatalogMeansCurrent=true
    username: root
    password: root

    redis:
      host: localhost port:6379
      connect-timeout: 1000
      jedis:
        pool:
          min-idle: 5
          max-active: 10
          max-idle: 10
          max-wait: 2000
com/pcy/controller/HelloController.java
@Slf4j
@RestController
@RequestMapping("/test")
public class HelloController {
 @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @GetMapping("/hello")
    public String hello(){
        stringRedisTemplate.opsForValue().set("hello","world");
        return stringRedisTemplate.opsForValue().get("hello");
    }
}

//先访问hello接口 再去redis-cli中尝试访问自己定义的内容
http://localhost:8080/swagger-ui/index.html#/hello-controller/hello

/*
127.0.0.1:6379> get hello
"world"
*/

Spring Security

pom.xml
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-security</artifactId>
        </dependency>
com/pcy/controller/HelloController.java
package com.pcy.controller;

import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.redis.core.StringRedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
@Slf4j
@RestController
@RequestMapping("/test")
public class HelloController {
    @GetMapping("/hi")
//  http://localhost:8080/hi
    public String hi(){
        log.info("hi");
        return "ok!";
    }

    @Autowired
    private StringRedisTemplate stringRedisTemplate;
    @GetMapping("/hello")
    public String hello(){
        stringRedisTemplate.opsForValue().set("hello","world");
        return stringRedisTemplate.opsForValue().get("hello");
    }
}

/* Console:
Using generated security password: 4147707e-58d6-46d9-b5cc-19865a2c523f
*/

账号:user
密码:4147707e-58d6-46d9-b5cc-19865a2c523f
com/pcy/config/SecurityConfig.java
package com.pcy.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

import static org.springframework.security.config.Customizer.withDefaults;

@Configuration
@EnableWebSecurity
public class SecurityConfig {

    // 配置HTTP安全性
    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .authorizeRequests(authorizeRequests ->
                        authorizeRequests
//          .antMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll() // 允许访问Swagger UI和API文档
                                .anyRequest().authenticated() // 所有请求都需要认证
                )
                .httpBasic(withDefaults()); // 使用HTTP Basic认证

        return http.build();
    }

    // 配置认证管理器
    @Bean
    public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
        AuthenticationManagerBuilder authenticationManagerBuilder =
                http.getSharedObject(AuthenticationManagerBuilder.class);

        authenticationManagerBuilder
                .inMemoryAuthentication()
                .withUser("pcy")
                .password(passwordEncoder().encode("123456"))
                .roles("admin");

        return authenticationManagerBuilder.build();
    }

    // 配置密码编码器
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}

/*
你可能无法访问 http://localhost:8080/swagger-ui/index.html 的原因可能与 Spring Security 配置有关。由于你启用了 Spring Security,默认情况下,所有请求都需要经过身份认证,这可能会阻止你访问 Swagger UI。

为了确保你能够访问 Swagger UI,你需要在 Spring Security 的配置中添加一个例外规则,允许对 /swagger-ui/** 和相关的 Swagger 资源进行无认证访问。

添加代码:.antMatchers("/swagger-ui/**", "/v3/api-docs/**").permitAll()


账号:pcy
密码:123456
*/

从数据库中获取用户信息

com/pcy/config/SecurityConfig.java
package com.pcy.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

import static org.springframework.security.config.Customizer.withDefaults;

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Autowired
    private UserDetailsService userDetailsService; // 使用 Spring Security 的 UserDetailsService

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests(authorizeRequests ->
                        authorizeRequests
                                .anyRequest().authenticated()
                )
                .userDetailsService(userDetailsService) // 设置 UserDetailsService
                .httpBasic(withDefaults())
                .csrf(csrf -> csrf.disable());

        return http.build();
    }

    // 配置认证管理器
    @Bean
    public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
        AuthenticationManagerBuilder authenticationManagerBuilder =
                http.getSharedObject(AuthenticationManagerBuilder.class);

        authenticationManagerBuilder
                .userDetailsService(userDetailsService)  // 使用数据库中的用户信息
                .passwordEncoder(passwordEncoder());

        return authenticationManagerBuilder.build();
    }

    // 配置密码编码器
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
com/pcy/service/impl/UserDetailsServiceImpl.java
package com.pcy.service.impl;

import com.baomidou.mybatisplus.core.toolkit.Wrappers;
import com.pcy.entity.SysUser;
import com.pcy.service.SysUserService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.security.core.authority.AuthorityUtils;
import org.springframework.security.core.userdetails.User;
import org.springframework.security.core.userdetails.UserDetails;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.core.userdetails.UsernameNotFoundException;
import org.springframework.stereotype.Service;
//确保你的 UserDetailsServiceImpl 类被 Spring 管理,且实现了 Spring Security 的 UserDetailsService 接口
@Service
public class UserDetailsServiceImpl implements UserDetailsService  {

    @Autowired
    private SysUserService sysUserService;

    @Override
    public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
        SysUser sysUser = sysUserService.getOne(Wrappers.<SysUser>lambdaQuery().eq(SysUser::getUsername, username));
        if (sysUser == null) {
            throw new UsernameNotFoundException("User not found with username: " + username);
        }
        return User.builder()
                .username(sysUser.getUsername())
                .password(sysUser.getPassword())
                .authorities(AuthorityUtils.commaSeparatedStringToAuthorityList(sysUser.getRole()))
                .build();
    }
}
com/pcy/entity/SysUser.java
package com.pcy.entity;
import com.baomidou.mybatisplus.annotation.IdType;
import com.baomidou.mybatisplus.annotation.TableId;
import com.baomidou.mybatisplus.extension.activerecord.Model;

import io.swagger.v3.oas.annotations.media.Schema;
import lombok.Data;
import lombok.EqualsAndHashCode;
import lombok.experimental.Accessors;

import java.io.Serializable;

@Data
@EqualsAndHashCode(callSuper = false)
@Accessors(chain = true)
@Schema(name = "SysUser对象", description = "系统用户表")
public class SysUser extends Model<SysUser> {

    private static final long serialVersionUID = 1L;

    @Schema(description = "主键 id")
    @TableId(value = "id", type = IdType.AUTO)
    private Integer id;

    @Schema(description = "用户名")
    private String username;

    @Schema(description = "密码")
    private String password;

    @Schema(description = "角色")
    private String role;


    @Override
    public Serializable pkVal() {
        return this.id;
    }
}
com/pcy/service/SysUserService.java
package com.pcy.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.pcy.entity.SysUser;

public interface SysUserService extends IService<SysUser> {
    String getCurrentUser();
}
com/pcy/service/impl/SysUserServiceImpl.java
package com.pcy.service.impl;

import com.baomidou.mybatisplus.extension.service.impl.ServiceImpl;
import com.pcy.common.ApiException;
import com.pcy.entity.SysUser;
import com.pcy.mapper.SysUserMapper;
import com.pcy.service.SysUserService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.security.authentication.AnonymousAuthenticationToken;
import org.springframework.security.core.Authentication;
import org.springframework.security.core.context.SecurityContextHolder;
import org.springframework.stereotype.Service;
@Slf4j
@Service
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements SysUserService {
    @Override
    public String getCurrentUser() {
        Authentication authentication = SecurityContextHolder.getContext().getAuthentication();
        // 非匿名用户访问才能获得用户信息
        if (!(authentication instanceof AnonymousAuthenticationToken)) {
            String userName = authentication.getName();
            log.info("userName by SecurityContextHolder: {}", userName);
            return userName;
        }
        throw new ApiException("用户不存在!");
    }
}
com/pcy/mapper/SysUserMapper.java
package com.pcy.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.pcy.entity.SysUser;

/**
 * <p>
 * 系统用户表 Mapper 接口
 * </p>
 */
public interface SysUserMapper extends BaseMapper<SysUser> {

}
com/pcy/common/ApiException.java
package com.pcy.common;

import com.pcy.entity.MessageEnum;
import lombok.Data;

@Data
public class ApiException extends RuntimeException {

    private Integer code;

    public ApiException(MessageEnum messageEnum) {
        super(messageEnum.getMessage());
        this.code = messageEnum.getCode();
    }

    public ApiException(String message) {
        super(message);
        this.code = 500;
    }
}
//【由于数据库的密码要被加密后的形式保存到数据中】
com/pcy/common/test.java
package com.pcy.common;

import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;

public class test {
    public static void main(String[] args) {
        BCryptPasswordEncoder encoder = new BCryptPasswordEncoder();
        String encodedPassword = encoder.encode("123456");
        System.out.println(encodedPassword);
    }
}

//$2a$10$GzDPdLyrzC9NudmE937AAetR2bef2VQzuSbP6KM6Y.I3045OuT/xC
修改创建SysUser用户的时候用Spring Security [登录的时候就可以用自己创建的了]
com/pcy/controller/UserController.java
/* 对比User数据
    @Operation(summary = "创建User用户", description = "创建一个新的User用户")
    @PostMapping("/create/")
    public User create(@RequestBody User User) {
        return userRepository.save(User);
    }
*/
    @Autowired
    private SysUserService sysUserService;

    @Autowired
    private PasswordEncoder passwordEncoder;

    @Operation(summary = "创建SysUser用户", description = "创建一个新的SysUser用户")
    @PostMapping("/create/test")
    public SysUser create(@RequestBody SysUser sysUser) {
        sysUser.setPassword(passwordEncoder.encode(sysUser.getPassword()));
        sysUserService.save(sysUser);
        return sysUser;
    }
package com.pcy.service;

import com.baomidou.mybatisplus.extension.service.IService;
import com.pcy.entity.SysUser;
public interface SysUserService extends IService<SysUser>{

    String getCurrentUser();
}
@Slf4j
@Service
public class SysUserServiceImpl extends ServiceImpl<SysUserMapper, SysUser> implements SysUserService {

 @Override
    public boolean save(SysUser sysUser) {
        return SqlHelper.retBool(this.baseMapper.insert(sysUser));
    }
}
com/pcy/mapper/SysUserMapper.java
package com.pcy.mapper;

import com.baomidou.mybatisplus.core.mapper.BaseMapper;
import com.pcy.entity.SysUser;

/**
 * <p>
 * 系统用户表 Mapper 接口
 * </p>
 */
public interface SysUserMapper extends BaseMapper<SysUser> {

}
权限控制
com/pcy/config/SecurityConfig.java
package com.pcy.config;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.security.authentication.AuthenticationManager;
import org.springframework.security.config.annotation.authentication.builders.AuthenticationManagerBuilder;
import org.springframework.security.config.annotation.web.builders.HttpSecurity;
import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity;
import org.springframework.security.core.userdetails.UserDetailsService;
import org.springframework.security.crypto.bcrypt.BCryptPasswordEncoder;
import org.springframework.security.crypto.password.PasswordEncoder;
import org.springframework.security.web.SecurityFilterChain;

import static org.springframework.security.config.Customizer.withDefaults;

@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Autowired
    private UserDetailsService userDetailsService; // 使用 Spring Security 的 UserDetailsService

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests(authorizeRequests -> authorizeRequests
                        .requestMatchers("/security/permitall").permitAll() // 允许所有人访问
                        .requestMatchers("/security/anonymous").anonymous() // 仅允许匿名用户访问
                        .requestMatchers("/security/config").hasAuthority("ROLE_config") // 仅拥有 ROLE_config 权限的用户可以访问
                        .requestMatchers("/security/Secured").hasRole("Secured") // 仅拥有 ROLE_Secured 的用户可以访问
                        .requestMatchers("/security/preAuthorize").hasAuthority("PreAuthorize") // 仅拥有 PreAuthorize 权限的用户可以访问
                        .anyRequest().authenticated() // 其他所有请求需要认证
                )
                .userDetailsService(userDetailsService) // 设置 UserDetailsService
                .httpBasic(withDefaults()) // 使用 HTTP Basic 认证
                .csrf(csrf -> csrf.disable()); // 禁用 CSRF

        return http.build();
    }

    // 配置认证管理器
    @Bean
    public AuthenticationManager authenticationManager(HttpSecurity http) throws Exception {
        AuthenticationManagerBuilder authenticationManagerBuilder =
                http.getSharedObject(AuthenticationManagerBuilder.class);

        authenticationManagerBuilder
                .userDetailsService(userDetailsService)  // 使用数据库中的用户信息
                .passwordEncoder(passwordEncoder());

        return authenticationManagerBuilder.build();
    }

    // 配置密码编码器
    @Bean
    public PasswordEncoder passwordEncoder() {
        return new BCryptPasswordEncoder();
    }
}
com/pcy/controller/SecurityController.java
package com.pcy.controller;

import com.pcy.entity.Result;
import io.swagger.v3.oas.annotations.Operation;
import io.swagger.v3.oas.annotations.tags.Tag;
import org.springframework.security.access.annotation.Secured;
import org.springframework.security.access.prepost.PreAuthorize;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/security")
@Tag(name = "权限控制", description = "权限控制")
public class SecurityController {
    // Anyone
    @Operation(summary = "permitAll 权限")
    @GetMapping(value = "/permitall")
    public Result<String> permitAll(){
        return Result.success("permitAll");
    }
    // 未登录时可以访问
    @Operation(summary = "anonymous 权限")
    @GetMapping(value = "/anonymous")
    public Result<String> anonymous(){
        return Result.success("anonymous");
    }
    // xiaopan可以访问
    @Operation(summary = "config 权限")
    @GetMapping(value = "/config")
    public Result<String> config(){
        return Result.success("permitAll");
    }
    // xiaochun可以访问
    @Operation(summary = "Secured 权限")
    @GetMapping(value = "/Secured")
    @Secured({"ROLE_Secured"})
    public Result<String> Secured(){
        return Result.success("Secured");
    }
    // panchunyao可以访问
    @Operation(summary = "PreAuthorize 权限")
    @GetMapping(value = "/preAuthorize")
    @PreAuthorize("hasAnyAuthority('PreAuthorize')")
    public Result<String> PreAuthorize(){
        return Result.success("PreAuthorize");
    }
}

/*
首先,确保在数据库中创建几个测试用户,并为每个用户分配不同的角色或权限。假设你有以下几个用户:
User 1: Username: xiaopan, Password: 123456, Role: ROLE_config
User 2: Username: xiaochun, Password: 123456, Role: ROLE_Secured
User 3: Username: panchun, Password: 123456, Authority: PreAuthorize

尝试使用不同用户登录:
使用 xiaopan 登录后,尝试访问 /security/config。
使用 xiaochun 登录后,尝试访问 /security/Secured。
使用 panchunyao 登录后,尝试访问 /security/preAuthorize。

检查响应:
/security/config: 只有 xiaopan 能访问,其他用户会被拒绝访问。
/security/Secured: 只有 xiaochun 能访问,其他用户会被拒绝访问。
/security/preAuthorize: 只有 panchunyao 能访问,其他用户会被拒绝访问。
/security/permitall: 所有用户都可以访问。
/security/anonymous: 只有未登录的用户可以访问,登录的用户会被拒绝。

验证权限控制
每个请求的响应应该反映你在 SecurityConfig 中配置的权限。
如果用户没有适当的角色或权限,应该会返回 403 Forbidden 或其他错误响应。
*/
@Configuration
public class SwaggerConfig {

    @Bean
    public GroupedOpenApi createRestApi() {
        return GroupedOpenApi.builder()
                .group("Spring Boot 实战")
                .pathsToMatch("/users/**", "/exception/**","/aspect/**","/test/**","/security/**")
                // .addPathsToMatch("/exception/**")
                .build();
    }
}
// 【问答环节】
我的数据库创建的是Role字段但是为什么 下面这些有.hasAuthority 有.hasRole 还有其他的 这是怎么匹配到我数据库 按照你的方式设置的数据的

/*
1. hasRole() 和 hasAuthority() 的区别

hasRole(String role):
hasRole 方法通常用于检查用户是否拥有特定的角色。
Spring Security 会在你传递的角色名称前自动加上 "ROLE_" 前缀。因此,当你使用 hasRole("Secured") 时,实际上它会检查用户是否有 "ROLE_Secured" 这个权限。

hasAuthority(String authority):
hasAuthority 方法用于检查用户是否拥有特定的权限(或授权)。
hasAuthority 不会自动添加任何前缀。所以当你使用 hasAuthority("ROLE_config") 时,它会直接匹配 "ROLE_config",而不会添加任何前缀。


2. 匹配数据库中的角色和权限
hasRole("Secured"):
代码中的 hasRole("Secured") 实际上会匹配数据库中的 ROLE_Secured,因为 hasRole 方法会自动加上 "ROLE_" 前缀。

hasAuthority("ROLE_config"):
代码中的 hasAuthority("ROLE_config") 会直接匹配数据库中的 "ROLE_config",没有任何前缀变化。

hasAuthority("PreAuthorize"):
代码中的 hasAuthority("PreAuthorize") 会直接匹配数据库中的 "PreAuthorize",因为没有添加任何前缀。
*/
    
它为什么能查到我数据库的role字段里面的数据 如果我把这个字段换成test这个名字 它又是怎么去匹配到的呢
/*
Spring Security 默认会使用 UserDetails 接口中的 getAuthorities() 方法来获取用户的权限或角色信息。这些权限或角色信息通常是通过你在 UserDetailsService 实现类中定义的逻辑从数据库中获取的。
而在SecurityConfig中有代码:
@Autowired // 使用 Spring Security 的UserDetailsService
private UserDetailsService userDetailsService; 
回顾securityFilterChain代码
下面会有 .userDetailsService(userDetailsService) // 设置 UserDetailsService

Spring Security 本身并不直接访问你的数据库表或字段。它依赖于你在 UserDetailsService 中提供的 UserDetails 对象的 getAuthorities() 方法的返回值。因此,当你在 SecurityConfig 中使用 hasRole() 或 hasAuthority() 方法时,它实际上是在检查用户的权限信息,即 UserDetails 对象中的 authorities。
===========================================================================
@Override
public UserDetails loadUserByUsername(String username) throws UsernameNotFoundException {
    SysUser sysUser = sysUserService.getOne(Wrappers.<SysUser>lambdaQuery().eq(SysUser::getUsername, username));
    if (sysUser == null) {
        throw new UsernameNotFoundException("User not found with username: " + username);
    }
    return User.builder()
            .username(sysUser.getUsername())
            .password(sysUser.getPassword())
            .authorities(AuthorityUtils.commaSeparatedStringToAuthorityList(sysUser.getTest()))  // 修改为使用 'test' 字段
            .build();
}

*/

记住我 √ Remember Me

基于SpringSession的方式
pom.xml
        <dependency>
            <groupId>org.springframework.session</groupId>
            <artifactId>spring-session-data-redis</artifactId>
        </dependency>
//【要新搞个登录界面 .ftl】
application.yaml

spring:
  freemarker:
    template-loader-path: /templates/
    suffix: .ftl
resources/templates/loginPage.ftl
<!DOCTYPE html>
<html lang="en">
<head>
    <meta charset="UTF-8">
    <meta name="viewport" content="width=device-width, initial-scale=1.0">
    <title>Login</title>
</head>
<body>
<h1>Login</h1>
<form action="/login" method="post">
    <div>
        <label for="username">Username:</label>
        <input type="text" id="username" name="username" required>
    </div>
    <div>
        <label for="password">Password:</label>
        <input type="password" id="password" name="password" required>
    </div>
    <div>
        <input type="checkbox" id="remember-me" name="remember-me">
        <label for="remember-me">Remember me</label>
    </div>
    <div>
        <button type="submit">Login</button>
    </div>
</form>
</body>
</html>
com/pcy/controller/LoginController.java
package com.pcy.controller;

import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.GetMapping;

@Controller
public class LoginController {
    @GetMapping("/login") // 修改为 "/custom-login"
    public String login() {
        return "loginPage"; // 返回的视图名仍然是 "loginPage"
    }
}

/*
http://localhost:8080/login

Please sign in
Username
    panchunyao
Password
    •••••••••••••
√ Remember me on this computer.


127.0.0.1:6379> keys spring*
1) "spring:session:sessions:96c83240-f939-4fd1-ac2c-93542f883aef"
2) "spring:session:sessions:56baf3c6-7a5c-483b-b04a-422b8a2be1b7"
*/
com/pcy/config/SecurityConfig.java
@Configuration
@EnableWebSecurity
public class SecurityConfig {
    @Autowired
    private UserDetailsService userDetailsService; // 使用 Spring Security 的 UserDetailsService

    @Bean
    public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception {
        http
                .authorizeHttpRequests(authorizeRequests -> authorizeRequests
                        .requestMatchers("/security/permitall").permitAll() // 允许所有人访问
                        .requestMatchers("/security/anonymous").anonymous() // 仅允许匿名用户访问
                        .requestMatchers("/security/config").hasAuthority("ROLE_config") // 仅拥有 ROLE_config 权限的用户可以访问
                        .requestMatchers("/security/Secured").hasRole("Secured") // 仅拥有 ROLE_Secured 的用户可以访问
                        .requestMatchers("/security/preAuthorize").hasAuthority("PreAuthorize") // 仅拥有 PreAuthorize 权限的用户可以访问
                        .anyRequest().authenticated() // 其他所有请求需要认证
                )
                .formLogin(form -> form
//                        .loginPage("/custom-login") // 将登录页面的路径改为 "/custom-login" 打开会循环重定向
                        .defaultSuccessUrl("http://localhost:8080/swagger-ui/index.html", true) // 登录成功后的跳转路径
                        .permitAll()
                )
                .rememberMe(rememberMe -> rememberMe
                        .rememberMeServices(rememberMeServices()) // 配置 Remember Me 服务
                )
                .userDetailsService(userDetailsService) // 设置 UserDetailsService
                .httpBasic(withDefaults()) // 使用 HTTP Basic 认证
                .csrf(csrf -> csrf.disable()); // 禁用 CSRF

        return http.build();
    }
}
......
//【问答环节】
分析一下freemarker这个配置的含义
    freemarker:
      template-loader-path: /templates/
      suffix: .ftl
/*
这些配置项的含义如下:
template-loader-path: /templates/: 这个配置指定了 Freemarker 模板文件的加载路径,也就是 /templates/ 目录。Spring Boot 会在这个目录下寻找所有的 .ftl 模板文件。

suffix: .ftl: 这个配置指定了 Freemarker 模板文件的文件后缀,也就是 .ftl。在控制器中返回视图名称时,Spring 会自动添加这个后缀来查找相应的模板文件。


配置的工作原理
@Controller
public class LoginController {
    @GetMapping("/login")
    public String login() {
        return "loginPage"; // 返回的视图名
    }
}
在 LoginController 中的 login() 方法中,你返回的是 "loginPage":
因为在 application.yaml 中已经配置了 template-loader-path 和 suffix,Spring Boot 会根据这些配置来查找模板文件:
它会在 template-loader-path 配置的 /templates/ 目录下寻找文件。
它会在视图名称 "loginPage" 后面自动添加 .ftl 后缀。
因此,最终 Spring Boot 会查找路径 /templates/loginPage.ftl,并使用这个模板文件来渲染登录页面。这就是为什么当你在控制器中返回 "loginPage" 时,Freemarker 能正确地找到并渲染 loginPage.ftl 模板。

如果你想修改 Freemarker 模板文件的目录或后缀,可以调整 application.yaml 中的相应配置。
视图名称在 return 中不需要包含目录或后缀,Spring 会根据配置自动处理。
*/

SpringBoot + Vue企业级狐狸

@RequestMapping 注解指定控制器类中的方法可以处理哪些格式的URL请求
@RequestMapping("/hello") 说明该方法将接收并处理格式为/hello的HTTP请求
@RestController 注解指定本类承担着SpringBoot项目的'控制器'效果
包名 所放置的业务代码类型
common 放置了通用的参数和业务方法
controller 放置了针对各业务请求的控制类
domain 放置了各种业务实体类
mapper 放置了针对MyBatis框架的映射关系类
service 放置了诸多实现业务逻辑的类
阅读全文

苍穹外卖

2024/9/28

苍穹外卖

软件开发整体介绍

软件开发流程

需求分析
  • 需求规格说明书(word)、产品原型
设计
  • UI设计、数据库设计、接口设计
编码
  • 项目代码、单元测试
测试
  • 测试用例、测试报告
上线运维
  • 软件环境安装、配置
角色分工
  • 项目经理:对整个项目负责,任务分配、把控进度
  • 产品经理:进行需求调研,输出需求调研文档、产品原型等
  • UI设计师:根据产品原型输出界面效果图
  • 架构师:项目整体架构设计、技术选型等
  • 开发工程师:代码实现
  • 测试工程师:编写测试用例,输出测试报告
  • 运维工程师:软件环境搭建、项目上线
软件环境
  • 开发环境:开发人员在开发阶段使用的环境

  • 测试环境:专门给测试人员使用的环境,用于项目测试

  • 生产环境:线上环境

第二轮补充知识点复习 会以橙色标注

苍穹外卖项目介绍

项目介绍
  • 定位:专门为餐饮制定的一款软件产品[管理端用户端]
功能架构 (体现项目中的业务功能模块)
  • 管理端:员工、分类、菜品、套餐、订单管理、工作台、数据统计、来单提醒
  • 用户端:微信登录、商品浏览、购物车、用户下单、微信支付、历史订单、地址管理、用户催单
产品原型“在文件里有用户端和管理端” (用于展示项目的业务功能 一般由产品经理进行设计)
技术选型 (展示项目中使用到的技术框架和中间件)
  • 用户层:node.js、VUE.js、ElementUI、微信小程序、apache echarts

  • 网关层:Nginx

  • 应用层:SpringBoot、SpringMVC、SpringTask、httpclient、SpringCache、JWT、阿里云OSS、Swagger、POI(操作excel表格)、WebSocket(网络协议<催单…>)

  • 数据层:MySQL、Redis、MyBatis、PageHelper、Spring Data Redis

  • 工具:Git、Maven、Junit、PostMan

开发环境搭建

前端:管理端(Web基于Nginx)、用户端(小程序)

前端环境位置:
E:\Java实例项目1-20套\第24套【项目实战】Java外卖项目实战《苍穹外卖》SpringBoot+SpringMVC+Vue+Swagger+Lombok+Mybatis+SpringSession+Redis+Nginx+小程序\0-0 源码资料\资料\day01\前端运行环境\nginx-1.20.2\html\sky

D:\nginx-1.20.2 [放在英文目录下 双击 nginx.exe] 默认端口号80

[苍穹外卖] (http://localhost/#/login) 如果被其他占用(比如RAGFlow)就把 localhost 换成 127.0.0.1

后端:后端服务(Java)

后端环境位置:
E:\Java实例项目1-20套\第24套【项目实战】Java外卖项目实战《苍穹外卖》SpringBoot+SpringMVC+Vue+Swagger+Lombok+Mybatis+SpringSession+Redis+Nginx+小程序\0-0 源码资料\资料\day01\后端初始工程\sky-take-out

把sky-take-out导入到idea

  • sky-take-out [maven父工程,统一管理依赖版本聚合其他子模块]
    • sky-common [子模块,存放公共类(工具类、常量类、异常类)]
    • sky-pojo [子模块,存放实体类、VO、DTO等]
    • sky-server [子模块,后端服务,存放配置文件、Controller、Service、Mapper等]
名称 说明
Entity 实体,通常和数据库中的表对应
DTO 数据传输对象,通常用程序中各层之间传递数据
VO 视图对象,为前端展示数据提供的对象
POJO 普通Java对象,只有属性和对应的Getter和Setter

深刻理解POJO

POJO的内在含义是指:那些没有继承任何类、也没有实现任何接口[可以实现],更没有被其它框架侵入的java对象。
POJO是一个简单的、普通Java对象,它包含业务逻辑处理或持久化逻辑等,但不是JavaBean、EntityBean等不具有任何特殊角色,不继承或不实现任何其它Java框架的类或接口。 可以包含类似与JavaBean属性和对属性访问的setter和getter方法的
一般在web应用程序中建立一个数据库的映射对象时,我们只能称它为POJO。

  • POJO持久化之后==〉PO(在运行期,由Hibernate中的cglib动态把POJO转换为PO,PO相对于POJO会增加一些用来管理数据库entity状态的属性和方法。PO对于programmer来说完全透明,由于是运行期生成PO,所以可以支持增量编译,增量调试。)
  • POJO传输过程中==> DTO
  • POJO用作表示层==> VO

深刻理解PO、DTO、VO

PO(persistent object):就是将对象与关系数据库绑定,用对象来表示关系数据,
最简单的PO就是对应数据库中某个表中的一条记录,多个记录可以用PO的集合。PO中应该不包含任何对数据库的操作。

  • 有时也被称为Data对象,对应数据库的entity,简单认为一个PO对应数据库中的一条记录
  • PO中不应该包含任何对数据库的操作
  • PO的属性是跟数据表的字段一一对应的
  • PO对象需要实现序列化接口

DTO(Data Transfer Object): → 数据传输对象
主要用于远程调用需要大量传输对象的地方
我们可以将PO中的部分属性抽取出来,就形成了DTO
举例说明
比如我们有一张表有100个字段,那么对应的PO就有100个属性
但是我们界面上需要显示10个字段,客户端用WEB service来获取数据,没必要把整个PO对象传递到客户端,这时我们就可以用只有这10个属性的DTO来传递结果到客户端,这样就不会暴露服务端表结构,到达客户端后,如果用这个对象来对应界面显示,那么此时它的身份就转为了VO(View Object)


VO
VO(value object) 是值对象,精确点讲它是业务对象,是存活在业务层的,是业务逻辑使用的,它存活的目的就是为数据提供一个生存的地方。VO的属性是根据当前业务的不同而不同的,也就是说,它的每一个属性都一一对应当前业务逻辑所需要的数据的名称。 VO是什么?它是值对象,准确地讲,它是业务对象,是生活在业务层的,是业务逻辑需要了解,需要使用的,再简单地讲,它是概念模型转换得到的。
重点
一个VO可以只是PO的一部分,也可以是多个PO构成,同样也等同于一个PO(指的是属性)。正因为这样,PO独立出来,数据持久层也就独立出来了,它不会受到任何业务的影响和干涉。又因为这样,业务逻辑层也独立开来,它不会受到数据持久层的影响,业务层只关心业务逻辑的处理,怎么存和读都交给别人。

深刻理解什么是DAO

DAO(Data Access Object):数据访问对象
主要用来封装对数据库的访问。通过它可以把POJO持久化为PO,用PO组装出来VO、DTO。
是一个sun的一个标准j2ee设计模式,这个模式中有个接口就是DAO,它负持久层的操作。为业务层提供接口。此对象用于访问数据库。通常和PO结合使用,DAO中包含了各种数据库的操作方法。通过它的方法,结合PO对数据库进行相关的操作。夹在业务逻辑与数据库资源中间。配合VO,提供数据库的CRUD操作…

  • 主要用来封装对DB(数据库)的访问(CRUD操作)。

  • 通过接收业务层的数据,把POJO持久化为PO。

深刻理解JavaBean

JavaBean是一个遵循特定写法的Java类,是一种Java语言编写的可重用组件,它的方法命名,构造及行为必须符合特定的约定:

1、这个类必须具有一个公共的(public)无参构造函数;
2、所有属性私有化(private);
3、私有化的属性必须通过public类型的方法(getter和setter)暴露给其他程序,并且方法的命名也必须遵循一定的命名规范。
4、这个类应是可序列化的。(比如可以实现Serializable 接口,用于实现bean的持久性)
JavaBean在JavaEE开发中,通常用于封装数据
许多开发者会把JavaBean看作村从特定命名约定的POJOPOJO按照JavaBean的规则来就可以变成JavaBean
当一个POJO可序列化,有一个无参的构造函数,使用getter和setter方法来访问属性时,他就是一个JavaBean
JavaBean是一种组件技术,就好像你做了一个扳手,而这个扳手会在很多地方被拿去用,这个扳子也提供多种功能(你可以拿这个扳手扳、锤、撬等等),而这个扳手就是一个组件。

common里的constant、context、properties代表什么意思

constant:
用于存放常量类。这些常量可能是项目中频繁使用的固定值,如状态码、错误码、系统配置项等。
常量类中的变量一般使用public static final修饰,确保其不可变性。
    
context:
用于存放上下文类。上下文类通常用来保存和传递运行时环境信息或状态。
在Spring框架中,ApplicationContext就是一个典型的上下文对象,它提供了对Bean的访问以及配置信息的管理。
    
properties:
用于存放属性文件。这些文件通常以.properties为扩展名,用于存储配置信息,如数据库连接字符串、系统参数等。
属性文件可以通过Properties类来读取和写入,方便在运行时动态调整系统行为。

Final的巩固

问:对于引用类型(如String、Object等),final变量的引用不能被改变,但引用的对象内部状态可以改变。 这句话是什么意思?

答:当一个引用类型的变量被声明为final时,这个变量的引用(即指向的对象)不能被改变,但该对象的内部状态是可以改变的。我们可以通过具体的例子来理解这一点。
public class Example {
    public static final String EMP_ID = "empId";
    public static void main(String[] args){
        // 下面这行代码会编译失败,因为EMP_ID是final的
        // EMP_ID = "newEmpId"; // 编译错误
        
// 但是可以创建一个新的String对象并使用EMP_ID内容
    String anotherId = EMP_ID + "123";
    sout(anotherId) => empId123;
    }
}

解析context (实现上下类的逻辑原理) 内的代码

package com.sky.context;

public class BaseContext {

    public static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
//ThreadLocal 是一个线程局部变量,每个线程都有自己的独立副本。这意味着不同线程之间不会共享同一个 ThreadLocal实例的数据,从而避免了多线程环境下的数据竞争问题。
    public static void setCurrentId(Long id) {
        threadLocal.set(id);
    }

    public static Long getCurrentId() {
        return threadLocal.get();
    }

    public static void removeCurrentId() {
        threadLocal.remove();
    }
}
/*
这个类通常用于需要在多线程环境中传递和管理线程上下文信息的场景。例如:

Web应用:在处理HTTP请求时,可能需要将用户ID或其他上下文信息绑定到当前线程,以便在整个请求处理过程中都能访问到这些信息。

日志记录:在日志记录中,可能需要记录每个操作的执行者ID,通过 ThreadLocal 可以方便地在日志记录器中获取当前操作者的ID。

事务管理:在分布式事务中,可能需要将事务ID绑定到当前线程,以便在事务的各个阶段都能访问到这个ID。

内存泄漏:如果 ThreadLocal 中存储的对象没有及时释放,可能会导致内存泄漏。因此,建议在不再需要 ThreadLocal 中的数据时,调用 remove 方法将其移除。

线程池:在使用线程池时,特别需要注意 ThreadLocal 的管理。线程池中的线程是复用的,如果不及时清理 ThreadLocal 中的数据,可能会导致数据混淆或内存泄漏。
*/

静态变量解析

//静态变量 (static)
静态变量:在 Java 中,静态变量属于类而不是类的实例。这意味着无论创建多少个类的实例,静态变量都只有一份拷贝,并且所有实例共享这份拷贝。
作用域:静态变量在类加载时初始化,并且在类卸载时销毁。它们存在于类的生命周期内,而不是实例的生命周期内。

//结合 static 和 ThreadLocal
在 BaseContext 类中,threadLocal 被声明为 static,这意味着所有 BaseContext 实例共享同一个 ThreadLocal 实例。但这并不意味着所有线程共享同一个 ThreadLocal 实例的数据。相反,每个线程都有自己独立的 ThreadLocal 数据副本。
    
静态变量:
public static ThreadLocal<Long> threadLocal = new ThreadLocal<>();
这行代码声明了一个静态的 ThreadLocal 变量 threadLocal,所有 BaseContext 实例共享这个 ThreadLocal 实例。

使用Git进行版本控制

  • 创建Git本地仓库
  • 创建Git远程仓库[GitHub、Gitee]
  • 将本地文件推送到Git远程仓库
.gitignore[文件设置]
    
//忽略git管理的文件
**/target/
.idea
*.iml
*.class
*Test.java
**/test/

创建远程仓库流程:【提交到本地】
VCS → Create Git Repository → sky-take-out → √ → Unversinoed Files(All) → Commit

去创建一个仓库:[Pluminary/sky-take-out (gitee.com)] (https://gitee.com/Pluminary/sky-take-out)

推送代码到Gitee远程仓库:Idea右上角的↗ → 定义一下本地和远程仓库关联 点击Define remote → Name: origin
URL: https://gitee.com/Pluminary/sky-take-out.git (这个是在Gitee上创建仓库后复制的代码)

推送成功:[Pluminary/sky-take-out (gitee.com)] (https://gitee.com/Pluminary/sky-take-out)

后端环境搭建

数据库环境搭建

Unknown collation: ‘utf8mb4_0900_ai_ci‘的解决方法_unknown collation utf8mb4-CSDN博客

前后端联调
浏览器

↓
Controller:
1.接收并封装参数
2.调用service方法查询数据库
3.封装结果并相应

↓
Service:
1.调用mapper查询数据库
2.密码对比
3.返回结果

↓
Mapper:
1.select * from employee where username = ? 

↓
数据库

IDEA中导入多module的Maven项目无法识别module的解决办法_idea modules太多 mvn clean 对某个module不起作用-CSDN博客

Maven → compile(编译聚合模块 )

[INFO] ————————————————————————
[INFO] Reactor Summary for sky-take-out 1.0-SNAPSHOT:
[INFO]
[INFO] sky-take-out ………………………………… SUCCESS [ 0.003 s]
[INFO] sky-common ………………………………….. SUCCESS [ 2.761 s]
[INFO] sky-pojo ……………………………………. SUCCESS [ 2.227 s]
[INFO] sky-server ………………………………….. SUCCESS [ 1.294 s]
[INFO] ————————————————————————
[INFO] BUILD SUCCESS

在数据库中 新建查询  SELECT VERSION(); 引擎是8.0.33的是正规操作mysql此时对应的任务管理器服务里搜索mysql(名称:MySQL80)开启这个  如果开启了服务里的MySQL那SELECT VERSION()查询就是11.0.5-MariaDB

handler:全局异常处理器

右侧Maven的具体用途

  1. clean
    功能:清除项目构建过程中生成的所有文件,通常包括 target 目录下的内容
    命令:mvn clean
    使用场景:
    在每次构建之前,确保没有旧的构建产物干扰新构建。
    清理项目目录,准备进行新的构建。

  2. validate
    功能:验证项目的正确性,确保所有必要的信息都已就绪
    命令:mvn validate
    使用场景:
    在构建过程的早期阶段,检查项目配置是否正确。
    **确保所有依赖项和资源都可用**。

  3. compile
    功能:编译项目的源代码
    命令:mvn compile
    使用场景:
    编译项目源代码,生成 .class 文件。
    通常在开发过程中频繁使用确保代码可以成功编译

  4. test
    功能:运行项目的单元测试。
    命令:mvn test
    使用场景:
    在代码提交前,确保所有单元测试通过。
    持续集成(CI)过程中,自动运行测试以确保代码质量。

  5. package
    功能:将编译后的代码打包成可分发的格式,如 JAR、WAR 等
    命令:mvn package
    使用场景:
    构建项目并生成可部署的包。
    通常在开发和部署过程中使用,生成最终的可发布版本。

  6. verify
    功能:运行任何检查以验证包的完整性和有效性。
    命令:mvn verify
    使用场景:
    在发布前,进行更严格的验证,确保包的质量。
    运行集成测试、性能测试等。

  7. install
    功能:将包安装到本地 Maven 仓库,供其他项目使用
    命令:mvn install
    使用场景:
    将项目依赖安装到本地仓库,以便其他项目可以引用。
    通常在开发和测试环境中使用,确保依赖项可用。

  8. site
    功能:生成项目的站点文档,包括项目报告、测试覆盖率等。
    命令:mvn site
    使用场景:
    生成项目文档,供团队成员和外部用户查阅。
    文档生成和发布,提高项目的透明度和可维护性。

  9. deploy
    功能:将最终的包部署到远程仓库,如 Nexus、Artifactory 等。
    命令:mvn deploy
    使用场景:
    将项目发布到远程仓库,供其他团队或项目使用。
    通常在持续集成和持续部署(CI/CD)流程中使用,确保发布的版本可用。

总结
clean:清理构建产物。
validate:验证项目配置。
compile:编译源代码。
test:运行单元测试。
package:打包项目。
verify:验证包的完整性和有效性。
install:安装到本地仓库。
site:生成项目文档。
deploy:部署到远程仓库。

思考:前端发送的请求,是如何请求到后端服务的?
前端请求地址:http://localhost/api/employee/login
后端接口地址:http://localhost:8080/admin/employee/login
nginx 反向代理的配置方式
nginx.conf

server{
    listen 80;
    server_name localhost;
    location /api/{
        proxy_pass http://localhost:8080/admin/;  #反向代理
    }
}
nginx 负载均衡的配置方法(平均转发到多台后端服务器)
nginx.conf

upstream webservers{
    server 192.168.100.128:8080;
    server 192.168.100.129:8080;
}

server{
    listen 80;
    server_name localhost;
    location /api/{
        proxy_pass http://webservers/admin/;  #反向代理
    }
}
nginx 负载均衡策略:
名称 说明
轮询 默认方式
weight 权重方式,默认为1,权重越高,被分配的客户端请求就越多
ip_hash 依据ip分配方式,这样每个访客可以固定访问一个后端服务
least_conn 依据最少连接方式,把请求优先分配给连接数少的后端服务
url_hash 依据url分配方式,这样相同的url会被分配到同一个后端服务
fair 依据相应时间方式,响应时间短的服务将会被优先分配

完善登录功能

问题:员工表中的密码是明文存储,安全性太低
  • 将密码加密后存储,提高安全性

  • 使用MD5加密方式对明文密码加密 [不可逆]

  • 修改数据库中的明文代码,改为MD5加密后的密文

  • 修改Java代码,前端提交的代码进行MD5加密后再跟数据库中密码比对

在Idea中有 “//TODO” 这代表着标记处 此处还未完成一些操作 标记后可以在idea的下面快速定位到TODO

MD5密码加密后 也区分大小写 如果相同的密文但是大小写不同 结果还是不同的

修改密码
com/sky/controller/admin/EmployeeController.java
 @PutMapping("/editPassword")
    @ApiOperation("修改密码")
    public Result editPassword(@RequestBody PasswordEditDTO passwordEditDTO) {
        log.info("修改密码:{}", passwordEditDTO);
        employeeService.updatePassword(passwordEditDTO);
        return Result.success();
    }
com/sky/service/EmployeeService.java
/**
     * 更改密码
     * @param passwordEditDTO
     */
    void updatePassword(PasswordEditDTO passwordEditDTO);
com/sky/service/impl/EmployeeServiceImpl.java
 /**
     * 更改密码
     * @param passwordEditDTO
     */
    @Override
    public void updatePassword(PasswordEditDTO passwordEditDTO) {
    //getCurrentId 方法:public static Long getCurrentId() 方法用于获取当前线程的用户ID。

        Long empId = BaseContext.getCurrentId();
    //select * from employee where id = #{id}   根据id查员工的所有
        Employee employee = employeeMapper.getById(empId);
    //用md根据从前端传来的oldpassword 去判断employee的原始代码是否相同
        if (!employee.getPassword().equals(DigestUtils.md5DigestAsHex(passwordEditDTO.getOldPassword().getBytes()))) { 
            throw new PasswordErrorException(MessageConstant.PASSWORD_ERROR);
        }
        String newPassword = DigestUtils.md5DigestAsHex(passwordEditDTO.getNewPassword().getBytes());
        employee.setPassword(newPassword);
        employeeMapper.update(employee);
    }

导入接口文档

前后端分离开发流程
  • 定制接口(定义规范) → 前端开发(mock数据) + 后端开发(后端自测) → 连调(校验格式) → 提测(自动化测试)
操作步骤 YApi Pro-高效、易用、功能强大的可视化接口管理平台

将课程资料中提供的项目接口导入YApi
苍穹外卖-管理端接口.json
苍穹外卖-用户端接口.json
苍穹外卖-管理端+用户端接口 → 数据管理 → 数据导入(json 随后把json文件拖入) → 点击接口可查看

Swagger介绍和使用方式

Knife4j是为Java MVC框架集成Swagger生成Api文档的增强解决方案

<dependency>
   <groupId>com.github.xiaoymin</groupId>
   <artifactId>knife4j-spring-boot-starter</artifactId>
   <version>3.0.2</version>
</dependency>
使用方式
  • 导入knife4j的maven坐标
  • 在配置类中加入knife4j相关配置
sky-server  com/sky/config/WebMvcConfiguration.java
/**
 * 通过knife4j生成接口文档
 * @return
*/ 
@Bean
    public Docket docket() {
        ApiInfo apiInfo = new ApiInfoBuilder()
                .title("苍穹外卖项目接口文档")
                .version("2.0")
                .description("苍穹外卖项目接口文档")
                .build();
        Docket docket = new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo)
                .select()
                    //指定生成接口需要扫描的包
                .apis(RequestHandlerSelectors.basePackage("com.sky.controller"))
                .paths(PathSelectors.any())
                .build();
        return docket;
    }
  • 设置静态资源映射,否则接口文档页面无法访问
/**
 * 设置静态资源映射
 * @param registry
*/
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/");
        registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
    }

[苍穹外卖项目接口文档] (http://localhost:8080/doc.html#/home) 这个文档是解析EmployeeController来的

通过Swagger就可以生成接口文档,那么我们不需要Yapi了?
  • Yapi是设计阶段使用的工具,管理和维护接口
  • Swagger在开发阶段使用的框架,帮助后端开发人员做后端的接口测试

编写接口文档 在企业中需要注意:

测试:
为每个API编写单元测试和集成测试,确保API的正确性和稳定性。
使用自动化测试工具(如Postman, JUnit等)来定期验证API的行为。

Swagger常用注解

注解 说明
@Api 用在类上,例如Controller,表明对类的说明
@ApiModel 用在类上,例如entity、DTO、VO
@ApiModelProperty 用在属性上,描述属性信息
@ApiOperation 用在方法上,例如Controller的方法,说明方法的用途、作用
sky-server  com/sky/controller/admin/EmployeeController.java
package com.sky.controller.admin;

import com.sky.constant.JwtClaimsConstant;
import com.sky.dto.EmployeeLoginDTO;
import com.sky.entity.Employee;
import com.sky.properties.JwtProperties;
import com.sky.result.Result;
import com.sky.service.EmployeeService;
import com.sky.utils.JwtUtil;
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.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

/**
 * 员工管理
 */
@RestController
@RequestMapping("/admin/employee")
@Slf4j
@Api(tags = "员工相关接口")
public class EmployeeController {

    @Autowired
    private EmployeeService employeeService;
    @Autowired
    private JwtProperties jwtProperties;

    /**
     * 登录
     *
     * @param employeeLoginDTO
     * @return
     */
    @PostMapping("/login")
    @ApiOperation(value = "员工登录")
    public Result<EmployeeLoginVO> login(@RequestBody EmployeeLoginDTO employeeLoginDTO) {
        log.info("员工登录:{}", employeeLoginDTO);

        Employee employee = employeeService.login(employeeLoginDTO);

        //登录成功后,生成jwt令牌
        Map<String, Object> claims = new HashMap<>();
        claims.put(JwtClaimsConstant.EMP_ID, employee.getId());
        String token = JwtUtil.createJWT(
                jwtProperties.getAdminSecretKey(),
                jwtProperties.getAdminTtl(),
                claims);

        EmployeeLoginVO employeeLoginVO = EmployeeLoginVO.builder()
                .id(employee.getId())
                .userName(employee.getUsername())
                .name(employee.getName())
                .token(token)
                .build();

        return Result.success(employeeLoginVO);
    }

    /**
     * 退出
     *
     * @return
     */
    @PostMapping("/logout")
    @ApiOperation(value = "员工退出")
    public Result<String> logout() {
        return Result.success();
    }
}
sky-pojo  com/sky/vo/EmployeeLoginVO.java
// 这里是最后返回的数据vo [已经经历过由po→DTO→vo的过程] 这里的po应该就是Employee
package com.sky.vo;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.AllArgsConstructor;
import lombok.Builder;
import lombok.Data;
import lombok.NoArgsConstructor;

import java.io.Serializable;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
@ApiModel(description = "员工登录返回的数据格式")
public class EmployeeLoginVO implements Serializable {

    @ApiModelProperty("主键值")
    private Long id;

    @ApiModelProperty("用户名")
    private String userName;

    @ApiModelProperty("姓名")
    private String name;

    @ApiModelProperty("jwt令牌")
    private String token;
}
sky-pojo  com/sky/dto/EmployeeLoginDTO.java
// 这里的DTO是传输中的数据
package com.sky.dto;

import io.swagger.annotations.ApiModel;
import io.swagger.annotations.ApiModelProperty;
import lombok.Data;

import java.io.Serializable;

@Data
@ApiModel(description = "员工登录时传递的数据模型")
public class EmployeeLoginDTO implements Serializable {

    @ApiModelProperty("用户名")
    private String username;

    @ApiModelProperty("密码")
    private String password;

}

Getter与Setter无中生有??    以及快速创建对象builder

在上述VO和DTO代码中很显然没有看到常见的Getter和Setter
这是因为代码使用了 Lombok 注解,Lombok 是一个 Java 库,可以通过注解自动生成常见的样板代码,如 getter、setter、toString、equals 和 hashCode 等方法。

Lombok 注解解释
@Data
作用:这是一个组合注解,包含了 @ToString、@EqualsAndHashCode、@Getter、@Setter 和 @RequiredArgsConstructor。
效果:自动生成所有字段的 getter 和 setter 方法,toString 方法,equals 和 hashCode 方法,以及一个包含所有 final 字段和 @NonNull 字段的构造函数。

@Builder
作用:生成一个构建器模式的类,使得对象的创建更加灵活和可读。
效果:自动生成一个静态内部类 EmployeeLoginVO.EmployeeLoginVOBuilder,并提供构建方法。

  • 生成的构建器类包含所有字段的设置方法,并提供一个 build 方法来最终构建对象

  • 使用构建器模式可以让你在创建对象时更清晰地指定各个字段的值,特别是在对象有很多字段时。
    构建器模式允许你按需设置字段,而不需要为每个字段组合创建多个构造函数。

  • 生成的构建器类:
    Lombok 会自动生成一个静态内部类 EmployeeLoginVOBuilder,包含所有字段的设置方法和一个 build 方法。

    • 设置字段:
      你可以按需调用构建器的设置方法来设置字段值,例如 id(1L)、userName(“john_doe”) 等。
    • 构建对象:
      最后调用 build 方法来创建 EmployeeLoginVO 对象。
 // 使用构建器创建 EmployeeLoginVO 对象
        EmployeeLoginVO employeeLoginVO = EmployeeLoginVO.builder()
                .id(1L)
                .userName("john_doe")
                .name("John Doe")
                .token("eyJhbGciOiJIUzI1NiJ9...")
                .build();

        System.out.println(employeeLoginVO);

@NoArgsConstructor
作用:生成一个无参构造函数。
效果:自动生成一个不带任何参数的构造函数。

@AllArgsConstructor
作用:生成一个全参构造函数。
效果:自动生成一个包含所有字段的构造函数。

详细解析@GetMapping 与 @PostMapping

选择使用 @GetMapping 还是 @PostMapping 主要取决于Http请求的性质和用途
@GetMapping

作用:

  • @GetMapping专门用于处理HTTP GET请求

  • GET请求通常用于从服务器获取资源,不会对服务器上的数据进行修改

特点:

  • 请求参数通常附加在URL中 @GetMapping("/xxx/{id}") 底下会跟 @PathVariable
  • 请求是安全的不会修改服务器状态

适用场景:

  • 查询数据:获取用户列表、搜索结果
  • 获取静态资源:图片、css文件
  • 获取单个资源:获取某个用户的详细信息

@PostMapping

作用:

  • @PostMapping专门用于处理HTTP POST请求
  • POST请求通常用于向服务器发送数据,可能会对服务器上的数据进行修改

特点:

  • 请求参数放在请求体中,不会显示在URL这种 @PostMapping("/users")底下会跟@RequestBody
  • 请求体的大小没有固定限制,可以发送大量数据
  • 多次相同的POST请求可能会产生不同的结果,可能会修改服务器状态
  • 请求是不安全的,可能会修改服务器状态

适用场景:

  • 创建新资源:注册新用户、添加新订单
  • 更新现有资源:修改用户信息、更新订单状态
  • 发送表单数据:登录、提交评论
  • 上传文件:图片、文档

以下是一些可能的原因,解释了为什么您看到的前端项目代码不是“正经的网页代码”:

  • 构建过程:现代前端项目通常有一个构建过程,使用工具如Webpack、Rollup或Parcel等将源代码转换和打包。原始的代码可能在src目录中,而您看到的是构建后的dist目录,其中包含了转换和压缩后的文件。
  • 模块化和组件化:前端框架和库鼓励开发者将UI拆分成可复用的模块和组件。这意味着不再有传统的HTML、CSS和JavaScript混合在一起的代码,而是分别在不同的文件中定义。
  • 框架和库的使用:现代前端开发往往依赖于各种框架和库,这些框架和库有自己的文件结构和编写方式,可能和您熟悉的传统网页代码写法有很大不同。

根据@PutMapping(“/{Id}”)提问

我为什么@PutMapping(“/{id}”)是id呢 是不是因为我后面需要数据库where id = #{id} 查询才这样传输 如果 我想以name去查询呢

在RESTful API设计中,资源通常通过其唯一标识符(如ID)来访问和操作。@PutMapping("/{id}")使用{id}作为路径变量是因为ID是数据库中记录的唯一标识,这使得通过ID查找和更新特定的记录变得直接和高效。

如果你想要通过name来查询和更新记录,这在理论上是可行的,但需要注意以下几点:

  1. name字段可能不是唯一的,这意味着可能有多条记录具有相同的名称。
  2. 使用非唯一字段作为更新依据可能会导致数据不一致或错误更新。

更新员工 前后端联调原理

★ 在前端使用Ajax与后端URL对应时,你需要确保Ajax请求的URL与后端控制器中定义的映射相匹配。在你提供的例子中,后端使用@PutMapping("/{id}")来定义更新员工信息的端点,因此前端的Ajax URL应该包含员工的ID

// 绑定更新按钮的点击事件
$('#updateBtn').click(function() {
    var employee = {
        id: $('#id').val(), // 假设这是员工的唯一标识符
        name: $('#name').val(),
        email: $('#email').val(),
        department: $('#department').val()
    };

    // 发起Ajax PUT请求,URL中包含员工的ID
    $.ajax({
        url: `/api/employees/${employee.id}`, // 注意这里的URL与后端的@PutMapping("/{id}")对应
        type: 'PUT',
        contentType: 'application/json', // 指定发送给服务器的数据类型
        data: JSON.stringify(employee), // 将JavaScript对象转换为JSON字符串
        success: function(response) {
            // 请求成功,可以在这里处理响应数据
            alert('Employee information updated successfully!');
            // 如果需要,可以在这里更新页面上的表单数据
        },
        error: function(xhr, status, error) {
            // 请求失败,可以在这里处理错误信息
            alert('Error updating employee information: ' + xhr.responseText);
        }
    });
});

//在这个例子中,employee.id是从表单中获取的员工ID,它被拼接到URL字符串中,以形成完整的请求URL。这个URL应该与后端控制器中定义的@PutMapping("/{id}")相对应。当点击更新按钮时,Ajax请求会被发送到后端,后端会根据提供的ID找到对应的员工记录并进行更新。
 @PutMapping("/{id}")
    public ResponseEntity<Employee> updateEmployee(@PathVariable Long id, @RequestBody Employee employeeDetails) {
        Employee employee = employeeService.getEmployeeById(id);
        if (employee != null) {
            employee.setName(employeeDetails.getName());
            employee.setEmail(employeeDetails.getEmail());
            employee.setDepartment(employeeDetails.getDepartment());
            Employee updatedEmployee = employeeService.updateEmployee(employee);
            return ResponseEntity.ok(updatedEmployee);
        } else {
            return ResponseEntity.notFound().build();
        }
    }
}
//这里{id}是路径变量,它会匹配Ajax请求URL中的employee.id。这样,前后端的URL就正确对应起来了。

新增员工(Post+Json提交格式)

需求分析和设计

账号必须是唯一的、手机号校验合法11位、性别单选男女、身份证合法18位号码、新增密码默认为123456

本项目约定

  • 管理端发出的请求,统一使用 /admin 作为前缀
  • 用户端发出的请求,统一使用 /user 作为前缀

代码开发

根据新增员工接口设计对应的DTO
注意:当前提交的数据和实体类中对应的属性差别比较大时,建议使用DTO(数据传输)来封装数据

sky-pojo  com/sky/dto/EmployeeDTO.java
package com.sky.dto;

import lombok.Data;

import java.io.Serializable;

@Data
public class EmployeeDTO implements Serializable {

    private Long id;

    private String username;

    private String name;

    private String phone;

    private String sex;

    private String idNumber;

}
sky-server  com/sky/controller/admin/EmployeeController.java
/**
     * 新增员工
     * @param employeeDTO
     * @return
     */
    @PostMapping
    @ApiOperation("新增员工")
    public Result save(@RequestBody  EmployeeDTO employeeDTO){
    // 因为是JSON格式 要加@RequestBody
        log.info("新增员工:{}",employeeDTO);
        employeeService.save(employeeDTO);
        return Result.success();
    }
sky-server  com/sky/service/EmployeeService.java
package com.sky.service;

import com.sky.dto.EmployeeDTO;
import com.sky.dto.EmployeeLoginDTO;
import com.sky.entity.Employee;

public interface EmployeeService {

    /**
     * 员工登录
     * @param employeeLoginDTO
     * @return
     */
    Employee login(EmployeeLoginDTO employeeLoginDTO);

    /**
     * 新增员工
     * @param employeeDTO
     */
    void save(EmployeeDTO employeeDTO);
}
sky-server  com/sky/service/impl/EmployeeServiceImpl.java
 /**
     * 新增员工
     * @param employeeDTO
     */
    @Override
    public void save(EmployeeDTO employeeDTO) {
        Employee employee = new Employee();
        //employee.setName(employeeDTO.getName()); 太多了 用对象属性拷贝
        BeanUtils.copyProperties(employeeDTO,employee); //其余的要手动设置
        //设置账号状态,默认正常状态 1正常 0锁定  规范封装
        employee.setStatus(StatusConstant.ENABLE);
        //设置密码,默认密码123456
        employee.setPassword(DigestUtils.md5DigestAsHex(PasswordConstant.DEFAULT_PASSWORD.getBytes()));
        //设置当前记录的创建时间和修改时间
        employee.setCreateTime(LocalDateTime.now());
        employee.setUpdateTime(LocalDateTime.now());
        //设置当前记录创建人id和修改人id
        //TODO 后期需要改为当前登录用户的id
        employee.setCreateUser(10L);
        employee.setUpdateUser(10L);

        employeeMapper.insert(employee);
    }
sky-pojo  com/sky/entity/Employee.java
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;

    //@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;

    //@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime updateTime;

    private Long createUser;

    private Long updateUser;

}
sky-server  com/sky/mapper/EmployeeMapper.java
/**
 * 插入员工数据
*/
    @Insert("insert into employee (name,username,password,phone,sex,id_number,create_time,update_time,create_user,update_user))" +
            "values" +
            "(#{name},#{username},#{password},#{phone},#{sex},#{idNumber},#{createTime},#{updateTime},#{createUser},#{updateUser},#{status})")
    void insert(Employee employee);

@Builder 和 @JsonFormat

@Builder 是 Lombok 提供的一个注解,用于自动生成构建器模式的代码。
它会在编译时生成一个静态的 Builder 类和相关的方法,使得对象的创建更加灵活和可读。
需要创建不可变对象时,可以使用 @Builder 结合 @Value 注解。
需要创建复杂的对象时,可以通过构建器模式逐步设置属性,提高代码的可读性和可维护性

@JsonFormat 是 Jackson 库提供的注解,用于指定日期时间字段在 JSON 序列化和反序列化时的格式。
通过设置 pattern 属性,可以控制日期时间字段的格式化方式。
当需要将 LocalDateTime、Date 等日期时间类型的字段转换为特定格式的字符串时。
在 RESTful API 中,返回的 JSON 数据需要符合特定的日期时间格式要求。

使用 @Builder 的场景
public class Main {
    public static void main(String[] args) {
        // 使用 @Builder 创建 Employee 对象
        Employee employee = Employee.builder()
                .id(1L)
                .username("user123")
                .name("张三")
                .password("password123")
                .phone("12345678901")
                .sex("男")
                .idNumber("123456789012345678")
                .status(1)
                .createTime(LocalDateTime.now())
                .updateTime(LocalDateTime.now())
                .createUser(1L)
                .updateUser(1L)
                .build();

        System.out.println(employee);
    }
}
使用 @JsonFormat 的场景
//创建 RESTful API
Employee里面的pojo就不详细写了
    //@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime createTime;

    //@JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime updateTime;
==========================================================================

@RestController
public class EmployeeController {

    @GetMapping("/employee")
    public ResponseEntity<String> getEmployee() throws Exception {
        // 创建 Employee 对象
        Employee employee = Employee.builder()
                .id(1L)
                .username("user123")
                .name("张三")
                .password("password123")
                .phone("12345678901")
                .sex("男")
                .idNumber("123456789012345678")
                .status(1)
                .createTime(LocalDateTime.now())
                .updateTime(LocalDateTime.now())
                .createUser(1L)
                .updateUser(1L)
                .build();

        // 使用 ObjectMapper 将 Employee 对象转换为 JSON 字符串
        ObjectMapper objectMapper = new ObjectMapper();
        String json = objectMapper.writeValueAsString(employee);

        return ResponseEntity.ok(json);
    }
}
===========================================================================
// 除了 @JsonFormat 注解,还有其他方式可以指定日期时间格式,具体取决于你的需求和使用的库。
 public static void main(String[] args) {
        LocalDateTime now = LocalDateTime.now();
        DateTimeFormatter formatter = DateTimeFormatter.ofPattern("yyyy-MM-dd HH:mm:ss");
        String formattedDateTime = now.format(formatter);

        Employee employee = Employee.builder()
                .id(1L)
                .username("user123")
                .name("张三")
                .password("password123")
                .phone("12345678901")
                .sex("男")
                .idNumber("123456789012345678")
                .status(1)
                .createTime(now)
                .updateTime(now)
                .createUser(1L)
                .updateUser(1L)
                .build();

        System.out.println("Formatted Create Time: " + formattedDateTime);
        System.out.println("Formatted Update Time: " + formattedDateTime);
    }

@Builder
通过 Employee.builder() 创建了一个构建器对象。
使用链式调用设置各个属性,最后调用 build() 方法生成 Employee 实例。
这种方式使得创建对象的代码更加简洁和易读,特别是当对象属性较多时。

@JsonFormat
在 createTime 和 updateTime 字段上使用了 @JsonFormat 注解,指定了日期时间的格式为 “yyyy-MM-dd HH:mm:ss”。
当 Employee 对象被转换为 JSON 字符串时,这两个字段会被格式化为指定的日期时间格式。
这样可以确保返回的 JSON 数据符合预期的格式要求。

RESTful风

可缓存性:
RESTful API 可以利用 HTTP 缓存机制,减少网络请求,提高性能。
客户端可以缓存响应,减少服务器的负载。

易于集成:
RESTful API 使用标准的 HTTP 协议,几乎所有的编程语言和框架都支持 HTTP 请求。
这使得不同系统之间的集成变得更加容易。

可读性强:
RESTful API 的 URL 设计通常非常直观,易于理解和记忆。
例如,/users/123 表示用户 ID 为 123 的资源,/users/123/orders 表示该用户的订单资源。

灵活性:
RESTful API 支持多种数据格式(如 JSON、XML 等),可以根据需要选择合适的格式。
客户端和服务器可以通过协商确定数据格式,提高了灵活性。

GET /users
GET /users/{id}
POST /users
PUT /users/{id}

功能测试

功能测试方式:
  • 通过接口文档测试
  • 通过前后端联调测试

注意:由于开发阶段前后端是并行开发的,后端完成某个功能后,此时前端对应的功能可能还没有开发完成,导致无法进行前后端联调测试。所以在开发阶段,后端测试主要以接口文档测试为主

[苍穹外卖项目接口文档] (http://localhost:8080/doc.html#/documentManager/GlobalParameters-default)

首先要拿到JWT令牌(去接口进行一次登录测试后会有) → 全局参数设置 → 添加参数
注意:这个jwt→json是有有效期的(2小时=7200000秒)

sky:
  jwt:
    # 设置jwt签名加密时使用的秘钥
    admin-secret-key: itcast
    # 设置jwt过期时间
    admin-ttl: 7200000
    # 设置前端传递过来的令牌名称
    admin-token-name: token

{
“code”: 1,
“msg”: null,
“data”: {
“id”: 1,
“userName”: “admin”,
“name”: “管理员”,
“token”: “eyJhbGciOiJIUzI1NiJ9.eyJlbXBJZCI6MSwiZXhwIjoxNzI3NjAxMTAxfQ.rnxaRc7fjPzMYwGHk3VzKA4EOxRFrYkKzesxEQsCQUc”
}
}


新增参数:
参数名称:token
参数值:eyJhbGciOiJIUzI1NiJ9.eyJlbXBJZCI6MSwiZXhwIjoxNzI3NjAxMTAxfQ.rnxaRc7fjPzMYwGHk3VzKA4EOxRFrYkKzesxEQsCQUc
参数类型:header

新增员工接口
{
“idNumber”: “1321321312”,
“name”: “张三”,
“phone”: “11111111111”,
“sex”: “1”,
“username”: “zhangsan”
}

响应内容:
{
“code”: 1,
“msg”: null,
“data”: null
}

sky-server  com/sky/interceptor/JwtTokenAdminInterceptor.java
package com.sky.interceptor;

import com.sky.constant.JwtClaimsConstant;
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  //将该类注册为 Spring 管理的 Bean。
@Slf4j //使用 Lombok 自动生成日志记录器
public class JwtTokenAdminInterceptor implements HandlerInterceptor {
// 包含 JWT 相关的配置属性,如令牌名称和密钥
    @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的方法还是其他资源
//检查当前拦截到的是否是 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);
            //3、通过,放行
            return true;
        } catch (Exception ex) {
            //4、不通过,响应401状态码
            response.setStatus(401);
            return false;
        }
    }
}
com/sky/properties/JwtProperties.java
package com.sky.properties;

import lombok.Data;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties(prefix = "sky.jwt")
@Data
public class JwtProperties {

    /**
     * 管理端员工生成jwt令牌相关配置
     */
    private String adminSecretKey;
    private long adminTtl;
    private String adminTokenName;

    /**
     * 用户端微信用户生成jwt令牌相关配置
     */
    private String userSecretKey;
    private long userTtl;
    private String userTokenName;
}

代码完善

程序存在的问题:
  • 录入的用户名已存在,抛出异常后没有处理
  • 新增员工时,创建人id和修改人id设置了固定值

当你在 Maven 中执行 compile 命令时,它会强制 Maven 重新编译整个项目,包括所有的类和资源。这一过程会清除任何旧的编译结果,确保所有的依赖和代码都是最新的。这可能导致以下几种情况,从而解决了你的问题:

**重新编译:**Maven 会重新编译所有的源代码,包括你修改或新增的类,这样就能解决因为旧的编译缓存而引起的引用问题。
**更新依赖:**如果你在项目中添加或修改了依赖,执行 compile 可以确保这些依赖被正确加载和引用。
清理旧缓存在编译过程中,Maven 会清理旧的缓存和临时文件,避免由于这些文件造成的潜在冲突。
**IDE 同步:**有时候,IDE 的状态可能与 Maven 项目状态不一致,执行 Maven 命令可以帮助 IDE 重新同步项目的状态。

问题①
sky-server  com/sky/handler/GlobalExceptionHandler.java
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;

import java.sql.SQLIntegrityConstraintViolationException;

/**
 * 全局异常处理器,处理项目中抛出的业务异常
 */
@RestControllerAdvice
@Slf4j
public class GlobalExceptionHandler {

    /**
     * 捕获业务异常
     * @param ex
     * @return
     */
    @ExceptionHandler
    public Result exceptionHandler(BaseException ex){
        log.error("异常信息:{}", ex.getMessage());
        return Result.error(ex.getMessage());
    }

    @ExceptionHandler
    public Result exceptionHandler(SQLIntegrityConstraintViolationException ex) {
        String message = ex.getMessage();
        if (message.contains("Duplicate entry")) {
            // Duplicate entry 'zhangsan' for key 'employee.idx_username'
// 在这里,我们使用 split("'") 将字符串分割为多个部分。这样,parts[1] 将得到 zhangsan,因为它位于单引号之间。这种方式可以正确提取用户名。
            String[] split = message.split("'");
            String username = split[1];
//            String msg = username + "已存在";
            String msg = username + MessageConstant.ALREADY_EXISTS;
            return Result.error(msg);
        }else {
            return Result.error(MessageConstant.UNKNOWN_ERROR);
        }
    }
}

Split的深入学习

  • 正则表达式

split 方法接受一个正则表达式作为参数,因此分隔符可以是复杂的模式,而不仅仅是单个字符。
例如,split(“\s+”) 可以用来按一个或多个空白字符(包括空格、制表符、换行符等)进行分割。

  • 限制分割次数

split 方法还有一个重载版本 split(String regex, int limit),可以限制分割的次数。
例如,split(“‘“, 3) 只会进行两次分割,结果数组最多包含三个元素。

问题② 解析出员工登录id后,如何转递给Service的save方法?ThreadLocal

前端会携带JWT令牌,通过JWT令牌可以解析出当前登录员工id:
        //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);
            //3、通过,放行
            return true;
        } catch (Exception ex) {
            //4、不通过,响应401状态码
            response.setStatus(401);
            return false;
        }
ThreadLocal

ThreadLocal并不是一个Thread,Thread的局部变量
ThreadLocal为每个线程提供单独一份存储空间,具有线程隔离的效果,只有在线程内才能获得到对应的值,线程外则不能访问

sky-common  com/sky/context/BaseContext.java
package com.sky.context;

public class BaseContext {

    public 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();
    }
}
sky-server  com/sky/interceptor/JwtTokenAdminInterceptor.java
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;
        }
    }
}
sky-server  com/sky/service/impl/EmployeeServiceImpl.java
/**
     * 新增员工
     * @param employeeDTO
     */
    @Override
    public void save(EmployeeDTO employeeDTO) {
        Employee employee = new Employee();
//        employee.setName(employeeDTO.getName()); 太多了 用对象属性拷贝
        BeanUtils.copyProperties(employeeDTO,employee); //其余的要手动设置
        //设置账号状态,默认正常状态 1正常 0锁定  规范封装
        employee.setStatus(StatusConstant.ENABLE);
        //设置密码,默认密码123456
        employee.setPassword(DigestUtils.md5DigestAsHex(PasswordConstant.DEFAULT_PASSWORD.getBytes()));
        //设置当前记录的创建时间和修改时间
        employee.setCreateTime(LocalDateTime.now());
        employee.setUpdateTime(LocalDateTime.now());
        //设置当前记录创建人id和修改人id
        //TODO 后期需要改为当前登录用户的id
//        employee.setCreateUser(10L);
// ★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★★
        employee.setCreateUser(BaseContext.getCurrentId());
        employee.setUpdateUser(BaseContext.getCurrentId());
    
        employeeMapper.insert(employee);
    }

// ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓ 如果想单独针对22行代码 测试部分的值是多少 ↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓↓
// 左键选中'BaseContext.getCurrentId()' 右键Evaluate Expression单独计算即可

将员工登录ID放在 ThreadLocal 中的原因

在多线程环境中,使用 ThreadLocal 来存储和传递员工的完整信息是一个常见的做法。这样可以确保每个线程都有独立的变量副本,避免并发问题。以下是一些步骤和最佳实践,帮助你在 ThreadLocal 中安全地传递和存储员工的完整信息。
ThreadLocal 主要用于在同一个线程内传递和存储数据,确保每个线程都有独立的变量副本。在你的例子中,ThreadLocal 用于存储员工ID,确保在多线程环境下员工ID的安全传递。

  • 线程隔离:ThreadLocal 确保每个线程都有独立的员工ID副本,避免了多线程环境下的并发问题。
    防止篡改:只有当前线程可以访问和修改 ThreadLocal 中的员工ID,其他线程无法访问,确保了ID的安全性。
  • 员工其他信息的安全性
    数据库查询:员工的其他信息是从数据库中查询的,而不是从 ThreadLocal 中获取的。数据库查询本身是安全的,只要数据库连接和查询操作是安全的。
    权限控制:确保只有经过认证的用户才能执行查询操作,防止未授权访问。
    数据加密:敏感信息(如密码)在存储和传输过程中应进行加密,确保数据的安全性。

线程安全
ThreadLocal 为每个线程提供独立的变量副本,避免了多线程环境下的并发问题。每个线程都可以安全地读取和修改自己的 ThreadLocal 变量,而不会影响其他线程。

简化代码
在 Web 应用中,通常需要在多个方法或组件之间传递用户身份信息(如员工登录ID)。使用 ThreadLocal 可以避免在每个方法调用中显式传递这些信息,从而简化代码。

全局访问
在同一个线程内,任何地方都可以访问 ThreadLocal 中存储的值,这使得在复杂的业务逻辑中传递和使用员工登录ID变得非常方便。

避免传递参数
在多层调用中,如果需要传递员工登录ID,通常需要在每个方法签名中添加相应的参数。使用 ThreadLocal 可以避免这种繁琐的参数传递,提高代码的可读性和可维护性。

// 设置员工登录ID:
在用户登录成功后,将员工登录ID设置到 ThreadLocal 中。
@PostMapping("/login")
public ResponseEntity<?> login(@RequestBody LoginRequest loginRequest) {
    // 验证用户名和密码
    User user = userService.validateUser(loginRequest.getUsername(), loginRequest.getPassword());
    if (user != null) {
        // 设置当前线程的员工登录ID
        BaseContext.setCurrentId(user.getId());
        // 返回登录成功信息
        return ResponseEntity.ok("Login successful");
    } else {
        return ResponseEntity.status(HttpStatus.UNAUTHORIZED).body("Invalid username or password");
    }
}
// 获取员工登录ID
在需要使用员工登录ID的地方,直接从 ThreadLocal 中获取
@Service
public class OrderService {

    public void createOrder(Order order) {
        Long currentUserId = BaseContext.getCurrentId();
        if (currentUserId != null) {
            order.setCreatedBy(currentUserId);
            orderRepository.save(order);
        } else {
            throw new RuntimeException("User ID not found in context");
        }
    }
}

将员工登录ID放在 ThreadLocal 中,可以确保每个线程都有独立的变量副本,避免多线程环境下的并发问题。同时,这种方式简化了代码,提供了全局访问的能力,避免了繁琐的参数传递,使得在复杂的业务逻辑中传递和使用员工登录ID变得非常方便。

员工分页查询

需求分析和设计
业务规则:(查询 → get)
  • 根据页码展示员工信息
  • 每页展示10条数据
  • 分页查询时可以根据需要,输入员工姓名进行查询
代码开发
根据分页查询接口设计对应的DTO:

Query

参数名称 是否必须 示例 备注
name 张三 员工姓名
page 1 页码
pageSize 10 每页记录数
@Data
public class EmployeePageQueryDTO implements Serializable{
    private String name;
    private int page;
    private int pageSize;
}
后面所有的分页查询,统一都封装成PageResult对象
/*封装分页查询结果*/
@Data
@AllArgsConstructor
@NoArgsConstructor
public class PageResult implements Serializable{
    private long total; 
    private List records;
}
员工信息分页查询后端返回的对象类型为:Result < PageResult >
sky-server  com/sky/controller/admin/EmployeeController.java
/**
     * 员工分页查询
     * @param employeePageQueryDTO
     * @return
     */
    @GetMapping("/page")
    public Result<PageResult> page(EmployeePageQueryDTO employeePageQueryDTO){
        //格式不是JSON不用加 @RequestBody
        log.info("员工分页查询,参数为:{}", employeePageQueryDTO);
        PageResult pageResult = employeeService.pageQuery(employeePageQueryDTO);
        return Result.success(pageResult);
    }
sky-server  com/sky/service/EmployeeService.java
/**
     * 分页查询
     * @param employeePageQueryDTO
     * @return
     */
    PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO);
sky-server  com/sky/service/impl/EmployeeServiceImpl.java
/**
     * 分页查询
     * @param employeePageQueryDTO
     * @return
     */
    @Override
    public PageResult pageQuery(EmployeePageQueryDTO employeePageQueryDTO) {
        // select * from employee limit 0,10
        // 开始分页查询 动态拼接
        PageHelper.startPage(employeePageQueryDTO.getPage(), employeePageQueryDTO.getPageSize());
        Page<Employee> page =  employeeMapper.pageQuery(employeePageQueryDTO);

        long total = page.getTotal();
        List<Employee> records = page.getResult();

        return new PageResult(total, records);
}

逐行研究分页查询

  • PageHelper.startPage 是 MyBatis 分页插件提供的方法,用于开启分页功能

    • employeePageQueryDTO.getPage() 获取当前页码。

    • employeePageQueryDTO.getPageSize() 获取每页显示的记录数

      这一行代码的作用是告诉 MyBatis 在接下来的查询中启用分页,并设置分页参数

  • employeeMapper.pageQuery(employeePageQueryDTO) 是调用 MyBatis 的 Mapper 接口方法,执行分页查询。

    • employeePageQueryDTO 包含了查询条件,如关键字、排序字段等。
    • 查询结果会被封装成 Page 对象,其中包含了分页数据和分页元数据。
  • page.getTotal() 获取分页查询的总记录数。

    • 总记录数用于计算总页数和其他分页相关的计算
  • List records = page.getResult();

    • page.getResult() 获取分页查询的实际数据列表。
    • 这个列表包含了当前页的员工记录
  • return new PageResult(total, records);

  • new PageResult(total, records) 创建一个新的 PageResult 对象,将总记录数和分页数据列表封装起来

  • PageResult 类通常包含 total 和 records 属性,用于返回给客户端

  • 假设 employeePageQueryDTO.getPage() 返回 2,employeePageQueryDTO.getPageSize() 返回 10,那么 MyBatis 生成的 SQL 可能类似于:

SELECT * FROM employee
WHERE ... -- 根据 employeePageQueryDTO 中的查询条件
LIMIT 10 OFFSET 10;

LIMIT 10:表示每页显示 10 条记录。
OFFSET 10:表示从第 11 条记录开始(因为页码从 1 开始,所以第 2 页的偏移量是 10)。

  • PageResult 类:用于封装分页查询的结果,包括总记录数和当前页的数据集合。
    使用场景:在分页查询服务中,将查询结果封装为 PageResult 对象,通过控制器返回给客户端。

  • Serializable 接口:是 Java 中的一个标记接口,没有定义任何方法。实现 Serializable 接口的类的对象可以被序列化,即将对象的状态转换为字节流,以便在网络上传输或持久化存储。反序列化则是将字节流恢复为对象的过程。

    • 序列化

    对象状态转换:将对象的状态(即对象的字段值)转换为字节流。
    默认序列化机制:Java 提供了默认的序列化机制,通过 ObjectOutputStream 类的 writeObject 方法实现。
    自定义序列化:可以通过实现 writeObject 和 readObject 方法来自定义序列化和反序列化过程。

    • 持久化

    持久化:序列化的主要目的是将对象的状态保存到存储介质中,或者通过网络传输对象。

查询结果会被封装到 PageResult 对象中,其中 total 表示总记录数,records 表示当前页的数据集合。

sky-server  com/sky/mapper/EmployeeMapper.java
 /**
     * 分页查询 [动态sql 不用注解了 写道 EmployeeMapper.xml]
     * @param employeePageQueryDTO
     * @return
     */
    Page<Employee> pageQuery(EmployeePageQueryDTO employeePageQueryDTO);
sky-server  mapper/EmployeeMapper.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.EmployeeMapper">
    <select id="pageQuery" resultType="com.sky.entity.Employee">
        select * from employee
        <where>
            <if test="name != null and name != ''">
                and name like concat('%',#{name},'%')
            </if>
        </where>
        order by create_time desc
    </select>
</mapper>
<!--
and name like concat('%',#{name},'%'):
如果条件成立,生成的 SQL 条件为 AND name LIKE '%${name}%',实现名称的模糊匹配
-->

[员工管理] (http://localhost/#/employee)

代码完善
问题:创建/更新时间那边传入的数据不是想要的
// 2024929214237
"createTime": [
          2024,
          9,
          29,
          22,
          10,
          37
        ],
        "updateTime": [
          2024,
          9,
          29,
          22,
          10,
          37
        ],
解决方式:
  • 方法一:在属性上加注解,对日期进行格式化(只能处理单独一个属性)

    @JsonFormat(pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime updateTime;
    ------------------------------------------------------------------------
     "createTime": "2024-09-29 22:10:37",
    
  • 方法二:在WebMvcConfiguration中扩展Spring MVC的消息转换器,统一对日期类型进行格式化处理

重写父类方法 去扩展 消息转换器

sky-server  com/sky/config/WebMvcConfiguration.java
 /**
     * 扩展Spring MVC框架的消息转化器
     * @param converters
     */
    protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        log.info("扩展消息转换器...");
        //创建一个消息转换器对象
        MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
        //需要为消息转换器设置一个对象转换器,对象转换器可以将Java对象序列化为json数据
        converter.setObjectMapper(new JacksonObjectMapper());
        //将自己的消息转化器加入容器中
        converters.add(0,converter);
    }
package com.sky.json;

import com.fasterxml.jackson.databind.DeserializationFeature;
import com.fasterxml.jackson.databind.ObjectMapper;
import com.fasterxml.jackson.databind.module.SimpleModule;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalDateTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.deser.LocalTimeDeserializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalDateTimeSerializer;
import com.fasterxml.jackson.datatype.jsr310.ser.LocalTimeSerializer;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.time.format.DateTimeFormatter;

import static com.fasterxml.jackson.databind.DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES;

/**
 * 对象映射器:基于jackson将Java对象转为json,或者将json转为Java对象
 * 将JSON解析为Java对象的过程称为 [从JSON反序列化Java对象]
 * 从Java对象生成JSON的过程称为 [序列化Java对象到JSON]
 */
public class JacksonObjectMapper extends ObjectMapper {

    public static final String DEFAULT_DATE_FORMAT = "yyyy-MM-dd";
    //public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm:ss";
    public static final String DEFAULT_DATE_TIME_FORMAT = "yyyy-MM-dd HH:mm";
    public static final String DEFAULT_TIME_FORMAT = "HH:mm:ss";

    public JacksonObjectMapper() {
        super();
        //收到未知属性时不报异常
        this.configure(FAIL_ON_UNKNOWN_PROPERTIES, false);

        //反序列化时,属性不存在的兼容处理
        this.getDeserializationConfig().withoutFeatures(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);

        SimpleModule simpleModule = new SimpleModule()
                .addDeserializer(LocalDateTime.class, new LocalDateTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
                .addDeserializer(LocalDate.class, new LocalDateDeserializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
                .addDeserializer(LocalTime.class, new LocalTimeDeserializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)))
                .addSerializer(LocalDateTime.class, new LocalDateTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_TIME_FORMAT)))
                .addSerializer(LocalDate.class, new LocalDateSerializer(DateTimeFormatter.ofPattern(DEFAULT_DATE_FORMAT)))
                .addSerializer(LocalTime.class, new LocalTimeSerializer(DateTimeFormatter.ofPattern(DEFAULT_TIME_FORMAT)));

        //注册功能模块 例如,可以添加自定义序列化器和反序列化器
        this.registerModule(simpleModule);
    }
}

启用禁用员工账号

需求分析和设计
业务规则:
  • 可以对状态为 “启用” 的员工账号进行 “禁用” 操作
  • 可以对状态为 “禁用” 的员工账号进行 “启用” 操作
  • 状态为 “禁用” 的员工账号不能登录系统
sky-server
/**
     * 启用禁用员工账号
     * @param status
     * @param id
     * @return
     */
    @PostMapping("/status/{status}")
    @ApiOperation("启用禁用员工账号")
    //因为上面的和下面的参数都是一致 不然需要@PathVariable("status")解释一下
    public Result startOrStop(@PathVariable("status") Integer status, Long id) {
        log.info("启用禁用员工账号: {},{}",status,id);
        employeeService.startOrStop(status, id);
        return Result.success();
    }
sky-server  com/sky/service/EmployeeService.java
 /**
     * 启用禁用员工账号
     * @param status
     * @param id
     * @return
     */
    void startOrStop(Integer status, Long id);
sky-server  com/sky/service/impl/EmployeeServiceImpl.java
/**
     * 启用禁用员工账号
     * @param status
     * @param id
     * @return
     */
    @Override
    public void startOrStop(Integer status, Long id) {
        // update employee set status = ? where id = ?
        Employee employee = new Employee();
        employee.setStatus(status);
        employee.setId(id);

/** 要在Employee.java中添加@Builder 才能使用这种风格
 *      Employee employee = Employee.builder()
 *              .status(status)
 *              .id(id)
 *              .build();
 */
        employeeMapper.update(employee);
    }
sky-server  com/sky/mapper/EmployeeMapper.java
/**
     * 根据主键动态修改属性
     * @param employee
     */
    void update(Employee employee);
sky-server  mapper/EmployeeMapper.xml
<update id="update" parameterType="Employee">
        update employee
        <set>
            <if test="name != null">name = #{name},</if>
            <if test="username != null">username = #{username},</if>
            <if test="password != null">password = #{password},</if>
            <if test="phone != null">phone = #{phone},</if>
            <if test="sex != null">sex = #{sex},</if>
            <if test="idNumber != null">id_Number = #{idNumber},</if>
            <if test="updateTime != null">update_Time = #{updateTime},</if>
            <if test="updateUser != null">update_User = #{updateUser},</if>
            <if test="status != null">status = #{status},</if>
        </set>
        where id = #{id}
    </update>

编辑员工

需求分析和设计[回写数据]

编辑员工功能涉及到两个接口:
  • 根据id查询员工信息
  • 编辑员工信息

代码开发

sky-server  com/sky/controller/admin/EmployeeController.java
    /**
     * 根据id查询员工信息
     * @param id
     * @return
     */
    @GetMapping("/{id}")
    @ApiOperation("根据id查询员工信息")
    public Result<Employee> getById(@PathVariable Long id){
        Employee employee = employeeService.getById(id);
        return Result.success(employee);
    }

    /**
     * 编辑员工信息
     * @param employeeDTO
     * @return
     */
    @PutMapping
    @ApiOperation("编辑员工信息")
    public Result update(@RequestBody EmployeeDTO employeeDTO){
        log.info("编辑员工信息:{}", employeeDTO);
        employeeService.update(employeeDTO);
        return Result.success();
    }
sky-server  com/sky/service/EmployeeService.java
/**
     * 根据id查询员工
     * @param id
     * @return
     */
    Employee getById(Long id);

    /**
     * 编辑员工信息
     * @param employeeDTO
     */
    void update(EmployeeDTO employeeDTO);
sky-server  com/sky/service/impl/EmployeeServiceImpl.java
/**
     * 根据id查询员工
     * @param id
     * @return
     */
    public Employee getById(Long id) {
        Employee employee = employeeMapper.getById(id);
        employee.setPassword("****");
        return employee;
    }

    /**
     * 编辑员工信息
     * @param employeeDTO
     */
    public void update(EmployeeDTO employeeDTO) {
        Employee employee = new Employee();
        BeanUtils.copyProperties(employeeDTO, employee);

        //employee.setUpdateTime(LocalDateTime.now());
        //employee.setUpdateUser(BaseContext.getCurrentId());

        employeeMapper.update(employee);
    }
sky-server  com/sky/mapper/EmployeeMapper.java
 /**
     * 根据主键动态修改属性
     * @param employee
     */
    void update(Employee employee);

    /**
     * 根据id查询员工信息
     * @param id
     * @return
     */
    @Select("select * from employee where id = #{id}")
    Employee getById(Long id);
  • 前端提交表单:
    用户在前端页面编辑员工信息并提交表单。
    表单数据被序列化为 JSON 格式,通过 HTTP PUT 请求发送到后端。
  • 后端接收数据:
    控制器方法 update 接收到 EmployeeDTO 对象。
    记录日志,输出接收到的员工信息。
    调用服务层的 update 方法,处理员工信息的更新。
  • 服务层处理:
    创建一个新的 Employee 对象。
    使用 BeanUtils.copyProperties 将 EmployeeDTO 的属性复制到 Employee 对象中。
    调用 MyBatis 的 employeeMapper,执行更新操作。
  • MyBatis 更新操作:
    生成动态 SQL 语句,只更新传入的非 null 属性。
    例如,如果 name 和 phone 不为 null,生成的 SQL 语句如下:

数据回写的具体过程

  • 前端请求获取员工信息 // 根据id查询员工信息
    当你点击编辑按钮时,前端会发起一个 HTTP GET 请求,从后端获取员工的详细信息。这些信息将被用来填充表单字段。
  • 后端处理 GET 请求
    后端需要提供一个接口来处理这个 GET 请求,并返回员工的详细信息。
  • 前端处理响应并填充表单
    前端接收到后端返回的员工信息后,将其填充到表单字段中
/**
     * 根据id查询员工信息
     * @param id
     * @return
     */
    @GetMapping("{/id}")
    @ApiOperation("根据id查询员工信息")
    public Result<Employee> getById(@PathVariable Long id) {
        log.info("根据id查询员工信息:{}", id);
        Employee employee = employeeService.getById(id);
        return Result.success(employee);
    }
<script>export default {
  data() {
    return {
      employee: {
        id: null,
        name: '',
        username: '',
        password: '',
        phone: '',
        sex: '',
        idNumber: ''
      }
    };
  },
  methods: {
    async fetchEmployee(id) {
      try {
        const response = await this.$axios.get(`/employees/${id}`);
        this.employee = response.data.data;
      } catch (error) {
        console.error('获取员工信息失败', error);
      }
    },
    async updateEmployee() {
      try {
        await this.$axios.put('/employees', this.employee);
        alert('员工信息更新成功');
      } catch (error) {
        console.error('更新员工信息失败', error);
      }
    }
  },
  mounted() {
    const id = this.$route.params.id; // 假设通过路由参数传递员工ID
    this.fetchEmployee(id);
  }
};
</script>
  • 提交表单
    当用户编辑完表单并点击保存按钮时,前端会发起一个 HTTP PUT 请求,将更新后的员工信息发送到后端进行处理

总结
前端请求获取员工信息:点击编辑按钮时,前端发起 GET 请求获取员工的详细信息。
后端处理 GET 请求:后端提供一个接口处理 GET 请求,返回员工的详细信息。
前端处理响应并填充表单:前端接收到员工信息后,将其填充到表单字段中。
提交表单:用户编辑完表单并点击保存按钮,前端发起 PUT 请求,将更新后的员工信息发送到后端进行处理。

[苍穹外卖项目接口文档] (http://localhost:8080/doc.html#/default/员工相关接口/updateUsingPUT)

导入分类管理功能代码

业务规则:
  • 分类名称必须是唯一
  • 分类按章类型可分为菜品分类套餐分类
  • 新添加的分类状态默认认为 “禁用
接口设计:
  • 新增分类
  • 分类分页查询
  • 根据id删除分类
  • 修改分类
  • 启用禁止分类
  • 根据类型调查分类

数据库设计(category表)

sky-server  com/sky/controller/admin/CategoryController.java
package com.sky.controller.admin;

import com.sky.dto.CategoryDTO;
import com.sky.dto.CategoryPageQueryDTO;
import com.sky.entity.Category;
import com.sky.result.PageResult;
import com.sky.result.Result;
import com.sky.service.CategoryService;
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.*;
import java.util.List;

/**
 * 分类管理
 */
@RestController
@RequestMapping("/admin/category")
@Api(tags = "分类相关接口")
@Slf4j
public class CategoryController {

    @Autowired
    private CategoryService categoryService;

    /**
     * 新增分类
     * @param categoryDTO
     * @return
     */
    @PostMapping
    @ApiOperation("新增分类")
    public Result<String> save(@RequestBody CategoryDTO categoryDTO){
        log.info("新增分类:{}", categoryDTO);
        categoryService.save(categoryDTO);
        return Result.success();
    }

    /**
     * 分类分页查询
     * @param categoryPageQueryDTO
     * @return
     */
    @GetMapping("/page")
    @ApiOperation("分类分页查询")
    public Result<PageResult> page(CategoryPageQueryDTO categoryPageQueryDTO){
        log.info("分页查询:{}", categoryPageQueryDTO);
        PageResult pageResult = categoryService.pageQuery(categoryPageQueryDTO);
        return Result.success(pageResult);
    }

    /**
     * 删除分类
     * @param id
     * @return
     */
    @DeleteMapping
    @ApiOperation("删除分类")
    public Result<String> deleteById(Long id){
        log.info("删除分类:{}", id);
        categoryService.deleteById(id);
        return Result.success();
    }

    /**
     * 修改分类
     * @param categoryDTO
     * @return
     */
    @PutMapping
    @ApiOperation("修改分类")
    public Result<String> update(@RequestBody CategoryDTO categoryDTO){
        categoryService.update(categoryDTO);
        return Result.success();
    }

    /**
     * 启用、禁用分类
     * @param status
     * @param id
     * @return
     */
    @PostMapping("/status/{status}")
    @ApiOperation("启用禁用分类")
    public Result<String> startOrStop(@PathVariable("status") Integer status, Long id){
        categoryService.startOrStop(status,id);
        return Result.success();
    }

    /**
     * 根据类型查询分类
     * @param type
     * @return
     */
    @GetMapping("/list")
    @ApiOperation("根据类型查询分类")
    public Result<List<Category>> list(Integer type){
        List<Category> list = categoryService.list(type);
        return Result.success(list);
    }
}
sky-server  com/sky/service/CategoryService.java
package com.sky.service;

import com.sky.dto.CategoryDTO;
import com.sky.dto.CategoryPageQueryDTO;
import com.sky.entity.Category;
import com.sky.result.PageResult;
import java.util.List;

public interface CategoryService {

    /**
     * 新增分类
     * @param categoryDTO
     */
    void save(CategoryDTO categoryDTO);

    /**
     * 分页查询
     * @param categoryPageQueryDTO
     * @return
     */
    PageResult pageQuery(CategoryPageQueryDTO categoryPageQueryDTO);

    /**
     * 根据id删除分类
     * @param id
     */
    void deleteById(Long id);

    /**
     * 修改分类
     * @param categoryDTO
     */
    void update(CategoryDTO categoryDTO);

    /**
     * 启用、禁用分类
     * @param status
     * @param id
     */
    void startOrStop(Integer status, Long id);

    /**
     * 根据类型查询分类
     * @param type
     * @return
     */
    List<Category> list(Integer type);
}
sky-server  com/sky/service/impl/CategoryServiceImpl.java
package com.sky.service.impl;

import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import com.sky.constant.MessageConstant;
import com.sky.constant.StatusConstant;
import com.sky.context.BaseContext;
import com.sky.dto.CategoryDTO;
import com.sky.dto.CategoryPageQueryDTO;
import com.sky.entity.Category;
import com.sky.exception.DeletionNotAllowedException;
import com.sky.mapper.CategoryMapper;
import com.sky.mapper.DishMapper;
import com.sky.mapper.SetmealMapper;
import com.sky.result.PageResult;
import com.sky.service.CategoryService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.List;

/**
 * 分类业务层
 */
@Service
@Slf4j
public class CategoryServiceImpl implements CategoryService {

    @Autowired
    private CategoryMapper categoryMapper;
    @Autowired
    private DishMapper dishMapper;
    @Autowired
    private SetmealMapper setmealMapper;

    /**
     * 新增分类
     * @param categoryDTO
     */
    public void save(CategoryDTO categoryDTO) {
        Category category = new Category();
        //属性拷贝
        BeanUtils.copyProperties(categoryDTO, category);

        //分类状态默认为禁用状态0
        category.setStatus(StatusConstant.DISABLE);

        //设置创建时间、修改时间、创建人、修改人
        category.setCreateTime(LocalDateTime.now());
        category.setUpdateTime(LocalDateTime.now());
        category.setCreateUser(BaseContext.getCurrentId());
        category.setUpdateUser(BaseContext.getCurrentId());

        categoryMapper.insert(category);
    }

    /**
     * 分页查询
     * @param categoryPageQueryDTO
     * @return
     */
    public PageResult pageQuery(CategoryPageQueryDTO categoryPageQueryDTO) {
        PageHelper.startPage(categoryPageQueryDTO.getPage(),categoryPageQueryDTO.getPageSize());
        //下一条sql进行分页,自动加入limit关键字分页
        Page<Category> page = categoryMapper.pageQuery(categoryPageQueryDTO);
        return new PageResult(page.getTotal(), page.getResult());
    }

    /**
     * 根据id删除分类
     * @param id
     */
    public void deleteById(Long id) {
        //查询当前分类是否关联了菜品,如果关联了就抛出业务异常
        Integer count = dishMapper.countByCategoryId(id);
        if(count > 0){
            //当前分类下有菜品,不能删除
            throw new DeletionNotAllowedException(MessageConstant.CATEGORY_BE_RELATED_BY_DISH);
        }

        //查询当前分类是否关联了套餐,如果关联了就抛出业务异常
        count = setmealMapper.countByCategoryId(id);
        if(count > 0){
            //当前分类下有菜品,不能删除
            throw new DeletionNotAllowedException(MessageConstant.CATEGORY_BE_RELATED_BY_SETMEAL);
        }

        //删除分类数据
        categoryMapper.deleteById(id);
    }

    /**
     * 修改分类
     * @param categoryDTO
     */
    public void update(CategoryDTO categoryDTO) {
        Category category = new Category();
        BeanUtils.copyProperties(categoryDTO,category);

        //设置修改时间、修改人
        category.setUpdateTime(LocalDateTime.now());
        category.setUpdateUser(BaseContext.getCurrentId());

        categoryMapper.update(category);
    }

    /**
     * 启用、禁用分类
     * @param status
     * @param id
     */
    public void startOrStop(Integer status, Long id) {
        Category category = Category.builder()
                .id(id)
                .status(status)
                .updateTime(LocalDateTime.now())
                .updateUser(BaseContext.getCurrentId())
                .build();
        categoryMapper.update(category);
    }

    /**
     * 根据类型查询分类
     * @param type
     * @return
     */
    public List<Category> list(Integer type) {
        return categoryMapper.list(type);
    }
}
sky-server  com/sky/mapper/CategoryMapper.java
package com.sky.mapper;

import com.github.pagehelper.Page;
import com.sky.enumeration.OperationType;
import com.sky.dto.CategoryPageQueryDTO;
import com.sky.entity.Category;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;

@Mapper
public interface CategoryMapper {

    /**
     * 插入数据
     * @param category
     */
    @Insert("insert into category(type, name, sort, status, create_time, update_time, create_user, update_user)" +
            " VALUES" +
            " (#{type}, #{name}, #{sort}, #{status}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser})")
    void insert(Category category);

    /**
     * 分页查询
     * @param categoryPageQueryDTO
     * @return
     */
    Page<Category> pageQuery(CategoryPageQueryDTO categoryPageQueryDTO);

    /**
     * 根据id删除分类
     * @param id
     */
    @Delete("delete from category where id = #{id}")
    void deleteById(Long id);

    /**
     * 根据id修改分类
     * @param category
     */
    void update(Category category);

    /**
     * 根据类型查询分类
     * @param type
     * @return
     */
    List<Category> list(Integer type);
}
sky-server  mapper/CategoryMapper.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.CategoryMapper">

    <select id="pageQuery" resultType="com.sky.entity.Category">
        select * from category
        <where>
            <if test="name != null and name != ''">
                and name like concat('%',#{name},'%')
            </if>
            <if test="type != null">
                and type = #{type}
            </if>
        </where>
        order by sort asc , create_time desc
    </select>

    <update id="update" parameterType="Category">
        update category
        <set>
            <if test="type != null">
                type = #{type},
            </if>
            <if test="name != null">
                name = #{name},
            </if>
            <if test="sort != null">
                sort = #{sort},
            </if>
            <if test="status != null">
                status = #{status},
            </if>
            <if test="updateTime != null">
                update_time = #{updateTime},
            </if>
            <if test="updateUser != null">
                update_user = #{updateUser}
            </if>
        </set>
        where id = #{id}
    </update>

    <select id="list" resultType="Category">
        select * from category
        where status = 1
        <if test="type != null">
            and type = #{type}
        </if>
        order by sort asc,create_time desc
    </select>
</mapper>
sky-server  com/sky/mapper/DishMapper.java
package com.sky.mapper;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;

@Mapper
public interface DishMapper {

    /**
     * 根据分类id查询菜品数量
     * @param categoryId
     * @return
     */
    @Select("select count(id) from dish where category_id = #{categoryId}")
    Integer countByCategoryId(Long categoryId);

}
sky-server  com/sky/mapper/SetmealMapper.java
package com.sky.mapper;

import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;

@Mapper
public interface SetmealMapper {

    /**
     * 根据分类id查询套餐的数量
     * @param id
     * @return
     */
    @Select("select count(id) from setmeal where category_id = #{categoryId}")
    Integer countByCategoryId(Long id);

}

菜品管理

公共字段自动填充

业务表中的公共字段:(后期会很多[菜品/套餐管理])

问题:代码冗余不利于后期维护
序号 字段名 含义 数据类型
1 create_time 创建时间 datetime
2 create_user 创建人id bigint
3 update_time 修改时间 datetime
4 update_user 修改人id bigint
解决:技术点 → 枚举、注解、AOP、反射
序号 字段名 含义 数据类型 操作类型
1 create_time 创建时间 datetime insert
2 create_user 创建人id bigint insert
3 update_time 修改时间 datetime insert、update
4 update_user 修改人id bigint insert、update
  • 自定义注解 AutoFill,用于标识需要进行公共字段自动填充的方法
  • 自定义切面 AutoFillAspect,统一拦截加入了 AutoFill 注解的方法,通过反射为公共字段赋值
  • Mapper 的方法上加入 AutoFill 注解

代码开发1

sky-server  com/sky/annotation/AutoFill.java
package com.sky.annotation;

import com.sky.enumeration.OperationType;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

/**
 * 自定义注解,用于标识某个方法需要进行功能字段自动填充处理
 */
@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface AutoFill {
// 枚举数据库操作类型:UPDATE INSERT [只要在这情况才有必要设置]
    OperationType value();
}
sky-server  com/sky/aspect/AutoFillAspect.java
package com.sky.aspect;

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.springframework.stereotype.Component;

/**
 * 自定义切面,实现公共字段自动填充处理逻辑
 */
@Aspect
@Component
@Slf4j
public class AutoFillAspect {
    /**
     * 切入点
     */
    // 拦截类 + 注解的东西
    @Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
    public void autoFillPointCut(){}

    // 前置通知,在通知中进行公共字段的赋值
    @Before("autoFillPointCut()")
    public void autoFill(JoinPoint joinPoint) {
        log.info("开始公共字段自动填充...");
    }
}
sky-server  com/sky/mapper/EmployeeMapper.java
//只在update和insert里加
@Mapper
public interface EmployeeMapper {
 /**
     * 插入员工数据
     * @param employee
     */
    @Insert("insert into employee (name, username, password, phone, sex, id_number, create_time, update_time, create_user, update_user,status) " +
            "values " +
            "(#{name},#{username},#{password},#{phone},#{sex},#{idNumber},#{createTime},#{updateTime},#{createUser},#{updateUser},#{status})")
    @AutoFill(value = OperationType.INSERT)
    void insert(Employee employee);
/**
     * 分页查询 [动态sql 不用注解了 写道 EmployeeMapper.xml]
     * @param employeePageQueryDTO
     * @return
     */
    Page<Employee> pageQuery(EmployeePageQueryDTO employeePageQueryDTO);
    @AutoFill(value = OperationType.UPDATE)
}
sky-server  com/sky/mapper/CategoryMapper.java
package com.sky.mapper;

import com.github.pagehelper.Page;
import com.sky.annotation.AutoFill;
import com.sky.enumeration.OperationType;
import com.sky.dto.CategoryPageQueryDTO;
import com.sky.entity.Category;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import java.util.List;
@Mapper
public interface CategoryMapper {

    /**
     * 插入数据
     * @param category
     */
    @Insert("insert into category(type, name, sort, status, create_time, update_time, create_user, update_user)" +
            " VALUES" +
            " (#{type}, #{name}, #{sort}, #{status}, #{createTime}, #{updateTime}, #{createUser}, #{updateUser})")
    void insert(Category category);

    /**
     * 根据id修改分类
     * @param category
     */
    void update(Category category);
}

详细讲解@AutoFill

@Target(ElementType.METHOD)
使用@Target注解指定自定义注解@AutoFill可以应用的目标元素类,这里指定了ElementType.METHOD,表示@AutoFill**只能应用于方法上**

@Retention(RetentionPolicy.RUNTIME)
使用 @Retention 注解指定自定义注解 @AutoFill 的保留策略。这里指定了 RetentionPolicy.RUNTIME,表示 @AutoFill 注解会在运行时保留,可以通过反射获取到。

public @interface AutoFill {…}
@interface:关键字,用于定义一个新的注解类型。
AutoFill:注解的名称,表示这个注解就叫做AutoFill

区分普通接口:@interface 与普通的 interface 不同,普通的interface用于定义接口,而@interface用于定义注解,@符号帮助编译器区分这两者

package com.sky.enumeration;

/**
 * 数据库操作类型
 */
public enum OperationType {
    /**
     * 更新操作
     */
    UPDATE,

    /**
     * 插入操作
     */
    INSERT
}

@Aspect
使用@Aspect注解将这个类标记为一个切面,切面是AOP(面向切面编程),用于定义切面关注点(日志记录、事务管理)

@Component
使用@Component注解将这个类标记为Spring管理的Bean,这样Spring容器会自动扫描并管理这个类的实例

@Slf4j
使用 @Slf4j 注解生成一个日志记录器(Logger)实例。这个注解来自 Lombok 库,可以简化日志记录器的创建

@Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
    public void autoFillPointCut(){}

使用@Pointcut注解定义一个切入点autoFillPointCut

  • execution(* com.sky.mapper.*.*(..)):匹配com.sky.mapper包下所有类的所有方法
  • && @annotation(com.sky.annotation.AutoFill):并且这些方法必须带有@AutoFill注解
  • public void autoFillPointCut():定义一个空的方法,用于标识这个切入点
/**
 * 自定义切面,实现公共字段自动填充处理逻辑
 */
@Aspect
@Component
@Slf4j
public class AutoFillAspect {
    /**
     * 切入点
     */
    // 拦截类 + 注解的东西
    @Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
    public void autoFillPointCut(){}

    // 前置通知,在通知中进行公共字段的赋值
    @Before("autoFillPointCut()")
    public void autoFill(JoinPoint joinPoint) {
        log.info("开始公共字段自动填充...");
    }
}

使用@Before注解定义一个前置通知autoFill,这个通知会在切入点方法执行前被调用

  • @Before("autoFillPointCut()"):指定这个通知应用于autoFillPointCut切入点
  • public void autoFill(JoinPoint joinPoint):定义通知方法,接收一个JoinPoint参数,JoinPoint包含了连接点的信息,如被拦截的方法、参数等

代码开发2

公共属性赋值后 Service里的 save(Employee employee) → employee.setCreateUser(BaseContext.getCurrentId())就不用再去赋值了

这个写完后 就可以把Service里的一些employee.setXXX的删除了 因为公共属性只需要加@AutoFill
sky-server  com/sky/annotation/AutoFill.java 不变
sky-server  com/sky/aspect/AutoFillAspect.java
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.Signature;
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.InvocationTargetException;
import java.lang.reflect.Method;
import java.time.LocalDateTime;

/**
 * 自定义切面,实现公共字段自动填充处理逻辑
 */
@Aspect
@Component
@Slf4j
public class AutoFillAspect {
    /**
     * 切入点
     */
    // 拦截类 + 注解的东西
    @Pointcut("execution(* com.sky.mapper.*.*(..)) && @annotation(com.sky.annotation.AutoFill)")
    public void autoFillPointCut(){}

    // 前置通知,在通知中进行公共字段的赋值
    @Before("autoFillPointCut()")
    public void autoFill(JoinPoint joinPoint) throws NoSuchMethodException, InvocationTargetException, IllegalAccessException {
        log.info("开始公共字段自动填充...");

        // 获取当前被拦截的方法上的数据库操作类型(Update/Insert)
        MethodSignature signature = (MethodSignature)joinPoint.getSignature(); //方法签名对象
        AutoFill autoFill = signature.getMethod().getAnnotation(AutoFill.class); //获得方法上的注解对象
        OperationType operationType = autoFill.value();//获得数据库操作类型

        // 获取当当前被拦截的方法的参数--实体对象 (Employee employee)
        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) {
            // 为4个公共字段赋值
            Method setCreateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_TIME, LocalDateTime.class);
            Method setCreateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_CREATE_USER, Long.class);
            Method setUpdateTime = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_TIME, LocalDateTime.class);
            Method setUpdateUser = entity.getClass().getDeclaredMethod(AutoFillConstant.SET_UPDATE_USER, Long.class);


            //通过反射对对象赋值属性
            setCreateTime.invoke(entity,now);
            setCreateUser.invoke(entity,currentId);
            setUpdateTime.invoke(entity,now);
            setUpdateUser.invoke(entity,currentId);
        } else if (operationType == OperationType.UPDATE) {
            // 为2个公共字段赋值
            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);
        }
    }
}

新增菜品

业务规则:
  • 菜品名称必须是唯一的
  • 菜品必须属于某个分类下,不能单独存在
  • 新增菜品时可以根据选择情况菜品的口味
  • 每个菜品必须对应一张图片
接口设计:
  • 根据类型查询分类(已完成) /admin/category/list GET

    这里要注意数据返回 因为它里面的口味算一个集合

  • 文件上传 /admin/common/upload POST

  • 新增菜品 /admin/dish POST

数据库设计:
  • dish菜品表 [一个菜品对应着多种口味]
  • dish_flavour口味表
开发文件上传接口:

浏览器 → 后端服务 → 阿里云OSS

sky-common  com/sky/utils/AliOssUtil.java
package com.sky.utils;

import com.aliyun.oss.ClientException;
import com.aliyun.oss.OSS;
import com.aliyun.oss.OSSClientBuilder;
import com.aliyun.oss.OSSException;
import lombok.AllArgsConstructor;
import lombok.Data;
import lombok.extern.slf4j.Slf4j;


import java.io.ByteArrayInputStream;

@Data
@AllArgsConstructor
@Slf4j
public class AliOssUtil {
// 通过配置类初始化这些数据
    private String endpoint;
    private String accessKeyId;
    private String accessKeySecret;
    private String bucketName;

    /**
     * 文件上传
     *
     * @param bytes
     * @param objectName
     * @return
     */
    public String upload(byte[] bytes, String objectName) {

 // 创建OSSClient实例。 将字节数组转换为输入流,并将其上传到指定的bucket和objectName
        OSS ossClient = new OSSClientBuilder().build(endpoint, accessKeyId, accessKeySecret);

        try {
            // 创建PutObject请求。
            ossClient.putObject(bucketName, objectName, new ByteArrayInputStream(bytes));
        } catch (OSSException oe) {
            System.out.println("Caught an OSSException, which means your request made it to OSS, "
                    + "but was rejected with an error response for some reason.");
            System.out.println("Error Message:" + oe.getErrorMessage());
            System.out.println("Error Code:" + oe.getErrorCode());
            System.out.println("Request ID:" + oe.getRequestId());
            System.out.println("Host ID:" + oe.getHostId());
        } catch (ClientException ce) {
            System.out.println("Caught an ClientException, which means the client encountered "
                    + "a serious internal problem while trying to communicate with OSS, "
                    + "such as not being able to access the network.");
            System.out.println("Error Message:" + ce.getMessage());
        } finally {
            if (ossClient != null) {
                ossClient.shutdown();
            }
        }

        //文件访问路径规则 https://BucketName.Endpoint/ObjectName
        StringBuilder stringBuilder = new StringBuilder("https://");
        stringBuilder
                .append(bucketName)
                .append(".")
                .append(endpoint)
                .append("/")
                .append(objectName);
        log.info("文件上传成功,访问路径:{}", stringBuilder);

        return stringBuilder.toString();
    }
}
sky-common  com/sky/constant/AutoFillConstant.java
package com.sky.constant;

/**
 * 公共字段自动填充相关常量
 */
public class AutoFillConstant {
    /**
     * 实体类中的方法名称
     */
    public static final String SET_CREATE_TIME = "setCreateTime";
    public static final String SET_UPDATE_TIME = "setUpdateTime";
    public static final String SET_CREATE_USER = "setCreateUser";
    public static final String SET_UPDATE_USER = "setUpdateUser";
}
application-dev.yml
sky:
  datasource:
    driver-class-name: com.mysql.cj.jdbc.Driver
    host: localhost
    port: 3306
    database: sky_take_out
    username: root
    password: root
  alioss:
    endpoint: XXXXXXXXX
    access-key-id: XXXXXXXXXXXX
    access-key-secret: XXXXXXXXXXX
    bucketName: XXXXXXXXX
sky-server  com/sky/controller/admin/CommonController.java
package com.sky.controller.admin;

import com.sky.result.Result;
import com.sky.utils.AliOssUtil;
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.PostMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.multipart.MultipartFile;

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

/**
 * 通用接口
 */
@RestController
@RequestMapping("/admin/common")
@Api(tags = "通用接口")
@Slf4j
public class CommonController {
    @Autowired
    private AliOssUtil aliOssUtil;
    @PostMapping("/upload")
    @ApiOperation("文件上传")
    // 如果要测试文件上传 只能用postman或者前后端联调
    public Result<String> upload(MultipartFile file) {
        log.info("文件上传:{}", file);
        // 防止重名覆盖
        try {
            // 原始文件名
            String filename = file.getOriginalFilename();
            // 截取原始文件名的后缀
            String extension = filename.substring(filename.lastIndexOf("."));
            // 构造新文件名UUID
            String objectName = UUID.randomUUID().toString() + extension;

            // 文件的请求路径
            String filepath = aliOssUtil.upload(file.getBytes(), objectName);
            log.info("文件上传成功,访问路径:{}", filepath);
            return Result.success(filepath);
        } catch (IOException e) {
            log.error("文件上传失败:{}", e);
        }
        return Result.error(MessageConstant.UPLOAD_FAILED);
    }
}

如何让application.yml识别到我在application-dev.yml里设置的值呢?

server:
  port: 8080

spring:
  profiles:
    active: dev

你已经在 application.yml 中指定了 spring.profiles.active: dev,这样在启动应用程序时,Spring Boot 会自动加载 application-dev.yml 中的配置

如果你使用的是 IDE(如 IntelliJ IDEA 或 Eclipse),你可以在运行配置中指定激活的环境配置文件。例如,在 IntelliJ IDEA 中:
打开 Run -> Edit Configurations。
选择你的应用程序配置。
在 VM options 中添加 -Dspring.profiles.active=dev。

# 在 VM options 中配置的原理:
在 VM options 中添加 -Dspring.profiles.active=dev 的原理是通过 Java 虚拟机(JVM)的系统属性来设置 Spring Boot 应用程序的活动配置文件。以下是详细的解释:
原理
JVM 系统属性:
JVM 提供了一种机制,允许你在启动时通过命令行参数传递系统属性。这些系统属性可以在应用程序中通过 System.getProperty 方法访问。
-D 前缀用于设置系统属性。例如,-Dkey=value 会将 key 设置为 value。
Spring Boot 配置:
Spring Boot 会读取 spring.profiles.active 系统属性来确定当前激活的配置文件。
当你通过 -Dspring.profiles.active=dev 设置系统属性时,Spring Boot 会在启动时读取这个属性,并根据其值加载相应的配置文件(如 application-dev.yml)。
新增菜品重要代码
sky-pojo  com/sky/dto/DishDTO.java
package com.sky.dto;

import com.sky.entity.DishFlavor;
import lombok.Data;
import java.io.Serializable;
import java.math.BigDecimal;
import java.util.ArrayList;
import java.util.List;

@Data
public class DishDTO implements Serializable {
    //dish属性封装成dto

    private Long id;
    //菜品名称
    private String name;
    //菜品分类id
    private Long categoryId;
    //菜品价格
    private BigDecimal price;
    //图片
    private String image;
    //描述信息
    private String description;
    //0 停售 1 起售
    private Integer status;
    //口味[因为有多种口味要区分]
    private List<DishFlavor> flavors = new ArrayList<>();

}
sky-pojo  com/sky/entity/DishFlavor.java
package com.sky.entity;

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

import java.io.Serializable;

/**
 * 菜品口味
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class DishFlavor implements Serializable {

    private static final long serialVersionUID = 1L;

    private Long id;
    //菜品id
    private Long dishId;

    //口味名称
    private String name;

    //口味数据list
    private String value;

}
sky-server  com/sky/controller/admin/DishController.java
package com.sky.controller.admin;

import com.sky.dto.DishDTO;
import com.sky.result.Result;
import com.sky.service.DishService;
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.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

/**
 * 菜品管理
 */
@RestController
@RequestMapping("/admin/dish")
@Api(tags = "菜品相关接口")
@Slf4j
public class DishController {
    @Autowired
    private DishService dishService;
    @PostMapping
    @ApiOperation("新增菜品")
    //@RequestBody 封装JSON格式的数据
    public Result save(@RequestBody DishDTO dishDTO) {
        log.info("新增菜品:{}", dishDTO);
        dishService.saveWithFlavour(dishDTO);
        return Result.success();
    }
}
com/sky/service/DishService.java
package com.sky.service;

import com.sky.dto.DishDTO;

public interface DishService {
    /**
     * 新增菜品和对应的口味
     * @param dishDTO
     */
    public void saveWithFlavour(DishDTO dishDTO);
}
com/sky/service/impl/DishServiceImpl.java
package com.sky.service.impl;

import com.sky.dto.DishDTO;
import com.sky.entity.Dish;
import com.sky.entity.DishFlavor;
import com.sky.mapper.DishFlavorMapper;
import com.sky.mapper.DishMapper;
import com.sky.service.DishService;
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 java.util.List;

@Service
@Slf4j

public class DishServiceImpl implements DishService {
    @Autowired
    private DishMapper dishMapper;
    @Autowired
    private DishFlavorMapper dishFlavorMapper;
    /**
     * 新增菜品和对应的口味
     * @param dishDTO
     */
    @Override
    @Transactional //保证事务一致性
    public void saveWithFlavour(DishDTO dishDTO) {
        Dish dish = new Dish();
        //直接new出来是空的需要先赋值 属性拷贝[属性命名要一致]
        BeanUtils.copyProperties(dishDTO,dish);

        // 向菜品表插入1条数据
        dishMapper.insert(dish);
        // 前端无法传 要获取dishId
// <insert id="insertBatch" useGeneratedKeys="true" keyProperty="id"> 获取主键值
        Long dishId = dish.getId();

        List<DishFlavor> flavors = dishDTO.getFlavors();
        if (flavors != null && flavors.size() > 0) {
            flavors.forEach(dishFlavor -> {
                dishFlavor.setDishId(dishId);
            });
            // 向口味表插入n条数据 集合对象批量传入不用集合
            dishFlavorMapper.insertBatch(flavors);
        }
    }
}
sky-server  com/sky/mapper/DishMapper.java
package com.sky.mapper;

import com.sky.annotation.AutoFill;
import com.sky.entity.Dish;
import com.sky.enumeration.OperationType;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;

@Mapper
public interface DishMapper {

    /**
     * 根据分类id查询菜品数量
     * @param categoryId
     * @return
     */
    @Select("select count(id) from dish where category_id = #{categoryId}")
    Integer countByCategoryId(Long categoryId);

    /**
     * 插入菜品数据
     */
    @AutoFill(value = OperationType.INSERT)
    void insert(Dish dish);
}
sky-server  mapper/DishMapper.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">
    <insert id="insert" useGeneratedKeys="true" keyProperty="id">
        insert into dish (name, category_id, price, image, description, create_time, update_time, create_user,
                          update_user, status)
        values (#{name}, #{categoryId}, #{price}, #{image}, #{description}, #{createTime}, #{updateTime}, #{createUser},
                #{updateUser}, #{status})
    </insert>
</mapper>
sky-server  com/sky/mapper/DishFlavorMapper.java
package com.sky.mapper;

import com.sky.entity.DishFlavor;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;

@Mapper
public interface DishFlavorMapper {
    /**
     * 批量插入口味数据
     */
    void insertBatch(List<DishFlavor> flavors);
}
sky-server  mapper/DishFlavorMapper.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.DishFlavorMapper">

    <insert id="insertBatch" useGeneratedKeys="true" keyProperty="id">
        insert into dish_flavor (dish_id, name, value) values
        <foreach collection="flavors" item="df" separator=",">
            (#{df.dishId},#{df.name},#{df.value})
        </foreach>
    </insert>
</mapper>

菜品分页查询

菜品名称[ ] 菜品分类[ ] 售卖状态[ ] [搜索]
菜品名称、图片、菜品分类、售价、售卖状态、最后操作事件、操作[修改 删除 启售,停售]
右下角 分页操作

业务规则:
  • 根据页码展示菜品信息
  • 每页展示10条数据
  • 分页查询时可以根据需要输入菜品名称、菜品分类、菜品状态进行查询
接口设计:

Path:/admin/dish/page
Method:GET

代码开发:

根据菜品分页查询接口定义设计对应的DTO
根据菜品分页查询接口定义设计对应的VO[转成Json数据给前端]

sky-server  com/sky/controller/admin/DishController.java
@GetMapping("/page")
    @ApiOperation("菜品分页查询")
    public Result<PageResult> page(DishPageQueryDTO dishPageQueryDTO){
        log.info("菜品分页查询:{}", dishPageQueryDTO);
        PageResult pageResult = dishService.pageQuery(dishPageQueryDTO);
        return Result.success(pageResult);
    }
sky-server  com/sky/service/DishService.java
package com.sky.service;

import com.sky.dto.DishDTO;
import com.sky.dto.DishPageQueryDTO;
import com.sky.result.PageResult;

public interface DishService {
    /**
     * 新增菜品和对应的口味
     * @param dishDTO
     */
    public void saveWithFlavour(DishDTO dishDTO);

    /**
     * 菜品分页查询
     * @param dishPageQueryDTO
     * @return
     */
    PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO);
}
sky-server  com/sky/service/impl/DishServiceImpl.java
@Service
@Slf4j
public class DishServiceImpl implements DishService {
    @Autowired
    private DishMapper dishMapper;
    @Autowired
    private DishFlavorMapper dishFlavorMapper;
 @Override
    public PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO) {
  // 1. 开启分页功能,设置当前页和每页显示的数量
        PageHelper.startPage(dishPageQueryDTO.getPage(), dishPageQueryDTO.getPageSize());
 // 2. 调用 dishMapper 的 pageQuery 方法进行分页查询,返回一个 Page<DishVO> 对象
        Page<DishVO> page = dishMapper.pageQuery(dishPageQueryDTO);
     // 3. 创建并返回 PageResult 对象,包含总记录数和查询结果列表
        return new PageResult(page.getTotal(), page.getResult());
    }
}
sky-server  com/sky/mapper/DishMapper.java
/**
     * 菜品分页查询
     * @param dishPageQueryDTO
     * @return
     */
    Page<DishVO> pageQuery(DishPageQueryDTO dishPageQueryDTO);
sky-server  mapper/DishMapper.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">
    <insert id="insert" useGeneratedKeys="true" keyProperty="id">
        insert into dish (name, category_id, price, image, description, create_time, update_time, create_user,
                          update_user, status)
        values (#{name}, #{categoryId}, #{price}, #{image}, #{description}, #{createTime}, #{updateTime}, #{createUser},
                #{updateUser}, #{status})
    </insert>
    <select id="pageQuery" resultType="com.sky.vo.DishVO">
        select d.*, c.name as categoryName
        from dish d
            left join category c
                on d.category_id=c.id
        <where>
            <if test="name != null and name != ''">
                and d.name like concat('%',#{name},'%')
            </if>
            <if test="categoryId != null">
                and d.category_id = #{categoryId}
            </if>
            <if test="status != null">
                and d.status = #{status}
            </if>
        </where>
        order by d.update_time desc
    </select>
</mapper>

分页查询SQL语句分析

<select id="pageQuery" resultType="com.sky.vo.DishVO">
        select d.*, c.name as categoryName
        from dish d
            left join category c
                on d.category_id=c.id
        <where>
            <if test="name != null and name != ''">
                and d.name like concat('%',#{name},'%')
            </if>
            <if test="categoryId != null">
                and d.category_id = #{categoryId}
            </if>
            <if test="status != null">
                and d.status = #{status}
            </if>
        </where>
        order by d.update_time desc
    </select>

从 dish 表中选择所有列,并从 category 表中选择 name 列,别名为 categoryName。
使用左连接 (left join) 将 dish 表和 category 表连接起来,连接条件是 d.category_id = c.id。

动态生成WHERE子句,< where >标签会自动处理AND和OR关键字的添加,并且会忽略第一个条件前的ADN和OR

删除菜品

单个删除、批量删除、先停售后删除

业务规则:
  • 可以一次删除一个菜品,也可以批量删除菜品

Path: /admin/dish
Method: DELETE
数据库设计
dish表 → id 【菜品】
dish_flavor表 → dish_id 【口味】
setmeal_dish表 → dish_id

  • 起售中的菜品不能删除
  • 被套餐关联的菜品不能删除
  • 删除菜品后,关联的口味数据也需要删除
代码开发:
sky-server  com/sky/controller/admin/DishController.java
 /**
     * 菜品的批量删除
     * @param ids
     * @return
     */
    @DeleteMapping
    @ApiOperation("批量删除菜品")
    //@RequestParam MVC动态解析字符串 ids提取出来
    public Result delete(@RequestParam List<Long> ids) { //ids
        log.info("批量删除菜品:{}", ids);
        dishService.deleteBatch(ids);
        return Result.success();
    }
sky-server  com/sky/service/DishService.java
 /**
     * 菜品的批量删除
     * @param ids
     * @return
     */
    void deleteBatch(List<Long> ids);
sky-server  com/sky/service/impl/DishServiceImpl.java
/**
     * 菜品的批量删除
     * @param ids
     * @return
     */
    @Override
    public void deleteBatch(List<Long> ids) {
        // 判断当前菜品是否能够删除--是否存在起售中的菜品?? 取出id
        for (Long id : ids) {
            Dish dish = dishMapper.getById(id);
            if (dish.getStatus() == StatusConstant.ENABLE) {
                //当前菜品处于起售中,不能删除
                throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE);
            }
        }

        // 判断当前菜品是否能够删除--是否被套餐关联了
        List<Long> setMealIds = setmealDishMapper.getSetmealIdsByDishId(ids);
        if (setMealIds != null && setMealIds.size() > 0) { //存在不允许删除
            // 当前菜品被套餐关联了,不能删除
            throw new DeletionNotAllowedException(MessageConstant.DISH_BE_RELATED_BY_SETMEAL);
        }
        // 删除菜品表中的菜品数据
        for (Long id : ids) {
            dishMapper.deleteById(id);
            // 删除菜品关联的口味数据
            dishFlavorMapper.deleteByDishId(id);
        }
    }
sky-server  com/sky/mapper/DishMapper.java
/**
     * 根据主键删除菜品数据
     */

    @Delete("delete from dish where id = #{id}")
    void deleteById(Long id);
sky-server  com/sky/mapper/DishFlavorMapper.java
/**
     * 根据菜品id删除对应的 口味数据
     * @param id
     */
    @Delete("delete from dish_flavor where dish_id = #{id}")
    void deleteByDishId(Long id);
sky-server  com/sky/mapper/SetmealDishMapper.java
package com.sky.mapper;

import org.apache.ibatis.annotations.Mapper;

import java.util.List;

@Mapper
public interface SetmealDishMapper {
    /**
     * 根据菜品id查询对应的套餐id
     * @param dishIds
     * @return
     */
    // select setmeal_id from setmeal_dish where dish_id in (1,2,3)
    // 在mapper.xml中dishIds是形参  <foreach collection="dishIds">
    List<Long> getSetmealIdsByDishId(List<Long> dishIds);
}
sky-server  mapper/SetmealDishMapper.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.SetmealDishMapper">

    <select id="getSetmealIdsByDishId" resultType="java.lang.Long">
        SELECT setmeal_id
        FROM setmeal_dish
        WHERE dish_id IN
        <foreach collection="dishIds" item="dishId" open="(" separator="," close=")">
           #{dishId}
        </foreach>
    </select>
</mapper>

<!--
★ foreach 标签用于遍历集合,并生成相应的 SQL 语句。
★ collection="dishIds":指定要遍历的集合名称,即传入的参数 dishIds。
★ item="dishId":指定集合中的每个元素的别名,即每次迭代时的变量名。
★ separator=",":指定每个元素之间的分隔符,这里是逗号 ,。
★ open="(" 和 close=")":指定生成的 SQL 语句的开始和结束符号,这里是括号 ( 和 )。
-->

@RequestParm详细分析

public Result delete(@RequestParam List<Long> ids)

@RequestParam:注解用于将请求参数绑定到方法参数上。具体来说,它可以从请求的查询参数中提取出指定的参数值,并将其转换为方法参数的类型;在这个例子中,@RequestParam List ids 表示从请求的查询参数中提取 ids 参数,并将其转换为 List 类型。

修改菜品

数据回显

接口设计:

  • 根据id查询菜品

口味也要回显
Path: /admin/dish/{id}
Method:GET

  • 根据类型查询分类(已实现)
  • 文件上传(已实现)
  • 修改菜品

根据ID修改
Path:/admin/dish
Method:PUT

代码开发:

根据id查询菜品进行信息回显

sky-server  com/sky/controller/admin/DishController.java
/**
     * 根据id查询菜品和对应的口味数据
     * @param id
     * @return
     */
    @GetMapping("/{id}")
    @ApiOperation("根据id查询菜品")
//  @PathVariable 注解用于从 URL 路径中的占位符参数中提取值,并将其绑定到方法参数上
    public Result<DishVO> getById(@PathVariable long id) {
        log.info("根据id查询菜品:{}", id);
        DishVO dishVO = dishService.getByIdWithFlavor(id);
        return Result.success(dishVO);
    }
sky-server  com/sky/service/DishService.java
 /**
     * 根据id查询菜品和对应的口味数据
     * @param id
     * @return
     */
    DishVO getByIdWithFlavor(long id);
sky-server  com/sky/service/impl/DishServiceImpl.java
/**
     * 根据id查询菜品和对应的口味数据
     * @param id
     * @return
     */
    @Override
    public DishVO getByIdWithFlavor(long id) {
        // 根据id查询菜品数据
        Dish dish = dishMapper.getById(id);
        // 根据菜品id查询口味数据
        List<DishFlavor> dishFlavors = dishFlavorMapper.getByDishId(id);

        // 将查询到的数据封装到VO
        DishVO dishVO = new DishVO();
            // 属性拷贝
        BeanUtils.copyProperties(dish,dishVO);
        dishVO.setFlavors(dishFlavors);

        return dishVO;
    }
sky-server  com/sky/mapper/DishFlavorMapper.java
/**
     * 根据菜品id查询对应的口味数据
     * @param id
     * @return
     */
    @Select("select * from dish_flavor where dish_id = #{id}")
    List<DishFlavor> getByDishId(long id);

修改菜品接口

sky-server  com/sky/controller/admin/DishController.java
/**
     * 根据id修改菜品和对应的口味数据
     * @param dishDTO
     * @return
     */
    @PutMapping
    @ApiOperation("修改菜品")
    public Result update(@RequestBody DishDTO dishDTO) {
        log.info("修改菜品:{}", dishDTO);
        dishService.updateWithFlavor(dishDTO);
        return Result.success();
    }
sky-server  com/sky/service/DishService.java
 /**
     * 根据id修改菜品和对应的口味数据
     * @param dishDTO
     * @return
     */
    void updateWithFlavor(DishDTO dishDTO);
sky-server  com/sky/service/impl/DishServiceImpl.java
/**
     * 根据id修改菜品和对应的口味数据
     * @param dishDTO
     * @return
     */
    @Override
    public void updateWithFlavor(DishDTO dishDTO) {
        Dish dish = new Dish();
        BeanUtils.copyProperties(dishDTO,dish);

        // 修改菜品表基本信息 只是基础信息噢
        dishMapper.update(dish);
        // 先删掉原先的
        dishFlavorMapper.deleteByDishId(dishDTO.getId());
        // 再重新插入新的
        List<DishFlavor> flavors = dishDTO.getFlavors();
        if (flavors != null && flavors.size() > 0) {
            flavors.forEach(dishFlavor -> {
                dishFlavor.setDishId(dishDTO.getId());
            });
            // 向口味表插入n条数据 集合对象批量传入不用集合
            dishFlavorMapper.insertBatch(flavors);
        }
    }
/*
这段代码中,将 dishDTO 的属性复制到 dish 对象的主要原因有以下几点:
数据模型分离:
dishDTO 通常用于数据传输,包含前端传来的所有数据。
dish 是数据库实体类,只包含数据库表中的字段。
安全性:
使用 BeanUtils.copyProperties 可以避免将不必要的字段(如前端传来的额外属性)写入数据库。
确保只有预期的字段被更新。
数据校验:
dishDTO 可以包含更多的验证逻辑或额外的属性,而 dish 对象则严格遵循数据库模型。
通过这种方式,可以在更新前对数据进行进一步校验。
事务管理:
添加 @Transactional 注解确保整个更新过程在一个事务中完成。
如果任何一步出错,整个事务都会回滚,保证数据一致性。
*/
sky-server  com/sky/mapper/DishMapper.java
 /**
     * 根据id修改菜品和对应的口味数据
     * @param dish
     */
    //有时间和修改人 不要忘记自动填充
    @AutoFill(value = OperationType.UPDATE)
    void update(Dish dish);
sky-server  mapper/DishMapper.xml
<update id="update">
        update dish
        <set>
            <if
                test="name != null and name != ''">
                name = #{name},
            </if>
        </set>
        where id = #{id}
    </update>
菜品起售停售
sky-server  com/sky/controller/admin/DishController.java
/**
     * 菜品起售停售
     *
     * @param status
     * @param id
     * @return
     */
    @PostMapping("/status/{status}")
    @ApiOperation("菜品起售停售")
    public Result<String> startOrStop(@PathVariable Integer status, Long id) {
        dishService.startOrStop(status, id);
        return Result.success();
    }
sky-server  com/sky/service/DishService.java
/**
     * 菜品起售停售
     *
     * @param status
     * @param id
     * @return
     */
    void startOrStop(Integer status, Long id);
sky-server  com/sky/service/impl/DishServiceImpl.java
/**
     * 菜品起售停售
     * @param status
     * @param id
     */
@Override
    public void startOrStop(Integer status, Long id) {
        Dish dish = Dish.builder()
                .id(id)
                .status(status)
                .build();
        dishMapper.update(dish);

        if (status == StatusConstant.DISABLE) {
            // 如果是停售操作,还需要将包含当前菜品的套餐也停售
            List<Long> dishIds = new ArrayList<>();
            dishIds.add(id);
            // select setmeal_id from setmeal_dish where dish_id in (?,?,?)
            List<Long> setmealIds = setmealDishMapper.getSetmealIdsByDishIds(dishIds);
            if (setmealIds != null && setmealIds.size() > 0) {
                for (Long setmealId : setmealIds) {
                    Setmeal setmeal = Setmeal.builder()
                            .id(setmealId)
                            .status(StatusConstant.DISABLE)
                            .build();
                    setmealMapper.update(setmeal);
                }
            }

        }
    }
sky-server  com/sky/mapper/SetmealMapper.java
/**
     * 根据id修改套餐
     *
     * @param setmeal
     */
    @AutoFill(OperationType.UPDATE)
    void update(Setmeal setmeal);
sky-server  com/sky/mapper/DishMapper.java

    /**
     * 根据套餐id查询菜品
     * @param setmealId
     * @return
     */
    @Select("select a.* from dish a left join setmeal_dish b on a.id = b.dish_id where b.setmeal_id = #{setmealId}")
    List<Dish> getBySetmealId(Long setmealId);
/*
在 SQL 查询中添加筛选条件。
确保返回的结果集中,setmeal_dish 表中的 setmeal_id 字段值与传入的 setmealId 参数值相匹配,从而获取与指定套餐 ID 相关的菜品列表。
*/
sky-server  mapper/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.SetmealMapper">
    <resultMap id="setmealAndDishMap" type="com.sky.vo.SetmealVO" autoMapping="true">
        <result column="id" property="id"/>
        <collection property="setmealDishes" ofType="SetmealDish">
            <result column="sd_id" property="id"/>
            <result column="setmeal_id" property="setmealId"/>
            <result column="dish_id" property="dishId"/>
            <result column="sd_name" property="name"/>
            <result column="sd_price" property="price"/>
            <result column="copies" property="copies"/>
        </collection>
    </resultMap>
    <update id="update" parameterType="Setmeal">
        update setmeal
        <set>
            <if test="name != null">
                name = #{name},
            </if>
            <if test="categoryId != null">
                category_id = #{categoryId},
            </if>
            <if test="price != null">
                price = #{price},
            </if>
            <if test="status != null">
                status = #{status},
            </if>
            <if test="description != null">
                description = #{description},
            </if>
            <if test="image != null">
                image = #{image},
            </if>
            <if test="updateTime != null">
                update_time = #{updateTime},
            </if>
            <if test="updateUser != null">
                update_user = #{updateUser}
            </if>
        </set>
        where id = #{id}
    </update>
</mapper>
@Mapper
public interface SetmealDishMapper {
    /**
     * 根据菜品id查询对应的套餐id
     * @param dishIds
     * @return
     */
    // select setmeal_id from setmeal_dish where dish_id in (1,2,3)
    // 在mapper.xml中dishIds是形参  <foreach collection="dishIds">
    List<Long> getSetmealIdsByDishIds(List<Long> dishIds);
SetmealDishMapper.xml
<select id="getSetmealIdsByDishIds" resultType="java.lang.Long">
        select setmeal_id from setmeal_dish where dish_id in
        <foreach collection="dishIds" item="dishId" separator="," open="(" close=")">
            #{dishId}
        </foreach>
    </select>

修改套餐那些事

sky-server  com/sky/controller/admin/SetmealController.java
package com.sky.controller.admin;

import com.sky.dto.SetmealDTO;
import com.sky.dto.SetmealPageQueryDTO;
import com.sky.result.PageResult;
import com.sky.result.Result;
import com.sky.service.SetmealService;
import com.sky.vo.SetmealVO;
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.cache.annotation.CacheEvict;
import org.springframework.web.bind.annotation.*;

import java.util.List;

/**
 * 套餐管理
 */
@RestController
@RequestMapping("/admin/setmeal")
@Api(tags = "套餐相关接口")
@Slf4j
public class SetmealController {

    @Autowired
    private SetmealService setmealService;

    /**
     * 新增套餐
     *
     * @param setmealDTO
     * @return
     */
    @PostMapping
    @ApiOperation("新增套餐")
    @CacheEvict(cacheNames = "setmealCache",key = "#setmealDTO.categoryId")//key: setmealCache::100
    public Result save(@RequestBody SetmealDTO setmealDTO) {
        setmealService.saveWithDish(setmealDTO);
        return Result.success();
    }

    /**
     * 分页查询
     *
     * @param setmealPageQueryDTO
     * @return
     */
    @GetMapping("/page")
    @ApiOperation("分页查询")
    public Result<PageResult> page(SetmealPageQueryDTO setmealPageQueryDTO) {
        PageResult pageResult = setmealService.pageQuery(setmealPageQueryDTO);
        return Result.success(pageResult);
    }

    /**
     * 批量删除套餐
     *
     * @param ids
     * @return
     */
    @DeleteMapping
    @ApiOperation("批量删除套餐")
    @CacheEvict(cacheNames = "setmealCache",allEntries = true)
    public Result delete(@RequestParam List<Long> ids) {
        setmealService.deleteBatch(ids);
        return Result.success();
    }

    /**
     * 根据id查询套餐,用于修改页面回显数据
     *
     * @param id
     * @return
     */
    @GetMapping("/{id}")
    @ApiOperation("根据id查询套餐")
    public Result<SetmealVO> getById(@PathVariable Long id) {
        SetmealVO setmealVO = setmealService.getByIdWithDish(id);
        return Result.success(setmealVO);
    }

    /**
     * 修改套餐
     *
     * @param setmealDTO
     * @return
     */
    @PutMapping
    @ApiOperation("修改套餐")
    @CacheEvict(cacheNames = "setmealCache",allEntries = true)
    public Result update(@RequestBody SetmealDTO setmealDTO) {
        setmealService.update(setmealDTO);
        return Result.success();
    }

    /**
     * 套餐起售停售
     *
     * @param status
     * @param id
     * @return
     */
    @PostMapping("/status/{status}")
    @ApiOperation("套餐起售停售")
    @CacheEvict(cacheNames = "setmealCache",allEntries = true)
    public Result startOrStop(@PathVariable Integer status, Long id) {
        setmealService.startOrStop(status, id);
        return Result.success();
    }
}
sky-server  com/sky/service/SetmealService.java
package com.sky.service;

import com.sky.dto.SetmealDTO;
import com.sky.dto.SetmealPageQueryDTO;
import com.sky.entity.Setmeal;
import com.sky.result.PageResult;
import com.sky.vo.DishItemVO;
import com.sky.vo.SetmealVO;

import java.util.List;

public interface SetmealService {

    /**
     * 新增套餐,同时需要保存套餐和菜品的关联关系
     *
     * @param setmealDTO
     */
    void saveWithDish(SetmealDTO setmealDTO);

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

    /**
     * 批量删除套餐
     *
     * @param ids
     */
    void deleteBatch(List<Long> ids);

    /**
     * 根据id查询套餐和关联的菜品数据
     *
     * @param id
     * @return
     */
    SetmealVO getByIdWithDish(Long id);

    /**
     * 修改套餐
     *
     * @param setmealDTO
     */
    void update(SetmealDTO setmealDTO);

    /**
     * 套餐起售、停售
     *
     * @param status
     * @param id
     */
    void startOrStop(Integer status, Long id);

    /**
     * 条件查询
     * @param setmeal
     * @return
     */
    List<Setmeal> list(Setmeal setmeal);

    /**
     * 根据id查询菜品选项
     * @param id
     * @return
     */
    List<DishItemVO> getDishItemById(Long id);
}
sky-server  com/sky/service/impl/SetmealServiceImpl.java
package com.sky.service.impl;

import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import com.sky.constant.MessageConstant;
import com.sky.constant.StatusConstant;
import com.sky.dto.SetmealDTO;
import com.sky.dto.SetmealPageQueryDTO;
import com.sky.entity.Dish;
import com.sky.entity.Setmeal;
import com.sky.entity.SetmealDish;
import com.sky.exception.DeletionNotAllowedException;
import com.sky.exception.SetmealEnableFailedException;
import com.sky.mapper.DishMapper;
import com.sky.mapper.SetmealDishMapper;
import com.sky.mapper.SetmealMapper;
import com.sky.result.PageResult;
import com.sky.service.SetmealService;
import com.sky.vo.DishItemVO;
import com.sky.vo.SetmealVO;
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 java.util.List;

/**
 * 套餐业务实现
 */
@Service
@Slf4j
public class SetmealServiceImpl implements SetmealService {

    @Autowired
    private SetmealMapper setmealMapper;
    @Autowired
    private SetmealDishMapper setmealDishMapper;
    @Autowired
    private DishMapper dishMapper;


    /**
     * 新增套餐,同时需要保存套餐和菜品的关联关系
     *
     * @param setmealDTO
     */
    @Transactional
    public void saveWithDish(SetmealDTO setmealDTO) {
        Setmeal setmeal = new Setmeal();
        BeanUtils.copyProperties(setmealDTO, setmeal);

        //向套餐表插入数据
        setmealMapper.insert(setmeal);

        //获取生成的套餐id
        Long setmealId = setmeal.getId();

        List<SetmealDish> setmealDishes = setmealDTO.getSetmealDishes();
        setmealDishes.forEach(setmealDish -> {
            setmealDish.setSetmealId(setmealId);
        });

        //保存套餐和菜品的关联关系
        setmealDishMapper.insertBatch(setmealDishes);
    }

    /**
     * 分页查询
     *
     * @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());
    }

    /**
     * 批量删除套餐
     *
     * @param ids
     */
    @Transactional
    public void deleteBatch(List<Long> ids) {
        ids.forEach(id -> {
            Setmeal setmeal = setmealMapper.getById(id);
            if (StatusConstant.ENABLE == setmeal.getStatus()) {
                //起售中的套餐不能删除
                throw new DeletionNotAllowedException(MessageConstant.SETMEAL_ON_SALE);
            }
        });

        ids.forEach(setmealId -> {
            //删除套餐表中的数据
            setmealMapper.deleteById(setmealId);
            //删除套餐菜品关系表中的数据
            setmealDishMapper.deleteBySetmealId(setmealId);
        });
    }
/**
ids.forEach(id -> { ... }):对ids集合中的每个元素id执行大括号内的操作。
id -> { ... }:定义了一个接受单个参数id的函数,并执行大括号内的逻辑。
在大括号内,根据id查询数据库获取套餐信息,并检查其状态,若状态符合启用条件,则抛出异常。
**/
    
    /**
     * 根据id查询套餐和套餐菜品关系
     *
     * @param id
     * @return
     */
    public SetmealVO getByIdWithDish(Long id) {
        SetmealVO setmealVO = setmealMapper.getByIdWithDish(id);
        return setmealVO;
    }

    /**
     * 修改套餐
     *
     * @param setmealDTO
     */
    @Transactional
    public void update(SetmealDTO setmealDTO) {
        Setmeal setmeal = new Setmeal();
        BeanUtils.copyProperties(setmealDTO, setmeal);

        //1、修改套餐表,执行update
        setmealMapper.update(setmeal);

        //套餐id
        Long setmealId = setmealDTO.getId();

        //2、删除套餐和菜品的关联关系,操作setmeal_dish表,执行delete
        setmealDishMapper.deleteBySetmealId(setmealId);

        List<SetmealDish> setmealDishes = setmealDTO.getSetmealDishes();
        setmealDishes.forEach(setmealDish -> {
            setmealDish.setSetmealId(setmealId);
        });
        //3、重新插入套餐和菜品的关联关系,操作setmeal_dish表,执行insert
        setmealDishMapper.insertBatch(setmealDishes);
    }

    /**
     * 套餐起售、停售
     *
     * @param status
     * @param id
     */
    public void startOrStop(Integer status, Long id) {
        //起售套餐时,判断套餐内是否有停售菜品,有停售菜品提示"套餐内包含未启售菜品,无法启售"
        if (status == StatusConstant.ENABLE) {
            //select a.* from dish a left join setmeal_dish b on a.id = b.dish_id where b.setmeal_id = ?
            List<Dish> dishList = dishMapper.getBySetmealId(id);
            if (dishList != null && dishList.size() > 0) {
                dishList.forEach(dish -> {
                    if (StatusConstant.DISABLE == dish.getStatus()) {
                        throw new SetmealEnableFailedException(MessageConstant.SETMEAL_ENABLE_FAILED);
                    }
                });
            }
        }

        Setmeal setmeal = Setmeal.builder()
                .id(id)
                .status(status)
                .build();
        setmealMapper.update(setmeal);
    }

    /**
     * 条件查询
     * @param setmeal
     * @return
     */
    public List<Setmeal> list(Setmeal setmeal) {
        List<Setmeal> list = setmealMapper.list(setmeal);
        return list;
    }

    /**
     * 根据id查询菜品选项
     * @param id
     * @return
     */
    public List<DishItemVO> getDishItemById(Long id) {
        return setmealMapper.getDishItemBySetmealId(id);
    }
}
sky-server  com/sky/mapper/SetmealMapper.java
package com.sky.mapper;

import com.github.pagehelper.Page;
import com.sky.annotation.AutoFill;
import com.sky.dto.SetmealPageQueryDTO;
import com.sky.entity.Setmeal;
import com.sky.enumeration.OperationType;
import com.sky.vo.DishItemVO;
import com.sky.vo.SetmealVO;
import org.apache.ibatis.annotations.Delete;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;

import java.util.List;
import java.util.Map;

@Mapper
public interface SetmealMapper {

    /**
     * 根据分类id查询套餐的数量
     *
     * @param id
     * @return
     */
    @Select("select count(id) from setmeal where category_id = #{categoryId}")
    Integer countByCategoryId(Long id);

    /**
     * 根据id修改套餐
     *
     * @param setmeal
     */
    @AutoFill(OperationType.UPDATE)
    void update(Setmeal setmeal);

    /**
     * 新增套餐
     *
     * @param setmeal
     */
    @AutoFill(OperationType.INSERT)
    void insert(Setmeal setmeal);

    /**
     * 分页查询
     * @param setmealPageQueryDTO
     * @return
     */
    Page<SetmealVO> pageQuery(SetmealPageQueryDTO setmealPageQueryDTO);

    /**
     * 根据id查询套餐
     * @param id
     * @return
     */
    @Select("select * from setmeal where id = #{id}")
    Setmeal getById(Long id);

    /**
     * 根据id删除套餐
     * @param setmealId
     */
    @Delete("delete from setmeal where id = #{id}")
    void deleteById(Long setmealId);

    /**
     * 根据id查询套餐和套餐菜品关系
     * @param id
     * @return
     */
    SetmealVO getByIdWithDish(Long id);

    /**
     * 动态条件查询套餐
     * @param setmeal
     * @return
     */
    List<Setmeal> list(Setmeal setmeal);

    /**
     * 根据套餐id查询菜品选项
     * @param setmealId
     * @return
     */
    @Select("select sd.name, sd.copies, d.image, d.description " +
            "from setmeal_dish sd left join dish d on sd.dish_id = d.id " +
            "where sd.setmeal_id = #{setmealId}")
    List<DishItemVO> getDishItemBySetmealId(Long setmealId);

    /**
     * 根据条件统计套餐数量
     * @param map
     * @return
     */
    Integer countByMap(Map map);
}
sky-server  mapper/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.SetmealMapper">
    <resultMap id="setmealAndDishMap" type="com.sky.vo.SetmealVO" autoMapping="true">
        <result column="id" property="id"/>
        <collection property="setmealDishes" ofType="SetmealDish">
            <result column="sd_id" property="id"/>
            <result column="setmeal_id" property="setmealId"/>
            <result column="dish_id" property="dishId"/>
            <result column="sd_name" property="name"/>
            <result column="sd_price" property="price"/>
            <result column="copies" property="copies"/>
        </collection>
    </resultMap>
    <update id="update" parameterType="Setmeal">
        update setmeal
        <set>
            <if test="name != null">
                name = #{name},
            </if>
            <if test="categoryId != null">
                category_id = #{categoryId},
            </if>
            <if test="price != null">
                price = #{price},
            </if>
            <if test="status != null">
                status = #{status},
            </if>
            <if test="description != null">
                description = #{description},
            </if>
            <if test="image != null">
                image = #{image},
            </if>
            <if test="updateTime != null">
                update_time = #{updateTime},
            </if>
            <if test="updateUser != null">
                update_user = #{updateUser}
            </if>
        </set>
        where id = #{id}
    </update>

<!--
    <insert>:表示这是一个插入操作。
    id="insert":指定这个 SQL 语句的唯一标识符,通常用于在 MyBatis 映射文件中引用此 SQL 语句。
    parameterType="Setmeal":指定插入操作的参数类型为 Setmeal 类型。
    useGeneratedKeys="true":指示 MyBatis 在执行插入操作后自动获取自动生成的主键。
    keyProperty="id":指定将自动生成的主键值设置到对象的 id 属性上。
-->
    <insert id="insert" parameterType="Setmeal" useGeneratedKeys="true" keyProperty="id">
        insert into setmeal
        (category_id, name, price, status, description, image, create_time, update_time, create_user, update_user)
        values (#{categoryId}, #{name}, #{price}, #{status}, #{description}, #{image}, #{createTime}, #{updateTime},
                #{createUser}, #{updateUser})
    </insert>

    <select id="pageQuery" resultType="com.sky.vo.SetmealVO">
        select
        s.*,c.name categoryName
        from
        setmeal s
        left join
        category c
        on
        s.category_id = c.id
        <where>
            <if test="name != null">
                and s.name like concat('%',#{name},'%')
            </if>
            <if test="status != null">
                and s.status = #{status}
            </if>
            <if test="categoryId != null">
                and s.category_id = #{categoryId}
            </if>
        </where>
        order by s.create_time desc
    </select>

    <select id="getByIdWithDish" parameterType="long" resultMap="setmealAndDishMap">
        select a.*,
               b.id    sd_id,
               b.setmeal_id,
               b.dish_id,
               b.name  sd_name,
               b.price sd_price,
               b.copies
        from setmeal a
                 left join
             setmeal_dish b
             on
                 a.id = b.setmeal_id
        where a.id = #{id}
    </select>

    <select id="list" parameterType="Setmeal" resultType="Setmeal">
        select * from setmeal
        <where>
            <if test="name != null">
                and name like concat('%',#{name},'%')
            </if>
            <if test="categoryId != null">
                and category_id = #{categoryId}
            </if>
            <if test="status != null">
                and status = #{status}
            </if>
        </where>
    </select>

    <select id="countByMap" resultType="java.lang.Integer">
        select count(id) from setmeal
        <where>
            <if test="status != null">
                and status = #{status}
            </if>
            <if test="categoryId != null">
                and category_id = #{categoryId}
            </if>
        </where>
    </select>
</mapper>

Redis入门 [调整营业状态]

Redis是一个基于内存的 key-value 结构数据库

  • 基于内存存储,读写性能高
  • 适合存储热点数据 (热点商品、资讯、新闻) 访问量较大
  • 企业应用广泛

Redis常用数据类型

Redis存储的是key-value结构的数据,其中key是字符串类型,value有5种常用的数据类型:
  • 字符串 string:普通字符串
  • 哈希 hash:散列,类似于java中的HashMap结构
  • 列表 list:按照插入顺序排序,可以有重复元素,类似于java中的LinkedList
  • 集合 set:无序集合,没有重复元素,类似于java中的HashSet
  • 有序集合 sorted set / zset:集合中每个元素关联一个分数(score),根据分数升序排序,没有重复元素

Redis常用命令

  • 字符串操作命令

    ValueOperations valueOperations = redisTemplate.opsForValue();

    • SET key value 设置指定key的值
    • GET key 获取指定key的值
    • SETEX key seconds value 设置指定key的值,并将key的过期时间设为 seconds秒
    • SETNX key value 只有在key不存在时设置key的值
  • 哈希操作命令 [key → value(field1 value1, field2 value2)]

    HashOperations hashOperations = redisTemplate.opsForHash();

    Redis hash是一个string类型的field和value的映射表,hash特别适合用于存储对象

    • HSET key field value 将哈希表key中的字段field的值设为value
    • HGET key field 获取存储在哈希表中指定字段的值
    • HDEL key field 删除存储在哈希表中的指定字段
    • HKEYS key 获取哈希表中所有字段
    • HVALS key 获取哈希表中所有值
  • 列表操作命令

    ListOperations listOperations = redisTemplate.opsForList();

    • LPUSH key value1 [value2] 将一个或多个值插入到列表头部
    • LRANGE key start stop 获取列表指定范围内的元素
    • RPOP key 移除并获取列表最后一个元素
    • LLEN key 获取列表长度
  • 集合操作命令

    SetOperations setOperations = redisTemplate.opsForSet();

    Redis set是string类型的无序集合。集合成员是唯一的,集合中不能出现重复的数据

    • SADD key member1 [member2] 向集合添加一个或多个成员 [无序插入]
    • SMEMBERS key 返回集合中的所有成员
    • SCARD key 获取集合的成员数
    • SINTER key1 [key2] 返回给定所有集合的交集
    • SUNION key1 [key2] 返回所有给定集合的并集
    • SREM key member1 [member2] 删除集合中一个或多个成员
  • 有序列表操作命令

    ZSetOperations zSetOperations = redisTemplate.opsForZSet();

    Redis有序集合是string类型元素的集合,且不允许重复成员。每个元素都会关联一个double类型的分数

    • ZADD key score1 member1 [score2 member2] 向有序集合添加一个或多个成员
    • ZRANGE key start stop [WITHSCORES] 通过索引区间返回有序集合中指定区间内的成员
    • ZINCRBY key increment member 有序集合中对指定成员的分数加上增量increment
    • ZREM key member [member …] 移除有序集合中的一个或多个成员
  • 通用命令

    Redis的通用命令是不分数据类型的,都可以使用的命令

    • KEYS pattern 查找所有符合给定模式(pattern)的key
    • EXISTS key 检查给定key是否存在
    • TYPE key 返回key所存储的值的类型
    • DEL key 该命令用于在key存在是删除key

在java中操作Redis_SpringDataRedis

序列化器:redisTemplate.setKeySerializer(new StringRedisSerializer());

Redis的Java客户端很多
  • Jedis
  • Lettuce
  • Spring Data Redis

Spring Data Redis 是 Spring 的一部分,对Redis底层开发包进行了高度封装
在Spring项目中,可以使用Spring Data Redis来简化操作

Spring Data Redis
  • 导入Spring Data Redis的maven坐标

    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-redis</artifactId>
        <version>2.7.3</version>
    </dependency>
    
  • 配置Redis数据源

    spring:
     redis:
      host: localhost
      port: 6379
      password:
    
  • 编写配置类,创建RedisTemplate对象

    package com.sky.config;
    
    import lombok.extern.slf4j.Slf4j;
    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.StringRedisSerializer;
    
    @Configuration
    @Slf4j
    public class RedisConfiguration {
    
        @Bean
        public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){
            log.info("开始创建redis模板对象...");
            RedisTemplate redisTemplate = new RedisTemplate();
            //设置redis的连接工厂对象
            redisTemplate.setConnectionFactory(redisConnectionFactory);
            //设置redis key的序列化器
            redisTemplate.setKeySerializer(new StringRedisSerializer());
            return redisTemplate;
        }
    }
    
  • 通过RedisTemplate对象操作Redis

测试连接
application.yml
  redis:
    host: ${sky.redis.host}
    port: ${sky.redis.port}
    database: ${sky.redis.database}
application-dev.yml
  redis:
    host: localhost
    port: 6379
    database: 1
sky-server  com/sky/config/RedisConfiguration.java
package com.sky.config;

import lombok.extern.slf4j.Slf4j;
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.StringRedisSerializer;

@Configuration
@Slf4j
public class RedisConfiguration {
    @Bean
    public RedisTemplate redisTemplate(RedisConnectionFactory redisConnectionFactory){
        log.info("开始创建redis模板对象...");
        RedisTemplate redisTemplate = new RedisTemplate();
        //设置redis的连接工厂对象
        redisTemplate.setConnectionFactory(redisConnectionFactory);
        //设置redis key的序列化器 在图形化界面不出现乱码
        redisTemplate.setKeySerializer(new StringRedisSerializer());
        return redisTemplate;
    }
}
sky-server【测试类】 com/sky/test/SpringDataRedisTest.java
package com.sky.test;

import com.mysql.cj.util.TimeUtil;
import net.sf.jsqlparser.statement.select.KSQLWindow;
import org.junit.jupiter.api.Test;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.test.context.SpringBootTest;
import org.springframework.data.redis.core.*;

import java.util.concurrent.TimeUnit;

@SpringBootTest //测试完记得注释 不然每次启动类就会运行这个测试类
public class SpringDataRedisTest {
    @Autowired
    private RedisTemplate redisTemplate;

    @Test
    public void testRedisTemplate(){
        System.out.println(redisTemplate);
        //创建根据字符串、哈希、列表、集合、有序列表、通用命令的代码
        ValueOperations valueOperations = redisTemplate.opsForValue();
        HashOperations hashOperations = redisTemplate.opsForHash();
        ListOperations listOperations = redisTemplate.opsForList();
        SetOperations setOperations = redisTemplate.opsForSet();
        ZSetOperations zSetOperations = redisTemplate.opsForZSet();
    }

    /**
     * 操作字符串类型的数据
     */
    @Test
    public void testString(){
        // set get setex setnx
        redisTemplate.opsForValue().set("city","北京");
        String city = (String) redisTemplate.opsForValue().get("city");
        System.out.println(city); // 北京

        redisTemplate.opsForValue().set("code", "1234", 3, TimeUnit.MINUTES);
        // 第一次调用可以设置成功
        redisTemplate.opsForValue().setIfAbsent("lock", "1");
        // 第二次不可以成功
        redisTemplate.opsForValue().setIfAbsent("lock", "2");
        Object lock = redisTemplate.opsForValue().get("lock");
        System.out.println(lock); // 1

        Object lock2 = redisTemplate.opsForValue().get("locwwk");
        System.out.println(lock2);// null
    }
    
    /**
     * 操作哈希类型的数据
     */
    @Test
    public void testHash(){
        //hset hget hdel hkeys havls
        HashOperations hashOperations = redisTemplate.opsForHash();
        hashOperations.put("100","name","tom");
        hashOperations.put("100","age","20");

        String name = (String) hashOperations.get("100","name");
        System.out.println(name);

        Set keys = hashOperations.keys("100");
        System.out.println(keys);

        List values = hashOperations.values("100");
        System.out.println(values);

        hashOperations.delete("100","age");
/**
     * 操作列表类型的数据
     */
    @Test
    public void testList(){
        //lpush lrange rpop llen
        ListOperations listOperations = redisTemplate.opsForList();

        listOperations.leftPushAll("mylist","a","b","c");
        listOperations.leftPush("mylist","d");

        List mylist = listOperations.range("mylist", 0, -1);
        System.out.println(mylist);

        listOperations.rightPop("mylist");

        Long size = listOperations.size("mylist");
        System.out.println(size);
    }

    /**
     * 操作集合类型的数据
     */
    @Test
    public void testSet(){
        //sadd smembers scard sinter sunion srem
        SetOperations setOperations = redisTemplate.opsForSet();

        setOperations.add("set1","a","b","c","d");
        setOperations.add("set2","a","b","x","y");

        Set members = setOperations.members("set1");
        System.out.println(members);

        Long size = setOperations.size("set1");
        System.out.println(size);

        Set intersect = setOperations.intersect("set1", "set2");
        System.out.println(intersect);

        Set union = setOperations.union("set1", "set2");
        System.out.println(union);

        setOperations.remove("set1","a","b");
    }

    /**
     * 操作有序集合类型的数据
     */
    @Test
    public void testZset(){
        //zadd zrange zincrby zrem
        ZSetOperations zSetOperations = redisTemplate.opsForZSet();

        zSetOperations.add("zset1","a",10);
        zSetOperations.add("zset1","b",12);
        zSetOperations.add("zset1","c",9);

        Set zset1 = zSetOperations.range("zset1", 0, -1);
        System.out.println(zset1);

        zSetOperations.incrementScore("zset1","c",10);

        zSetOperations.remove("zset1","a","b");
    }

    /**
     * 通用命令操作
     */
    @Test
    public void testCommon(){
        //keys exists type del
        Set keys = redisTemplate.keys("*");
        System.out.println(keys);

        Boolean name = redisTemplate.hasKey("name");
        Boolean set1 = redisTemplate.hasKey("set1");

        for (Object key : keys) {
            DataType type = redisTemplate.type(key);
            System.out.println(type.name());
        }

        redisTemplate.delete("mylist");
    }
}

店铺营业状态设置 【存入Redis】

接口设计:
  • 设置营业状态

    Path:/admin/shop/{status}
    Method:PUT
    status 1 店铺营业状态:1为营业,0为打样

  • 管理端查询营业状态

    Path:/admin/shop/status
    Method:GET

  • 用户端查询营业状态

    Path:/user/shop/status
    Method:GET

★ ★ 本项目约定 ★ ★

  • 管理端发出的请求,统一使用**/admin**作为前缀
  • 用户端发出的请求,统一使用**/user**作为前缀

营业状态数据存储方式:基于Redis的字符串来进行存储
key: SHOP_STATUS value: 1 1为营业,0为打样

代码开发:
sky-server  com/sky/controller/admin/ShopController.java
package com.sky.controller.admin;

import com.sky.result.Result;
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.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.*;

@RestController("adminShopController")
@RequestMapping("/admin/shop")
@Api(tags = "店铺相关接口")
@Slf4j
public class ShopController {
    public static final String KEY = "SHOP_STATUS";

    @Autowired
    private RedisTemplate redisTemplate;
    /**
     * 设置店铺营业状态
     * @param status
     * @return
     */
    @PutMapping("/{status}") //动态取到status
    @ApiOperation("设置店铺营业状态")
    public Result setStatus(@PathVariable Integer status) {
        log.info("设置店铺的营业状态为:{}", status == 1 ? "营业中" : "打样中");
        redisTemplate.opsForValue().set(KEY, status);
        return Result.success();
    }

    /**
     * 获取店铺的营业状态
     * @return
     */
    @GetMapping("/status")
    @ApiOperation("获取店铺的营业状态")
    public Result<Integer> getStatus(){
        Integer status = (Integer) redisTemplate.opsForValue().get(KEY);
        log.info("获取到的店铺营业状态为:{}",status == 1 ? "营业中" : "打样中");
        return Result.success(status);
    }
}
sky-server  com/sky/controller/user/ShopController.java
package com.sky.controller.user;

import com.sky.result.Result;
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.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.*;
//@RestController("userShopController") 指定了这个控制器的名称为 userShopController
//这有助于在应用中唯一标识这个控制器,便于管理和调用
@RestController("userShopController")
@RequestMapping("/user/shop")
@Api(tags = "店铺相关接口")
@Slf4j
public class ShopController {
    public static final String KEY = "SHOP_STATUS";

    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 获取店铺的营业状态
     * @return
     */
    @GetMapping("/status")
    @ApiOperation("获取店铺的营业状态")
    public Result<Integer> getStatus(){
        Integer status = (Integer) redisTemplate.opsForValue().get(KEY);
        log.info("获取到的店铺营业状态为:{}",status == 1 ? "营业中" : "打样中");
        return Result.success(status);
    }
}
sky-server  com/sky/config/WebMvcConfiguration.java
// 设置两个接口文档方便在前端文档处调试【管理端+用户端】
package com.sky.config;

import com.sky.interceptor.JwtTokenAdminInterceptor;
import com.sky.interceptor.JwtTokenUserInterceptor;
import com.sky.json.JacksonObjectMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.converter.HttpMessageConverter;
import org.springframework.http.converter.json.MappingJackson2HttpMessageConverter;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
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 java.util.List;

/**
 * 配置类,注册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");
    }

    @Bean
    public Docket docket1(){
        log.info("准备生成接口文档...");
        ApiInfo apiInfo = new ApiInfoBuilder()
                .title("苍穹外卖项目接口文档")
                .version("2.0")
                .description("苍穹外卖项目接口文档")
                .build();

        Docket docket = new Docket(DocumentationType.SWAGGER_2)
                .groupName("管理端接口")
                .apiInfo(apiInfo)
                .select()
                //指定生成接口需要扫描的包
                .apis(RequestHandlerSelectors.basePackage("com.sky.controller.admin"))
                .paths(PathSelectors.any())
                .build();

        return docket;
    }

    @Bean
    public Docket docket2(){
        log.info("准备生成接口文档...");
        ApiInfo apiInfo = new ApiInfoBuilder()
                .title("苍穹外卖项目接口文档")
                .version("2.0")
                .description("苍穹外卖项目接口文档")
                .build();

        Docket docket = new Docket(DocumentationType.SWAGGER_2)
                .groupName("用户端接口")
                .apiInfo(apiInfo)
                .select()
                //指定生成接口需要扫描的包
                .apis(RequestHandlerSelectors.basePackage("com.sky.controller.user"))
                .paths(PathSelectors.any())
                .build();

        return docket;
    }

    /**
     * 设置静态资源映射,主要是访问接口文档(html、js、css)
     * @param registry
     */
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        log.info("开始设置静态资源映射...");
        registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/");
        registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
    }

    /**
     * 扩展Spring MVC框架的消息转化器
     * @param converters
     */
    protected void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
        log.info("扩展消息转换器...");
        //创建一个消息转换器对象
        MappingJackson2HttpMessageConverter converter = new MappingJackson2HttpMessageConverter();
        //需要为消息转换器设置一个对象转换器,对象转换器可以将Java对象序列化为json数据
        converter.setObjectMapper(new JacksonObjectMapper());
        //将自己的消息转化器加入容器中
        converters.add(0,converter);
    }
}

回顾拦截器原理

HttpClient & 微信小程序开发

HttpClient

HttpClient 是 Apache Jakarta Common下的子项目,可以用来提供高效的、最新的、功能丰富的支持HTTP协议的客户端编程工具包,并且它支持HTTP协议最新的版本和建议

<dependency>
    <groupId>org.apache.httpcomponents</groupId>
    <artifactId>httpclient</artifactId>
    <version>4.5.13</version>
</dependency>

核心API:

  • HttpClient
  • HttpClients
  • CloseableHttpClient
  • HttpGet
  • HttpPost

发送请求步骤:

  • 创建HttpClient对象
  • 创建Http请求对象
  • 调用HttpClient的execute方法发送请求
发送GET方式请求 [要先把项目跑起来]
sky-server  com/sky/test/HttpClientTest.java
package com.sky.test;

import org.apache.http.HttpEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.util.EntityUtils;
import org.junit.jupiter.api.Test;
import org.springframework.boot.test.context.SpringBootTest;

import java.io.IOException;

@SpringBootTest
public class HttpClientTest {
    /**
     * 测试通过httpclient发送GET方式的请求
     */

    @Test
    public void testGET() throws IOException {
        // 创建httpclient对象
        CloseableHttpClient httpClient = HttpClients.createDefault();

        // 创建请求对象接口 (GET请求方式+请求地址)
        HttpGet httpGet = new HttpGet("http://localhost:8080/user/shop/status");

        // 发送请求,接受响应结果
        CloseableHttpResponse response = httpClient.execute(httpGet);

        // 获取服务端返回的状态码
        int statusCode = response.getStatusLine().getStatusCode();
        System.out.println("服务端返回的状态码为:" + statusCode);


        HttpEntity entity = response.getEntity();// 获得请求体
        String body = EntityUtils.toString(entity);
        System.out.println("服务端返回的数据为:" + body);

        // 关闭资源
        response.close();
        httpClient.close();
    }

    /**
     * 测试通过httpclient发送POST方式的请求
     */
}
--------------------------------------------------------------------------------
服务端返回的状态码为:200
服务端返回的数据为:{"code":1,"msg":null,"data":0}
发送POST方式请求 [要先把项目跑起来]
sky-server   com/sky/test/HttpClientTest.java
/**
     * 测试通过httpclient发送POST方式的请求
     */
    @Test
    public void testPOST() throws Exception{
        // 创建httpclient对象
        CloseableHttpClient httpClient = HttpClients.createDefault();

        // 创建请求对象
        HttpPost httpPost = new HttpPost("http://localhost:8080/admin/employee/login");
        // 以json方式请求提交参数
        JSONObject jsonObject = new JSONObject();
        jsonObject.put("username","admin");
        jsonObject.put("password","123456");

        StringEntity entity = new StringEntity(jsonObject.toString());
        // 指定请求编码方式
        entity.setContentEncoding("utf-8");
        // 数据格式
        entity.setContentType("application/json");
        httpPost.setEntity(entity);

        // 发送请求
        CloseableHttpResponse response = httpClient.execute(httpPost);

        // 解析返回结果
        int statusCode = response.getStatusLine().getStatusCode();
        System.out.println("响应码为:" + statusCode);

        HttpEntity entity1 = response.getEntity();
        String body = EntityUtils.toString(entity1);
        System.out.println("响应数据为:" + body);

        // 关闭资源
        response.close();
        httpClient.close();
    }
--------------------------------------------------------------------------------
响应码为:200
响应数据为:{"code":1,"msg":null,"data":{"id":1,"userName":"admin","name":"管理员","token":"eyJhbGciOiJIUzI1NiJ9.eyJlbXBJZCI6MSwiZXhwIjoxNzI4MTMyMzczfQ.8M2nIkgtHx8wpORNfhKEWjbprBV6OwC82wgYjAMxe2I"}}
封装后的HttpClientUtil
package com.sky.utils;

import com.alibaba.fastjson.JSONObject;
import org.apache.http.NameValuePair;
import org.apache.http.client.config.RequestConfig;
import org.apache.http.client.entity.UrlEncodedFormEntity;
import org.apache.http.client.methods.CloseableHttpResponse;
import org.apache.http.client.methods.HttpGet;
import org.apache.http.client.methods.HttpPost;
import org.apache.http.client.utils.URIBuilder;
import org.apache.http.entity.StringEntity;
import org.apache.http.impl.client.CloseableHttpClient;
import org.apache.http.impl.client.HttpClients;
import org.apache.http.message.BasicNameValuePair;
import org.apache.http.util.EntityUtils;

import java.io.IOException;
import java.net.URI;
import java.util.ArrayList;
import java.util.List;
import java.util.Map;

/**
 * Http工具类
 */
public class HttpClientUtil {

    static final  int TIMEOUT_MSEC = 5 * 1000;

    /**
     * 发送GET方式请求
     * @param url
     * @param paramMap
     * @return
     */
    public static String doGet(String url,Map<String,String> paramMap){
        // 创建Httpclient对象
        CloseableHttpClient httpClient = HttpClients.createDefault();

        String result = "";
        CloseableHttpResponse response = null;

        try{
            URIBuilder builder = new URIBuilder(url);
            if(paramMap != null){
                for (String key : paramMap.keySet()) {
                    builder.addParameter(key,paramMap.get(key));
                }
            }
            URI uri = builder.build();

            //创建GET请求
            HttpGet httpGet = new HttpGet(uri);

            //发送请求
            response = httpClient.execute(httpGet);

            //判断响应状态
            if(response.getStatusLine().getStatusCode() == 200){
                result = EntityUtils.toString(response.getEntity(),"UTF-8");
            }
        }catch (Exception e){
            e.printStackTrace();
        }finally {
            try {
                response.close();
                httpClient.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        return result;
    }

    /**
     * 发送POST方式请求
     * @param url
     * @param paramMap
     * @return
     * @throws IOException
     */
    public static String doPost(String url, Map<String, String> paramMap) throws IOException {
        // 创建Httpclient对象
        CloseableHttpClient httpClient = HttpClients.createDefault();
        CloseableHttpResponse response = null;
        String resultString = "";

        try {
            // 创建Http Post请求
            HttpPost httpPost = new HttpPost(url);

            // 创建参数列表
            if (paramMap != null) {
                List<NameValuePair> paramList = new ArrayList();
                for (Map.Entry<String, String> param : paramMap.entrySet()) {
                    paramList.add(new BasicNameValuePair(param.getKey(), param.getValue()));
                }
                // 模拟表单
                UrlEncodedFormEntity entity = new UrlEncodedFormEntity(paramList);
                httpPost.setEntity(entity);
            }

            httpPost.setConfig(builderRequestConfig());

            // 执行http请求
            response = httpClient.execute(httpPost);

            resultString = EntityUtils.toString(response.getEntity(), "UTF-8");
        } catch (Exception e) {
            throw e;
        } finally {
            try {
                response.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        return resultString;
    }

    /**
     * 发送POST方式请求
     * @param url
     * @param paramMap
     * @return
     * @throws IOException
     */
    public static String doPost4Json(String url, Map<String, String> paramMap) throws IOException {
        // 创建Httpclient对象
        CloseableHttpClient httpClient = HttpClients.createDefault();
        CloseableHttpResponse response = null;
        String resultString = "";

        try {
            // 创建Http Post请求
            HttpPost httpPost = new HttpPost(url);

            if (paramMap != null) {
                //构造json格式数据
                JSONObject jsonObject = new JSONObject();
                for (Map.Entry<String, String> param : paramMap.entrySet()) {
                    jsonObject.put(param.getKey(),param.getValue());
                }
                StringEntity entity = new StringEntity(jsonObject.toString(),"utf-8");
                //设置请求编码
                entity.setContentEncoding("utf-8");
                //设置数据类型
                entity.setContentType("application/json");
                httpPost.setEntity(entity);
            }

            httpPost.setConfig(builderRequestConfig());

            // 执行http请求
            response = httpClient.execute(httpPost);

            resultString = EntityUtils.toString(response.getEntity(), "UTF-8");
        } catch (Exception e) {
            throw e;
        } finally {
            try {
                response.close();
            } catch (IOException e) {
                e.printStackTrace();
            }
        }

        return resultString;
    }
    private static RequestConfig builderRequestConfig() {
        return RequestConfig.custom()
                .setConnectTimeout(TIMEOUT_MSEC)
                .setConnectionRequestTimeout(TIMEOUT_MSEC)
                .setSocketTimeout(TIMEOUT_MSEC).build();
    }
}

微信小程序开发

小程序 (qq.com)

详情 → 本地设置 → [取消勾选]不校验合法域名…

操作步骤
  • 了解小程序目录结构

    小程序包含一个描述整体程序app和多个和描述各自页面page,一个小程序主体部分由三个文件组村,必须放在项目的根目录

    文件 必需 作用
    app.js 小程序逻辑
    app.json 小程序公共配置
    app.wxss 小程序公共样式表
    一个小程序页面由四个文件组成 [pages → index → …]
    文件类型 必需 作用
    js 页面逻辑
    wxml 页面结构
    json 页面配置
    wxss 页面样式表
  • 编写测试小程序代码

    微信getUserProfile不弹出授权框_wx.getuserprofile没有弹窗-CSDN博客

    app.json 【外面一层】
    {
      "pages": [
        "pages/index/index",
        "pages/logs/logs"
      ],
      "window": {
        "navigationBarTextStyle": "black",
        "navigationBarTitleText": "Sky-Delivery",
        "navigationBarBackgroundColor": "#ffffff"
      },
      "style": "v2",
      "componentFramework": "glass-easel",
      "sitemapLocation": "sitemap.json",
      "lazyCodeLoading": "requiredComponents"
    }
    
    pages/index/index.wxml
    <view class="container">
      <view>
        {{msg}}
      </view>
    
      <view>
        <button bindtap="getUserInfo" type="primary">获取用户信息</button>
        昵称:{{nickName}}
        <image src="{{url}}" style="width: 200px;height: 200px;"></image>
        <button bindtap="wxLogin" type="warn">微信登录</button>
        授权码:{{code}}
      </view>
    
      <view>
        <button bindtap="sendRequest" type="default">发送请求</button>
      </view>
    </view>
    
    pages/index/index.js
    Page({
      data: {
        msg: 'hello world',
        nickName: '',
        url:'',
        code:'',
      },
    
      // 获取微信用户的头像和昵称
      getUserInfo(e){
        wx.getUserProfile({
          desc: '获取用户信息',
          success: (res) => {
            console.log(res.userInfo);
            // 为数据赋值
            this.setData({
              nickName: res.userInfo.nickName,
              url: res.userInfo.avatarUrl
            })
          },
          fail:(err) => {
            console.error('获取用户信息失败', err);
          }
        });
      },
      
      //微信登录,获取微信用户的授权码 
      //拿到后可以去请求微信服务器获得openId
      //授权码提交到后端去调用服务器
      wxLogin(){
        wx.login({
          success: (res) => {
            console.log(res.code)
            this.setData({
              code: res.code
            })
          }
        })
      },
    
      //发送请求
      sendRequest(){
        wx.request({
          url: 'http://localhost:8080/user/shop/status',
          method: 'GET',
          success: (res)=>{
            // data是后端响应回来的整个数据
            console.log(res.data)
          }
        })
      }
    });
    
  • 编译小程序

微信登录

导入小程序代码

E:\Java实例项目1-20套\第24套【项目实战】Java外卖项目实战《苍穹外卖》SpringBoot+SpringMVC+Vue+Swagger+Lombok+Mybatis+SpringSession+Redis+Nginx+小程序\0-0 源码资料\资料\day06\微信小程序代码\mp-weixin
【注意:导入后有很多包名错误common、components】

微信登录流程

开放能力 / 用户信息 / 小程序登录 (qq.com)

属性 类型 默认值 必填 说明
appid string 小程序 appId
secret string 小程序 appSecret
js_code string 登录时获取的 code
grant_type string 授权类型,此处只需填写 authorization_code

PostMan测试 →
GET:https://api.weixin.qq.com/sns/jscode2session?appid=wxa33b4bae9165c5a5&secret=c2d6fc237953d711146c4ad5db3ef947&js_code=0f1hdA200TsYYS1ghD100c3GZJ1hdA2w&grant_type=authorization_code

返回:
{“session_key”:”HsYD32ryqarcnrCXbEyWhg==”,”openid”:”obaex5N3w1_oAP6a4h-c-CkQBsZQ”}

需求分析和设计

数据库设计(user表)

代码开发
sky-server  application.yml
sky:
  jwt:
    # 设置jwt签名加密时使用的秘钥
    admin-secret-key: itcast
    # 设置jwt过期时间
    admin-ttl: 7200000
    # 设置前端传递过来的令牌名称
    admin-token-name: token
    user-secret-key: itheima
    user-ttl: 7200000
    user-token-name: authentication
  alioss:
    endpoint: ${sky.alioss.endpoint}
    access-key-id: ${sky.alioss.access-key-id}
    access-key-secret: ${sky.alioss.access-key-secret}
    bucket: ${sky.alioss.bucket}
  wechat:
    appid: ${sky.wechat.appid}
    secret: ${sky.wechat.secret}
sky-server  application-dev.yml
  wechat:
    appid: xxxxxxx
    secret: xxxxxxx
sky-common  com/sky/properties/WeChatProperties.java
package com.sky.properties;

import lombok.Data;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
@ConfigurationProperties(prefix = "sky.wechat")
@Data
public class WeChatProperties {

    private String appid; //小程序的appid
    private String secret; //小程序的秘钥
    private String mchid; //商户号
    private String mchSerialNo; //商户API证书的证书序列号
    private String privateKeyFilePath; //商户私钥文件
    private String apiV3Key; //证书解密的密钥
    private String weChatPayCertFilePath; //平台证书
    private String notifyUrl; //支付成功的回调地址
    private String refundNotifyUrl; //退款成功的回调地址
}
sky-server  com/sky/controller/user/UserController.java
package com.sky.controller.user;

import com.sky.constant.JwtClaimsConstant;
import com.sky.dto.UserLoginDTO;
import com.sky.entity.User;
import com.sky.properties.JwtProperties;
import com.sky.result.Result;
import com.sky.service.UserService;
import com.sky.utils.JwtUtil;
import com.sky.vo.UserLoginVO;
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.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.util.HashMap;
import java.util.Map;

@RestController
@RequestMapping("/user/user")
@Api(tags = "C端用户相关接口")
@Slf4j
public class UserController {
    @Autowired
    private UserService userService;
    @Autowired
    private JwtProperties jwtProperties;
    /**
     * 微信登录
     * @param userLoginDTO
     * @return
     */
    @PostMapping("/login")
    @ApiOperation("微信登录")
    public Result<UserLoginVO> login(@RequestBody UserLoginDTO userLoginDTO) {
        log.info("微信登录:{}", userLoginDTO.getCode());
        //微信登录
        User user = userService.wxLogin(userLoginDTO);

        //为微信用户生成jwt令牌
        Map<String, Object> claims = new HashMap<>();
        claims.put(JwtClaimsConstant.USER_ID, user.getId());
        String token = JwtUtil.createJWT(jwtProperties.getUserSecretKey(),jwtProperties.getUserTtl(),claims);
        UserLoginVO userLoginVO = UserLoginVO.builder()
                .id(user.getId())
                .openid(user.getOpenid())
                .token(token)
                .build();
        return Result.success(userLoginVO);
    }
}
sky-server  com/sky/service/UserService.java
package com.sky.service;

import com.sky.dto.UserLoginDTO;
import com.sky.entity.User;

public interface UserService {
    /**
     * 微信登录
     * @return
     */
    User wxLogin(UserLoginDTO userLoginDTO);
}
sky-server  com/sky/service/impl/UserServiceImpl.java
package com.sky.service.impl;

import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
import com.sky.constant.MessageConstant;
import com.sky.dto.UserLoginDTO;
import com.sky.entity.User;
import com.sky.exception.LoginFailedException;
import com.sky.mapper.UserMapper;
import com.sky.properties.WeChatProperties;
import com.sky.service.UserService;
import com.sky.utils.HttpClientUtil;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.HashMap;
import java.util.Map;

@Service
@Slf4j
public class UserServiceImpl implements UserService {
    // 微信服务接口地址
    public static final String WX_LOGIN = "https://api.weixin.qq.com/sns/jscode2session";

    @Autowired
    private WeChatProperties weChatProperties;
    @Autowired
    private UserMapper userMapper;
    /**
     * 微信登录
     * @param userLoginDTO
     * @return
     */
    @Override
    public User wxLogin(UserLoginDTO userLoginDTO) {
        String openid = getOpenid(userLoginDTO.getCode());
        // 判断openId是否真的获取到 如果为空代表失败 业务异常
        if (openid == null){
            throw new LoginFailedException(MessageConstant.LOGIN_FAILED);
        }
        // openId是否在表里 可判断是否为新用户
        User user = userMapper.getByOpenid(openid);
        // 如果是新用户,自动完成注册
        if (user == null) {
            user = User.builder()
                    .openid(openid)
                    .createTime(LocalDateTime.now())
                    .build();
            userMapper.insert(user);
        }
        // 返回这个用户对象
        return user;
    }

    /**
     * 调用微信接口服务,获取微信用户的openid
     * @param code
     * @return
     */
    //只有当前类用到
    private String getOpenid(String code) {
        // 调用微信服务器接口 获得当前用户的openid
        // 四个请求参数
        Map<String, String> map = new HashMap<>();
        map.put("appid", weChatProperties.getAppid());
        map.put("secret", weChatProperties.getSecret());
        map.put("js_code", code);
        map.put("grant_type", "authorization_code");
        String json = HttpClientUtil.doGet(WX_LOGIN, map);

        // 获得json对象
        JSONObject jsonObject = JSON.parseObject(json);
        String openid = jsonObject.getString("openid");
        return openid;
    }
}
sky-server  com/sky/mapper/UserMapper.java
package com.sky.mapper;

import com.sky.entity.User;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;

@Mapper
public interface UserMapper {
    /**
     * 根据openid查询用户
     * @param openid
     * @return
     */
    @Select("select * from user where openid = #{openid}")
    User getByOpenid(String openid);

    /**
     * 新增用户
     * @param user
     */
    void insert(User user);
}
sky-server  mapper/UserMapper.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.UserMapper">

    <insert id="insert" useGeneratedKeys="true" keyProperty="id">
        insert into user (openid, name, phone, sex, id_number, avatar, create_time)
        values (#{openid}, #{name}, #{phone}, #{sex}, #{idNumber}, #{avatar}, #{createTime})
    </insert>
</mapper>
【检测小程序用户是否登陆性】
sky-server  com/sky/interceptor/JwtTokenUserInterceptor.java
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 JwtTokenUserInterceptor 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.getUserTokenName());

        //2、校验令牌
        try {
            log.info("jwt校验:{}", token);
            Claims claims = JwtUtil.parseJWT(jwtProperties.getUserSecretKey(), token);
            Long userId = Long.valueOf(claims.get(JwtClaimsConstant.USER_ID).toString());
            log.info("当前用户的id:", userId);
            BaseContext.setCurrentId(userId);
            //3、通过,放行
            return true;
        } catch (Exception ex) {
            //4、不通过,响应401状态码
            response.setStatus(401);
            return false;
        }
    }
}
sky-server  com/sky/config/WebMvcConfiguration.java 【增加jwtTokenUserInterceptor】
/**
 * 配置类,注册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");
    }
}

导入商品浏览功能代码

接口设计
  • 查询分类

    Path: /user/category/list
    Method:GET
    请求参数
    Type: 分类类型→1.菜品分类 2.套餐分类

  • 根据分类id查询菜品

    Path: /user/dish/list
    Method:GET
    请求参数
    categoryId 分类id

  • 根据分类id查询套餐

    Path: /user/setmeal/list?category=111
    Method:GET
    请求参数
    categoryId 分类id

  • 根据套餐id查询包含的菜品

    Path: /user/setmeal/dish/10
    Method:GET
    请求参数
    id 套餐id
    返回数据:
    copies 份数
    description 菜品描述
    image 菜品图片
    name 菜品名称

sky-server  com/sky/controller/user/DishController.java
package com.sky.controller.user;

import com.sky.constant.StatusConstant;
import com.sky.entity.Dish;
import com.sky.result.Result;
import com.sky.service.DishService;
import com.sky.vo.DishVO;
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.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;

@RestController("userDishController")
@RequestMapping("/user/dish")
@Slf4j
@Api(tags = "C端-菜品浏览接口")
public class DishController {
    @Autowired
    private DishService dishService;

    /**
     * 根据分类id查询菜品
     *
     * @param categoryId
     * @return
     */
    @GetMapping("/list")
    @ApiOperation("根据分类id查询菜品")
    public Result<List<DishVO>> list(Long categoryId) {
        Dish dish = new Dish();
        dish.setCategoryId(categoryId);
        dish.setStatus(StatusConstant.ENABLE);//查询起售中的菜品

        List<DishVO> list = dishService.listWithFlavor(dish);

        return Result.success(list);
    }
}
sky-server  com/sky/service/DishService.java
package com.sky.service;

import com.sky.dto.DishDTO;
import com.sky.dto.DishPageQueryDTO;
import com.sky.entity.Dish;
import com.sky.result.PageResult;
import com.sky.vo.DishVO;

import java.util.List;

public interface DishService {
    /**
     * 新增菜品和对应的口味
     * @param dishDTO
     */
    public void saveWithFlavour(DishDTO dishDTO);

    /**
     * 菜品分页查询
     * @param dishPageQueryDTO
     * @return
     */
    PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO);

    /**
     * 菜品的批量删除
     * @param ids
     * @return
     */
    void deleteBatch(List<Long> ids);

    /**
     * 根据id查询菜品和对应的口味数据
     * @param id
     * @return
     */
    DishVO getByIdWithFlavor(long id);

    /**
     * 修改菜品
     * @param dishDTO
     * @return
     */
    void updateWithFlavor(DishDTO dishDTO);

    /**
     * 菜品起售停售
     *
     * @param status
     * @param id
     * @return
     */
    void startOrStop(Integer status, Long id);

    /**
     * 根据分类id查询菜品
     * @param categoryId
     * @return
     */
    List<Dish> list(Long categoryId);

    /**
     * 条件查询菜品和口味
     * @param dish
     * @return
     */
    List<DishVO> listWithFlavor(Dish dish);
}
sky-server  com/sky/service/impl/DishServiceImpl.java
package com.sky.service.impl;

import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import com.sky.constant.MessageConstant;
import com.sky.constant.StatusConstant;
import com.sky.dto.DishDTO;
import com.sky.dto.DishPageQueryDTO;
import com.sky.entity.Dish;
import com.sky.entity.DishFlavor;
import com.sky.entity.Setmeal;
import com.sky.exception.DeletionNotAllowedException;
import com.sky.mapper.DishFlavorMapper;
import com.sky.mapper.DishMapper;
import com.sky.mapper.SetmealDishMapper;
import com.sky.mapper.SetmealMapper;
import com.sky.result.PageResult;
import com.sky.service.DishService;
import com.sky.vo.DishVO;
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 java.util.ArrayList;
import java.util.List;

@Service
@Slf4j

public class DishServiceImpl implements DishService {
    @Autowired
    private DishMapper dishMapper;
    @Autowired
    private DishFlavorMapper dishFlavorMapper;
    @Autowired
    private SetmealDishMapper setmealDishMapper;
    @Autowired
    private SetmealMapper setmealMapper;
    /**
     * 新增菜品和对应的口味
     * @param dishDTO
     */
    @Override
    @Transactional //保证事务一致性
    public void saveWithFlavour(DishDTO dishDTO) {
        Dish dish = new Dish();
        //直接new出来是空的需要先赋值 属性拷贝[属性命名要一致]
        BeanUtils.copyProperties(dishDTO,dish);

        // 向菜品表插入1条数据
        dishMapper.insert(dish);
        // 前端无法传 要获取dishId
// <insert id="insertBatch" useGeneratedKeys="true" keyProperty="id"> 获取主键值
        Long dishId = dish.getId();

        List<DishFlavor> flavors = dishDTO.getFlavors();
        if (flavors != null && flavors.size() > 0) {
            flavors.forEach(dishFlavor -> {
                dishFlavor.setDishId(dishId);
            });
            // 向口味表插入n条数据 集合对象批量传入不用集合
            dishFlavorMapper.insertBatch(flavors);
        }
    }

    /**
     * 菜品分页查询
     * @param dishPageQueryDTO
     * @return
     */
    @Override
    public PageResult pageQuery(DishPageQueryDTO dishPageQueryDTO) {
        PageHelper.startPage(dishPageQueryDTO.getPage(), dishPageQueryDTO.getPageSize());
        Page<DishVO> page = dishMapper.pageQuery(dishPageQueryDTO);
        return new PageResult(page.getTotal(), page.getResult());
    }

    /**
     * 菜品的批量删除
     * @param ids
     * @return
     */
    @Transactional
    public void deleteBatch(List<Long> ids) {
        // 判断当前菜品是否能够删除--是否存在起售中的菜品?? 取出id
        for (Long id : ids) {
            Dish dish = dishMapper.getById(id);
            if (dish.getStatus() == StatusConstant.ENABLE) {
                //当前菜品处于起售中,不能删除
                throw new DeletionNotAllowedException(MessageConstant.DISH_ON_SALE);
            }
        }

        // 判断当前菜品是否能够删除--是否被套餐关联了
        List<Long> setMealIds = setmealDishMapper.getSetmealIdsByDishIds(ids);
        if (setMealIds != null && setMealIds.size() > 0) { //存在不允许删除
            // 当前菜品被套餐关联了,不能删除
            throw new DeletionNotAllowedException(MessageConstant.DISH_BE_RELATED_BY_SETMEAL);
        }
        // 删除菜品表中的菜品数据
        for (Long id : ids) {
            dishMapper.deleteById(id);
            // 删除菜品关联的口味数据
            dishFlavorMapper.deleteByDishId(id);
        }
    }

    /**
     * 根据id查询菜品和对应的口味数据
     * @param id
     * @return
     */
    @Override
    public DishVO getByIdWithFlavor(long id) {
        // 根据id查询菜品数据
        Dish dish = dishMapper.getById(id);
        // 根据菜品id查询口味数据
        List<DishFlavor> dishFlavors = dishFlavorMapper.getByDishId(id);

        // 将查询到的数据封装到VO
        DishVO dishVO = new DishVO();
            // 属性拷贝
        BeanUtils.copyProperties(dish,dishVO);
        dishVO.setFlavors(dishFlavors);

        return dishVO;
    }



    /**
     * 修改菜品
     * @param dishDTO
     * @return
     */
    @Override
    public void updateWithFlavor(DishDTO dishDTO) {
        Dish dish = new Dish();
        BeanUtils.copyProperties(dishDTO,dish);

        // 修改菜品表基本信息 只是基础信息噢
        dishMapper.update(dish);
        // 先删掉原先的
        dishFlavorMapper.deleteByDishId(dishDTO.getId());
        // 再重新插入新的
        List<DishFlavor> flavors = dishDTO.getFlavors();
        if (flavors != null && flavors.size() > 0) {
            flavors.forEach(dishFlavor -> {
                dishFlavor.setDishId(dishDTO.getId());
            });
            // 向口味表插入n条数据 集合对象批量传入不用集合
            dishFlavorMapper.insertBatch(flavors);
        }
    }

    /**
     * 菜品起售停售
     * @param status
     * @param id
     */
    @Override
    public void startOrStop(Integer status, Long id) {
        Dish dish = Dish.builder()
                .id(id)
                .status(status)
                .build();
        dishMapper.update(dish);

        if (status == StatusConstant.DISABLE) {
            // 如果是停售操作,还需要将包含当前菜品的套餐也停售
            List<Long> dishIds = new ArrayList<>();
            dishIds.add(id);
            // select setmeal_id from setmeal_dish where dish_id in (?,?,?)
            List<Long> setmealIds = setmealDishMapper.getSetmealIdsByDishIds(dishIds);
            if (setmealIds != null && setmealIds.size() > 0) {
                for (Long setmealId : setmealIds) {
                    Setmeal setmeal = Setmeal.builder()
                            .id(setmealId)
                            .status(StatusConstant.DISABLE)
                            .build();
                    setmealMapper.update(setmeal);
                }
            }

        }
    }

    /**
     * 根据分类id查询菜品
     * @param categoryId
     * @return
     */
    public List<Dish> list(Long categoryId) {
        Dish dish = Dish.builder()
                .categoryId(categoryId)
                .status(StatusConstant.ENABLE)
                .build();
        return dishMapper.list(dish);
    }

    /**
     * 条件查询菜品和口味
     * @param dish
     * @return
     */
    public List<DishVO> listWithFlavor(Dish dish) {
        List<Dish> dishList = dishMapper.list(dish);

        List<DishVO> dishVOList = new ArrayList<>();

        for (Dish d : dishList) {
            DishVO dishVO = new DishVO();
            BeanUtils.copyProperties(d,dishVO);

            //根据菜品id查询对应的口味
            List<DishFlavor> flavors = dishFlavorMapper.getByDishId(d.getId());

            dishVO.setFlavors(flavors);
            dishVOList.add(dishVO);
        }

        return dishVOList;
    }
}

sky-server  com/sky/controller/user/SetmealController.java
package com.sky.controller.user;

import com.sky.constant.StatusConstant;
import com.sky.entity.Setmeal;
import com.sky.result.Result;
import com.sky.service.SetmealService;
import com.sky.vo.DishItemVO;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;

@RestController("userSetmealController")
@RequestMapping("/user/setmeal")
@Api(tags = "C端-套餐浏览接口")
public class SetmealController {
    @Autowired
    private SetmealService setmealService;

    /**
     * 条件查询
     *
     * @param categoryId
     * @return
     */
    @GetMapping("/list")
    @ApiOperation("根据分类id查询套餐")
    public Result<List<Setmeal>> list(Long categoryId) {
        Setmeal setmeal = new Setmeal();
        setmeal.setCategoryId(categoryId);
        setmeal.setStatus(StatusConstant.ENABLE);

        List<Setmeal> list = setmealService.list(setmeal);
        return Result.success(list);
    }

    /**
     * 根据套餐id查询包含的菜品列表
     *
     * @param id
     * @return
     */
    @GetMapping("/dish/{id}")
    @ApiOperation("根据套餐id查询包含的菜品列表")
    public Result<List<DishItemVO>> dishList(@PathVariable("id") Long id) {
        List<DishItemVO> list = setmealService.getDishItemById(id);
        return Result.success(list);
    }
}
sky-server  com/sky/service/SetmealService.java
package com.sky.service;

import com.sky.dto.SetmealDTO;
import com.sky.dto.SetmealPageQueryDTO;
import com.sky.entity.Setmeal;
import com.sky.result.PageResult;
import com.sky.vo.DishItemVO;
import com.sky.vo.SetmealVO;

import java.util.List;

public interface SetmealService {

    /**
     * 新增套餐,同时需要保存套餐和菜品的关联关系
     *
     * @param setmealDTO
     */
    void saveWithDish(SetmealDTO setmealDTO);

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

    /**
     * 批量删除套餐
     *
     * @param ids
     */
    void deleteBatch(List<Long> ids);

    /**
     * 根据id查询套餐和关联的菜品数据
     *
     * @param id
     * @return
     */
    SetmealVO getByIdWithDish(Long id);

    /**
     * 修改套餐
     *
     * @param setmealDTO
     */
    void update(SetmealDTO setmealDTO);

    /**
     * 套餐起售、停售
     *
     * @param status
     * @param id
     */
    void startOrStop(Integer status, Long id);

    /**
     * 条件查询
     * @param setmeal
     * @return
     */
    List<Setmeal> list(Setmeal setmeal);

    /**
     * 根据id查询菜品选项
     * @param id
     * @return
     */
    List<DishItemVO> getDishItemById(Long id);
}
sky-server  com/sky/service/impl/SetmealServiceImpl.java
package com.sky.service.impl;

import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import com.sky.constant.MessageConstant;
import com.sky.constant.StatusConstant;
import com.sky.dto.SetmealDTO;
import com.sky.dto.SetmealPageQueryDTO;
import com.sky.entity.Dish;
import com.sky.entity.Setmeal;
import com.sky.entity.SetmealDish;
import com.sky.exception.DeletionNotAllowedException;
import com.sky.exception.SetmealEnableFailedException;
import com.sky.mapper.DishMapper;
import com.sky.mapper.SetmealDishMapper;
import com.sky.mapper.SetmealMapper;
import com.sky.result.PageResult;
import com.sky.service.SetmealService;
import com.sky.vo.DishItemVO;
import com.sky.vo.SetmealVO;
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 java.util.List;

/**
 * 套餐业务实现
 */
@Service
@Slf4j
public class SetmealServiceImpl implements SetmealService {

    @Autowired
    private SetmealMapper setmealMapper;
    @Autowired
    private SetmealDishMapper setmealDishMapper;
    @Autowired
    private DishMapper dishMapper;


    /**
     * 新增套餐,同时需要保存套餐和菜品的关联关系
     *
     * @param setmealDTO
     */
    @Transactional
    public void saveWithDish(SetmealDTO setmealDTO) {
        Setmeal setmeal = new Setmeal();
        BeanUtils.copyProperties(setmealDTO, setmeal);

        //向套餐表插入数据
        setmealMapper.insert(setmeal);

        //获取生成的套餐id
        Long setmealId = setmeal.getId();

        List<SetmealDish> setmealDishes = setmealDTO.getSetmealDishes();
        setmealDishes.forEach(setmealDish -> {
            setmealDish.setSetmealId(setmealId);
        });

        //保存套餐和菜品的关联关系
        setmealDishMapper.insertBatch(setmealDishes);
    }

    /**
     * 分页查询
     *
     * @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());
    }

    /**
     * 批量删除套餐
     *
     * @param ids
     */
    @Transactional
    public void deleteBatch(List<Long> ids) {
        ids.forEach(id -> {
            Setmeal setmeal = setmealMapper.getById(id);
            if (StatusConstant.ENABLE == setmeal.getStatus()) {
                //起售中的套餐不能删除
                throw new DeletionNotAllowedException(MessageConstant.SETMEAL_ON_SALE);
            }
        });

        ids.forEach(setmealId -> {
            //删除套餐表中的数据
            setmealMapper.deleteById(setmealId);
            //删除套餐菜品关系表中的数据
            setmealDishMapper.deleteBySetmealId(setmealId);
        });
    }

    /**
     * 根据id查询套餐和套餐菜品关系
     *
     * @param id
     * @return
     */
    public SetmealVO getByIdWithDish(Long id) {
        SetmealVO setmealVO = setmealMapper.getByIdWithDish(id);
        return setmealVO;
    }

    /**
     * 修改套餐
     *
     * @param setmealDTO
     */
    @Transactional
    public void update(SetmealDTO setmealDTO) {
        Setmeal setmeal = new Setmeal();
        BeanUtils.copyProperties(setmealDTO, setmeal);

        //1、修改套餐表,执行update
        setmealMapper.update(setmeal);

        //套餐id
        Long setmealId = setmealDTO.getId();

        //2、删除套餐和菜品的关联关系,操作setmeal_dish表,执行delete
        setmealDishMapper.deleteBySetmealId(setmealId);

        List<SetmealDish> setmealDishes = setmealDTO.getSetmealDishes();
        setmealDishes.forEach(setmealDish -> {
            setmealDish.setSetmealId(setmealId);
        });
        //3、重新插入套餐和菜品的关联关系,操作setmeal_dish表,执行insert
        setmealDishMapper.insertBatch(setmealDishes);
    }

    /**
     * 套餐起售、停售
     *
     * @param status
     * @param id
     */
    public void startOrStop(Integer status, Long id) {
        //起售套餐时,判断套餐内是否有停售菜品,有停售菜品提示"套餐内包含未启售菜品,无法启售"
        if (status == StatusConstant.ENABLE) {
            //select a.* from dish a left join setmeal_dish b on a.id = b.dish_id where b.setmeal_id = ?
            List<Dish> dishList = dishMapper.getBySetmealId(id);
            if (dishList != null && dishList.size() > 0) {
                dishList.forEach(dish -> {
                    if (StatusConstant.DISABLE == dish.getStatus()) {
                        throw new SetmealEnableFailedException(MessageConstant.SETMEAL_ENABLE_FAILED);
                    }
                });
            }
        }

        Setmeal setmeal = Setmeal.builder()
                .id(id)
                .status(status)
                .build();
        setmealMapper.update(setmeal);
    }

    /**
     * 条件查询
     * @param setmeal
     * @return
     */
    public List<Setmeal> list(Setmeal setmeal) {
        List<Setmeal> list = setmealMapper.list(setmeal);
        return list;
    }

    /**
     * 根据id查询菜品选项
     * @param id
     * @return
     */
    public List<DishItemVO> getDishItemById(Long id) {
        return setmealMapper.getDishItemBySetmealId(id);
    }
}

sky-server  com/sky/controller/user/CategoryController.java
package com.sky.controller.user;

import com.sky.entity.Category;
import com.sky.result.Result;
import com.sky.service.CategoryService;
import io.swagger.annotations.Api;
import io.swagger.annotations.ApiOperation;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;

@RestController("userCategoryController")
@RequestMapping("/user/category")
@Api(tags = "C端-分类接口")
public class CategoryController {

    @Autowired
    private CategoryService categoryService;

    /**
     * 查询分类
     * @param type
     * @return
     */
    @GetMapping("/list")
    @ApiOperation("查询分类")
    public Result<List<Category>> list(Integer type) {
        List<Category> list = categoryService.list(type);
        return Result.success(list);
    }
}
sky-server  com/sky/service/CategoryService.java
package com.sky.service;

import com.sky.annotation.AutoFill;
import com.sky.dto.CategoryDTO;
import com.sky.dto.CategoryPageQueryDTO;
import com.sky.entity.Category;
import com.sky.result.PageResult;
import java.util.List;

public interface CategoryService {

    /**
     * 新增分类
     * @param categoryDTO
     */
    void save(CategoryDTO categoryDTO);
    /**
     * 分页查询
     * @param categoryPageQueryDTO
     * @return
     */
    PageResult pageQuery(CategoryPageQueryDTO categoryPageQueryDTO);

    /**
     * 根据id删除分类
     * @param id
     */
    void deleteById(Long id);

    /**
     * 修改分类
     * @param categoryDTO
     */
    void update(CategoryDTO categoryDTO);

    /**
     * 启用、禁用分类
     * @param status
     * @param id
     */
    void startOrStop(Integer status, Long id);

    /**
     * 根据类型查询分类
     * @param type
     * @return
     */
    List<Category> list(Integer type);
}
sky-server  com/sky/service/impl/CategoryServiceImpl.java
package com.sky.service.impl;

import com.github.pagehelper.Page;
import com.github.pagehelper.PageHelper;
import com.sky.constant.MessageConstant;
import com.sky.constant.StatusConstant;
import com.sky.context.BaseContext;
import com.sky.dto.CategoryDTO;
import com.sky.dto.CategoryPageQueryDTO;
import com.sky.entity.Category;
import com.sky.exception.DeletionNotAllowedException;
import com.sky.mapper.CategoryMapper;
import com.sky.mapper.DishMapper;
import com.sky.mapper.SetmealMapper;
import com.sky.result.PageResult;
import com.sky.service.CategoryService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.util.List;

/**
 * 分类业务层
 */
@Service
@Slf4j
public class CategoryServiceImpl implements CategoryService {

    @Autowired
    private CategoryMapper categoryMapper;
    @Autowired
    private DishMapper dishMapper;
    @Autowired
    private SetmealMapper setmealMapper;

    /**
     * 新增分类
     * @param categoryDTO
     */
    public void save(CategoryDTO categoryDTO) {
        Category category = new Category();
        //属性拷贝
        BeanUtils.copyProperties(categoryDTO, category);

        //分类状态默认为禁用状态0
        category.setStatus(StatusConstant.DISABLE);
/**  公共属性
        //设置创建时间、修改时间、创建人、修改人
        category.setCreateTime(LocalDateTime.now());
        category.setUpdateTime(LocalDateTime.now());
        category.setCreateUser(BaseContext.getCurrentId());
        category.setUpdateUser(BaseContext.getCurrentId());
 **/

        categoryMapper.insert(category);
    }

    /**
     * 分页查询
     * @param categoryPageQueryDTO
     * @return
     */
    public PageResult pageQuery(CategoryPageQueryDTO categoryPageQueryDTO) {
        PageHelper.startPage(categoryPageQueryDTO.getPage(),categoryPageQueryDTO.getPageSize());
        //下一条sql进行分页,自动加入limit关键字分页
        Page<Category> page = categoryMapper.pageQuery(categoryPageQueryDTO);
        return new PageResult(page.getTotal(), page.getResult());
    }

    /**
     * 根据id删除分类
     * @param id
     */
    public void deleteById(Long id) {
        //查询当前分类是否关联了菜品,如果关联了就抛出业务异常
        Integer count = dishMapper.countByCategoryId(id);
        if(count > 0){
            //当前分类下有菜品,不能删除
            throw new DeletionNotAllowedException(MessageConstant.CATEGORY_BE_RELATED_BY_DISH);
        }

        //查询当前分类是否关联了套餐,如果关联了就抛出业务异常
        count = setmealMapper.countByCategoryId(id);
        if(count > 0){
            //当前分类下有菜品,不能删除
            throw new DeletionNotAllowedException(MessageConstant.CATEGORY_BE_RELATED_BY_SETMEAL);
        }

        //删除分类数据
        categoryMapper.deleteById(id);
    }

    /**
     * 修改分类
     * @param categoryDTO
     */
    public void update(CategoryDTO categoryDTO) {
        Category category = new Category();
        BeanUtils.copyProperties(categoryDTO,category);

        //设置修改时间、修改人 (公共属性)
//        category.setUpdateTime(LocalDateTime.now());
//        category.setUpdateUser(BaseContext.getCurrentId());

        categoryMapper.update(category);
    }

    /**
     * 启用、禁用分类
     * @param status
     * @param id
     */
    public void startOrStop(Integer status, Long id) {
        Category category = Category.builder()
                .id(id)
                .status(status) // 下面注释是公共属性AOP有写
//                .updateTime(LocalDateTime.now())
//                .updateUser(BaseContext.getCurrentId())
                .build();
        categoryMapper.update(category);
    }

    /**
     * 根据类型查询分类
     * @param type
     * @return
     */
    public List<Category> list(Integer type) {
        return categoryMapper.list(type);
    }
}

缓存菜品 【redis】

问题说明:

用户端小程序展示的菜品数据都是通过查询数据库获得,如果用户端访问量比较大,数据库访问压力随之增大

实现思路:

通过Redis来缓存菜品数据,减少数据库查询操作

开始→(查询菜品)→后端服务→缓存是否存在→(是)→读取缓存
(否)→查询数据库→载入缓存

缓存逻辑分析:
  • 每个分类下的菜品保存一份缓存数据

    key:dish_1
    value:string(…) [List集合]

sky-server com/sky/controller/user/DishController.java
package com.sky.controller.user;

import com.sky.constant.StatusConstant;
import com.sky.entity.Dish;
import com.sky.result.Result;
import com.sky.service.DishService;
import com.sky.vo.DishVO;
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.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.util.List;

@RestController("userDishController")
@RequestMapping("/user/dish")
@Slf4j
@Api(tags = "C端-菜品浏览接口")
public class DishController {
    @Autowired
    private DishService dishService;
    @Autowired
    private RedisTemplate redisTemplate;

    /**
     * 根据分类id查询菜品
     *
     * @param categoryId
     * @return
     */
    @GetMapping("/list")
    @ApiOperation("根据分类id查询菜品")
    public Result<List<DishVO>> list(Long categoryId) {

        //构造redis中的key,规则:dish_分类id
        String key = "dish_" + categoryId;

        //查询redis中是否存在菜品数据
        List<DishVO> list = (List<DishVO>) redisTemplate.opsForValue().get(key);
        if(list != null && list.size() > 0){
            //如果存在,直接返回,无须查询数据库
            return Result.success(list);
        }

        Dish dish = new Dish();
        dish.setCategoryId(categoryId);
        dish.setStatus(StatusConstant.ENABLE);//查询起售中的菜品

        //如果不存在,查询数据库,将查询到的数据放入redis中
        list = dishService.listWithFlavor(dish);
        redisTemplate.opsForValue().set(key, list);

        return Result.success(list);
    }
}
清理缓存数据
防止 新增/更改/删除/起售停售 后无法及时在用户手机端接收

修改管理端接口 DishController 加入清理缓存的逻辑 (新增菜品、修改菜品、批量删除菜品、起售停售菜品)

sky-server  com/sky/controller/admin/DishController.java
/**
 * 菜品管理
 */
@RestController
@RequestMapping("/admin/dish")
@Api(tags = "菜品相关接口")
@Slf4j
public class DishController {
    @Autowired
    private DishService dishService;
    @Autowired
    private RedisTemplate redisTemplate;
    @PostMapping
    @ApiOperation("新增菜品")
    //@RequestBody 封装JSON格式的数据
    public Result save(@RequestBody DishDTO dishDTO) {
        log.info("新增菜品:{}", dishDTO);
        dishService.saveWithFlavour(dishDTO);

        //清理缓存数据(精确查询)
        String key = "dish_" + dishDTO.getCategoryId();
        cleanCache(key);
        return Result.success();
    }
 /**
     * 菜品的批量删除
     * @param ids
     * @return
     */
    @DeleteMapping
    @ApiOperation("批量删除菜品")
    //@RequestParam MVC动态解析字符串 ids提取出来
    public Result delete(@RequestParam List<Long> ids) { //ids
        log.info("批量删除菜品:{}", ids);
        dishService.deleteBatch(ids);

        // 将所有的菜品缓存数据清理掉,所有的以dish_开头的key
        cleanCache("dish_*");
        return Result.success();
    }
/**
     * 修改菜品
     * @param dishDTO
     * @return
     */
    @PutMapping
    @ApiOperation("修改菜品")
    public Result update(@RequestBody DishDTO dishDTO) {
        log.info("修改菜品:{}", dishDTO);
        dishService.updateWithFlavor(dishDTO);

        // 将所有的菜品缓存数据清理掉,所有的以dish_开头的key
        cleanCache("dish_*");;
        return Result.success();
    }
/**
     * 菜品起售停售
     *
     * @param status
     * @param id
     * @return
     */
    @PostMapping("/status/{status}")
    @ApiOperation("菜品起售停售")
    public Result<String> startOrStop(@PathVariable Integer status, Long id) {
        dishService.startOrStop(status, id);

        // 将所有的菜品缓存数据清理掉,所有的以dish_开头的key
        cleanCache("dish_*");

        return Result.success();
    }
private void cleanCache(String pattern){
        /** 因为单独清理每个菜品可能会有关联套餐 就直接清理全部
         * 1. 先获取到所有的key
         * 2. 遍历key,判断是否以pattern开头
         * 3. 删除所有的key
         */
        Set keys = redisTemplate.keys(pattern);
        redisTemplate.delete(keys);
    }
}

缓存套餐 【SpringCache】

Spring Cache 是一个框架,实现了基于注解的缓存功能,只需要简单地加一个注解,就能实现缓存功能
Spring Cache 提供了一层抽象,底层可以切换不同的缓存实现

  • EHCache
  • Caffeine
  • Redis
<dependency>
     <groupId>org.springframework.boot</groupId>
     <artifactId>spring-boot-starter-cache</artifactId>
</dependency>
Spring Cache
注解 说明
@EnableCaching 开启缓存注解功能,通常加在启动类
@Cacheable 在方法执行前先查询缓存中是否有数据,如果有数据,则直接返回缓存数据;如果没有缓存数据,调用方法并将方法返回值放到缓存中
@CachePut 将方法的返回值放到缓存中
@CacheEvict 将一条或多条数据从缓存中删除
@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {

     @CachePut(cacheNames = "userCache",key = "#user.id")
// 将方法的返回值放到缓存中
    // 如果使用Spring Cache缓存数据,key的生成"#user.id"
    public User save(@RequestBody User user){
        userMapper.insert(user);
        return user;
    }

    @DeleteMapping
    @CacheEvict(cacheNames = "userCache",key = "#id")
    public void deleteById(Long id){
        userMapper.deleteById(id);
    }

    @DeleteMapping("/delAll")
    @CacheEvict(cacheNames = "userCache",allEntries = true)
    public void deleteAll(){
        userMapper.deleteAll();
    }

    @GetMapping
    @Cacheable(cacheNames = "userCache",key = "#id")
// 在方法执行前先查询缓存中是否有数据,如果有数据,则直接返回缓存数据;如果没有缓存数据,调用方法并将方法返回值放到缓存中
    public User getById(Long id){
        User user = userMapper.getById(id);
        return user;
    }
SpringCache入门案例
初始资源:
package com.itheima.controller;

import com.itheima.entity.User;
import com.itheima.mapper.UserMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;

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

    @Autowired
    private UserMapper userMapper;

    @PostMapping
    public User save(@RequestBody User user){
        userMapper.insert(user);
        return user;
    }

    @DeleteMapping
    public void deleteById(Long id){
        userMapper.deleteById(id);
    }

    @DeleteMapping("/delAll")
    public void deleteAll(){
        userMapper.deleteAll();
    }

    @GetMapping
    public User getById(Long id){
        User user = userMapper.getById(id);
        return user;
    }

}
package com.itheima.mapper;

import com.itheima.entity.User;
import org.apache.ibatis.annotations.*;

@Mapper
public interface UserMapper{

    @Insert("insert into user(name,age) values (#{name},#{age})")
    @Options(useGeneratedKeys = true,keyProperty = "id")
    void insert(User user);

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

    @Delete("delete from user")
    void deleteAll();

    @Select("select * from user where id = #{id}")
    User getById(Long id);
}
package com.itheima.entity;

import lombok.Data;
import java.io.Serializable;

@Data
public class User implements Serializable {

    private static final long serialVersionUID = 1L;

    private Long id;

    private String name;

    private int age;

}
package com.itheima.config;

import lombok.extern.slf4j.Slf4j;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ResourceHandlerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurationSupport;
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;

@Configuration
@Slf4j
public class WebMvcConfiguration extends WebMvcConfigurationSupport {

    /**
     * 生成接口文档配置
     * @return
     */
    @Bean
    public Docket docket(){
        log.info("准备生成接口文档...");

        ApiInfo apiInfo = new ApiInfoBuilder()
                .title("接口文档")
                .version("2.0")
                .description("接口文档")
                .build();

        Docket docket = new Docket(DocumentationType.SWAGGER_2)
                .apiInfo(apiInfo)
                .select()
                //指定生成接口需要扫描的包
                .apis(RequestHandlerSelectors.basePackage("com.itheima.controller"))
                .paths(PathSelectors.any())
                .build();

        return docket;
    }

    /**
     * 设置静态资源映射
     * @param registry
     */
    protected void addResourceHandlers(ResourceHandlerRegistry registry) {
        log.info("开始设置静态资源映射...");
        registry.addResourceHandler("/doc.html").addResourceLocations("classpath:/META-INF/resources/");
        registry.addResourceHandler("/webjars/**").addResourceLocations("classpath:/META-INF/resources/webjars/");
    }
}
application.yml
server:
  port: 8888
spring:
  datasource:
    druid:
      driver-class-name: com.mysql.cj.jdbc.Driver
      url: jdbc:mysql://localhost:3306/spring_cache_demo?serverTimezone=Asia/Shanghai&useUnicode=true&characterEncoding=utf-8&zeroDateTimeBehavior=convertToNull&useSSL=false&allowPublicKeyRetrieval=true
      username: root
      password: root
  redis:
    host: localhost
    port: 6379
    database: 1
logging:
  level:
    com:
      itheima:
        mapper: debug
        service: info
        controller: info
springcachedemo.sql
DROP TABLE IF EXISTS `user`;
CREATE TABLE `user` (
  `id` bigint NOT NULL AUTO_INCREMENT,
  `name` varchar(45) DEFAULT NULL,
  `age` int DEFAULT NULL,
  PRIMARY KEY (`id`)
);
pom.xml
<?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 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>2.7.3</version>
        <relativePath/>
    </parent>
    <groupId>com.itheima</groupId>
    <artifactId>springcache-demo</artifactId>
    <version>1.0-SNAPSHOT</version>

    <properties>
        <maven.compiler.source>11</maven.compiler.source>
        <maven.compiler.target>11</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
            <scope>compile</scope>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.20</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>fastjson</artifactId>
            <version>1.2.76</version>
        </dependency>

        <dependency>
            <groupId>commons-lang</groupId>
            <artifactId>commons-lang</artifactId>
            <version>2.6</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>

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

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

        <dependency>
            <groupId>org.mybatis.spring.boot</groupId>
            <artifactId>mybatis-spring-boot-starter</artifactId>
            <version>2.2.0</version>
        </dependency>

        <dependency>
            <groupId>com.alibaba</groupId>
            <artifactId>druid-spring-boot-starter</artifactId>
            <version>1.2.1</version>
        </dependency>

        <dependency>
            <groupId>com.github.xiaoymin</groupId>
            <artifactId>knife4j-spring-boot-starter</artifactId>
            <version>3.0.2</version>
        </dependency>

        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
        </dependency>
    </dependencies>
    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.7.3</version>
            </plugin>
        </plugins>
    </build>
</project>


开始调试咯
com/itheima/CacheDemoApplication.java
package com.itheima;

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
//@EnableCaching 放在 Application 类上,这样整个应用就启用了缓存支持
@Slf4j
@SpringBootApplication
@EnableCaching
public class CacheDemoApplication {
    public static void main(String[] args) {
        SpringApplication.run(CacheDemoApplication.class,args);
        log.info("项目启动成功...");
    }
}
com/itheima/controller/UserController.java
package com.itheima.controller;

import com.itheima.entity.User;
import com.itheima.mapper.UserMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.cache.annotation.CacheEvict;
import org.springframework.cache.annotation.CachePut;
import org.springframework.cache.annotation.Cacheable;
import org.springframework.web.bind.annotation.*;

@RestController
@RequestMapping("/user")
@Slf4j
public class UserController {
    /*
    set a:b:c:d: itheima 这个就是树形结构在Redis里面 文件夹包着文件夹
    */
    @Autowired
    private UserMapper userMapper;

    @PostMapping
//  @CachePut(cacheNames = "userCache",key = "#result.id") 对象导航
//  @CachePut(cacheNames = "userCache",key = "#p0.id")
//  @CachePut(cacheNames = "userCache",key = "#root.args[0]")
    @CachePut(cacheNames = "userCache",key = "#user.id")
// 将方法的返回值放到缓存中
    // 如果使用Spring Cache缓存数据,key的生成"#user.id"
    public User save(@RequestBody User user){
        userMapper.insert(user);
        return user;
    }

    @DeleteMapping
    @CacheEvict(cacheNames = "userCache",key = "#id")
    public void deleteById(Long id){
        userMapper.deleteById(id);
    }

    @DeleteMapping("/delAll")
    @CacheEvict(cacheNames = "userCache",allEntries = true)
    public void deleteAll(){
        userMapper.deleteAll();
    }


    @GetMapping
    @Cacheable(cacheNames = "userCache",key = "#id")
// 在方法执行前先查询缓存中是否有数据,如果有数据,则直接返回缓存数据;如果没有缓存数据,调用方法并将方法返回值放到缓存中
    public User getById(Long id){
        User user = userMapper.getById(id);
        return user;
    }
}

缓存套餐_代码开发

实现思路
  • 导入 Spring CacheRedis 相关maven坐标
  • 启动类上加入 @EnableCaching 注解,开启缓存注解功能
  • 用户端接口 SetmealControllerlist 方法上加入 @Cacheable 注解
  • 管理端接口 SetmealControllersave、delete、update、startOrStop 等方法上
    加入**@CacheEvict** 注解保证数据一致性
com/sky/SkyApplication.java
package com.sky;

import lombok.extern.slf4j.Slf4j;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.cache.annotation.EnableCaching;
import org.springframework.transaction.annotation.EnableTransactionManagement;

@SpringBootApplication
@EnableTransactionManagement //开启注解方式的事务管理
@Slf4j
@EnableCaching //开启缓存注解
public class SkyApplication {
    public static void main(String[] args) {
        SpringApplication.run(SkyApplication.class, args);
        log.info("server started");
    }
}
sky-server  com/sky/controller/user/SetmealController.java
@RestController("userSetmealController")
@RequestMapping("/user/setmeal")
@Api(tags = "C端-套餐浏览接口")
public class SetmealController {
    @Autowired
    private SetmealService setmealService;

    /**
     * 条件查询
     *
     * @param categoryId
     * @return
     */
    @GetMapping("/list")
    @ApiOperation("根据分类id查询套餐")
    @Cacheable(cacheNames = "setmealCache",key = "#categoryId") //key: setmealCache::100
    public Result<List<Setmeal>> list(Long categoryId) {
        Setmeal setmeal = new Setmeal();
        setmeal.setCategoryId(categoryId);
        setmeal.setStatus(StatusConstant.ENABLE);

        List<Setmeal> list = setmealService.list(setmeal);
        return Result.success(list);
    }
}
sky-server  com/sky/controller/admin/SetmealController.java

/**
 * 套餐管理
 */
@RestController
@RequestMapping("/admin/setmeal")
@Api(tags = "套餐相关接口")
@Slf4j
public class SetmealController {

    @Autowired
    private SetmealService setmealService;

    /**
     * 新增套餐
     *
     * @param setmealDTO
     * @return
     */
    @PostMapping
    @ApiOperation("新增套餐")
    @CacheEvict(cacheNames = "setmealCache",key = "#setmealDTO.categoryId")//key: setmealCache::100
    public Result save(@RequestBody SetmealDTO setmealDTO) {
        setmealService.saveWithDish(setmealDTO);
        return Result.success();
    }
 /**
     * 批量删除套餐
     *
     * @param ids
     * @return
     */
    @DeleteMapping
    @ApiOperation("批量删除套餐")
    @CacheEvict(cacheNames = "setmealCache",allEntries = true)
    public Result delete(@RequestParam List<Long> ids) {
        setmealService.deleteBatch(ids);
        return Result.success();
    }
/**
     * 修改套餐
     *
     * @param setmealDTO
     * @return
     */
    @PutMapping
    @ApiOperation("修改套餐")
    @CacheEvict(cacheNames = "setmealCache",allEntries = true)
    public Result update(@RequestBody SetmealDTO setmealDTO) {
        setmealService.update(setmealDTO);
        return Result.success();
    }

    /**
     * 套餐起售停售
     *
     * @param status
     * @param id
     * @return
     */
    @PostMapping("/status/{status}")
    @ApiOperation("套餐起售停售")
    @CacheEvict(cacheNames = "setmealCache",allEntries = true)
    public Result startOrStop(@PathVariable Integer status, Long id) {
        setmealService.startOrStop(status, id);
        return Result.success();
    }
}

添加购物车

套餐直接点击加号
菜品+ 或者有口味数据的选择后才可以加入购物车

接口设计:
  • 请求方式:POST
  • 请求路径:/user/shoppingCart/add
  • 请求参数:菜品id(dish_id)、口味(dish_flavor) 或 套餐id(setmeal_id) (JSON请求体)
  • 返回结果:code、data、msg
数据库设计(shopping_cart表):设置冗余字段可提高数据库效率
  • 作用:暂时存放所选商品的地方
  • 选的什么商品
  • 每个商品都买了几个
  • 不同用户的购物车需要区分开
sky-pojo  com/sky/dto/ShoppingCartDTO.java
package com.sky.dto;

import lombok.Data;
import java.io.Serializable;

@Data
public class ShoppingCartDTO implements Serializable {

    private Long dishId;
    private Long setmealId;
    private String dishFlavor;
}
sky-server  com/sky/controller/user/ShoppingCartController.java
package com.sky.controller.user;

import com.sky.dto.ShoppingCartDTO;
import com.sky.result.Result;
import com.sky.service.ShoppingCartService;
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.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController
@RequestMapping("/user/shoppingCart")
@Slf4j
@Api(tags = "C端添加购物车接口")
public class ShoppingCartController {
    @Autowired
    private ShoppingCartService shoppingCartService;
    /**
     * 添加购物车
     * @param shoppingCartDTO
     * @return
     */
    @PostMapping("/add")
    @ApiOperation("添加购物车")
    public Result add(@RequestBody ShoppingCartDTO shoppingCartDTO){
        log.info("添加购物车,商品信息为:{}",shoppingCartDTO);
        shoppingCartService.addShoppingCart(shoppingCartDTO);
        return Result.success();
    }
}
sky-server  com/sky/service/ShoppingCartService.java
package com.sky.service;

import com.sky.dto.ShoppingCartDTO;
import org.springframework.stereotype.Service;

public interface ShoppingCartService {
    /**
     * 添加购物车
     * @param shoppingCartDTO
     */
    void addShoppingCart(ShoppingCartDTO shoppingCartDTO);
}
sky-server  com/sky/service/impl/ShoppingCartServiceImpl.java
package com.sky.service.impl;

import com.sky.context.BaseContext;
import com.sky.dto.ShoppingCartDTO;
import com.sky.entity.Dish;
import com.sky.entity.Setmeal;
import com.sky.entity.ShoppingCart;
import com.sky.mapper.DishMapper;
import com.sky.mapper.SetmealMapper;
import com.sky.mapper.ShoppingCartMapper;
import com.sky.service.ShoppingCartService;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.List;
@Service
@Slf4j
public class ShoppingCartServiceImpl implements ShoppingCartService {
    /**
     * 添加购物车
     * @param shoppingCartDTO
     */
    @Autowired
    private ShoppingCartMapper shoppingCartMapper;
    @Autowired
    private DishMapper dishMapper;
    @Autowired
    private SetmealMapper setmealMapper;
    @Override
    public void addShoppingCart(ShoppingCartDTO shoppingCartDTO) {
        // 判断当前加入购物车中的商品是否已经存在了 (user_id + setmeal_id)
        ShoppingCart shoppingCart = new ShoppingCart();
        BeanUtils.copyProperties(shoppingCartDTO, shoppingCart);
        Long userId = BaseContext.getCurrentId();
        shoppingCart.setUserId(userId);

        List<ShoppingCart> list = shoppingCartMapper.list(shoppingCart);

        // 如果已经存在了,只需要将数量+1
        if (list != null && list.size() > 0) {
            ShoppingCart cart = list.get(0);
            cart.setNumber(cart.getNumber() + 1);
            // update shopping_cart set number = ? where id = ?
            shoppingCartMapper.updateNumberById(cart);
        } else {
            // 如果不存在,需要插入一条购物车数据
            // [先确定套餐or菜品]
            // 判断本次添加到购物车的是菜品还是套餐
            Long dishId = shoppingCartDTO.getDishId();
            if (dishId != null) {
                //本次添加到购物车的是菜品
                Dish dish = dishMapper.getById(dishId);
                shoppingCart.setName(dish.getName());
                shoppingCart.setImage(dish.getImage());
                shoppingCart.setAmount(dish.getPrice());
            } else {
                //本次添加到购物车的是套餐 查菜品表
                Long setmealId = shoppingCartDTO.getSetmealId();
                Setmeal setmeal = setmealMapper.getById(setmealId);
                shoppingCart.setName(setmeal.getName());
                shoppingCart.setImage(setmeal.getImage());
                shoppingCart.setAmount(setmeal.getPrice());

            }
            shoppingCart.setNumber(1);
            shoppingCart.setCreateTime(LocalDateTime.now());
            shoppingCartMapper.insert(shoppingCart);
        }
    }
}
sky-server  com/sky/mapper/ShoppingCartMapper.java
package com.sky.mapper;

import com.sky.entity.ShoppingCart;
import org.apache.ibatis.annotations.Insert;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Update;

import java.util.List;

@Mapper
public interface ShoppingCartMapper {
    List<ShoppingCart> list(ShoppingCart shoppingCart);

    /**
     * 根据id修改商品数量
     * @param shoppingCart
     */
    @Update("update shopping_cart set number = #{number} where id = #{id}")
    void updateNumberById(ShoppingCart shoppingCart);

    /**
     * 插入购物车数据
     * @param shoppingCart
     */
    @Insert("insert into shopping_cart (name, user_id, dish_id, setmeal_id, dish_flavor, number, amount, image, create_time) " +
            " values (#{name},#{userId},#{dishId},#{setmealId},#{dishFlavor},#{number},#{amount},#{image},#{createTime})")
    void insert(ShoppingCart shoppingCart);
}
sky-server  mapper/ShoppingCartMapper.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.ShoppingCartMapper">

    <select id="list" resultType="com.sky.entity.ShoppingCart">
        select * from shopping_cart
        <where>
            <if test="userId != null">
                and user_id = #{userId}
            </if>
            <if test="dishId != null">
                and dish_id = #{dishId}
            </if>
            <if test="setmealId != null">
                and setmeal_id = #{setmealId}
            </if>
            <if test="dishFlavor != null">
                and dish_flavor = #{dishFlavor}
            </if>
        </where>
    </select>
</mapper>

删除购物车

sky-server  com/sky/controller/user/ShoppingCartController.java
/**
     * 删除购物车
     * @param shoppingCartDTO
     * @return
     */
    @PostMapping("/sub")
    @ApiOperation("删除购物车")
    public Result sub(@RequestBody ShoppingCartDTO shoppingCartDTO){
        log.info("删除购物车,商品信息为:{}",shoppingCartDTO);
        shoppingCartService.subShoppingCart(shoppingCartDTO);
        return Result.success();
    }
sky-server  com/sky/service/ShoppingCartService.java
    /**
     * 删除购物车
     * @param shoppingCartDTO
     */
    void subShoppingCart(ShoppingCartDTO shoppingCartDTO);
sky-server  com/sky/service/impl/ShoppingCartServiceImpl.java
/**
     * 删除购物车中的商品
     * @param shoppingCartDTO
     */
    @Override
    public void subShoppingCart(ShoppingCartDTO shoppingCartDTO) {
        Long userId = BaseContext.getCurrentId();
        ShoppingCart shoppingCart = new ShoppingCart();
        BeanUtils.copyProperties(shoppingCartDTO, shoppingCart);
        shoppingCart.setUserId(userId);

        List<ShoppingCart> list = shoppingCartMapper.list(shoppingCart);
        if (list != null && list.size() > 0) {
            ShoppingCart cart = list.get(0);
            if (cart.getNumber() > 1) {
                // 如果 number >1, 则需要将 number - 1
                cart.setNumber(cart.getNumber() - 1);
                shoppingCartMapper.updateNumberById(cart);
            } else {
                // 如果 number <=1,则直接删除该购物车数据
                shoppingCartMapper.deleteById(cart.getId());
            }
        }
    }
sky-server  com/sky/mapper/ShoppingCartMapper.java
/**
     * 根据id删除购物车数据
     * @param id
     */
    @Delete("delete from shopping_cart where id = #{id}")
    void deleteById(Long id);

查看购物车

名称、价格、商品、数量
Path:/user/shoppingCart/list
PUT

sky-server  com/sky/controller/user/ShoppingCartController.java
/**
     * 查看购物车
     * @return
     */
    @GetMapping("/list")
    @ApiOperation("查看购物车")
    public Result<List<ShoppingCart>> list(){
       List<ShoppingCart> list = shoppingCartService.showShoppingCart();
       return Result.success(list);
    }
sky-server  com/sky/service/ShoppingCartService.java
    /**
     * 查看购物车
     * @return
     */
    List<ShoppingCart> showShoppingCart();
sky-server  com/sky/service/impl/ShoppingCartServiceImpl.java
/**
     * 查看购物车
     * @return
     */
    @Override
    public List<ShoppingCart> showShoppingCart() {
        // 获取到当前微信用户的id
        Long userId = BaseContext.getCurrentId();
        ShoppingCart shoppingCart = ShoppingCart.builder()
                .userId(userId)
                .build();
        List<ShoppingCart> list = shoppingCartMapper.list(shoppingCart);
        return list;
    }

清空购物车

Path:user/shoppingCart/clean
Method:DELETE
返回:code date msg

sky-server  com/sky/controller/user/ShoppingCartController.java
/**
     * 清空购物车
     * @return
     */
    @DeleteMapping("/clean")
    @ApiOperation("清空购物车")
    public Result clean() {
        shoppingCartService.cleanShoppingCart();
        return Result.success();
    }
sky-server  com/sky/service/ShoppingCartService.java
/**
     * 清空购物车 删除自己的购物车
     */
    void cleanShoppingCart();
sky-server  com/sky/service/impl/ShoppingCartServiceImpl.java
/**
     * 清空购物车
     */
    @Override
    public void cleanShoppingCart() {
        //获取到当前用户的id
        Long userId = BaseContext.getCurrentId();
        shoppingCartMapper.deleteByUserId(userId);
    }
sky-server  com/sky/mapper/ShoppingCartMapper.java
/**
     * 根据用户id清空购物车
     * @param userId
     */
    @Delete("delete from shopping_cart where user_id = #{userId}")
    void deleteByUserId(Long userId);

支付接口

导入地址簿功能代码

业务功能:

  • 查询地址列表

  • 新增地址

  • 修改地址

    Path:/user/addressBook
    Method:PUT

  • 删除地址

  • 设置默认地址

  • 查询默认地址

接口设计:

  • 新增地址

    Path: /user/addressBook
    Method: POST

  • 查询当前登录用户的所有地址信息

    Path: /user/addressBook/list
    Method: GET

  • 查询默认地址

  • 根据id修改地址

  • 根据id删除地址

    Path:/user/addressBook
    Method:DELETE

  • 根据id查询地址

    Path:/user/addressBook/{id}
    Method:GET

  • 设置默认地址

    Path:/user/addressBook/default
    Method:PUT

数据库设计(address_book表)

用户下单

在电商系统中,用户是通过下单的方式通知商家,用户已经购买了商品,需要商家进行备货和发货

用户下单后会产生订单相关数据,订单数据需要体现信息:

  • 订单总金额是多少
  • 哪个用户下的单
  • 买的哪些商品
  • 每个商品数量是多少
  • 收货地址是哪
  • 用户手机号是多少

餐盒费:用数量算

用户下单接口设计

请求方式:POST
请求路径:/user/order/submit

参数:

  • 地址簿idaddressBookId
  • 配送状态(立即送出、选择送出时间)deliveryStatus
  • 打包费packAmount
  • 总金额amount
  • 备注remark
  • 餐具数量tablewareNumber
支付订单接口设计

返回数据:

  • 下单时间
  • 订单总金额
  • 订单号
  • 订单id
数据库设计订单表orders、订单明细表order_detail
  • 订单表 orders
    • 谁的订单?
    • 送哪去?
    • 打哪个电话联系?
    • 多少钱?
    • 什么时间下的单?
    • 什么时间支付的?
    • 订单的状态?
    • 订单号是多少?
  • 订单明细表 order_detail
    • 当前明细属于哪个订单?
    • 具体点的是什么商品?
    • 这个商品点了几份?

代码开发

用户下单1
根据用户下单接口的参数设计DTO:
sky-pojo  com/sky/dto/OrdersSubmitDTO.java
package com.sky.dto;

import com.fasterxml.jackson.annotation.JsonFormat;
import lombok.Data;

import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;

@Data
public class OrdersSubmitDTO implements Serializable {
    //地址簿id
    private Long addressBookId;
    //付款方式
    private int payMethod;
    //备注
    private String remark;
    //预计送达时间
    @JsonFormat(shape = JsonFormat.Shape.STRING, pattern = "yyyy-MM-dd HH:mm:ss")
    private LocalDateTime estimatedDeliveryTime;
    //配送状态  1立即送出  0选择具体时间
    private Integer deliveryStatus;
    //餐具数量
    private Integer tablewareNumber;
    //餐具数量状态  1按餐量提供  0选择具体数量
    private Integer tablewareStatus;
    //打包费
    private Integer packAmount;
    //总金额
    private BigDecimal amount;
}
sky-pojo  com/sky/vo/OrdersSubmitVO.java
package com.sky.vo;

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

import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OrderSubmitVO implements Serializable {
    //订单id
    private Long id;
    //订单号
    private String orderNumber;
    //订单金额
    private BigDecimal orderAmount;
    //下单时间
    private LocalDateTime orderTime;
}
sky-server  com/sky/controller/user/OrderController.java
package com.sky.controller.user;

import com.sky.dto.OrdersSubmitDTO;
import com.sky.result.Result;
import com.sky.service.OrderService;
import com.sky.vo.OrderSubmitVO;
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.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

@RestController("userOrderController")
@RequestMapping("/user/order")
@Api(tags = "用户订单相关接口")
@Slf4j
public class OrderController {
    @Autowired
    private OrderService orderService;
    /**
     * 用户下单
     * @param ordersSubmitDTO
     * @return
     */
    @PostMapping("/submit")
    @ApiOperation("用户下单")
    public Result<OrderSubmitVO> submit(@RequestBody OrdersSubmitDTO ordersSubmitDTO) {
        log.info("用户下单,参数为:{}", ordersSubmitDTO);
        OrderSubmitVO orderSubmitVO = orderService.submitOrder(ordersSubmitDTO);
        return Result.success(orderSubmitVO);
    }
}
sky-server  com/sky/service/OrderService.java
package com.sky.vo;

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

import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDateTime;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OrderSubmitVO implements Serializable {
    //订单id
    private Long id;
    //订单号
    private String orderNumber;
    //订单金额
    private BigDecimal orderAmount;
    //下单时间
    private LocalDateTime orderTime;
}
用户下单2
sky-pojo  com/sky/entity/AddressBook.java
package com.sky.entity;

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

import java.io.Serializable;

/**
 * 地址簿
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class AddressBook implements Serializable {

    private static final long serialVersionUID = 1L;

    private Long id;

    //用户id
    private Long userId;

    //收货人
    private String consignee;

    //手机号
    private String phone;

    //性别 0 女 1 男
    private String sex;

    //省级区划编号
    private String provinceCode;

    //省级名称
    private String provinceName;

    //市级区划编号
    private String cityCode;

    //市级名称
    private String cityName;

    //区级区划编号
    private String districtCode;

    //区级名称
    private String districtName;

    //详细地址
    private String detail;

    //标签
    private String label;

    //是否默认 0否 1是
    private Integer isDefault;
}
sky-server  com/sky/service/impl/OrderServiceImpl.java
package com.sky.service.impl;

import com.sky.constant.MessageConstant;
import com.sky.context.BaseContext;
import com.sky.dto.OrdersSubmitDTO;
import com.sky.entity.AddressBook;
import com.sky.entity.Orders;
import com.sky.entity.ShoppingCart;
import com.sky.exception.AddressBookBusinessException;
import com.sky.exception.ShoppingCartBusinessException;
import com.sky.mapper.AddressBookMapper;
import com.sky.mapper.OrderDetailMapper;
import com.sky.mapper.OrderMapper;
import com.sky.mapper.ShoppingCartMapper;
import com.sky.service.OrderService;
import com.sky.vo.OrderSubmitVO;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.time.LocalDateTime;
import java.util.List;

@Service
public class OrderServiceImpl implements OrderService {
    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private OrderDetailMapper orderDetailMapper;
    @Autowired
    private AddressBookMapper addressBookMapper;
    @Autowired
    private ShoppingCartMapper shoppingCartMapper;
    /**
     * 用户下单
     * @param ordersSubmitDTO
     * @return
     */
    @Override
    public OrderSubmitVO submitOrder(OrdersSubmitDTO ordersSubmitDTO) {

        // 1.处理各种业务异常(地址簿为空,购物车数据为空)
        AddressBook addressBook = addressBookMapper.getById(ordersSubmitDTO.getAddressBookId());
        if (addressBook == null) {
            // 抛出业务异常
            throw new AddressBookBusinessException(MessageConstant.ADDRESS_BOOK_IS_NULL);
        }
        // 查询当前用户购物车信息
        Long userId = BaseContext.getCurrentId();
        ShoppingCart shoppingCart = new ShoppingCart();
        shoppingCart.setUserId(userId);
        List<ShoppingCart> shoppingCartList = shoppingCartMapper.list(shoppingCart);
        if (shoppingCartList == null || shoppingCartList.size() == 0) {
            // 抛出业务异常
            throw new ShoppingCartBusinessException(MessageConstant.SHOPPING_CART_IS_NULL);
        }

        // 2.向订单表插入1条数据
        Orders orders = new Orders();
        BeanUtils.copyProperties(ordersSubmitDTO, orders);
        orders.setOrderTime(LocalDateTime.now());
        orders.setPayStatus(Orders.UN_PAID);
        orders.setStatus(Orders.PENDING_PAYMENT);
        orders.setNumber(String.valueOf(System.currentTimeMillis()));//订单号
        orders.setPhone(addressBook.getPhone());
        orders.setConsignee(addressBook.getConsignee());
        orders.setUserId(userId);
        orderMapper.insert(orders);

        // 3.向订单明细表插入n条数据

        // 4,清空当前用户的购物车数据

        // 5.封装VO返回结果
        return null;
    }
}
sky-server  com/sky/mapper/OrderMapper.java
package com.sky.mapper;

import com.sky.entity.Orders;
import org.apache.ibatis.annotations.Mapper;

@Mapper
public interface OrderMapper {
    /**
     * 用户下单
     * @param orders
     */
    void insert(Orders orders);
}
代码开发3
sky-server  com/sky/service/impl/OrderServiceImpl.java
package com.sky.service.impl;

import com.sky.constant.MessageConstant;
import com.sky.context.BaseContext;
import com.sky.dto.OrdersSubmitDTO;
import com.sky.entity.AddressBook;
import com.sky.entity.OrderDetail;
import com.sky.entity.Orders;
import com.sky.entity.ShoppingCart;
import com.sky.exception.AddressBookBusinessException;
import com.sky.exception.ShoppingCartBusinessException;
import com.sky.mapper.AddressBookMapper;
import com.sky.mapper.OrderDetailMapper;
import com.sky.mapper.OrderMapper;
import com.sky.mapper.ShoppingCartMapper;
import com.sky.service.OrderService;
import com.sky.vo.OrderSubmitVO;
import org.springframework.beans.BeanUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import java.time.LocalDateTime;
import java.util.ArrayList;
import java.util.List;

@Service
public class OrderServiceImpl implements OrderService {
    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private OrderDetailMapper orderDetailMapper;
    @Autowired
    private AddressBookMapper addressBookMapper;
    @Autowired
    private ShoppingCartMapper shoppingCartMapper;
    /**
     * 用户下单
     * @param ordersSubmitDTO
     * @return
     */
    @Transactional
    public OrderSubmitVO submitOrder(OrdersSubmitDTO ordersSubmitDTO) {

        // 1.处理各种业务异常(地址簿为空,购物车数据为空)
        AddressBook addressBook = addressBookMapper.getById(ordersSubmitDTO.getAddressBookId());
        if (addressBook == null) {
            // 抛出业务异常
            throw new AddressBookBusinessException(MessageConstant.ADDRESS_BOOK_IS_NULL);
        }
        // 查询当前用户购物车信息
        Long userId = BaseContext.getCurrentId();
        ShoppingCart shoppingCart = new ShoppingCart();
        shoppingCart.setUserId(userId);
        List<ShoppingCart> shoppingCartList = shoppingCartMapper.list(shoppingCart);
        if (shoppingCartList == null || shoppingCartList.size() == 0) {
            // 抛出业务异常
            throw new ShoppingCartBusinessException(MessageConstant.SHOPPING_CART_IS_NULL);
        }

        // 2.向订单表插入1条数据
        Orders orders = new Orders();
        BeanUtils.copyProperties(ordersSubmitDTO, orders);
        orders.setOrderTime(LocalDateTime.now());
        orders.setPayStatus(Orders.UN_PAID);
        orders.setStatus(Orders.PENDING_PAYMENT);
        orders.setNumber(String.valueOf(System.currentTimeMillis()));//订单号
        orders.setPhone(addressBook.getPhone());
        orders.setConsignee(addressBook.getConsignee());
        orders.setUserId(userId);
        orderMapper.insert(orders);

        //批量插入订单明细数据
        List<OrderDetail> orderDetailList = new ArrayList<>();
        // 3.向订单明细表插入n条数据
        for (ShoppingCart cart : shoppingCartList) {
            OrderDetail orderDetail = new OrderDetail(); //订单明细
            BeanUtils.copyProperties(cart, orderDetail);
            orderDetail.setOrderId(orders.getId()); //设置当前订单明细关联的订单id
            orderDetailList.add(orderDetail);
        }
        orderDetailMapper.insertBatch(orderDetailList);
        // 4.清空当前用户的购物车数据
        shoppingCartMapper.deleteByUserId(userId);
        // 5.封装VO返回结果
        OrderSubmitVO ordersubmitVO = OrderSubmitVO.builder()
                .id(orders.getId())
                .orderNumber(orders.getNumber())
                .orderAmount(orders.getAmount())
                .orderTime(orders.getOrderTime())
                .build();
        return ordersubmitVO;
    }
}
sky-server  com/sky/mapper/OrderDetailMapper.java
package com.sky.mapper;

import com.sky.entity.OrderDetail;
import org.apache.ibatis.annotations.Mapper;

import java.util.List;

@Mapper
public interface OrderDetailMapper {
    /**
     * 批量插入订单明细数据
     */
    void insertBatch(List<OrderDetail> orderDetailList);
}
sky-server  mapper/OrderDetailMapper.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.OrderDetailMapper">

    <insert id="insertBatch">
        insert into order_detail (name, image, order_id, dish_id, setmeal_id, dish_flavor, number, amount)
        values
        <foreach collection="orderDetailList" item="od" separator=",">
            (#{od.name},#{od.image},#{od.orderId},#{od.dishId},#{od.setmealId},#{od.dishFlavor},#{od.number},#{od.amount})
        </foreach>
    </insert>
</mapper>

订单支付

微信支付产品 + 微信支付

参考:https://pay.weixin.qq.com/static/product/product_index.shtml

微信支付接入流程:

提交资料 → 签署协议 → 绑定场景

微信小程序支付时序图:

JSAPI下单:商户系统调用该接口在微信支付服务后台生成预支付交易单

请求URLhttps://api.mch.weixin.qq.com/v3/pay/transactions/jsapi

获取微信支付平台证书、商户私钥文件:

内网穿透工具

E:\Java实例项目1-20套\第24套【项目实战】Java外卖项目实战《苍穹外卖》SpringBoot+SpringMVC+Vue+Swagger+Lombok+Mybatis+SpringSession+Redis+Nginx+小程序\0-0 源码资料\资料\day08\安装包cpolar_amd64.msi

cpolar - secure introspectable tunnels to localhost
验证
你的隧道
[复制token] → 在cpolar文件里/cmd[C:\Program Files\cpolar] → cpolar.exe authtoken xxxx[Authtoken:这个是在网站验证里复制的] → cpolar.exe http 8080

cpolar by @bestexpresser (Ctrl+C to quit)

Tunnel Status online
Account Pluminary (Plan: Free)
Version 2.86.16/3.18
Web Interface 127.0.0.1:4042
Forwarding http://22d34b67.r9.cpolar.top -> http://localhost:8080
Forwarding https://22d34b67.r9.cpolar.top -> http://localhost:8080

Conn 0

Avg Conn Time 0.00ms

启动穿透地址:[22d34b67.r9.cpolar.top/doc.html] (https://22d34b67.r9.cpolar.top/doc.html)

此时正在下载资源
HTTP Requests


GET /v2/api-docs 200
GET /swagger-resources 200
GET /webjars/js/chunk-3b888a6 200
GET /webjars/js/chunk-589faee 200
GET /webjars/js/chunk-2d0bd79 200
GET /webjars/js/chunk-0fd6771 200
GET /webjars/js/chunk-0c58d94 200
GET /webjars/css/chunk-62d2fe 200
GET /webjars/js/app.0f2f48b5. 200

随后就可以访问到接口文档了!!
原理:使用内网穿透工具临时获得一个域名

CPolarSwitchHosts 是两种不同类型的软件,它们的功能和用途有所区别。

CPolar 是一款内网穿透软件,主要用于将本地运行的服务暴露到公网上,使得外网可以访问。它通过在本地和公网服务器之间建立一个安全的隧道,使得用户可以在任何地方通过互联网访问到本地的服务,比如网站、SSH、数据库等。

SwitchHosts 则是一款用于管理和切换本地hosts文件的软件。Hosts文件是操作系统用于将一些域名解析到特定的IP地址的一个文本文件。SwitchHosts 允许用户方便地添加、切换、备份不同的hosts规则,对于开发者来说,这在开发过程中进行域名映射和测试非常有用。

总结来说,CPolar主要用于内网穿透,而SwitchHosts用于hosts文件管理。两者解决的问题和适用场景不同,不是同一种软件。


内网、公网、外网和CPolar这几个概念在网络通信中扮演着不同的角色,以下是它们的定义和它们之间的联系:

  1. 内网(Local Network 或 Intranet): 内网是指一个私有网络,通常是在家庭、办公室或企业内部使用。内网中的设备通常通过路由器连接,并使用私有IP地址(如192.168.x.x或10.x.x.x)。内网中的设备一般不能直接从外部互联网访问,它们之间的通信受到防火墙和NAT(网络地址转换)的保护。
  2. 公网(Public Network 或 Internet): 公网是指全球范围内的开放网络,即互联网。公网上的设备使用公网IP地址,这些地址是全球唯一的,可以通过互联网被其他设备访问。网站、电子邮件服务器和其他在线服务都部署在公网上。
  3. 外网(External Network): 外网通常是指相对于内网而言的任何外部网络,特别是指互联网。当说“外网”时,通常是指从内网之外访问的资源或服务。
  4. CPolar: CPolar是一款内网穿透工具,它的主要作用是帮助内网中的设备暴露服务到公网上,使得这些服务可以被外网访问。以下是CPolar与内网、公网、外网之间的联系:
  • 内网到公网:CPolar在本地设备上运行一个客户端,该客户端与CPolar的服务器建立连接。当外部网络(公网)尝试访问CPolar服务器上配置的特定端口时,CPolar服务器会将这些请求转发到运行CPolar客户端的内网设备上。
  • 公网访问:通过CPolar,内网中的服务可以被赋予一个公网可访问的地址(通常是CPolar服务器的一个子域名或自定义域名),这样外网的任何用户都可以通过这个地址访问到内网的服务。

简而言之,CPolar是实现内网服务与公网之间通信的桥梁,它使得原本只能在局域网内部访问的服务能够被外网的用户访问。这对于远程工作、调试、以及需要在公网上提供服务的内网应用来说非常有用

导入功能代码【由于没有微信凭证 此接口未能正常开发 但代码均可学习】
sky-server  application-dev.yml
 wechat:
    appid: wxffb3637a228223b8
    secret: 84311df9199ecacdf4f12d27b6b9522d
    mchid : 1561414331
    mchSerialNo: 4B3B3DC35414AD50B1B755BAF8DE9CC7CF407606
    privateKeyFilePath: D:\pay\apiclient_key.pem
    apiV3Key: CZBK51236435wxpay435434323FFDuv3
    weChatPayCertFilePath: D:\pay\wechatpay_166D96F876F45C7D07CE98952A96EC980368ACFC.pem
    notifyUrl: https://58869fb.r2.cpolar.top/notify/paySuccess
    refundNotifyUrl: https://58869fb.r2.cpolar.top/notify/refundSuccess
sky-server  com/sky/controller/user/OrderController.java
/**
     * 订单支付
     *
     * @param ordersPaymentDTO
     * @return
     */
    @PutMapping("/payment")
    @ApiOperation("订单支付")
    public Result<OrderPaymentVO> payment(@RequestBody OrdersPaymentDTO ordersPaymentDTO) throws Exception {
        log.info("订单支付:{}", ordersPaymentDTO);
        OrderPaymentVO orderPaymentVO = orderService.payment(ordersPaymentDTO);
        log.info("生成预支付交易单:{}", orderPaymentVO);
        return Result.success(orderPaymentVO);
    }
sky-server  com/sky/service/OrderService.java
/**
     * 订单支付
     * @param ordersPaymentDTO
     * @return
     */
    OrderPaymentVO payment(OrdersPaymentDTO ordersPaymentDTO) throws Exception;

    /**
     * 支付成功,修改订单状态
     * @param outTradeNo
     */
    void paySuccess(String outTradeNo);
sky-server  com/sky/service/impl/OrderServiceImpl.java
 @Autowired
    private UserMapper userMapper;
 @Autowired
    private WeChatPayUtil weChatPayUtil;

    
    /**
     * 订单支付
     *
     * @param ordersPaymentDTO
     * @return
     */
    public OrderPaymentVO payment(OrdersPaymentDTO ordersPaymentDTO) throws Exception {
        // 查询订单
        Orders order = orderMapper.getByOrderNumber(ordersPaymentDTO.getOrderNumber());
        if (order == null) {
            throw new OrderBusinessException("订单不存在");
        }

        // 检查订单支付状态
        if (order.getPayStatus() == 1) { // 1 表示已支付
            throw new OrderBusinessException("该订单已支付");
        }
        order.setPayStatus(1);

        // 更新订单支付状态为已支付
        order.setPayStatus(Orders.PAID);
        order.setCheckoutTime(LocalDateTime.now());
        order.setPayMethod(ordersPaymentDTO.getPayMethod());
        order.setStatus(Orders.CONFIRMED);

        orderMapper.update(order);

        // 构造并返回支付结果对象
        OrderPaymentVO orderPaymentVO = new OrderPaymentVO();
        orderPaymentVO.setOrderNumber(order.getNumber()); // 订单号
        orderPaymentVO.setPaymentTime(new Date());
        orderPaymentVO.setPaymentStatus("SUCCESS");

        return orderPaymentVO;
    }
    /**
     * 支付成功,修改订单状态
     *
     * @param outTradeNo
     */
    public void paySuccess(String outTradeNo) {
        // 当前登录用户id
        Long userId = BaseContext.getCurrentId();

        // 根据订单号查询当前用户的订单
        Orders ordersDB = orderMapper.getByNumberAndUserId(outTradeNo, userId);

        // 根据订单id更新订单的状态、支付方式、支付状态、结账时间
        Orders orders = Orders.builder()
                .id(ordersDB.getId())
                .status(Orders.TO_BE_CONFIRMED)
                .payStatus(Orders.PAID)
                .checkoutTime(LocalDateTime.now())
                .build();

        orderMapper.update(orders);
    }
}
sky-server  com/sky/mapper/OrderMapper.java
package com.sky.mapper;

import com.github.pagehelper.Page;
import com.sky.dto.GoodsSalesDTO;
import com.sky.dto.OrdersPageQueryDTO;
import com.sky.entity.Orders;
import org.apache.ibatis.annotations.Mapper;
import org.apache.ibatis.annotations.Select;
import java.time.LocalDateTime;
import java.util.List;
import java.util.Map;

@Mapper
public interface OrderMapper {
    /**
     * 插入订单数据
     * @param order
     */
    void insert(Orders order);

    /**
     * 根据订单号和用户id查询订单
     * @param orderNumber
     * @param userId
     */
    @Select("select * from orders where number = #{orderNumber} and user_id= #{userId}")
    Orders getByNumberAndUserId(String orderNumber, Long userId);

    /**
     * 修改订单信息
     * @param orders
     */
    void update(Orders orders);
    /**
     * 根据id查询订单
     * @param id
     */
    @Select("select * from orders where id = #{id}}")
    Orders getById(Long id);
}
resources/mapper/OrderMapper.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.OrderMapper">

    <insert id="insert" parameterType="Orders" useGeneratedKeys="true" keyProperty="id">
        insert into orders
        (number, status, user_id, address_book_id, order_time, checkout_time, pay_method, pay_status, amount, remark,
         phone, address, consignee, estimated_delivery_time, delivery_status, pack_amount, tableware_number,
         tableware_status)
        values (#{number}, #{status}, #{userId}, #{addressBookId}, #{orderTime}, #{checkoutTime}, #{payMethod},
                #{payStatus}, #{amount}, #{remark}, #{phone}, #{address}, #{consignee},
                #{estimatedDeliveryTime}, #{deliveryStatus}, #{packAmount}, #{tablewareNumber}, #{tablewareStatus})
    </insert>

    <update id="update" parameterType="com.sky.entity.Orders">
        update orders
        <set>
            <if test="cancelReason != null and cancelReason!='' ">
                cancel_reason=#{cancelReason},
            </if>
            <if test="rejectionReason != null and rejectionReason!='' ">
                rejection_reason=#{rejectionReason},
            </if>
            <if test="cancelTime != null">
                cancel_time=#{cancelTime},
            </if>
            <if test="payStatus != null">
                pay_status=#{payStatus},
            </if>
            <if test="payMethod != null">
                pay_method=#{payMethod},
            </if>
            <if test="checkoutTime != null">
                checkout_time=#{checkoutTime},
            </if>
            <if test="status != null">
                status = #{status},
            </if>
            <if test="deliveryTime != null">
                delivery_time = #{deliveryTime}
            </if>
        </set>
        where id = #{id}
    </update>
        
        <!-- 根据订单号查询订单 -->
    <select id="getByOrderNumber" parameterType="String" resultType="Orders">
        select * from orders where number = #{orderNumber}
    </select>
</mapper>
sky-server  com/sky/mapper/UserMapper.java
  @Select("select * from user where id = #{id}}")
    User getById(Long userId);
sky-server  com/sky/controller/notify/PayNotifyController.java
package com.sky.controller.notify;

import com.alibaba.druid.support.json.JSONUtils;
import com.alibaba.fastjson.JSON;
import com.alibaba.fastjson.JSONObject;
//import com.sky.annotation.IgnoreToken;
import com.sky.properties.WeChatProperties;
import com.sky.service.OrderService;
import com.wechat.pay.contrib.apache.httpclient.util.AesUtil;
import lombok.extern.slf4j.Slf4j;
import org.apache.http.entity.ContentType;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.BufferedReader;
import java.nio.charset.StandardCharsets;
import java.util.HashMap;

/**
 * 支付回调相关接口
 */
@RestController
@RequestMapping("/notify")
@Slf4j
public class PayNotifyController {
    @Autowired
    private OrderService orderService;
    @Autowired
    private WeChatProperties weChatProperties;

    /**
     * 支付成功回调
     *
     * @param request
     */
//  @IgnoreToken
    @RequestMapping("/paySuccess")
    public void paySuccessNotify(HttpServletRequest request, HttpServletResponse response) throws Exception {
        //读取数据
        String body = readData(request);
        log.info("支付成功回调:{}", body);

        //数据解密
        String plainText = decryptData(body);
        log.info("解密后的文本:{}", plainText);

        JSONObject jsonObject = JSON.parseObject(plainText);
        String outTradeNo = jsonObject.getString("out_trade_no");//商户平台订单号
        String transactionId = jsonObject.getString("transaction_id");//微信支付交易号

        log.info("商户平台订单号:{}", outTradeNo);
        log.info("微信支付交易号:{}", transactionId);

        //业务处理,修改订单状态、来单提醒
        orderService.paySuccess(outTradeNo);

        //给微信响应
        responseToWeixin(response);
    }

    /**
     * 读取数据
     *
     * @param request
     * @return
     * @throws Exception
     */
    private String readData(HttpServletRequest request) throws Exception {
        BufferedReader reader = request.getReader();
        StringBuilder result = new StringBuilder();
        String line = null;
        while ((line = reader.readLine()) != null) {
            if (result.length() > 0) {
                result.append("\n");
            }
            result.append(line);
        }
        return result.toString();
    }

    /**
     * 数据解密
     *
     * @param body
     * @return
     * @throws Exception
     */
    private String decryptData(String body) throws Exception {
        JSONObject resultObject = JSON.parseObject(body);
        JSONObject resource = resultObject.getJSONObject("resource");
        String ciphertext = resource.getString("ciphertext");
        String nonce = resource.getString("nonce");
        String associatedData = resource.getString("associated_data");

        AesUtil aesUtil = new AesUtil(weChatProperties.getApiV3Key().getBytes(StandardCharsets.UTF_8));
        //密文解密
        String plainText = aesUtil.decryptToString(associatedData.getBytes(StandardCharsets.UTF_8),
                nonce.getBytes(StandardCharsets.UTF_8),
                ciphertext);

        return plainText;
    }

    /**
     * 给微信响应
     * @param response
     */
    private void responseToWeixin(HttpServletResponse response) throws Exception{
        response.setStatus(200);
        HashMap<Object, Object> map = new HashMap<>();
        map.put("code", "SUCCESS");
        map.put("message", "SUCCESS");
        response.setHeader("Content-type", ContentType.APPLICATION_JSON.toString());
        response.getOutputStream().write(JSONUtils.toJSONString(map).getBytes(StandardCharsets.UTF_8));
        response.flushBuffer();
    }
}

查询历史订单

业务规则

  • 分页查询历史订单
  • 可以根据订单状态查询
  • 展示订单数据时,需要展示的数据包括:下单时间、订单状态、订单金额、订单明细(商品名称、图片)

接口设计:参见接口文档

1.2 代码实现

1.2.1 user/OrderController

    /**
     * 历史订单查询
     *
     * @param page
     * @param pageSize
     * @param status   订单状态 1待付款 2待接单 3已接单 4派送中 5已完成 6已取消
     * @return
     */
    @GetMapping("/historyOrders")
    @ApiOperation("历史订单查询")
    public Result<PageResult> page(int page, int pageSize, Integer status) {
        PageResult pageResult = orderService.pageQuery4User(page, pageSize, status);
        return Result.success(pageResult);
    }

1.2.2 OrderService

    /**
     * 用户端订单分页查询
     * @param page
     * @param pageSize
     * @param status
     * @return
     */
    PageResult pageQuery4User(int page, int pageSize, Integer status);

1.2.3 OrderServiceImpl

/**
     * 用户端订单分页查询
     *
     * @param pageNum
     * @param pageSize
     * @param status
     * @return
     */
    public PageResult pageQuery4User(int pageNum, int pageSize, Integer status) {
        // 设置分页
        PageHelper.startPage(pageNum, pageSize);

        OrdersPageQueryDTO ordersPageQueryDTO = new OrdersPageQueryDTO();
        ordersPageQueryDTO.setUserId(BaseContext.getCurrentId());
        ordersPageQueryDTO.setStatus(status);

        // 分页条件查询
        Page<Orders> page = orderMapper.pageQuery(ordersPageQueryDTO);

        List<OrderVO> list = new ArrayList();

        // 查询出订单明细,并封装入OrderVO进行响应
        if (page != null && page.getTotal() > 0) {
            for (Orders orders : page) {
                Long orderId = orders.getId();// 订单id

                // 查询订单明细
                List<OrderDetail> orderDetails = orderDetailMapper.getByOrderId(orderId);

                OrderVO orderVO = new OrderVO();
                BeanUtils.copyProperties(orders, orderVO);
                orderVO.setOrderDetailList(orderDetails);

                list.add(orderVO);
            }
        }
        return new PageResult(page.getTotal(), list);
    }

1.2.4 OrderMapper

    /**
     * 分页条件查询并按下单时间排序
     * @param ordersPageQueryDTO
     */
    Page<Orders> pageQuery(OrdersPageQueryDTO ordersPageQueryDTO);

1.2.5 OrderMapper.xml

    <select id="pageQuery" resultType="Orders">
        select * from orders
        <where>
            <if test="number != null and number!=''">
                and number like concat('%',#{number},'%')
            </if>
            <if test="phone != null and phone!=''">
                and phone like concat('%',#{phone},'%')
            </if>
            <if test="userId != null">
                and user_id = #{userId}
            </if>
            <if test="status != null">
                and status = #{status}
            </if>
            <if test="beginTime != null">
                and order_time &gt;= #{beginTime}
            </if>
            <if test="endTime != null">
                and order_time &lt;= #{endTime}
            </if>
        </where>
        order by order_time desc
    </select>

1.2.6 OrderDetailMapper

    /**
     * 根据订单id查询订单明细
     * @param orderId
     * @return
     */
    @Select("select * from order_detail where order_id = #{orderId}")
    List<OrderDetail> getByOrderId(Long orderId);

查询订单详情

2.2 代码实现

2.2.1 user/OrderController
    /**
     * 查询订单详情
     *
     * @param id
     * @return
     */
    @GetMapping("/orderDetail/{id}")
    @ApiOperation("查询订单详情")
    public Result<OrderVO> details(@PathVariable("id") Long id) {
        OrderVO orderVO = orderService.details(id);
        return Result.success(orderVO);
    }

2.2.2 OrderService

    /**
     * 查询订单详情
     * @param id
     * @return
     */
    OrderVO details(Long id);

2.2.3 OrderServiceImpl

    /**
     * 查询订单详情
     *
     * @param id
     * @return
     */
    public OrderVO details(Long id) {
        // 根据id查询订单
        Orders orders = orderMapper.getById(id);

        // 查询该订单对应的菜品/套餐明细
        List<OrderDetail> orderDetailList = orderDetailMapper.getByOrderId(orders.getId());

        // 将该订单及其详情封装到OrderVO并返回
        OrderVO orderVO = new OrderVO();
        BeanUtils.copyProperties(orders, orderVO);
        orderVO.setOrderDetailList(orderDetailList);

        return orderVO;
    }

2.2.4 OrderMapper

    /**
     * 根据id查询订单
     * @param id
     */
    @Select("select * from orders where id=#{id}")
    Orders getById(Long id);

取消订单

业务规则:

  • 待支付和待接单状态下,用户可直接取消订单
  • 商家已接单状态下,用户取消订单需电话沟通商家
  • 派送中状态下,用户取消订单需电话沟通商家
  • 如果在待接单状态下取消订单,需要给用户退款
  • 取消订单后需要将订单状态修改为“已取消”

3.2.1 user/OrderController

    /**
     * 用户取消订单
     *
     * @return
     */
    @PutMapping("/cancel/{id}")
    @ApiOperation("取消订单")
    public Result cancel(@PathVariable("id") Long id) throws Exception {
        orderService.userCancelById(id);
        return Result.success();
    }

3.2.2 OrderService

    /**
     * 用户取消订单
     * @param id
     */
    void userCancelById(Long id) throws Exception;

3.2.3 OrderServiceImpl

    /**
     * 用户取消订单
     *
     * @param id
     */
    public void userCancelById(Long id) throws Exception {
        // 根据id查询订单
        Orders ordersDB = orderMapper.getById(id);

        // 校验订单是否存在
        if (ordersDB == null) {
            throw new OrderBusinessException(MessageConstant.ORDER_NOT_FOUND);
        }

        //订单状态 1待付款 2待接单 3已接单 4派送中 5已完成 6已取消
        if (ordersDB.getStatus() > 2) {
            throw new OrderBusinessException(MessageConstant.ORDER_STATUS_ERROR);
        }

        Orders orders = new Orders();
        orders.setId(ordersDB.getId());

        // 订单处于待接单状态下取消,需要进行退款
        if (ordersDB.getStatus().equals(Orders.TO_BE_CONFIRMED)) {
            //调用微信支付退款接口
            weChatPayUtil.refund(
                    ordersDB.getNumber(), //商户订单号
                    ordersDB.getNumber(), //商户退款单号
                    new BigDecimal(0.01),//退款金额,单位 元
                    new BigDecimal(0.01));//原订单金额

            //支付状态修改为 退款
            orders.setPayStatus(Orders.REFUND);
        }

        // 更新订单状态、取消原因、取消时间
        orders.setStatus(Orders.CANCELLED);
        orders.setCancelReason("用户取消");
        orders.setCancelTime(LocalDateTime.now());
        orderMapper.update(orders);
    }

再来一单

4.2.1 user/OrderController

    /**
     * 再来一单
     *
     * @param id
     * @return
     */
    @PostMapping("/repetition/{id}")
    @ApiOperation("再来一单")
    public Result repetition(@PathVariable Long id) {
        orderService.repetition(id);
        return Result.success();
    }

4.2.2 OrderService

    /**
     * 再来一单
     *
     * @param id
     */
    void repetition(Long id);

4.2.3 OrderServiceImpl

    /**
     * 再来一单
     *
     * @param id
     */
   @Override
    public void repetition(Long id) {
        //查询当前用户id
        Long userId = BaseContext.getCurrentId();
        //根据订单id查询当前订单详情
        List<OrderDetail> orderDetailList = orderDetailMapper.getByOrderId(id);

        // 将订单详情对象转换为购物车对象
        // 这一行使用 map 方法对每个 OrderDetail 对象进行转换操作,x 是当前遍历的 OrderDetail 对象
        List<ShoppingCart> shoppingCartList = orderDetailList.stream().map(x -> {
            //表示一个函数,该函数接受一个参数 x 并返回一个新对象
            ShoppingCart shoppingCart = new ShoppingCart();

            // 将原订单详情里面的菜品信息重新复制到购物车对象中
            BeanUtils.copyProperties(x, shoppingCart, "id");
            shoppingCart.setUserId(userId);
            shoppingCart.setCreateTime(LocalDateTime.now());

            return shoppingCart;
        }).collect(Collectors.toList());
// 使用 collect 方法将转换后的 ShoppingCart 对象收集到一个新的 List<ShoppingCart> 列表中
        // 将购物车对象批量添加到数据库
        shoppingCartMapper.insertBatch(shoppingCartList);
    }

4.2.4 ShoppingCartMapper

    /**
     * 批量插入购物车数据
     *
     * @param shoppingCartList
     */
    void insertBatch(List<ShoppingCart> shoppingCartList);

4.2.5 ShoppingCartMapper.xml

<insert id="insertBatch" parameterType="list">
        insert into shopping_cart
        (name, image, user_id, dish_id, setmeal_id, dish_flavor, number, amount, create_time)
        values
        <foreach collection="shoppingCartList" item="sc" separator=",">
            
        </foreach>
</insert>

订单搜索

1.2.1 admin/OrderController

在admin包下创建OrderController

/**
 * 订单管理
 */
@RestController("adminOrderController")
@RequestMapping("/admin/order")
@Slf4j
@Api(tags = "订单管理接口")
public class OrderController {

    @Autowired
    private OrderService orderService;

    /**
     * 订单搜索
     *
     * @param ordersPageQueryDTO
     * @return
     */
    @GetMapping("/conditionSearch")
    @ApiOperation("订单搜索")
    public Result<PageResult> conditionSearch(OrdersPageQueryDTO ordersPageQueryDTO) {
        PageResult pageResult = orderService.conditionSearch(ordersPageQueryDTO);
        return Result.success(pageResult);
    }
}

1.2.2 OrderService

    /**
     * 条件搜索订单
     * @param ordersPageQueryDTO
     * @return
     */
    PageResult conditionSearch(OrdersPageQueryDTO ordersPageQueryDTO);

1.2.3 OrderServiceImpl

    /**
     * 订单搜索
     *
     * @param ordersPageQueryDTO
     * @return
     */
    public PageResult conditionSearch(OrdersPageQueryDTO ordersPageQueryDTO) {
        PageHelper.startPage(ordersPageQueryDTO.getPage(), ordersPageQueryDTO.getPageSize());

        Page<Orders> page = orderMapper.pageQuery(ordersPageQueryDTO);

        // 部分订单状态,需要额外返回订单菜品信息,将Orders转化为OrderVO
        List<OrderVO> orderVOList = getOrderVOList(page);

        return new PageResult(page.getTotal(), orderVOList);
    }

    private List<OrderVO> getOrderVOList(Page<Orders> page) {
        // 需要返回订单菜品信息,自定义OrderVO响应结果
        List<OrderVO> orderVOList = new ArrayList<>();

        List<Orders> ordersList = page.getResult();
        if (!CollectionUtils.isEmpty(ordersList)) {
            for (Orders orders : ordersList) {
                // 将共同字段复制到OrderVO
                OrderVO orderVO = new OrderVO();
                BeanUtils.copyProperties(orders, orderVO);
                String orderDishes = getOrderDishesStr(orders);

                // 将订单菜品信息封装到orderVO中,并添加到orderVOList
                orderVO.setOrderDishes(orderDishes);
                orderVOList.add(orderVO);
            }
        }
        return orderVOList;
    }

    /**
     * 根据订单id获取菜品信息字符串
     *
     * @param orders
     * @return
     */
    private String getOrderDishesStr(Orders orders) {
        // 查询订单菜品详情信息(订单中的菜品和数量)
        List<OrderDetail> orderDetailList = orderDetailMapper.getByOrderId(orders.getId());

        // 将每一条订单菜品信息拼接为字符串(格式:宫保鸡丁*3;)
        List<String> orderDishList = orderDetailList.stream().map(x -> {
            String orderDish = x.getName() + "*" + x.getNumber() + ";";
            return orderDish;
        }).collect(Collectors.toList());

        // 将该订单对应的所有菜品信息拼接在一起
        return String.join("", orderDishList);
    }

各个状态的订单数量统计

2.2.1 admin/OrderController

    /**
     * 各个状态的订单数量统计
     *
     * @return
     */
    @GetMapping("/statistics")
    @ApiOperation("各个状态的订单数量统计")
    public Result<OrderStatisticsVO> statistics() {
        OrderStatisticsVO orderStatisticsVO = orderService.statistics();
        return Result.success(orderStatisticsVO);
    }

2.2.2 OrderService

    /**
     * 各个状态的订单数量统计
     * @return
     */
    OrderStatisticsVO statistics();

2.2.3 OrderServiceImpl

    /**
     * 各个状态的订单数量统计
     *
     * @return
     */
    public OrderStatisticsVO statistics() {
        // 根据状态,分别查询出待接单、待派送、派送中的订单数量
        Integer toBeConfirmed = orderMapper.countStatus(Orders.TO_BE_CONFIRMED);
        Integer confirmed = orderMapper.countStatus(Orders.CONFIRMED);
        Integer deliveryInProgress = orderMapper.countStatus(Orders.DELIVERY_IN_PROGRESS);

        // 将查询出的数据封装到orderStatisticsVO中响应
        OrderStatisticsVO orderStatisticsVO = new OrderStatisticsVO();
        orderStatisticsVO.setToBeConfirmed(toBeConfirmed);
        orderStatisticsVO.setConfirmed(confirmed);
        orderStatisticsVO.setDeliveryInProgress(deliveryInProgress);
        return orderStatisticsVO;
    }

2.2.4 OrderMapper

    /**
     * 根据状态统计订单数量
     * @param status
     */
    @Select("select count(id) from orders where status = #{status}")
    Integer countStatus(Integer status);

查询订单详情

业务规则:

  • 订单详情页面需要展示订单基本信息(状态、订单号、下单时间、收货人、电话、收货地址、金额等)
  • 订单详情页面需要展示订单明细数据(商品名称、数量、单价)

3.2.1 admin/OrderController

    /**
     * 订单详情
     *
     * @param id
     * @return
     */
    @GetMapping("/details/{id}")
    @ApiOperation("查询订单详情")
    public Result<OrderVO> details(@PathVariable("id") Long id) {
        OrderVO orderVO = orderService.details(id);
        return Result.success(orderVO);
    }

接单

业务规则:

  • 商家接单其实就是将订单的状态修改为“已接单”

4.2.1 admin/OrderController

    /**
     * 接单
     *
     * @return
     */
    @PutMapping("/confirm")
    @ApiOperation("接单")
    public Result confirm(@RequestBody OrdersConfirmDTO ordersConfirmDTO) {
        orderService.confirm(ordersConfirmDTO);
        return Result.success();
    }

4.2.2 OrderService

    /**
     * 接单
     *
     * @param ordersConfirmDTO
     */
    void confirm(OrdersConfirmDTO ordersConfirmDTO);

4.2.3 OrderServiceImpl

    /**
     * 接单
     *
     * @param ordersConfirmDTO
     */
    public void confirm(OrdersConfirmDTO ordersConfirmDTO) {
        Orders orders = Orders.builder()
                .id(ordersConfirmDTO.getId())
                .status(Orders.CONFIRMED)
                .build();

        orderMapper.update(orders);
    }

拒单

业务规则:

  • 商家拒单其实就是将订单状态修改为“已取消”
  • 只有订单处于“待接单”状态时可以执行拒单操作
  • 商家拒单时需要指定拒单原因
  • 商家拒单时,如果用户已经完成了支付,需要为用户退款

5.2.1 admin/OrderController

    /**
     * 拒单
     *
     * @return
     */
    @PutMapping("/rejection")
    @ApiOperation("拒单")
    public Result rejection(@RequestBody OrdersRejectionDTO ordersRejectionDTO) throws Exception {
        orderService.rejection(ordersRejectionDTO);
        return Result.success();
    }

5.2.2 OrderService

    /**
     * 拒单
     *
     * @param ordersRejectionDTO
     */
    void rejection(OrdersRejectionDTO ordersRejectionDTO) throws Exception;

5.2.3 OrderServiceImpl

    /**
     * 拒单
     *
     * @param ordersRejectionDTO
     */
    public void rejection(OrdersRejectionDTO ordersRejectionDTO) throws Exception {
        // 根据id查询订单
        Orders ordersDB = orderMapper.getById(ordersRejectionDTO.getId());

        // 订单只有存在且状态为2(待接单)才可以拒单
        if (ordersDB == null || !ordersDB.getStatus().equals(Orders.TO_BE_CONFIRMED)) {
            throw new OrderBusinessException(MessageConstant.ORDER_STATUS_ERROR);
        }

        //支付状态
        Integer payStatus = ordersDB.getPayStatus();
        if (payStatus == Orders.PAID) {
            //用户已支付,需要退款
            String refund = weChatPayUtil.refund(
                    ordersDB.getNumber(),
                    ordersDB.getNumber(),
                    new BigDecimal(0.01),
                    new BigDecimal(0.01));
            log.info("申请退款:{}", refund);
        }

        // 拒单需要退款,根据订单id更新订单状态、拒单原因、取消时间
        Orders orders = new Orders();
        orders.setId(ordersDB.getId());
        orders.setStatus(Orders.CANCELLED);
        orders.setRejectionReason(ordersRejectionDTO.getRejectionReason());
        orders.setCancelTime(LocalDateTime.now());

        orderMapper.update(orders);
    }

取消订单

6.2 代码实现

6.2.1 admin/OrderController

    /**
     * 取消订单
     *
     * @return
     */
    @PutMapping("/cancel")
    @ApiOperation("取消订单")
    public Result cancel(@RequestBody OrdersCancelDTO ordersCancelDTO) throws Exception {
        orderService.cancel(ordersCancelDTO);
        return Result.success();
    }

6.2.2 OrderService

    /**
     * 商家取消订单
     *
     * @param ordersCancelDTO
     */
    void cancel(OrdersCancelDTO ordersCancelDTO) throws Exception;

6.2.3 OrderServiceImpl

    /**
     * 取消订单
     *
     * @param ordersCancelDTO
     */
    public void cancel(OrdersCancelDTO ordersCancelDTO) throws Exception {
        // 根据id查询订单
        Orders ordersDB = orderMapper.getById(ordersCancelDTO.getId());

        //支付状态
        Integer payStatus = ordersDB.getPayStatus();
        if (payStatus == 1) {
            //用户已支付,需要退款
            String refund = weChatPayUtil.refund(
                    ordersDB.getNumber(),
                    ordersDB.getNumber(),
                    new BigDecimal(0.01),
                    new BigDecimal(0.01));
            log.info("申请退款:{}", refund);
        }

        // 管理端取消订单需要退款,根据订单id更新订单状态、取消原因、取消时间
        Orders orders = new Orders();
        orders.setId(ordersCancelDTO.getId());
        orders.setStatus(Orders.CANCELLED);
        orders.setCancelReason(ordersCancelDTO.getCancelReason());
        orders.setCancelTime(LocalDateTime.now());
        orderMapper.update(orders);
    }

派送订单

业务规则:

  • 派送订单其实就是将订单状态修改为“派送中”
  • 只有状态为“待派送”的订单可以执行派送订单操作

7.2.1 admin/OrderController

    /**
     * 派送订单
     *
     * @return
     */
    @PutMapping("/delivery/{id}")
    @ApiOperation("派送订单")
    public Result delivery(@PathVariable("id") Long id) {
        orderService.delivery(id);
        return Result.success();
    }

7.2.2 OrderService

    /**
     * 派送订单
     *
     * @param id
     */
    void delivery(Long id);

7.2.3 OrderServiceImpl

    /**
     * 派送订单
     *
     * @param id
     */
    public void delivery(Long id) {
        // 根据id查询订单
        Orders ordersDB = orderMapper.getById(id);

        // 校验订单是否存在,并且状态为3
        if (ordersDB == null || !ordersDB.getStatus().equals(Orders.CONFIRMED)) {
            throw new OrderBusinessException(MessageConstant.ORDER_STATUS_ERROR);
        }

        Orders orders = new Orders();
        orders.setId(ordersDB.getId());
        // 更新订单状态,状态转为派送中
        orders.setStatus(Orders.DELIVERY_IN_PROGRESS);

        orderMapper.update(orders);
    }

完成订单

业务规则:

  • 完成订单其实就是将订单状态修改为“已完成”
  • 只有状态为“派送中”的订单可以执行订单完成操作

8.2.1 admin/OrderController

    /**
     * 完成订单
     *
     * @return
     */
    @PutMapping("/complete/{id}")
    @ApiOperation("完成订单")
    public Result complete(@PathVariable("id") Long id) {
        orderService.complete(id);
        return Result.success();
    }

8.2.2 OrderService

    /**
     * 完成订单
     *
     * @param id
     */
    void complete(Long id);

8.2.3 OrderServiceImpl

    /**
     * 完成订单
     *
     * @param id
     */
    public void complete(Long id) {
        // 根据id查询订单
        Orders ordersDB = orderMapper.getById(id);

        // 校验订单是否存在,并且状态为4
        if (ordersDB == null || !ordersDB.getStatus().equals(Orders.DELIVERY_IN_PROGRESS)) {
            throw new OrderBusinessException(MessageConstant.ORDER_STATUS_ERROR);
        }

        Orders orders = new Orders();
        orders.setId(ordersDB.getId());
        // 更新订单状态,状态转为完成
        orders.setStatus(Orders.COMPLETED);
        orders.setDeliveryTime(LocalDateTime.now());

        orderMapper.update(orders);
    }

校验收货地址是否超出配送范围

1. 环境准备

注册账号:https://passport.baidu.com/v2/?reg&tt=1671699340600&overseas=&gid=CF954C2-A3D2-417F-9FE6-B0F249ED7E33&tpl=pp&u=https%3A%2F%2Flbsyun.baidu.com%2Findex.php%3Ftitle%3D%E9%A6%96%E9%A1%B5

登录百度地图开放平台:https://lbsyun.baidu.com/

进入控制台,创建应用,获取AK:

![image-20221222170049729](E:\Java实例项目1-20套\第24套【项目实战】Java外卖项目实战《苍穹外卖》SpringBoot+SpringMVC+Vue+Swagger+Lombok+Mybatis+SpringSession+Redis+Nginx+小程序\0-0 源码资料\资料\day09\项目实战参考答案\assets\image-20221222170049729.png)

![image-20221222170256927](E:\Java实例项目1-20套\第24套【项目实战】Java外卖项目实战《苍穹外卖》SpringBoot+SpringMVC+Vue+Swagger+Lombok+Mybatis+SpringSession+Redis+Nginx+小程序\0-0 源码资料\资料\day09\项目实战参考答案\assets\image-20221222170256927.png)

相关接口:

https://lbsyun.baidu.com/index.php?title=webapi/guide/webservice-geocoding

https://lbsyun.baidu.com/index.php?title=webapi/directionlite-v1

2. 代码开发

2.1 application.yml

配置外卖商家店铺地址和百度地图的AK:

![image-20221222170819582](E:\Java实例项目1-20套\第24套【项目实战】Java外卖项目实战《苍穹外卖》SpringBoot+SpringMVC+Vue+Swagger+Lombok+Mybatis+SpringSession+Redis+Nginx+小程序\0-0 源码资料\资料\day09\项目实战参考答案\assets\image-20221222170819582.png)

2.2 OrderServiceImpl

改造OrderServiceImpl,注入上面的配置项:

com/sky/properties/BaiDuProperties.java
package com.sky.properties;

import lombok.Data;
import lombok.Value;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.stereotype.Component;

@Component
// 多个prefix
@ConfigurationProperties(prefix = "sky.baidu")
@Data
public class BaiDuProperties {
    private String shopAddress;
    private String ak;
}
application.yml
  baidu:
    ak: ${sky.baidu.ak}
    shopAddress: ${sky.baidu.shopAddress}
application-dev.yml
  baidu:
    ak: xxxxxxxxx
    shopAddress: 河北省唐山市丰润区燕山路街道美景花园

在OrderServiceImpl中提供校验方法:

/**
     * 检查客户的收货地址是否超出配送范围
     * @param address
     */
    private void checkOutOfRange(String address) {
        Map<String, String> map = new HashMap<>();
        map.put("address", baiDuProperties.getShopAddress());
        map.put("output", "json");
        map.put("ak", baiDuProperties.getAk());

        //获取店铺的经纬度坐标
        String shopCoordinate = HttpClientUtil.doGet("https://api.map.baidu.com/geocoding/v3", map);

        JSONObject jsonObject = JSON.parseObject(shopCoordinate);
        if(!jsonObject.getString("status").equals("0")){
            throw new OrderBusinessException("店铺地址解析失败");
        }

        //数据解析
        JSONObject location = jsonObject.getJSONObject("result").getJSONObject("location");
        String lat = location.getString("lat");
        String lng = location.getString("lng");
        //店铺经纬度坐标
        String shopLngLat = lat + "," + lng;

        map.put("address",address);
        //获取用户收货地址的经纬度坐标
        String userCoordinate = HttpClientUtil.doGet("https://api.map.baidu.com/geocoding/v3", map);

        jsonObject = JSON.parseObject(userCoordinate);
        if(!jsonObject.getString("status").equals("0")){
            throw new OrderBusinessException("收货地址解析失败");
        }

        //数据解析
        location = jsonObject.getJSONObject("result").getJSONObject("location");
        lat = location.getString("lat");
        lng = location.getString("lng");
        //用户收货地址经纬度坐标
        String userLngLat = lat + "," + lng;

        map.put("origin",shopLngLat);
        map.put("destination",userLngLat);
        map.put("steps_info","0");

        //路线规划
        String json = HttpClientUtil.doGet("https://api.map.baidu.com/directionlite/v1/driving", map);

        jsonObject = JSON.parseObject(json);
        if(!jsonObject.getString("status").equals("0")){
            throw new OrderBusinessException("配送路线规划失败");
        }

        //数据解析
        JSONObject result = jsonObject.getJSONObject("result");
        JSONArray jsonArray = (JSONArray) result.get("routes");
        Integer distance = (Integer) ((JSONObject) jsonArray.get(0)).get("distance");

        if(distance > 5000){
            //配送距离超过5000米
            throw new OrderBusinessException("超出配送范围");
        }
    }

在OrderServiceImpl的submitOrder方法中调用上面的校验方法:

   // 检查用户的收获地址是否超出配送范围
        checkOutOfRange(addressBook.getCityName() + addressBook.getDistrictName() + addressBook.getDetail());

![image-20221222171444981](E:\Java实例项目1-20套\第24套【项目实战】Java外卖项目实战《苍穹外卖》SpringBoot+SpringMVC+Vue+Swagger+Lombok+Mybatis+SpringSession+Redis+Nginx+小程序\0-0 源码资料\资料\day09\项目实战参考答案\assets\image-20221222171444981.png)

SpringTask[定时任务]定时自动执行某段Java代码

SpringTask是Spring框架提供的任务调度工具,可以按照约定的时间自动执行某个代码逻辑

应用场景
  • 信用卡每月还款提醒
  • 银行贷款每月还款提醒
  • 火车票售票系统处理未支付订单
  • 入职纪念日为用户发送通知
cron表达式

cron表达式其实就是一个字符串,通过cron表达式可以定义任务触发时间
构成规则:分为6或7个域,由空格分隔开,每个域代表一个含义
每个域的含义分别为:秒、分钟、小时、日、月、周、年(可选)

2022年10月12日上午9点整 对应的cron表达式(日 和 周 不能同时定义)

0 0 9 12 10 ? 2022
https://cron.qqe2.com

分钟 小时
0 0 9 12 10
SpringTask使用步骤:
  • 导入maven坐标 spring-context(已存在)
  • 启动类添加注解 @EnableScheduling 开启任务调度
  • 自定义定时任务类
sky-server  com/sky/task/MyTask.java
package com.sky.task;

import lombok.extern.slf4j.Slf4j;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.util.Date;

/**
 * 自定义定时任务类
 */
@Component
@Slf4j
public class MyTask {

    /**
     * 定时任务 每隔5秒触发一次
     */
    @Scheduled(cron = "0/5 * * * * ?")
    public void executeTask(){
        log.info("定时任务开始执行:{}", new Date());
    }
}

订单状态定时处理

用户下单后可能存在的情况:
  • 下单后未支付,订单一直处于”待支付“状态
  • 用户收获后管理端未点击完成按钮,订单一直处于“派送中”状态
    • 通过定时任务每分钟检查一次是否存在支付超时订单(超过15min),如果存在则修改订单状态为”已取消”
    • 通过定时任务每天凌晨1点检查一次是否存在”派送中”的订单,如果存在则修改订单状态为”已完成”
代码开发:
sky-server  com/sky/task/OrderTask.java
package com.sky.task;

import com.sky.entity.Orders;
import com.sky.mapper.OrderMapper;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;

import java.time.LocalDateTime;
import java.util.List;

@Component
@Slf4j
public class OrderTask {
    @Autowired
    private OrderMapper orderMapper;

    /**
     * 处理超时订单的方法
     */
    @Scheduled(cron = "0 * * * * ?")//每分钟触发一次
    public void processTimeoutOrder(){
        log.info("定时处理超时订单");
        // select * from orders where status = ? and order_time = (当前时间 - 15分钟)
        LocalDateTime time = LocalDateTime.now().plusMinutes(-15);
        List<Orders> ordersList = orderMapper.getByStatusAndOrderTimeLT(Orders.PENDING_PAYMENT, time);
        if (ordersList != null && ordersList.size() > 0) {
            for (Orders orders : ordersList) {
                orders.setStatus(Orders.CANCELLED);
                orders.setCancelReason("订单超时,自动取消");
                orders.setCancelTime(LocalDateTime.now());
                orderMapper.update(orders);
            }
        }
    }

    /**
     * 处理一直处于派送中状态的订单
     */
    @Scheduled(cron = "0 0 1 * * ?")//每天凌晨一点
    public void processDeliveryOrder() {
        log.info("定时处理处于派送中的订单");
        LocalDateTime time = LocalDateTime.now().plusMinutes(-60);
        List<Orders> ordersList = orderMapper.getByStatusAndOrderTimeLT(Orders.DELIVERY_IN_PROGRESS, time);
        if (ordersList != null && ordersList.size() > 0) {
            for (Orders orders : ordersList) {
                orders.setStatus(Orders.COMPLETED);
                orderMapper.update(orders);
            }
        }
    }
}
sky-server  com/sky/mapper/OrderMapper.java
    /**
     *
     * 根据订单状态和下单时间查询订单
     * @param status
     * @param orderTime
     * @return
     */
    @Select("select * from orders where status = #{status} and order_time < #{orderTime}")
    List<Orders> getByStatusAndOrderTimeLT(Integer status, LocalDateTime orderTime);

WebSocket

WebSocket是基于TCP的一种新的网络协议,它实现了浏览器与服务器全双工通信——浏览器和服务器只需要完成一次握手,两者之间就可以创建持续性连接,并进行双向数据传输

应用场景:
  • 视频弹幕
  • 网页聊天
  • 体育实况更新
  • 股票基金报价实时更新
入门案例

实现步骤:

  • 直接使用websocket.html页面坐位WebSocket客户端
  • 导入WebSocket的maven坐标
  • 导入WebSocket服务端组件WebSocketServer,用于和客户端通信
  • 导入配置类WebSocketConfiguration,注册WebSocket的服务端组件
  • 导入定时人物类WebSocketTask,定时向客户端推送数据
websocket.html
<!DOCTYPE HTML>
<html>
<head>
    <meta charset="UTF-8">
    <title>WebSocket Demo</title>
</head>
<body>
    <input id="text" type="text" />
    <button onclick="send()">发送消息</button>
    <button onclick="closeWebSocket()">关闭连接</button>
    <div id="message">
    </div>
</body>
<script type="text/javascript">
    var websocket = null;
    var clientId = Math.random().toString(36).substr(2);

    //判断当前浏览器是否支持WebSocket
    if('WebSocket' in window){
        //连接WebSocket节点
        websocket = new WebSocket("ws://localhost:8080/ws/"+clientId);
    }
    else{
        alert('Not support websocket')
    }

    //连接发生错误的回调方法
    websocket.onerror = function(){
        setMessageInnerHTML("error");
    };

    //连接成功建立的回调方法
    websocket.onopen = function(){
        setMessageInnerHTML("连接成功");
    }

    //接收到消息的回调方法
    websocket.onmessage = function(event){
        setMessageInnerHTML(event.data);
    }

    //连接关闭的回调方法
    websocket.onclose = function(){
        setMessageInnerHTML("close");
    }

    //监听窗口关闭事件,当窗口关闭时,主动去关闭websocket连接,防止连接还没断开就关闭窗口,server端会抛异常。
    window.onbeforeunload = function(){
        websocket.close();
    }

    //将消息显示在网页上
    function setMessageInnerHTML(innerHTML){
        document.getElementById('message').innerHTML += innerHTML + '<br/>';
    }

    //发送消息
    function send(){
        var message = document.getElementById('text').value;
        websocket.send(message);
    }
    
    //关闭连接
    function closeWebSocket() {
        websocket.close();
    }
</script>
</html>
com/sky/websocket/WebSocketServer.java
package com.sky.websocket;

import org.springframework.stereotype.Component;
import javax.websocket.OnClose;
import javax.websocket.OnMessage;
import javax.websocket.OnOpen;
import javax.websocket.Session;
import javax.websocket.server.PathParam;
import javax.websocket.server.ServerEndpoint;
import java.util.Collection;
import java.util.HashMap;
import java.util.Map;

/**
 * WebSocket服务
 */
@Component //交给spring容器管理
@ServerEndpoint("/ws/{sid}")
public class WebSocketServer {

    //存放会话对象
    private static Map<String, Session> sessionMap = new HashMap();

    /**
     * 连接建立成功调用的方法
     */
    @OnOpen
    public void onOpen(Session session, @PathParam("sid") String sid) {
        System.out.println("客户端:" + sid + "建立连接");
        sessionMap.put(sid, session);
    }

    /**
     * 收到客户端消息后调用的方法
     *
     * @param message 客户端发送过来的消息
     */
    @OnMessage
    public void onMessage(String message, @PathParam("sid") String sid) {
        System.out.println("收到来自客户端:" + sid + "的信息:" + message);
    }

    /**
     * 连接关闭调用的方法
     *
     * @param sid
     */
    @OnClose
    public void onClose(@PathParam("sid") String sid) {
        System.out.println("连接断开:" + sid);
        sessionMap.remove(sid);
    }

    /**
     * 群发
     *
     * @param message
     */
    public void sendToAllClient(String message) {
        Collection<Session> sessions = sessionMap.values();
        for (Session session : sessions) {
            try {
                //服务器向客户端发送消息
                session.getBasicRemote().sendText(message);
            } catch (Exception e) {
                e.printStackTrace();
            }
        }
    }
}
com/sky/config/WebSocketConfiguration.java
package com.sky.config;

import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.socket.server.standard.ServerEndpointExporter;

/**
 * WebSocket配置类,用于注册WebSocket的Bean
 */
@Configuration
public class WebSocketConfiguration {

    @Bean
    public ServerEndpointExporter serverEndpointExporter() {
        return new ServerEndpointExporter();
    }
}
com/sky/task/WebSocketTask.java
package com.sky.task;

import com.sky.websocket.WebSocketServer;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.scheduling.annotation.Scheduled;
import org.springframework.stereotype.Component;
import java.time.LocalDateTime;
import java.time.format.DateTimeFormatter;

@Component
public class WebSocketTask {
    @Autowired
    private WebSocketServer webSocketServer;

    /**
     * 通过WebSocket每隔5秒向客户端发送消息
     */
    @Scheduled(cron = "0/5 * * * * ?")
    public void sendMessageToClient() {
        webSocketServer.sendToAllClient("这是来自服务端的消息:" + DateTimeFormatter.ofPattern("HH:mm:ss").format(LocalDateTime.now()));
    }
}

来单提醒

用户下单并且支付成功后,需要第一时间通知外卖商家

  • 语音播报
  • 弹出提示框
设计:
  • 通过WebSocket实现管理端页面和服务端保持长连接状态
  • 当客户支付后,调用WebSocket的相关API实现服务端向客户端推送消息
  • 客户端浏览器解析服务端推送的消息,判断是来单提醒还是客户催单,进行相应的消息提示和语音播报
  • 约定服务器发送给客户端浏览器的数据格式为JSON,字段包括:type,orderId,content
    • type 为消息类型,1为来单提醒 2为客户催单
    • orderId 为订单id
    • content 为消息内容
sky-server  com/sky/service/impl/OrderServiceImpl.java
 /**
     * 订单支付
     *
     * @param ordersPaymentDTO
     * @return
     */
    public OrderPaymentVO payment(OrdersPaymentDTO ordersPaymentDTO) throws Exception {
        // 查询订单
        Orders order = orderMapper.getByOrderNumber(ordersPaymentDTO.getOrderNumber());
        if (order == null) {
            throw new OrderBusinessException("订单不存在");
        }

        // 检查订单支付状态
        if (order.getPayStatus() == 1) { // 1 表示已支付
            throw new OrderBusinessException("该订单已支付");
        }
        order.setPayStatus(1);

        // 更新订单支付状态为已支付
        order.setPayStatus(Orders.PAID);
        order.setCheckoutTime(LocalDateTime.now());
        order.setPayMethod(ordersPaymentDTO.getPayMethod());
        order.setStatus(Orders.TO_BE_CONFIRMED);

        // 支付成功后通过 WebSocket 向客户端推送消息
        Map<String, Object> map = new HashMap<>();
        map.put("type", 1); // 1 表示来单提醒
        map.put("orderId", order.getId());
        map.put("content", "订单号:" + ordersPaymentDTO.getOrderNumber());

        webSocketServer.sendToAllClient(JSON.toJSONString(map));


        orderMapper.update(order);

        // 构造并返回支付结果对象
        OrderPaymentVO orderPaymentVO = new OrderPaymentVO();
        orderPaymentVO.setOrderNumber(order.getNumber()); // 订单号
        orderPaymentVO.setPaymentTime(new Date());
        orderPaymentVO.setPaymentStatus("SUCCESS");

        return orderPaymentVO;

    }

用户催单

com/sky/controller/user/OrderController.java 
/**
     * 客户催单
     * @param id
     * @return
     */
    @GetMapping("/reminder/{id}")
    @ApiOperation("客户催单")
    public Result reminder(@PathVariable("id") Long id){
        orderService.reminder(id);
        return Result.success();
    }
com/sky/service/OrderService.java
 /**
     * 用户催单
     * @param id
     */
    void reminder(Long id);
com/sky/service/impl/OrderServiceImpl.java
/**
     * 客户催单
     * @param id
     */
    public void reminder(Long id) {
        // 根据id查询订单
        Orders ordersDB = orderMapper.getById(id);

        // 校验订单是否存在
        if (ordersDB == null) {
            throw new OrderBusinessException(MessageConstant.ORDER_STATUS_ERROR);
        }

        Map map = new HashMap();
        map.put("type",2); //1表示来单提醒 2表示客户催单
        map.put("orderId",id);
        map.put("content","订单号:" + ordersDB.getNumber());

        //通过websocket向客户端浏览器推送消息
        webSocketServer.sendToAllClient(JSON.toJSONString(map));
    }

ApacheECharts

http://echarts.apache.org/zh/index.html

  • 柱形图 bar
  • 饼形图
  • 折线图

使用Echarts,重点在于研究当前图标所需数据格式,通常是需要后端提供符合格式要求的动态数据,然后相应给前端来展示图表

营业额统计

业务规则
  • 营业额指订单状态为已完成的订单金额合计
  • 基于可视化报表的折线图展示营业额数据,x轴为日期,y轴为营业额
  • 根据时间选择区间,展示每天的营业额数据
根据接口定义设计对应的vo:
sky-pojo  com/sky/vo/TurnoverReportVO.java
package com.sky.vo;

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

import java.io.Serializable;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class TurnoverReportVO implements Serializable {

    //日期,以逗号分隔,例如:2022-10-01,2022-10-02,2022-10-03 [开始到结束的每一天]
    private String dateList;

    //营业额,以逗号分隔,例如:406.0,1520.0,75.0 [营业额一一对应]
    private String turnoverList;
}
sky-server  com/sky/controller/admin/ReportController.java
package com.sky.controller.admin;

import com.sky.result.Result;
import com.sky.service.ReportService;
import com.sky.vo.TurnoverReportVO;
import io.swagger.annotations.Api;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.time.LocalDate;

/**
 * 数据统计相关接口
 */
@RestController
@RequestMapping("/admin/report")
@Api(tags = "数据统计接口")
@Slf4j
public class ReportController {
    @Autowired
    private ReportService reportService;

    /**
     * 统计指定时间区间内的营业额数据
     * @param begin
     * @param end
     * @return
     */
    @GetMapping("/turnoverStatistics")
    public Result<TurnoverReportVO> turnoverStatistics(
            @DateTimeFormat(pattern = "yyyy-MM-dd")LocalDate begin,
            @DateTimeFormat(pattern = "yyyy-MM-dd")LocalDate end) {
        log.info("营业额统计:{},{}",begin,end);
        return Result.success(reportService.getTurnoverStatistics(begin,end));
    }
}
sky-server  com/sky/service/ReportService.java
package com.sky.service;

import com.sky.vo.TurnoverReportVO;

import java.time.LocalDate;

public interface ReportService {
    /**
     * 统计指定时间区间内的营业额数据
     * @param begin
     * @param end
     * @return
     */
    TurnoverReportVO getTurnoverStatistics(LocalDate begin, LocalDate end);
}
sky-server  com/sky/service/impl/ReportServiceImpl.java
package com.sky.service.impl;

import com.sky.entity.Orders;
import com.sky.mapper.OrderMapper;
import com.sky.service.ReportService;
import com.sky.vo.TurnoverReportVO;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Service
@Slf4j
public class ReportServiceImpl implements ReportService {
    @Autowired
    private OrderMapper orderMapper;
    /**
     * 统计指定时间区间内的营业额数据
     * @param begin
     * @param end
     * @return
     */
    @Override
    public TurnoverReportVO getTurnoverStatistics(LocalDate begin, LocalDate end) {
        // 当前集合用于存放从begin到end范围内的每天的日期
        List<LocalDate> dateList = new ArrayList<>();
        dateList.add(begin);

        while (!begin.equals(end)) {
            //日期计算,计算指定日期的后一天对应的日期
            begin = begin.plusDays(1);
            dateList.add(begin);
        }

        // 存放每天的营业额
        List<Double> turnoverList = new ArrayList<>();
        for (LocalDate date : dateList) { //LocalDate只是年月日 而下单的Order有时分秒
            // 查询Date日期对应的营业额数据,数据额是指:订单状态为“已完成”的订单金额合计
            LocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN);
            LocalDateTime endTime = LocalDateTime.of(date, LocalTime.MAX);
        // select sum(count) from orders where order_time > ? and order_time < ? and status = 5
            Map map = new HashMap<>();
            map.put("begin", beginTime);
            map.put("end", endTime);
            map.put("status", Orders.COMPLETED);
            Double turnover = orderMapper.sumByMap(map);
            turnover = turnover == null ? 0.0 : turnover;//没有营业额则默认为0
            turnoverList.add(turnover);
        }

        return TurnoverReportVO.builder()
                .dateList(StringUtils.join(dateList, ","))
                .turnoverList(StringUtils.join(turnoverList, ","))
                .build();
    }
}
sky-server  com/sky/mapper/OrderMapper.java
/**
     * 根据动态条件统计营业额数据
     * @param map
     * @return
     */
    Double sumByMap(Map map);
sky-server  mapper/OrderMapper.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.OrderMapper">

    <insert id="insert" parameterType="Orders" useGeneratedKeys="true" keyProperty="id">
        insert into orders
        (number, status, user_id, address_book_id, order_time, checkout_time, pay_method, pay_status, amount, remark,
         phone, address, consignee, estimated_delivery_time, delivery_status, pack_amount, tableware_number,
         tableware_status)
        values (#{number}, #{status}, #{userId}, #{addressBookId}, #{orderTime}, #{checkoutTime}, #{payMethod},
                #{payStatus}, #{amount}, #{remark}, #{phone}, #{address}, #{consignee},
                #{estimatedDeliveryTime}, #{deliveryStatus}, #{packAmount}, #{tablewareNumber}, #{tablewareStatus})
    </insert>

    <update id="update" parameterType="com.sky.entity.Orders">
        update orders
        <set>
            <if test="cancelReason != null and cancelReason!='' ">
                cancel_reason=#{cancelReason},
            </if>
            <if test="rejectionReason != null and rejectionReason!='' ">
                rejection_reason=#{rejectionReason},
            </if>
            <if test="cancelTime != null">
                cancel_time=#{cancelTime},
            </if>
            <if test="payStatus != null">
                pay_status=#{payStatus},
            </if>
            <if test="payMethod != null">
                pay_method=#{payMethod},
            </if>
            <if test="checkoutTime != null">
                checkout_time=#{checkoutTime},
            </if>
            <if test="status != null">
                status = #{status},
            </if>
            <if test="deliveryTime != null">
                delivery_time = #{deliveryTime}
            </if>
        </set>
        where id = #{id}
    </update>

    <select id="pageQuery" resultType="Orders">
        select * from orders
        <where>
            <if test="number != null and number!=''">
                and number like concat('%',#{number},'%')
            </if>
            <if test="phone != null and phone!=''">
                and phone like concat('%',#{phone},'%')
            </if>
            <if test="userId != null">
                and user_id = #{userId}
            </if>
            <if test="status != null">
                and status = #{status}
            </if>
            <if test="beginTime != null">
                and order_time &gt;= #{beginTime}
            </if>
            <if test="endTime != null">
                and order_time &lt;= #{endTime}
            </if>
        </where>
        order by order_time desc
    </select>
    <!-- 根据订单号查询订单 -->
    <select id="getByOrderNumber" parameterType="String" resultType="Orders">
        select * from orders where number = #{orderNumber}
    </select>
    <select id="sumByMap" resultType="java.lang.Double">
        select sum(amount) from orders
        <where>
            <if test="begin != null">
                and order_time &gt; #{begin}
            </if>
            <if test="end != null">
                and order_time &lt; #{end}
            </if>
            <if test="status != null">
                and status = #{status}
            </if>
        </where>
    </select>
</mapper>

用户统计

业务规则:

  • 根据时间选择区间,展示每天的用户总量和新增用户量数据
据接口定义设计对应的vo:
sky-pojo  com/sky/vo/UserReportVO.java
package com.sky.vo;

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

import java.io.Serializable;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class UserReportVO implements Serializable {

    //日期,以逗号分隔,例如:2022-10-01,2022-10-02,2022-10-03
    private String dateList;

    //用户总量,以逗号分隔,例如:200,210,220
    private String totalUserList;

    //新增用户,以逗号分隔,例如:20,21,10
    private String newUserList;

}
sky-server  com/sky/controller/admin/ReportController.java
package com.sky.controller.admin;

import com.sky.result.Result;
import com.sky.service.ReportService;
import com.sky.vo.TurnoverReportVO;
import io.swagger.annotations.Api;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.time.LocalDate;

/**
     * 用户统计
     * @param begin
     * @param end
     * @return
     */
    @GetMapping("/userStatistics")
    @ApiOperation("用户统计")
    public Result<UserReportVO> userStatistics(
            @DateTimeFormat(pattern = "yyyy-MM-dd")LocalDate begin,
            @DateTimeFormat(pattern = "yyyy-MM-dd")LocalDate end){
        log.info("用户数据统计:{},{}",begin,end);
        return Result.success(reportService.getUserStatistics(begin,end));
    }
sky-server  com/sky/service/ReportService.java
package com.sky.service;

import com.sky.vo.TurnoverReportVO;

import java.time.LocalDate;

public interface ReportService {
   /**
     * 用户统计
     * @param begin
     * @param end
     * @return
     */
    UserReportVO getUserStatistics(LocalDate begin, LocalDate end);
}
sky-server  com/sky/service/impl/ReportServiceImpl.java
package com.sky.service.impl;

import com.sky.entity.Orders;
import com.sky.mapper.OrderMapper;
import com.sky.service.ReportService;
import com.sky.vo.TurnoverReportVO;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

@Service
@Slf4j
public class ReportServiceImpl implements ReportService {
    /**
     * 统计指定时间区间内的用户数据
     * @param begin
     * @param end
     * @return
     */
    @Override
    public UserReportVO getUserStatistics(LocalDate begin, LocalDate end) {
        // 存放从begin 到 end之间的日期
        List<LocalDate> dateList = new ArrayList<>();
        dateList.add(begin);
        while (!begin.equals(end)) {
            //日期计算,计算指定日期的后一天的日期
            begin = begin.plusDays(1);
            dateList.add(begin);
        }

        // 存放每天新增用户数量 select count(id) from user where create_time > ? and create_time < ?
        List<Integer> newUserList = new ArrayList<>();
        // 存放每天的总用户数量 select count(id) from user where create_time <= ?
        List<Integer> totalUserList = new ArrayList<>();

        for (LocalDate date : dateList) {
        // 遍历每一天的用户总量和数量
            LocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN);
            LocalDateTime endTime = LocalDateTime.of(date, LocalTime.MAX);

            Map map = new HashMap<>();
            map.put("end", endTime);

            // 总用户数量
            Integer integer = userMapper.countByMap(map);

            map.put("begin", beginTime);
            //新增用户数量
            Integer newUser = userMapper.countByMap(map);
            totalUserList.add(integer);
            newUserList.add(newUser);
        }
        return UserReportVO.builder()
                .dateList(StringUtils.join(dateList, ","))
                .totalUserList(StringUtils.join(totalUserList, ","))
                .newUserList(StringUtils.join(newUserList, ","))
                .build();
    }
}
sky-server  com/sky/mapper/UserMapper.java
/**
     * 根据动态条件统计用户数量
     * @param map
     * @return
     */
    Integer countByMap(Map map);
sky-server  mapper/UserMapper.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.OrderMapper">

<select id="countByMap" resultType="java.lang.Integer">
        select count(id) from orders
        <where>
            <if test="begin != null">
                and order_time &gt; #{begin}
            </if>
            <if test="end != null">
                and order_time &lt; #{end}
            </if>
        </where>
    </select>
</mapper>

订单统计

业务规则
  • 根据时间选择区间,展示每天的订单总数和有效订单数
  • 展示所选时间区间内的有效订单数、总订单数、订单完成率
  • 订单完成率 = 有效订单数 / 总订单数 * 100%
返回数据
  • dataList 日期列表以逗号分隔
  • orderCompletionRate 订单完成率
  • orderCountList 订单数列表以逗号分隔
  • totalOrderCount 订单总数
  • validOrderCount 有效订单数
  • validOrderCountList 有效订单数列表以逗号分隔
据接口定义设计对应的vo:
sky-pojo  com/sky/vo/OrderReportVO.java
package com.sky.vo;

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

import java.io.Serializable;

@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class OrderReportVO implements Serializable {

    //日期,以逗号分隔,例如:2022-10-01,2022-10-02,2022-10-03
    private String dateList;

    //每日订单数,以逗号分隔,例如:260,210,215
    private String orderCountList;

    //每日有效订单数,以逗号分隔,例如:20,21,10
    private String validOrderCountList;

    //订单总数
    private Integer totalOrderCount;

    //有效订单数
    private Integer validOrderCount;

    //订单完成率
    private Double orderCompletionRate;

}
sky-server  com/sky/controller/admin/ReportController.java
package com.sky.controller.admin;

import com.sky.result.Result;
import com.sky.service.ReportService;
import com.sky.vo.TurnoverReportVO;
import io.swagger.annotations.Api;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.format.annotation.DateTimeFormat;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;

import java.time.LocalDate;

/**
     * 订单统计
     * @param begin
     * @param end
     * @return
     */
    @GetMapping("/ordersStatistics")
    @ApiOperation("订单统计")
    public Result<OrderReportVO> ordersStatistics(
            @DateTimeFormat(pattern = "yyyy-MM-dd")  LocalDate begin,
            @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end){
        log.info("订单数据统计:{},{}",begin,end);
        return Result.success(reportService.getOrderStatistics(begin,end));
    }
sky-server  com/sky/service/ReportService.java
package com.sky.service;

import com.sky.vo.TurnoverReportVO;

import java.time.LocalDate;

public interface ReportService {
 /**
     * 统计指定时间区间内的订单数据
     * @param begin
     * @param end
     * @return
     */
    OrderReportVO getOrderStatistics(LocalDate begin, LocalDate end);
}
sky-server  com/sky/service/impl/ReportServiceImpl.java
package com.sky.service.impl;

import com.sky.entity.Orders;
import com.sky.mapper.OrderMapper;
import com.sky.service.ReportService;
import com.sky.vo.TurnoverReportVO;
import lombok.extern.slf4j.Slf4j;
import org.apache.commons.lang3.StringUtils;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

import java.time.LocalDate;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.List;
import java.util.Map;

 /**
     * 统计指定时间区间内的订单数据
     * @param begin
     * @param end
     * @return
     */
    public OrderReportVO getOrderStatistics(LocalDate begin, LocalDate end) {
        //存放从begin到end之间的每天对应的日期
        List<LocalDate> dateList = new ArrayList<>();

        dateList.add(begin);

        while (!begin.equals(end)) {
            begin = begin.plusDays(1);
            dateList.add(begin);
        }

        //存放每天的订单总数
        List<Integer> orderCountList = new ArrayList<>();
        //存放每天的有效订单数
        List<Integer> validOrderCountList = new ArrayList<>();

        //遍历dateList集合,查询每天的有效订单数和订单总数
        for (LocalDate date : dateList) {
            //查询每天的订单总数 select count(id) from orders where order_time > ? and order_time < ?
            LocalDateTime beginTime = LocalDateTime.of(date, LocalTime.MIN);
            LocalDateTime endTime = LocalDateTime.of(date, LocalTime.MAX);
            Integer orderCount = getOrderCount(beginTime, endTime, null);

            //查询每天的有效订单数 select count(id) from orders where order_time > ? and order_time < ? and status = 5
            Integer validOrderCount = getOrderCount(beginTime, endTime, Orders.COMPLETED);

            orderCountList.add(orderCount);
            validOrderCountList.add(validOrderCount);
        }

        //计算时间区间内的订单总数量
        Integer totalOrderCount = orderCountList.stream().reduce(Integer::sum).get();

        //计算时间区间内的有效订单数量
        Integer validOrderCount = validOrderCountList.stream().reduce(Integer::sum).get();

        Double orderCompletionRate = 0.0;
        if(totalOrderCount != 0){
            //计算订单完成率
            orderCompletionRate = validOrderCount.doubleValue() / totalOrderCount;
        }

        return  OrderReportVO.builder()
                .dateList(StringUtils.join(dateList,","))
                .orderCountList(StringUtils.join(orderCountList,","))
                .validOrderCountList(StringUtils.join(validOrderCountList,","))
                .totalOrderCount(totalOrderCount)
                .validOrderCount(validOrderCount)
                .orderCompletionRate(orderCompletionRate)
                .build();
    }
    /**
     * 根据条件统计订单数量
     * @param begin
     * @param end
     * @param status
     * @return
     */
    private Integer getOrderCount(LocalDateTime begin, LocalDateTime end, Integer status){
        Map map = new HashMap();
        map.put("begin",begin);
        map.put("end",end);
        map.put("status",status);

        return orderMapper.countByMap(map);
    }
sky-server  com/sky/mapper/OrderMapper.java
/**
     * 根据动态条件统计用户数量
     * @param map
     * @return
     */
    Integer countByMap(Map map);
sky-server  mapper/OrderMapper.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.OrderMapper">

<select id="countByMap" resultType="java.lang.Integer">
        select count(id) from orders
        <where>
            <if test="begin != null">
                and order_time &gt; #{begin}
            </if>
            <if test="end != null">
                and order_time &lt; #{end}
            </if>
            <if test="status != null">
                and status = #{status}
            </if>
        </where>
    </select>
</mapper>

销量排名Top10

产品原型 (查已完成的数据)
  • 此处的销量为商品销售的份数
sky-server  com/sky/controller/admin/ReportController.java
/**
     * 销量排名统计
     * @param begin
     * @param end
     * @return
     */
    @GetMapping("/top10")
    @ApiOperation("销量排名top10")
    public Result<SalesTop10ReportVO> top10(
            @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate begin,
            @DateTimeFormat(pattern = "yyyy-MM-dd") LocalDate end){
        log.info("销量排名top10:{},{}",begin,end);
        return Result.success(reportService.getSalesTop10(begin,end));
    }
sky-server  com/sky/service/ReportService.java
/**
     * 销量排名统计
     * @param begin
     * @param end
     * @return
     */
    SalesTop10ReportVO getSalesTop10(LocalDate begin, LocalDate end);
sky-server  com/sky/service/impl/ReportServiceImpl.java
    /**
     * 统计指定时间区间内的销量排名前10
     * @param begin
     * @param end
     * @return
     */
    @Override
    public SalesTop10ReportVO getSalesTop10(LocalDate begin, LocalDate end) {
        LocalDateTime beginTime = LocalDateTime.of(begin, LocalTime.MIN);
        LocalDateTime endTime = LocalDateTime.of(end, LocalTime.MAX);

        List<GoodsSalesDTO> salesTop10 = orderMapper.getSalesTop10(beginTime, endTime);
        List<String> names = salesTop10.stream().map(GoodsSalesDTO::getName).collect(Collectors.toList());
        String nameList = StringUtils.join(names, ",");

        List<Integer> numbers = salesTop10.stream().map(GoodsSalesDTO::getNumber).collect(Collectors.toList());
        String numberList = StringUtils.join(numbers, ",");

        //封装返回结果数据
        return SalesTop10ReportVO
                .builder()
                .nameList(nameList)
                .numberList(numberList)
                .build();
    }
sky-server  com/sky/mapper/OrderMapper.java
  /**
     * 统计指定时间内的销量排名
     * @return
     */
    List<GoodsSalesDTO> getSalesTop10(LocalDateTime begin,LocalDateTime end);
sky-server  mapper/OrderMapper.xml
<select id="getSalesTop10" resultType="com.sky.dto.GoodsSalesDTO">
        select od.name, sum(od.number) number
        from order_detail od,orders o
        where od.order_id = o.id and o.status = 5
        <if test="begin != null">
            and o.order_time &gt; #{begin}
        </if>
        <if test="end != null">
            and o.order_time &lt; #{end}
        </if>
        group by od.name
        order by number desc
        limit 0,10
    </select>
重装数据库
C:\Windows\System32>cd D:\MySQL\MySQL Server 8.0\bin

C:\Windows\System32>mysqld --install MySQL80
Service successfully installed.

C:\Windows\System32>sc query | findstr MySQL

C:\Windows\System32>net start MySQL80
MySQL80 服务正在启动 .
MySQL80 服务已经启动成功。
服务里的MySQL80是Mysql服务
-----------------------------------------------------------------------------------------

C:\Windows\System32>cd D:\MariaDB 11.0\bin

C:\Windows\System32>mysqld --install MariaDB
Service successfully installed.

C:\Windows\System32>net start MariaDB
MariaDB 服务正在启动 .
MariaDB 服务无法启动。
服务里的MariaDB是MariaDB服务

工作台

工作台是系统运营的数据看板,并提供快捷操作入口,可以有效提高商家的工作效率

功能工作台展示的数据:

  • 今日数据
  • 订单管理
  • 菜品总览
  • 套餐总览
  • 订单信息
名词解释:
  • 营业额:已完成订单的总金额
  • 有效订单:已完成订单的数量
  • 订单完成率:有效订单数 / 总订单数 * 100%
  • 平均客单价:营业额 / 有效订单数
  • 新增用户:新增用户的数量
接口设计:
  • 今日数据接口

    Path: /admin/workspace/businessData
    Method: Get

  • 订单管理接口

    Path: /admin/workspace/overviewOrders
    Method: Get

  • 菜品总览接口

    Path: /admin/workspace/overviewDishes
    Method: Get

  • 套餐总览接口

    Path: /admin/workspace/overviewSetmeals
    Method: Get

  • 订单搜索(已完成)

sky-server  com/sky/controller/admin/WorkSpaceController.java
package com.sky.controller.admin;

import com.sky.result.Result;
import com.sky.service.WorkspaceService;
import com.sky.vo.BusinessDataVO;
import com.sky.vo.DishOverViewVO;
import com.sky.vo.OrderOverViewVO;
import com.sky.vo.SetmealOverViewVO;
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.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import java.time.LocalDateTime;
import java.time.LocalTime;

/**
 * 工作台
 */
@RestController
@RequestMapping("/admin/workspace")
@Slf4j
@Api(tags = "工作台相关接口")
public class WorkSpaceController {

    @Autowired
    private WorkspaceService workspaceService;

    /**
     * 工作台今日数据查询
     * @return
     */
    @GetMapping("/businessData")
    @ApiOperation("工作台今日数据查询")
    public Result<BusinessDataVO> businessData(){
        //获得当天的开始时间
        LocalDateTime begin = LocalDateTime.now().with(LocalTime.MIN);
        //获得当天的结束时间
        LocalDateTime end = LocalDateTime.now().with(LocalTime.MAX);

        BusinessDataVO businessDataVO = workspaceService.getBusinessData(begin, end);
        return Result.success(businessDataVO);
    }

    /**
     * 查询订单管理数据
     * @return
     */
    @GetMapping("/overviewOrders")
    @ApiOperation("查询订单管理数据")
    public Result<OrderOverViewVO> orderOverView(){
        return Result.success(workspaceService.getOrderOverView());
    }

    /**
     * 查询菜品总览
     * @return
     */
    @GetMapping("/overviewDishes")
    @ApiOperation("查询菜品总览")
    public Result<DishOverViewVO> dishOverView(){
        return Result.success(workspaceService.getDishOverView());
    }

    /**
     * 查询套餐总览
     * @return
     */
    @GetMapping("/overviewSetmeals")
    @ApiOperation("查询套餐总览")
    public Result<SetmealOverViewVO> setmealOverView(){
        return Result.success(workspaceService.getSetmealOverView());
    }
}
sky-server  com/sky/service/WorkspaceService.java
package com.sky.service;

import com.sky.vo.BusinessDataVO;
import com.sky.vo.DishOverViewVO;
import com.sky.vo.OrderOverViewVO;
import com.sky.vo.SetmealOverViewVO;
import java.time.LocalDateTime;

public interface WorkspaceService {

    /**
     * 根据时间段统计营业数据
     * @param begin
     * @param end
     * @return
     */
    BusinessDataVO getBusinessData(LocalDateTime begin, LocalDateTime end);

    /**
     * 查询订单管理数据
     * @return
     */
    OrderOverViewVO getOrderOverView();

    /**
     * 查询菜品总览
     * @return
     */
    DishOverViewVO getDishOverView();

    /**
     * 查询套餐总览
     * @return
     */
    SetmealOverViewVO getSetmealOverView();

}
sky-server  com/sky/service/impl/WorkspaceServiceImpl.java
package com.sky.service.impl;

import com.sky.constant.StatusConstant;
import com.sky.entity.Orders;
import com.sky.mapper.DishMapper;
import com.sky.mapper.OrderMapper;
import com.sky.mapper.SetmealMapper;
import com.sky.mapper.UserMapper;
import com.sky.service.WorkspaceService;
import com.sky.vo.BusinessDataVO;
import com.sky.vo.DishOverViewVO;
import com.sky.vo.OrderOverViewVO;
import com.sky.vo.SetmealOverViewVO;
import lombok.extern.slf4j.Slf4j;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import java.time.LocalDateTime;
import java.time.LocalTime;
import java.util.HashMap;
import java.util.Map;

@Service
@Slf4j
public class WorkspaceServiceImpl implements WorkspaceService {

    @Autowired
    private OrderMapper orderMapper;
    @Autowired
    private UserMapper userMapper;
    @Autowired
    private DishMapper dishMapper;
    @Autowired
    private SetmealMapper setmealMapper;

    /**
     * 根据时间段统计营业数据
     * @param begin
     * @param end
     * @return
     */
    public BusinessDataVO getBusinessData(LocalDateTime begin, LocalDateTime end) {
        /**
         * 营业额:当日已完成订单的总金额
         * 有效订单:当日已完成订单的数量
         * 订单完成率:有效订单数 / 总订单数
         * 平均客单价:营业额 / 有效订单数
         * 新增用户:当日新增用户的数量
         */

        Map map = new HashMap();
        map.put("begin",begin);
        map.put("end",end);

        //查询总订单数
        Integer totalOrderCount = orderMapper.countByMap(map);

        map.put("status", Orders.COMPLETED);
        //营业额
        Double turnover = orderMapper.sumByMap(map);
        turnover = turnover == null? 0.0 : turnover;

        //有效订单数
        Integer validOrderCount = orderMapper.countByMap(map);

        Double unitPrice = 0.0;

        Double orderCompletionRate = 0.0;
        if(totalOrderCount != 0 && validOrderCount != 0){
            //订单完成率
            orderCompletionRate = validOrderCount.doubleValue() / totalOrderCount;
            //平均客单价
            unitPrice = turnover / validOrderCount;
        }

        //新增用户数
        Integer newUsers = userMapper.countByMap(map);

        return BusinessDataVO.builder()
                .turnover(turnover)
                .validOrderCount(validOrderCount)
                .orderCompletionRate(orderCompletionRate)
                .unitPrice(unitPrice)
                .newUsers(newUsers)
                .build();
    }


    /**
     * 查询订单管理数据
     *
     * @return
     */
    public OrderOverViewVO getOrderOverView() {
        Map map = new HashMap();
        map.put("begin", LocalDateTime.now().with(LocalTime.MIN));
        map.put("status", Orders.TO_BE_CONFIRMED);

        //待接单
        Integer waitingOrders = orderMapper.countByMap(map);

        //待派送
        map.put("status", Orders.CONFIRMED);
        Integer deliveredOrders = orderMapper.countByMap(map);

        //已完成
        map.put("status", Orders.COMPLETED);
        Integer completedOrders = orderMapper.countByMap(map);

        //已取消
        map.put("status", Orders.CANCELLED);
        Integer cancelledOrders = orderMapper.countByMap(map);

        //全部订单
        map.put("status", null);
        Integer allOrders = orderMapper.countByMap(map);

        return OrderOverViewVO.builder()
                .waitingOrders(waitingOrders)
                .deliveredOrders(deliveredOrders)
                .completedOrders(completedOrders)
                .cancelledOrders(cancelledOrders)
                .allOrders(allOrders)
                .build();
    }

    /**
     * 查询菜品总览
     *
     * @return
     */
    public DishOverViewVO getDishOverView() {
        Map map = new HashMap();
        map.put("status", StatusConstant.ENABLE);
        Integer sold = dishMapper.countByMap(map);

        map.put("status", StatusConstant.DISABLE);
        Integer discontinued = dishMapper.countByMap(map);

        return DishOverViewVO.builder()
                .sold(sold)
                .discontinued(discontinued)
                .build();
    }

    /**
     * 查询套餐总览
     *
     * @return
     */
    public SetmealOverViewVO getSetmealOverView() {
        Map map = new HashMap();
        map.put("status", StatusConstant.ENABLE);
        Integer sold = setmealMapper.countByMap(map);

        map.put("status", StatusConstant.DISABLE);
        Integer discontinued = setmealMapper.countByMap(map);

        return SetmealOverViewVO.builder()
                .sold(sold)
                .discontinued(discontinued)
                .build();
    }
}
sky-server  com/sky/mapper/DishMapper.java
/**
     * 根据条件统计菜品数量
     * @param map
     * @return
     */
    Integer countByMap(Map map);
sky-server  mapper/DishMapper.xml
<select id="countByMap" resultType="java.lang.Integer">
        select count(id) from dish
        <where>
            <if test="status != null">
                and status = #{status}
            </if>
            <if test="categoryId != null">
                and category_id = #{categoryId}
            </if>
        </where>
    </select>
sky-server  com/sky/mapper/SetmealMapper.java
/**
     * 根据条件统计套餐数量
     * @param map
     * @return
     */
    Integer countByMap(Map map);
sky-server  mapper/SetmealMapper.xml
<select id="countByMap" resultType="java.lang.Integer">
        select count(id) from setmeal
        <where>
            <if test="status != null">
                and status = #{status}
            </if>
            <if test="categoryId != null">
                and category_id = #{categoryId}
            </if>
        </where>
    </select>

Apache POI

在Java中操控Excel文件 [读写操作]

Apache POI 是一个处理Miscrosoft Office各种文件格式的开源项目,POI都是用于操作Excel文件

Apache POI应用场景:
  • 银行网银系统导出交易明细
  • 各种业务系统到出Excel报表
  • 批量导入业务数据
sky-server  com/sky/test/POITest.java
package com.sky.test;

import org.apache.poi.xssf.usermodel.XSSFRow;
import org.apache.poi.xssf.usermodel.XSSFSheet;
import org.apache.poi.xssf.usermodel.XSSFWorkbook;

import java.io.File;
import java.io.FileNotFoundException;
import java.io.FileOutputStream;

public class POITest {
    /**
     * 通过POI创建Excel文件并且写入文件内容
     */
    public static void write() throws Exception {
        // 在内存中创建一个Excel文件
        XSSFWorkbook excel = new XSSFWorkbook();
        // 在Excel文件中创建一个sheet页
        XSSFSheet sheet = excel.createSheet("info");
        // 在Sheet中创建行对象, rownum编号从0开始
        XSSFRow row = sheet.createRow(1);
        // 创建单元格并写入文件内容
        row.createCell(1).setCellValue("姓名");
        row.createCell(2).setCellValue("城市");

        // 创建一个新行
        row = sheet.createRow(2);
        row.createCell(1).setCellValue("张三");
        row.createCell(2).setCellValue("北京");

        row = sheet.createRow(3);
        row.createCell(1).setCellValue("李四");
        row.createCell(2).setCellValue("南京");

        // 通过输出流将内存中的Excel文件写入到磁盘
        FileOutputStream out = new FileOutputStream(new File("C:\\Users\\Pluminary\\Desktop\\itcast.xlsx"));
        excel.write(out);

        // 关闭资源
        out.close();
        excel.close();
    }

    public static void main(String[] args) throws Exception {
        write();
    }
}

导出运营数据Excel报表

实现步骤:
  • 设计Excel模板文件
  • 查询近30天的运营数据
  • 将查询到的运营数据写入模板文件
  • 通过输出流将Excel文件下载到客户端浏览器
sky-pojo  com/sky/vo/BusinessDataVO.java
package com.sky.vo;

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

import java.io.Serializable;

/**
 * 数据概览
 */
@Data
@Builder
@NoArgsConstructor
@AllArgsConstructor
public class BusinessDataVO implements Serializable {

    private Double turnover;//营业额

    private Integer validOrderCount;//有效订单数

    private Double orderCompletionRate;//订单完成率

    private Double unitPrice;//平均客单价

    private Integer newUsers;//新增用户数

}
sky-server  com/sky/controller/admin/ReportController.java
/**
     * 导出运营数据报表
     * @param response
     */
    @GetMapping("/export")
    @ApiOperation("导出运营数据报表")
    public void export(HttpServletResponse response) {
        reportService.exportBusinessData(response);
    }
sky-server  com/sky/service/ReportService.java
 /**
     * 导出运营数据报表
     * @param response
     */
    void exportBusinessData(HttpServletResponse response);
sky-server  com/sky/service/impl/ReportServiceImpl.java
/**
     * 导出运营数据报表
     * @param response
     */
    @Override
    public void exportBusinessData(HttpServletResponse response) {
        // 查询数据库 获取营业数据 -- 查询最近30天的营业数据
        LocalDate dateBegin = LocalDate.now().minusDays(30);
        LocalDate dateEnd = LocalDate.now().minusDays(1);
        // 查询概览数据
        BusinessDataVO businessDataVO = workspaceService.getBusinessData(LocalDateTime.of(dateBegin, LocalTime.MIN),LocalDateTime.of(dateEnd, LocalTime.MAX));

        // 查询的数据通过POI写入Excel文件中 (获得对象 获得类加载器 类加载器读取资源)
        InputStream in = this.getClass().getClassLoader().getResourceAsStream("template/运营数据报表模板.xlsx");
        try {
            // 基于模板文件创建一个新的Excel文件
            XSSFWorkbook excel = new XSSFWorkbook();

            // 填充数据 [获取标签页]

            XSSFSheet sheet = excel.getSheet("Sheet1");
            // 获取第二行[索引是从0开始]
            sheet.getRow(1).createCell(1).setCellValue("时间:" + dateBegin + "至" + dateEnd);

            // 获得第四行
            XSSFRow row = sheet.getRow(3);
            row.getCell(2).setCellValue(businessDataVO.getTurnover());//营业额
            row.getCell(4).setCellValue(businessDataVO.getOrderCompletionRate());//订单完成率
            row.getCell(6).setCellValue(businessDataVO.getNewUsers());//新增用户数

            // 获得第五行
            row = sheet.getRow(4);
            row.getCell(2).setCellValue(businessDataVO.getValidOrderCount());//有效订单数
            row.getCell(4).setCellValue(businessDataVO.getUnitPrice());//平均单品价格

            // 填充明细数据
            for (int i = 0; i < 30; i++) {
                LocalDate date = dateBegin.plusDays(i);
                // 查询某一天的营业数据
                workspaceService.getBusinessData(LocalDateTime.of(date, LocalTime.MIN), LocalDateTime.of(date, LocalTime.MAX));
                // 获得某一行
                row = sheet.getRow(7 + i);// 利用循环 超越循环
                row.getCell(1).setCellValue(date.toString());
                row.getCell(2).setCellValue(businessDataVO.getTurnover());
                row.getCell(3).setCellValue(businessDataVO.getValidOrderCount());
                row.getCell(6).setCellValue(businessDataVO.getOrderCompletionRate());
                row.getCell(4).setCellValue(businessDataVO.getUnitPrice());
                row.getCell(5).setCellValue(businessDataVO.getNewUsers());
            }

            // 通过输出流将Excel文件下载到客户端浏览器
            ServletOutputStream out = response.getOutputStream();
            excel.write(out);

            // 关闭资源
            out.close();
            excel.close();
        } catch (Exception e) {
            throw new RuntimeException(e);
        }
    }
阅读全文
头像
Asuna
You are the one who can always get to me even with screen between us.