jingyier

进程 vs 线程

对比点 进程(餐厅) 线程(员工)
独立性 每家餐厅独立运营,互不干涉 员工都在同一家店里干活
资源占用 很占地方(房租、装修、设备) 只占工位和小工具
创建速度 开一家新餐厅很慢 招一个新员工很快
沟通难度 两家餐厅合作要打电话、签合同 员工面对面喊一声就行
出问题的影响 这家店着火,隔壁店没事 一个员工犯错,可能拖累整个店(进程挂掉)

一、单进程单线程:你一个人开小面馆

/**
 * 单进程单线程
 * 你一个人开小面馆,自己下面、自己端、自己收钱
 * 来两桌客人就手忙脚乱
 */
@GetMapping("/single-thread")
public String singleThreadNoodleShop() {
    // 你一个人,既当老板又当厨师又当服务员
    System.out.println("【老板】来客了,我亲自下面...");
    
    // 下面(模拟耗时操作)
    try { Thread.sleep(3000); } catch (InterruptedException e) {}
    
    // 端盘子
    System.out.println("【老板】面好了,我亲自端过去...");
    
    // 收钱
    System.out.println("【老板】客人吃完了,我亲自收钱...");
    
    return "单线程:忙完一桌,才能接下一桌";
}

效果

一次只能服务一桌,第二桌要等第一桌全流程干完,效率很低。


二、多进程:兰州拉面连锁,每家店独立

/**
 * 多进程
 * 你开成“兰州拉面连锁”,每家店独立
 * 一家店着火,别家没事,但开分店贵
 */
@GetMapping("/multi-process")
public String multiProcessNoodleChain() throws Exception {
    // 开第一家店
    ProcessBuilder shop1 = new ProcessBuilder("java", "-version");
    Process p1 = shop1.start();
    p1.waitFor();
    System.out.println("【分店1】兰州拉面城东店 已开业,独立经营");
    
    // 开第二家店
    ProcessBuilder shop2 = new ProcessBuilder("java", "-version");
    Process p2 = shop2.start();
    p2.waitFor();
    System.out.println("【分店2】兰州拉面城西店 已开业,独立经营");
    
    // 开第三家店
    ProcessBuilder shop3 = new ProcessBuilder("java", "-version");
    Process p3 = shop3.start();
    p3.waitFor();
    System.out.println("【分店3】兰州拉面城南店 已开业,独立经营");
    
    return "多进程:每家店独立,一家着火不影响别家,但开分店成本高";
}

效果

每个 Process都是一家“分店”,有自己独立的资源和空间,一个挂了不影响其他,但创建和销毁很“贵”。


三、多线程:一家店多招员工

/**
 * 多线程
 * 你还是一家店,但多招几个员工
 * 切菜、下面、端盘、收银各司其职,效率高
 * 但一个员工搞砸,全店遭殃
 */
@GetMapping("/multi-thread")
public String multiThreadNoodleShop() {
    // 你招了三个员工
    Thread cutter = new Thread(() -> {
        System.out.println("【切菜工】切菜中...");
        try { Thread.sleep(2000); } catch (InterruptedException e) {}
        System.out.println("【切菜工】菜切好了");
    });

    Thread cooker = new Thread(() -> {
        System.out.println("【厨师】下面中...");
        try { Thread.sleep(3000); } catch (InterruptedException e) {}
        System.out.println("【厨师】面煮好了");
    });

    Thread waiter = new Thread(() -> {
        System.out.println("【服务员】端盘中...");
        try { Thread.sleep(1000); } catch (InterruptedException e) {}
        System.out.println("【服务员】面端上桌了");
    });

    // 同时开工
    cutter.start();
    cooker.start();
    waiter.start();

    // 等他们都干完
    try {
        cutter.join();
        cooker.join();
        waiter.join();
    } catch (InterruptedException e) {}

    System.out.println("【老板】一桌饭齐活了,效率高!");
    return "多线程:一个店多个员工,各干各的,但一个员工搞砸,全店遭殃";
}

效果

一个进程里多个线程同时干活,效率比单线程高,但大家共享“厨房”,一个线程把锅烧了,整个店(进程)可能挂。


四、线程池:固定养一支员工队伍

/**
 * 线程池
 * 你固定养 5~10 个员工,平时待命
 * 来活就分给他们,没活就休息
 * 不用来一个客人现招人,降低成本,方便管理
 */
@Configuration
public class NoodleShopThreadPoolConfig {

    @Bean("noodleShopPool")
    public Executor noodleShopPool() {
        ThreadPoolTaskExecutor pool = new ThreadPoolTaskExecutor();
        pool.setCorePoolSize(5);         // 平时常备5个员工
        pool.setMaxPoolSize(10);        // 忙的时候最多扩到10个
        pool.setQueueCapacity(100);     // 排队等活的任务最多100个
        pool.setKeepAliveSeconds(60);   // 闲下来60秒没活,就裁掉多余的人
        pool.setThreadNamePrefix("noodle-"); // 员工名字前缀
        pool.initialize();
        return pool;
    }
}

@RestController
public class NoodleShopController {

    @Autowired
    @Qualifier("noodleShopPool")
    private Executor noodleShopPool;

    @GetMapping("/thread-pool")
    public String useThreadPool() {
        System.out.println("【老板】有客人来了,从员工池里派活...");

        for (int i = 1; i <= 8; i++) {
            int table = i;
            noodleShopPool.execute(() -> {
                System.out.println("【员工-" + Thread.currentThread().getName() + 
                                   "】去服务" + table + "号桌");
                try { Thread.sleep(2000); } catch (InterruptedException e) {}
                System.out.println("【员工-" + Thread.currentThread().getName() + 
                                   "】" + table + "号桌服务完毕");
            });
        }

        return "线程池:活来了就分,不用来一个客人现招人,系统稳得很";
    }
}

效果

任务来了,从池子里拿个空闲“员工”去干

干完,员工回池子待命

你只管“分活”,不操心“招人/辞退”


五、在 Spring 里用:搭好“员工池”,只管分活

/**
 * 在 Spring 里
 * 你写一个 @Configuration 类,把“员工池”搭好
 * 业务代码里用 @Autowired 拿到这个池子,把任务丢进去
 * 你只管“分活”,不操心“招人/辞退”,系统稳得很
 */
@Service
public class NoodleOrderService {

