Lucaju 的技术笔记 - 框架与中间件
type: channel
link: https://www.lucaju.cn/index.php/category/framework/
description: Spring 全家桶（Spring Boot、Spring Cloud、Spring Security）、MyBatis/MyBatis-Plus 实战、Redis 缓存设计（过期策略、分布式锁）、消息队列（RabbitMQ/Kafka 使用场景与问题排查）等
items: Jakarta Validation 优雅实现参数校验：从基础使用到自定义扩展
items: Spring Retry 重试机制：优雅解决接口调用失败问题
items: 玩转 MyBatis-Plus 多数据源配置，Spring Boot 项目快速实现多库操作
items: Java 实战：基于 WebSocket 获取大模型流式输出并转为 Flux
items: Spring AI Tool 工具方法调用源码深度解析：从流式交互到工具执行全流程
items: Spring AI 实战：调用本地 Tool 工具方法实现大模型能力扩展
items: Spring AI 无法获取大模型深度思考内容？解决方案来了
items: 在 Ubuntu 环境下安装与配置 Nginx 的完整指南
items: 自定义 Spring-Boot-Starter 结合 TrueLicense 实现证书授权拦截
items: 响应式编程学习笔记

Jakarta Validation 优雅实现参数校验：从基础使用到自定义扩展
type: item
date: 2026-02-03T16:38:20+08:00
description: 在后端开发中，参数校验是保障接口安全性和数据合法性的核心环节，硬编码的if-else校验逻辑不仅繁琐冗余，还会让代码可读性大打折扣。Jakarta Validation（原Java Validation）为我们提供了一套轻量、优雅的注解式参数校验方案，通过标准化的注解即可实现各类参数校验规则，大幅简化开发流程。本文将从基础注解使用、实战场景落地、统一异常处理到自定义校验注解，全方位讲解Jakarta Validation的使用技巧，结合实际业务代码示例，让你快速上手并灵活运用到项目中。一、核心校验注解速查Jakarta Validation提供了一系列开箱即用的校验注解，覆盖空值、长度、格式、数值、日期等绝大多数日常校验场景，核心注解及功能如下表所示，可直接作为开发速查手册：注解适用类型核心功能@NotNull所有类型字段值不能为null@NotBlank字符串不能为null，且去除首尾空格后长度大于0@NotEmpty字符串/集合/数组不能为null，且长度/元素个数大于0@Size(min, max)字符串/集合/数组长度/元素个数在[min, max]范围内@Pattern(regexp)字符串必须匹配指定的正则表达式@Email字符串必须符合合法的邮箱格式（支持自定义正则）@Min(value)数值类型数值必须大于等于value@Max(value)数值类型数值必须小于等于value@Positive数值类型必须为正数（大于0）@Negative数值类型必须为负数（小于0）@PositiveOrZero数值类型必须为正数或0@NegativeOrZero数值类型必须为负数或0@Future日期/时间类型必须是未来的时间@FutureOrPresent日期/时间类型必须是未来或当前时间@Past日期/时间类型必须是过去的时间@PastOrPresent日期/时间类型必须是过去或当前时间注解使用小技巧@NotBlank/@NotEmpty/@NotNull 区分：字符串优先用@NotBlank（过滤空白字符），集合/数组用@NotEmpty，非字符串非集合类型用@NotNull；注解组合使用：实际业务中可组合多个注解，如用户账号需同时满足「非空、正则匹配、长度限制」；默认提示语自定义：所有注解都支持message属性，用于自定义校验失败的提示信息，贴合业务场景。二、实战场景落地：三种核心使用方式Jakarta Validation的注解可根据参数传递方式灵活使用，核心分为「简单参数直接注解」「实体对象属性注解」两种核心场景，后者是项目中最常用的方式。场景1：GET请求简单参数，直接注解参数对于GET请求的URL拼接参数（如/user/get?name=test&id=1），可直接在接口方法的参数前添加校验注解，适用于参数数量少的简单场景。/** * 根据用户名和ID查询用户 * @param name 用户名（不能为空） * @param id 用户ID（必须为正数） * @return 用户信息 */ @GetMapping("/get") public CommonResult<UserVO> getUser( @NotBlank(message = "用户名不能为空") String name, @Positive(message = "用户ID必须为正数") Long id ) { UserVO user = userService.getByNameAndId(name, id); return CommonResult.success(user); }场景2：POST请求实体参数，注解+@Valid 触发校验对于POST/PUT请求，参数通常封装为实体对象（如新增/编辑用户的入参），只需在实体的属性上添加校验注解，并在接口方法的实体参数前添加@Valid（或@Validated）注解，即可触发整体校验逻辑。这是项目中最常用的方式，适合复杂参数的校验场景。步骤1：实体类添加校验注解@Data @Schema(description = "新增用户请求参数") public class UserSaveReqVO { @Schema(description = "用户编号（编辑时传，新增时不传）", example = "1024") private Long id; @Schema(description = "用户账号", requiredMode = Schema.RequiredMode.REQUIRED, example = "yudao") @NotBlank(message = "用户账号不能为空") @Pattern(regexp = "^[a-zA-Z0-9]+$", message = "用户账…

Spring Retry 重试机制：优雅解决接口调用失败问题
type: item
date: 2026-01-16T17:04:00+08:00
description: 在日常开发中，我们经常会遇到第三方接口不稳定、网络抖动导致的调用失败场景。很多人第一反应是在 try-catch 里写 for 循环重试，再搭配 Thread.sleep() 控制间隔——这种写法不仅冗余，还难以维护。今天给大家推荐 Spring Retry 框架，它基于 AOP 实现，能让你零侵入式地为方法添加重试功能，大幅简化代码！一、快速上手：三步集成 Spring Retry1. 添加 Maven 依赖Spring Retry 核心依赖 + AOP 依赖（因为其底层是 AOP 实现），这里推荐使用 2.0.12 稳定版本： <dependency> <groupId>org.springframework.retry</groupId> <artifactId>spring-retry</artifactId> <version>2.0.12</version> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-aop</artifactId> </dependency>2. 启用 Spring Retry 功能在 Spring Boot 主启动类上添加 @EnableRetry 注解，一键开启重试功能：import org.springframework.retry.annotation.EnableRetry; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; @EnableRetry // 启用重试功能 @SpringBootApplication public class SpringRetryDemoApplication { public static void main(String[] args) { SpringApplication.run(SpringRetryDemoApplication.class, args); } }3. 核心注解：@Retryable 标记重试方法在需要重试的方法上添加 @Retryable 注解，即可实现重试逻辑。基础用法import org.springframework.retry.annotation.Retryable; import org.springframework.stereotype.Service; @Service public class RetryDemoService { // 标记该方法需要重试 @Retryable public void basicRetry() { int random = (int) (Math.random() * 10); System.out.println("当前随机数：" + random); // 模拟异常：随机数为偶数时抛出异常 if (random % 2 == 0) { throw new RuntimeException("随机数为偶数，触发异常"); } System.out.println("方法执行成功！"); } }基础用法说明未指定异常类型时，方法抛出任何异常都会触发重试。默认重试次数：3次（包含首次执行，实际重试 2 次）。默认重试间隔：1秒。当重试次数耗尽仍失败时，会抛出 ExhaustedRetryException 异常。二、进阶配置：灵活定制重试策略@Retryable 注解提供了丰富的属性，可根据业务需求精准控制重试逻辑。1. @Retryable 核心属性说明属性名作用示例value/retryFor指定触发重试的异常类型retryFor = RuntimeException.classinclude同 value，优先级更高include = {NullPointerException.class}exclude指定不触发重试的异常类型exclude = IllegalArgumentException.classmaxAttempts最大重试次数（包含首次执行）maxAttempts = 5backoff配置重试间隔策略@Backoff(delay = 1000, multiplier = 2)stateful是否有状态重试（异常信息保留）stateful = true2. 实战示例：指数退避重试需求：调用第三方接口时，仅在抛出 RuntimeException 时重试，最大重试 5 次，重试间隔按 1s → 2s → 4s …

玩转 MyBatis-Plus 多数据源配置，Spring Boot 项目快速实现多库操作
type: item
date: 2026-01-13T11:23:55+08:00
description: 在实际开发中，我们经常会遇到一个 Spring Boot 项目需要操作多个数据库的场景，比如从 Oracle 拉取数据同步到 MySQL。如果手动管理多个数据源的连接和切换，不仅开发效率低，还容易出现数据源混淆的问题。而 MyBatis-Plus 提供的 dynamic-datasource-spring-boot-starter 依赖，能够帮助我们快速实现多数据源的配置和动态切换，极大简化多库操作的开发流程。本文就来详细讲解 Spring Boot 整合 MyBatis-Plus 多数据源的具体实现步骤。一、核心依赖引入首先，我们需要在项目的 pom.xml 文件中引入 MyBatis-Plus 多数据源的核心依赖。这里以 3.1.0 版本为例（可根据项目实际情况选择兼容版本）：<dependency> <groupId>com.baomidou</groupId> <artifactId>dynamic-datasource-spring-boot-starter</artifactId> <version>4.2.0</version> </dependency>注意：如果项目中已经引入了 MyBatis-Plus 的核心依赖，无需重复引入，该 starter 已包含相关依赖。二、多数据源配置（application.yml）接下来，在 Spring Boot 的核心配置文件 application.yml 中配置多个数据源的连接信息。本文以 1 个 MySQL 数据源 + 2 个 Oracle 数据源为例，配置如下：spring: datasource: dynamic: # 设置默认的数据源，默认数据源的 key 需与下方 datasource 中的配置一致 primary: mysql-ocr # 严格模式：默认 true，未匹配到指定数据源时抛异常，false 则使用默认数据源 strict: true # 配置多个数据源，key 自定义（建议与业务相关，便于区分） datasource: # MySQL 数据源：ocr 业务库 mysql-ocr: url: jdbc:mysql://ip:port/ocr?useUnicode=true&characterEncoding=UTF-8&serverTimezone=Asia/Shanghai username: username password: password driver-class-name: com.mysql.cj.jdbc.Driver # Oracle 数据源1：orcl 业务库 oracle-orcl: url: jdbc:oracle:thin:@ip:port:helowin username: username password: password driver-class-name: oracle.jdbc.OracleDriver # Oracle 数据源2：cbxx 业务库 oracle-cbxx: url: jdbc:oracle:thin:@ip:port:helowin username: username password: password driver-class-name: oracle.jdbc.OracleDriver配置参数说明参数作用primary指定默认数据源，当不指定数据源时，默认使用该配置的数据源strict严格模式开关，开启后若调用不存在的数据源会抛出异常，关闭则默认使用主数据源datasource多数据源的具体配置节点，每个子节点对应一个数据源，key 为自定义的数据源名称三、Mapper 层目录结构与数据源注解配置为了更清晰地管理不同数据源对应的 Mapper 接口，我们可以按照数据源划分包结构，同时通过 @DS 注解指定 Mapper 对应的数据源。1. Mapper 层目录结构推荐按照数据源名称创建独立的包，将不同数据源的 Mapper 接口分类存放，便于后期维护：src/main/java/com/xxx/mapper ├── cbxx // oracle-cbxx 数据源对应的 Mapper 包 │ └── VOcrCbxxMapper.java ├── ocr // mysql-ocr 数据源对应的 Mapper 包 │ └── OcrCmMapper.java └── orcl // oracle-orcl 数据源对应的 Mapper 包 └── VOcrMeterMapper.java2. @DS 注解指定数据源@DS 注解是 MyBatis-Plus 多数据源的核心注解，用于指定当前 Mapper 接口或方法对应的数据源，支持类级别和方法级别，遵循就近原则（方法上的注解优先级高于…

Java 实战：基于 WebSocket 获取大模型流式输出并转为 Flux
type: item
date: 2025-09-30T18:05:00+08:00
description: 在大模型问答场景中，流式输出已成为提升用户体验的关键技术 —— 它能让模型生成的内容 “逐段呈现”，避免用户长时间等待完整结果。目前行业内最主流的实现方式是HTTP Chunked 编码，几乎所有大模型官网的示例代码都采用这种方案。但最近我遇到了一个特殊的对接需求：必须通过WebSocket接收流式输出。虽然这种方式不常用，但梳理清楚实现逻辑后发现，它在长连接稳定性、双向通信灵活性上有独特优势。于是整理了这篇实战指南，希望能帮到有同样需求的开发者。一切都是最好的安排～一、核心需求与技术选型1. 需求背景需通过 WebSocket 协议与大模型服务建立连接，实时接收流式输出项目前端已适配 HTTP 流式输出的 Flux 格式，为减少前端改动，需将 WebSocket 接收的流转为 Flux 响应需处理连接异常、消息结束等边界场景，保证流传输的稳定性2. 关键技术依赖实现核心是 Spring 生态中的ReactorNettyWebSocketClient，它基于响应式编程模型，能天然适配 Flux 的流处理特性，主要依赖如下（Spring Boot 项目可直接引入）： 我使用的是jdk17 <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-webflux</artifactId> </dependency> <dependency> <groupId>io.projectreactor.netty</groupId> <artifactId>reactor-netty-core</artifactId> </dependency>二、完整实现代码与逻辑解析下面是从 WebSocket 连接建立、流式消息接收，到转为 Flux 的完整代码，每一步都附带详细注释：import lombok.extern.slf4j.Slf4j; import org.springframework.web.reactive.socket.WebSocketMessage; import org.springframework.web.reactive.socket.WebSocketSession; import org.springframework.web.reactive.socket.client.ReactorNettyWebSocketClient; import org.springframework.web.reactive.socket.client.WebSocketClient; import reactor.core.publisher.Flux; import reactor.core.publisher.FluxSink; import reactor.core.publisher.Mono; import java.net.URI; /** * WebSocket流式输出处理工具类 * 功能：建立WebSocket连接、接收流式消息、转为Flux响应 * * @author : jucunqi * @since : 2025/9/30 */ @Slf4j public class WebSocketFluxUtils { /** * 核心方法：通过WebSocket获取流式输出并转为Flux<String> * @param request 发送给大模型的请求参数（通常为JSON格式字符串） * @param wsUrl WebSocket服务地址（例：ws://localhost:8080/model/stream） * @return 流式输出的Flux<String>，可直接响应给前端 */ public Flux<String> getWebSocketStream(String request, String wsUrl) { // 1. 创建WebSocket客户端（基于Reactor Netty实现） WebSocketClient webSocketClient = new ReactorNettyWebSocketClient(); // 2. 用Flux.create创建可手动发射消息的Flux // 作用：将WebSocket接收的消息“转发”到Flux，供前端订阅 return Flux.create(sink -> { // 3. 执行WebSocket连接：参数1为服务地址，参数2为会话处理逻辑 Mono<Void> connectMono = webSocketClient.execute( URI.create(wsUrl), session -> handleW…

Spring AI Tool 工具方法调用源码深度解析：从流式交互到工具执行全流程
type: item
date: 2025-08-23T14:06:58+08:00
description: Spring AI Tool 工具方法调用源码深度解析：从流式交互到工具执行全流程前言：为什么需要读源码？如何高效读源码？在上一篇博客中，我们介绍了如何通过 Spring AI 快速调用本地 Tool 方法实现大模型的工具能力扩展。但对于开发者来说，仅仅会用还不够 —— 理解框架的底层逻辑，才能在遇到问题时快速定位、在定制需求时游刃有余。博客链接：https://www.lucaju.cn/index.php/archives/131/很多小伙伴对读源码望而却步，其实掌握方法就能事半功倍：详略得当：聚焦核心业务逻辑，忽略日志、校验等辅助代码从命名和注释入手：规范框架的源码命名和注释会清晰指引核心流程由浅入深：先抓整体流程，再钻关键细节，避免一开始陷入代码迷宫本文将从 Spring AI 调用大模型的业务代码出发，逐步深入源码，解析 Tool 工具方法调用的完整流程，重点剖析工具执行的核心逻辑。1. 业务代码回顾：流式调用大模型的入口先看一段典型的 Spring AI 流式调用大模型并启用工具的业务代码，这是我们源码解析的起点：public Flux<String> stream(String content) { // 创建chatModel对象，配置模型参数和工具回调管理器 OpenAiChatModel chatModel = OpenAiChatModel.builder() .openAiApi(OpenAiApi.builder() .baseUrl("https://api.siliconflow.cn") .apiKey(System.getenv("SiliconFlow_API")) .build()) .defaultOptions(OpenAiChatOptions.builder() .model("Qwen/Qwen3-8B") .build()) // 关键：配置工具调用管理器 .toolCallingManager(SpringUtil.getBean(ToolCallingManager.class)) .build(); // 创建prompt对象 Prompt prompt = new Prompt(content); // 调用流式输出接口 Flux<ChatResponse> stream = chatModel.stream(prompt); return stream.map(chunk -> { String text = chunk.getResult() != null ? chunk.getResult().getOutput() != null ? chunk.getResult().getOutput().getText() : "" : ""; text = StrUtil.nullToDefault(text, ""); return text; }); }核心逻辑很清晰：创建配置好的OpenAiChatModel，构造Prompt，调用stream方法获取流式响应。其中toolCallingManager的配置是启用工具调用的关键。2. 入口：ChatModel 的 stream 方法从业务代码的chatModel.stream(prompt)进入源码，这是整个流程的入口：@Override public Flux<ChatResponse> stream(Prompt prompt) { // 合并运行时和默认选项，创建最终请求prompt Prompt requestPrompt = buildRequestPrompt(prompt); // 实际发起请求 return internalStream(requestPrompt, null); }2.1 配置合并：buildRequestPrompt 方法buildRequestPrompt的核心作用是合并运行时配置和默认配置，确保模型使用正确的参数（如工具列表、回调、上下文等）：Prompt buildRequestPrompt(Prompt prompt) { // 处理运行时prompt options OpenAiChatOptions runtimeOptions = null; if (prompt.getOptions() != null) { // 转换运行时选项为OpenAiChatOptions类型 if (prompt.getOptions() instanceof ToolCallingChatOptions toolCallingChatOptions) { runtimeOptions = ModelOptionsUtils.copyToTarget(toolCallingChatO…

Spring AI 实战：调用本地 Tool 工具方法实现大模型能力扩展
type: item
date: 2025-08-20T16:47:35+08:00
description: 前言大模型虽强，但在实时信息获取（如当前日期、天气）、复杂计算等场景下存在局限。而 Tool 工具方法 正是解决这一问题的关键 —— 它让大模型能调用本地代码获取结果，弥补自身能力短板。本文将通过具体代码案例，手把手教你在 Spring AI 中集成本地 Tool，实现大模型与本地逻辑的联动。一、基础准备：Spring AI 调用大模型环境搭建在集成 Tool 前，先搭建 Spring AI 调用大模型的基础环境，确保能正常与大模型交互。1. 添加核心依赖在 pom.xml 中引入 Spring AI OpenAI 适配器依赖（兼容主流大模型平台）： <dependency> <groupId>org.springframework.ai</groupId> <artifactId>spring-ai-starter-model-openai</artifactId> <version>1.0.0</version> </dependency>2. 基础调用代码实现（1）Controller：定义接口入口创建一个简单的 HTTP 接口，接收用户提问并调用服务层处理：import com.yeeiee.ailogic.module.ai.service.chat.SpringAiTestService; import jakarta.annotation.Resource; import org.springframework.http.MediaType; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; import reactor.core.publisher.Flux; @RestController @RequestMapping("ai-test") public class SpringAiTestController { @Resource private SpringAiTestService springAiTestService; // 流式输出接口（支持实时返回大模型响应） @GetMapping(value = "stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE) public Flux<String> stream(@RequestParam("content") String content) { return springAiTestService.stream(content); } }（2）Service：实现大模型调用逻辑定义服务接口及实现类，配置大模型连接信息并发起调用：// 服务接口 public interface SpringAiTestService { // 大模型流式输出方法 Flux<String> stream(String content); } // 服务实现类 import cn.hutool.core.util.StrUtil; import org.springframework.ai.chat.model.ChatResponse; import org.springframework.ai.chat.prompt.Prompt; import org.springframework.ai.openai.OpenAiChatModel; import org.springframework.ai.openai.OpenAiChatOptions; import org.springframework.ai.openai.api.OpenAiApi; import org.springframework.stereotype.Service; import reactor.core.publisher.Flux; @Service public class SpringAiTestServiceImpl implements SpringAiTestService { @Override public Flux<String> stream(String content) { // 配置大模型平台（以硅基流动为例，兼容 OpenAI 接口格式） OpenAiChatMode…

Spring AI 无法获取大模型深度思考内容？解决方案来了
type: item
date: 2025-08-13T17:54:00+08:00
description: 大模型的 “深度思考” 能力（即生成回答前的推理过程）正在成为提升交互体验的关键比如下面图中所看到的，Qwen 等模型会将推理过程放在reasoning_content字段中，让用户看到 “思考过程” 而非直接得到结果。但在 Java 开发中，使用 Spring AI 或 LangChain4J 等框架时，你可能会发现：这些框架会忽略reasoning_content字段，只返回最终回答。 这篇文章就来拆解这个问题的原因，并提供一套通过自定义接口调用实现 “获取完整思考过程” 的解决方案。问题：为什么框架无法获取深度思考内容？目前主流 Java 框架（Spring AI、LangChain4J）在设计时，默认只处理大模型的 “最终回答”（通常在content字段），而忽略了部分模型新增的 “思考过程” 字段（如reasoning_content）。具体表现为：字段被过滤：框架的响应解析逻辑中，没有处理reasoning_content的代码，导致这部分数据被直接丢弃。交互体验割裂：大模型的生成逻辑是 “先思考、后输出”，但框架会等待完整结果生成后才返回，用户需要长时间等待，且看不到中间思考过程。举个例子：当你调用 Qwen 模型时，原始接口返回会包含两部分内容：reasoning_content：“用户问我是谁，我需要先介绍自己的名字，再说明我的能力，还要保持友好的语气……”（思考过程）content：“我是通义千问，由通义实验室研发的超大规模语言模型。我能够帮助用户回答问题、创作文字，以及进行对话交流。如果你有任何问题或需要帮助，随时告诉我！”（最终回答）但通过 Spring AI 调用时，你只能拿到content部分，错失了展示 “思考过程” 的机会。解决方案：自定义接口调用，掌控原始数据既然框架的封装会丢失信息，那最直接的方案就是：绕过框架的限制，直接调用大模型的原生 API，获取完整响应数据。这种方式的优势在于：可获取所有字段（包括reasoning_content、usage等元数据）；灵活控制流式输出逻辑，实现 “思考过程” 和 “最终回答” 的实时展示；便于业务扩展（如自定义缓存、日志、过滤规则等）。下面以 “调用硅基流动接口获取 Qwen 模型的思考过程” 为例，演示具体实现。实战：用 Spring WebFlux 调用硅基流动 API硅基流动是一个大模型聚合平台，支持 Qwen、GPT 等模型的调用，其 API 会返回完整的reasoning_content字段。我们用 Spring WebFlux（响应式 HTTP 客户端）实现调用，既能处理流式输出，又能获取原始响应。步骤 1：了解接口参数参考硅基流动官方 API 文档，核心请求参数如下：messages：对话历史（包含role和content）；model：模型名称（如moonshotai/Kimi-Dev-72B）；stream：是否开启流式输出（设为true，实时获取思考和回答）；temperature：控制输出随机性（0-1 之间）。步骤 2：编码实现（获取完整响应）import org.junit.jupiter.api.Test; import org.springframework.http.MediaType; import org.springframework.web.reactive.function.client.WebClient; import reactor.core.publisher.Flux; public class SiliconFlowTest { // 硅基流动API地址 private static final String API_URL = "https://api.siliconflow.cn/v1/chat/completions"; // 你的API Key（从硅基流动控制台获取） private static final String API_KEY = "your api-key"; private static final String NULL_MESSAGE = "null"; @Test public void testGetReasoningContent() { // 1. 构建请求体（包含对话和参数） String requestBody = """ { "messages": [ { "content": "你是一个聊天大师，请回答用户的问题。", "role": "system" }, { "content": "什么是Java响应式编程？", "role": "user" } ], "model": "Qwen/Qwen3-8B", "stream": true, "tempe…

在 Ubuntu 环境下安装与配置 Nginx 的完整指南
type: item
date: 2025-08-05T16:29:00+08:00
description: Nginx简介 Nginx是一款高性能的开源 Web 服务器、反向代理服务器、负载均衡器和 HTTP 缓存工具。它由俄罗斯程序员伊戈尔・赛索耶夫（Igor Sysoev）于 2004 年首次公开发布，最初设计的目标是解决高并发场景下的性能瓶颈，如今已成为全球最流行的服务器软件之一，被 Netflix、Airbnb、GitHub、腾讯、阿里等众多大型企业广泛使用。本文将详细介绍如何在 Ubuntu 系统中安装、配置并优化 Nginx，适合初学者入门参考。一、安装 NginxUbuntu 的官方软件仓库中已经包含了 Nginx，我们可以通过 APT 包管理器轻松安装。更新系统包列表首先确保系统包列表是最新的：sudo apt update安装 Nginx执行以下命令安装 Nginx：sudo apt install nginx验证安装是否成功安装完成后，Nginx 会自动启动。可以通过以下命令检查其运行状态：sudo systemctl status nginx如果看到 "active (running)" 字样，说明 Nginx 已经成功启动。配置防火墙 4.1 如果你的 Ubuntu 系统启用了 UFW 防火墙，需要允许 HTTP（80 端口）和 HTTPS（443 端口）流量：sudo ufw allow 'Nginx Full'可以通过以下命令验证防火墙规则：sudo ufw status 4.2 如果你跟我一样，使用的是云服务器，那么只需要在安全组中开放80端口即可二、Nginx的基本操作掌握以下基本命令可以帮助你管理 Nginx 服务：启动 Nginx：sudo systemctl start nginx停止 Nginx：sudo systemctl stop nginx重启 Nginx：sudo systemctl restart nginx重新加载配置（不中断服务）：sudo systemctl reload nginx设置开机自启动：sudo systemctl enable nginx禁止开机自启动：sudo systemctl disable nginx三、Nginx 的配置文件结构Nginx 的配置文件位于/etc/nginx目录下，主要文件和目录包括：/etc/nginx/nginx.conf：主配置文件/etc/nginx/sites-available/：存储所有网站的配置文件/etc/nginx/sites-enabled/：存储启用的网站配置（通常是指向 sites-available 目录的软链接）/etc/nginx/conf.d/：可以存放额外的配置片段/etc/nginx/mime.types：定义 MIME 类型这种结构允许我们为每个网站创建独立的配置文件，便于管理。四、配置一个基本的 Web 站点下面我们创建一个简单的 Web 站点配置：创建网站目录首先为网站创建一个目录，并设置适当的权限：sudo mkdir -p /var/www/example.com/html sudo chown -R $USER:$USER /var/www/example.com/html sudo chmod -R 755 /var/www创建测试页面在网站目录下创建一个简单的 HTML 文件：nano /var/www/example.com/html/index.html添加以下内容：预览 Welcome to Example.com! Success! The example.com server block is working! 保存并关闭文件。创建服务器配置文件在sites-available目录下创建一个新的配置文件：sudo nano /etc/nginx/sites-available/example.com添加以下配置：server { listen 80; listen [::]:80; root /var/www/example.com/html; index index.html index.htm index.nginx-debian.html; server_name example.com www.example.com; # 替换为你的域名或服务器 IP（如 1.2.3.4） location / { try_files $uri $uri/ =404; } }这个配置指定了：监听 80 端口（HTTP）网站文件根目录默认索引文件服务器域名基本的请求处理规则启用站点配置通过创建软链接将配置文件链接到sites-enabled目录：sudo ln -s /etc/nginx/sites-available/example.com /etc/nginx/sites-enab…

自定义 Spring-Boot-Starter 结合 TrueLicense 实现证书授权拦截
type: item
date: 2025-03-14T13:30:00+08:00
description: 引言在软件产品交付场景中，授权管理是保障软件权益的重要手段。传统的硬编码时间限制方式存在修改麻烦、需重新部署等问题，而基于证书（License）的授权方式可通过替换证书文件实现灵活授权，无需改动源码。本文将详解如何基于 Spring Boot 自定义 Starter，并整合开源证书管理引擎 TrueLicense，实现对接口的证书授权拦截功能，帮助开发者快速搭建可控的授权体系。一、技术背景与场景说明1.1 什么是 TrueLicense？TrueLicense 是一个基于 Java 的开源证书管理引擎，提供了证书的生成、颁发、验证等核心功能，支持通过密钥对加密证书内容，确保授权信息的安全性。其官网地址为：https://truelicense.java.net。1.2 为什么需要自定义 Spring Boot Starter？Spring Boot Starter 的核心作用是简化依赖管理和自动配置。通过自定义 Starter，我们可以将证书校验逻辑封装为独立组件，只需在目标项目中引入依赖并配置参数，即可快速集成证书授权功能，实现 "即插即用"。1.3 核心场景软件试用期授权：通过证书指定有效期，到期后自动限制使用。硬件绑定授权：限制软件仅能在指定 MAC 地址的设备上运行。接口级授权控制：对敏感接口添加证书校验，未授权请求直接拦截。二、密钥对生成（基于 keytool）证书的安全性依赖于非对称加密的密钥对（私钥用于生成证书，公钥用于验证证书）。我们使用 JDK 自带的keytool工具生成密钥对，步骤如下：2.1 生成私钥库私钥库用于存储生成证书的私钥，执行以下命令：keytool -genkey -alias privatekey -keystore privateKeys.store -storepass "123456q" -keypass "123456q" -keysize 1024 -validity 3650参数说明：-alias privatekey：私钥别名（后续生成证书需引用）。-keystore privateKeys.store：生成的私钥库文件名。-storepass "123456q"：私钥库访问密码。-keypass "123456q"：私钥本身的密码（建议与 storepass 一致，简化管理）。-keysize 1024：密钥长度（1024 位及以上确保安全性）。-validity 3650：私钥有效期（单位：天，此处为 10 年）。执行后需输入所有者信息（如姓名、组织等），可根据实际情况填写。2.2 导出公钥证书从私钥库中导出公钥证书（用于校验证书的合法性）：keytool -export -alias privatekey -file certfile.cer -keystore privateKeys.store -storepass "123456q"参数说明：-export：指定操作类型为导出证书。-file certfile.cer：导出的公钥证书文件名。执行成功后，当前目录会生成certfile.cer公钥文件。2.3 导入公钥到公钥库将公钥证书导入公钥库（供应用程序验证证书时使用）：keytool -import -alias publiccert -file certfile.cer -keystore publicCerts.store -storepass "123456q"参数说明：-alias publiccert：公钥在公钥库中的别名（后续校验需引用）。-keystore publicCerts.store：生成的公钥库文件名。执行时需确认导入（输入yes），完成后公钥库publicCerts.store生成。注意：私钥库（privateKeys.store）需妥善保管，公钥库（publicCerts.store）和公钥证书（certfile.cer）可随应用程序部署。三、证书生成工具实现基于 TrueLicense 的 API，我们可以通过代码生成证书文件。以下是核心实现步骤：3.1 核心参数类定义首先定义证书生成所需的参数封装类（LicenseCreatorParam）：/** * License证书生成类需要的参数 * @author : jucunqi * @since : 2025/3/12 */ @Data public class LicenseCreatorParam implements Serializable { private static final long serialVersionUID = 2832129012982731724L; /** * 证书subject * */ private String subject; /** …

响应式编程学习笔记
type: item
date: 2025-01-15T15:41:00+08:00
description: 响应式编程1、Reactor核心前置知识1、Lambda2、Function根据出参，入参分类1、有入参，有出参 --> FunctionFunction<String, Integer> function = a -> Integer.parseInt(a);2、有入参，无出参Consumer<String> consumer = a -> System.out.println(a);3、无入参，有出参Supplier<String> supplier = () -> UUID.randomUUID().toString();4、无入参，无出参Runnable runnable = () -> System.out.println("xixi"); 3、StreamAPI流式操作，三大步骤1、创建流Stream<Integer> integerStream = Stream.of(1, 2, 3); Stream<Integer> stream = list.stream();2、中间操作(intermediate operation)，可以有多个filter,map,mapToInt,mapToLong,mapToDouble,flatMap,flatMapToInt,flatMapToLong,flatMapToDouble,mapMulti,mapMultiToInt,mapMultiToLong,mapMultiToDouble,peek...3、终止操作(terminal operation)，只能有一个forEach,forEachOrdered,toArray,toArray,reduce,collect,toList,min,max,count,anyMatch,findFirst,findAny...流式操作是否并发？ // 流的三大部份 // 1.创建流 2.N个中间操作 3.一个终止操作 Stream<Integer> integerStream = Stream.of(1, 2, 3); Stream<Object> buildStream = Stream.builder().add(1).add(2).add(3).build(); Stream<Object> concatStream = Stream.concat(integerStream, buildStream); Stream<Integer> stream = list.stream(); List<Integer> resultList = new ArrayList<>(); System.out.println("main线程： "+Thread.currentThread().getName()); // 流是不是并发操作？ 答：默认单线程，可以通过parallel开启多线程，但是如果开启多线程，则需要自身注意线程安全问题 long count = list.stream() .parallel() // 开启多线程 并发流 .filter(i -> { // resultList.add(i); // 开启多线程，不能这样写，要保证流里面的数据是无状态的，即流里面的数据只在流内部使用 // 可以计算完成以后返回出去，但是不能在内部又引用外部的数据，可能会出现问题 System.out.println("filter线程: " + Thread.currentThread().getName()); return i > 2; }) .count(); System.out.println(resultList);注意： 要保证流里面的数据是无状态的中间操作：filter：过滤，挑出我们要的元素takeWhile示例List<Integer> collect = Stream.of(1, 2, 3, 4, 5, 6) .filter(a -> a > 2) // 无条件遍历 .toList(); System.out.println(collect); List<Integer> collect1 = Stream.of(1, 2, 3, 4, 5, 6) .takeWhile(a -> a < 2) // 当条件不满足时，直接返回 .toList(); System.out.println(collect1);map：映射，一对一映射mapToInt，MapToDouble..flatMap: 打散、散列、展开，一对多映射...终止操作：forEach、forEachOrdered、toArray、reduce、collect、toList、min、 max、count、anyMatch、allMatch、noneMatc…