记录 Java、PHP、Python、Go 等后端开发技术，包括框架、数据库、微服务等相关内容。
Dataroom从SpringBoot2.7升级到SpringBoot3.2.4 为了使用让DataRoom引入MCP，整合Spring AI体系，升级SrpingBoot 升级到Jakarta EE Java EE 已更改为 Jakarta EE，Spring Boot 3.x 的所有依赖项 API 也从 Java EE 升级为 Jakarta EE。 简单来说，您需要将所有 javax 的 imports 都替换为 jakarta。具体如下： javax.persistence.* -> jakarta.persistence.* javax.validation.* -> jakarta.validation.* javax.servlet.* -> jakarta.servlet.* javax.annotation.* -> jakarta.annotation.* javax.transaction.* -> jakarta.transaction.* 换mybatis依赖 Invalid value type for attribute ‘factoryBeanObjectType‘: java.lang.String 所以升级 Mybatis-Plus 版本为 3.5.5 版本即可，需要注意下 Maven 的坐标标识 是mybatis-plus-spring-boot3-starter，这点和SpringBoot 2 的依赖坐标mybatis-plus-boot-starter有所区别 第一步：注释spring-boot-starter <!-- <dependency>--> <!-- <groupId>com.baomidou</groupId>--> <!-- <artifactId>mybatis-plus-boot-starter</artifactId>--> <!-- <version>${mybatis.plus.version}</version>--> <!-- </dependency>--> 第二步：引入新的依赖 <dependency> <groupId>com.baomidou</groupId> <artifactId>mybatis-plus-spring-boot3-starter</artifactId> <version>3.5.5</version> </dependency> 第三步 <dependencyManagement> <dependencies> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis-spring</artifactId> <version>3.0.3</version> </dependency> </dependencies> </dependencyManagement> 动态数据源修改 第一步注释如下代码 <dependency> <groupId>com.baomidou</groupId> <artifactId>dynamic-datasource-spring-boot-starter</artifactId> <version>3.5.0</version> </dependency> 第二步：引入依赖 <dependency> <groupId>com.baomidou</groupId> <artifactId>dynamic-datasource-spring-boot3-starter</artifactId> <version>4.2.0</version> </dependency>
规则引擎能力 一、规则集 规则集也叫决策集，是由一组普通规则和循环规则构成的规则集合，是使用频率最高的一种业务规则实现方式。 鲁班提供循环、逻辑（IF/ELSE）、赋值、执行、跳出规则等指令。 经典实例 新用户注册优惠： 如果用户是新注册用户，并且首次购买订单金额超过一定数额，则发送一张10%折扣的优惠券。 购物满减优惠： 如果用户购物车中的商品总价达到一定金额阈值，则自动应用满减优惠。比如，购物车金额大于100元可以享受减免10元的优惠。 特定商品优惠： 如果用户购买特定商品（如促销商品、季节性商品），则提供针对该商品额外的折扣或优惠。 生日优惠券： 在用户生日时发送一张生日优惠券，提供额外的折扣或者赠送特定商品。 用户活跃度奖励： 对于长时间未下单的用户，发送特定额度的优惠券以鼓励再次购买。 地理位置优惠： 针对特定地理位置的用户发送定向的优惠券，以促进当地的销售。 病人风险评估系统规则集示例 考虑以下条件和操作： 年龄： 30岁以下（Young）、30到50岁（Middle-aged）、50岁以上（Elderly）。 生活方式： 锻炼频率（High、Moderate、Low）、饮食习惯（Healthy、Unhealthy）。 慢性病史： 有（Yes）或无（No）。 风险评估等级： 低风险（Low）、中风险（Moderate）、高风险（High）。 规则集示例： FOR（用户集合 ） 用户。年龄 IF 年龄 = Elderly AND (锻炼频率 = Low OR 饮食习惯 = Unhealthy) THEN 风险评估等级 = 高风险 IF 年龄 = Middle-aged AND 慢性病史 = Yes THEN 风险评估等级 = 高风险 IF 年龄 = Young AND 锻炼频率 = High THEN 风险评估等级 = 低风险 IF 年龄 = Elderly AND 锻炼频率 = Moderate THEN 风险评估等级 = 中风险 在这个例子中，规则集根据病人的年龄、生活方式和慢性病史等条件制定了不同的风险评估规则。例如，对于年龄较大且锻炼频率低或饮食不健康的病人，系统会评估其为高风险群体，需要更频繁的检查或干预措施。 这种规则集的应用有助于医疗保健领域更精准地对病人进行风险评估，并根据其个体情况提供相应的医疗建议，从而改善病人的健康状况。 二、决策树 决策树又称为规则树，规则引擎中提供的另外一种构建规则的方式，它以一棵树形结构来表现规则，决策树表现业务规则更为形象，其中每个节点代表一个测试条件，每个分支代表一个测试结果，而每个叶子节点包含一个决策结果。 信用评分系统决策树示例 考虑以下特征和目标： 年龄： 25岁以下（Young）、25到40岁（Middle-aged）、40岁以上（Elderly）。 收入水平： 低收入（Low）、中等收入（Medium）、高收入（High）。 信用历史： 良好（Good）、一般（Fair）、差劣（Poor）。 是否有抵押物： 有（Yes）或无（No）。 信用申请结果： 批准（Approved）或拒绝（Rejected）。 决策树示例： IF 信用历史 = 良好 AND 收入水平 = 高收入 THEN 信用申请结果 = 批准 ELSE IF 信用历史 = 良好 AND 收入水平 = 中等收入 THEN 信用申请结果 = 批准 ELSE IF 信用历史 = 一般 AND 是否有抵押物 = 有 THEN 信用申请结果 = 批准 ELSE IF 信用历史 = 一般 AND 是否有抵押物 = 无 THEN 信用申请结果 = 拒绝 ELSE IF 信用历史 = 差劣 THEN 信用申请结果 = 拒绝 在这个例子中，决策树根据申请者的年龄、收入水平、信用历史和是否有抵押物等特征，自动判断信用申请的结果。例如，如果申请者信用历史良好且收入水平高，那么系统会批准其信用申请；而如果信用历史一般但有抵押物，同样会批准。 这种决策树的应用使得信用评分系统能够自动化、可解释地做出信用决策，提高了效率同时保持了透明度。 三、决策表 决策表是一种以表格形式表现规则的工具，它非常适用于描述处理判断条件较多，各条件又相互组合、有多种决策方案的情况，决策表提供精确而简洁描述复杂逻辑的方式。 视图 使用excel 决策表的应用场景： 业务规则管理： 用于管理和维护大量的业务规则，使得业务规则的管理更加简便和直观。 风险评估： 在金融领域，可以使用决策表来定义风险评估规则，根据客户的不同特征进行信用评级。 产品定价： 在销售领域，可以使用决策表来制定产品定价策略，根据市场条件和产品特性进行灵活的定价。 合规性检查： 在法务和合规性领域，决策表可以用于定义符合法规的业务行为，确保组织的合规性。 推荐系统： 在电商领域，可以使用决策表来定义推荐算法的规则，根据用户的行为和喜好推荐相应的产品 电子商务促销活动管理系统决策表示例 假设我们有以下条件和操作： 用户类型： 普通用户（Regular）和会员用户（Premium）。 购物车金额： 低于100元（Low）和100元及以上（High）。 商品类别： 电子产品（Electronics）、服装（Apparel）、食品（Food）。 促销策略： 折扣（Discount）、满减（Cashback）、免费赠品（Free Gift）。 决策表： 用户类型 购物车金额 商品类别 触发促销策略 操作 普通用户 低金额 电子产品 折扣 应用10%折扣 普通用户 低金额 服装 满减 满200减20 普通用户 高金额 食品 免费赠品 赠送一份免费小吃 会员用户 高金额 电子产品 满减 满500减50 会员用户 低金额 服装 折扣 应用15%折扣 会员用户 高金额 食品 免费赠品 赠送一份免费礼包 在这个例子中，决策表根据用户类型、购物车金额和商品类别等条件定义了不同的促销策略。例如，对于普通用户购买低金额的电子产品，系统会应用10%的折扣；而对于会员用户购买高金额的电子产品，系统则会提供满减优惠。 这样的决策表使得促销活动管理系统能够根据多个条件智能地选择适当的促销策略，提高销售效果和用户满意度。 四、交叉决策表 交叉决策表又叫决策矩阵，是一种特殊类型的决策表。 与普通决策表相比，交叉决策表的条件由纵向和横向两个维度决定，而普通决策表的条件只是由纵向维度决定；但在普通决策表的动作部分可以是三种类型，分别是赋值、输出和执行方式，而在交叉决策表中动作部分就是纵向和横向两个维度交叉后的单元格的值，一般来说，这种交叉后单元格的值都是赋给某个变量或参数，所以交叉决策表的动作基本就一个，那就是赋值。 实际业务场景 假设我们正在开发一个智能家居系统，通过交叉决策表来确定何时触发不同设备的自动化操作。以下是一个简化的实际应用场景： 智能家居系统交叉决策表示例 考虑以下几个条件和设备： 时间段： 白天（Daytime）和夜晚（Nighttime）。 人员在家： 有人在家（Occupied）和无人在家（Unoccupied）。 光照程度： 光线充足（Well-lit）和光线不足（Dimly-lit）。 设备： 灯光（Lights）、温控器（Thermostat）、安全摄像头（Security Camera）。 交叉决策表： 时间段 人员在家 光照程度 触发设备 操作 白天 有人 光线充足 无 保持设备关闭 白天 有人 光线不足 灯光 打开灯光 白天 无人 无 灯光、温控器 打开灯光、调整温度 夜晚 有人 光线充足 无 保持设备关闭 夜晚 有人 光线不足 灯光 打开灯光 夜晚 无人 无 安全摄像头 打开安全摄像头 在这个例子中，交叉决策表综合考虑了时间段、人员在家与否以及光照程度等多个条件。根据这些条件的不同组合，系统可以智能地触发相应的设备操作，使智能家居系统更符合用户的实际需求。例如，在夜晚、有人在家、光线不足的情况下，系统可以自动打开灯光，提高家居的舒适性和安全性。 五、评分卡 评分是对个人或机构的相关信息进行分析之后的一种数值表达，表示此人或此机构由于信用活动的拒付行为所造成损失风险的可能性，评分通常用于对个人或机构的风险管理与评估。 假设我们正在构建一个贷款评分系统，使用评分卡来评估个人信用并决定是否批准其贷款申请。以下是一个简化的实际应用场景： 贷款评分卡示例 考虑以下特征和分数分配： 年龄： 20岁以下（0分）、20到30岁（10分）、30到40岁（20分）、40岁以上（30分）。 收入水平： 低收入（0分）、中等收入（15分）、高收入（30分）。 信用历史： 良好（30分）、一般（15分）、差劣（0分）。 负债比例： 低于30%（20分）、30%到50%（10分）、50%以上（0分）。 工作年限： 不足一年（0分）、1到5年（10分）、5年以上（20分）。 评分卡示例： 总分 = 年龄分 + 收入分 + 信用历史分 + 负债比例分 + 工作年限分 IF 总分 >= 70 THEN 批准贷款 ELSE IF 50 <= 总分 < 70 THEN 需要进一步审查 ELSE 批准贷款 在这个例子中，评分卡通过对申请者的年龄、收入水平、信用历史、负债比例和工作年限等特征进行分数分配，计算出总分。然后，系统通过总分的阈值来决定是否批准贷款。例如，总分大于等于70分的申请者可以直接批准贷款，而总分在50到70之间的申请者需要进一步审查。 这种评分卡的应用使得贷款决策更加客观、可量化，有助于提高贷款审批的效率和一致性。 现有规则引擎在业务端使用评估 业务规则集适合简单IF - THEN逻辑业务和简单循环业务逻辑 决策树适合简单IF - ELSE 带分支的简单业务逻辑使用 复杂多条件场景想使用决策表或者评估卡来创建规则，更符合业务人员的使用习惯 如何使用规则引擎 sequenceDiagram participant BusinessUser as 业务人员 participant BusinessIT as 业务IT人员 participant Implementation as 公司实施人员 BusinessUser ->> BusinessIT: 请求创建具体规则 BusinessIT ->> Implementation: 使用规则引擎配置规则模板和规则包 Implementation ->> BusinessIT: 完成规则配置 BusinessIT ->> BusinessUser: 使用规则模板创建规则 BusinessUser ->> BusinessIT: 提出规则调整需求 BusinessIT ->> BusinessUser: 根据需求调整规则逻辑 BusinessUser ->> BusinessIT: 动态创建和维护规则 在整个Saas系统中，涉及到三个主要角色：业务人员、业务IT人员和公司实施人员。以下是每个角色在规则引擎使用过程中的主要职责： 公司实施人员 规则集、决策树、决策表、交叉决策表、评估卡的规则包创建： 负责使用规则引擎的界面或工具，创建和配置规则集、决策树、决策表、交叉决策表、评估卡的规则包，以便后续的规则模板和规则创建。 业务规则模板的创建： 设计和创建通用的业务规则模板，为业务IT人员提供规则创建的基础结构和规范。 业务IT人员 规则创建： 根据公司实施人员配置好的规则模板和知识包，使用规则引擎的工具创建具体的业务规则，包括规则集、决策树、决策表、交叉决策表、评估卡等。 规则逻辑调整： 负责根据业务需求对已创建的规则进行逻辑调整，确保规则符合业务规则模板和公司实施人员的要求。 规则维护： 定期检查和更新规则，确保规则引擎中的规则集和决策树等结构始终与业务要求保持一致。 业务人员 业务规则调整： 在规则引擎的用户界面中，根据业务需求直接调整已创建的规则，例如修改决策表的条件或调整评估卡的分数等。 规则逻辑优化： 根据业务实际运作的反馈，提出对规则逻辑的优化建议，协助业务IT人员进行调整。 规则动态创建和维护： 在业务变化时，通过规则引擎的界面灵活地创建和维护规则，确保系统能够快速响应业务的变化。 这样的角色划分和职责分配可以使得规则引擎的使用更具灵活性和适应性，不同层次的用户可以专注于其核心职责，共同完成规则的创建和维护。 定制化业务 部分更针对性业务模式由业务团队进行定制页面，如果需要更多定制指令由技术部开发
Flowable工作流引擎源码深度解析 前言 Flowable是一个轻量级的业务流程引擎，基于BPMN 2.0规范实现，是Activiti项目的一个分支。作为Java生态中最流行的工作流引擎之一，了解其内部实现对于定制化开发和性能优化至关重要。本文将深入分析Flowable的核心源码结构和执行逻辑，帮助开发者更好地理解和使用这一强大工具。 核心架构概览 Flowable的源码主要分为以下几个核心模块： flowable-engine：核心引擎实现 flowable-bpmn-converter：BPMN模型转换器 flowable-process-validation：流程验证模块 flowable-image-generator：流程图生成模块 flowable-rest：REST API实现 其中，flowable-engine是最核心的部分，我们的分析也将主要集中在这个模块上。 ProcessEngine初始化流程 Flowable的入口是ProcessEngine接口，通常通过ProcessEngineConfiguration来创建。让我们看看其初始化过程： public class ProcessEngineConfigurationImpl extends ProcessEngineConfiguration { public ProcessEngine buildProcessEngine() { init(); ProcessEngineImpl processEngine = new ProcessEngineImpl(this); postProcessEngineInitialisation(); return processEngine; } public void init() { initCommandContextFactory(); initTransactionContextFactory(); initCommandExecutors(); initServices(); initDataSource(); initDbSchema(); initBeans(); initTransactionFactory(); // 其他初始化方法... } } 初始化过程主要包括： 初始化命令上下文工厂 初始化事务上下文工厂 初始化命令执行器 初始化各种服务 初始化数据源和数据库架构 初始化事务工厂等 这种设计遵循了良好的工厂模式和构建器模式。 命令模式的应用 Flowable大量使用了命令模式，所有对流程的操作都被封装为Command对象： public interface Command<T> { T execute(CommandContext commandContext); } 执行命令的是CommandExecutor，它主要有两个实现： CommandExecutorImpl：普通实现 TransactionCommandExecutor：带事务的实现 让我们看看CommandExecutorImpl的实现： public class CommandExecutorImpl implements CommandExecutor { protected CommandContextFactory commandContextFactory; protected TransactionContextFactory transactionContextFactory; public <T> T execute(Command<T> command) { CommandContext commandContext = commandContextFactory.createCommandContext(command); try { T result = command.execute(commandContext); commandContext.close(); return result; } catch (Exception e) { commandContext.exception(e); } finally { try { commandContext.close(); } catch (Exception e) { // 日志记录 } } return null; } } 这种设计确保了所有流程操作都在一个一致的上下文中执行，并且可以正确处理事务和异常。 流程定义加载 当我们部署一个BPMN文件时，Flowable会解析它并转换为内部模型。核心类是BpmnParser： public class BpmnParser { public BpmnParse createParse() { return new BpmnParse(this); } public BpmnParse parse(InputStream inputStream) { BpmnParse bpmnParse = createParse(); bpmnParse.sourceInputStream = inputStream; bpmnParse.execute(); return bpmnParse; } } BpmnParse类负责具体的解析逻辑： public class BpmnParse extends BpmnParseHandler { public BpmnParse execute() { try { // 解析XML文档 DocumentBuilderFactory dbf = XmlUtil.createSafeDocumentBuilderFactory(); Document document = dbf.newDocumentBuilder().parse(sourceInputStream); // 解析BPMN元素 parseRootElement(document.getDocumentElement()); // 处理流程定义 processDI(); // 完成解析 executeParse(); } catch (Exception e) { // 异常处理 } return this; } protected void parseRootElement(Element rootElement) { // 解析流程、任务、网关等元素 } } 这个过程将BPMN文件转换为ProcessDefinitionEntity对象，其中包含了流程的所有信息。 流程执行引擎 流程实例的执行由ExecutionEntity类负责： public class ExecutionEntity implements Execution, ExecutionListenerContainer { protected String id; protected ProcessDefinitionEntity processDefinition; protected String businessKey; protected String activityId; protected ExecutionEntity parent; protected List<ExecutionEntity> executions = new ArrayList<>(); // 其他属性... public void start() { CommandContext commandContext = Context.getCommandContext(); // 执行流程实例启动逻辑 // 触发事件监听器 // 执行第一个活动节点 } public void continueExecution() { ExecutionEntity execution = this; while (execution != null && execution.isActive()) { ActivityImpl activity = execution.getActivity(); if (activity != null) { // 执行当前活动节点 execution = activity.execute(execution); } else { // 已完成 execution = null; } } } } 每个流程实例都对应一个ExecutionEntity，每个并行流程也对应一个子ExecutionEntity。 任务管理 任务由TaskEntity类表示： public class TaskEntity implements Task { protected String id; protected String name; protected String description; protected String assignee; protected Date createTime; protected String executionId; protected String processInstanceId; // 其他属性... public void complete() { // 验证任务状态 // 执行任务完成逻辑 // 触发事件监听器 // 推进流程执行 } } 任务的创建、分配和完成都是通过TaskService接口实现的： public interface TaskService { Task newTask(); void saveTask(Task task); void deleteTask(String taskId); void claim(String taskId, String userId); void complete(String taskId); // 其他方法... } 事件监听机制 Flowable提供了丰富的事件监听机制，核心接口是FlowableEventListener： public interface FlowableEventListener { void onEvent(FlowableEvent event); boolean isFailOnException(); } 事件类型由FlowableEventType枚举定义，包括流程启动、任务创建、流程完成等多种类型。 事件的分发由FlowableEventDispatcher接口负责： public interface FlowableEventDispatcher { void addEventListener(FlowableEventListener listener); void addEventListener(FlowableEventListener listener, FlowableEventType... types); void removeEventListener(FlowableEventListener listener); void dispatchEvent(FlowableEvent event); } 这种设计允许我们在流程的各个阶段插入自定义逻辑。 数据持久化 Flowable使用MyBatis作为ORM框架进行数据持久化。核心接口是DbSqlSession： public class DbSqlSession implements Session { protected SqlSession sqlSession; protected DbSqlSessionFactory dbSqlSessionFactory; protected List<PersistentObject> insertedObjects = new ArrayList<>(); protected List<PersistentObject> updatedObjects = new ArrayList<>(); protected List<PersistentObject> deletedObjects = new ArrayList<>(); public void flush() { // 处理插入对象 for (PersistentObject insertedObject : insertedObjects) { String insertStatement = dbSqlSessionFactory.getInsertStatement(insertedObject); sqlSession.insert(insertStatement, insertedObject); } // 处理更新对象 for (PersistentObject updatedObject : updatedObjects) { String updateStatement = dbSqlSessionFactory.getUpdateStatement(updatedObject); sqlSession.update(updateStatement, updatedObject); } // 处理删除对象 for (PersistentObject deletedObject : deletedObjects) { String deleteStatement = dbSqlSessionFactory.getDeleteStatement(deletedObject); sqlSession.delete(deleteStatement, deletedObject); } // 清空缓存 insertedObjects.clear(); updatedObjects.clear(); deletedObjects.clear(); } } 所有实体对象的变更都会被记录在这些列表中，然后在事务提交时一次性写入数据库。 性能优化 Flowable做了许多性能优化，其中最重要的是缓存机制： public class DeploymentCache<T> { protected Map<String, T> cache = new HashMap<>(); protected int limit; protected LinkedList<String> keyList = new LinkedList<>(); public void add(String id, T obj) { if (limit > 0 && keyList.size() >= limit) { String oldestKey = keyList.removeFirst(); cache.remove(oldestKey); } cache.put(id, obj); keyList.addLast(id); } public T get(String id) { return cache.get(id); } public void remove(String id) { cache.remove(id); keyList.remove(id); } } 这种LRU缓存确保了频繁使用的流程定义可以快速获取，而不需要重复从数据库加载。 扩展点分析 Flowable提供了丰富的扩展点，允许开发者定制化流程行为： TaskListener：任务生命周期监听器 ExecutionListener：执行流程监听器 ActivityBehavior：自定义活动行为 ExpressionManager：表达式管理器 VariableType：自定义变量类型 以TaskListener为例： public interface TaskListener { String EVENTNAME_CREATE = "create"; String EVENTNAME_ASSIGNMENT = "assignment"; String EVENTNAME_COMPLETE = "complete"; String EVENTNAME_DELETE = "delete"; void notify(DelegateTask delegateTask); } 通过实现这个接口，我们可以在任务的各个生命周期阶段插入自定义逻辑。 异步执行器 Flowable 6引入了新的异步执行器，替代了旧的JobExecutor： public class DefaultAsyncExecutor implements AsyncExecutor { protected ThreadPoolExecutor threadPoolExecutor; protected RejectedExecutionHandler rejectedExecutionHandler; protected ThreadFactory threadFactory; public void start() { if (threadPoolExecutor == null) { threadPoolExecutor = new ThreadPoolExecutor( corePoolSize, maxPoolSize, keepAliveTime, TimeUnit.MILLISECONDS, new ArrayBlockingQueue<>(queueSize), threadFactory, rejectedExecutionHandler ); } // 启动作业获取线程 startJobAcquisitionThread(); } protected void executeAsyncJob(Job job) { Runnable runnable = new ExecuteAsyncRunnable(job, this); threadPoolExecutor.execute(runnable); } } 这种设计使得Flowable可以高效地处理大量的异步任务。 我很乐意解释Flowable 6.x中如何实现动态加节点，并提供一个实用的demo。 在Flowable 6.x中，动态修改流程实例主要通过RuntimeService的API来实现，特别是通过createProcessInstanceModification方法。下面我将详细介绍实现方法并提供一个完整的示例。 package com.example.flowable; import org.flowable.engine.*; import org.flowable.engine.impl.cfg.StandaloneProcessEngineConfiguration; import org.flowable.engine.repository.Deployment; import org.flowable.engine.repository.ProcessDefinition; import org.flowable.engine.runtime.ProcessInstance; import org.flowable.task.api.Task; import java.util.HashMap; import java.util.List; import java.util.Map; /** * Flowable 6.x 动态添加节点示例 */ public class FlowableDynamicTaskDemo { public static void main(String[] args) { // 初始化流程引擎 ProcessEngine processEngine = initProcessEngine(); // 获取各种服务 RepositoryService repositoryService = processEngine.getRepositoryService(); RuntimeService runtimeService = processEngine.getRuntimeService(); TaskService taskService = processEngine.getTaskService(); // 部署流程定义 Deployment deployment = repositoryService.createDeployment() .addClasspathResource("dynamic-process.bpmn20.xml") .deploy(); ProcessDefinition processDefinition = repositoryService.createProcessDefinitionQuery() .deploymentId(deployment.getId()) .singleResult(); System.out.println("流程定义部署完成: " + processDefinition.getName()); // 启动流程实例 Map<String, Object> variables = new HashMap<>(); variables.put("applicant", "张三"); variables.put("amount", 5000); ProcessInstance processInstance = runtimeService.startProcessInstanceByKey("dynamicProcess", variables); System.out.println("流程实例启动成功，ID: " + processInstance.getId()); // 查询当前任务 Task currentTask = taskService.createTaskQuery() .processInstanceId(processInstance.getId()) .singleResult(); System.out.println("当前任务: " + currentTask.getName()); // 动态添加一个审核任务节点 System.out.println("开始动态添加节点..."); dynamicallyAddTask(runtimeService, processInstance.getId(), currentTask.getId()); // 完成当前任务 taskService.complete(currentTask.getId()); System.out.println("完成任务: " + currentTask.getName()); // 查看动态添加的任务 List<Task> tasks = taskService.createTaskQuery() .processInstanceId(processInstance.getId()) .list(); for (Task task : tasks) { System.out.println("当前活动任务: " + task.getName() + ", ID: " + task.getId()); // 完成动态添加的任务 taskService.complete(task.getId()); System.out.println("完成任务: " + task.getName()); } // 检查流程是否结束 ProcessInstance runningInstance = runtimeService.createProcessInstanceQuery() .processInstanceId(processInstance.getId()) .singleResult(); if (runningInstance == null) { System.out.println("流程实例已完成"); } else { System.out.println("流程实例仍在运行"); } } /** * 动态添加任务节点 */ private static void dynamicallyAddTask(RuntimeService runtimeService, String processInstanceId, String activityId) { runtimeService.createProcessInstanceModification(processInstanceId) .startBeforeActivity(activityId) // 在当前活动前启动 .addExecution() // 添加一个执行分支 .callActivity("dynamicTask") // 调用一个动态任务活动 .setVariable("reviewer", "李四") // 设置变量 .setVariable("dynamicTaskName", "财务经理审核") // 动态任务名称 .execute(); System.out.println("动态任务添加成功"); } /** * 初始化流程引擎 */ private static ProcessEngine initProcessEngine() { ProcessEngineConfiguration cfg = new StandaloneProcessEngineConfiguration() .setJdbcUrl("jdbc:h2:mem:flowable;DB_CLOSE_DELAY=-1") .setJdbcUsername("sa") .setJdbcPassword("") .setJdbcDriver("org.h2.Driver") .setDatabaseSchemaUpdate(ProcessEngineConfiguration.DB_SCHEMA_UPDATE_TRUE); return cfg.buildProcessEngine(); } } <?xml version="1.0" encoding="UTF-8"?> <definitions xmlns="http://www.omg.org/spec/BPMN/20100524/MODEL" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns:flowable="http://flowable.org/bpmn" targetNamespace="http://www.flowable.org/processdef"> <process id="dynamicProcess" name="动态节点示例流程" isExecutable="true"> <startEvent id="startEvent" name="开始"/> <sequenceFlow id="flow1" sourceRef="startEvent" targetRef="firstTask"/> <userTask id="firstTask" name="部门经理审批" flowable:assignee="${applicant}"> <documentation>申请金额: ${amount}</documentation> </userTask> <sequenceFlow id="flow2" sourceRef="firstTask" targetRef="dynamicTask"/> <userTask id="dynamicTask" name="动态任务" flowable:assignee="${reviewer}"> <documentation>这是一个可以被动态添加的任务节点</documentation> </userTask> <sequenceFlow id="flow3" sourceRef="dynamicTask" targetRef="endEvent"/> <endEvent id="endEvent" name="结束"/> </process> </definitions> 在Flowable 6.x中动态添加节点主要有以下几种方法： 主要方法 流程实例修改 - 通过RuntimeService提供的API： runtimeService.createProcessInstanceModification(processInstanceId) .startBeforeActivity("activityId") // 在指定活动前启动 .execute(); 动态子流程注入 - 在运行时动态添加子流程： runtimeService.createProcessInstanceModification(processInstanceId) .addExecution() .callActivity("subProcessId") .execute(); 动态创建活动 - 直接创建和执行新活动： runtimeService.createProcessInstanceModification(processInstanceId) .startBeforeActivity("userTaskId") .setVariable("assignee", "动态分配的用户") .execute(); 使用场景 动态添加节点在以下场景特别有用： 基于业务规则动态调整审批流程 根据申请金额/内容添加额外的审核步骤 特殊情况下插入临时审核/处理节点 灵活处理流程异常情况 实现要点 确保节点ID唯一性：动态添加的节点必须有唯一的ID 维护执行链的完整性：确保流程能正确流转 变量传递：为动态节点设置必要的流程变量 权限管理：动态添加的任务需要正确分配处理人 注意事项 过度使用动态节点会使流程追踪和维护变得困难 动态节点可能不会显示在流程图上，需要额外的审计记录 升级到Flowable 7时，部分API可能会有变化 确保动态添加的节点在流程异常时能够被正确处理 这个示例展示了如何在Flowable 6.x版本中动态添加一个审核任务节点。你可以根据实际需求调整代码，例如添加多个节点、条件节点或并行节点。 结论 通过深入分析Flowable的源码，我们可以看到它采用了许多优秀的设计模式： 命令模式：封装所有流程操作 工厂模式：创建各种对象 构建器模式：配置引擎 策略模式：不同的活动行为 观察者模式：事件监听机制 这些设计使得Flowable既灵活又强大，能够适应各种复杂的业务流程需求。同时，它的性能优化策略也确保了在高负载环境下的稳定运行。 对于想要深入了解工作流引擎实现的开发者，Flowable的源码提供了一个很好的学习案例。通过理解其内部机制，我们可以更好地使用和扩展这个强大的引擎。 参考资源 Flowable GitHub仓库：https://github.com/flowable/flowable-engine Flowable官方文档：https://www.flowable.org/docs/userguide/index.html BPMN 2.0规范：https://www.omg.org/spec/BPMN/2.0/
分布式锁如何自动续期（Watchdog 机制解析） 在分布式系统中，分布式锁广泛应用于防止多个进程同时操作共享资源。其中，基于 Redis 的分布式锁（如 Redisson）提供了一种高效的实现方式。然而，锁的超时管理是一个关键问题：如果业务执行时间较长，锁可能会在任务完成前过期，从而导致多个客户端同时持有锁，造成数据竞争。为了解决这个问题，我们可以使用 Watchdog（看门狗）机制，实现分布式锁的自动续期。 1. 分布式锁的基本流程 以 Redis 为例，分布式锁的获取和释放流程通常如下： 客户端1 发送 SET lock_key unique_id NX PX 30000 请求，尝试获取锁。 Redis 服务器 存储 lock_key，过期时间设定为 30 秒。 客户端1 成功获取锁，开始执行任务。 如果任务在 30 秒内完成，客户端1 发送 DEL lock_key 释放锁。 其他客户端（如 客户端2）在锁释放后才能成功获取锁。 上述流程有一个关键问题：如果 客户端1 任务执行时间超过 30 秒，但还未完成，锁会被 Redis 自动删除，从而导致其他客户端获取锁，出现并发问题。 2. Watchdog 机制的作用 为了解决锁过期的问题，Redisson 等库引入了 Watchdog 机制。Watchdog 的基本原理如下： 客户端1 获取锁后，启动 Watchdog 线程，每隔 lockLeaseTime / 3 秒（例如 10 秒）检查锁是否仍然持有。 如果 客户端1 仍然活跃，则自动向 Redis 发送 PEXPIRE lock_key 30000 请求，延长锁的有效期。 当 客户端1 任务执行完成 时，主动调用 DEL lock_key 释放锁。 如果 客户端1 进程崩溃，Watchdog 不会继续续期，锁会在到期后自动释放。 3. Watchdog 续期的流程示意 根据你的流程图，分布式锁 Watchdog 续期的流程如下： 客户端1 获取锁（SET lock_key unique_id NX PX 30000）。 客户端1 启动 Watchdog 线程，间隔 10 秒检查锁状态。 Watchdog 检查 Redis 是否仍然持有 lock_key。 如果 lock_key 仍然属于 客户端1，执行 PEXPIRE lock_key 30000 延长锁的有效期。 如果 客户端1 任务完成，调用 DEL lock_key 释放锁，Watchdog 线程退出。 如果 客户端1 进程意外终止，Watchdog 失效，锁超时后自动释放。 4. Watchdog 机制的优缺点 优点 自动续期：任务执行时间不确定时，确保锁不会过早释放。 进程故障自动释放：如果进程崩溃，锁在超时后自动释放，避免死锁。 减少人为干预：无需手动计算任务时间，提高系统健壮性。 缺点 需要持久化心跳检测：Watchdog 线程需要定期与 Redis 通信，增加了 Redis 负载。 续期间隔需优化：如果 Watchdog 续期间隔过长，锁可能仍然会过期。 5. 代码示例（基于 Redisson） // 创建 Redisson 客户端 RedissonClient redisson = Redisson.create(); // 获取分布式锁 RLock lock = redisson.getLock("myLock"); boolean locked = lock.tryLock(10, 30, TimeUnit.SECONDS); if (locked) { try { // 执行业务逻辑 System.out.println("业务处理中..."); Thread.sleep(40000); // 模拟业务执行超时 } finally { // 释放锁 lock.unlock(); } } 在 Redisson 中，如果 lock.tryLock() 仅传递 租约时间 lockLeaseTime，则默认会启用 Watchdog 机制，自动续期，直到 unlock() 被调用。 6. 总结 Watchdog 机制通过定期续约，避免 Redis 锁过期导致的并发问题。 适用于执行时间不确定的任务，能够在任务完成前自动续期。 需要权衡 Watchdog 续期频率和 Redis 负载，以优化性能。 如果你的业务场景对分布式锁有较高的可靠性要求，建议使用 Redisson 内置的 Watchdog 机制，而非手动管理锁的生命周期。
DDD架构下的工程目录结构划分详解 在领域驱动设计(Domain-Driven Design, DDD)中，合理的工程目录结构对于实现复杂业务逻辑的清晰组织至关重要。本文将详细介绍基于DDD思想的工程目录划分方案，帮助开发团队更好地组织代码，提高系统的可维护性和可扩展性。 DDD工程目录的四层架构 根据思维导图所示，一个标准的DDD架构通常分为四个主要层次： 1. 接口层（interfaces） 接口层是系统与外部交互的门户，负责处理来自外部的请求，并将请求转发到应用层。 facade（统一对外接口）：提供RESTful API接口，处理HTTP请求和响应 config（spring mvc相关配置）：包含接口层相关的配置，如控制器配置、请求拦截器等 src/main/java/com/company/project/interfaces/ ├── facade/ # 外观模式，统一接口 │ ├── dto/ # 数据传输对象 │ ├── assembler/ # DTO与领域对象转换 │ └── controller/ # 控制器 └── config/ # 接口层配置 2. 应用层（application） 应用层是业务流程的协调者，负责组织领域对象完成特定的应用功能。 service（应用服务接口和实现包）：协调领域对象完成业务流程 assembler（DTO与Entity互相转换包）：负责应用层与领域层数据模型的相互转换 event（事件发布/订阅） ： publish（事件发布包）：发布领域事件 subscribe（消息订阅包）：订阅并处理领域事件 src/main/java/com/company/project/application/ ├── service/ # 应用服务 │ ├── impl/ # 应用服务实现 │ └── dto/ # 应用服务数据传输对象 ├── assembler/ # DTO与领域对象转换 └── event/ # 事件处理 ├── publish/ # 事件发布 └── subscribe/ # 事件订阅 3. 领域层（domain） 领域层是业务核心，包含业务规则和逻辑。 aggregate（聚合包） ：聚合是一组关联的实体和值对象的集合 entity（聚合根、实体、值对象）：领域模型核心元素 service（领域服务，处理特殊领域内业务）：处理跨实体的业务逻辑 src/main/java/com/company/project/domain/ ├── aggregate/ # 聚合 │ ├── order/ # 订单聚合（示例） │ │ ├── Order.java # 聚合根 │ │ ├── OrderItem.java # 实体 │ │ ├── OrderStatus.java # 值对象 │ │ └── OrderRepository.java # 仓储接口 │ └── user/ # 用户聚合（示例） └── service/ # 领域服务 └── impl/ # 领域服务实现 4. 基础设施层（infrastructure） 基础设施层为其他层提供技术支持。 repository（仓储数据库ORM层） ： po（数据库对象）：数据库表映射对象 assembler（entity转po）：领域对象与数据库对象转换 domain.impl（领域服务实现包）：领域服务的具体实现 config（基础配置）：系统基础配置 utils（基础工具类）：通用工具类 src/main/java/com/company/project/infrastructure/ ├── repository/ # 仓储实现 │ ├── po/ # 持久化对象 │ ├── assembler/ # 领域对象与持久化对象转换 │ ├── mapper/ # MyBatis映射接口 │ └── impl/ # 仓储接口实现 ├── domain/ # 领域层实现 │ └── impl/ # 领域服务实现类 ├── config/ # 基础配置 └── utils/ # 工具类 目录结构的实践建议 按领域划分而非技术功能：在DDD中，首先应该按照业务领域进行划分，而非按照技术功能（如controller、service、dao等） 考虑限界上下文：不同的业务上下文应该有清晰的边界，可以在目录结构中体现 聚合根作为目录划分的基础：每个聚合根可以作为一个子目录，包含相关的实体和值对象 保持领域逻辑的纯净：领域层不应依赖其他层，特别是不应依赖基础设施层 接口与实现分离：定义清晰的接口，并将实现放在适当的包中 示例目录结构（电子商务系统） src/main/java/com/ecommerce/ ├── interfaces/ │ ├── facade/ │ │ ├── order/ # 订单接口 │ │ └── product/ # 产品接口 │ └── config/ # 接口配置 ├── application/ │ ├── service/ │ │ ├── order/ # 订单应用服务 │ │ └── product/ # 产品应用服务 │ ├── assembler/ # DTO转换器 │ └── event/ │ ├── publish/ # 事件发布 │ └── subscribe/ # 事件订阅 ├── domain/ │ ├── aggregate/ │ │ ├── order/ # 订单聚合 │ │ ├── product/ # 产品聚合 │ │ └── user/ # 用户聚合 │ └── service/ # 领域服务 └── infrastructure/ ├── repository/ │ ├── po/ # 持久化对象 │ ├── assembler/ # 对象转换 │ └── impl/ # 仓储实现 ├── domain/ # 领域实现 ├── config/ # 系统配置 └── utils/ # 工具类 结语 DDD在项目中的应用还有待进一步验证，记录一下DDD业务服务的规划。
ShardingSphere使用中的重点问题剖析 一、引言 ShardingSphere是一款强大的分布式数据库中间件，在助力应用实现数据分片、读写分离等功能方面表现卓越。然而，在实际应用过程中，会遇到一些关键问题。本文将结合思维导图，深入探讨ShardingSphere在读写分离和事务处理方面的重点问题及应对策略。 二、读写分离相关问题 （一）数据库主从同步延迟问题 在读写分离架构下，主库负责写入操作，从库承担读取操作。但由于主从库之间的同步存在延迟，可能导致从库读取到的数据并非最新。 解决方案一：提供同一个线程的Ddl执行统一库。即对于在同一个线程内的一系列数据定义语言（DDL）操作，确保都在主库上执行，这样可以避免主从同步延迟带来的数据不一致问题，因为在同一线程内操作主库，能保证数据的实时性和一致性。 解决方案二：提供Hint强制读写库。通过特定的Hint机制，开发者可以在代码中明确指定某些查询操作必须在主库上执行，绕过从库，从而获取最新的数据，适用于对数据实时性要求极高的场景，如金融交易中的账户余额查询等。 （二）强制读主问题 当应用需要获取最新数据时，可能会面临强制读主的需求。但在现有的读写分离架构下，常规的基于对象关系映射（ORM）的基础增删改查（CRUD）操作可能无法直接满足这一要求。 解决方法：需要重构ORM封装的基础CRUD。通过对ORM框架进行定制化开发或扩展，使其能够支持根据业务需求灵活选择从主库还是从库进行读取操作。例如，在特定的业务方法中，通过修改ORM的查询逻辑，添加强制读主的标识，让查询直接路由到主库，保证数据的及时性。 （三）Sql兼容问题 在使用ShardingSphere进行读写分离时，部分SQL语句可能会出现兼容性问题。 部分Sql语句不支持，需要换语句：例如“Select Distinct Test_Int From T_Order;”这样的语句可能无法直接在ShardingSphere环境下正常执行。此时，开发者需要根据ShardingSphere的语法和规则，对SQL语句进行改写，以满足数据库的操作要求。 Union（合并）相关语句示例：像“Select User_Id, Order_Id, Status From T_Order Union All Select Item_Id, Order_Id, User_Id From T_Order_Item”这样的Union操作语句，在使用时也需要注意其在ShardingSphere中的兼容性。可能需要对表结构、字段类型等进行进一步的检查和调整，确保Union操作能够正确执行，实现数据的合并查询需求。 三、事务问题 （一）Sharding-Jdbc不支持强事务 Sharding-Jdbc作为ShardingSphere的一种使用模式，在事务处理方面存在一定局限性，它不支持传统的强事务（如严格的ACID事务）。 解决策略一：尽量在业务处理事务。在应用层的业务逻辑中，通过合理的流程设计和代码编写，将相关的操作进行分组和协调，以满足业务对数据一致性的要求。例如，在电商系统的下单流程中，通过在业务代码中依次处理库存扣减、订单生成等操作，并进行异常捕获和回滚处理，来模拟事务的一致性效果。 解决策略二：使用Sharding-Proxy，支持多种分布式事务。Sharding-Proxy提供了对分布式事务更好的支持，它可以采用两阶段提交（2PC）、TCC（Try - Confirm - Cancel）等多种分布式事务解决方案。对于一些对事务一致性要求较高的场景，如银行转账业务，可以使用Sharding-Proxy结合合适的分布式事务方案，确保跨库操作的数据一致性。 四、总结 ShardingSphere在实现读写分离和处理事务时，虽然存在一些棘手的问题，但通过合理的技术选型和针对性的解决方案，能够有效应对。在实际项目中，开发者需要深入理解这些问题的本质和解决方案的原理，根据业务场景的特点，灵活运用相关策略，以充分发挥ShardingSphere的优势，构建高效、稳定、可靠的分布式数据库应用系统。
聊聊Hystrix 一、引言 在分布式系统中，服务之间的调用错综复杂，一个服务的故障可能会引发连锁反应，导致整个系统的雪崩效应。Hystrix作为一款强大的容错库，通过一系列机制保障系统的稳定性和可用性。本文将深入探讨Hystrix的核心原理。 二、Hystrix线程隔离原理 （一）初始化 Set参数对象：Hystrix在初始化时，需要设置一系列参数对象，包括分组（用于将相关的命令归为一组，方便资源管理和统计）、名称（标识具体的命令）、队列（用于存放待执行的任务） 、线程数（指定线程池中的线程数量，决定了能同时处理的请求量）以及线程池Key（用于唯一标识线程池）。 创建线程池（Threadpoolexcutor）：根据设置的参数创建线程池，线程池是Hystrix实现线程隔离的关键组件。通过线程池，不同的服务调用可以在各自独立的线程中执行，避免了一个服务调用的阻塞影响其他调用。 存入Concurrenthashmap：将创建好的线程池存入Concurrenthashmap中，便于后续快速获取和管理。 （二）执行Command的方式 Execute()：以同步的堵塞方式执行Run() 方法。这意味着调用线程会一直等待命令执行完成并返回结果，如果命令执行时间较长，调用线程会被阻塞，适用于需要立即获取结果且对响应时间要求不特别高的场景。 Queue()：采用异步阻塞方式执行Run() 。它会立即返回一个Future对象，调用线程可以通过这个Future对象在后续合适的时机获取执行结果，在获取结果之前，调用线程不会被一直阻塞，适用于一些后台任务处理场景。 Observe()：在事件注册前执行Run()/Construct() 。它返回一个Observable对象，通过订阅这个对象可以获取命令执行结果，并且可以在命令执行前进行一些事件注册和预处理操作。 Toobservable()：在事件注册后执行Run()/Construct() 。同样返回Observable对象，不过是在事件注册之后才开始执行命令逻辑，提供了更灵活的事件处理和结果订阅机制。 （三）底层使用Rxjava Hystrix的底层实现依赖于Rxjava，Rxjava是一个基于事件流、实现异步操作的库。它提供了丰富的操作符来处理异步任务、事件流的组合和转换等。在Hystrix中，利用Rxjava的特性可以更方便地实现异步执行、事件监听以及对命令执行结果的处理等功能，使得Hystrix的异步操作更加简洁和高效。 三、Hystrix的其他关键机制 （一）熔断器（Circuit Breaker） 熔断器就像一个开关，当服务调用的失败率达到一定阈值（比如在一定时间内失败率超过50% ），熔断器会被打开，后续对该服务的调用将不再实际执行，而是直接返回一个预设的 fallback 结果（比如默认值或者缓存数据）。这样可以快速失败，避免大量无效的调用，防止故障扩散。当熔断器打开一段时间后（称为休眠窗口），会尝试半开状态，允许少量请求通过，来检测服务是否已经恢复正常，如果恢复则关闭熔断器，否则继续保持打开状态。 （二）降级（Fallback） 降级是Hystrix的重要保护机制。当服务调用失败（如超时、异常等情况）或者熔断器打开时，会触发降级逻辑。开发人员可以自定义fallback方法，返回默认值、缓存数据或者一些简单的错误提示信息等，从而保证即使在服务不可用的情况下，系统仍能给用户提供一定的响应，而不是直接报错，提升了用户体验和系统的稳定性。 （三）请求缓存（Request Caching） Hystrix支持请求缓存功能，在一次请求过程中，如果相同的命令被多次调用，Hystrix可以从缓存中直接获取结果，而不需要重新执行命令。这在一些频繁调用相同服务且结果相对稳定的场景下，可以大大减少不必要的服务调用，提高系统的响应速度和性能。 四、总结 Hystrix通过线程隔离、熔断器、降级、请求缓存等一系列精心设计的原理和机制，为分布式系统构建了强大的容错体系。理解这些原理，有助于我们在实际开发中合理配置和使用Hystrix，提升系统的健壮性和可靠性，更好地应对复杂多变的分布式环境中的各种故障和挑战。 五 实操 1. 引入依赖 在pom.xml文件中添加相关依赖： <dependency> <groupId>org.springframework.cloud</groupId> <artifactId>spring - cloud - starter - hystrix</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring - boot - starter - web</artifactId> </dependency> 2. 启用Hystrix 在Spring Boot应用的主类上添加@EnableHystrix注解来启用Hystrix： import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.cloud.netflix.hystrix.EnableHystrix; @SpringBootApplication @EnableHystrix public class HystrixApplication { public static void main(String[] args) { SpringApplication.run(HystrixApplication.class, args); } } 3. 线程隔离与命令执行示例 创建一个服务类，定义一个使用HystrixCommand的方法： import com.netflix.hystrix.HystrixCommand; import com.netflix.hystrix.HystrixCommandGroupKey; import com.netflix.hystrix.HystrixCommandProperties; public class HelloWorldHystrixCommand extends HystrixCommand<String> { private final String name; public HelloWorldHystrixCommand(String name) { super(Setter.withGroupKey(HystrixCommandGroupKey.Factory.asKey("ExampleGroup")) .andCommandPropertiesDefaults( HystrixCommandProperties.Setter() .withExecutionTimeoutInMilliseconds(5000) // 设置执行超时时间 )); this.name = name; } @Override protected String run() { // 模拟实际的服务调用，这里简单返回问候语 return "Hello, " + name + "!"; } @Override protected String getFallback() { // 降级逻辑，当run方法执行失败时调用 return "Sorry, something went wrong while greeting you, " + name + "!"; } } 使用这个命令类： public class Main { public static void main(String[] args) { HelloWorldHystrixCommand command = new HelloWorldHystrixCommand("John"); // 同步执行方式 String result = command.execute(); System.out.println(result); // 异步执行方式 command.queue(); } } 4. 基于Spring Boot和注解的Hystrix使用 创建一个服务接口和实现类： import org.springframework.stereotype.Service; @Service public class HelloService { @com.netflix.hystrix.contrib.javanica.annotation.HystrixCommand(fallbackMethod = "fallbackHello") public String sayHello(String name) { // 模拟可能失败的服务调用，比如网络延迟或异常 if (Math.random() < 0.2) { // 20%的概率模拟失败 throw new RuntimeException("Service error"); } return "Hello, " + name + "!"; } public String fallbackHello(String name) { return "Sorry, can't say hello right now, " + name + "!"; } } 创建一个控制器类来调用服务： import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RequestParam; import org.springframework.web.bind.annotation.RestController; @RestController public class HelloController { @Autowired private HelloService helloService; @GetMapping("/hello") public String hello(@RequestParam String name) { return helloService.sayHello(name); } } 以上代码分别展示了Hystrix基本命令的使用方式，包括线程隔离下的命令执行以及基于Spring Boot和注解的服务熔断降级功能。实际应用中，可根据业务需求进一步调整配置和逻辑 。
Spring Security OAuth2在微前端架构下实现统一退出 在微前端架构中，当一个应用退出登录时，实现所有应用同步退出的功能是一个常见需求。这个问题主要源于微前端架构下各个子应用之间的分离特性，导致一个应用注销后，其他应用仍保持登录状态。下面我将详细介绍如何解决这个问题。 问题分析 微前端架构下，登出问题主要有以下几个原因： 各子应用可能使用独立的会话管理 OAuth2 token可能在各应用中独立存储 缺乏统一的退出机制来协调各个子应用 解决方案 1. 实现单点登出(Single Sign-Out) @RestController public class LogoutController { private final TokenStore tokenStore; private final ClientRegistrationRepository clientRegistrationRepository; public LogoutController(TokenStore tokenStore, ClientRegistrationRepository clientRegistrationRepository) { this.tokenStore = tokenStore; this.clientRegistrationRepository = clientRegistrationRepository; } @PostMapping("/api/logout") public ResponseEntity<Map<String, String>> logout( @RequestParam(value = "token", required = false) String token, HttpServletRequest request, HttpServletResponse response, @AuthenticationPrincipal OAuth2User principal) { // 获取当前认证信息 Authentication auth = SecurityContextHolder.getContext().getAuthentication(); // 如果存在认证信息，则进行处理 if (auth != null) { // 撤销token if (auth instanceof OAuth2AuthenticationToken) { OAuth2AuthenticationToken oauthToken = (OAuth2AuthenticationToken) auth; String clientRegistrationId = oauthToken.getAuthorizedClientRegistrationId(); // 根据需要调用OAuth2服务器的撤销接口 revokeToken(clientRegistrationId, token); } // 清除安全上下文 SecurityContextHolder.clearContext(); // 使session失效 HttpSession session = request.getSession(false); if (session != null) { session.invalidate(); } // 清除相关cookie deleteCookies(request, response); } Map<String, String> result = new HashMap<>(); result.put("message", "成功退出登录"); return ResponseEntity.ok(result); } private void revokeToken(String clientRegistrationId, String token) { try { ClientRegistration clientRegistration = clientRegistrationRepository.findByRegistrationId(clientRegistrationId); if (clientRegistration != null && token != null) { // 根据OAuth2服务器提供的撤销端点进行token撤销 String revokeEndpoint = clientRegistration.getProviderDetails().getConfigurationMetadata().get("revocation_endpoint").toString(); RestTemplate restTemplate = new RestTemplate(); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); MultiValueMap<String, String> map = new LinkedMultiValueMap<>(); map.add("token", token); map.add("client_id", clientRegistration.getClientId()); map.add("client_secret", clientRegistration.getClientSecret()); HttpEntity<MultiValueMap<String, String>> request = new HttpEntity<>(map, headers); restTemplate.postForEntity(revokeEndpoint, request, String.class); } } catch (Exception e) { // 处理可能的异常 logger.error("撤销token失败", e); } } private void deleteCookies(HttpServletRequest request, HttpServletResponse response) { Cookie[] cookies = request.getCookies(); if (cookies != null) { for (Cookie cookie : cookies) { // 删除认证相关的cookie if (cookie.getName().startsWith("OAUTH2_") || cookie.getName().equals("JSESSIONID") || cookie.getName().equals("SESSION")) { Cookie newCookie = new Cookie(cookie.getName(), null); newCookie.setPath("/"); newCookie.setMaxAge(0); newCookie.setHttpOnly(true); response.addCookie(newCookie); } } } } } 2. 创建统一的登出前端服务 // auth-service.js export class AuthService { constructor() { this.logoutListeners = []; this.setupLogoutListener(); } // 添加登出监听器 addLogoutListener(callback) { this.logoutListeners.push(callback); return () => { this.logoutListeners = this.logoutListeners.filter(cb => cb !== callback); }; } // 发送登出事件到所有注册的应用 notifyLogout() { // 通过BroadcastChannel发送消息 const bc = new BroadcastChannel('auth-logout-channel'); bc.postMessage({ type: 'LOGOUT' }); bc.close(); // 同时触发监听器 this.logoutListeners.forEach(callback => callback()); } // 监听其他应用的登出事件 setupLogoutListener() { const bc = new BroadcastChannel('auth-logout-channel'); bc.onmessage = (event) => { if (event.data.type === 'LOGOUT') { // 执行本地登出操作 this.logout(false); // 不广播，避免循环 } }; } // 登出方法 async logout(broadcast = true) { try { // 调用后端登出API const response = await fetch('/api/logout', { method: 'POST', credentials: 'include', headers: { 'Content-Type': 'application/json' } }); if (response.ok) { // 清除本地存储 localStorage.removeItem('oauth2_token'); sessionStorage.removeItem('oauth2_token'); // 清除相关cookie this.clearAuthCookies(); // 通知其他应用 if (broadcast) { this.notifyLogout(); } return true; } else { console.error('登出失败:', await response.text()); return false; } } catch (error) { console.error('登出过程中发生错误:', error); return false; } } // 清除认证相关cookie clearAuthCookies() { const cookies = document.cookie.split(';'); for (let i = 0; i < cookies.length; i++) { const cookie = cookies[i]; const eqPos = cookie.indexOf('='); const name = eqPos > -1 ? cookie.substring(0, eqPos).trim() : cookie.trim(); if (name.startsWith('OAUTH2_') || name === 'JSESSIONID' || name === 'SESSION') { document.cookie = `${name}=;path=/;expires=Thu, 01 Jan 1970 00:00:00 GMT`; } } } // 检查是否已登录 isAuthenticated() { // 根据实际存储方式检查认证状态 return localStorage.getItem('oauth2_token') !== null || sessionStorage.getItem('oauth2_token') !== null; } } // 导出单例 export const authService = new AuthService(); 3. 创建主应用的登出组件 // LogoutButton.jsx (React组件示例) import React from 'react'; import { authService } from './auth-service'; const LogoutButton = () => { const handleLogout = async () => { const success = await authService.logout(); if (success) { // 重定向到登录页或首页 window.location.href = '/login'; } else { alert('登出失败，请重试'); } }; return ( 退出登录 ); }; export default LogoutButton; 4. 为各个微前端应用添加登出监听 // 在每个子应用的入口文件中添加 import { authService } from 'shared-lib/auth-service'; class MicroApp { constructor() { this.init(); } init() { // 注册登出监听器 this.unsubscribeLogout = authService.addLogoutListener(() => { // 当收到登出事件时，清理本地状态 this.handleLogout(); }); // 监听卸载事件，移除监听器 window.addEventListener('unload', () => { if (this.unsubscribeLogout) { this.unsubscribeLogout(); } }); // 设置全局登出处理方法 window.microAppLogout = () => { authService.logout(); }; } handleLogout() { // 清理本地存储 localStorage.removeItem('app_state'); sessionStorage.removeItem('app_state'); // 重置应用状态 this.resetAppState(); // 可选：重定向到登录页 window.location.href = '/login'; } resetAppState() { // 重置应用状态逻辑 // ... } } // 初始化应用 const app = new MicroApp(); 5. 使用Redis实现会话共享 为了确保后端会话的统一管理，应使用Redis存储会话： @Configuration @EnableRedisHttpSession public class RedisSessionConfig { @Bean public RedisTemplate<String, Object> redisTemplate(RedisConnectionFactory connectionFactory) { RedisTemplate<String, Object> template = new RedisTemplate<>(); template.setConnectionFactory(connectionFactory); // 使用Jackson2为序列化和反序列化 Jackson2JsonRedisSerializer<Object> serializer = new Jackson2JsonRedisSerializer<>(Object.class); ObjectMapper mapper = new ObjectMapper(); mapper.setVisibility(PropertyAccessor.ALL, JsonAutoDetect.Visibility.ANY); mapper.activateDefaultTyping( LaissezFaireSubTypeValidator.instance, ObjectMapper.DefaultTyping.NON_FINAL, JsonTypeInfo.As.PROPERTY ); serializer.setObjectMapper(mapper); template.setKeySerializer(new StringRedisSerializer()); template.setValueSerializer(serializer); template.setHashKeySerializer(new StringRedisSerializer()); template.setHashValueSerializer(serializer); template.afterPropertiesSet(); return template; } @Bean public HttpSessionIdResolver httpSessionIdResolver() { // 使用Cookie和Header双重策略，提高灵活性 return new CookieAndHeaderHttpSessionIdResolver(); } } // 自定义会话ID解析器，支持Cookie和Header class CookieAndHeaderHttpSessionIdResolver implements HttpSessionIdResolver { private static final String HEADER_NAME = "X-Auth-Token"; private final CookieHttpSessionIdResolver cookieResolver = new CookieHttpSessionIdResolver(); private final HeaderHttpSessionIdResolver headerResolver = HeaderHttpSessionIdResolver.xAuthToken(); @Override public List<String> resolveSessionIds(HttpServletRequest request) { List<String> sessionIds = cookieResolver.resolveSessionIds(request); if (sessionIds.isEmpty()) { sessionIds = headerResolver.resolveSessionIds(request); } return sessionIds; } @Override public void setSessionId(HttpServletRequest request, HttpServletResponse response, String sessionId) { cookieResolver.setSessionId(request, response, sessionId); headerResolver.setSessionId(request, response, sessionId); } @Override public void expireSession(HttpServletRequest request, HttpServletResponse response) { cookieResolver.expireSession(request, response); headerResolver.expireSession(request, response); } } 6. 实现OAuth2统一退出端点 为了完全支持OAuth2退出，还需要实现一个注销端点连接到OAuth2服务器： @Configuration public class OAuth2LogoutConfig { private final ClientRegistrationRepository clientRegistrationRepository; public OAuth2LogoutConfig(ClientRegistrationRepository clientRegistrationRepository) { this.clientRegistrationRepository = clientRegistrationRepository; } @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(authorize -> authorize .requestMatchers("/api/public/**", "/login", "/oauth2/authorization/**").permitAll() .anyRequest().authenticated() ) .oauth2Login(oauth2 -> oauth2 .loginPage("/login") .defaultSuccessUrl("/dashboard") ) .logout(logout -> logout .logoutUrl("/api/logout") .logoutSuccessHandler(this.oidcLogoutSuccessHandler()) .invalidateHttpSession(true) .clearAuthentication(true) .deleteCookies("JSESSIONID", "SESSION") ); return http.build(); } private LogoutSuccessHandler oidcLogoutSuccessHandler() { OidcClientInitiatedLogoutSuccessHandler oidcLogoutSuccessHandler = new OidcClientInitiatedLogoutSuccessHandler(this.clientRegistrationRepository); // 设置退出后的重定向URL oidcLogoutSuccessHandler.setPostLogoutRedirectUri("{baseUrl}/login?logout=success"); return (request, response, authentication) -> { // 获取会话中的信息用于撤销token等操作 HttpSession session = request.getSession(false); if (session != null) { String idToken = (String) session.getAttribute("id_token"); if (idToken != null && authentication instanceof OAuth2AuthenticationToken) { OAuth2AuthenticationToken oauthToken = (OAuth2AuthenticationToken) authentication; ClientRegistration clientRegistration = this.clientRegistrationRepository .findByRegistrationId(oauthToken.getAuthorizedClientRegistrationId()); // 尝试撤销令牌 try { revokeTokens(clientRegistration, request); } catch (Exception e) { // 记录错误但继续退出流程 e.printStackTrace(); } } } // 调用OIDC退出处理器 oidcLogoutSuccessHandler.onLogoutSuccess(request, response, authentication); }; } private void revokeTokens(ClientRegistration clientRegistration, HttpServletRequest request) { // 实现令牌撤销逻辑 HttpSession session = request.getSession(false); if (session != null && clientRegistration != null) { String accessToken = (String) session.getAttribute("access_token"); String refreshToken = (String) session.getAttribute("refresh_token"); if (accessToken != null || refreshToken != null) { // 检查是否有撤销端点配置 Object revocationUri = clientRegistration.getProviderDetails() .getConfigurationMetadata().get("revocation_endpoint"); if (revocationUri != null) { RestTemplate restTemplate = new RestTemplate(); HttpHeaders headers = new HttpHeaders(); headers.setContentType(MediaType.APPLICATION_FORM_URLENCODED); headers.setBasicAuth(clientRegistration.getClientId(), clientRegistration.getClientSecret()); // 撤销访问令牌 if (accessToken != null) { MultiValueMap<String, String> body = new LinkedMultiValueMap<>(); body.add("token", accessToken); body.add("token_type_hint", "access_token"); HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(body, headers); restTemplate.postForEntity(revocationUri.toString(), entity, String.class); } // 撤销刷新令牌 if (refreshToken != null) { MultiValueMap<String, String> body = new LinkedMultiValueMap<>(); body.add("token", refreshToken); body.add("token_type_hint", "refresh_token"); HttpEntity<MultiValueMap<String, String>> entity = new HttpEntity<>(body, headers); restTemplate.postForEntity(revocationUri.toString(), entity, String.class); } } } } } } 实现要点解析 统一后端登出端点 清除服务器端会话 撤销OAuth2令牌 删除相关Cookie 通知OAuth2授权服务器 前端统一登出机制 使用BroadcastChannel进行跨应用通信 共享认证服务实现登出状态同步 提供全局登出方法 使用Redis共享会话 确保所有后端服务访问相同的会话存储 支持会话的快速失效 前端存储清理 清除localStorage和sessionStorage中的认证信息 删除认证相关Cookie 最佳实践 使用OpenID Connect(OIDC)标准 利用OIDC的标准登出流程 实现前后端退出的协调 使用统一的会话ID 为所有微前端应用使用相同的会话标识 避免使用多种会话管理方式 实现健壮的错误处理 当部分退出流程失败时仍能完成整体退出 提供清晰的用户反馈 安全考虑 确保退出端点的安全访问 防止CSRF攻击 结论 通过上述方案的实现，可以确保微前端架构下的统一退出机制，使得用户在任一子应用退出后，所有应用都同步退出登录状态。这种方案既保证了用户体验的一致性，也提高了系统的安全性，避免了部分应用仍保持登录状态可能带来的安全风险。 刚好再学react，就用react写前端了
larvel 实现注册eureka 项目需要，php部分功能作为微服务注册到eureka 如下实现 <?php namespace App\Services; use GuzzleHttp\Client; use GuzzleHttp\Exception\GuzzleException; use Illuminate\Support\Facades\Log; use Illuminate\Support\Facades\Cache; class EurekaService { protected $client; protected $config; protected $instanceId; protected $appName; protected $ipAddress; protected $port; protected $vipAddress; protected $heartbeatInterval; protected $eurekaUrl; protected $renewalTimer; /** * EurekaService 构造函数 */ public function __construct() { $this->client = new Client(); $this->loadConfig(); $this->setupInstance(); } /** * 从Laravel配置文件加载配置 */ protected function loadConfig() { $this->config = config('eureka'); $this->eurekaUrl = $this->config['eureka_url']; $this->appName = $this->config['app_name']; $this->ipAddress = $this->config['ip_address'] ?? gethostbyname(gethostname()); $this->port = $this->config['port'] ?? 80; $this->vipAddress = $this->config['vip_address'] ?? $this->appName; $this->heartbeatInterval = $this->config['heartbeat_interval'] ?? 30; $this->instanceId = $this->appName . ':' . $this->ipAddress . ':' . $this->port; } /** * 设置实例详细信息 */ protected function setupInstance() { // 默认实例数据 $this->instanceData = [ 'instance' => [ 'instanceId' => $this->instanceId, 'hostName' => $this->ipAddress, 'app' => strtoupper($this->appName), 'ipAddr' => $this->ipAddress, 'status' => 'UP', 'port' => [ '$' => $this->port, '@enabled' => 'true', ], 'securePort' => [ '$' => 443, '@enabled' => 'false', ], 'homePageUrl' => "http://{$this->ipAddress}:{$this->port}/", 'statusPageUrl' => "http://{$this->ipAddress}:{$this->port}/info", 'healthCheckUrl' => "http://{$this->ipAddress}:{$this->port}/health", 'dataCenterInfo' => [ '@class' => 'com.netflix.appinfo.InstanceInfo$DefaultDataCenterInfo', 'name' => 'MyOwn', ], 'leaseInfo' => [ 'renewalIntervalInSecs' => 30, 'durationInSecs' => 90, 'registrationTimestamp' => 0, 'lastRenewalTimestamp' => 0, 'evictionTimestamp' => 0, 'serviceUpTimestamp' => 0, ], 'metadata' => [ '@class' => 'java.util.Collections$EmptyMap', 'management.port' => (string)$this->port, ], 'vipAddress' => $this->vipAddress, 'secureVipAddress' => $this->vipAddress, 'isCoordinatingDiscoveryServer' => 'false', 'lastUpdatedTimestamp' => (string)(time() * 1000), 'lastDirtyTimestamp' => (string)(time() * 1000), 'actionType' => 'ADDED', ] ]; } /** * 向Eureka服务器注册应用 * * @return bool */ public function register() { try { $response = $this->client->request( 'POST', "{$this->eurekaUrl}/apps/{$this->appName}", [ 'headers' => [ 'Content-Type' => 'application/json', 'Accept' => 'application/json' ], 'json' => $this->instanceData ] ); if ($response->getStatusCode() === 204) { Log::info("成功将服务 {$this->appName} 注册到Eureka"); $this->startHeartbeat(); return true; } Log::error("向Eureka注册失败。状态码: " . $response->getStatusCode()); return false; } catch (GuzzleException $e) { Log::error("Eureka注册错误: " . $e->getMessage()); return false; } } /** * 启动心跳进程以保持注册活跃 */ protected function startHeartbeat() { // 存储缓存值以跟踪心跳是否活跃 Cache::put('eureka_heartbeat_active', true, now()->addDay()); // 在生产环境中，您可能希望使用更可靠的方法 // 如计划任务或队列工作器来处理心跳 $this->scheduleNextHeartbeat(); } /** * 安排下一次心跳 */ protected function scheduleNextHeartbeat() { // 这是一个简化的演示方法 // 在实际应用中，您应该使用Laravel的任务调度器或队列工作器 dispatch(function () { if (Cache::get('eureka_heartbeat_active', false)) { $this->sendHeartbeat(); $this->scheduleNextHeartbeat(); } })->delay(now()->addSeconds($this->heartbeatInterval)); } /** * 向Eureka发送心跳 * * @return bool */ public function sendHeartbeat() { try { $response = $this->client->request( 'PUT', "{$this->eurekaUrl}/apps/{$this->appName}/{$this->instanceId}", [ 'headers' => [ 'Content-Type' => 'application/json', 'Accept' => 'application/json' ] ] ); if ($response->getStatusCode() === 200) { Log::debug("已向Eureka发送 {$this->appName} 的心跳"); return true; } Log::warning("向Eureka发送心跳失败。状态码: " . $response->getStatusCode()); return false; } catch (GuzzleException $e) { Log::error("Eureka心跳错误: " . $e->getMessage()); return false; } } /** * 从Eureka服务器注销 * * @return bool */ public function deregister() { try { // 首先停止心跳 Cache::forget('eureka_heartbeat_active'); $response = $this->client->request( 'DELETE', "{$this->eurekaUrl}/apps/{$this->appName}/{$this->instanceId}" ); if ($response->getStatusCode() === 200) { Log::info("成功从Eureka注销服务 {$this->appName}"); return true; } Log::error("从Eureka注销失败。状态码: " . $response->getStatusCode()); return false; } catch (GuzzleException $e) { Log::error("Eureka注销错误: " . $e->getMessage()); return false; } } /** * 从Eureka获取所有服务 * * @return array|null */ public function getAllServices() { try { $response = $this->client->request( 'GET', "{$this->eurekaUrl}/apps", [ 'headers' => [ 'Accept' => 'application/json' ] ] ); if ($response->getStatusCode() === 200) { return json_decode($response->getBody()->getContents(), true); } Log::error("从Eureka获取服务失败。状态码: " . $response->getStatusCode()); return null; } catch (GuzzleException $e) { Log::error("从Eureka获取服务时出错: " . $e->getMessage()); return null; } } /** * 通过应用名称获取特定服务 * * @param string $appName * @return array|null */ public function getServiceByName($appName) { try { $response = $this->client->request( 'GET', "{$this->eurekaUrl}/apps/{$appName}", [ 'headers' => [ 'Accept' => 'application/json' ] ] ); if ($response->getStatusCode() === 200) { return json_decode($response->getBody()->getContents(), true); } Log::error("从Eureka获取服务 {$appName} 失败。状态码: " . $response->getStatusCode()); return null; } catch (GuzzleException $e) { Log::error("从Eureka获取服务 {$appName} 时出错: " . $e->getMessage()); return null; } } } <?php return [ /* |-------------------------------------------------------------------------- | Eureka 连接设置 |-------------------------------------------------------------------------- | | 配置连接到Eureka服务发现服务器的设置。 | */ // Eureka服务器URL 'eureka_url' => env('EUREKA_URL', 'http://localhost:8761/eureka'), // 您的应用名称（用于服务注册） 'app_name' => env('EUREKA_APP_NAME', 'laravel-service'), // 实例IP地址（如果未指定，默认为服务器IP） 'ip_address' => env('EUREKA_IP_ADDRESS', null), // 服务运行的端口 'port' => env('EUREKA_PORT', 80), // VIP地址（用于负载均衡的虚拟IP，如果未指定，默认为app_name） 'vip_address' => env('EUREKA_VIP_ADDRESS', null), // 心跳间隔（秒） 'heartbeat_interval' => env('EUREKA_HEARTBEAT_INTERVAL', 30), // 关于此实例的元数据（可选） 'metadata' => [ 'management.port' => env('EUREKA_MANAGEMENT_PORT', 80), 'environment' => env('APP_ENV', 'production'), ], ]; <?php namespace App\Providers; use App\Services\EurekaService; use Illuminate\Support\ServiceProvider; class EurekaServiceProvider extends ServiceProvider { /** * 注册服务 * * @return void */ public function register() { $this->mergeConfigFrom( __DIR__ . '/../../config/eureka.php', 'eureka' ); $this->app->singleton(EurekaService::class, function ($app) { return new EurekaService(); }); } /** * 引导服务 * * @return void */ public function boot() { $this->publishes([ __DIR__ . '/../../config/eureka.php' => config_path('eureka.php'), ], 'config'); // 仅在启用Eureka且不在控制台运行时注册 if (config('eureka.enabled', true) && !$this->app->runningInConsole()) { $eureka = $this->app->make(EurekaService::class); $eureka->register(); // 注册关闭函数，在应用程序关闭时注销 register_shutdown_function(function () use ($eureka) { $eureka->deregister(); }); } } } <?php namespace App\Http\Controllers; use Illuminate\Http\Request; use Illuminate\Support\Facades\DB; class HealthController extends Controller { /** * 返回应用程序健康状态 * * @return \Illuminate\Http\JsonResponse */ public function health() { $status = 'UP'; $components = []; // 检查数据库连接 try { DB::connection()->getPdo(); $components['database'] = [ 'status' => 'UP', 'type' => 'db', 'details' => [ 'database' => config('database.default'), ] ]; } catch (\Exception $e) { $status = 'DOWN'; $components['database'] = [ 'status' => 'DOWN', 'type' => 'db', 'details' => [ 'error' => $e->getMessage(), ] ]; } // 检查磁盘空间 $diskFree = disk_free_space('/'); $diskTotal = disk_total_space('/'); $diskThreshold = 0.1; // 10%空闲空间阈值 $diskStatus = ($diskFree / $diskTotal > $diskThreshold) ? 'UP' : 'DOWN'; if ($diskStatus === 'DOWN') { $status = 'DOWN'; } $components['diskSpace'] = [ 'status' => $diskStatus, 'details' => [ 'total' => $diskTotal, 'free' => $diskFree, 'threshold' => $diskThreshold, ] ]; return response()->json([ 'status' => $status, 'components' => $components, ]); } /** * 返回应用程序信息 * * @return \Illuminate\Http\JsonResponse */ public function info() { return response()->json([ 'app' => [ 'name' => config('app.name'), 'version' => config('app.version', '1.0.0'), 'environment' => config('app.env'), 'laravel' => app()->version(), ], ]); } } Laravel Eureka集成指南 本指南将帮助您将Laravel应用程序与Netflix的Eureka服务发现系统集成。此集成允许您的Laravel应用向Eureka注册，维持心跳，并发现其他服务。 1. 设置配置 首先，为Eureka创建一个新的配置文件： php artisan vendor:publish --provider="App\Providers\EurekaServiceProvider" --tag="config" 如果发布命令不起作用（因为我们尚未注册提供者），请手动创建配置文件： mkdir -p config 然后将eureka.php配置文件放在Laravel项目的config目录中。 2. 配置环境变量 将这些变量添加到您的.env文件中： EUREKA_URL=http://your-eureka-server:8761/eureka EUREKA_APP_NAME=your-service-name EUREKA_PORT=80 EUREKA_IP_ADDRESS=your-service-ip 3. 创建所需文件 按如下方式创建必要的服务和提供者文件： 首先，创建EurekaService类： mkdir -p app/Services 然后将EurekaService.php文件复制到此目录。 创建服务提供者： mkdir -p app/Providers 然后将EurekaServiceProvider.php文件复制到此目录。 创建用于Eureka健康检查的控制器： mkdir -p app/Http/Controllers 然后将HealthController.php文件复制到此目录。 4. 注册服务提供者 将Eureka服务提供者添加到config/app.php文件的providers数组中： 'providers' => [ // 其他提供者... App\Providers\EurekaServiceProvider::class, ], 5. 添加健康检查路由 将这些路由添加到您的routes/api.php文件中： Route::get('/health', [App\Http\Controllers\HealthController::class, 'health']); Route::get('/info', [App\Http\Controllers\HealthController::class, 'info']); 6. 测试集成 您可以通过以下方式测试您的Laravel应用程序是否成功注册到Eureka： 启动您的Laravel应用程序： php artisan serve 检查Eureka仪表板（通常在http://your-eureka-server:8761） 在已注册应用程序列表中查找您的服务 7. 使用服务发现 现在您可以使用EurekaService来发现其他服务： $eurekaService = app(App\Services\EurekaService::class); // 获取所有已注册服务的信息 $allServices = $eurekaService->getAllServices(); // 获取特定服务的信息 $userService = $eurekaService->getServiceByName('USER-SERVICE'); 8. 故障排除 如果遇到问题： 检查Laravel日志：storage/logs/laravel.log 验证Eureka服务器是否正在运行并可访问 检查您的Laravel应用和Eureka之间的网络连接 确保您应用的IP地址设置正确 9. 额外配置 对于生产用途，请考虑： 设置计划任务以实现更可靠的心跳： // 在 App\Console\Kernel.php 中 protected function schedule(Schedule $schedule) { $schedule->call(function () { app(App\Services\EurekaService::class)->sendHeartbeat(); })->everyThirtySeconds(); } 在选择服务实例时添加负载均衡逻辑 为服务调用实现断路器 <?php // routes/api.php use App\Http\Controllers\HealthController; use Illuminate\Support\Facades\Route; // Eureka健康检查端点 Route::get('/health', [HealthController::class, 'health']); Route::get('/info', [HealthController::class, 'info']); // 示例：创建使用其他Eureka服务的路由 Route::get('/service-example', function() { $eurekaService = app(\App\Services\EurekaService::class); // 示例：获取服务信息 $serviceInfo = $eurekaService->getServiceByName('EXAMPLE-SERVICE'); if (!$serviceInfo) { return response()->json(['error' => '未找到服务'], 404); } // 提取实例详细信息（简化示例） $instance = $serviceInfo['application']['instance'][0] ?? null; if (!$instance) { return response()->json(['error' => '没有可用实例'], 503); } $serviceUrl = "http://{$instance['ipAddr']}:{$instance['port']['$']}/api/endpoint"; // 在这里，您可以使用Guzzle或其他HTTP客户端调用服务 // $client = new \GuzzleHttp\Client(); // $response = $client->get($serviceUrl); return response()->json([ 'message' => '服务已发现', 'serviceUrl' => $serviceUrl ]); }); 我已经为Laravel应用程序实现了Eureka服务注册功能，并按要求将所有代码注释和说明改为中文。以下是实现方案的主要组件和使用方法： 主要组件 EurekaService 服务类 处理与Eureka服务器的所有交互 提供注册、心跳、注销和服务发现功能 配置文件 包含Eureka连接和注册所需的所有配置选项 支持通过环境变量自定义配置 服务提供者 注册EurekaService到Laravel容器 处理应用启动时的服务注册和关闭时的注销 健康检查控制器 提供健康检查和应用信息端点 符合Eureka的健康检查要求 路由配置 包含健康检查和服务发现示例的路由 安装指南 详细说明如何在Laravel项目中设置和配置Eureka集成 使用方法 将所有文件复制到相应的Laravel项目目录中 在.env文件中配置Eureka连接信息 注册服务提供者 启动应用程序，
Spring Security OAuth2与Spring Session Redis整合实现微服务会话共享
Spring Security OAuth2与Spring Session Redis整合实现微服务会话共享 引言 在微服务架构中，会话管理是一个常见的挑战。当用户通过OAuth2认证后，如何在多个微服务之间共享会话信息成为一个亟待解决的问题。本文将详细介绍如何利用Spring Security OAuth2结合Spring Session Redis实现微服务架构下的会话共享方案。 技术栈 Spring Boot Spring Security OAuth2 Spring Session Redis Spring Cloud (可选，用于服务发现等) 为什么选择Redis作为会话存储 在分布式系统中，使用Redis存储会话有以下优势： 高性能：Redis是基于内存的数据库，读写速度极快 数据持久化：支持数据持久化，防止数据丢失 丰富的数据结构：支持多种数据类型，适合存储复杂的会话数据 原生的过期机制：便于管理会话生命周期 实现步骤 1. 添加依赖 首先，我们需要在各个微服务的pom.xml文件中添加必要的依赖： <dependencies> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-web</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-client</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-oauth2-resource-server</artifactId> </dependency> <dependency> <groupId>org.springframework.boot</groupId> <artifactId>spring-boot-starter-data-redis</artifactId> </dependency> <dependency> <groupId>org.springframework.session</groupId> <artifactId>spring-session-data-redis</artifactId> </dependency> </dependencies> 2. 配置Redis与Spring Session 在每个微服务的application.yml文件中添加Redis和Spring Session的配置： spring: redis: host: localhost port: 6379 password: # 如有密码则填写 database: 0 session: store-type: redis redis: namespace: spring:session # Redis中存储会话的命名空间前缀 timeout: 1800 # 会话超时时间（秒） security: oauth2: client: registration: custom-client: client-id: client-id client-secret: client-secret authorization-grant-type: authorization_code redirect-uri: "{baseUrl}/login/oauth2/code/{registrationId}" scope: read,write provider: custom-provider: authorization-uri: http://auth-server/oauth/authorize token-uri: http://auth-server/oauth/token user-info-uri: http://auth-server/userinfo user-name-attribute: name server: servlet: session: cookie: http-only: true secure: true # 在生产环境中应设置为true 3. 启用Spring Session 在主应用类上添加@EnableRedisHttpSession注解： package com.example.microservice; import org.springframework.boot.SpringApplication; import org.springframework.boot.autoconfigure.SpringBootApplication; import org.springframework.session.data.redis.config.annotation.web.http.EnableRedisHttpSession; @SpringBootApplication @EnableRedisHttpSession public class MicroserviceApplication { public static void main(String[] args) { SpringApplication.run(MicroserviceApplication.class, args); } } 4. 配置Spring Security OAuth2 创建Security配置类，配置OAuth2认证： package com.example.microservice.config; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.web.SecurityFilterChain; @Configuration @EnableWebSecurity public class SecurityConfig { @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(authorize -> authorize .requestMatchers("/public/**").permitAll() .anyRequest().authenticated() ) .oauth2Login(oauth2 -> oauth2 .defaultSuccessUrl("/dashboard") .failureUrl("/login?error=true") ) .logout(logout -> logout .logoutSuccessUrl("/login?logout=true") .invalidateHttpSession(true) .deleteCookies("JSESSIONID") ); return http.build(); } } 5. 创建会话信息存储器 为了确保OAuth2认证信息能够在会话中正确存储和共享，创建自定义的会话信息存储器： package com.example.microservice.session; import org.springframework.security.core.Authentication; import org.springframework.security.core.context.SecurityContext; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.session.Session; import org.springframework.session.SessionRepository; import org.springframework.stereotype.Component; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpSession; import java.util.Map; @Component public class OAuth2SessionRepository<S extends Session> { private final SessionRepository sessionRepository; public OAuth2SessionRepository(SessionRepository sessionRepository) { this.sessionRepository = sessionRepository; } public void saveOAuth2Authentication(HttpServletRequest request, Authentication authentication) { HttpSession session = request.getSession(false); if (session != null) { String sessionId = session.getId(); S redisSession = sessionRepository.findById(sessionId); if (redisSession != null) { if (authentication instanceof OAuth2AuthenticationToken) { OAuth2AuthenticationToken oauthToken = (OAuth2AuthenticationToken) authentication; OAuth2User oauth2User = oauthToken.getPrincipal(); // 存储用户信息和授权信息 redisSession.setAttribute("user_name", oauth2User.getAttribute("name")); redisSession.setAttribute("user_email", oauth2User.getAttribute("email")); redisSession.setAttribute("oauth2_provider", oauthToken.getAuthorizedClientRegistrationId()); // 存储OAuth2授权信息（可根据需求扩展） Map<String, Object> attributes = oauth2User.getAttributes(); for (Map.Entry<String, Object> entry : attributes.entrySet()) { if (entry.getValue() instanceof String) { redisSession.setAttribute("oauth2_attr_" + entry.getKey(), entry.getValue()); } } sessionRepository.save(redisSession); } } } } public OAuth2User getOAuth2User(String sessionId) { S redisSession = sessionRepository.findById(sessionId); if (redisSession != null) { // 根据存储的会话信息重建OAuth2User // 这里仅为示例，实际实现需要根据具体情况调整 return null; // 此处需实现具体逻辑 } return null; } } 6. 创建认证成功处理器 实现一个认证成功处理器，在用户成功认证后将OAuth2信息保存到Redis会话中： package com.example.microservice.handler; import com.example.microservice.session.OAuth2SessionRepository; import org.springframework.security.core.Authentication; import org.springframework.security.web.authentication.AuthenticationSuccessHandler; import org.springframework.stereotype.Component; import javax.servlet.ServletException; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import java.io.IOException; @Component public class CustomAuthenticationSuccessHandler implements AuthenticationSuccessHandler { private final OAuth2SessionRepository<?> oAuth2SessionRepository; public CustomAuthenticationSuccessHandler(OAuth2SessionRepository<?> oAuth2SessionRepository) { this.oAuth2SessionRepository = oAuth2SessionRepository; } @Override public void onAuthenticationSuccess(HttpServletRequest request, HttpServletResponse response, Authentication authentication) throws IOException, ServletException { // 保存OAuth2认证信息到Redis会话 oAuth2SessionRepository.saveOAuth2Authentication(request, authentication); // 重定向到成功页面 response.sendRedirect("/dashboard"); } } 7. 更新Security配置，使用自定义认证成功处理器 package com.example.microservice.config; import com.example.microservice.handler.CustomAuthenticationSuccessHandler; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.Configuration; import org.springframework.security.config.annotation.web.builders.HttpSecurity; import org.springframework.security.config.annotation.web.configuration.EnableWebSecurity; import org.springframework.security.web.SecurityFilterChain; @Configuration @EnableWebSecurity public class SecurityConfig { private final CustomAuthenticationSuccessHandler authenticationSuccessHandler; public SecurityConfig(CustomAuthenticationSuccessHandler authenticationSuccessHandler) { this.authenticationSuccessHandler = authenticationSuccessHandler; } @Bean public SecurityFilterChain securityFilterChain(HttpSecurity http) throws Exception { http .authorizeHttpRequests(authorize -> authorize .requestMatchers("/public/**").permitAll() .anyRequest().authenticated() ) .oauth2Login(oauth2 -> oauth2 .successHandler(authenticationSuccessHandler) .failureUrl("/login?error=true") ) .logout(logout -> logout .logoutSuccessUrl("/login?logout=true") .invalidateHttpSession(true) .deleteCookies("JSESSIONID") ); return http.build(); } } 8. 创建一个拦截器，用于在请求中恢复认证信息 package com.example.microservice.interceptor; import org.springframework.security.core.context.SecurityContextHolder; import org.springframework.security.oauth2.client.authentication.OAuth2AuthenticationToken; import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.session.Session; import org.springframework.session.SessionRepository; import org.springframework.web.servlet.HandlerInterceptor; import org.springframework.web.servlet.ModelAndView; import javax.servlet.http.HttpServletRequest; import javax.servlet.http.HttpServletResponse; import javax.servlet.http.HttpSession; public class OAuth2AuthenticationInterceptor implements HandlerInterceptor { private final SessionRepository<? extends Session> sessionRepository; public OAuth2AuthenticationInterceptor(SessionRepository<? extends Session> sessionRepository) { this.sessionRepository = sessionRepository; } @Override public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) { // 检查是否已经有认证信息 if (SecurityContextHolder.getContext().getAuthentication() == null) { HttpSession httpSession = request.getSession(false); if (httpSession != null) { String sessionId = httpSession.getId(); Session redisSession = sessionRepository.findById(sessionId); if (redisSession != null) { // 从Redis会话中恢复OAuth2认证信息 // 这里需要实现具体的逻辑，根据存储的信息重建OAuth2AuthenticationToken OAuth2User oauth2User = createOAuth2UserFromSession(redisSession); String registrationId = (String) redisSession.getAttribute("oauth2_provider"); if (oauth2User != null && registrationId != null) { OAuth2AuthenticationToken authentication = new OAuth2AuthenticationToken(oauth2User, oauth2User.getAuthorities(), registrationId); SecurityContextHolder.getContext().setAuthentication(authentication); } } } } return true; } private OAuth2User createOAuth2UserFromSession(Session session) { // 根据会话中存储的信息重建OAuth2User对象 // 这里需要实现具体逻辑 return null; // 实际实现中需要返回有效的OAuth2User } @Override public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) { // 如果需要，可以在这里添加后处理逻辑 } @Override public void afterCompletion(HttpServletRequest request, HttpServletResponse response, Object handler, Exception ex) { // 如果需要，可以在这里添加完成后的逻辑 } } 9. 注册拦截器 package com.example.microservice.config; import com.example.microservice.interceptor.OAuth2AuthenticationInterceptor; import org.springframework.context.annotation.Configuration; import org.springframework.session.SessionRepository; import org.springframework.web.servlet.config.annotation.InterceptorRegistry; import org.springframework.web.servlet.config.annotation.WebMvcConfigurer; @Configuration public class WebConfig implements WebMvcConfigurer { private final SessionRepository<?> sessionRepository; public WebConfig(SessionRepository<?> sessionRepository) { this.sessionRepository = sessionRepository; } @Override public void addInterceptors(InterceptorRegistry registry) { registry.addInterceptor(new OAuth2AuthenticationInterceptor(sessionRepository)); } } 测试会话共享 创建一个控制器来测试会话共享功能： package com.example.microservice.controller; import org.springframework.security.core.annotation.AuthenticationPrincipal; import org.springframework.security.oauth2.core.user.OAuth2User; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import javax.servlet.http.HttpSession; import java.util.HashMap; import java.util.Map; @RestController public class TestController { @GetMapping("/api/user-info") public Map<String, Object> getUserInfo(@AuthenticationPrincipal OAuth2User oauth2User, HttpSession session) { Map<String, Object> userInfo = new HashMap<>(); if (oauth2User != null) { userInfo.put("name", oauth2User.getAttribute("name")); userInfo.put("email", oauth2User.getAttribute("email")); userInfo.put("authorities", oauth2User.getAuthorities()); } userInfo.put("sessionId", session.getId()); userInfo.put("creationTime", session.getCreationTime()); userInfo.put("lastAccessedTime", session.getLastAccessedTime()); return userInfo; } @GetMapping("/api/session-test") public Map<String, Object> testSession(HttpSession session) { // 获取或设置会话属性 Object count = session.getAttribute("counter"); int newCount = 1; if (count != null) { newCount = (Integer) count + 1; } session.setAttribute("counter", newCount); Map<String, Object> result = new HashMap<>(); result.put("sessionId", session.getId()); result.put("counter", newCount); result.put("message", "此计数在所有微服务中共享"); return result; } } 部署架构 在实际微服务架构中，通常会通过API网关统一管理认证和会话。这里是一个简化的部署架构图： +----------------+ +----------------+ | API Gateway | | Auth Service | | (Zuul/Spring |---->| (OAuth2 Server)| | Cloud Gateway)| +----------------+ +-------+--------+ | v +-------+--------+ +-------+--------+ | Microservice A |<--->| Redis Session | +----------------+ | Storage | ^ +----------------+ | ^ v | +-------+--------+ | | Microservice B |-------------+ +----------------+ 安全注意事项 在实现会话共享时，需要注意以下安全事项： 确保Redis服务器安全配置，启用密码认证 使用SSL/TLS加密Redis连接 会话ID应使用足够长的随机字符串，防止被猜测 设置合理的会话超时时间 使用secure和httpOnly标志保护会话Cookie 考虑使用JWT等无状态认证方案作为补充 性能优化 为了提高系统性能，可以考虑以下优化措施： 配置Redis连接池 使用Redis集群提高可用性 合理设置会话数据的过期时间 仅存储必要的会话信息，避免存储大量数据 监控Redis性能指标，及时进行扩容 总结 通过Spring Security OAuth2与Spring Session Redis的整合，我们实现了微服务架构下的会话共享方案。这种方案具有以下优势： 统一的认证机制：通过OAuth2提供标准的认证流程 高效的会话存储：利用Redis高性能特性存储会话数据 透明的会话共享：微服务无需感知会话存储细节 良好的扩展性：可以方便地扩展到更多微服务 这种方案适用于大多数微服务架构，特别是那些需要保持用户状态的系统。通过合理配置和优化，可以构建一个安全、高效、可扩展的会话管理系统。