    @Autowired
    @Qualifier("noodleShopPool")
    private Executor noodleShopPool;

    public void takeOrder(int tableId) {
        // 收到订单,把任务丢进“员工池”
        noodleShopPool.execute(() -> {
            System.out.println("【接单员】" + tableId + "号桌的订单已交给员工 " + 
                               Thread.currentThread().getName());
            // 模拟做面
            try { Thread.sleep(3000); } catch (InterruptedException e) {}
            System.out.println("【接单员】" + tableId + "号桌的面做好了");
        });
    }
}

@RestController
public class OrderController {

    @Autowired
    private NoodleOrderService orderService;

    @GetMapping("/order/{tableId}")
    public String placeOrder(@PathVariable int tableId) {
        orderService.takeOrder(tableId);
        return tableId + "号桌下单成功,后厨正在做面...";
    }
}

效果

你只管 orderService.takeOrder(tableId)分活

具体谁干、怎么干、招多少人,全是“员工池”和 Spring 帮你管

你这个“老板”轻松多了,系统也稳

总结

单进程单线程

你一个人开小面馆,自己下面、端盘、收钱

来两桌客人就手忙脚乱

代码:new Thread或直接在主线程里干

多进程

开成“兰州拉面连锁”,每家店独立

一家着火,别家没事,但开分店贵

代码:new ProcessBuilder().start()

多线程

还是一家店,多招员工

切菜、下面、端盘、收银各司其职,效率高

但一个员工搞砸,全店遭殃

代码:自己 new Thread启动多个

线程池

固定养 5~10 个员工,平时待命

来活就分,没活休息

不用来一个客人现招人

代码:ThreadPoolTaskExecutor配好,用 executor.execute()

在 Spring 里用

写个 @Configuration搭好“员工池”

业务里 @Autowired拿池子,把任务丢进去

你只管“分活”,不操心“招人/辞退”,系统稳得很

代码:上面 NoodleShopThreadPoolConfig+ NoodleOrderService那一套

chatModel调用了相关大模型的api,chatClient进一步对其进行了封装

可以把它们的关系理解为:

ChatModel(底层)

  • 直接对接大模型厂商的 API(如 DashScope、OpenAI 等)。
  • 负责具体的 HTTP 请求、鉴权、参数拼装、返回解析。
  • 接口比较“原始”,比如 call(String)call(Message...)stream(...)

ChatClient(上层封装)

  • ChatModel之上再做一层封装。

  • 提供更符合 Spring 风格、更易用的 API,比如 chat()prompt()streamChat()

    内部还是调用 ChatModel的方法,只是帮你处理了:

    • 消息对象的组装(SystemMessage、UserMessage 等)
    • Prompt 模板管理
    • 统一的异常处理、日志、配置管理
    • 与 Spring 生态的集成(如自动注入、配置绑定)

ChatModel 一般一个项目一个,ChatClient 可以因不同模型或不同使用场景(如翻译、摘要、对话)封装多个。


HTTP

你去餐厅点餐 → 服务员给你菜 → 走人

想再吃就得重新排队点餐

WebSocket

你坐包间,和服务员连着对讲机

随时喊:“再加个菜”“上汤了没”

服务员也能随时告诉你:“菜好了”

这是为您生成的清晰对比表格:

对比维度 HTTP (超文本传输协议) WebSocket (全双工通信协议)
连接方式 短连接 一次请求对应一次响应,完成后立即断开 长连接 握手后连接持续保持,直到一方主动关闭
通信模式 单工/半双工 只有客户端主动请求,服务器被动响应 全双工 客户端和服务器都可以主动发送数据
实时性 较低 依赖轮询(Polling),存在延迟和开销 很高 服务端可即时推送,无延迟
技术复杂度 简单 生态成熟,开发调试成本低 较复杂 需要处理连接保活、断线重连等问题
典型场景 网页浏览、RESTful API、文件下载、表单提交 即时聊天(IM)、在线游戏、实时股票行情、协同编辑

今天做了苍穹外卖的用户催单、管理端订单提醒、以及定时任务处理未支付订单和取消配送成功但仍然是派送中的订单。前二者的核心就是websocket,通过其建立的长连接实现即时推送,只需添加pom坐标,导入configuration 类和重点的WebSocketServer类,我们实现代码的关键方法就在这里,将其作为容器管理后直接自动注入相应的service层就能实现相关接口;定时任务处理要先在启动类添加注解,并在相关模块添加cron表达式,使其按照规范操作数据库。

一、Spring AI ChatClient 中 system prompt 的优先级从高到低是:

你手动 new Prompt 里的 SystemMessage

.system("xxx")临时设置

defaultSystem("xxx")默认配置**

只要你传了 Prompt,前面两个全部失效。


public void afterPropertiesSet() throws Exception {
        chatClient =ChatClient.builder(chatModel)
                .defaultSystem("1")
                .build();
    }

    @RequestMapping("/call")
    public String chat(@RequestParam String message){
        return chatClient.prompt(message).system("加3").call().content();
    }

    
    @RequestMapping("/callOverwrite")
    public String chat2(@RequestParam String message){
        return chatClient.prompt(new Prompt(new SystemMessage("加3"),new UserMessage( message))).call().content();
    }

二、.call(),.stream(),.content()的作用

一、.call()的作用

.call()是 Spring AI 中用于「同步调用大模型」的方法。

它的作用是:

✅ 将当前构建好的 Prompt(包含 system、user、memory 等信息) 发送给底层的 ChatModel

✅ 阻塞等待模型计算完成

✅ 返回一个 ChatResponse对象

📌 一句话记忆:

.call()= 真正发起一次大模型请求


二、.content()的作用

.content()是从模型返回的 ChatResponse中,提取最终生成的文本内容。

它的作用是:

✅ 从 ChatResponse中取出 AssistantMessage

✅ 再从中取出 content

✅ 返回一个 String

📌 一句话记忆:

.content()= 只要模型说出来的那句话

另外流式输出就用stream:

   //第二个参数用来指定输出内容的字节码
    @RequestMapping("/stream")
    public Flux<String> chat3(@RequestParam String message, HttpServletResponse response){
        response.setCharacterEncoding("utf-8");
        return chatClient.prompt(message).system("加3").stream().content();
    }

.call()和 .stream()是 ChatClient 的“出口”

前面都是在拼 Prompt,到这里才开始真正请求模型

他们的返回方式:

