@TransactionlEventListener 的事务问题

@TransactionalEventListener 是 Spring 框架提供的一个注解,用于在事务提交后或其他事务状态下执行事件监听器的方法。大多数情况下(也是默认的情况),我们一般会在事务提交后执行一些操作,例如发送消息、记录日志等。 @TransactionalEventListener 会在事务成功提交之后才触发监听器方法。如果我们对其运行机制理解不够深入,在使用过程中,很容易踩到坑。 问题说明 假设我们有这样一个业务场景:用户发表文章后需要记录用户操作,并在用户操作记录后发送通知。具体流程如下: 发表文章,发布"文章已发布"事件 监听文章发布事件,记录用户操作 操作记录保存后,发布"操作已记录"事件 处理操作记录后的通知发送逻辑 看起来很简单的流程,但实际实现时却踩到了坑。让我们看看具体的代码实现: @Service public class ArticleService { @Transactional public void publishArticle(Article article) { String articleId = "A" + System.currentTimeMillis(); article.setId(articleId); articleRepository.save(article); applicationContext.publishEvent(new ArticlePublishedEvent(articleId)); } } @Component public class ArticlePublishedEventListener { @TransactionalEventListener public void handle(ArticlePublishedEvent event) { userActionService.recordAction(event.getUserId(), "发表文章", event.getArticleId()); } } @Service public class UserActionService { @Transactional public void recordAction(Long userId, String action, String targetId) { UserAction userAction = new UserAction(userId, action, targetId); userActionRepository....

October 24, 2024

Data Modelling —— 实体分类

在设计复杂系统或数据库时,对实体进行分类可以帮助我们更好地理解它们的作用和功能,从而提高设计的结构性、可维护性和可追溯性。 以下是实体的一些常见分类: 记录型实体(Record-based Entities) 事务型实体(Transaction-based Entities) 日志型实体(Log-based Entities) 历史型实体(History-based Entities) 配置型实体(Configuration-based Entities) 记录型实体(Record-based Entities) 反映长期存在且具有相对稳定状态的数据实体。这类实体通常表示核心的业务对象,并且它们的生命周期较长。 例如: 用户(User):代表系统中的注册用户或个人,具有唯一的身份标识符和相关信息,如姓名、联系方式等。 账户(Account):金融机构中的银行账户或应用中的用户账户,记录长期状态如余额、账户信息等。 产品(Product):电商平台中的商品,描述着产品的规格、价格、库存等信息。 项目(Project):在管理系统中的项目实体,用于跟踪长期存在的任务或工作目标。 特点: 记录型实体通常具有唯一标识符(例如用户ID、账户号码、产品编号等),用于唯一标识每个对象。这些标识符可以方便地用于引用、查询和管理该实体。 在其生命周期中,数据可能会更新,但频率通常不高。例如,用户信息可能会因联系方式变更而更新,但不会频繁变化。 反映业务的核心对象,是业务运作的基础。通常与其他实体或模块有密切关联。 事务型实体(Transaction-based Entities) 事务型实体是用于描述在业务系统中具有动态过程和状态变化的数据实体。它们通常用于跟踪和记录特定的业务流程,并随着事务的推进而更新状态。 例如: 订单(Order):从创建到待支付、待发货、已完成等多个状态。 付款(Payment):从待发起、处理中到已成功、已失败等状态。 申请(Application):某角色提交申请后,申请状态可能经历待审查、已批准或已拒绝等状态。 特点: 每条记录通常代表一次特定的业务操作或流程,如一次订单交易、一次付款行为或一次申请过程。 具有明确的生命周期和状态,清晰的开始和结束状态。通过不同的业务事件推动状态变化。 数据可能在短期内频繁更新,但一旦事务完成,记录通常不再变化。 日志型实体(Log-based Entities) 用于记录系统中发生的事件、活动或操作,通常是按时间顺序排列。可用于监控、调试、审计和分析系统行为。 例如: 用户活动日志(UserActivityLog):跟踪用户在系统中的操作,如登录、浏览、操作等活动。 错误日志(ErrorLog):记录系统中发生的异常、错误或故障,便于后续分析和问题排查。 特点: 新事件发生时,数据按时间顺序追加(Append-only)到日志中。几乎不涉及更新或删除操作,数据为一次性写入,后续不再修改。 每条日志记录通常都会包含一个明确的时间戳,用于标记事件发生的时间。此外,还可能包含事件的来源、类型、发生的上下文等信息。 可能会定期归档旧日志或者清理过期数据。 历史型实体(History-based Entities) 用于保存实体或其字段在其生命周期内的多个状态或版本,记录着随时间的演变。 例如: 账户余额流水(AccountBalanceHistory):记录账户余额的变化情况,包括交易金额、交易后余额、交易类型等。 产品价格历史(ProductPriceHistory):记录每个产品的价格变动,包括调整日期和调整原因。 特点: 一旦历史记录被创建,通常不允许修改。 支持查询和追溯实体或字段在任意时间点的状态。 配置型实体(Configuration-based Entities) 配置型实体可分为以下 3 种表现形式: 常量形式 参数形式 关联形式 常量形式 用于存储有限的、离散的值集,来定义业务中的某些可选项或状态。 例如: 角色(Role):例如 管理员、用户、访客 等。 支付方式(PaymentMethod):例如 信用卡、借记卡、PayPal、支付宝、微信 等。 国家/地区代码(CountryCode):例如 CN(中国)、US(美国)、JP(日本)等。 特点:...

October 3, 2024

基于 ListForm 简化复杂查询开发

ListForm 属于 DevKit 中的一个模块。 在中后台系统的开发中,我们经常会遇到需要展示列表数据的场景,这些列表数据需要支持排序、筛选、分页等功能。为了应对复杂的查询逻辑,开发人员通常需要编写大量重复的代码,涉及过滤条件、排序规则、分页控制、关联实体查询等。而通过设计一个灵活的 ListForm,自动生成 buildQuery 方法的实现细节,可以极大地提高开发效率,减少冗余代码的编写。 什么是 ListForm? ListForm 是一种用于处理数据查询请求的表单类。它主要用于接收查询参数,例如过滤条件、排序规则、分页信息等。通过 ListForm,开发人员可以清晰地定义查询的输入参数,而不必关心查询的实现细节。 在 ListForm 中,可以包含以下几个关键要素: 过滤条件:支持基于字段的等值查询、范围查询、集合查询等。 排序规则:允许对多个字段进行排序,支持升序和降序。 分页控制:通过指定页码和每页大小来控制查询的结果集。 关联实体查询:支持查询与主实体关联的子实体信息。 复杂查询封装:例如关键词搜索、特定条件匹配等。 ListForm 有以下几个特点: 自动构建查询:根据声明的表单参数和注解自动生成具体的 buildQuery 逻辑。 类型安全:生成的代码是类型安全的,避免了手动拼接 SQL 语句查询,减少了运行时错误。 条件受控:通过定义控制查询条件的可见性,避免了不合理的查询方式和组合。 一个示例 下面是一个简单的 ListForm 定义示例: @Getter @Setter @ListForm public class PlatformEmployeeListForm extends ListFormBase<PlatformEmployeeQuery> { /* 只可根据 ID 排序,不可基于 ID 查询 */ @Sortable Void id; /* 下面 3 个默认 EQ 查询 */ String phoneNumber; Boolean admin; Boolean locked; /* 下面 3 个显式 @Filter 指定过滤类型和对应字段 */ @Filter(value = FilterType....

October 3, 2024

点击网络请求,定位到对应代码位置

最近接手了一个前端项目,在熟悉的过程中,经常需要在项目中定位页面中的某一组件或元素,普遍的方式是通过搜索页面上的关键字或者根据路由路径来查找对应的页面。不过,在这之前,我有了解过一个 Vite 插件,能够实现通过点击页面元素定位到对应代码位置,它就是 Code Inspector。正好,接手的这个项目也是基于 Vite 构建的,于是我尝试了一下这个插件,确实非常方便。然后,我就把这个插件分享给了团队的其他成员。对于前端开发人员来说,这个插件是一个非常实用的工具,可以提高开发效率,减少查找代码的时间,大家都很喜欢。这时,有一些后端开发人员问后端有没有类似的工具,例如通过点击网络请求,定位到对应的处理代码位置。我第一时间就去网上搜寻了一番,很遗憾的是没有找到类似的工具。不过,我觉得这个功能确实很有用,于是我就想着自己实现一个类似的工具。 说干就干,简单梳理了一下思路: 在后端每个请求处理的地方,响应一些标识信息,例如处理请求的源文件路径、行号等。 开发一个浏览器插件,用于监听后端开发服务处理的网络请求。列出这些请求,点击跳转到对应的源文件位置。 最后效果如下,点击请求路径可自动在 IDEA 中定位到处理请求的方法行: 响应标识信息 为了让浏览器插件能够区分出哪些请求是需要进行定位的,以及知道定位的代码文件路径和行号,我们可以利用 HTTP 响应头来响应这些标识信息。 要在后端服务响应这些标识信息,有好几种方案: 开发 Spring MVC Interceptor,拦截请求,响应标识信息。 开发 Aspect,切面拦截请求,响应标识信息。 开发 Maven 插件,编译时修改字节码,插入标识信息。 开发 JRebel 插件,运行时修改字节码,插入标识信息。 最后,我选择了方案 4,因为前面 3 种方案都需要修改项目代码,侵入性较大。 自定义 JRebel 插件 在 JRebel 的官方文档 Custom JRebel plugins 中有详细说明如何自定义 JRebel 插件。 首先,创建一个 Maven 项目,引入 JRebel 插件开发相关的依赖,并声明插件的入口类(me.ligang.jrebel.plugin.SourceLocationPlugin): <?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>me.ligang.jrebel.plugin</groupId> <artifactId>source-location-plugin</artifactId> <version>1.0-SNAPSHOT</version> <properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <sdk.version>7.0.0</sdk.version> </properties> <dependencies> <dependency> <groupId>org.zeroturnaround</groupId> <artifactId>jr-sdk</artifactId> <version>${sdk....

August 18, 2024

Next.js 中的 Route Handlers 和 Server Action 有什么区别?

本文首先会简单说明一下 Next.js 中的 Route Handlers 和 Server Action 是什么,然后再对比它们之间的区别,以及什么时候使用 Route Handlers,什么时候使用 Server Action。 Router Handlers 是什么? Route Handlers allow you to create custom request handlers for a given route using the Web Request and Response APIs. 在 Next.js 中,Route Handlers 允许你使用 Web 请求和响应 API,为给定的路由创建自定义请求处理程序。Route Handlers 只能在 app 目录中定义,等同 Pages Router 中在 pages 目录中的 API Routes。 下面的示例代码定义了 Router Handlers: // app/api/route.js export async function GET(request) { return new Response(JSON.stringify({ message: 'Hello, world!...

August 3, 2024

最近在开发 DevKit

最近在开发一个项目,觉得非常有趣,想要分享一下。这个项目不仅仅是一个框架、一个库或一个工具,它还涵盖了与日常开发密切相关的各个方面,可以看作是工作中所积累经验的总结沉淀。 我给它起了一个名字 DevKit,接着又为它想了一段简短的介绍: DevKit 是一个以开发者优先(Developer-first),在结构、框架、流程、工具和环境等多个方面提供最佳实践的开发解决方案,适用于开发以数据和业务流程驱动的应用。 它旨在解决的问题及所提供的方案: 方案 增强开发体验 提升开发效率 保障开发质量 减少沟通成本 洞察效能指标 项目级 DSL ✅ ✅ ✅ ✅ ✅ 提供运行时环境 ✅ ✅ ✅ N/A N/A 基础代码生成 ✅ ✅ ✅ ✅ N/A 简化开发流程 ✅ ✅ ✅ ✅ N/A 提供通用基础模块 ✅ ✅ ✅ ✅ N/A 提供编码最佳实践 ✅ ✅ ✅ ✅ N/A 类型安全的查询构建器 ✅ ✅ ✅ ✅ N/A 自动化标准约束 ✅ ✅ ✅ ✅ N/A 自动化安全检测 ✅ ✅ ✅ ✅ N/A IDE 插件支持 ✅ ✅ N/A ✅ N/A 自动化数据库迁移 ✅ ✅ N/A N/A N/A 随机测试数据 ✅ ✅ N/A N/A N/A 管理端的前后端生成 ✅ N/A N/A ✅ N/A 接口文档生成 ✅ ✅ N/A ✅ N/A 关系和流程图生成 ✅ ✅ N/A ✅ ✅ 业务流程文档生成 ✅ N/A N/A ✅ ✅ 数据库文档生成 ✅ N/A N/A ✅ N/A 实体文档生成 ✅ N/A N/A ✅ N/A 业务复杂度度量 N/A N/A N/A N/A ✅ AI 知识库 ✅ ✅ N/A ✅ N/A AI Agent ✅ ✅ N/A N/A N/A

July 28, 2024

在 VitePress 中为代码块添加自定义语言的语法高亮

最近在使用 VitePress 搭建技术文档网站,遇到了一个小问题。虽然 VitePress 的 Markdown 代码块支持非常多语言的语法高亮,但我们的文档中存在许多 DSL(领域特定语言)代码,这些代码没有现成的语言能够适配,只能以纯文本(Plain Text)样式显示,导致阅读起来非常不直观。因此,我决定为 VitePress 添加自定义的语言进行语法高亮,来改善这种情况。 通过阅读 VitePress 的文档 Syntax Highlighting in Code Blocks,我了解到它使用了 markdown-it 进行 Markdown 解析,并通过 Shiki 实现了代码块的语法高亮。不过,在我搜寻了一番 VitePress 的文档后,最终也没有找到有关于添加自定义语言进行语法高亮的具体说明。看来这个只能自己摸索了。 VitePress 文档中有提到可以通过配置 markdown 选项来自定义语法高亮的主题,我顺着查看了 jsdocs ,发现除了可以配置主题外,还有一个 shikiSetup 选项,可以用来自定义 Shiki 的 Highlighter 配置。 /** * Setup Shiki instance */ shikiSetup?: (shiki: Highlighter) => void | Promise<void> 接下来,我又查看了 Shiki 的文档 Load Custom Languages,得知 Highlighter 有一个 loadLanguage 方法,可以用来加载自定义语言。于是,我就有了具体的实现思路。 实现步骤 下面通过一个简单的例子来具体说明如何为 VitePress 添加自定义语言进行代码高亮。 Shiki 是基于 TextMate 的语法,如果我们要自定义语言的语法高亮,就需要先编写一个 TextMate 语法文件。先在 ....

July 27, 2024

JWT 替代 Session?

在构建 Web 应用程序时,用户认证和会话管理是关键部分。我发现,无论什么场景,很多开发者都倾向于使用 JWT(JSON Web Token)来替代传统的 Session 机制实现用户认证和会话管理。有些开发者甚至认为 Session 是旧时代的产物,应该被淘汰,使用 JWT 才是标准和未来,还有一些开发者甚至都没有真正去了解过 JWT。 很多文章都在讨论 JWT 和 Session 的不同,但我认为这种讨论是相对比较片面的。 大多数人对 Session 机制的偏见 许多人对 Session 机制缺乏深入了解,仅凭道听途说,因此形成了一些偏见: 认为 Session 消耗服务器资源,仅仅是因为 Session 数据都需要存储在内存中。 其实通过优化存储策略和清理机制,Session 对服务器资源的占用可以被有效管理,不会导致严重的资源消耗和性能问题。 认为 Session 不支持分布式,也是因为数据存储在应用服务内存中,无法实现或难以实现跨服务的共享。 在分布式场景下,可以通过分布式缓存服务(例如:Redis)来解决。在 Spring 中,也有针对性的实现(例如:Spring Session),对开发者而言是透明的,无需关心具体实现。 认为 Session 不支持跨域,因为 Session 是基于 Cookie 的,而 Cookie 有跨域限制。 实际上,Session 机制并不只依赖于 Cookie,只是大多数情况下会使用 Cookie 来传递 Session ID 而已。除了可以配置 Cookie 支持跨域,但也可以通过其他方式传递 Session ID,例如:URL 参数、Header 等。 JWT 的主要问题 许多人有了前面的对 Session 机制的一些偏见,却又忽略了 JWT 的问题。Session 机制确实无法做到像 JWT 的自包含和无状态,但是,在会话管理这个场景,本身就是需要有状态的,JWT 的无状态优势并不适用于此,反而会增加一些不可避免的问题:...

July 20, 2024

分词并生成词云

在当今数据爆炸的时代,能够从海量文本中提取有价值的信息成为了一个关键能力。无论是数据分析师、市场研究员还是内容创作者,都需要高效的工具来理解和可视化文本数据。 今天尝试了一下,使用 Python 来对文本进行分词,然后通过词云的方式展示词频,效果还不错,想着也应该写一篇文章来记录一下。 一共分为两步: 分词 生成词云 在这里,我使用了 jieba 库来进行中文分词,wordcloud 库来生成词云。 分词 分词是将连续文本切分成独立词语的过程。对于中文等词语边界不明显的语言,分词尤为重要。 分词技术主要有三种方法: 基于规则的分词:通过预设的词典和规则进行切分,速度快,但依赖词典更新。 基于统计的分词:通过对大规模语料进行训练,统计词频、词间关联度,实现自动切分,适应性强。 基于深度学习的分词:结合神经网络,通过上下文理解文本,效果更佳,但计算资源要求较高。 在 Python 中,jieba 库是一个流行的中文分词工具,它主要基于规则和统计的分词方法,不过,同时也支持基于深度学习的分词。 下面是一个使用示例: import jieba text = "人工智能正在改变我们的生活方式" words = jieba.cut(text, cut_all=False) print(" ".join(words)) 输出: 人工智能 正在 改变 我们 的 生活 方式 更多关于 jieba 的使用方法,可以查看官方文档:jieba 生成词云 词云是一种将文本数据中的关键词以视觉化方式展示的技术。它能直观地反映文本中词语的重要性和频率。 Python 的 wordcloud 库提供了强大的词云生成功能。下面是一个基本示例: from wordcloud import WordCloud import matplotlib.pyplot as plt # 假设我们已经有了分词后的文本 word_list = "人工智能 大数据 云计算 区块链 物联网" wordcloud = WordCloud( font_path='path/to/chinese/font....

June 20, 2024