.call():等模型说完,一次性给你

.stream():模型一边说,你一边听

三、自定义初始化逻辑:

理解一下这段代码:

public class PromptEngineerController implements InitializingBean {

    @Autowired
    private ChatModel chatModel;

    private ChatClient chatClient;

    public void afterPropertiesSet() throws Exception {
        chatClient =ChatClient.builder(chatModel)
                .defaultSystem("你是一个助手,你需要帮助用户完成一个任务")
                .build();
    }

}

Spring 里没有 chatClient 这个 Bean,

我就自己造一个,

但我必须等 Spring 先把 chatModel 给我,

所以我借用了 InitializingBean,

让 Spring 在所有东西都准备好(也就是afterPropertiesSet)之后再叫我。**

讲正规一点就是:InitializingBean是 Spring 提供的一个生命周期接口,用于在 Bean 的所有属性注入完成后执行自定义初始化逻辑,其 afterPropertiesSet()方法由 Spring 容器自动调用。

四、**Map.of()**是干嘛的?

一句话:用来快速创建一个 Map(键值对)

以前你可能见过这种写法:

Map<String, String> map = new HashMap<>();
map.put("topic", "Java");

现在用 Map.of可以一行搞定:

Map<String, String> map = Map.of("topic", "Java");

使用时:成对传入,不可重复,只读

Book book  = chatClient
                .prompt("推荐好看的小说")
                .call().entity(Book.class);

一、entity() 内部到底干了什么?

你可以把 entity()看成这样一个隐藏流程

// 伪代码(非常接近真实源码)
1. 自动创建 BeanOutputConverter<T>
2. 从 Converter 里拿 JSON Schema
3. 把 Schema 悄悄塞进 Prompt
4. 调用 AI
5. 拿到 AI 返回的字符串
6. 调用 converter.convert(json)
7. 返回 Java 对象

但是entity() 不是万能的

如果 AI 返回的内容:

多了多余文字

JSON 不完整

忘了按格式输出

调用.entity()时就会直接爆出异常

二、BeanOutputConverter是干什么的?

BeanOutputConverter 的作用只有一个:

把 AI 返回的字符串(通常是 JSON),转换成你指定的 Java Bean


对ai而言:

AI 只会说 文本

即使它“看起来”返回了 JSON, 本质上还是 String类型

对Java而言,你想要的是:

Book book = new Book();
book.setName("xxx");

中间缺了什么?

一个能把 JSON 文本 → Java 对象的转换器

BeanOutputConverter 就是这个角色

String(AI 返回)

BeanOutputConverter

Java Object(Book)

对应到代码中:

        //创建一个 BeanOutputConverter转换器
        BeanOutputConverter<Book> bookBeanOutputConverter = new BeanOutputConverter<>(Book.class);

        //先用 getFormat()约束 AI 的输出
        String result = chatClient.prompt(promptTemplate.create(Map.of("format", bookBeanOutputConverter.getFormat()))).call().content();

        //再用 convert()把 AI 输出转成 Java 对象
        Book book = bookBeanOutputConverter.convert(result);

三、三种converter对比

Converter 是否可传类型 返回类型
BeanOutputConverter 支持 T
MapOutputConverter 不支持 Map<String,Object>
ListOutputConverter 不支持 List<String>

后两个返回类型不是泛型,只能处理一些简单情况,无法自定义指定实体类对象

这是 Spring AI 中获取 List<Book>的标准写法:

.entity(new ParameterizedTypeReference<List<Book>>() {})

代码示例:

 @RequestMapping("/convertList")
    public String convertList(){

       List<Book> books  = chatClient
                .prompt("推荐好看的小说")
                .call()
                .entity(new ParameterizedTypeReference<List<Book>>() {
                });

        return books.toString();
    }

四、短期记忆

1.直接手动拼接

把多轮对话历史扁平化为一个 Prompt,一次性发送给模型,由模型基于上下文生成下一次回复。

 List<Message> messages = new ArrayList<>();

        messages.add(new SystemMessage("你是一个旅行助手"));
        messages.add(new UserMessage("我想去新疆玩"));
        messages.add(new SystemMessage("好的,你准备什么时候去玩"));
        messages.add(new UserMessage("我要元旦去玩"));
        messages.add(new SystemMessage("你想玩什么"));
        messages.add(new UserMessage("我喜欢自然风光"));


        Prompt prompt = new Prompt(messages);
        return chatModel.call(prompt).getResult().getOutput().getText();

本质还是「一次 API 调用」

不管是:

单条 system + user还是 10 轮对话

底层都是一次 model.call(prompt)

LLM 本身没有记忆

所谓“多轮对话”,全是靠我自己把历史塞回去

2026.4.19

不容易,今天终于做完了苍穹外卖。之前一直觉得我的妈呀这么难这么难,要学这儿还要学那就一直打退堂鼓。直到今天做完之后,感觉也不是想象中那么高不可攀,最终也是花了21天拿下了。我觉得时至今日也无需去想为什么开始的这么晚云云,确定方向本事就需要考虑和打磨,这也算是我尝试许多选择的一条技术栈。从今以后就要全力拿下ai项目课,因为我对此并不熟悉,所以我也无法预估完成时间。我只能说尽力而为就好。


以后你看到任何 Spring AI 代码,只问自己 3 个问题

是不是 ChatClient?

有没有 Advisor?

Memory 是显式还是隐式?

只要这三点清晰,就 不会被继承链绕晕

一、Spring AI = ChatClient + 一堆“插件”

第 1 步:你发起请求

chatClient.prompt()
    .user("你好")
    .advisors(...)

👉 含义只有一句话:

“我要发一个请求,并且允许一些插件插手”


第 2 步:插件先动手(Before)

在你真正访问 LLM 之前

插件们会依次做事情,比如:

  • 打日志
  • 查数据库(读历史记忆)
  • 准备工具定义
  • 检查参数

👉你可以理解为:

for (插件 : 插件列表) {
    插件.before();
}

第 3 步:真正访问大模型

ChatModel.call()
ChatModel.stream()

👉 这一步只会发生一次(不考虑 retry / tool)


第 4 步:大模型返回结果

  • 普通回答
  • 或:要调用工具

第 5 步:插件再动手(After)

在返回给你之前

插件们会:

  • 处理工具调用
  • 把对话存进数据库(写记忆)
  • 打日志
  • 统计 token

👉 顺序是反过来的:

for (插件 : 插件列表倒序) {
    插件.after();
}

第 6 步:你拿到结果

.stream().content();

例子:

你这段代码:

chatClient
    .prompt()
    .user(message)
    .advisors(
        new MessageChatMemoryAdvisor(jdbcChatMemory)
    )
    .advisors(spec -> spec.param(ChatMemory.CONVERSATION_ID, chatId))
    .stream()
    .content();

翻译成人话就是:

  1. 我要发消息
  2. Memory 插件先查数据库(读)
  3. 发给 LLM
  4. LLM 回答
  5. Memory 插件再把这次对话写回数据库
  6. 返回前端

没有复杂继承

没有链式魔法

就是“插件前后插手”


二、advisor理解

  • 对象封装:进入 Advisor 链之前,请求会被封装为 ChatClientRequest;离开 Advisor 链之后,响应会被封装为 ChatClientResponse
  • 调用关系:Advisor 之间确实是依次调用的关系,形成了一个责任链(Chain of Responsibility)。

洋葱模型(The Onion Model)

这个流程通常被称为“洋葱模型”,其执行顺序如下:

  1. 请求阶段(正向传递)

    • 请求从第一个 Advisor 进入。
    • 依次经过 Advisor 1-> Advisor 2-> … -> 最后一个 Advisor
    • 最后一个 Advisor 负责调用底层的 ChatModel(如 OpenAI、Ollama)。

响应阶段(逆向返回)

  • ’ChatModel`返回响应。
  • 响应逆向经过 最后一个 Advisor-> … -> Advisor 2-> Advisor 1
  • 最终返回给调用者。

关键机制

  • ChatClientRequest& ChatClientResponse:这两个对象包裹了原始的 PromptChatResponse,并附带了一个非常重要的 AdvisedContext(上下文)。这个上下文可以在链中的不同 Advisor 之间共享数据。
  • 拦截与修改:在正向传递时,Advisor 可以修改请求(例如 MessageChatMemoryAdvisor把历史记录加到 Prompt 里);在逆向返回时,Advisor 可以修改响应(例如记录日志)。

所以,你看到的官方示意图描述的正是这个“先进后出”的栈式调用过程。


体测给我累死了,早上测得现在六点半了腿还疼,这两天似乎肾结石再现,我看起来不慌,其实已经吓坏了


三、@Qualifier

@Qualifier= 在多实现中选一个

在 Spring AI 里,它最常见的用途就是: 一个项目里同时存在多个 ChatModel

写法 1(推荐,清晰)

@Autowired
@Qualifier("ollamaChatModel")
private ChatModel ollamaChatModel;

写法 2(更简洁)

@Resource(name = "ollamaChatModel")
private ChatModel ollamaChatModel;

一、jdk,JVM是什么

JDK 是用来“写 + 跑”Java 的

JRE 是用来“只跑”Java 的

JVM (java 虚拟机)是它们俩共同依赖的“运行引擎”

二、动态代理之proxy

Proxy 是 JVM 在运行期生成的一个“替身对象”,

它实现了你的接口,

把所有方法调用转发给 InvocationHandler,

让你可以在不修改原代码的情况下增强行为

1. Proxy 是一个类

java.lang.reflect.Proxy

✅ 它是 JDK 自带的

✅ 专门用来 生成代理对象


2. Proxy 本身不是“代理对象”

Proxy.newProxyInstance(...)

Proxy是工厂

✅ 返回的那个对象才是真正的 Proxy 实例


3. Proxy 实例长什么样?

假设有接口:

interface UserService {
    void save();
}

JVM 在内存里生成了一个类:

final class $Proxy0 implements UserService {

    private InvocationHandler h;

    public $Proxy0(InvocationHandler h) {
        this.h = h;
    }

    public void save() throws Throwable {
        h.invoke(this,
                 UserService.class.getMethod("save"),
                 new Object[]{});
    }
}

✅ 这就是 Proxy 的真面目


4. Proxy 做了哪几件事?

实现接口

implements UserService

把所有方法调用“转发”

h.invoke(...)

不让调用直接进入真实对象,这是 AOP / 增强的关键

Proxy 是“框架为了增强行为,在运行期偷偷生成的对象”

new 是“你明确告诉 JVM:我要这个类的实例

5. Proxy vs 普通对象

普通对象 Proxy
编译期存在 运行期生成
方法直接执行 方法被拦截
难统一增强 极易增强
new 出来的 Proxy.newProxyInstance

三、动态代理

1.前提条件

JDK 动态代理只能代理「接口」

interface UserService {
    void save();
}

不能代理普通类, 只能代理接口

这对我来说还真挺重要,我的确不知道

2.调用流程

你调用 proxy.save()

JVM 生成的代理方法接管

把控制权交给 InvocationHandler

你在 invoke() 里加增强逻辑

(可选)调用真实对象的 save()

3.动态代理剧场 · 全对照版

(1)角色总表
生活故事 Java 机制 Spring AOP LangChain4j AI Service
明星 Interface 业务接口 Assistant 接口
经纪人 Proxy AOP 代理对象 AI Service Proxy
老板 InvocationHandler MethodInterceptor AiServiceInvocationHandler
明星本人 真实对象 原始 Bean 无(接口无实现)
唱歌 方法 save() / create() chat()

(2)你找明星唱歌

你(粉丝)发起请求

star.sing();
场景 Spring AOP AI Service
你写的代码 orderService.save() assistant.chat("你好")

✅ 你以为在直接调用

❌ 实际面对的是 经纪人


(3)经纪人拦截(Proxy)

实际执行的是

经纪人.sing();

对应 Java:

$Proxy0.sing();
场景 Spring AOP AI Service
代理对象 AOP Proxy AI Service Proxy
作用 拦截方法 拦截方法

(4)交给老板决策(InvocationHandler)
老板.invoke(唱歌请求);

对应代码:

invoke(proxy, method, args)

✅ Spring AOP 在这里干什么?

invoke() {
    before();          // 开启事务
    Object result = method.invoke(target);
    afterReturning();  // 提交事务
    return result;
}

✅ 事务 / 日志 / 鉴权 / 监控


✅ AI Service 在这里干什么?

invoke() {
    Prompt prompt = buildPrompt(args);
    addMemory(prompt);
    addTools(prompt);
    ChatResponse res = model.chat(prompt);
    return parseResult(res);
}

✅ 拼 Prompt

✅ 管理对话记忆

✅ 执行 Tool Calling

✅ 结构化输出


(5)是否让明星本人上场
明星.sing();
场景 Spring AOP AI Service
是否调用真实对象 ✅ 是 ❌ 否
原因 有业务实现 接口无实现

(6)一句话总结

**Spring AOP 的 Proxy 是“有明星本人的剧组”,

AI Service 的 Proxy 是“连明星都是临时凑出来的剧组”。**

2026 3.19

@Pathvariable:用来接收路径参数,把一个路径参数绑定给方法形参,如

请求参数:/emp/1
@GetMapping("/{id}")
    public Result getInfo(@PathVariable Integer id){
    、、、、、、
    }

关于Mybatis的xml映射文件

resultType(自动映射)

单表查询,且数据库列名(字段名) = Java属性名(如 name对 name)。

resultMap(手动映射)

列名 ≠ 属性名(如 user_name对 userName)

mybatis:
  configuration:
    map-underscore-to-camel-case: true

有嵌套对象/集合(如 Emp里包含一个 List

映射示例

    <resultMap id="empResultMap" type="com.itheima.pojo.Emp">
        <id column="id" property="id" />  //主键用id
        //都有两个参数,第一个是字段名,第二个是属性名
        <result column="username" property="username" /> //其余属性用result
        <result column="password" property="password" />
        <result column="name" property="name" />
        <result column="gender" property="gender" />
        <result column="phone" property="phone"/>
        <result column="job" property="job"/>
        <result column="salary" property="salary"/>
        <result column="image" property="image" />
        <result column="entry_date" property="entryDate" />
        <result column="dept_id" property="deptId" />
        <result column="create_time" property="createTime" />
        <result column="update_time" property="updateTime" />
        
        <!--封装exprList-->
        <collection property="exprList" ofType="com.itheima.pojo.EmpExpr">//集合用collection
            <id column="ee_id" property="id"/>
            <result column="ee_company" property="company"/>
            <result column="ee_job" property="job"/>
            <result column="ee_begin" property="begin"/>
            <result column="ee_end" property="end"/>
            <result column="ee_empid" property="empId"/>
        </collection>
    </resultMap>

   #具体数据库操作语句
    <select id="getById" resultMap="empResultMap"> 
        select e.*,
               ee.id ee_id,
               ee.emp_id ee_empid,
               ee.begin ee_begin,
               ee.end ee_end,
               ee.company ee_company,
               ee.job ee_job
            from emp e left join emp_expr ee on e.id = ee.emp_id
            where e.id=#{id}
    </select>

杂项

CollectionUtils工具类常用来处理集合参数、结果集等

if(!CollectionUtils.isEmpty(exprList))      //判断集合非空

forEach()是集合遍历方法,可对集合内每一个元素执行括号里的操作。

exprList.forEach(empExpr -> empExpr.setEmpId(emp.getId())); 

json格式的数据要想封装到对象中,要加注解@RequestBody

public Result update(@RequestBody Emp emp){
、、、
}

+ —— 用于 UPDATE 语句

动态拼接 SET子句,自动处理逗号(避免最后多一个逗号),常用于更新部分字段。

<update id="updateById">
    UPDATE emp
    <set>
        <if test="username != null and username != ''">username = #{username},</if>
        <if test="password != null and password != ''">password = #{password},</if>
        <if test="name != null and name != ''">name = #{name},</if>
        <if test="gender != null">gender = #{gender},</if>
        <if test="phone != null and phone != ''">phone = #{phone},</if>
        <if test="job != null">job = #{job},</if>
        <if test="salary != null">salary = #{salary},</if>
        <if test="image != null and image != ''">image = #{image},</if>
        <if test="entryDate != null">entry_date = #{entryDate},</if>
        <if test="deptId != null">dept_id = #{deptId},</if>
        <if test="updateTime != null">update_time = #{updateTime}</if>
    </set>
    WHERE id = #{id}
</update>

+ —— 用于 SELECT / UPDATE / DELETE 语句

动态拼接 WHERE子句,自动处理开头的 ANDOR,避免语法错误。

<select id="findByCondition" resultType="Emp">
    SELECT * FROM emp
    <where>
        <if test="username != null and username != ''">AND username = #{username}</if>
        <if test="gender != null">AND gender = #{gender}</if>
        <if test="deptId != null">AND dept_id = #{deptId}</if>
    </where>
</select>

2026 3.20

数据库出现group by分组字段后,select后只能使用分组字段和聚合函数

数据库的 if 流程控制函数

if(expr,val1,val2):如果表达式expr成立,取val1,否则取val2

select
         if(gender=1,'男性员工','女性员工') name,
         count(*) value
          from emp group by gender

case when条件判断

CASE WHEN 条件 THEN 结果 [ELSE 其他] END

作用:多条件分支判断,按条件返回对应值,可替代简单 IF嵌套。

SELECT CASE WHEN score>=90 THEN '优'

WHEN score>=60 THEN '及格'

ELSE '不及格' END FROM stu;

昨天下载了前端项目内容,今天处理了之前所有的前后端联调测试,在1~2天内要完成班级接口,学员接口的创建,然后再开后面的登录操作内容。

2026.3.21

今天开始班级接口和学生接口的开发;汇总一下问题

1.新增班级接口下拉班主任列表无选项,apifox键入emp的id后成功录入;

2.无法批量删去学生(已解决)

死于没有仔细看接口文档

@DeleteMapping("/students/{ids}")   //把{ids}没写

还有要搞清楚常用注解的使用:

这里先留一下,做完接口统一汇总一下


用户第一次访问服务器时,服务器为他创建一个 Session 并存入数据,同时生成一个唯一的 SessionID,通过 Set-Cookie发给浏览器保存;之后浏览器每次访问都带上这个 SessionID(把它保存成cookie),服务器用它找到对应的 Session,读取里面的数据,从而实现会话级别的身份或状态保持。

jwt令牌

  1. JWT令牌由哪几个部分组成 ,每个部分都存储什么内容?
    • header (头) ,记录令牌类型、签名算法
    • payload (载荷) ,携带一些自定义的信息
    • signature (签名) ,访问被篡改,保证安全性
  2. JWT令牌生成及校验?
    • Jwts.builder()…
    • Jwts.parser()…
  3. JWT令牌解析(校验)时什么情况会报错?
    • JWT令牌被篡改 或 过期失效了
  4. 注意事项
    • JWT校验时使用的签名秘钥,必须和生成JWT令牌时使用的秘钥是配套的

2026.3.21

过滤器

  1. 过滤器的执行流程

放行前 -> 放行 -> 资源 -> 放行后

  1. 配置的过滤器的拦截路径 /*/emps/*分别代表什么意思?

/*:表示拦截所有

/emps/*:表示目录拦截,拦截 /emps`下的所有资源

  1. 什么是过滤器链?

当一个请求进入 Web 应用时,容器(如 Tomcat)会根据过滤器的配置顺序(或注解的 @Order优先级),将请求传递给第一个过滤器。

每个过滤器的处理逻辑分为三个阶段:

放行前(Pre-processing):过滤器执行自己的逻辑(如权限校验、字符编码设置、Token 验证等)。

放行(Forward):调用 filterChain.doFilter(request, response),将请求传递给下一个过滤器(如果没有下一个过滤器,则传递给目标资源,如 Servlet/Controller)。

放行后(Post-processing):目标资源处理完请求后,响应会沿着过滤器链反向返回,此时过滤器可以执行“收尾逻辑”(如日志记录、响应头修改等)。

拦截器

@Override
public void addInterceptors(InterceptorRegistry registry) {
    registry.addInterceptor(demoInterceptor)
            .addPathPatterns("/**")       // 需要拦截的资源(任意级路径)
            .excludePathPatterns("/login"); // 不需要拦截的资源
}

拦截路径规则表:

拦截路径 含义 举例(匹配/不匹配)
/* 一级路径 匹配 /depts/emps;不匹配 /depts/1
/** 任意级路径 匹配 /depts/depts/1/depts/1/2
/depts/* /depts下一级路径 匹配 /depts/1;不匹配 /depts/1/2/depts
/depts/** /depts下任意级路径 匹配 /depts/depts/1/depts/1/2;不匹配 /emps/1

拦截器是Spring提供的技术,它只会拦截对于Spring当中资源的请求;过滤器不是Spring提供的技术,是sevlet提供的规范,它可以拦截项目中的所有资源。同时存在过滤器和拦截器时,先处理过滤器,再处理拦截器。

2026.3.25

Spring AOP概念

Spring AOP 的核心思想就是:在不修改原有业务代码的前提下,通过定义切入点(Pointcut)锁定目标方法,然后在切面(Aspect)中统一添加额外功能。

记忆(区别)方法:

连接点 = 全集,切入点 = 子集

连接点是“候选名单”,切入点是“最终名单

概念 通俗说法 技术含义
连接点 Join Point 有哪些地方能被切 程序中可被拦截的点(主要是方法执行)
切入点 Pointcut 我到底切哪些地方 从连接点中选出的具体匹配规则
通知 Advice 切进去后做什么 在切入点处执行的增强逻辑

动态代理

特点 说明
代理类不是手写 由 JVM / 框架在运行时动态生成字节码,生成 .class 文件
通用性强 同一个代理工具可以为很多不同的接口/类生成代理
常用于 AOP Spring AOP 就是用动态代理来实现“在不改源码的情况下增强方法功能”
两种主流实现 JDK 动态代理(基于接口)、CGLIB(基于继承,可代理普通类)

动态代理就是:在程序运行过程中,“当场捏”出一个代理对象,让它帮你干活并顺便加点额外操作。

切入点表达式:execution

在 Spring AOP 里,切入点表达式写在 @Pointcut(...)注解里,决定了切面作用的“目标范围”。

语法骨架:

execution([修饰符] 返回值类型 包名.类名.方法名(参数列表) [throws 异常])

各部分含义:

部分 写法示例 说明
修饰符 publicprivate(可选) 一般不写,默认匹配任意访问修饰符
返回值类型 *(任意)、voidString *表示任意返回值
包名 com.example.service.* *匹配一层,..匹配多层
类名 *ServiceUserService *匹配任意类名
方法名 *(任意)、save*login *匹配任意方法
参数列表 ()(String)(..) ..表示任意参数个数和类型
异常 throws Exception(可选) 一般省略
// 匹配 com.example.service 包下所有类的所有方法
execution(* com.example.service.*.*(..))

// 匹配所有以 save 开头、任意参数的方法
execution(* com.example.service.*.save*(..))

// 匹配返回值为 String 的方法
execution(String com.example.service.UserService.getUsername(..))

可以用 &&(与)、||(或)、!(非)组合多个表达式。

// 在 service 包中,且方法上有 @Log 注解
@Pointcut("within(com.example.service..*) && @annotation(com.example.annotation.Log)")

JoinPoint

JoinPoint 是 Spring AOP 中所有方法执行点的抽象,是切入点的候选集,在通知中通过它可以获取方法签名、参数、目标对象等信息。

作用:在通知方法里声明 JoinPoint joinPoint,常用 API:

方法 作用
getSignature() 获取方法签名(返回类型、方法名、参数类型)
getArgs() 获取方法实参数组
getTarget() 获取被代理的真实对象(目标对象)
getThis() 获取代理对象本身
getSourceLocation() 获取源代码位置(行号等,调试用)

2026.3.31更新:

joinPoint.getTarget()得到的是“被增强的业务对象(Mapper/Service)”,

joinPoint.getArgs()[0]才是“方法调用时传入的实体对象”,

自动填充公共字段必须操作实体对象,因此不能直接使用 getTarget()


今天终于完成了tlias项目的实现!现在就剩下原理篇,还剩下linux的配置,docker的部署

2026.3.26

配置文件优先级:properties>yml>yaml

+两种外部配置:命令行>java系统属性>properties>yml>yaml

部署 Java Web 应用(Spring Boot JAR 包)

系统:CentOS 7+(使用 yumsystemctl

应用:Spring Boot 打包好的 tlias-web-management.jar

目标:部署到服务器,后台运行,开机自启,开放端口,日志可追踪


一、准备环境

  1. 更新系统 & 安装基础工具
yum update -y
yum install -y vim wget curl net-tools lsof

  1. 安装 JDK 17
cd /usr/local   #目录
wget https://download.oracle.com/java/17/latest/jdk-17_linux-x64_bin.tar.gz

如果 wget不能用,可在本机下载后 scp上传到服务器。

我选择直接把准备好的jdk文件拖动到了FinalShell底栏的文件列表对应的目录

解压并配置环境变量

tar -zxvf jdk-17_linux-x64_bin.tar.gz     #文件操作相关知识点
mv jdk-17.* jdk-17   #移动操作

编辑 /etc/profile

vim /etc/profile     #vim进行文本编辑,编辑对象为目录所指对象

在末尾添加:

export JAVA_HOME=/usr/local/jdk-17   
export PATH=$JAVA_HOME/bin:$PATH     #配置环境变量

使配置生效

source /etc/profile   #使环境变量配置生效
java -version  #检查jdk是否安装成功

应输出 java version "17.x.x"


二、上传并放置应用 JAR

  1. 创建应用目录
mkdir -p /usr/local/tlias-app   
cd /usr/local/tlias-app
  1. 上传 JAR 包
scp /本地路径/tlias-web-management.jar root@服务器IP:/usr/local/tlias-app/
  1. 查看文件
ls -lh

确认 tlias-web-management.jar存在。


三、测试运行应用

前台启动(调试用)

java -jar tlias-web-management.jar

观察控制台输出是否有报错。

Ctrl+C停止。


四、正式部署(后台运行 + 日志)

  1. 后台运行并记录日志
nohup java -jar tlias-web-management.jar > tlias.log 2>&1 &

nohup:退出终端不中断进程

> tlias.log 2>&1:标准输出和错误都写入 tlias.log

&:后台运行

  1. 查看进程
ps -ef | grep java

应看到类似:

root 12345 1 0 10:00 ? 00:00:30 java -jar tlias-web-management.jar
  1. 查看实时日志
tail -f tlias.log

Ctrl+C退出跟踪。


五、开放应用端口(后端8080,前端80)

  1. 检查 firewalld 状态
systemctl status firewalld

若未运行:

systemctl start firewalld
systemctl enable firewalld
  1. 开放 8080 端口
firewall-cmd --zone=public --add-port=8080/tcp --permanent
firewall-cmd --reload
firewall-cmd --zone=public --list-ports

输出应包含 8080/tcp

nginx -t
systemctl start nginx
systemctl enable nginx
firewall-cmd --add-port=80/tcp --permanent
firewall-cmd --reload

六、部署完成验证

浏览器访问:

http://服务器IP:8080

或通过 Nginx:

http://服务器IP

查看日志无报错:

tail -f /usr/local/tlias-app/tlias.log

docker理解

我们可以把 Docker 想象成一种**“打包+快递+隔离送货”**的服务,用三个生活场景来理解:

1. 「镜像」→ 预制好的“外卖套餐”

比如你要吃一份“宫保鸡丁盖饭”,餐厅提前把鸡肉、花生、米饭、调料、烹饪步骤都打包成一个“标准化套餐”(镜像)。这个套餐里包含了所有需要的“东西”(应用代码、运行环境、配置文件、依赖库等),拿到这个套餐,你就有了做/吃这份饭的全部条件。

对应到 Docker:镜像是**“打包好的应用+运行环境”**,比如 MySQL 镜像里,已经包含了 MySQL 软件、它的运行依赖、配置模板等,相当于“开箱即用”的软件包。

2. 「容器」→ 打开套餐后的“用餐环境”

你拿到“宫保鸡丁套餐”后,会在自己的餐桌上(宿主机的某个空间)拆开,开始吃饭。这个“餐桌+餐具+你用餐的过程”就是容器——它是镜像运行起来的“隔离环境”。

对应到 Docker:容器是**“镜像运行时的实例”**,每个容器都有自己独立的空间(比如独立的文件系统、网络、进程),就像你在自己家吃饭,和邻居家的用餐环境互不干扰。即使你同时开了 3 个“宫保鸡丁容器”,它们的食材、用量也不会互相影响。

3. 「镜像仓库」→ 外卖平台的“商家列表”

你需要点外卖时,会去美团、饿了么(镜像仓库,比如 DockerHub)找“宫保鸡丁商家”。仓库里存着无数个“预制套餐”(各种镜像,比如 MySQL、Python、Nginx 的镜像),你可以直接下载(拉取)别人做好的镜像来用。

对应到 Docker:镜像仓库是**“存储和分发镜像的平台”**,DockerHub 是最大的公共仓库,里面有各种现成的镜像,你也可以把自己的镜像传到私有仓库(比如公司内部的仓库)分享给别人。


docker run 常见参数

-d:后台运行容器( detached 模式,容器在后台默默工作,不占用当前终端)。

--name:给容器起唯一名字(方便后续管理,比如 docker stop 名字停止容器)。

-e:设置环境变量(给容器内的程序传递配置,比如 MySQL 的 root 密码、时区)。

-p端口映射(把“宿主机的端口”和“容器内的端口”关联起来,让外部能访问容器内的服务)。

镜像名称结构:Repository:TAG

Repository:镜像名(比如 mysql是镜像名,代表 MySQL 这个软件的镜像)。

TAG:版本号(比如 8代表 MySQL 8 版本,默认是 latest即最新版)。

示例:

docker run -d \
  --name mysql8 \
  -p 3307:3306 \
  -e TZ=Asia/Shanghai \
  -e MYSQL_ROOT_PASSWORD=123456 \
  mysql:8

端口映射理解

你去奶茶店买奶茶(访问服务),奶茶店的“制作间”(容器)里有奶茶机(服务)用的是 8080端口,但顾客不能直接进制作间。于是奶茶店在门口(宿主机)开了一个窗口,告诉顾客:“点单请拨窗口号 9000(宿主机端口),我们把订单转到制作间的 8080端口来制作”。这里 9000:8080就是端口映射,让顾客(外部)能通过窗口(宿主机端口)间接使用制作间的服务(容器端口)。

Docker 常见命令

1. 镜像相关

命令 说明 实例
docker pull <镜像名:标签> 拉取镜像 docker pull mysql:8
docker images 查看本地镜像 docker images
docker rmi <镜像ID/名> 删除镜像 docker rmi mysql:8

2. 容器生命周期

命令 说明 实例
docker run [参数] <镜像> 创建并启动容器 docker run -d --name mysql -p 3307:3306 -e MYSQL_ROOT_PASSWORD=123 mysql:8
docker start <容器名/ID> 启动已停止容器 docker start mysql
docker stop <容器名/ID> 停止容器 docker stop mysql
docker restart <容器名/ID> 重启容器 docker restart mysql
docker rm <容器名/ID> 删除容器 docker rm mysql
docker ps 查看运行中的容器 docker ps
docker ps -a 查看所有容器(含已停止) docker ps -a

3. 容器操作

命令 说明 实例
docker exec -it <容器名/ID> <命令> 进入容器执行命令 docker exec -it mysql bash
docker logs <容器名/ID> 查看容器日志 docker logs mysql
docker logs -f <容器名/ID> 实时查看日志 docker logs -f mysql
docker inspect <容器名/ID> 查看容器详细信息 docker inspect mysql

数据卷命令速查表(含挂载方式)

1. 数据卷管理

命令 说明 实例
docker volume create <卷名> 创建数据卷 docker volume create mysql_data
docker volume ls 查看所有数据卷 docker volume ls
docker volume inspect <卷名> 查看数据卷详情 docker volume inspect mysql_data
docker volume rm <卷名> 删除数据卷 docker volume rm mysql_data
docker volume prune 清理未使用的数据卷 docker volume prune

3. 实用组合示例

① 具名卷 + 端口映射

docker run -d \
  --name mysql \
  -p 3307:3306 \
  -v mysql_data:/var/lib/mysql \
  -e MYSQL_ROOT_PASSWORD=123 \
  mysql:8

② 绑定挂载配置文件

docker run -d \
  --name nginx \
  -p 80:80 \
  -v /home/user/nginx.conf:/etc/nginx/nginx.conf:ro \
  nginx:latest

③ 匿名卷 + 日志持久化

docker run -d \
  --name redis \
  -v /var/log/redis \
  redis:7

2026.3.28

可以把 Dockerfile 自定义镜像 理解成:

👉 给一台“空白小电脑”写一份“装机 + 配置 + 启动说明书”,以后谁拿到这份说明书,都能一键复刻出一模一样的运行环境。

下面我用「生活类比 + 简单示例」给你讲清楚。


一、先用一句话理解 Dockerfile

Dockerfile = 镜像的源代码

docker build = 按 Dockerfile 编译出一个镜像

docker run = 用镜像启动一个容器(运行中的程序)


二、把 Dockerfile 想象成“做菜食谱”

假设你要在服务器上跑一个 Python 程序:

你写一个 Dockerfile,相当于:

“我要一台 Ubuntu 机器

装 Python 3.9

装 requests、flask

把我的代码放进去

启动时运行 app.py”

别人只要执行:

docker build -t myapp .
docker run myapp

不管在哪台机器,环境一模一样


三、Dockerfile 里每一行都在干嘛

# 选基础镜像(相当于买一台裸机)
FROM python:3.9-slim

#  设置工作目录(相当于 cd 到某个文件夹)
WORKDIR /app

#  拷贝依赖清单
COPY requirements.txt .

# 安装依赖(pip install)
RUN pip install -r requirements.txt

#  拷贝代码
COPY . .

#  暴露端口(告诉别人我用了 80)
EXPOSE 80

# 启动命令(容器启动时干啥)
CMD ["python", "app.py"]

对应现实世界:

Dockerfile 现实生活
FROM 买什么系统的电脑
WORKDIR 在哪个文件夹干活
RUN 安装软件
COPY 把文件拷进去
CMD 开机自动运行的程序

四、为什么要“自定义镜像”?

因为官方镜像太“干净”了,只满足通用需求。

比如:

python镜像:只有 Python

没有你的代码

没有你的依赖

没有你的配置文件

自定义镜像 = 官方镜像 + 你的东西


五、镜像 vs 容器

一句话区分:

镜像 = 模板(只读)

容器 = 镜像跑起来后的实例

类比:

镜像 = Windows 安装 ISO

容器 = 已经装好正在用的 Windows 系统

**Dockerfile 就是告诉 Docker:

从一个干净系统开始,一步步把它变成我能直接跑的环境,并且这个环境可以随时复制、随处运行。**


docker自定义网络为什么能用容器名互相访问?

你住在一个小区里,有两户人家:

老王家,老李家

他们想互相串门,有两种方式:

  1. 记门牌号

    老王知道老李家是 3栋502,于是他每次都去 3栋502 找老李。

    → 这就像用 IP 地址 访问,能找到,但记数字很烦。

  2. 喊名字

    老王在小区里大喊:“老李!出来玩!”

    物业(小区广播)听到后,直接去把老李叫出来。

    → 这就像用 容器名字 访问,喊一声就行,不用管他住几栋几楼。


把“小区”换成 Docker 网络

默认网络 的小区:

没有物业广播,邻居之间只能靠“门牌号”找人,不能喊名字。

所以你要用 ping 172.17.0.2这种 IP 才能找到对方。

自定义网络 的小区:

有物业(内置 DNS),你一喊“老李”,物业马上告诉你老李的门牌号,然后你就能找到他。

所以在容器里,你直接 ping 老李就能通。

2026.3.29

Docker Compose


一、是什么

用于定义和运行多容器 Docker 应用

通过 docker-compose.yml 统一管理服务、网络、数据卷


二、核心概念

Project:一组服务(默认以目录名命名)

Service:应用组件(Web、DB 等)

Container:服务实例

Volume:数据持久化

Network:服务间通信


三、docker-compose.yml 结构

version: '3.8'

services:
  web:
    image: nginx
    ports: ["80:80"]
    volumes: ["./html:/usr/share/nginx/html"]
    depends_on: [db]
    restart: unless-stopped
    networks: [app-net]

  db:
    image: mysql:8
    environment:
      MYSQL_ROOT_PASSWORD: root
    volumes: [db_data:/var/lib/mysql]
    networks: [app-net]

volumes:
  db_data:

networks:
  app-net:

常用配置项

image/ build:镜像来源或构建

ports:端口映射

volumes:数据挂载

environment/ env_file:环境变量

depends_on:启动顺序

restart:重启策略

networks:网络归属

healthcheck:健康检查


四、常用命令

启停管理

docker-compose up -d       # 启动
docker-compose down        # 停止并清理
docker-compose restart     # 重启
docker-compose ps          # 查看状态

日志 & 调试

docker-compose logs -f
docker-compose logs web
docker-compose exec web sh

构建 & 维护

docker-compose build
docker-compose pull
docker-compose config

六、多环境

多文件叠加:

docker-compose -f base.yml -f prod.yml up

使用 profiles

services:
  debug:
    profiles: ["dev"]