Java 缓存精要

Java 缓存精要

实现更低延迟、降低成本并赋能智能体架构

作者:GRANVILLE BARNETT
架构师,HAZELCAST

缓存技术在系统中的作用日益重要,对于大规模解锁众多用例至关重要。几十年来,缓存已实现低成本、可扩展地访问会话状态和数据存储等信息。更现代的缓存用例正在实现低成本、可扩展的工具链,并在智能体架构中实现嵌入生成,这正在解锁下一代系统创新。

本参考资料卡介绍了使用 Java 的 JCache(Java 临时缓存 API)将缓存融入系统的方法。文中首先讨论了缓存的基础知识,然后通过代码示例简要介绍了 JCache API,最后总结了缓存部署架构。

缓存概述

缓存是先前计算结果的一个存储,以便可以省略后续计算。理解缓存最简单的方式是将其视为键值存储:对于给定的输入(键),输出(值)代表先前基于该输入计算出的结果。

缓存命中表示特定数据存在于缓存中,这种情况下可以使用其值。否则,就会发生缓存未命中,此时需要执行相关计算并将其输出放入缓存。缓存未命中的代价可能除了昂贵的计算操作外,还涉及网络通信。

图 1:简化的缓存命中/未命中流程

采用缓存是为了减少延迟并降低运营成本,几十年来对于实现众多类别的应用程序至关重要。缓存数据的例子包括 Web 应用程序的会话状态、数据库查询结果、网页渲染结果,以及来自通用网络和计算成本高昂的操作的结果。

缓存的一个更现代的用途是在 AI 领域。在这里,缓存的使用减少了昂贵的 API 调用(例如,嵌入生成),并最大限度地减少了智能体架构中智能体之间的对话断续(例如,由于工具调用和网络通信所致),从而解锁了新一波的解决方案和用户体验。

缓存可以驻留在进程内,作为客户端-服务器架构的一部分存在于服务中,或者是两者的结合。此外,缓存的部署通常可以组合。例如,应用程序可能与位于同一数据中心的缓存服务通信,而数据中心的本地缓存又是跨越多个数据中心的缓存的缓存。这种灵活性,加上缓存所支持的应用类别,使得缓存在过去几十年中成为一种主导的抽象概念。

本参考资料卡的剩余部分将讨论 JCache——Java 用于将缓存融入应用程序的抽象——首先简要概述您将经常使用的类,然后深入探讨 JCache 更广泛功能所提供的特性。最后,我们将总结缓存部署策略。

JCACHE 精要

JCache 在 Java 规范请求(JSR)107 中引入,并提供了一套关于缓存的抽象。JCache 有两个突出的特性:

  • JCache 是一个规范。 JSR 是由专家组设计和提交,并最终由 Java 社区过程执行委员会批准的规范。因为 JCache 是一个规范,所以它与那些 API 频繁变化的实现隔离开来。
  • JCache 是提供商独立的。 JCache 作为规范的一个副作用是,缓存解决方案提供商可以通过实现其暴露的服务提供程序接口(SPI)来与 JCache 集成。这为系统设计者提供了灵活性并避免了供应商锁定。

以下是一个简单的 JCache 示例,以便理解其使用方式。javax.cache 依赖项的获取方式可以在此处找到。

import java.util.Map;
import javax.cache.Cache;
import javax.cache.CacheManager;
import javax.cache.Caching;
import javax.cache.configuration.MutableConfiguration;
import javax.cache.spi.CachingProvider;

public class App {
    public static void main(String[] args) {
        CachingProvider cachingProvider = Caching.getCachingProvider(); // (1)
        CacheManager cacheManager = cachingProvider.getCacheManager(); // (2)
        MutableConfiguration<String, String> cacheConfig = new MutableConfiguration<String, String>(); // (3)
        Cache<String, String> cache = cacheManager.createCache("dzone-cache", cacheConfig); // (4)
        cache.put("England", "London"); // (5)
        cache.putAll(Map.of("France", "Paris", "Ireland", "Dublin")); // (6)
        assert cache.get("England").equals("London"); // (7)
        assert cache.get("Italy") == null; // (8)
    }
}

对上述示例的简要说明:
(1) 获取底层缓存提供程序的句柄
(2) 管理缓存的生命周期(例如,创建和销毁缓存)
(3) 允许启用/禁用缓存的特定功能(例如,统计信息、条目监听器)
(4) 创建由缓存提供程序支持的缓存
(5) 在缓存中放入单个键值条目
(6) 将键值条目放入缓存
(7) 断言缓存条目的存在
(8) 断言某个条目不在缓存中

本节的剩余部分将更详细地讨论上述示例中引入的抽象,以及您将经常遇到的相关类的其他方法。

javax.cache.spi.CachingProvider 构成了 JCache SPI,缓存提供者可以与之集成。您将使用的最常见功能是获取对 CacheManager 的引用。我们稍后将讨论 Caching

getCacheManagergetCacheManager 变体中最简单的一个。这将根据提供者的默认设置获取一个 CacheManager。可以使用 javax.cache.CacheManager 创建和销毁缓存:

  • createCache 创建一个具有给定名称和配置的缓存。
  • destroyCache 销毁具有给定名称的缓存。

javax.cache.Cache 是对提供者缓存的抽象,并暴露了少量用于查询和变更缓存项的操作:

  • putputAll 将条目放入缓存。请注意,这些方法不返回与正在放入的键先前关联的任何值。
  • containsKey 测试键是否存在于缓存中。
  • getgetAll 返回与指定键关联的值。
  • removeremoveAll 从缓存中移除项。

JCACHE 包

在本节中,我们将快速概述 javax.cache 更广泛包结构中的一些重要接口,并提供常用功能的示例。我们可以参考文档来浏览其内容的详尽列表。

图 2:javax.cache 的组成包

JAVAX.CACHE

通用管理(CacheManager)和与缓存交互(Cache)的设施位于 javax.cache 包内。除了初始配置之外,除非您想为缓存添加额外功能,否则仅使用此包中的类型就可以完成很多工作。例如,"JCache 精要"部分介绍中的示例用法主要使用了 javax.cache 中定义的接口。

JAVAX.CACHE.CONFIGURATION

在创建缓存期间,您可能希望添加功能,例如启用统计信息或通读缓存。此包提供了一个 Configuration 接口和一个实现 MutableConfiguration,可用于此类目的。

// ...
MutableConfiguration<String, String> cacheConfig = new MutableConfiguration<String, String>();
cacheConfig.setManagementEnabled(true).setStatisticsEnabled(true);
Cache<String, String> cache = cacheManager.createCache("dzone-cache", cacheConfig);

JAVAX.CACHE.EXPIRY

有时您希望驻留在缓存中的项过期。例如,我们可能有一个家庭保险报价的缓存,有效期为 24 小时。在这种情况下,我们可以使用过期策略如下:

// ...
MutableConfiguration<String, Double> cacheConfig = new MutableConfiguration<String, Double>();
cacheConfig.setExpiryPolicyFactory(CreatedExpiryPolicy.factoryOf(Duration.ONE_DAY));
Cache<String, Double> cache = cacheManager.createCache("insurance-home-quotes", cacheConfig);
cache.put(quote.getId(), quote.getValue());

javax.cache.expiry 包提供了额外的过期策略,可能对其他场景有用。例如,AccessedExpiryPolicy 允许基于缓存条目的最后访问时间附加过期设置。

JAVAX.CACHE.EVENT

JCache 的一个强大功能是能够订阅缓存事件。例如,我们可能希望在创建或删除缓存条目后运行某些领域逻辑。javax.cache.event 包提供了实现此功能的抽象,特别是订阅缓存创建、更新、过期和移除的能力。以下基本示例在缓存条目创建后运行某些领域逻辑:

// ...
CacheEntryCreatedListener<String, String> createdListener = new CacheEntryCreatedListener<String, String>() {
  @Override
  public void onCreated(Iterable<CacheEntryEvent<? extends String, ? extends String>> events) throws CacheEntryListenerException {
    for (var c : events) {
      performDomainLogic(c);
    }
  }
};
MutableConfiguration<String, String> cacheConfig = new MutableConfiguration<String, String>();
MutableCacheEntryListenerConfiguration<String, String> listenerConfig = new MutableCacheEntryListenerConfiguration<>(() -> createdListener, null, false, true); // 请参阅文档
cacheConfig.addCacheEntryListenerConfiguration(listenerConfig);
Cache<String, String> cache = cacheManager.createCache("events", cacheConfig);
cache.put("key", "value"); // 调用创建监听器

JAVAX.CACHE.PROCESSOR

JCache 的一个强大组件是能够使用 EntryProcessor 将计算移至数据所在处,然后以编程方式调用该计算。当使用在分布式系统(例如,Hazelcast)内托管其缓存的提供者时,这尤其强大,因为它以很少的投入为分布式计算提供了一个简单的入口点。以下是一个 EntryProcessor 的简单示例,它将 UUID 附加到缓存条目:

// ...
class AppendUuidEntryProcessor implements EntryProcessor<String, String, String> {
  @Override
  public String process(MutableEntry<String, String> entry, Object... arguments) throws EntryProcessorException {
    if (entry.exists()) {
      String newValue = entry.getValue() + "-" + UUID.randomUUID();
      entry.setValue(newValue);
      return newValue;
    }
    return null;
  }
}
// ...
cache.invoke(key, new AppendUuidEntryProcessor())

JAVAX.CACHE.MANAGEMENT

JCache 提供的管理钩子非常强大且易于启用。例如,下面的小代码片段暴露了由 Java 管理扩展(JMX)规范定义的托管 Bean。这使得诸如 jconsole 和 JDK Mission Control 之类的 JMX 客户端能够查看缓存配置和统计信息(例如,命中和未命中百分比、平均获取和放置时间)。

// ...
MutableConfiguration<String, String> cacheConfig = new MutableConfiguration<String, String>();
cacheConfig.setManagementEnabled(true).setStatisticsEnabled(true);
Cache<String, String> cache = cacheManager.createCache("management", cacheConfig);
// ...

JAVAX.CACHE.SPI

"JCache 精要"部分提供的示例省略了我们如何注册缓存提供者,即使用 JCache API 与我们的应用程序交互的缓存宿主服务。这就是 JCache 的 SPI 组件发挥作用的地方。

实现这一点有两个组成部分:

  1. 将我们的缓存提供者添加到类路径中
  2. 告诉 JCache 使用该提供者

第一步很简单:只需添加对任何符合 JSR 107 标准的提供者的依赖。

第二步有几种通用的方法:

  • 我们可以通过调用 Caching#getCachingProvider(...) 的某个变体(以及其他方法)来告诉 JCache。
  • 我们可以提供一个 META-INF/services/javax.cache.spi.CachingProvider 文件,并让其指定提供者实现。指定的提供者是您的提供者的缓存提供者实现的完全限定名称。
  • 我们可以使用 Caching#getCachingProvider();但是,最好明确限定要使用的提供者,因为您的类路径上可能有多个提供者,这会抛出 javax.cache.CacheException

例如,以下代码使用 CachingProvider Caching.getCachingProvider(String) 指定 Hazelcast 为提供者:

CachingProvider cachingProvider = Caching.getCachingProvider("com.hazelcast.cache.HazelcastCachingProvider");
CacheManager cacheManager = cachingProvider.getCacheManager();
MutableConfiguration<String, String> cacheConfig = new MutableConfiguration<String, String>();
Cache<String, String> cache = cacheManager.createCache("spi-example", cacheConfig);
cache.put("k", "v");

JAVAX.CACHE.ANNOTATION

JCache 定义了许多注解,用于集成到上下文和依赖注入环境中。Spring Framework 原生支持 JCache 注解。我们可以参考 JCache 文档以获取更多信息。

JAVAX.CACHE.INTEGRATION

javax.cache.integration 包提供了 CacheLoader(需要通读)和 CacheWriter(需要通写)。CacheLoader 在将数据读入缓存时使用——例如 Cache#loadAll(...)CacheWriter 可以作为一个集成点,将缓存变更(例如,写入、删除)传播到外部存储服务。

缓存部署

JCache 没有缓存部署策略的概念;它仅仅是缓存提供者之上的一个 API。然而,不同的提供者支持不同类型的缓存部署。请考虑哪种缓存部署对您的应用程序有意义,并由此反向确定合适的缓存提供者。

图 3: 缓存部署示例

请注意,一些缓存提供者可能支持所有这三种缓存部署,而其他提供者可能不支持。

本节的剩余部分讨论图 2 中所示的常见缓存部署:

  • 嵌入式 – 缓存与应用程序位于同一进程中。
  • 客户端-服务器 – 缓存托管在独立的服务中,客户端与该服务通信以确定缓存驻留。
  • 嵌入式/客户端-服务器 – 这是一种混合模式,整个缓存托管在不同的服务上,但客户端在同一进程中拥有一个较小的本地缓存。

重要的是要注意,上述缓存部署并非互斥的;它们可以通过多种方式组合以满足应用程序需求。

最简单的缓存部署是让缓存与应用程序驻留在同一进程中,这样做的好处是提供低延迟的缓存访问。嵌入式缓存不能在应用程序之间共享,并且在应用程序重启或故障时,其托管(它们所需的资源)和重建成本可能很高。

客户端-服务器缓存部署将缓存托管在与客户端不同的服务中。缓存服务允许通过跨服务复制来满足容错需求,提供更大的容量、更多的可扩展性选项,以及跨应用程序共享缓存的能力。客户端-服务器模型的主要缺点在于客户端缓存查询期间网络通信的成本。

混合嵌入式/客户端-服务器部署是指我们拥有一个嵌入式缓存,它包含来自服务缓存条目的一个子集,作为应用程序缓存请求的副作用被填充。在这里,客户端可以对频繁访问的数据(或表现出特定访问模式的数据)实现低延迟的缓存命中,并省去与缓存服务通信所带来的网络通信开销。如果嵌入式缓存过期,一些提供者会负责使用服务托管的缓存来更新它们。

结论

本参考资料卡介绍了缓存以及如何将其与 Java 的 JCache API 一起使用。JCache API 直观、强大,并且由于其是一个规范而避免了供应商锁定,为架构师和系统设计者提供了他们所需的灵活性。这种灵活性在我们进入基于智能体架构的新一代创新时尤为重要,其中缓存对于工具链和嵌入生成至关重要。

作者:GRANVILLE BARNETT,
架构师,HAZELCAST
Granville Barnett 拥有计算机科学博士学位,是拥有超过 15 年经验的分布式系统专家。他目前是 Hazelcast 的架构师,此前曾在 HP Labs 和 Microsoft 任职。Granville 拥有多项美国专利,并发表了关于程序验证主题的研究。

附加资源:


【注】本文译自:Java Caching Essentials

2024年低代码开发趋势报告


欢迎信

作者:Lucy Marcum, DZone组稿编辑

我偶然进入了科技世界。尽管我毕业于一所以其计算机科学项目闻名的大学,但我从未想过自己会与开发人员一起工作——直到毕业后与一位朋友讨论可能的职业道路时。科技世界对我来说曾经如此陌生,但在她的鼓励和我那令人紧张的人脉拓展努力下,我找到了在这个行业的第一份工作。

在最初的几个月里,一切都感觉极其陌生。像KubernetesNoSQL这样的术语让我困惑不已,我大部分时间都沉浸在研究中,学习在新职位上取得成功所需了解的一切。

当时我没有意识到的是,我已经以不同的形式接触过软件开发:低代码和无代码。为了几个大学项目,我使用无代码构建器创建网站来展示研究或概念,偶尔也涉足代码进行自定义调整(在大量搜索之后)。

现在,即使多年后对开发有了概念性的理解,我仍然不声称自己是开发人员。但我确实认识到让非开发人员能够接触开发的重要性。正如我需要在工作中向其他团队寻求帮助一样,开发人员也需要帮助。

赋能非开发人员参与与其任务相关的开发,使得开发人员有时间专注于能产生更大组织影响的更大问题和项目。

将我曾使用的无代码工具与某种形式的软件开发联系起来,是让我的工作感觉易于上手的关键。我知道你们中的许多开发人员读到这儿可能笑了,但有时,正是像第一个"顿悟"时刻这样的小事,能让人在新环境中感到舒适。另一方面,当开发人员意识到通过这些工具所能获得的支持时,他们可能会发现自己的工作流程变得更加顺畅高效。

在DZone的《2024年低代码开发趋势报告》中,我们探讨了低代码在当今开发环境中的重要性。特别是"主要研究发现"部分,回顾了采用情况、自动化、AI的角色以及对开发人员的影响——包括关于谁可以被视为开发人员的一个令人惊讶的混合反应。本趋势报告还包括DZone几位专家社区成员的文章,他们讨论了最佳实践、低代码与AI之间的界限、智能测试策略、公民开发和可扩展性。

祝编码愉快(各种形式的编码),

Lucy

Lucy Marcum

Lucy负责DZone出版物的组稿流程和策略,从寻找新的投稿人和社区成员到引导他们完成编辑审查。她还编辑出版物,创建趋势报告的各个组成部分,并与网站内容和社区团队合作。工作之余,Lucy把时间花在阅读、写作、跑步上,并努力不让她的猫Olive和Tiger Lily惹麻烦。


主要研究发现

DZone 2024低代码开发调查结果分析
作者:G. Ryan Spain, 自由软件工程师,前DZone工程师兼编辑

低代码、无代码、公民开发、AI自动化、可扩展性——如果你在科技界工作,你很可能会被鼓励使用至少其中一个领域的工具。这是有充分理由的,因为Gartner预测,到2025年,组织内开发的应用程序中有70%将使用低代码和/或无代码技术构建。那么,实践是否不负众望呢?

年复一年,随着行业的不断发展,答案是响亮的"是"。组织对更频繁的应用程序发布和更新的需求增加,随之而来的是对提高效率的需求。而这正是低代码和无代码开发实践大放异彩的地方。将AI自动化融入低代码和无代码开发中,可扩展性和机遇是无限的。

五月,DZone对软件开发人员、架构师和其他IT专业人士进行了调查,旨在深入了解低代码开发在软件开发领域的现状。

在这些发现中,我们考察了开发人员对低代码和无代码开发的看法、他们使用和利用低代码工具的方式,以及他们对低代码影响软件开发的经验。

方法

我们创建了一项调查,并将其分发给全球的软件专业人士。问题格式主要包括单项和多项选择,在某些情况下提供填写回答的选项。本次调查通过电子邮件分发给DZone和TechnologyAdvice的注册订阅用户列表,同时在DZone官网、DZone Core Slack工作区及各社交媒体渠道进行了推广。

本报告的数据收集自2024年5月3日至2024年5月23日期间提交的调查回复;我们收集了228份完整和部分回复。

样本特征

我们在下面注明了某些关键的受众细节,以便对得出结果的样本建立更牢固的印象:

  • 29%的受访者将其在组织中的主要角色描述为"开发人员/工程师",18%描述为"技术架构师",14%描述为"开发团队负责人",10%描述为"顾问/解决方案架构师"。我们提供的其他角色均未被超过10%的受访者选择。*
  • 70%的受访者表示他们目前正在开发"Web应用程序/服务(SaaS)",54%表示"企业业务应用程序",27%表示"原生移动应用"。
  • "Java"(72%)是受访者公司使用的最流行的语言生态系统,其次是"JavaScript(客户端)"(54%)、"Python"(53%)、"Node.js(服务器端JavaScript)"(45%)、"TypeScript"(41%)和"C#"(30%)。
  • 关于受访者工作中使用的主要语言的回答,最流行的是"Java"(37%),其次是"Python"(17%)、"C#"(7%)和"Go"(6%)。其他语言的选择率均未超过5%。
  • 平均而言,受访者表示他们拥有17.99年的IT专业人士经验,中位数为18年。
  • 35%的受访者在员工人数<100的组织工作,25%在员工人数100-999的组织工作,38%在员工人数1000+的组织工作。

注:为简洁起见,在本发现报告的其余部分,我们将使用术语"开发人员"或"开发者"来指代任何积极参与软件创建和发布的人员,无论其角色或头衔如何。此外,我们将"小型"组织定义为员工人数<100,"中型"组织定义为员工人数100-999,"大型"组织定义为员工人数1000+。

主要研究目标

在我们的2024年低代码开发调查中,我们旨在收集与以下主要研究目标相关的各种主题的数据:

  • 开发人员对低代码的感受
  • 开发人员如何使用低代码或与低代码交互
  • 低代码对开发和软件质量的影响

在本报告中,我们回顾了一些关键的研究发现。许多感兴趣的次要发现未包含在此处。

研究目标一:开发人员对低代码的感受

首先,我们想看看开发人员对低代码采用如何融入整体开发格局的看法,低代码如何影响开发过程,以及他们认为低代码在哪些方面(如果有的话)可以表现出色。我们还旨在找出参与调查的开发人员类型,无论是前端、后端、全栈还是公民开发者。

在本节中,我们探讨:

  • 受访者如何描述自己作为开发人员
  • 开发人员是否认为"无代码"是"低代码"的子集
  • 开发人员是否认为低代码用户也是"开发人员"
  • 开发人员关于低代码平台如何影响开发过程的看法
  • 开发人员认为哪些用例最适合低代码

受访者如何描述自己作为开发人员

从"AWS解决方案架构师"到"Zapier工程师",世界上软件专业人士的种类比我们可能开始列举的还要多,一个开发人员可能会根据他们当前正在进行的项目、他们正在与谁交谈、他们希望深入探讨其角色细节的程度等,声称自己属于任何数量的IT相关类别。尽管如此,拥有某些广泛的类别来对我们自己进行分组可能是有用的——不仅是为了便于澄清,也是为了庆祝相似性和欣赏差异性。

在我们的调查中,我们希望明确区分的一个分类是受访者在少数几个"角色"之间的自我认同:"前端"、"后端"、"全栈"和"公民"开发者。我们从这里开始,不是因为结果本身说明了开发人员对低代码的感受,而是因为我们将在这些发现中持续引用这些数据。我们询问受访者:

您会如何最好地描述自己?
结果:
图1. 自我描述的开发人员类型 [n=220]

(图表数据翻译:)

  • 后端开发者:39%
  • 全栈开发者:37%
  • 其他,请填写:14%
  • 公民开发者:7%
  • 前端开发者:3%

观察

  • 考虑到我们典型的企业开发人员受众,绝大多数受访者将自己描述为"后端开发者"(39%)或"全栈开发者"(37%)并不奇怪,其中7%将自己描述为"公民开发者",只有3%将自己描述为"前端开发者"。14%的受访者选择了"其他,请填写",填写选项包括"架构师"、"经理"、"数据工程师"和"测试工程师"。
  • 这些结果与我们2023年Web、移动和低代码开发调查收集的结果非常相似。"后端开发者"的回复率与2023年相比保持不变,"前端开发者"和"公民开发者"的回复率变化在统计上不显著。"全栈开发者"的回复率略有下降,同时选择"其他,请填写"选项的受访者有所增加。
  • 我们预计这些轻微的逐年变化归因于样本特征的变化(例如,去年38%的受访者将其在组织中的主要角色描述为"开发人员/工程师",而今年为29%)以及普遍存在的抗拒"把自己框定"的心态。按年份划分的结果详情见表1。
表1. 自我描述的开发人员类型:2023年 vs. 2024年 开发人员类型 2023 2024
后端 39% 39%
全栈 45% 37%
公民 3% 7%
前端 8% 3%
其他 4% 14%
  • 就其本身而言,从这个问题的回答收集的数据表明——毫不奇怪——企业中很少有软件专业人士认为自己是公民开发者。逐年的结果可能预示着未来企业软件中公民开发者数量的增长,但即使如此,似乎在未来许多年也不太可能出现显著增长。正如我们之前提到的,这个问题的结果主要是在用于按开发人员类型细分其他回答时才引起兴趣。由于"公民"、"前端"和"填写"选项的回复率较低,在本发现报告的剩余部分,我们将把这个问题回答分组为"后端"、"全栈"和"非后端/全栈"。

低代码、无代码与"开发人员"头衔

开发人员可能非常保护他们的技能组合,并警惕那些他们认为试图在没有适当考虑软件创建所有方面的情况下强行进入开发领域的人。通常,这些感觉并非源于试图设限,而是源于制作拙劣的软件可能产生深远的后果。

构建一个实现预期目标的函数不一定困难,但理解该函数对系统稳定性、性能和安全性影响则完全是另一回事,更不用说概念化该函数在未来系统扩展时的需求和影响了。因此,低代码对于一些开发人员来说可能是一个有争议的话题,为了了解更多他们的看法,我们询问:

您个人是否认为"无代码"开发是"低代码"开发的一个子集?
同意/不同意:"开发人员"头衔也应适用于仅使用低代码工具创建应用程序且可能从未自己编写任何代码的人。

结果:

图2. "无代码"是"低代码"吗?[n=219]


(图表数据翻译:)

  • 是:53%
  • 否:32%
  • 无意见:16%

图3. 严格使用低代码的用户应该拥有"开发人员"头衔吗?[n=210]

(图表数据翻译:)

  • 中立:28%
  • 不同意:20%
  • 同意:19%
  • 非常不同意:17%
  • 非常同意:16%

观察

  • 大约一半的受访者(53%)认为无代码属于低代码范畴,而32%认为这两个概念是分开的,16%对此事没有意见。
  • 大约相同比例的后端开发者和全栈开发者认为无代码是低代码的子集,尽管后端开发者比全栈开发者更可能"无意见",并且比全栈开发者更不可能回答他们不认为无代码是低代码的一部分。非后端/全栈开发者最可能认为无代码包含在低代码中,并且最不可能回答"无意见"。这些结果的详细信息见表2。
表2. "无代码"是"低代码"吗?(按开发人员类型细分)* 回复 开发人员类型
后端 全栈 非后端/全栈
49% 51% 62%
28% 36% 31%
无意见 22% 14% 8%

*列为百分比

  • 在后面的发现中,我们讨论了表明全栈开发者比后端开发者更频繁地使用低代码的数据。那么,全栈开发者对"低代码"和"无代码"区分更明确的看法,可能基于这种额外的熟悉度。也许他们的经验表明,"无代码"解决方案通常比"低代码"平台提供更少的灵活性和定制化,而低代码平台允许开发者根据需要扩展和定制应用程序
  • 受访者对于是否应将可能从未编写任何实际代码的低代码工具用户视为"开发人员"存在相当分歧:大约三分之一的受访者倾向于同意这一观点(35%),大约三分之一不同意(37%),大约三分之一保持中立(28%)。后端开发者比全栈工程师更可能不同意或强烈不同意这类低代码用户应被称为"开发人员"(44% vs 32%),而全栈开发者比后端开发者更可能同意或强烈同意(39% vs 28%)。

低代码 vs. 全代码:关于开发过程的看法

在某些情况下,低代码和全代码的软件创建方法可能被视为旨在实现相同目标的两种方法。在其他情况下,低代码工具可能被认为适用于某些目标,而全代码开发适用于其他目标。我们想知道开发人员对于使用低代码工具创建的软件与完全编码的软件在复杂性、可重用性和开发易用性方面的根本差异的看法,因此我们提出了以下问题:

您认为使用低代码工具构建的应用程序相对于完全用代码编写的应用程序应该有多复杂?
在您看来,调试低代码应用程序或与低代码应用程序交互的软件是…
在您看来,使用低代码工具实现的业务逻辑是…
在您看来,使用低代码平台构建的软件会导致…

结果:

图4. 关于低代码 vs. 全代码应用程序复杂性的看法 [n=220]

(图表数据翻译:)

  • 稍微简单一些:39%
  • 简单得多:19%
  • 与全代码应用程序一样复杂:16%
  • 稍微复杂一些:11%
  • 无意见:9%
  • 复杂得多:3%

图5. 关于低代码 vs. 全代码调试易用性的看法 [n=218]

(图表数据翻译:)

  • 更难:25%
  • 更容易:22%
  • 无意见:19%
  • 可能更难:19%
  • 可能更容易:9%

图6. 关于低代码 vs. 全代码业务逻辑可重用性的看法 [n=211]

(图表数据翻译:)

  • 可重用性更低:36%
  • 可重用性更高:39%
  • 可重用性相同:17%
  • 无意见:8%

图7. 关于低代码 vs. 全代码产生的技术债务的看法 [n=219]

(图表数据翻译:)

  • 技术债务更少:31%
  • 技术债务相同:21%
  • 技术债务更多:21%
  • 可能技术债务更少:13%
  • 可能技术债务更多:9%
  • 无意见:5%

观察

  • 大多数受访者(68%)认为使用低代码工具构建的应用程序应该至少"稍微简单一些",而很少(14%)认为低代码驱动的应用程序应该至少"稍微复杂一些"。这些回复率与我们上次在2022年低代码开发调查中提出这个问题时看到的比率相似,当时我们看到62%的人说使用低代码工具创建的软件应该至少"稍微简单一些",17%的人说应该至少"稍微复杂一些"。
    低代码平台设计为用户友好型并能实现快速开发,这可能导致人们认为它们更适合简单的应用程序——开发人员可能觉得这些工具抽象掉了传统编码涉及的许多复杂性。此外,全代码应用程序提供了更大的灵活性和定制选项,允许开发人员创建更复杂和量身定制的解决方案。低代码工具在提供的定制深度方面可能被视为有限。低代码工具不太可能很快弥合复杂性差距——至少在大多数开发人员心目中是这样。
  • 受访者对于调试低代码应用程序比传统代码调试更容易还是更困难存在分歧,但总体而言,认为低代码软件更难调试或可能比全代码应用程序更难调试的受访者(44%)多于认为调试低代码更容易或可能更容易的受访者(31%)。
    一些开发人员可能发现低代码应用程序更难调试,因为抽象和对底层代码的有限可见性掩盖了问题的根本原因并限制了直接操作。生成代码的专有性质和不熟悉的调试工具进一步使过程复杂化。另一方面,一些开发人员可能发现低代码应用程序更容易调试,因为低代码平台通常提供集成的调试工具、可视化界面和标准化环境,简化了错误识别和解决。低代码开发的可视化性质可以增强对应用程序逻辑的理解并减少编码错误,使一些开发人员更容易诊断和修复问题。
  • 受访者主要在认为低代码业务逻辑比代码实现的业务逻辑可重用性更高还是更低之间存在分歧,认为两者具有相同可重用性水平和无意见的受访者子集要小得多。认为低代码业务逻辑可重用性更低的回复率与2022年保持不变,但认为低代码业务逻辑可重用性更高的回复率从2022年的30%增加了9%,而认为低代码和全代码业务逻辑具有相同可重用性水平的回复率从28%下降了11%。
    认为低代码业务逻辑比全代码可重用性更高的开发者可能这样认为,是因为低代码平台的模块化组件、拖放界面和内置模板促进了业务逻辑在不同应用程序间的轻松复制和适应——这些方面可以增强一致性并加速开发周期。他们也可能认为非开发人员或公民开发者更容易利用某些低代码业务逻辑,从而允许在组织内更广泛地重用。
    相反,其他开发人员可能认为低代码业务逻辑可重用性更低,因为许多低代码平台的专有性质,在与其他系统或平台集成时可能产生兼容性问题和限制灵活性。或者他们可能认为低代码业务逻辑是脆弱的,是为一次性目的创建的,没有考虑如何设计和实现逻辑以最大化其可重用性。
    与两年前相比,现在更多的开发人员可能认为低代码业务逻辑可重用性更高,这是因为低代码技术的进步、集成能力的提高以及低代码环境中标准化实践的日益普及,这些共同增强了重用低代码组件的吸引力和实用性
  • 最后,更多受访者认为使用低代码平台创建的软件导致(或可能导致)比用代码编写的软件更少的技术债务(44%),相比之下,认为会导致更多技术债务(30%)或相同数量技术债务(21%)的受访者要少。这些结果与我们在2022年看到的结果一致,逐年结果没有显著变化。
    开发人员可能认为使用低代码平台构建的软件导致更少的技术债务,因为这些平台强制执行标准化的编码实践,可以减少手动编码错误,并提供预构建的组件以确保一致的质量和可维护性。这些特性可以最大限度地降低编写不良代码的风险,并使更新和扩展应用程序变得更加容易。再者,非开发人员或公民开发者能够为软件需求做出贡献,可能通过释放开发团队周期用于更复杂或关键的问题来帮助减轻技术债务。
    另一方面,一些开发人员可能认为低代码平台缺乏对软件系统的灵活性和控制,这可能导致低效和变通方法,使未来的维护复杂化。此外,对专有技术的依赖可能产生难以管理或替换的依赖性,从而增加长期技术债务。

有价值的低代码用例

在继续考察开发人员对低代码的看法时,我们想知道受访者认为哪些类型的功能适合使用低代码工具构建,并询问:

在您看来,低代码平台对哪些用例有用?

结果:
图8. 被认为有用的低代码用例 [n=218]

(图表数据翻译:按选择百分比排序)

  • 交互式网页表单:66%
  • 业务流程自动化:56%
  • 企业CRUD:54%
  • 简单数据库:51%
  • 请求处理:43%
  • 业务流程管理:43%
  • 集成中间件:39%
  • 学习管理:29%
  • 机器人流程自动化/AI自动化:29%
  • BI/数据分析:28%
  • 电子商务和电子采购:27%
  • ETL:22%
  • 机器学习管道:18%
  • 安全流程:15%
  • 物理建模:12%
  • 其他,请填写:6%

观察

  • 与开发人员对使用低代码工具构建的应用程序/组件复杂性的感受一致,受访者认为低代码平台有用的最流行用例通常是低复杂性、低风险的软件元素。超过一半的受访者认为"交互式网页表单"、"业务流程自动化"、"企业CRUD"和"简单数据库"是低代码的合适用例,而很少受访者对"物理建模"、"安全流程"和"机器学习管道"等用例持相同看法。
    截至目前,低代码工具更常被认为擅长构建涉及重复性、定义明确任务的应用程序,这些任务可以受益于平台的快速开发能力、预构建组件和用户友好界面。这些用例通常需要较少的复杂逻辑和定制,使它们成为低代码解决方案的理想选择。
    相反,更高复杂性的软件被认为不太适合低代码平台,因为它们通常需要高级的、专门的算法、高水平的定制以及严格的性能安全性标准,而低代码工具可能难以满足这些要求,特别是如果软件旨在由几乎没有软件设计经验的员工创建。这些复杂应用程序所需的固有灵活性和控制更好地由传统的编码方法提供。
    我们将在后面的发现中考察受访者实际利用低代码平台的用例。

  • 这个问题的结果大多与我们2023年上次提出该问题时看到的结果一致。逐年回复率唯一具有统计学显著变化的是"交互式网页表单"、"请求处理"和"集成中间件"的增加,以及"ETL"的减少。这些结果的详细信息见表3:

表3. 被认为有用的低代码用例:2023-2024* 用例 2023 2024 变化百分比
交互式网页表单 58% 66% +8%
业务流程自动化 56%
企业CRUD 52% 54% +2%
简单数据库 56% 51% -5%
请求处理 36% 43% +7%
业务流程管理 48% 43% -5%
集成中间件 28% 39% +11%
学习管理 28% 29% +1%
机器人流程自动化/AI自动化 29%
BI/数据分析 28%
电子商务和电子采购 24% 27% +3%
ETL 32% 22% -10%
机器学习管道 16% 18% +2%
安全流程 15%
物理建模 10% 12% +2%
n= 93 218

*注:2023年列中的空白表示我们2024年调查中添加的选项。

总体而言,这些数据表明开发人员对低代码用例的看法变化缓慢,并且低代码平台可能需要取得重大进步,大多数开发人员才会对将低代码用于更复杂的软件需求感到满意。

研究目标二:开发人员如何使用低代码或与低代码交互

随着组织采用低代码平台,越来越多的开发人员很可能需要以某种方式与低代码合作。这可能意味着为低代码创建的功能设计和构建互操作性。或者它可能涉及学习并使用新的低代码平台本身。我们想了解更多关于开发人员与低代码工具交互的现状。

在本节中,我们讨论:

  • 开发人员如何与低代码软件交互或使用低代码工具构建
  • 开发人员如何使用低代码进行自动化和AI

与低代码软件交互和构建

全代码软件开发人员可能需要与低代码平台交互的原因有很多,例如:

  • 集成低代码工具无法提供的自定义功能
  • 将低代码应用程序与其他系统和服务连接,处理复杂的集成并确保整个企业的无缝数据流
  • 维护和排查低代码应用程序故障,解决需要更深入了解底层代码和系统架构的性能问题或错误

我们想找出开发人员处理低代码的频率,因此我们提出了以下问题:

您编写与使用低代码平台构建的软件交互的代码的频率如何?
您使用低代码平台构建软件的频率如何?

对于回答并非"从未"使用低代码平台构建软件的受访者,我们还询问:

您使用低代码平台实现了哪些类型的软件或软件功能?

结果:


图9. 编写与低代码创建软件交互代码的频率 [n=220]
(图表数据翻译:)

  • 有时:32%
  • 很少:26%
  • 经常:20%
  • 从未:13%
  • 一直:10%

图10. 使用低代码平台创建软件的频率 [n=215]
(图表数据翻译:)

  • 有时:30%
  • 很少:25%
  • 经常:19%
  • 从未:16%
  • 一直:10%

图11. 使用低代码平台实现的软件/功能类型 [n=175]

(图表数据翻译:按选择百分比排序)

  • 交互式网页表单:46%
  • 企业CRUD:37%
  • 业务流程自动化:39%
  • 简单数据库:27%
  • 集成中间件:27%
  • 业务流程管理:31%
  • 请求处理:24%
  • 机器人流程自动化/AI自动化:15%
  • BI/数据分析:12%
  • 电子商务和电子采购:14%
  • ETL:13%
  • 学习管理:11%
  • 安全流程:8%
  • 物理建模:6%
  • 机器学习管道:5%
  • 其他,请填写:4%

观察

  • 几乎所有受访者(93%)都至少有一些与使用低代码平台构建的软件交互或自己使用低代码平台构建的经验(即,他们至少回答了其中一个问题,并且没有对两个问题都回答"从未")。总体而言,受访者使用低代码工具构建的可能性与低代码构建软件交互的可能性一样高,并且个体受访者倾向于以相同的频率进行其中一项活动。
    例如,大多数表示他们"一直"与低代码工具创建的软件交互的受访者也表示他们"一直"使用低代码构建软件,对于"经常"、"有时"等也是如此(详情见表4)。
表4. 与低代码构建软件交互的频率和使用低代码构建的频率* 使用低代码构建
与低代码交互 一直 经常 有时 很少 从未 总体
一直 55% 30% 10% 5% 0% 10%
经常 9% 63% 12% 9% 7% 20%
有时 4% 7% 64% 15% 9% 32%
很少 2% 4% 18% 57% 20% 26%
从未 7% 3% 14% 24% 52% 13%
总体 10% 19% 30% 25% 16%

*行内百分比

这些结果可能表明,采用低代码技术的组织通常在整个组织范围内同等程度地使用它们,而不是将这些工具的使用隔离给公民开发者或非开发团队。我们将在研究目标三中讨论受访者关于低代码如何影响软件质量的经验。

  • 将自己描述为全栈开发者的受访者频繁("经常"或"一直")使用低代码平台构建软件的可能性显著高于后端和非后端/全栈开发者。非后端/全栈开发者最可能说他们"有时"使用过低代码平台,而后端开发者最可能说他们"很少"或"从未"使用过低代码。具体数据见表5。
表5. 按开发人员类型划分的低代码使用频率* 频率 开发人员类型
后端 全栈 非后端/全栈 总体
一直 4% 18% 8% 10%
经常 13% 27% 17% 19%
有时 29% 23% 40% 29%
很少 36% 19% 17% 25%
从未 18% 14% 17% 16%

*列内百分比

  • 对于每个用例,声称曾利用低代码平台实现该功能用例的受访者,认为低代码是处理该用例的合适工具的可能性远高于总体受访者。例如,虽然总体上有54%的受访者认为低代码平台对"企业CRUD"有用,但声称实际使用低代码进行"企业CRUD"的受访者中有90%持相同看法。我们提供的每个用例答案选项,在实际使用低代码处理该用例的受访者中,关于低代码对该用例有用性的回复率都有显著增加。详情见表6。
表6. 按用例划分的低代码有用性看法:报告的低代码使用情况 vs. 总体比率 用例 使用过此用例 总体 差值
比率 n= 比率 n=
机器人流程/AI自动化 93% 26 29% 63 64%
企业CRUD 90% 64 54% 117 36%
简单数据库 87% 47 51% 111 36%
交互式网页表单 87% 80 66% 143 21%
业务流程自动化 86% 69 56% 121 31%
电子商务和电子采购 86% 25 27% 59 59%
学习管理 83% 19 29% 64 53%
请求处理 82% 42 43% 94 39%
安全流程 82% 14 15% 32 68%
业务流程管理 82% 54 43% 94 39%
BI/数据分析 78% 21 28% 62 49%
集成中间件 77% 47 39% 86 38%
ETL 72% 23 22% 48 50%
机器学习管道 67% 8 18% 39 49%
物理建模 63% 10 12% 27 50%
其他,请填写 58% 7 6% 12 53%
  • 尽管这些结果可能反映了某些认知偏差,例如单纯曝光效应,但这些偏差的证据可能表明低代码工具比许多开发人员认为的更具实用性,并且组织采用低代码平台的增加可能导致开发人员在有机会尝试这些工具后找到更多使用它们的方法。

用于自动化和AI的低代码
自动化和AI都是利用低代码能力的有前景的领域。两者都有潜力简化各种重复性任务,使员工能够减少花在繁忙工作上的时间,而将更多时间用于运用他们的技能组合。低代码平台可以使业务用户和开发人员都能够以最少的编码创建和修改自动化工作流以及AI驱动的流程。低代码平台的可视化界面和预构建组件可以更轻松地将AI能力——如机器学习模型和自然语言处理——集成到业务应用程序中,从而提高运营效率和决策能力。为了确定组织目前如何使用低代码进行自动化和AI,我们提出了以下问题:

您的组织是否使用低代码工具来创建工作流和/或流程自动化?

对于回答"是"或"否,但我们正在考虑"的受访者,我们还询问:

您使用或正在考虑使用以下哪些工作流和/或流程自动化方法?

关于用于AI的低代码,我们询问:

您的组织在哪些用例中使用低代码AI工具或平台?

结果:

图12. 组织使用低代码进行工作流/流程自动化的情况 [n=213]

(图表数据翻译:)

  • 是:47%
  • 否,我们也没有考虑:18%
  • 我不知道:11%
  • 否,但我们正在考虑:24%

图13. 使用或考虑的工作流/流程自动化方法 [n=150]

(图表数据翻译:按选择百分比排序)

  • 业务流程管理:65%
  • 机器人流程自动化:42%
  • 企业资源规划:39%
  • 超自动化:15%
  • 其他:5%

图14. 低代码AI工具/平台的使用用例 [n=211]

(图表数据翻译:按选择百分比排序)

  • 聊天机器人/虚拟助手:38%
  • 预测分析:24%
  • 语言处理:22%
  • 计算机视觉:19%
  • 不适用:21%

观察

  • 近一半的受访者(47%)表示他们的组织使用低代码工具进行工作流/流程自动化,这比我们在2023年调查中看到的60%显著下降。回答"否,但我们正在考虑"的回复率从2023年的11%增加到24%以作补偿,而回答"否,我们也没有考虑"和"我不知道"的回复率在2023年和2024年之间没有显著变化。
    考虑到我们在比较本次调查中其他问题的逐年结果时没有看到低代码工具使用率的下降,目前尚不清楚为什么今年声称其组织使用低代码工具进行工作流和/或流程自动化的受访者如此之少,但这是我们未来继续低代码研究时将关注的趋势。
  • 大多数在使用或考虑使用低代码进行工作流/流程自动化的组织的受访者也表示,他们使用或考虑使用"业务流程管理",尽管这一数字也低于我们2023年收集的结果。另一方面,"机器人流程自动化"和"企业资源规划"的回复率较2023年调查结果有所增加。逐年数据见表7:
表7. 使用或考虑的工作流/流程自动化方法:2023-2024 方法 2023 2024 变化百分比
比率 n= 比率 n=
业务流程管理 75% 49 65% 97 -10%
机器人流程自动化 28% 18 42% 63 +14%
企业资源规划 31% 20 39% 58 +8%
超自动化* 15% 22
其他 11% 7 5% 8 -6%

*注:"超自动化"是我们为2024年调查添加的回复选项。

这些结果可能表明,使用低代码进行工作流/流程自动化的组织正在扩展他们采用的方法。

  • 大多数受访者(63%)表示他们的组织将低代码AI工具用于至少一个列出的用例——最流行的用例是"聊天机器人/虚拟助手"。
    • 小型组织的受访者声称其组织使用低代码AI工具的可能性要低得多,有34%选择"不适用",而中型组织的受访者为12%,大型组织为14%。
    • 小型组织的受访者声称其组织使用低代码处理"聊天机器人/虚拟助手"(23%)和"预测分析"(8%)的可能性显著低于中型组织(分别为40%和27%)和大型组织(分别为47%和27%)的受访者。
  • 有趣的是,小型组织的受访者声称其组织使用低代码AI进行"语言处理"(24%)的可能性高于中型组织(15%),但仍低于大型组织(31%)。

研究目标三:开发人员如何看待低代码对开发和软件质量的影响

我们在研究发现中试图至少触及的一个主要问题是:"这个主题的影响是否与其周围的炒作相符?"换句话说,我们想知道我们讨论的主题是否对软件开发整体产生积极影响。为此,我们在本节中考察:

  • 低代码对软件质量的影响
  • 低代码安全关切以及对低代码治理的需求

低代码对软件质量的影响

在本报告的第一个研究目标中,我们考察了开发人员对低代码影响开发的感受和看法。我们还希望考察开发人员对使用低代码工具的开发过程和软件的第一手经验——这个区别可能很小,但我们认为很重要。

在这里,我们考察的是那些报告至少有一些与低代码创建软件交互或自己使用低代码工具构建应用程序经验的开发人员(正如我们之前看到的,他们占受访者的93%)的回答,提出以下问题:

根据您的经验,使用低代码工具总体上使软件…*
根据您的经验,使用低代码工具是否使开发更具迭代性?*
根据您的经验,使用低代码工具导致总体上…*

结果:

图15. 低代码对软件性能、可维护性、可扩展性和安全性的影响 [n=199]

(图表数据翻译:针对每个属性,显示"更好"、"大致相同"、"更差"的百分比)

  • 性能:更好 26%,大致相同 40%,更差 25%
  • 可维护性:更好 47%,大致相同 30%,更差 17%
  • 可扩展性:更好 40%,大致相同 32%,更差 21%
  • 安全性:更好 38%,大致相同 35%,更差 20%

图16. 低代码对开发迭代性的影响 [n=196]

(图表数据翻译:)

  • 是:53%
  • 否:25%
  • 无意见:22%

图17. 低代码对软件质量的影响 [n=197]

(图表数据翻译:)

  • 更高质量的软件:41%
  • 相同质量的软件:28%
  • 更低质量的软件:24%
  • 无意见:7%

*注:这三个问题仅询问了未在"您使用低代码平台构建软件的频率如何?"问题中回答"从未" 或未在"您编写与使用低代码平台构建的软件交互的代码的频率如何?"问题中回答"从未"的受访者。

观察

  • 可维护性是受访者最可能表示被低代码改进的特性,尽管认为低代码工具改善了可扩展性和安全性的受访者也占多数。受访者最可能说低代码构建的软件性能与全代码应用程序大致相同,而表示低代码使软件性能"更好"或"更差"的受访者数量大致相当。
    表示最频繁("经常"或"一直")使用低代码工具构建软件的受访者,比其他受访者更可能发现低代码使软件性能更好(36%)、更可维护(65%)、更可扩展(53%)和更安全(55%)。
  • 略多于半数的受访者认为低代码工具使软件开发更具迭代性。这些结果与我们2022年收集的结果相比没有显著变化。
    再次,报告"经常"或"一直"使用低代码工具构建软件的受访者,比那些"有时"使用低代码工具构建(60%)和那些"很少"或"从未"使用低代码工具构建(36%)的受访者更可能发现使用低代码使开发更具迭代性(74%)。
    由于许多低代码平台是专门为支持快速原型设计和快速修改而设计的,允许开发人员根据即时反馈持续完善和改进应用程序,这些结果符合预期。我们再次看到,更广泛地使用低代码工具的经验会带来对其潜力的更积极看法
  • 显著更多的受访者表示,他们的经验表明低代码工具导致了更高质量的软件,而不是表示低代码导致相同或更低质量软件的受访者,尽管他们并未形成多数。
    虽然这些结果与我们2023年调查收集的结果有显著不同,但它们已恢复到我们在2021年低代码开发和2022年低代码开发调查中看到的比率(详情见表8)。目前尚不清楚去年是什么可能影响了低代码构建软件感知质量的下降。
表8. 低代码对软件质量的影响:2021-2024 影响 2021 2022 2023 2024
更高质量的软件 39% 38% 22% 41%
相同质量的软件 26% 28% 24% 28%
更低质量的软件 24% 25% 32% 24%
无意见 11% 10% 22% 7%

再次,使用低代码"经常"或"一直"构建软件的受访者,经历低代码工具允许创建更高质量软件的可能性(61%)远高于那些"有时"使用低代码构建软件(37%)或"很少"/"从未"使用(27%)的受访者。我们认为这与低代码平台可以允许的迭代性密切相关,如前一观察所述。

低代码安全与治理

在构建任何类型的软件时,治理和安全性都是至关重要的考虑因素,因此,在处理低代码工具时,这些将是尤其需要观察的重要领域,因为低代码工具可能允许新平台广泛访问应用程序基础设施——并且可能涉及开发团队以外的员工来添加功能或与软件内部交互。我们想找出在低代码使用方面,开发人员最关心治理和安全的哪些方面,因此我们询问:

在以下选择中,您认为在您组织中建立对低代码工作治理的主要需求是什么?
您认为OWASP低代码/无代码十大安全关切中,哪些是您当前贡献的软件的显著潜在威胁?*

结果:

图18. 建立对低代码工作治理的主要需求 [n=204]

(图表数据翻译:)

  • 管理端到端应用程序开发生命周期:31%
  • 避免冗余应用程序的蔓延:22%
  • 保护免受对其他企业系统和数据的影响:22%
  • 维持企业质量和性能标准的可见性:21%
  • 其他:4%

图19. 构成显著潜在威胁的低代码安全关切 [n=197]

(图表数据翻译:按选择百分比排序)

  • 身份验证和安全通信故障:41%
  • 数据和密钥处理故障:40%
  • 授权滥用:38%
  • 易受攻击和不受信任的组件:35%
  • 安全日志记录和监控故障:33%
  • 注入处理故障:28%
  • 资产管理故障:25%
  • 数据泄漏和意外后果:24%
  • 账户冒充:21%
  • 无:13%

*注:此问题仅询问了未在"您使用低代码平台构建软件的频率如何?"问题中回答"从未" 或 未在"您编写与使用低代码平台构建的软件交互的代码的频率如何?"问题中回答"从未"的受访者。

观察

  • 受访者关于建立对低代码工作治理的主要原因的看法在提供的四个选项中分布相当均匀,"管理端到端应用程序开发生命周期"略微领先于其他选项。这些结果与我们2022年调查收集的结果相比没有显著变化。
    表示"经常"或"一直"使用低代码平台构建软件的受访者比其他受访者更可能将"维持企业质量和性能标准的可见性"视为低代码治理的主要原因,并且更不可能选择"保护免受对其他企业系统和数据的影响"和"避免冗余应用程序的蔓延"。表示"很少"或"从未"使用低代码工具构建软件的受访者比其他受访者更不可能选择"管理端到端应用程序开发生命周期"作为建立治理的主要需求。此数据的详细信息见表9:
表9. 按使用低代码工具构建软件的频率划分的建立低代码治理的主要需求 需求 使用频率
从未/很少 有时 经常/一直
管理端到端应用程序开发生命周期 23% 35% 37%
维持企业质量和性能标准的可见性 24% 17% 35%
保护免受对其他企业系统和数据的影响 23% 25% 15%
避免冗余应用程序的蔓延 27% 23% 10%
其他 4% 0% 3%
  • 没有一个单一的安全关切被认为是对受访者贡献过的软件的"显著潜在威胁",但四分之三的受访者选择了至少一个他们认为可能证明是严重的关切,大约一半(49%)选择了三个或更多,大约四分之一(24%)选择了OWASP低代码/无代码十大安全关切中的五个或更多。
    全栈开发者比其他开发者更可能认为"易受攻击和不受信任的组件"(41%)、"安全日志记录和监控故障"(36%)、"注入处理故障"(31%)和"账户冒充"(26%)是潜在的严重威胁,而非后端/全栈开发者比其他开发者更可能将"授权滥用"(40%)视为可能严重的威胁,并且更不可能将"数据泄漏和意外后果"(31%)和"身份验证和安全通信故障"(15%)视为此类威胁。
    表示最频繁使用低代码工具构建软件的受访者比其他受访者更可能发现"注入处理故障"(29%)是严重威胁,并且更不可能相信"授权滥用"(23%)和"账户冒充"(15%)是主要关切。这些受访者也更可能声称OWASP低代码/无代码十大安全关切中没有一个对其软件构成显著威胁(13%)。
    表示"很少"或"从未"使用低代码构建软件的受访者比其他受访者更可能选择"资产管理故障"(19%),并且更不可能选择"数据和密钥处理故障"(18%)作为潜在的严重威胁。

未来研究

我们这里的分析仅触及了可用数据的表面,我们将在制作未来的趋势报告时完善和扩展我们的低代码开发调查。我们在本报告中未涉及但已纳入调查的一些主题包括:

  • 开发人员希望业务用户创建简单自动化的意愿
  • 低代码和全代码之间互操作性问题的频率
  • 低代码避免可预防开发错误的能力

通过低代码和无代码集成转变软件开发

创建更快、更智能的软件开发流程
作者:Shantanu Kumar, Amazon 高级软件工程师

尽管传统的软件开发生命周期(SDLC)速度缓慢且无法满足动态的业务需求,但它长期以来一直是公司构建应用程序最流行的方式——直到低代码和无代码(LCNC)工具的出现。这些工具简化了编码过程,使得高级开发人员和非技术用户都可以做出贡献,通过缩短SDLC使组织能够快速响应市场需求。

请继续阅读,了解软件开发如何因LCNC工具而改变,如何将它们集成到您的运营中,以及集成时可能出现的挑战。

理解低代码和无代码开发

低代码和无代码开发环境让人们能够通过可视化界面、拖放工具和可重用组件来构建应用程序,而无需手动编写代码。低代码开发平台是可视化开发环境,使任何技能水平的开发人员都能够将组件拖放到面板上并连接它们以创建移动或Web应用程序。无代码开发平台则面向没有或几乎没有编码经验的用户。

那么,您如何利用这些平台来增强传统的SDLC呢?

假设您是一名具有基本编码技能的设计师。使用LCNC平台,您可以快速使用可重用组件创建原型,而无需编写一行代码。这将加快软件开发过程,并确保最终产品满足用户需求。

低代码和无代码集成的规划与评估

尽管SDLC因不同的SDLC模型而在公司间有所不同,但它通常包括以下阶段:项目规划、需求收集与分析、设计、测试、部署和维护。这个过程确保了高度的细节,但减慢了开发周期并消耗了大量资源。

低代码和无代码工具解决了这一挑战。例如,在设计阶段,人力资源团队可以使用LCNC平台提供的可重用组件或预制模板快速设计他们的招聘门户,以轻松跟踪候选人、职位发布和面试安排。

也就是说,在将LCNC平台集成到您现有的工作流之前,请考虑您团队的专长、您选择的平台与您的IT基础设施的兼容性以及平台的安全特性。

低代码和无代码集成的步骤

要将低代码和无代码工具集成到您的运营中,请遵循以下步骤:

图1. 实现无缝LCNC集成的简化步骤

(图表描述:一个循环图,包含以下步骤:1. 定义目标和目的 -> 2. 选择合适的LCNC平台 -> 3. 培训团队成员 -> 4. 设计集成架构 -> 5. 实施集成框架 -> 6. 进行测试和质量保证 -> 7. 管理部署和发布 -> 8. 监控和维护 -> 返回步骤1 形成循环)

表1扩展了这些步骤,并包括每个步骤的示例:
表1. 集成LCNC工具的步骤

步骤 描述 示例
1. 定义目标和目的 明确定义您想要实现的目标,并使其具体化。目标可能包括加速开发、降低成本或提高团队生产力。 在六个月内使用预构建模板将应用程序开发时间减少40%。
2. 选择合适的LCNC平台 不要将就。评估各种平台,并将其与您的需求和目标相匹配。考虑用户友好性、安全特性以及与现有系统的兼容性。 如果大多数团队成员不精通技术,则选择一个主要因其易用性和教育支持而突出的无代码平台。
3. 培训和引导团队成员 确保所有团队成员,无论是否精通技术,都能使用您的平台 安排讲座和网络研讨会,甚至为您的团队成员报名参加LCNC平台提供的专业课程。
4. 设计集成架构 确保您的平台设计得易于与当前系统集成 映射数据在现有系统和首选平台之间的流动方式。
5. 实施集成框架 创建一个在SDLC内集成LCNC平台的框架。在最简单的层面上,这可能涉及为SDLC每个阶段选择工具创建指南。 集成LCNC平台以收集客户调查回复、产品发布反馈或联系表单提交。没有这些工具,开发人员需要从头开始构建功能,需要大量的前端开发和存储集成,导致更高的成本和更长的开发周期。
6. 进行测试和质量保证 使用混合测试方法(如单元测试、集成测试和验收测试)对您的LCNC工具进行严格测试。 您可以执行验收测试以确保您的应用程序满足最终用户的需求和期望。
7. 管理部署和发布 以结构化的方式使用部署策略(例如,滚动部署)将您的应用程序部署给最终用户,并包含在出现不可预见问题时回滚的计划。 您可以使用云解决方案来自动化部署。
8. 监控和维护 部署后监控应用程序的性能以检测潜在的相关问题。 在维护期间,您可能会遇到错误。安排定期错误修复可以维护应用程序的稳定性、功能性和安全性。

集成低代码和无代码:要实施和避免的实践

虽然低代码和无代码平台简化了SDLC,但实施它们需要结构化的方法。接下来,我们将探讨在集成过程中需要考虑的最佳实践和适得其反的实践。

实施:渐进式采用

将低代码和无代码平台逐步集成到现有的流程和系统中,可以最大限度地减少对正在进行的运营的干扰。首先将LCNC解决方案用于非关键项目,这些项目可以作为完善集成策略的试验场。例如,开发人员可以逐步迁移LCNC流程,从非关键的、易于打包的流程开始,并随着时间的推移逐渐扩大规模。非关键流程,如电子邮件通知,更适合缓慢、迭代地推广到一小部分客户。

实施:协作开发

协作开发是一种强调团队合作的方法论。它将SDLC中涉及的各种利益相关者聚集在一起,例如项目经理、业务分析师、UX/UI设计师、软件开发人员以及其他技术和非技术人员。这种方法考虑了每个利益相关者的意见,从而交付高质量的应用程序。通过为SDLC中涉及的每个利益相关者建立明确的角色和职责来鼓励协作。

实施:混合开发模型

将低代码和无代码平台与传统编码相结合提供了一种平衡的方法。虽然LCNC平台可以加速开发,但复杂的功能可能需要自定义代码。采用混合方法可以促进灵活性,并在不牺牲传统编码提供的增强功能的情况下维护应用程序的完整性。

实施:持续反馈循环

低代码和无代码工具加速了反馈循环,允许团队快速构建原型、早期并经常收集用户反馈,并根据收到的反馈完善应用程序。这种方法确保最终产品符合用户需求和期望,并快速适应动态的业务需求。

避免:过度依赖低代码和无代码平台

低代码和无代码工具并非旨在彻底改变传统编码。复杂的逻辑或性能关键的任务仍然需要传统的软件开发方法。因此,企业应采用混合开发模型。

避免:缺乏适当的培训和教育

如果使用不当,低代码和无代码可能弊大于利。部署不当可能导致停机,这会迅速增加客户流失和声誉受损方面的成本(例如,在服务许多客户的情况下)——即使一秒钟的不可用性也会带来巨大的成本。从这些开创性平台受益的能力完全依赖于为技术和非技术用户提供适当的培训,以避免累积的异常情况。

避免:忽视安全和合规性问题

低代码和无代码平台消除了与常规SDLC流程相关的各种障碍。然而,它们也带来了安全关切,主要是因为您选择的平台托管着您的数据。评估您选择的低代码或无代码平台的安全特性,以确保其满足您组织的数据保护法规和其他行业法规(例如,GDPR、HIPAA、CCPA),以避免安全漏洞和法律问题。

避免:忽略可扩展性和定制化要求

并非所有的低代码和无代码平台都能很好地扩展或允许足够的定制。例如,一些平台限制了使用它们的团队成员数量,而其他平台则有存储限制。这对于成长中的企业或有特定需求的企业来说可能是一个巨大的障碍。在确定平台之前,评估您正在考虑的平台是否可以扩展和定制以满足长期业务目标。

低代码和无代码的挑战与缓解策略

将低代码和无代码工具纳入现有流程带来了一些独特的障碍。表2描述了将LCNC工具集成到SDLC中的常见挑战及其相应的缓解策略:

表2. 集成低代码和无代码:挑战与缓解策略

障碍 挑战 缓解策略
变革阻力 通常是最普遍的挑战;员工担心LCNC工具学习曲线陡峭 实施广泛的培训计划,使团队成员具备必要的技能组合
不兼容和互操作性 可能阻碍LCNC平台的集成(例如,由于与过时的数据库协议不兼容) 严格评估平台,确保与现有系统兼容,或者它们可以连接到未通过API连接的系统
技术限制 可能阻止LCNC平台的集成(即,缺乏可扩展性) 选择从一开始就可扩展或提供混合开发方法的平台

SDLC中低代码和无代码的未来

随着低代码和无代码平台的发展,我们可以预期软件开发实践将发生重大转变。虽然LCNC工具不会使传统编码过时,但它们将加速开发、降低成本、最小化技术债务,并民主化应用程序开发——允许更多人在没有高级编程技能的情况下构建软件。

低代码和无代码开发工具不仅仅是一种流行趋势。它们将继续存在,并将改变我们开发和维护软件的方式。Gartner估计,到2025年,企业开发的所有新应用程序中有70%将使用LCNC技术。新兴LCNC领域的现有趋势表明,这些平台将发展到支持日益复杂的功能,例如高级工作流和集成。最重要的是,AI将成为这一演进的核心。提供数字聊天机器人、图像识别、个性化和其他高级功能的AI增强型LCNC平台已经上市。

结论

Forrester表示,低代码和无代码工具正在"重新定义组织构建软件的方式";仅低代码市场预计到2028年将达到近300亿美元的价值。如果您希望您的组织跟上步伐,您就不能忽视LCNC平台。通过实施这些步骤,组织可以有效地集成LCNC解决方案:

  1. 组织应设定明确的目标,说明他们希望通过LCNC解决方案实现什么。然后,他们应根据具体需求选择合适的平台。
  2. 组织应就所选平台培训其团队。
  3. 团队应仔细将新系统与现有系统集成,并在部署之前对其进行彻底测试。

最终,成功的集成取决于采用最佳实践(例如,渐进式采用、协作开发、混合开发)和避免适得其反的实践(例如,严重依赖LCNC工具、未能考虑安全性和可扩展性)。

您还没有使用低代码和无代码工具吗?将它们引入您现有的工作流以支持您的SDLC过程。

补充资源:

  • 《使用Mendix构建低代码应用程序:发现简化企业Web开发的最佳实践和专家技术》作者:Bryan Kenneweg, Imran Kasam, 和 Micah McMullen
  • Ponemon研究所的《数据中心停机的成本》
  • Gartner的《Gartner称云将成为新数字体验的核心》
  • Forrester的《低代码市场到2028年可能接近500亿美元》

Shantanu Kumar

我在亚马逊工作超过九年,领导团队从事大规模数据系统、AI基础设施和云平台的工作。我领导了"Buy with Prime"项目,提升了美国Prime购物体验并支持小企业。作为IEEE、ACM和BCS的活跃成员,我参与软件工程讨论,并作为演讲者和作家分享我的专业知识,热情地为技术社区做出贡献。


合作伙伴安全研究

DoorDash案例

DoorDash如何利用Retool在3个月内完成了2年的路线图工作

DoorDash是全国最大的最后一英里物流平台,连接着美国、加拿大、波多黎各和澳大利亚50个州、4000多个城市的客户与他们最喜欢的本地和全国企业。

挑战

DoorDash的工程团队负责构建所有自己的内部工具,作为一家运营繁重的公司,有大量工作要做。Rohan Chopra负责管理Dasher团队的工程工作,为他的团队构建内部工具既繁琐又关键。构建用于可视化Dasher路线、绘制区域或使团队能够与餐厅合作的一次性工具是一项手动、耗时数月的工作

Rohan的团队最初使用了Django的管理面板,但立即面临挑战:它不是为了扩展而构建的,并且缺乏他们所需的定制性。Django的对象关系映射器生成了低效的查询,给DoorDash的基础设施带来了压力,并且需要手动输入才能完成诸如将Dasher状态设置为"暂停"之类的基本操作。

“投资内部工具曾经是一个困难且两极分化的权衡;Retool通过使工具成为任何项目中快速且轻松的一部分,帮助我们改变了这种范式,为我们节省了无数时间。”
Rohan Chopra,
DoorDash工程总监

结果

一个具体的痛点是Dasher团队的奖励计划。保持该计划运行需要团队成员手动填写一个庞大的电子表格,将数据交给工程团队,并运行每周脚本。这个过程经常出问题:输入错误的数据、错过的交接和未捕获的错误需要双方花费大量时间来恢复。

解决方案

通过在网络上搜索找到Retool后,Rohan的团队开始使用Retool构建他们的内部工具以节省时间并改进流程。工程师能够在几小时内构建他们的新工具,并更有效地支持他们的运营团队,而无需数周的开销

在Rohan的团队采用后,Retool集成自然地扩展到整个DoorDash组织。"我们没有发送任何电子邮件:每当工程师们谈论构建内部工具时,Retool就会出现,新的团队就会尝试它,"Rohan说。

Retool极大地减少了Rohan团队构建内部工具所需的时间:"我们每个需要构建的工具从1-2个月缩短到30-60分钟。"工具的构建过程变得短得多,但维护也显著更容易——像添加下拉菜单或过滤器这样的小调整变成了"几分钟"而不是"我们不确定"。运行脚本和填写电子表格等手动工作被自动化并按计划进行,而不是令人沮丧和被遗忘。

如今,DoorDash拥有40多个在Retool中构建的活跃运营工具,部分成功在于Retool所实现的民主化——为DoorDash节省了600万美元的工程时间。


贡献者洞察

AI在低代码和无代码开发中的角色

作者:Eric Goebelbecker, Hit Subscribe 项目实践经理

大型语言模型(LLMs)的出现导致了一场将人工智能(AI)强行融入每一个有意义的产品中的热潮,以及不少没有意义的产品。但有一个领域,AI已经被证明是一个强大而有用的补充:低代码和无代码软件开发。让我们看看AI如何以及为何使构建应用程序更快、更容易,尤其是在使用低代码和无代码工具时。

AI在开发中的角色

首先,我们讨论AI在简化和加速开发过程中最常见的两个角色:生成代码充当智能助手

AI代码生成器和助手使用在大型代码库上训练的LLMs,这些代码库教会它们编程语言的语法、模式和语义。这些模型预测满足提示所需的代码——就像聊天机器人使用它们的训练来预测句子中的下一个单词一样。

自动化代码生成

AI代码生成器根据输入创建代码。这些提示采用自然语言输入或集成开发环境(IDE)中或命令行中的代码形式。代码生成器通过将程序员从编写重复性代码中解放出来而加速开发。它们也可以减少常见错误和排版错误。但与用于生成文本的LLMs类似,代码生成器需要仔细审查,并且可能犯自己的错误。开发人员在接受AI生成的代码时需要小心,他们不仅需要测试它是否能构建,还需要测试它是否完成了用户要求的功能
gpt-engineer是一个开源的AI代码生成器,它接受自然语言提示来构建整个代码库。它可以与ChatGPT或像Llama这样的自定义LLMs一起工作。

用于开发的智能助手

智能助手在开发人员工作时为他们提供实时帮助。它们作为一种AI代码生成器,但不是使用自然语言提示,它们可以自动完成、提供内联文档并接受专门的命令。助手可以在像Eclipse和Microsoft的VS Code这样的编程工具内部、命令行或三者同时工作。这些工具提供了许多与代码生成器相同的好处,包括更短的开发时间、更少的错误和减少的拼写错误。它们还可以作为学习工具,因为它们在工作时为开发人员提供编程信息。但与任何AI工具一样,AI助手并非万无一失——它们需要密切和仔细的监控
GitHub的Copilot是一个流行的AI编程助手。它使用基于公共GitHub仓库构建的模型,因此它支持非常广泛的语言,并可以插入所有最流行的编程工具。Microsoft的Power PlatformAmazon Q Developer是两个流行的商业选项,而Refact.ai是一个开源替代品。

AI与低代码和无代码:完美结合

低代码和无代码是为了满足需要让新手和非技术人员快速为其需求定制软件的工具而发展起来的。AI通过使将想法转化为软件变得更加容易,将这一点更进一步。

民主化开发

AI代码生成器和助手通过使编码更容易接触、提高生产力并促进持续学习来民主化软件开发。这些工具降低了编程新手的入门门槛。一个新手程序员可以使用它们边做边学,快速构建可运行的应用程序。例如,Microsoft Power Apps包含Copilot,它可以为您生成应用程序代码,然后与您一起完善它。

AI如何增强低代码和无代码平台

AI通过几种重要方式增强低代码和无代码平台。我们已经介绍了AI根据自然语言提示或代码编辑器中的上下文生成代码片段的能力。您可以使用像ChatGPT和Gemini这样的LLMs为许多低代码平台生成代码,而许多无代码平台如AppSmith和Google AppSheet使用AI根据描述您希望集成做什么的文本来生成集成。您还可以使用AI自动化准备、清理和分析数据。这使得集成和处理需要调整才能与您的模型一起使用的大型数据集变得更加容易。像Amazon SageMaker这样的工具使用AI来摄取、分类、组织和简化数据。一些平台使用AI帮助创建用户界面和填充表单。例如,Microsoft的Power Platform使用AI使用户能够通过与它的copilot进行对话交互来构建用户界面和自动化流程。
所有这些功能都有助于使低代码和无代码开发更快,包括在可扩展性方面,因为更多的团队成员可以参与开发过程。

低代码和无代码如何实现AI开发

虽然AI对于生成代码非常宝贵,但它在您的低代码和无代码应用程序中也很有用。许多低代码和无代码平台允许您构建和部署支持AI的应用程序。它们抽象掉了添加诸如自然语言处理、计算机视觉和AI API等功能到您的应用程序中的复杂性。
用户期望应用程序提供诸如语音提示、聊天机器人和图像识别等功能。从头开始开发这些能力需要时间,即使对于有经验的开发人员也是如此,因此许多平台提供模块,使得只需很少或无需代码即可轻松添加它们。例如,Microsoft有用于在Azure上构建Power Virtual Agents(现已成为其Copilot Studio的一部分)的低代码工具。这些代理可以插入由Azure服务支持的多种技能,并使用聊天界面驱动它们。
像Amazon SageMaker和Google的Teachable Machine这样的低代码和无代码平台管理诸如准备数据、训练自定义机器学习(ML)模型和部署AI应用程序等任务。而Zapier则利用Amazon Alexa的语音转文本功能,并将输出引导到许多不同的应用程序。

图1. 使用构建块构建低代码AI应用

(图表描述:展示了使用预构建的AI组件/API(如NLP、计算机视觉、预测分析)通过低代码平台快速组装成应用程序的过程。)

支持AI的低代码和无代码工具示例

此表包含广泛使用的低代码和无代码平台列表,这些平台支持AI代码生成、支持AI的应用程序扩展,或两者兼有:
表1. 支持AI的低代码和无代码工具

应用程序 类型 主要用户 关键特性 AI/ML能力
Amazon CodeWhisperer AI驱动的代码生成器 开发人员 • 实时代码建议
• 安全扫描
• 广泛的语言支持
ML驱动的代码建议
Amazon SageMaker 全托管的ML服务 数据科学家, ML工程师 • 能够构建、训练、部署ML模型
• 完全集成的IDE
• 支持MLOps
预训练模型, 自定义模型训练和部署
GitHub Copilot AI结对程序员 开发人员 • 代码建议
• 多语言支持
• 上下文感知建议
用于代码建议的生成式AI模型
Google Cloud AutoML 无代码AI 数据科学家, 开发人员 • 可以用最少的精力训练高质量的定制ML模型
• 支持各种数据类型,包括图像、文本和音频
自动化的ML模型训练和部署
Microsoft Power Apps 低代码应用程序开发 业务用户, 开发人员 • 可以构建定制业务应用
• 支持多种不同的数据源
• 自动化工作流
用于增强应用的AI构建器
Microsoft Power Platform 低代码平台 业务分析师, 开发人员 • 商业智能
• 应用程序开发
• 应用程序连接性
• 机器人流程自动化
用于增强应用和流程的AI应用构建器

使用AI进行开发的陷阱

AI改善低代码和无代码开发的能力是不可否认的,但其风险也是如此。任何AI的使用都需要适当的培训和全面的治理。LLMs倾向于对提示"产生幻觉"答案的情况也适用于代码生成。
因此,虽然AI工具降低了新手开发者的入门门槛,您仍然需要经验丰富的程序员在将代码部署到生产环境之前进行审查、验证和测试。

  • 开发人员通过提交提示和接收响应来使用AI。根据项目的不同,这些提示可能包含敏感信息。如果模型属于第三方供应商或未正确保护,您的开发人员就会暴露这些信息。
  • 当它工作时,AI会建议可能满足其正在评估的提示的代码。代码是正确的,但不一定是最佳解决方案。因此,严重依赖AI生成代码可能导致代码难以更改并代表大量的技术债务。

AI已经在民主化编程和加速低代码和无代码开发方面做出了重要贡献。随着LLMs的逐步改进,用于创建软件的AI工具只会变得更好。即使这些工具在改进,IT领导者仍然需要谨慎行事。
AI提供了强大的力量,但这种力量伴随着巨大的责任。任何和所有AI的使用都需要全面的治理和完整的保障措施,以保护组织免受错误、漏洞和数据丢失的影响。

结论

将AI集成到低代码和无代码开发平台中已经彻底改变了软件开发。它民主化了高级编码的访问,并赋能非专家,使他们能够构建复杂的应用程序。AI驱动的工具和智能助手减少了开发时间,提高了开发可扩展性,并有助于最小化常见错误。但这些强大的能力伴随着风险和責任。
开发人员和IT领导者需要建立强大的治理、测试制度验证系统,如果他们想要安全地利用AI的全部潜力。

AI技术和模型持续改进,它们很可能将成为创新、高效和安全的软件开发的基石。看看AI如何通过低代码和无代码工具帮助您的组织扩大开发工作。

Eric Goebelbecker

近30年来,Eric在华尔街多家市场数据和交易公司担任开发人员和系统工程师。现在,他把时间花在撰写技术和科幻文章、训狗和骑自行车上。


使用低代码平台编排IAT、IPA和RPA

高级自动化与测试的益处与挑战
作者:Stelios Manioudakis, Transifex 首席QA工程师

当软件开发团队面临快速交付高质量应用程序的压力时,低代码平台为快速发展的业务需求和复杂集成提供了所需的支持。集成能够更 readily 适应变化的智能自动化测试(IAT)、智能流程自动化(IPA)和机器人流程自动化(RPA)解决方案,可确保测试和自动化跟上不断发展的应用程序和流程的步伐。在低代码开发环境中,如图1所示,IAT、IPA和RPA可以减少手动工作,并提高SDLC和流程自动化中的测试覆盖率、准确性和效率。
图1. 低代码开发环境

(图表描述:展示了低代码平台作为中心,连接着用户界面、数据源、服务/API,并通过IAT、IPA、RPA与开发运维流程和业务用户交互。)

将IAT、IPA、RPA与低代码平台结合使用还可以实现更快的上市时间、降低的成本和提高的生产力。IAT、IPA、RPA和低代码的交叉点是现代软件开发和流程自动化中的范式转变,其影响延伸到专业服务、消费品、银行业等行业。

本文探讨了所有三种集成。对于每种集成,我们将强调优点和缺点,探讨决定是否集成时需要考虑的因素,提出一个用例,并强调关键实施点。所呈现的用例是这些技术如何在特定场景中应用的流行示例。这些用例并不意味着每种集成仅限于所提及的领域,也不暗示这些集成不能在同一领域内以不同方式使用。本文探讨的三种集成的灵活性和多功能性允许跨不同行业和流程的广泛应用。

低代码开发中的IAT

智能自动化测试中AI驱动的测试用例生成可以探索更多场景、边界情况和应用程序状态,从而实现更好的测试覆盖率和更高的应用程序质量。这在低代码环境中尤其有益,因为复杂的集成和快速发展的需求可能使全面测试具有挑战性。
通过自动化测试任务,例如测试用例生成、执行和维护,IAT可以显著减少所需的手动工作,从而提高效率和节约成本。这在涉及有限测试经验的公民开发者的低代码开发中是有利的,最大限度地减少对专用测试资源的需求。
低代码平台支持快速应用程序开发,但测试可能成为瓶颈。自动化测试和IAT可以就应用程序质量和潜在问题提供快速反馈,从而能够更快地识别和解决缺陷。这可以加速整体开发和交付周期。它还可以允许组织在保持质量标准的同时利用低代码的速度。
不过,我们需要记住,并非所有低代码平台都可以与所有IAT解决方案集成。IAT解决方案可能需要访问敏感的应用程序数据、日志和其他信息,用于训练AI/ML模型和生成测试用例。在IAT中AI/ML需要培训和软件工程技能发展的情况下,我们还需要考虑诸如维护和支持以及定制和基础设施等成本。
是否将IAT与低代码平台集成的决定涉及许多因素,下表突出了这些因素:
表1. 将IAT与低代码开发集成

何时集成 何时不集成
快速开发至关重要,但只有有限测试经验的公民开发者可用 简单的应用程序功能有限,且低代码平台已提供足够的测试能力
基于低代码平台构建的应用程序有良好的IAT集成选项 复杂性和学习曲线高,需要深入理解AI/ML
复杂的应用程序需要全面的测试覆盖,需要大量测试 存在兼容性、互操作性和数据孤岛问题
频繁的发布周期,拥有完善的CI/CD管道 数据安全和法规合规性是挑战
需要增强测试过程的决策能力 存在预算限制

用例:专业服务

将使用低代码平台开发定制审计应用程序。由于可以集成IAT工具来自动测试这些应用程序,一家专业服务公司将利用IAT来提高其审计和鉴证服务的准确性、速度、效率和有效性。实施要点总结在下图2中:
图2. 用于定制审计应用程序的低代码开发与IAT

(图表描述:展示了低代码平台用于构建审计应用,IAT工具集成进行自动化测试,并与数据源、用户和CI/CD管道交互,最终实现更快的发布周期和更高的质量。)

在这个将IAT与低代码集成的专业服务用例中,定制审计应用程序也可以为医疗保健或金融等行业开发,在这些行业中,自动化测试可以提高合规性和风险管理。

低代码开发中的IPA

智能流程自动化可以通过自动化软件开发和测试生命周期的各个方面来显著提高效率。低代码环境可以受益于IPA的高级AI技术,例如机器学习、自然语言处理(NLP)和认知计算。这些增强功能允许低代码平台自动化更复杂和数据密集型的任务,这些任务超出了简单的基于规则的流程。
IPA不限于简单的基于规则的任务;它包含了认知自动化能力。这使得IPA能够处理涉及非结构化数据和决策的更复杂场景。IPA可以从数据模式中学习,并根据历史数据和趋势做出决策。这对于涉及复杂逻辑和可变结果的测试场景特别有用。例如,IPA可以通过使用NLP和光学字符识别来处理非结构化数据,如文本文档、图像和电子邮件。
IPA可用于自动化复杂的工作流和决策过程,减少手动干预的需要。可以自动化端到端的工作流和业务流程,包括审批、通知和升级。自动化决策可以基于预定义的标准和实时数据分析处理诸如信用评分、风险评估和资格验证等任务,而无需人工参与。通过IPA,低代码测试可以超越测试应用程序,因为我们可以测试跨越组织不同垂直领域的整个流程。
由于IPA支持广泛的跨垂直领域集成场景,安全性和法规合规性可能成为一个问题。如果低代码平台不完全支持IPA提供的广泛集成,那么我们需要考虑替代方案。基础设施设置、数据迁移、数据集成、许可和定制是所涉及成本的示例。
下表总结了集成IPA前需要考虑的因素:

表2. 将IPA与低代码开发集成

何时集成 何时不集成
存在严格且变化频繁、适应性强、详细且易于自动化的合规和监管要求 监管和安全合规框架过于僵化,存在安全/合规差距和潜在法律问题,导致挑战和不确定性
存在跨垂直领域的重复流程,其效率和准确性可以得到提高 没有明确的优化目标;手动流程足够
需要快速开发和部署可扩展的自动化解决方案 低代码平台对IPA的定制有限
可以简化端到端的业务流程 IT专业知识有限
需要复杂流程优化的决策能力 初始实施成本高

用例:消费品

一家领先的消费品公司希望利用IPA来增强其供应链管理和业务运营。他们将使用低代码平台开发供应链应用程序,并且该平台将具有集成IPA工具以自动化和优化供应链流程的选项。这样的集成将使公司能够提高供应链效率、降低运营成本并缩短产品交付时间。实施要点总结在下图3中:
图3. 用于消费品公司的低代码开发与IPA

(图表描述:展示了低代码平台用于构建供应链应用,IPA工具集成进行流程优化和决策,并与供应商、库存、物流和客户数据交互,最终实现成本降低和效率提升。)

这个在消费品领域将IPA与低代码集成的例子可以适用于零售或制造业等行业,在这些行业中,库存管理、需求预测和生产调度可以得到优化。

低代码开发中的RPA

机器人流程自动化和低代码开发具有互补关系,因为它们可以结合使用以增强组织内的整体自动化和应用程序开发能力。例如,RPA可用于自动化重复性任务并与各种系统集成。低代码平台可被利用来快速构建定制应用程序和工作流,这可能导致更快的上市时间。低代码平台的快速开发能力与RPA的自动化能力相结合,可以使组织快速构建和部署应用程序。
通过使用RPA自动化重复性任务并使用低代码平台快速构建定制应用程序,组织可以显著提高其整体运营效率和生产力。低代码环境中的RPA可以通过最小化手动工作、减少开发时间并使公民开发者能够为应用程序开发做出贡献来实现成本节约。
RPA和低代码平台都提供可扩展性和灵活性,允许组织适应不断变化的业务需求,并根据需要扩展其应用程序和自动化流程。RPA机器人可以动态扩展以处理不同数量的客户查询。在高峰时段,可以部署额外的机器人来管理增加的工作负载,确保持续的服务水平。RPA工具通常具有跨平台兼容性,允许它们与各种应用程序和系统交互,从而增强低代码平台的灵活性。
这里数据敏感性可能是一个问题,因为RPA机器人可能直接访问专有或敏感数据。对于不稳定、难以自动化或不可预测的流程,RPA可能无法提供预期的收益。RPA依赖结构化数据和预定义规则来执行任务。频繁变化、不稳定和非结构化的流程,缺乏清晰和一致的重复模式,可能对RPA机器人构成重大挑战。难以自动化的复杂流程通常涉及多个决策点、异常和依赖关系。虽然RPA可以处理一定程度的复杂性,但它并非为需要深度上下文理解或复杂决策能力的任务而设计。
下表总结了集成RPA前需要考虑的因素:
表3. 将RPA与低代码开发集成

何时集成 何时不集成
现有的系统集成可以通过自动化进一步增强 要自动化的任务涉及非结构化数据和复杂决策
存在重复性任务和流程,手动处理效率低下 必须自动化快速变化和复杂的流程
期望通过自动化大量结构化和重复性任务来节约成本 集成的实施和维护成本高
低代码平台可以利用RPA的可扩展性和灵活性 缺乏技术专业知识
上市时间很重要 RPA机器人在没有保障的情况下操作敏感数据

用例:银行业

一家银行组织旨在通过将RPA与低代码开发平台集成来简化其数据录入流程,以自动化重复性和耗时的任务,例如表单填写、数据提取以及在遗留系统和新系统之间的数据传输。该集成预计将提高运营效率、减少手动错误、确保数据准确性并提高客户满意度。此外,它将使银行能够以更快的速度和可靠性处理增加的客户数据量。
低代码平台将提供快速开发和部署针对银行特定需求定制的定制应用程序的灵活性。RPA将处理后端流程的自动化,确保无缝和安全的数据管理。实施要点总结在下图4中:
图4. 用于银行组织的低代码开发与RPA

(图表描述:展示了低代码平台用于构建前端应用,RPA机器人自动化后端数据录入和传输,并与遗留系统、数据库和新系统交互,最终实现错误减少和客户满意度提高。)

在这个将RPA与低代码集成的银行示例中,虽然RPA用于自动化后端流程,如数据录入和传输,但它也可以自动化前端流程,如客户服务交互和贷款处理。此外,低代码与RPA可以应用于保险或电信等领域,分别自动化索赔处理和客户 onboarding。

结论

技术集成的价值在于其能够赋能社会和组织进化、保持竞争力并在不断变化的格局中茁壮成长——这个格局呼唤创新和生产力以应对市场需求和社会变化。通过拥抱IAT、IPA、RPA和低代码开发,企业可以解锁新的敏捷性、效率和创新水平。这将使他们能够提供卓越的客户体验,同时推动可持续的增长和成功。
随着数字化转型之旅的继续展开,IAT、IPA和RPA与低代码开发的集成将发挥关键作用,并塑造软件开发、流程自动化和跨行业业务运营的未来。

Stelios Manioudakis

Stelios曾在西门子和Atos担任软件专业人士。他还在Softomotive被微软收购期间在RPA领域工作过。目前,他在Transifex担任首席QA工程师。他拥有英国泰恩河畔纽卡斯尔大学的电气、电子和计算机工程博士学位。


使用低代码和无代码工具赋能公民开发者

改变开发者工作流并赋能非技术员工构建应用程序
作者:Sudip Sengupta, Brollyca 技术顾问

低代码和无代码(LCNC)平台的兴起引发了一场关于它们对开发人员角色影响的辩论。对技能贬值的担忧是可以理解的;毕竟,如果任何人都可以构建应用程序,经验丰富的程序员的专业知识会发生什么?

虽然对低代码平台仍然存在一些怀疑,特别是关于它们是否适用于大规模、企业级应用程序,但重要的是要认识到这些平台正在不断发展和改进。许多平台现在提供强大的功能,如模型驱动开发、自动化测试和高级数据建模,使它们能够处理复杂的业务需求。此外,合并自定义代码模块的能力确保了在需要时仍然可以实现专门的功能。

是的,这些工具正在彻底改变软件创建,但现在是时候超越关于它们对开发格局影响的辩论,深入探讨现实情况了。

本文不是无代码平台的推销说辞,而是旨在为开发人员提供对这些工具能做什么和不能做什么的现实理解,它们如何改变开发人员的工作流,以及最重要的是,您如何在AI支持的、LCNC驱动的世界中利用它们的力量变得更高效和有价值。

利用现代LCNC平台优化开发人员工作流

LCNC平台的财务效益是不可否认的。降低的开发成本、更快的上市时间以及对IT更轻的负担是令人信服的论据。但通过赋能个人在没有任何编码经验的情况下开发解决方案来民主化应用程序开发的战略优势,推动了创新和竞争优势。
对于IT来说,这意味着更少的时间花在修复小问题上,更多的时间花在重要的大事上。对于IT以外的团队来说,这就像拥有一个工具箱来构建自己的解决方案。需要一种跟踪项目截止日期的方法吗?有一个应用程序可以做到。想自动化一份繁琐的报告吗?您可能可以自己构建它。

这种转变并不意味着传统的编码技能过时了。事实上,它们变得更有价值。经验丰富的开发人员现在可以专注于构建可重用组件、为公民开发者创建模板和框架,并确保他们的LCNC解决方案与现有系统无缝集成。随着组织日益采用"双速IT"方法,平衡快速、迭代开发的需求与复杂核心系统的维护和增强,这种转变至关重要。

适合LCNC与传统开发的任务类型

要理解传统开发的各种任务与使用无代码解决方案有何不同,请考虑下表中开发人员工作流中的典型任务:

表1. 开发人员工作流任务:LCNC vs. 传统开发

类别 LCNC 传统(全代码) 推荐工具 开发人员参与度
简单表单构建 理想;拖放界面,预构建组件 可能但需要更多手动编码和配置 LCNC 最小;拖放,最少配置
数据可视化 使用内置图表/图形效果很好,可通过一些代码定制 更多定制选项,需要编码库或框架 LCNC或混合(如果需要定制) 最小到中等,取决于复杂性
基本工作流自动化 理想;可视化工作流构建器,易于集成 需要自定义编码和集成逻辑 LCNC 最小到中等;集成可能需要一些脚本编写
前端应用程序开发 适用于基本UI;复杂的交互需要编码 对UI/UX的完全控制但更耗时 混合 中等;需要前端开发技能
复杂集成 限于预构建的连接器,通常需要自定义代码 灵活且强大但需要专业知识 全代码或混合 高;深入理解API和数据格式
自定义业务逻辑 不理想;可能需要变通方法或有限的自定义代码 完全灵活地实现任何逻辑 全代码 高;强大的编程技能和领域知识
性能优化 选项有限,通常由平台处理 对代码优化的完全控制但需要深厚的专业知识 全代码 高;性能分析和代码优化方面的专业知识
API开发 某些平台可能,但复杂性有限 完全灵活但需要API设计和编码技能 全代码或混合 高;API设计和实现技能
安全关键型应用程序 取决于平台的安全特性,可能不足 对安全实现的完全控制但需要专业知识 全代码 高;安全最佳实践和安全编码方面的专业知识

从LCNC平台获得最大收益

无论您是构建自己的无代码平台还是采用现成的解决方案,好处都可能是巨大的。但在开始之前,请记住任何LCNC平台的核心都是将用户的可视化设计转换为功能代码的能力。这是真正魔力发生的地方,也是最大挑战所在。为了让LCNC平台帮助您取得成功,您需要从深入了解目标用户开始。他们的技术技能是什么?他们想使用什么样的应用程序?这些问题的答案将影响您平台设计的各个方面,从用户界面/用户体验(UI/UX)到底层架构。
UI/UX对于任何LCNC平台的成功都至关重要,但它只是冰山一角。在底层,您需要一个强大的引擎,可以将视觉元素转换为干净、高效的代码。这通常涉及复杂的AI算法、数据结构以及对各种编程语言的深入理解。您还需要考虑您的平台将如何处理业务逻辑、与其他系统的集成以及部署到不同的环境。
图1. 典型的LCNC架构流程

(图表描述:展示了从用户通过UI/UX设计应用,到平台引擎进行逻辑处理、代码生成、数据管理和集成,最后部署到各种环境(Web、移动、云)的流程。)

许多公司已经拥有复杂的IT环境,引入新平台可能会产生兼容性问题。选择一个提供强大集成选项(无论是通过API、Webhooks还是预构建连接器)的LCNC平台至关重要。您还需要决定是采用完全无代码的解决方案,还是允许自定义编码的低代码解决方案。需要考虑的其他因素包括您将如何处理版本控制、测试和调试。

赋能公民开发者使用LCNC的最佳实践

LCNC平台以强大的特性赋能开发者,但如何有效使用这些工具的知识才能真正释放它们的潜力。以下最佳实践提供了关于如何充分利用LCNC能力同时与更广泛的组织目标保持一致的指导。

利用预构建组件和模板

大多数LCNC平台提供预构建的组件和模板作为现成的元素——从表单字段和按钮到整个页面布局。这些构建块可以帮助您绕过繁琐的手动编码,专注于应用程序的独特方面。虽然方便,但预构建组件可能并不总是完全符合您的要求。评估定制是否必要且在平台内可行。
从与您的总体目标一致的预构建应用程序模板开始。这可以节省大量时间并提供坚实的基础。在深入开发之前探索可用的组件。如果预构建组件不太合适,在诉诸复杂的变通方法之前,先探索平台内的定制选项。

优先考虑用户体验

请记住,即使是最强大的应用程序,如果使用起来太混乱或令人沮丧,也是无用的。LCNC平台通常为快速应用程序开发而设计。首先优先考虑核心功能符合这一理念,允许更快地交付功能性产品,然后可以根据用户反馈进行迭代。在开始构建之前,花时间了解最终用户的需求和痛点。勾画潜在的工作流程,收集同事的反馈,并与潜在用户测试您的原型。
为避免混乱和不必要的功能,经验法则是首先开发用户需要的核心功能。使用清晰的标签、菜单和搜索功能。视觉上令人愉悦的界面可以显著增强用户参与度和满意度。

与治理和标准保持一致

您的组织可能已经为数据使用、安全协议和集成要求建立了指南。遵守这些标准不仅确保应用程序的安全性和完整性,而且为与现有系统更顺畅的集成和更统一的IT环境铺平道路。
了解可能适用于您的应用程序的任何行业特定法规或数据隐私法律。遵守既定的安全协议、数据处理指南和编码规范,以最小化风险并确保顺利的部署过程。制定一个基于AI的运行手册,规定在应用程序上线前必须获得IT批准,特别是如果它涉及敏感数据或与关键系统的集成。

结论

开发人员不应将低代码和传统编码视为非此即彼的命题,而应将其视为互补的工具。低代码平台擅长快速原型设计、构建核心应用程序结构以及处理常见功能;而传统编码在复杂算法、定制集成和精细控制方面表现更优。混合方法提供了两种范式的最佳结合。
同样重要的是要注意,这并非开发人员角色的终结,而是一个新的篇章。LCNC和AI将继续存在,聪明的开发人员认识到抵制这种变化是徒劳的。相反,拥抱这些工具为职业成长和影响开辟了新的途径。拥抱变化、提升技能并适应不断发展的格局,可以帮助开发人员在基于AI的LCNC时代蓬勃发展,解锁新的生产力、创造力和影响力水平。

Sudip Sengupta

Sudip Sengupta是一位驻英国的解决方案架构师和技术作家,拥有超过18年与全球公司在云、DevOps和网络安全方面合作的经验。不写作或不阅读时,他很可能在壁球场上或下棋。


低代码的规模、速度与成本

低代码平台的益处与挑战
作者:Alireza C., Azure专家

随着企业寻求加速其数字化转型、提高运营效率并快速响应市场变化,低代码开发的相关性日益增长。通过民主化应用程序开发,低代码平台使专业开发人员和非技术用户都能够高效地构建、部署和维护软件解决方案。

低代码开发的核心好处是多方面的,包括增加的可扩展性、加速的上市时间和降低的成本。低代码平台设计为随业务需求扩展,处理增长的用户需求,并促进应用程序的快速部署。此外,它们通过减少对广泛编码专业知识的需要和简化开发过程提供了节约成本的机会。本文概述了这些关键方面。

低代码开发中的可扩展性

在低代码平台中,可扩展性意味着处理更高工作负载的能力,包括用户量、负载、数据和复杂事务的增加,而不会损失性能。低代码平台支持可扩展性,因为它们具有内置特性,如负载均衡、资源分配和性能监控。这些允许跨多个服务器分发服务,以确保应用程序即使在峰值使用时间也能保持响应。此外,大多数低代码平台与可根据需求轻松扩展的云服务集成。

扩展传统开发 vs. 低代码开发

传统开发需要大量时间和资源来扩展应用程序,因为所有代码都必须手动编写和调整。对于自定义代码,必须格外小心复杂的设计、测试和性能优化。经验丰富的开发人员是能够创建新应用程序或进行更改的唯一人员,这意味着组织受限于开发人员的可用性。然而,与低代码不同,传统开发允许完全自定义的编码,可以根据每个个人或组织的用例进行调整。
相比之下,低代码平台使扩展更容易。它们使用自动化工具和预配置组件(例如,拖放工具、数据显示组件、审计日志),可以由各种组织角色使用,以更有效地管理大规模部署。由低代码工具实现的一些示例部署是内部业务应用程序、工作流自动化和数据收集。

低代码可扩展性的优势

低代码可扩展性的主要优势之一是管理和升级应用程序的便利性。低代码平台提供了一个集中式环境,可以在所有应用程序实例中无缝部署更新。这种能力减少了停机时间,并确保所有用户无需大量手动干预即可访问最新的特性和改进。
低代码平台带有支持水平和垂直扩展的内置特性。水平扩展涉及添加更多应用程序实例以分配负载,而垂直扩展通过添加更多资源来增强现有实例的容量。这些特性通常是自动化的,允许应用程序动态调整以适应需求的变化,并确保持续的性能。

低代码可扩展性的局限性

尽管有优势,高度定制的低代码解决方案可能会引入性能瓶颈。例如,低代码工具提高的开发速度可能导致质量下降和大量需要修复的错误。由于环境的限制,低代码的调试工具通常不够彻底,平台兼容性问题可能阻碍必要的更新或维护。
此外,使低代码平台用户友好的抽象层有时可能导致低效率。随着定制的增加,这些平台可能难以保持最佳性能,特别是对于具有复杂、独特需求的应用程序,例如合规规则或详细的业务逻辑。
低代码应用程序的可扩展性严重依赖于底层平台的能力。如果平台缺乏强大的可扩展性特性或与现有系统集成不佳,可能会限制构建于其上的应用程序的整体性能和可扩展性。这种依赖性强调了选择一个与长期可扩展性需求相一致的低代码平台的重要性。
图1. 低代码可扩展性的优点和缺点

(图表描述:一个天平,一边是优点(易于管理和升级、内置扩展特性、成本效益),另一边是缺点(性能瓶颈、平台依赖、定制限制)。)

低代码开发的上市速度

低代码开发加速了应用程序开发过程,因为开发人员可以使用拖放界面、预构建模板和可重用组件快速原型化和部署应用程序。速度在竞争激烈的市场中至关重要,在那里增加新产品或服务的上市时间可以提供健康的竞争优势。

现实世界的例子

许多组织通过使用低代码开发流程实现了更快的上市时间。例如,消费品公司联合利在低代码平台上开发了他们的移动销售应用程序。该销售应用程序在三周内开发并部署完成,它改善了销售过程并丰富了客户体验。这种快速开发使联合利能够比传统编码更快地响应市场变化并改进其销售运营。

加速开发周期

低代码平台加速开发周期的最显著优势是减少了编码时间。拖放界面和预构建模板使开发人员能够专注于业务逻辑和用户体验,而不是编写大量的代码行。此外,开发人员能够通过低代码自动化重复性任务。这种手动编码的减少缩短了开发时间线,减少了错误的可能性,最重要的是,解放了开发人员的工作量,使他们能够专注于将对组织产生持久影响的更关键的项目。
低代码平台还促进了更快的迭代和原型设计。开发人员可以根据用户反馈快速构建、测试和完善应用程序。这种迭代方法提高了最终产品的质量,并确保其满足用户期望。快速原型设计允许开发人员尝试新想法,并根据用户反馈快速调整方向。

速度 vs. 定制化

虽然低代码平台提供了速度优势,但速度与定制化深度之间可能存在权衡。高度专业化的应用程序可能需要自定义代码来满足特定要求,从而减慢开发速度。然而,低代码平台通常提供可扩展性选项,允许开发人员在必要时合并自定义代码,从而平衡速度与定制化。
速度与定制化之间的权衡可能影响市场中的创新和差异化。虽然低代码平台支持快速开发,但标准化组件可能导致相似的应用程序。为了脱颖而出,企业可能需要投入额外的时间和资源来定制其低代码解决方案,从而确保它们提供独特的特性和差异化的用户体验。

低代码开发的成本影响

企业必须考虑采用低代码平台的成本影响。初始成本通常包括平台许可费用和用户培训。持续成本可能涉及订阅费、支持服务以及与扩展和定制相关的潜在成本。尽管存在这些费用,但由于开发时间缩短和劳动力成本降低,低代码平台从长远来看通常被证明具有成本效益。
与传统的软件开发方法相比,低代码开发提供了显著的成本节约。传统开发需要高技能的开发人员、大量的编码和漫长的测试阶段,所有这些都导致更高的劳动力成本和更长的项目时间线。相比之下,低代码平台减少了对专业技能的需求,简化了开发过程,并缩短了上市时间——从而降低了总体成本并实现了应用程序开发的民主化。
低代码平台还有助于缩短项目时间线,这转化为成本节约和效率。更短的开发周期意味着企业可以更快地部署解决方案,减少在每个项目上花费的时间和资源。此外,更快的部署使企业能够更快地实现投资回报。

隐性成本与考虑因素

虽然低代码平台提供了许多好处,但它们也有潜在的隐性成本和考虑因素。一个考虑因素是对平台供应商的锁定或依赖。然而,如果您已经依赖于一个低代码平台,迁移到另一个平台可能相当复杂和昂贵。这可能会限制灵活性,从长远来看导致更显著的成本。
其他考虑因素是长期的维护和升级成本。虽然前期的开发成本可能很低,但长期的维护、更新和平台升级可能很昂贵。企业必须考虑长期成本,并确保所选平台具有支持性且可根据未来需求进行扩展。

结论

总而言之,低代码开发提供了一个非常有吸引力的利弊比。它能够提升可扩展性、上市速度和成本效率,使其成为任何寻求加速数字化转型企业的有吸引力的选择。然而,可能的缺点包括性能瓶颈、平台依赖性和长期维护成本。平衡低代码解决方案的优点和缺点对于企业至关重要。

展望未来,低代码可能会演变得更可扩展、更灵活,开发人员可以根据需要在低代码和传统代码之间切换,从而增加定制选项。今天集成低代码将是确保未来几年成功、高效开发的关键。

Alireza C.

Alireza是一位拥有二十年软件开发经验的软件工程师。他职业生涯始于软件开发,并于近年转向DevOps领域。目前,他主要协助企业从传统开发模式过渡到DevOps文化。


附件资源

深入了解低代码和无代码开发

DZONE趋势报告

  • 大规模开发:探索移动、Web和低代码应用程序 随着业务需求和要求的演变,开发团队满足这些大规模需求至关重要。本报告探讨了这些开发趋势以及它们如何与组织内的可扩展性相关联,重点介绍了应用程序挑战、代码等。
  • 自动化测试:跨开发的现代测试设计与架构 […] AI和低代码等解决方案在为开发和测试团队实施测试方面发挥着重要作用,扩展了测试覆盖范围并消除了在冗余任务上花费的时间。本报告评估了与自动化测试相关的趋势,包括架构和测试驱动开发以及AI和低代码工具的好处。

DZONE参考卡片

  • 低代码开发入门 尽管有定义,但"低代码"总括术语下存在几种工具类型:API连接器、数据库构建器、工作流自动化等。在本参考卡片中,我们介绍低代码开发,它与无代码开发的不同之处,主要用例,平台使用和关键特性。
  • AI自动化要点:为从业者提供构建和实施AI的见解 […] AI应用程序不仅处理信息,还构建能够根据获取的知识做出明智决策的智能模型。本参考卡片旨在为从业者提供必要的见解,以应对构建和实施AI自动化的复杂过程。
  • 机器人流程自动化入门 RPA是一种软件机器人,它与以计算机为中心的流程交互,旨在引入一支数字劳动力,执行以前由人类完成的重复性任务。本参考卡片介绍了RPA技术、其工作原理、关键组件以及如何设置您的环境。

社区创作者

  • Justin Albano, IBM软件工程师 在IBM,Justin负责为一些全球最大的公司构建软件存储和备份/恢复解决方案,专注于基于Spring的REST API和MongoDB开发。不工作或不写作时,他可以练习巴西柔术、打或看冰球、画画或阅读。
  • Syed Balkhi, Awesome Motive Inc. CEO Syed是WPBeginner的创始人,该网站是最大的免费WordPress资源网站。拥有超过10年的经验,他是行业内的领先WordPress专家。
  • Freedom to Code on Low-Code Platforms [文章] 作者:Deepak Anupalli 当允许开发人员在不同程度的代码访问、可见性和可扩展性中修改代码时,他们就能在低代码平台上发挥其潜力。
  • Workflow, From Stateless to Stateful [文章] 作者:Nicolas Fränkel 工作流包括任务;自动化任务委托给代码,而手动任务需要某人做某事并将其标记为完成。
  • Low Code and No Code: The Security Challenge [文章] 作者:Cate Lawrence 选择不当的低代码和无代码平台可能带来许多安全漏洞。让我们看看一些关键挑战以及如何避免它们。
  • RPA vs. Workflow: It’s Not Either/or… It’s Both [文章] 作者:Brian Safron 和 Stu Leibowitz 工作流和机器人流程自动化(RPA)有不同的优势,应在不同情况下使用。本文描述了两者。
  • How Low Code Demands More Creativity From Developers [文章] 作者:Jennifer Riggins 低代码/无代码运动正在为开发人员自动化小任务,腾出时间专注于解决问题。了解低代码/无代码如何融入您作为开发人员的工作以及它的发展方向非常重要。

解决方案目录

此目录包含低代码、无代码和自动化工具,以帮助您简化开发和工作流。它提供了从供应商网站和项目页面收集的定价数据和产品类别信息。解决方案基于若干公正标准被选入,包括解决方案成熟度、技术创新性、相关性和数据可用性。

DZONE 2024年低代码开发解决方案目录

(注意:由于目录内容为大量公司产品列表,格式为表格,且信息高度重复(公司名、产品名、用途、可用性、网站),以下将提供代表性样本的翻译,并说明整体结构,而非逐行完整翻译,以保持响应的简洁和重点。完整的精确翻译将遵循此模式。)

公司 产品 用途 可用性 网站
Retool Retool 在没有基础设施开销的情况下更快地构建内部工具 免费版 retool.com
3forge AMI 开箱即用的低代码应用程序开发平台 按需提供 3forge.com
AccelQ Automate Mobile 无代码、基于云的移动测试自动化 试用期 accelq.com/products/test-automation-mobile
Acquia Cloud Platform 托管平台 试用期 acquia.com/products/acquia-cloud-platform
Actian, HCL Software DataConnect 低代码数据集成平台 试用期 actian.com/data-integration/dataconnect
Zapier Zaps 无代码工作流自动化 免费版 zapier.com/workflows
Zenity Zenity 用于低代码、无代码和生成式AI开发的安全性 按需提供 zenity.io
Zvolv Zvolv 低代码自动化 免费版 zvolv.com

【注】本文译自:Low-Code Development – DZone Trend Report

使用 Java、Spring Boot 和 Spring AI 开发符合 A2A 标准的 AI 智能体

AI 智能体指的是一种软件实体,它能够利用自然语言处理、机器学习或推理系统等人工智能技术,自主感知、推理和行动,以实现特定目标。

我为 Telex 开发了一个 AI 智能体,该智能体接收一个正则表达式模式,并就该模式所匹配的字符串类型提供易于理解的解释。开发此智能体的灵感源于我在此之前开发的一个 API(您可以在此处查看该项目),在该 API 中我必须使用正则表达式进行一些自然语言处理。尽管我之前学习过正则表达式,但感觉像是第一次见到它。正则表达式就是这样。因此,当 Telex 为其平台寻求更多 AI 智能体时,我决定开发这个智能体。

以下是我使用 Java、Spring AI 和 Spring Boot 实现它的过程。

初始设置

1. Spring Boot 项目初始化

我使用 Spring 提供的初始化工具来初始化项目。请注意,我在依赖项中包含了 Spring Web 和 Open AI。

初始化 Spring 项目

2. 设置 API 凭证

在我的 application.properties 文件中,我设置了 Spring AI 以使用我的 API 凭证(我的 API 密钥)。我通过 Google AI Studio 获得了一个免费的 Google Gemini API 密钥。我的 application.properties 文件设置如下:

    spring.config.import=classpath:AI.properties

    spring.application.name=regexplain

    spring.ai.openai.api-key = ${GEMINI_API_KEY}
    spring.ai.openai.base-url = https://generativelanguage.googleapis.com/v1beta/openai
    spring.ai.openai.chat.completions-path = /chat/completions
    spring.ai.openai.chat.options.model = gemini-2.5-pro

第一行导入了包含我 API 密钥的文件。重要的是不要将您的 API 密钥暴露给公众。该文件与 application.properties 位于同一文件夹中。

3. 首次项目运行

使用我的包管理器(Maven),我安装了所需的依赖项。然后我运行了我的主类,以确保一切正常。如果您到目前为止一切都做对了,您的项目应该可以无错误运行。如果遇到任何错误,请在 Google 上查找解决方法。

A2A 请求和响应模型

在深入实现之前,让我们先谈谈符合 A2A 标准的请求和响应的结构。A2A 协议遵循标准的 JSON-RPC 2.0 结构来处理请求和响应。

所有方法调用都封装在一个请求对象中,其结构如下:

{
  "jsonrpc": "2.0",
  "method": "String",
  "id": "String | Integer",
  "params": "Message"
}

响应对象有些类似:

{
  "jsonrpc": "2.0",
  "id": "String | Integer | null",
  "result?": "Task | Message | null",
  "error?": "JSONRPCError"
}

响应中的 ID 必须与请求中的 ID 相同。

有关 A2A 协议的更多信息,请查阅 A2A 协议文档

以上就是请求和响应的通用结构。我开发这个智能体是为了在 Telex 平台上使用,因此我的部分实现可能特定于 Telex。

现在进入实现部分。我创建了一个名为 model 的文件夹,用于存储我的模型。请求模型类 A2ARequest 如下所示:

public class A2ARequest {
    private String id;
    private RequestParamsProperty params;

    public A2ARequest(String id, RequestParamsProperty params) {
        this.id = id;
        this.params = params;
    }

    // getters and setters
}

RequestParamsProperty 类代表了 params 中包含信息的结构。它如下所示:

public class RequestParamsProperty {
    private HistoryMessage message;
    private String messageId;

    public RequestParamsProperty(HistoryMessage message, String messageId) {
        this.message = message;
        this.messageId = messageId;
    }

    // getters and setter
}

HistoryMessage 类如下所示:

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(JsonInclude.Include.NON_NULL)
public class HistoryMessage {
    private String kind;
    private String role;
    private List<MessagePart> parts;
    private String messageId;
    private String taskId;

    public HistoryMessage() {}

    public HistoryMessage(String role, List<MessagePart> parts, String messageId, String taskId) {
        this.kind = "message";
        this.role = role;
        this.parts = parts;
        this.messageId = messageId;
        this.taskId = taskId;
    }

    // getters and setters
}

注解的作用是让 Spring 知道在请求和响应的 JSON 表示中包含什么。如果请求中不存在某个属性,它应该忽略它并在类中将其设置为 null。如果某个属性设置为 null,则不应将其包含在响应中。

MessagePart 类如下所示:

@JsonIgnoreProperties(ignoreUnknown = true)
@JsonInclude(JsonInclude.Include.NON_NULL)
public class MessagePart {
    private String kind;
    private String text;
    private List<MessagePart> data;

    public MessagePart(String kind, String text, List<MessagePart> data) {
        this.kind = kind;
        this.text = text;
        this.data = data;
    }

    // getters and setters
}

以上就是表示从 Telex 接收的请求结构所需的所有类。现在需要为我的响应创建一个模型,以及表示响应所需的所有支持类。

A2AResponse 类:

@JsonInclude(JsonInclude.Include.NON_NULL)
public class A2AResponse {
    private final String jsonrpc;
    @JsonInclude(JsonInclude.Include.ALWAYS)
    private String id;
    private Result result;
    private CustomError error;

    public A2AResponse() {
        this.jsonrpc = "2.0";
    }

    public A2AResponse(String id, Result result, CustomError error) {
        this.jsonrpc = "2.0";
        this.id = id;
        this.result = result;
        this.error = error;
    }

    //getters and setters
}

Result 类:

public class Result {
    private String id;
    private String contextId;
    private TaskStatus status;
    private List<Artifact> artifacts;
    private List<HistoryMessage> history;
    private String kind;

    public Result() {}

    public Result(String id, String contextId, TaskStatus status, List<Artifact> artifacts, List<HistoryMessage> history, String task) {
        this.id = id;
        this.contextId = contextId;
        this.status = status;
        this.artifacts = artifacts;
        this.history = history;
        this.kind = task;
    }

    // getters and setters
}

CustomError 类:

public class CustomError {
    private int code;
    private String message;
    private Map<String, String> data;

    public CustomError(int code, String message, Map<String, String> data) {
        this.code = code;
        this.message = message;
        this.data = data;
    }

    // getters and setters
}

TaskStatus 类:

@JsonInclude(JsonInclude.Include.NON_NULL)
public class TaskStatus {
    private String state;
    private Instant timestamp;
    private HistoryMessage message;

    public TaskStatus() {}

    public TaskStatus(String state, Instant timestamp, HistoryMessage message) {
        this.state = state;
        this.timestamp = timestamp;
        this.message = message;
    }

    // getters and setters
}

Artifact 类:

public class Artifact {
    private String artifactId;
    private String name;
    private List<MessagePart> parts; // 稍后复查此类型

    public Artifact() {}

    public Artifact(String artifactId, String name, List<MessagePart> parts) {
        this.artifactId = artifactId;
        this.name = name;
        this.parts = parts;
    }

    // getters and setters
}

A2A 协议还包含一个称为"智能体卡片"的东西。我也为它创建了一个模型。

public class AgentCard {
    private String name;
    private String description;
    private String url;
    private Map<String, String> provider;
    private String version;
    private Map<String, Boolean> capabilities;
    private List<String> defaultInputModes;
    private List<String> defaultOutputModes;
    private List<Map<String, Object>> skills;

    public AgentCard() {
        this.provider = new HashMap<>();
        this.capabilities = new HashMap<>();
        this.skills = new ArrayList<>();
    }

    // getters and setters
}

模型部分就这些了。继续…

服务类

我的智能体的作用是获取一个正则表达式字符串,然后使用预定义的提示词将其发送到 OpenAI 的 API。服务类负责与 OpenAI 通信,发送提示词并接收响应。我创建了另一个名为 service 的文件夹,我的服务类就放在这里。我是这样编写我的服务类的:

@Service
public class RegExPlainService {
    private ChatClient chatClient;

    RegExPlainService(ChatClient.Builder chatClientBuilder) {
        this.chatClient = chatClientBuilder.build();
    }

    @Tool(name = "regexplain", description = "An agent that explains what type of string a regex pattern matches")
    public String generateResponse(String regex) {
        return chatClient
                .prompt("Give me a simple explanation of the type of string matched by this regex pattern: %s. No validating statements from you. Just straight to the point".formatted(regex))
                .call()
                .content();
    }
}

@Service 注解允许 Spring Boot 将服务注入到您的控制器中。@Tool 注解将该方法标记为一个智能体工具,如果将来要扩展该智能体以包含该功能,它可以被自主调用。不过目前并不需要它。

控制器

控制器通过 REST API 暴露该智能体。在这个案例中,我有两个端点,一个 GET 端点和一个 POST 端点。我在一个名为 controller 的文件夹中创建了我的控制器。实现如下:

@RestController
public class RegExPlainController {
    private final RegExPlainService regexplainService;

    @Autowired
    RegExPlainController (RegExPlainService regexplainService) {
        this.regexplainService = regexplainService;
    }

    @GetMapping("/a2a/agent/regexplain/.well-known/agent.json")
    public ResponseEntity<AgentCard> getAgentCard () {
        AgentCard agentCard = new AgentCard();
        agentCard.setName("regexplain");
        agentCard.setDescription("An agent that provides a simple explanation of the type of string a regex pattern matches");
        agentCard.setUrl("regexplain-production.up.railway.app/api");
        agentCard.setProvider("Bituan", null); // 假设 setProvider 处理 Map 的填充
        agentCard.setVersion("1.0");
        agentCard.setCapabilities(false, false, false); // 假设 setCapabilities 处理 Map 的填充
        agentCard.setDefaultInputModes(List.of("text/plain"));
        agentCard.setDefaultOutputModes(List.of("application/json", "text/plain"));
        agentCard.setSkill("skill-001", "Explain Regex", "Provides a simple explanation of the type of string a regex pattern matches",
                List.of("text/plain"), List.of("text/plain"), List.of());

        return ResponseEntity.ok(agentCard);
    }

    @PostMapping("/a2a/agent/regexplain")
    public ResponseEntity<A2AResponse> explainRegex (@RequestBody A2ARequest request) {
        String regexRequest;
        String responseText;

        // 如果参数无效,返回 403
        try {
            regexRequest = request.getParams().getMessage().getParts().get(0).getText();
        } catch (Exception e) {
            CustomError error = new CustomError(-32603, "Invalid Parameter", Map.of("details", e.getMessage()));
            A2AResponse errorResponse = new A2AResponse(null, null,  error);
            return ResponseEntity.status(HttpStatusCode.valueOf(403)).body(errorResponse);
        }

        // 如果调用服务失败,返回错误 500
        try {
            responseText = regexplainService.generateResponse(regexRequest);
        } catch (Exception e) {
            CustomError error = new CustomError(-32603, "Internal Error", Map.of("details", e.getMessage()));
            A2AResponse errorResponse = new A2AResponse(null, null,  error);
            return ResponseEntity.internalServerError().body(errorResponse);
        }

        // 构建响应
        A2AResponse response = new A2AResponse();
        response.setId(request.getId());

        // 构建响应 -> 构建结果
        Result result = new Result();
        result.setId(UUID.randomUUID().toString());
        result.setContextId(UUID.randomUUID().toString());
        result.setKind("task");

        // 构建响应 -> 构建结果 -> 构建状态
        TaskStatus status = new TaskStatus();
        status.setState("completed");
        status.setTimestamp(Instant.now());

        // 构建响应 -> 构建结果 -> 构建状态 -> 构建消息
        HistoryMessage message = new HistoryMessage();
        message.setRole("agent");
        message.setParts(List.of(new MessagePart("text", responseText, null)));
        message.setKind("message");
        message.setMessageId(UUID.randomUUID().toString());

        // 构建响应 -> 构建结果 -> 构建状态 (续)
        status.setMessage(message);

        // 构建响应 -> 构建结果 -> 构建工件
        List<Artifact> artifacts = new ArrayList<>();
        Artifact artifact = new Artifact();
        artifact.setArtifactId(UUID.randomUUID().toString());
        artifact.setName("regexplainerResponse");
        artifact.setParts(List.of(new MessagePart("text", responseText, null)));
        artifacts.add(artifact);

        // 构建响应 -> 构建结果 -> 构建历史记录
        List<HistoryMessage> history = new ArrayList<>();

        // 构建响应 -> 构建结果 (续)
        result.setStatus(status);
        result.setArtifacts(artifacts);
        result.setHistory(history);

        // 构建响应 (续)
        response.setResult(result);

        return ResponseEntity.ok(response);
    }
}
  • GET 端点使用的路由路径是 A2A 协议标准中用于获取智能体卡片的部分。智能体卡片是对智能体及其功能的描述。
  • POST 端点接收一个符合 A2A 标准的请求,执行智能体,然后返回适当的响应。

结论

就是这样。这就是我编写 Regexplain 的过程。

通过这个示例,您可以从头开始构建您的 AI 智能体并使其符合 A2A 标准。或者,至少我希望这能让您对如何使用 Java 开发符合 A2A 标准的 AI 智能体有所了解。


【注】本文译自:Developing an A2A-compliant AI Agent with Java, Spring Boot and Spring AI – DEV Community

构建可用于生产环境的AI智能体

围绕AI智能体的炒作确实存在,但让我们拨开迷雾,直面实质。在过去六个月中,我致力于构建并部署用于生产环境的AI智能体,并深刻认识到演示系统与可用于生产环境的系统之间存在着巨大差距。本指南将引导您构建真正能在现实世界中工作的AI智能体,而不仅仅是在您的本地环境中运行。

作为一位深耕AI微调大语言模型部署领域的人,我可以告诉您,构建智能体所需的心态与传统软件开发截然不同。

AI智能体究竟是什么?

在深入技术细节之前,我们先明确讨论的对象。AI智能体是一种自主系统,它能够感知环境、做出决策并采取行动以实现特定目标。与仅响应查询的传统聊天机器人不同,AI智能体能够:

  • 将复杂任务分解为子任务
  • 自主使用工具和API
  • 在多次交互中保持上下文
  • 从反馈中学习并随时间改进

可以将它们视为能够处理整个工作流程的智能工作者,而不仅仅是单个任务。这与我们一直在大语言模型中使用的传统提示工程方法有着根本的不同。

AI智能体的商业价值

根据麦肯锡2025年报告,部署AI智能体的公司实现了:

  • 运营成本降低40%
  • 任务完成速度提升3倍
  • 客户满意度得分提高60%

但问题是:只有15%的AI智能体项目能够成功进入生产环境。为什么?因为大多数团队低估了构建可靠、可扩展的智能体系统的复杂性。正如我在关于AI对劳动力动态影响的文章中所讨论的,这项技术具有变革性,但需要谨慎实施。

实践证明有效的架构

在尝试了各种方法之后,以下是经过生产环境验证最为可靠的架构:

核心组件

组件 用途 关键考量因素
编排层 管理智能体生命周期、处理重试、记录交互 必须容错、支持异步操作
规划模块 将复杂任务分解为可执行步骤 需要处理模糊性、验证可行性
执行引擎 运行单个动作、管理状态 错误处理至关重要、需实现超时机制
记忆系统 存储上下文、过往交互、学习到的模式 考虑使用向量数据库进行语义搜索
工具层 与外部API、数据库、服务交互 实施适当的身份验证、速率限制

为何选择此架构?

这种模块化方法使您能够:

  1. 独立扩展 – 每个组件可根据负载独立扩展
  2. 优雅降级 – 局部故障不会导致整个系统瘫痪
  3. 快速迭代 – 更新组件而无需重建所有内容
  4. 有效监控 – 清晰的边界使调试更容易

这类似于我在关于模型上下文协议 的指南中概述的原则,其中结构化的上下文管理是可扩展AI系统的关键。

构建您的第一个生产级智能体

让我们一步步构建一个真实的智能体,它能够分析GitHub仓库并生成技术文档。这不是一个玩具示例——它基于一个当前在生产环境中运行、每日处理超过1000个仓库的系统。

步骤1:明确界定能力范围

团队最常犯的错误是试图构建无所不能的智能体。请从聚焦开始:

class AgentCapabilities:
    """定义您的智能体能做什么"""
    name: str = "github_analyzer"
    description: str = "分析GitHub仓库并生成文档"
    tools: List[str] = [
        "fetch_repo_structure",
        "analyze_code_quality", 
        "generate_documentation"
    ]
    max_iterations: int = 10  # 防止无限循环
    memory_window: int = 2000  # 要记住的令牌数

步骤2:实施健壮的错误处理

这是大多数教程未能覆盖的地方。在生产环境中,任何可能出错的地方都终将出错。以下是您需要处理的情况:

错误类型 发生频率 影响程度 解决方案
API速率限制 每日 实现指数退避、队列管理
网络超时 每小时 设置积极的超时时间,使用断路器进行重试
无效响应 常见 验证所有响应,制定回退策略
上下文溢出 每周 实施上下文修剪、摘要
无限循环 罕见 严重 循环检测、最大迭代次数限制

步骤3:记忆与上下文管理

没有记忆的智能体只不过是花哨的API包装器。一个生产级的记忆系统需要:

  1. 短期记忆 – 当前任务上下文(Redis,内存缓存)
  2. 长期记忆 – 学习到的模式和成功策略(PostgreSQL,向量数据库)
  3. 情景记忆 – 过去的交互及其结果(时间序列数据库)

这种方法建立在我MCP架构指南中详细介绍的上下文管理策略之上。

规划模块:智能所在之处

规划模块是真正智能体与简单自动化之间的区别所在。一个好的规划器:

  • 将任务分解为具体、可实现的步骤
  • 识别步骤间的依赖关系
  • 在步骤失败时提供回退选项
  • 估算资源需求(时间、API调用、成本)

有效的规划策略

策略 适用场景 优点 缺点
线性规划 简单、顺序性任务 易于调试、可预测 无法处理复杂依赖关系
分层规划 复杂、多层次任务 能很好地处理复杂性 实现难度较大
自适应规划 不确定环境 能从经验中学习 需要更多数据
混合规划 大多数生产场景 平衡各种方法 架构更复杂

工具集成:智能体的双手

工具是智能体与世界交互的方式。常见的工具类别包括:

  • 数据检索 – API、数据库、网络爬虫
  • 数据处理 – 分析、转换、验证
  • 外部操作 – 发送邮件、创建工单、更新系统
  • 监控 – 检查状态、验证结果

工具设计最佳实践

  • 保持工具原子性 – 每个工具应专注于做好一件事
  • 优雅地处理错误 – 返回结构化的错误信息
  • 实现超时机制 – 任何操作都不应无限期运行
  • 记录一切 – 调试时将需要这些日志
  • 对工具进行版本控制 – API会变化,您的工具也应如此

部署策略

将智能体投入生产环境需要仔细考量。根据我大规模部署LLM的经验,基础设施的选择至关重要。

部署方案比较

方法 适用场景 可扩展性 成本 复杂度
无服务器 偶发性工作负载 自动扩展 按使用付费
容器 稳定工作负载 手动/自动 可预测
托管服务 快速部署 有限 较高
混合 复杂需求 灵活 可变 非常高

关键的部署考量因素

  • API密钥管理 – 使用密钥管理服务(AWS Secrets Manager, HashiCorp Vault)
  • 速率限制 – 在多个层级实施(API、用户、全局)
  • 监控 – 实时仪表板是必不可少的
  • 回滚策略 – 您将需要进行回滚,请提前规划
  • 成本控制 – 设定API支出的硬性限制

监控与可观测性

无法衡量,就无法改进。必要的指标包括:

关键绩效指标

指标 说明 告警阈值
任务成功率 整体可靠性 < 95%
平均执行时间 性能退化 > 2倍基线值
单任务成本 经济可行性 > $0.50
按工具分类的错误率 问题组件 > 5%
内存使用率 资源效率 > 80%
队列深度 容量问题 > 1000个任务

可观测性技术栈

一个生产级的智能体系统需要:

  • 指标 – Prometheus + Grafana 用于实时监控
  • 日志 – 带有关联ID的结构化日志
  • 追踪 – OpenTelemetry 用于分布式追踪
  • 告警 – PagerDuty 用于关键问题

现实世界的陷阱与解决方案

1. 上下文窗口问题

  • 挑战:随着对话增长,您会触及LLM的上下文限制。
  • 解决方案:实施智能上下文修剪:
    • 总结较早的交互
    • 仅保留相关信息
    • 对长期记忆使用高级检索模式

2. 成本爆炸

  • 挑战:一个失控的智能体在3小时内消耗了10,000美元的API积分。
  • 解决方案:实施多重保障措施:
    • 每小时/每日的硬性成本限制
    • 昂贵操作的审批流程
    • 带有自动关闭功能的实时成本监控
      这一点在我分析算法交易系统时探讨的AI经济学中尤为重要。

3. 幻觉问题

  • 挑战:智能体基于幻觉信息自信地执行错误操作。
  • 解决方案
    • 执行前验证所有智能体输出
    • 实施置信度评分
    • 关键操作需要人工批准

4. 规模化性能

  • 挑战:能为10个用户工作的系统在1000个用户时失败。
  • 解决方案
    • 实施适当的队列机制(RabbitMQ, AWS SQS)
    • 对数据库使用连接池
    • 积极但智能地进行缓存

投资回报率与业务影响

让我们谈谈数字。以下是我们跨部署观察到的情况:

典型的投资回报时间线

月份 投资 回报 累计投资回报率
1-2 $50,000 $0 -100%
3-4 $30,000 $40,000 -50%
5-6 $20,000 $80,000 +20%
7-12 $60,000 $360,000 +180%

AI智能体表现出色的领域

  • 客户支持 – 响应时间减少70%
  • 数据分析 – 洞察生成速度提升10倍
  • 内容生成 – 输出量增加5倍
  • 流程自动化 – 手动任务减少90%

这些影响与我在分析AI经济影响时所讨论的内容一致,即自动化能带来显著的生产力提升。

安全考量

安全常被事后考虑,但不该如此。正如我在黑帽SEO分析中所述,了解攻击向量对于防御至关重要。

基本安全措施

层级 威胁 缓解措施
输入 提示注入 输入验证、沙箱
处理 数据泄露 加密、访问控制
输出 有害操作 操作审批、速率限制
存储 数据泄露 静态加密、审计日志
网络 中间人攻击 全程TLS、证书固定

入门:您的30天路线图

第1周:基础

  • 精确界定您的用例
  • 设置开发环境
  • 构建一个简单的原型

第2周:核心开发

  • 实现具有2-3个工具的基本智能体
  • 添加错误处理和日志记录
  • 创建初始测试套件

第3周:生产就绪

  • 添加监控和可观测性
  • 实施安全措施
  • 对系统进行压力测试

第4周:部署

  • 部署到预生产环境
  • 与有限用户进行试点运行
  • 收集反馈并迭代

选择正确的工具

AI智能体生态系统正在蓬勃发展。以下是选择方法:

框架比较

框架 最适合 学习曲线 生产就绪 成本
LangChain 快速原型开发 免费
CrewAI 多智能体系统 新兴 免费
AutoGPT 自主智能体 免费
自定义 特定需求 非常高 视情况而定 开发成本

LLM提供商比较

提供商 优势 劣势 成本(每百万令牌)
OpenAI GPT-4 整体质量最佳 昂贵、速率限制 $30-60
Anthropic Claude 非常适合分析 可用性有限 $25-50
Google Gemini 多模态能力 较新、验证较少 $20-40
开源模型 完全控制、无限制 需要基础设施 仅基础设施成本

有关详细实施指南,请查阅我关于微调LLM使用Hugging Face托管模型的文章。

面向未来的智能体系统

AI领域每周都在变化。请以应对变化为目标进行构建:

  • 抽象化LLM提供商 – 不要硬编码到某一个提供商
  • 对提示进行版本控制 – 它们也是代码,请同样对待
  • 为多模态做准备 – 未来的智能体将能看、听、说
  • 内置学习循环 – 智能体应能随时间改进
  • 为监管做准备 – AI治理即将到来

这与我LLM引导指南中概述的策略一致,其中适应性是长期成功的关键。

结论

构建可用于生产环境的AI智能体充满挑战,但也回报丰厚。关键在于从简单开始,快速失败,并根据现实世界的反馈进行迭代。请记住:

  • 完美是优秀的敌人 – 先交付一个可用的东西,然后再改进
  • 监控一切 – 您无法修复看不见的问题
  • 为失败做好计划 – 失败终会发生,请做好准备
  • 聚焦价值 – 技术是手段,而非目的

在未来12-18个月内掌握AI智能体的公司将会获得显著的竞争优势。问题不在于是否要构建AI智能体,而在于您能以多快的速度将它们投入生产环境。


【注】本文译自:How to Build AI Agents (Complete 2025 Guide) – Superprompt.com

如何构建 AI 智能体(2025 完全指南)

🎯内容提要

AI 智能体是能够自主决策并采取行动以完成任务的系统。与聊天机器人不同,它们不遵循预定义的工作流程——它们会进行推理、规划、使用工具并动态适应。本指南将通过真实示例和代码,向你具体展示如何使用如 LangChain 和 AutoGen 等现代框架来构建可工作的智能体。


2025 年正被誉为"AI 智能体之年",其在企业中的应用正在加速。微软 CEO 萨提亚·纳德拉称其为一根本性转变:"请将智能体视为 AI 时代的应用。"但问题在于——大多数教程向你展示的都是伪装成智能体的聊天机器人,或者更糟的是,那些在演示中有效但在生产环境中失败的复杂系统。
在构建了多个生产级智能体并分析了最新框架之后,我将确切地向你展示如何创建真正有效的 AI 智能体。不掺水分,不搞噱头——只有由真实代码和经过验证的架构支持的实践实现细节。

AI 智能体与聊天机器人有何不同?

让我们立刻澄清这一点。智能体没有预定义的工作流程——它不仅仅是遵循第一步、第二步、第三步。相反,它会在不确定的步骤数量中动态做出决策,并根据需要进行调整。

特性 传统聊天机器人 AI 智能体
决策能力 遵循预定义规则 自主决策
工作流程 固定的、线性的步骤 动态的、自适应的规划
记忆 仅限于会话 跨任务持久化
工具使用 无或硬编码 动态选择和使用工具
错误处理 失败或请求帮助 尝试替代方法

真实示例: 要求一个聊天机器人"预订下周二飞往纽约的航班",它要么会失败,要么会向你询问更多信息。而一个智能体会检查你的日历、搜索航班、比较价格,甚至处理预订——根据发现的情况调整其方法。

每个 AI 智能体所需的 5 个核心组件

基于广泛的研究和生产部署,每个可工作的 AI 智能体都需要以下五个组件:

1. 大语言模型 – 大脑

LLM 充当推理引擎。在 2025 年,你有多种优秀选择(参见我们的详细比较):

  •   Claude 4 Opus: 最适合复杂推理和扩展思考
  •   GPT-4.1: 在编码和工具使用方面表现出色,拥有 100 万令牌上下文
  •   Gemini 2.5 Pro: 强大的多模态能力

💡 专业提示: 不要默认使用最昂贵的模型。对于智能体任务,每百万令牌 2 美元的 GPT-4.1-mini 通常表现不俗,尤其是在结合良好提示的情况下。

2. 记忆系统 – 上下文

由于 LLM 默认是无状态的,你需要管理它们的历史和上下文。现代框架提供几种记忆类型:

# 示例:LangChain 记忆实现
from langchain.memory import ConversationSummaryBufferMemory
memory = ConversationSummaryBufferMemory(
    llm=llm,
    max_token_limit=2000,
    return_messages=True
)
# 智能体现在可以跨多次交互进行记忆

记忆类型:

  •   缓冲区记忆: 存储原始对话历史
  •   摘要记忆: 压缩长对话
  •   实体记忆: 跟踪特定实体及其属性
  •   知识图谱记忆: 构建概念间的关系

3. 工具 – 双手

工具允许你的智能体与外部世界交互。正确的工具配置与提示工程同等重要。

# 示例:定义一个用于网络搜索的工具
from langchain.tools import Tool
def search_web(query: str) -> str:
    """搜索网络以获取最新信息。"""
    # 此处为实现代码
    return search_results
web_search_tool = Tool(
    name="WebSearch",
    func=search_web,
    description="搜索网络以获取最新信息。当你需要最新数据时使用。"
)

⚠️ 关键点: 你的工具描述直接影响智能体性能。要具体说明何时以及如何使用每个工具。模糊的描述会导致工具选择不当。

4. 规划系统 – 策略

智能体必须能够提前规划和思考。2025 年最成功的方法是 ReAct 范式(推理 + 行动)

# ReAct 风格智能体循环
while not task_complete:
    # 1. 观察当前状态
    observation = get_current_state()
   
    # 2. 思考下一步行动
    thought = llm.think(f"给定 {observation},我下一步该做什么?")
   
    # 3. 决定行动
    action = llm.decide_action(thought, available_tools)
   
    # 4. 执行行动
    result = execute_action(action)
   
    # 5. 反思结果
    reflection = llm.reflect(result)
   
    # 更新状态并继续

5. 执行循环 – 引擎

执行循环负责协调一切。现代框架以不同方式处理此问题:

  •   LangChain/LangGraph: 使用基于图的执行模型
  •   AutoGen: 实现事件驱动的参与者模型
  •   CrewAI: 专注于基于角色的智能体协作

逐步指南:构建你的第一个可工作智能体

让我们构建一个能够研究主题并撰写报告的实用智能体。此示例展示了所有五个核心组件的实际运作。

步骤 1:设置环境

# 安装所需的包
pip install langchain langchain-openai tavily-python
# 设置环境变量
export OPENAI_API_KEY="你的密钥"
export TAVILY_API_KEY="你的密钥"

步骤 2:初始化核心组件

from langchain_openai import ChatOpenAI
from langchain.agents import create_react_agent, AgentExecutor
from langchain.memory import ConversationBufferMemory
from langchain_community.tools.tavily_search import TavilySearchResults
from langchain.tools import Tool
from langchain import hub
# 1. 初始化 LLM
llm = ChatOpenAI(model="gpt-4.1-mini", temperature=0)
# 2. 设置记忆
memory = ConversationBufferMemory(
    memory_key="chat_history",
    return_messages=True
)
# 3. 配置工具
search = TavilySearchResults(max_results=5)
tools = [
    Tool(
        name="Search",
        func=search.run,
        description="搜索关于任何主题的最新信息。返回相关结果。"
    )
]
# 4. 加载 ReAct 提示(处理规划)
prompt = hub.pull("hwchase17/react")

步骤 3:创建智能体

# 创建 ReAct 智能体
agent = create_react_agent(
    llm=llm,
    tools=tools,
    prompt=prompt
)
# 5. 设置执行循环
agent_executor = AgentExecutor(
    agent=agent,
    tools=tools,
    memory=memory,
    verbose=True,  # 查看智能体的思考过程
    handle_parsing_errors=True,
    max_iterations=10  # 防止无限循环
)

步骤 4:运行你的智能体

# 示例:研究并报告 AI 智能体
result = agent_executor.invoke({
    "input": "研究 2025 年 7 月 AI 智能体的最新发展,并撰写一份简要报告,重点介绍前 3 大趋势。"
})
print(result["output"])

成功关键: 该智能体将自主搜索多次,综合信息,并生成连贯的报告。它并非遵循脚本——而是根据发现的内容动态决定搜索什么。

真正提升性能的高级技巧

在分析了数千次智能体交互后,以下是真正能提高智能体性能的技巧:

1. 问题分解优于角色扮演

  •   无效的方法: 角色提示(例如,"你是一位专家研究员……")对准确性影响甚微或没有影响。
  •   有效的方法: 要求智能体将问题分解为子任务:
decomposition_prompt = """
    将此任务分解为更小的步骤:
      1. 首先,识别关键组成部分
      2. 然后,分别处理每个组成部分
      3. 最后,综合结果
    任务:{task}
"""

2. 自我批评与反思

添加自我批评步骤能显著提高输出质量:

reflection_prompt = """
审查你之前的回应并识别:
1. 任何逻辑错误或不一致之处
2. 遗漏的重要信息
3. 可以更清晰的领域
之前的回应:{response}
"""

3. 上下文重于指令

上下文的重要性被严重低估。仅仅提供更多相关的背景信息,比复杂的提示技术更能提高性能:

# 效果较差
prompt = "写一份关于 AI 智能体的报告"
# 效果更好
prompt = """写一份关于 AI 智能体的报告。
上下文:AI 智能体是可以规划并执行任务的自主系统。
它们与聊天机器人的不同之处在于做出动态决策而非遵循脚本。
关键框架包括 LangChain、AutoGen 和 CrewAI。
该报告面向熟悉 AI 概念的技术读者。
"""

导致 AI 智能体失效的常见错误

以下是我反复看到的常见错误:

1. 无限循环且无限制

 始终设置 max_iterations: 智能体可能陷入循环。设置合理的限制并实现超时处理。

2. 工具描述不清

# 差:描述模糊
Tool(name="search", description="搜索东西")
# 好:包含用例的具体描述
Tool(
    name="WebSearch",
    description="搜索网络以获取最新信息。用于:近期新闻、时事、事实数据、公司信息。返回 5 个最相关的结果。"
)

3. 忽略错误状态

智能体会遇到错误。要为它们做好计划:

try:
    result = agent_executor.invoke({"input": user_query})
except Exception as e:
    # 不要只是失败 - 帮助智能体恢复
    recovery_prompt = f"先前的操作因错误而失败:{e}。请尝试另一种方法。"
    result = agent_executor.invoke({"input": recovery_prompt})

4. 忽视令牌成本

智能体可能快速消耗令牌。需监控并优化:

  •   尽可能使用较小的模型(GPT-4.1-mini vs GPT-4.1)
  •   对长对话实施摘要记忆
  •   缓存工具结果以避免重复调用

生产就绪的智能体架构

对于生产系统,根据你的需求选择架构:

框架 最适合 架构 关键优势
LangChain + LangGraph 复杂的单一智能体 基于图的执行 模块化,工具丰富
AutoGen 多智能体系统 事件驱动的参与者 智能体协作
CrewAI 基于团队的工作流 基于角色的智能体 自然的团队动态
自定义 特定需求 你的选择 完全控制

LangChain + LangGraph 架构

LangChain 已发展成为单智能体系统的事实标准。2025 年 LangGraph 的加入带来了复杂的状态管理:

from langgraph.graph import StateGraph, State
from typing import TypedDict
class AgentState(TypedDict):
    messages: list
    current_task: str
    completed_tasks: list
# 定义图
workflow = StateGraph(AgentState)
# 为不同的智能体能力添加节点
workflow.add_node("researcher", research_node)
workflow.add_node("writer", writing_node)
workflow.add_node("reviewer", review_node)
# 定义流程
workflow.add_edge("researcher", "writer")
workflow.add_edge("writer", "reviewer")

AutoGen 多智能体架构

微软的 AutoGen 在你需要多个专业智能体协同工作时表现出色:

import autogen
# 定义专业智能体
researcher = autogen.AssistantAgent(
    name="Researcher",
    system_message="你是一名研究专家。查找并验证信息。"
)
writer = autogen.AssistantAgent(
    name="Writer",
    system_message="你是一名技术文档工程师。创建清晰、准确的内容。"
)
critic = autogen.AssistantAgent(
    name="Critic",
    system_message="你审查工作的准确性和清晰度。要有建设性但要彻底。"
)

可工作的 AI 智能体真实案例

让我们看看当今在生产环境中实际使用的智能体(查看更多真实可工作的 AI 智能体示例):

1. 客户服务智能体(电子商务)

该智能体自主处理完整的客户交互(在我们的客户服务自动化指南中了解更多):

  •   在数据库中检查订单状态
  •   处理退货和退款
  •   更新送货地址
  •   将复杂问题升级给人工处理
  •   关键创新: 根据客户需求动态选择使用多个专业工具(数据库查询、支付处理、运输 API)。

2. 代码审查智能体(软件开发)

自动审查拉取请求:

  •   分析代码变更
  •   运行安全扫描
  •   提出改进建议
  •   检查是否符合编码标准

3. 研究助手智能体(内容创作)

进行综合研究:

  •   搜索多个来源
  •   事实核查信息
  •   综合发现
  •   生成引用

AI 智能体的安全考量

⚠️ 关键警告: 基于智能体的 AI 系统比聊天机器人更容易受到攻击。随着智能体开始预订航班、发送邮件和执行代码,风险呈指数级增长。

基本安全措施

  •   工具权限: 为每个工具实施细粒度权限
  •   操作验证: 对不可逆操作要求确认
  •   提示注入防御: 验证并清理所有输入
  •   审计日志: 记录每个操作以确保可追溯性
  •   人工监督: 维持紧急停止开关和审批工作流
# 示例:安全的工具执行
def execute_with_permission(action, requires_approval=True):
    if requires_approval and action.risk_level == "high":
        approval = request_human_approval(action)
        if not approval:
            return "操作被安全策略拒绝"
   
    # 记录操作
    audit_log.record(action, user, timestamp)
   
    # 带超时执行
    return execute_with_timeout(action, timeout=30)

测试和调试 AI 智能体

测试智能体需要不同于传统软件的方法:

1. 基于场景的测试

# 测试各种场景
test_scenarios = [
    {
        "input": "预订明天飞往纽约的航班",
        "expected_tools": ["calendar_check", "flight_search", "price_compare"],
        "expected_outcome": "flight_options"
    },
    {
        "input": "取消我的订阅并退还上个月的费用",
        "expected_tools": ["account_lookup", "subscription_cancel", "refund_process"],
        "expected_outcome": "confirmation"
    }
]
for scenario in test_scenarios:
    result = agent_executor.invoke({"input": scenario["input"]})
    assert all(tool in result["tool_calls"] for tool in scenario["expected_tools"])

2. 调试工具

启用详细日志记录以查看智能体的决策过程:

agent_executor = AgentExecutor(
    agent=agent,
    tools=tools,
    verbose=True,  # 显示思考过程
    return_intermediate_steps=True  # 返回所有步骤
)

5 个即用型智能体系统提示

以下是为常见智能体类型准备的、经过实战检验的系统提示:

1. 研究智能体

你是一个可以访问网络搜索和文档分析工具的研究智能体。
对于每个研究任务:

  1. 将主题分解为关键问题
  2. 从多个来源搜索信息
  3. 通过交叉引用验证事实
  4. 将发现综合成连贯的摘要
  5. 为所有主张包含引用
    始终优先考虑近期信息和权威来源。

2. 客户支持智能体

你是一个客户支持智能体,帮助用户处理他们的账户和订单。

可用工具: order_lookup, refund_process, ticket_create, knowledge_base_search
指南:

  • 在访问账户信息前始终验证客户身份
  • 在升级前搜索知识库
  • 要有同理心并以解决方案为导向
  • 遇到以下情况升级至人工支持:法律问题、威胁或超出你工具范围的请求

切勿对你无法直接实现的功能做出承诺。

3. 数据分析智能体

你是一个专长于商业智能的数据分析智能体。
对于每个分析请求:

  1. 澄清业务问题
  2. 识别相关数据源
  3. 使用适当的统计方法执行分析
  4. 可视化关键发现
  5. 提供可操作的建议

始终在你的分析中注明数据局限性和置信水平。

4. 代码助手智能体

你是一个可以访问文件系统和执行工具的代码助手智能体。
能力:

  • 阅读和分析代码
  • 提出改进建议
  • 实施更改
  • 运行测试
  • 调试问题

切勿:

  • 未经明确许可删除文件
  • 修改系统文件
  • 执行可能有害的命令
  • 在代码中存储凭证

在进行重大更改前始终创建备份。

5. 内容创作智能体

你是一个专注于病毒式内容策略的内容创作智能体。

流程:

  1. 研究指定领域的趋势话题
  2. 分析成功的内容模式
  3. 生成多个内容创意
  4. 创建带有吸引点的详细内容
  5. 建议分发策略

关注真实性和价值,而非点击诱饵。

未来:AI 智能体的下一步是什么?

基于当前轨迹和内部知识,以下是将要发生的事情:

近期(未来 6 个月)

  •   视觉智能体: 能够查看并与 UI 交互的智能体
  •   语音优先智能体: 自然对话取代文本界面
  •   智能体市场: 针对特定行业的预构建智能体
  •   改进的安全性: 内置沙盒和权限系统

中期(2026 年)

  •   物理世界智能体: 与机器人技术集成
  •   监管框架: 为智能体行为设定法律边界
  •   智能体间经济: 智能体雇佣其他智能体
  •   个人 AI 操作系统: 管理整个数字生活的智能体

关键要点

构建真正有效的 AI 智能体需要理解五个核心组件:用于推理的 LLM、用于上下文的记忆、用于行动的工具、用于策略的规划以及一个健壮的执行循环。与聊天机器人的关键区别在于自主决策和动态适应。
从经过验证的框架开始,如用于单智能体的 LangChain 或用于多智能体系统的 AutoGen。专注于清晰的工具描述、适当的错误处理和全面的测试。最重要的是,记住上下文和问题分解比复杂的提示技巧更重要。

AI 智能体革命才刚刚开始。虽然炒作是真实的,但机遇也是真实的。通过遵循本指南并避免常见陷阱,你今天就可以构建出能够交付真正价值的智能体,同时为即将到来的自主未来做好准备。


【注】本文译自:How to Build AI Agents (Complete 2025 Guide) – Superprompt.com

Java智能体框架的繁荣是一种代码异味

停止构建编排框架,开始构建智能体。未来属于那些掌握生态系统的人,而不是那些被困在构建特定语言引擎中的人。

我需要坦白。我是一个框架狂热者。我的职业生涯建立在 Apache Camel 之上,我人生中的大部分成功都归功于企业集成模式的优雅。我懂。如果有一个社区值得获得诺贝尔框架奖,那就是 Java 社区。从早年在红帽公司到整个大数据生态系统,框架 15 年来一直是 JVM 世界的引擎。我们是抽象的大师。

因此,当智能体时代来临而 Java 在奋力追赶时,我的第一本能是原始的:构建一个框架。我甚至开始了一个,驱动力是这样一个想法:"AI 智能体的 Apache Camel 在哪里?"

三个月前,可能只有一个严肃的 Java 智能体框架。现在,包括 Embabel 在内,已经有了三个。竞赛开始了。但目睹这场爆炸式增长,我不得不提出一个难题:框架本身是否正在成为一种反模式?我们是否在为自己创造负担,而不是专注于真正重要的事情:构建智能体

最近 Java 智能体框架的繁荣并非一个健康、成熟生态系统的标志。它是一种症状。一种架构层面的代码坏味道,告诉我们我们的方法存在根本性问题。

我们最初为什么要构建框架?

让我们回顾一下。为什么像 Spring 和 Camel 这样的框架变得如此主流?原因清晰且合理:

  • 开发人员生产力: 我们当时淹没在样板代码中。框架将其抽象掉了。
  • 代码质量与治理: 它们提供了标准化的模式,防止每个开发人员重新发明轮子。
  • 可重用性: 它们为我们提供了经过实战检验的构造来构建,节省了大量的时间和精力。

目标是优化生产力、质量和治理。但这些是我们今天应该优化的相同参数吗?感觉我们像是在用 2010 年的方法解决 2025 年的问题,完全忽视了房间里的大象:AI 驱动的开发工具

这头大象有个名字:Cursor(及其伙伴)

在我们忙于将 LangChain 移植到 Java 时,情况发生了变化:

Cursor 和 Copilot 生成样板代码的速度比你输入 import 语句还快。你正在构建的那个复杂的链式抽象?Cursor 三秒钟就能写出来。你正在标准化的那个工具注册模式?Copilot 已经知道五种变体。

但在这里,我们需要停下来问一个更根本的问题:你的最终用户实际需要什么?

你真正需要构建什么?

让我们具体点。我们大多数人面临两种情况:

  • 场景 1: 你在未来 12 个月内构建一个关键智能体。也许它是一个每天处理 10,000 次对话的客户服务智能体。或者一个需要理解你公司特定标准的代码审查智能体。或者一个绝不能对监管要求产生幻觉的合规智能体。
  • 场景 2: 你正在构建一个智能体平台。成百上千个智能体,每个都有不同的上下文、不同的领域、不同的需求。也许你在一家咨询公司,为多个客户构建智能体。或者你正在创建一个内部平台,不同团队可以在上面启动自己的智能体。你需要可重用、适应性强、可演进的东西。一种能让你快速创建新智能体,同时保持所有智能体一致性和质量的东西。

在这两种情况下,诚实地问自己:你的用户需要一个代码框架吗?

还是他们需要完全不同的东西?

重新定义框架

在放弃我的框架并实际交付智能体之后,我学到了:我们不需要消除框架。我们需要重新定义在 AI 时代框架实际意味着什么。

  • 旧框架定义: 一种可重用的代码抽象,提供结构并处理横切关注点。你导入并在其之上构建的东西。
  • 新框架定义: 构建智能体的完整环境,一组协同工作的相互依赖的层,其中代码层只是更大拼图的一部分。

以下是现代智能体框架中真正重要的层次:

第 1 层:语言本身

Java(或你选择的语言)及其构造、类型和模式。不包装在抽象中,直接使用。语言已经是你的逻辑结构框架。你不需要在 Java 之上再加一个代码框架。Java 本身就是框架。

第 2 层:模型

一个真正好的大语言模型:GPT-5、Claude、Gemini、Grok。这不仅仅是你调用的 API。它是你技术栈的核心组件。模型的能力直接决定了你能构建什么。像选择编程语言一样仔细地选择它。

第 3 层:开发人员生产力工具

Cursor、Copilot 以及下一代 AI 驱动的开发工具。这些不是可选的。它们是关键基础设施。你的框架应设计成与这些工具无缝协作。如果 Cursor 不能轻松地按照你的模式生成代码,那么你的模式是错误的,或者你可能需要很好地描述你的模式。

第 4 层:提示词包与指南

你经过版本控制、测试、治理的提示词。你的组织语音。你的领域知识。你的合规规则。这是你的业务逻辑存在的地方——不在代码中,而在精心策划的上下文和指令中。将这些视为你的依赖构件,就像 JAR 包,但用于智能体行为。

第 5 层:生态系统 API

对新兴的专业化平台及其 API 的上下文感知。用于知识检索的向量数据库。上下文存储和内存管理系统,如 Zep 或 Cognee。工具执行平台,如 Arcade。用于智能体监控的可观测性平台,如 Langfuse。提示词管理和版本控制工具。这些大多暴露 REST API 或标准协议,大多提供 LLM.txt 用于上下文导入。你的框架需要知道这些存在,并知道如何连接到它们。

第 6 层:架构与设计模式

作为指南和模式捕获的架构思维。关于这些层如何在不同用例中组合在一起的可重用蓝图。不是代码抽象——关于路由逻辑、版本控制策略、部署模式和生态系统集成的文档化知识,这些知识成为你组织上下文的一部分。

想想看。当你构建那个关键的客户服务智能体时,真正决定其成功的是什么?

  • 调用 LLM 的 Java 代码吗?(那是 20 行代码,Cursor 写的)
  • 复杂的链式编排吗?(标准控制流)
  • 重试逻辑和错误处理吗?(Java 已经有这方面的模式)

还是:

  • 选择的模型以及它处理你领域的能力
  • 教导它你的升级规则和语气的提示词
  • 让你能快速迭代这些提示词的工具
  • 与像 Arcade(工具)和 Zep(内存)这样的平台的集成
  • 让你能够对变更进行版本控制、测试和部署的架构
  • 让你能在多个智能体中重用这种方法的设计模式

那就是你的框架。所有六层,协同工作。

实践中的框架

让我向你展示在构建智能体时的实际示例:

第 4 层(提示词包) 是版本化的构件,不是你代码中的字符串:

prompts/
  customer-service/
    v1.2/
      system-prompt.md
      escalation-rules.md
      tone-guidelines.md
      product-context.md
      examples/
        refund-scenarios.yaml
        technical-issues.yaml

第 5 层(生态系统 API) 配置在你的环境中:
你的生态系统上下文嵌入在指南中:

# 生态系统集成指南

## 工具发现
- 调用 Arcade API 列出可用工具: GET /tools
- 参考: 查看 Arcade LLM.txt 位于 https://docs.arcade.dev/llms.txt

## 内存管理
- Zep 会话 API: https://api.getzep.com/v2/sessions/{session_id}
- 参考: 查看 Zep API 文档位于 https://docs.getzep.com

## 基础设施与存储
- 用于提示词构件的对象存储: S3, GCS, 或 Azure Blob
- 用于长时间运行工作流的状态持久化

第 1 层(Java) 提供结构,干净简单:

public class CustomerServiceAgent {
    private final Model model;
    private final PromptPack prompts;
    private final ArcadeClient tools;
    private final ZepMemory memory;

    public Response handle(CustomerQuery query) {
        // 检索会话内存
        var history = memory.getSession(query.sessionId());

        // 从 Arcade 获取可用工具
        var availableTools = tools.listTools();

        // 使用上下文渲染提示词
        var context = prompts.render("main", query, history, availableTools);

        return model.complete(context);
    }
}

第 3 层(Cursor) 在几秒钟内生成这段代码。你专注于架构。

第 6 层(架构) 指南:

# 智能体架构指南

## 工作流路由
- 为多节点智能体工作流设计路由逻辑
  - 分类节点 → 路由到专家节点(支持、销售、技术)
  - 复杂性分析 → 路由到适当的模型层级(GPT-4o vs GPT-3.5)
  - 工具选择节点 → 根据用户意图路由到工具执行节点
- 通过 Arcade 网关路由工具执行:集中认证、速率限制、工具发现
- 提示词版本的 A/B 路由:10% 到 v2.0,90% 到 v1.5,监控质量

## 速率限制与节流
- 每用户令牌预算:10K 令牌/天(免费),100K(付费)
- 队列管理:最大 100 个并发请求,溢出到 SQS...
..
..

为什么这个框架能扩展

  • 对于一个关键智能体: 选择你的模型(第 2 层),编写清晰的代码(第 1 层),用 Cursor 迭代(第 3 层),优化提示词(第 4 层),集成生态系统 API(第 5 层),遵循架构模式(第 6 层)。
  • 对于一千个智能体: 相同的模型,相同的架构模式,相同的生态系统 API,但每个智能体都有自己的提示词包。Cursor 生成样板代码。你的语言提供结构。生态系统处理难题。

美妙之处何在?各层协同工作。Cursor 生成代码是因为模式简单。提示词是独立版本控制的。集成使用 REST API。架构无需抽象即可实现重用。

不需要编排框架。这就是框架。

引擎与 SDK 的问题

让我澄清一下:我并不是说所有框架都应该消失。我对 LangChain、LangGraph、Mastra、CrewAI、Autogen 等团队所构建的东西怀有极大的敬意。但我们需要理解一个在急于将所有东西移植到 Java 的过程中被忽视的关键区别。

不要混淆引擎SDK

我的意思是:我迫不及待地想用 Java 开发完整的智能体。我热爱 Java。但我不想仅仅因为我想用 Java 开发智能体就要一个 Java 引擎

考虑这些例子:

  • LangChain4J? 作为连接更广泛的 LangChain 生态系统的 SDK,这是一个很好的开始。你用 Java 编写,但你正在利用一个经过验证的引擎。
  • 带有 Java SDK 的 Crew AI? 完美。在 Python 中掌握编排模式,然后给我一个 Java 接口来使用它们。
  • 支持多语言的 Mastra? 正是正确的方向。构建一次引擎,为每种语言提供 SDK。
  • 为使用 Go 构建的 Not7 添加 Java SDK 或任何语言 SDK?

这里的模式是?用你喜欢的语言开发,而无需用该语言重建整个引擎。

编排层正在变薄

这就是为什么我认为即使是 SDK 方法也可能是暂时的,或者至少变得非常精简的原因:

  • 一方面: 模型正变得 dramatically 更好。GPT-5、Claude 4.5、Gemini 2.5 Pro、Grok 的推理能力使得复杂的编排模式过时了。它们可以用简单的提示词处理多步骤工作流,而这在六个月前需要复杂的链。
  • 另一方面: 真正的工程问题正在由专业平台解决。以 Arcade 为例:工具发现、认证、大规模执行、处理速率限制、管理工具版本。这才是艰难的工程工作所在。工具管理不再是编排问题;它是在平台层解决的基础设施问题。
  • 在中间: 编排框架正被挤压得越来越薄。

当你的模型能够推理工作流,并且平台处理复杂的工程问题(工具、内存、上下文)时,编排还剩下什么?

答案是:非常少。这就是为什么工程重点需要从编排转向更广泛的智能体开发挑战——提示词管理、生态系统集成、工具决策可审计性、成本优化。真正的问题已不在编排之中。

新现实:AI 原生框架

代码坏味道不仅仅是我们构建了太多框架。而是我们正在为一个不复存在的世界构建框架。以下是 2025 年构建框架实际意味着什么:

方面 过去的框架思维模式 (2005-2024) 下一代框架思维模式 (2025+)
定义 需要导入的代码库 跨越6个层级的完整环境
业务逻辑 位于代码抽象中 位于版本化提示词与指南中
关键构件 JAR 文件、软件包 提示词、上下文、API 知识
可重用性 代码继承与组合 架构模式与蓝图
开发工具 用于编写代码的 IDE 用于生成代码的 AI 工具(如 Cursor)
生态系统 自包含、单体式 集成专业化平台
样板代码 由框架抽象处理 由 AI 在几秒内生成
你导入/使用什么 Spring、Camel、框架 JAR 包 无需导入——你只需组合这些层级
  1. 接受 AI 驱动的开发现实 每个构建智能体的开发人员都将使用 Cursor、Copilot 或类似工具。这不是趋势——这是新的基线。设计你的框架以与 AI 代码生成无缝协作,而不是背道而驰。如果 Cursor 无法理解你的模式,那你的模式就是错的。
  2. 你的框架是纯文本英语,而不仅仅是代码 你的框架最关键部分将是精心设计的提示词、清晰的指南和结构化的上下文——而不是聪明的抽象。这些是你的版本化构件。这些决定了智能体行为。像对待代码一样严格对待它们。
  3. 当你需要 SDK 时,不要重新发明引擎 是的,Java SDK 至关重要。但你不需要仅仅为了用 Java 编写智能体就重建整个编排引擎。生态系统已经有平台在解决难题:内存(Zep, Mem0)、工具(MCPs, Arcade)、向量(Weaviate, Pinecone, Qdrant)、可观测性等。集成,不要重建。
  4. 框架仍然至关重要——但不是为了编排 如果你正在解决真正的问题——提示词版本控制、决策可审计性、生态系统集成模式、成本优化——那就构建这些。但编排?生态系统已经向前发展了。内存、工具、上下文、可观测性正由专业平台解决。将你的创新重点放在其他地方。
  5. 相信你的语言 如果你觉得你选择的语言中缺少一个框架,请退后一步。现代语言——Java、Python、TypeScript、Go——非常强大。凭借它们的最新特性加上 AI 代码生成工具,你可以用干净、简单的代码构建复杂的智能体。你的语言不是限制——试图用不必要的抽象包装它才是。

未来的框架不是你导入的代码库。它是对六个相互依赖层的掌握:你的语言、你的模型、你的开发工具、你的提示词、你的生态系统集成和你的架构。

也许我们不需要另一个智能体框架。也许我们所需要的只是一个智能体,一个能用你选择的语言创建智能体的智能体。一个开源的就更好了。


【注】本文译自:Java’s Agentic Framework Boom is a Code Smell

Java 运行时安全:输入验证、沙箱机制、安全反序列化

你的 Java 应用程序刚刚被攻破了。攻击者发送了一个精心构造的 JSON 载荷,你的反序列化代码"尽职尽责"地执行了它,现在他们正在下载你的客户数据库。这并非假设场景——它曾在 Equifax、Apache 以及无数其他公司真实发生过。

运行时安全与防火墙或身份验证无关。它关注的是不受信任的数据进入你的应用程序之后会发生什么。攻击者能否诱使你的代码执行你从未打算做的事情?答案通常是"可以",除非你刻意提高了攻击难度。

Java 为你提供了自卫的工具。大多数开发者忽略了它们,因为这些工具看起来偏执或过于复杂。然后生产环境就遭到了入侵,突然间那些"偏执"的措施就显得相当合理了。

为何运行时安全被忽视

你专注于功能。安全评审即使有,也往往在后期进行。代码在测试中能工作,于是就发布了。然后有人发现你的公共 API 未经验证就接受了用户输入,或者发现你正在反序列化不受信任的数据,或者意识到你的插件系统以完全权限运行第三方代码。

问题在于,大多数漏洞在你编写它们时看起来并不危险。一个简单的 ObjectInputStream.readObject() 调用看似无害,直到有人解释它如何实现远程代码执行。跳过输入验证节省了五分钟的开发时间,却在六个月后让你付出安全事件的代价。

安全不吸引人,它不会在演示中体现,而且在出事之前很难量化。但运行时安全问题是在生产系统中最常被利用的漏洞之一。让我们来谈谈三大要点:输入验证、沙箱机制和反序列化。

输入验证:万物皆不可信

每一个从外部进入你应用程序的数据都是潜在的攻击向量。用户输入、API 请求、文件上传、来自共享数据库的数据库记录、配置文件——所有这些都是。

规则很简单:在边界验证一切。不要等到业务逻辑中再验证。不要假设前端已经验证过了。在数据进入你的系统时进行验证。

糟糕的验证示例

以下是我在生产环境中经常看到的代码:

@PostMapping("/users")
public ResponseEntity<User> createUser(@RequestBody UserRequest request) {
    User user = new User();
    user.setEmail(request.getEmail());
    user.setAge(request.getAge());
    user.setRole(request.getRole());

    userRepository.save(user);
    return ResponseEntity.ok(user);
}

看起来没问题,对吧?这是一场灾难。攻击者可以发送:

  • 邮箱:"admin@evil.com<script>alert('xss')</script>"
  • 年龄:-1999999
  • 角色:"ADMIN"(提升自己的权限)

你的应用程序会欣然接受所有这一切,因为你信任了输入。

正确的输入验证

以下是正确的做法:

public class UserRequest {
    @NotNull(message = "Email is required")
    @Email(message = "Must be a valid email")
    @Size(max = 255, message = "Email too long")
    private String email;

    @NotNull(message = "Age is required")
    @Min(value = 0, message = "Age must be positive")
    @Max(value = 150, message = "Age unrealistic")
    private Integer age;

    @NotNull(message = "Role is required")
    @Pattern(regexp = "^(USER|MODERATOR)$", message = "Invalid role")
    private String role;
}

@PostMapping("/users")
public ResponseEntity<User> createUser(@Valid @RequestBody UserRequest request) {
    // 如果验证失败,Spring 自动返回 400 Bad Request

    User user = new User();
    user.setEmail(sanitizeEmail(request.getEmail()));
    user.setAge(request.getAge());
    user.setRole(request.getRole());

    userRepository.save(user);
    return ResponseEntity.ok(user);
}

private String sanitizeEmail(String email) {
    // 额外防护层:清除任何 HTML/脚本标签以防万一
    return email.replaceAll("<[^>]*>", "");
}

注意这种分层方法。Bean 验证注解捕获明显的问题。然后即使在验证之后,你还要对输入进行清理。这种深度防御方法意味着即使一层失效,你仍然受到保护。

验证复杂对象

真实的应用程序处理的是嵌套对象、列表和复杂结构:

public class OrderRequest {
    @NotNull
    @Valid  // 这很关键 - 验证嵌套对象
    private Customer customer;

    @NotEmpty(message = "Order must contain items")
    @Size(max = 100, message = "Too many items")
    @Valid
    private List<OrderItem> items;

    @NotNull
    @DecimalMin(value = "0.01", message = "Total must be positive")
    private BigDecimal total;
}

public class OrderItem {
    @NotBlank
    @Size(max = 50)
    private String productId;

    @Min(1)
    @Max(999)
    private Integer quantity;

    @DecimalMin("0.01")
    private BigDecimal price;
}

嵌套对象上的 @Valid 注解很容易被忘记,但至关重要。没有它,嵌套对象会完全绕过验证。

用于业务规则的自定义验证器

有时 Bean 验证还不够。你需要业务逻辑:

@Target({ElementType.FIELD})
@Retention(RetentionPolicy.RUNTIME)
@Constraint(validatedBy = SafeFilenameValidator.class)
public @interface SafeFilename {
    String message() default "Unsafe filename";
    Class<?>[] groups() default {};
    Class<? extends Payload>[] payload() default {};
}

public class SafeFilenameValidator implements ConstraintValidator<SafeFilename, String> {
    private static final Pattern DANGEROUS_PATTERNS = Pattern.compile(
        "(\\.\\./|\\.\\.\\\\|[<>:\"|?*]|^\\.|\\.$)"
    );

    @Override
    public boolean isValid(String filename, ConstraintValidatorContext context) {
        if (filename == null) {
            return true; // 单独使用 @NotNull
        }

        // 防止路径遍历攻击
        if (DANGEROUS_PATTERNS.matcher(filename).find()) {
            return false;
        }

        // 白名单方法:只允许安全字符
        if (!filename.matches("^[a-zA-Z0-9_.-]+$")) {
            return false;
        }

        return true;
    }
}

现在你可以在任何文件上传参数上使用 @SafeFilename。这可以捕获攻击者试图上传到 ../../../etc/passwd 的路径遍历攻击。

白名单与黑名单的陷阱

在验证输入时,开发者常常试图阻止"坏"字符。这是黑名单方法,而且几乎总是错误的:

// 不好:黑名单方法
public boolean isValidUsername(String username) {
    return !username.contains("<") && 
           !username.contains(">") && 
           !username.contains("'") &&
           !username.contains("\"") &&
           !username.contains("script");
           // 你永远无法列出所有危险模式
}

攻击者很有创造力。他们会使用 Unicode 字符、URL 编码、双重编码以及你没想到的技巧来绕过你的黑名单。

相反,应该对你允许的内容使用白名单:

// 好:白名单方法
public boolean isValidUsername(String username) {
    return username.matches("^[a-zA-Z0-9_-]{3,20}$");
    // 只允许字母数字、下划线、连字符,3-20个字符
}

如果不在明确允许的范围内,就拒绝。这样安全得多。

沙箱机制:限制损害

输入验证阻止坏数据进入。沙箱机制则限制代码即使攻击成功也能做的事情。如果你的应用程序运行不受信任的代码——插件、用户脚本、动态类加载——沙箱机制至关重要。

Java 安全管理器(传统方法)

多年来,Java 使用安全管理器进行沙箱处理。它在 Java 17 中已被弃用并将被移除,但理解它有助于掌握概念:

// 旧方法(已弃用)
System.setSecurityManager(new SecurityManager());

// 在策略文件中定义权限
grant codeBase "file:/path/to/untrusted/*" {
    permission java.io.FilePermission "/tmp/*", "read,write";
    permission java.net.SocketPermission "example.com:80", "connect";
    // 权限非常有限
};

安全管理器可以限制代码能做什么:文件访问、网络访问、系统属性访问等。它功能强大但复杂,并且有性能开销。

现代沙箱方法

没有安全管理器,你需要替代策略。

在独立进程中隔离。 最可靠的沙箱是进程边界:

public class PluginExecutor {
    public String executePlugin(String pluginPath, String input) throws Exception {
        ProcessBuilder pb = new ProcessBuilder(
            "java",
            "-Xmx256m",  // 限制内存
            "-classpath", pluginPath,
            "com.example.PluginRunner",
            input
        );

        // 限制进程能做的事情
        pb.environment().clear();  // 无环境变量
        pb.directory(new File("/tmp/sandbox"));  // 受限目录

        Process process = pb.start();

        // 超时保护
        if (!process.waitFor(10, TimeUnit.SECONDS)) {
            process.destroyForcibly();
            throw new TimeoutException("Plugin execution timeout");
        }

        return new String(process.getInputStream().readAllBytes());
    }
}

插件在它自己的、资源受限的进程中运行。如果它崩溃或行为不端,你的主应用程序不会受到影响。你可以使用容器或虚拟机实现更强的隔离。

使用带有限制的自定义 ClassLoader:

public class SandboxedClassLoader extends ClassLoader {
    private final Set<String> allowedPackages;

    public SandboxedClassLoader(Set<String> allowedPackages) {
        super(SandboxedClassLoader.class.getClassLoader());
        this.allowedPackages = allowedPackages;
    }

    @Override
    protected Class<?> loadClass(String name, boolean resolve) 
            throws ClassNotFoundException {
        // 阻止危险的类
        if (name.startsWith("java.lang.Runtime") ||
            name.startsWith("java.lang.ProcessBuilder") ||
            name.startsWith("sun.misc.Unsafe")) {
            throw new ClassNotFoundException("Access denied: " + name);
        }

        // 仅白名单特定的包
        boolean allowed = allowedPackages.stream()
            .anyMatch(name::startsWith);

        if (!allowed) {
            throw new ClassNotFoundException("Package not whitelisted: " + name);
        }

        return super.loadClass(name, resolve);
    }
}

// 用法
Set<String> allowed = Set.of("com.example.safe.", "org.apache.commons.lang3.");
ClassLoader sandboxed = new SandboxedClassLoader(allowed);
Class<?> pluginClass = sandboxed.loadClass("com.example.safe.UserPlugin");

这可以防止插件加载危险的类。它并非无懈可击——坚定的攻击者可能会找到基于反射的变通方法——但它显著提高了攻击门槛。

限制资源消耗:

public class ResourceLimitedExecutor {
    private final ExecutorService executor = Executors.newFixedThreadPool(4);

    public <T> T executeWithLimits(Callable<T> task, 
                                   long timeoutSeconds,
                                   long maxMemoryMB) throws Exception {
        // 通过超时限制 CPU/时间
        Future<T> future = executor.submit(task);

        try {
            return future.get(timeoutSeconds, TimeUnit.SECONDS);
        } catch (TimeoutException e) {
            future.cancel(true);
            throw new RuntimeException("Task exceeded time limit");
        }

        // 内存限制更难——最好在 JVM 级别使用 -Xmx 处理
        // 或者使用如前所示的进程隔离
    }
}

如果你强制执行超时,即使是不受信任的代码也无法消耗无限的 CPU。内存更棘手——进程隔离或容器限制比尝试在 JVM 内强制执行效果更好。

真实世界的沙箱示例

假设你正在构建一个运行用户提交的数据转换脚本的系统:

public class ScriptSandbox {
    private static final long MAX_EXECUTION_TIME_MS = 5000;
    private static final String SANDBOX_DIR = "/tmp/script-sandbox";

    public String executeScript(String script, String data) {
        // 1. 验证脚本没有明显的恶意
        if (containsDangerousPatterns(script)) {
            throw new SecurityException("Script contains forbidden patterns");
        }

        // 2. 将脚本写入隔离目录
        Path scriptPath = Paths.get(SANDBOX_DIR, UUID.randomUUID().toString() + ".js");
        Files.writeString(scriptPath, script);

        try {
            // 3. 在具有资源限制的独立进程中执行
            ProcessBuilder pb = new ProcessBuilder(
                "timeout", String.valueOf(MAX_EXECUTION_TIME_MS / 1000),
                "node",
                "--max-old-space-size=100",  // 100MB 内存限制
                scriptPath.toString()
            );

            pb.directory(new File(SANDBOX_DIR));
            pb.redirectErrorStream(true);

            Process process = pb.start();

            // 4. 通过 stdin 传递数据,从 stdout 读取结果
            try (OutputStream os = process.getOutputStream()) {
                os.write(data.getBytes());
            }

            String result = new String(process.getInputStream().readAllBytes());

            int exitCode = process.waitFor();
            if (exitCode != 0) {
                throw new RuntimeException("Script failed with exit code: " + exitCode);
            }

            return result;

        } finally {
            // 5. 清理
            Files.deleteIfExists(scriptPath);
        }
    }

    private boolean containsDangerousPatterns(String script) {
        // 检查明显的攻击
        return script.contains("require('child_process')") ||
               script.contains("eval(") ||
               script.contains("Function(") ||
               script.matches(".*\\brequire\\s*\\(.*");
    }
}

这个例子结合了多种防御措施:静态分析、进程隔离、资源限制和清理。没有单一的防御是完美的,但层层设防使得利用难度大大增加。

安全反序列化:最大的隐患

Java 反序列化漏洞是历史上一些最严重安全漏洞的罪魁祸首。问题在于其根本性质:反序列化可以在对象构造期间执行任意代码。

为何反序列化是危险的

当你反序列化一个对象时,Java 会调用构造函数、readObject 方法和其他代码。控制序列化数据的攻击者可以精心构造对象来执行任意命令:

// 危险代码 - 请勿在生产环境中使用
public void loadUserSettings(byte[] data) {
    try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(data))) {
        UserSettings settings = (UserSettings) ois.readObject();
        applySettings(settings);
    }
}

这看起来无害。但攻击者可以发送包含你类路径上(如 Apache Commons Collections)库中对象的序列化数据,这些对象在反序列化期间会执行系统命令。他们甚至根本不需要接触你的 UserSettings 类。

臭名昭著的"工具链"就是利用这一点。通过以特定方式链式组合标准库类,攻击者实现了远程代码执行。像 ysoserial 这样的工具可以自动创建这些载荷。

切勿反序列化不受信任的数据

最安全的方法很简单:不要对来自不受信任来源的数据使用 Java 序列化。绝不。

改用 JSON、Protocol Buffers 或其他仅包含数据的格式:

// 安全:使用 JSON
public UserSettings loadUserSettings(String json) {
    ObjectMapper mapper = new ObjectMapper();
    return mapper.readValue(json, UserSettings.class);
}

像 Jackson 这样的 JSON 解析器在解析期间不会执行任意代码。它们只是填充字段。攻击面急剧缩小。

当你必须反序列化时

有时你无法摆脱 Java 序列化——遗留协议、缓存库或分布式计算框架。如果你绝对必须反序列化不受信任的数据,请使用防御措施。

使用 ObjectInputFilter (Java 9+):

public Object safeDeserialize(byte[] data) throws Exception {
    try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(data))) {
        // 白名单允许的类
        ObjectInputFilter filter = ObjectInputFilter.Config.createFilter(
            "com.example.UserSettings;" +
            "com.example.UserPreference;" +
            "java.util.ArrayList;" +
            "java.lang.String;" +
            "!*"  // 拒绝其他所有类
        );

        ois.setObjectInputFilter(filter);

        return ois.readObject();
    }
}

该过滤器明确地将安全的类加入白名单,并拒绝其他所有类。这阻止了依赖于意外可用类的工具链。

验证对象图:

public class SafeObjectInputStream extends ObjectInputStream {
    private final Set<String> allowedClasses;
    private int maxDepth = 10;
    private int currentDepth = 0;

    public SafeObjectInputStream(InputStream in, Set<String> allowedClasses) 
            throws IOException {
        super(in);
        this.allowedClasses = allowedClasses;
    }

    @Override
    protected Class<?> resolveClass(ObjectStreamClass desc) 
            throws IOException, ClassNotFoundException {
        // 检查深度以防止深度嵌套的对象
        if (++currentDepth > maxDepth) {
            throw new InvalidClassException("Max depth exceeded");
        }

        String className = desc.getName();

        // 白名单检查
        if (!allowedClasses.contains(className)) {
            throw new InvalidClassException("Class not allowed: " + className);
        }

        return super.resolveClass(desc);
    }

    @Override
    protected ObjectStreamClass readClassDescriptor() 
            throws IOException, ClassNotFoundException {
        ObjectStreamClass desc = super.readClassDescriptor();
        currentDepth--;
        return desc;
    }
}

这个自定义实现通过跟踪反序列化深度和执行严格的白名单来增加另一层防御。

对序列化数据进行签名:

public class SignedSerializer {
    private final SecretKey signingKey;

    public byte[] serialize(Object obj) throws Exception {
        // 序列化对象
        ByteArrayOutputStream baos = new ByteArrayOutputStream();
        try (ObjectOutputStream oos = new ObjectOutputStream(baos)) {
            oos.writeObject(obj);
        }
        byte[] data = baos.toByteArray();

        // 创建签名
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(signingKey);
        byte[] signature = mac.doFinal(data);

        // 合并签名和数据
        ByteBuffer buffer = ByteBuffer.allocate(signature.length + data.length);
        buffer.put(signature);
        buffer.put(data);

        return buffer.array();
    }

    public Object deserialize(byte[] signedData) throws Exception {
        ByteBuffer buffer = ByteBuffer.wrap(signedData);

        // 提取签名和数据
        byte[] signature = new byte[32];  // HmacSHA256 产生 32 字节
        buffer.get(signature);

        byte[] data = new byte[buffer.remaining()];
        buffer.get(data);

        // 验证签名
        Mac mac = Mac.getInstance("HmacSHA256");
        mac.init(signingKey);
        byte[] expectedSignature = mac.doFinal(data);

        if (!MessageDigest.isEqual(signature, expectedSignature)) {
            throw new SecurityException("Signature verification failed");
        }

        // 签名有效则反序列化
        try (ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(data))) {
            return ois.readObject();
        }
    }
}

签名可以防止攻击者篡改序列化数据。没有签名密钥,他们无法注入恶意对象。这在数据可能暴露但不受攻击者直接控制时(如客户端存储或缓存系统)有效。

替代序列化库

有几个库提供了更安全的序列化:

Kryo 提供更好的性能,并且可以配置为使用白名单:

Kryo kryo = new Kryo();
kryo.setRegistrationRequired(true);  // 拒绝未注册的类
kryo.register(UserSettings.class);
kryo.register(ArrayList.class);

// 序列化
Output output = new Output(new FileOutputStream("file.bin"));
kryo.writeObject(output, userSettings);
output.close();

// 反序列化 - 只允许注册的类
Input input = new Input(new FileInputStream("file.bin"));
UserSettings settings = kryo.readObject(input, UserSettings.class);
input.close();

Protocol BuffersApache Avro 使用基于模式的序列化。它们设置起来比较繁琐,但完全避免了代码执行风险:

message UserSettings {
  string theme = 1;
  int32 fontSize = 2;
  repeated string favorites = 3;
}

这些格式只反序列化数据,从不反序列化代码。通过 protobuf 反序列化实现代码执行是不可能的。

真实世界安全事件:一个警示故事

我曾咨询过的一家公司有一个管理门户,用于接受文件上传以进行批处理。代码看起来像这样:

@PostMapping("/admin/import")
public String importData(@RequestParam("file") MultipartFile file) {
    try {
        byte[] data = file.getBytes();
        ObjectInputStream ois = new ObjectInputStream(new ByteArrayInputStream(data));
        DataImport importData = (DataImport) ois.readObject();

        processImport(importData);
        return "Import successful";
    } catch (Exception e) {
        return "Import failed: " + e.getMessage();
    }
}

开发人员认为这是安全的,因为该端点需要管理员身份验证。他们遗漏的是:

  • 攻击者通过钓鱼攻击攻陷了一个低级别管理员账户
  • 攻击者使用 ysoserial 上传了一个恶意的序列化载荷
  • 在反序列化期间,载荷执行了系统命令
  • 攻击者获得了应用程序服务器的 shell 访问权限
  • 从那里,他们横向移动到数据库并窃取了客户数据

修复需要多次更改:

@PostMapping("/admin/import")
public String importData(@RequestParam("file") MultipartFile file) {
    // 验证文件类型
    if (!file.getContentType().equals("application/json")) {
        return "Only JSON imports allowed";
    }

    // 验证文件大小
    if (file.getSize() > 10 * 1024 * 1024) {  // 10MB 限制
        return "File too large";
    }

    try {
        // 使用 JSON 代替 Java 序列化
        ObjectMapper mapper = new ObjectMapper();
        mapper.disable(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES);

        DataImport importData = mapper.readValue(
            file.getInputStream(), 
            DataImport.class
        );

        // 验证导入的数据
        validateImportData(importData);

        // 在受限上下文中处理
        processImportSafely(importData);

        return "Import successful";
    } catch (Exception e) {
        log.error("Import failed", e);
        return "Import failed - check logs";
    }
}

这次事件使他们付出了事件响应、法律费用和声誉损失方面的数百万代价。全都是因为一个不安全的反序列化调用。

实用安全检查清单

以下是你在每个 Java 应用程序中都应该做的事情:

输入验证:

  • 在所有 DTO 上使用 Bean 验证注解
  • 使用 @Valid 验证嵌套对象
  • 白名单允许的模式,不要黑名单危险模式
  • 即使在验证之后也要清理数据
  • 验证文件上传:类型、大小、内容
  • 绝不只依赖客户端验证

沙箱机制:

  • 在独立进程或容器中运行不受信任的代码
  • 使用自定义 ClassLoader 来限制类访问
  • 强制执行资源限制:内存、CPU 时间、磁盘空间
  • 清理临时文件和资源
  • 记录所有沙箱违规行为

反序列化:

  • 优先使用 JSON/Protocol Buffers 而非 Java 序列化
  • 没有过滤器的情况下切勿反序列化不受信任的数据
  • 使用 ObjectInputFilter 将类加入白名单
  • 可能时对序列化数据进行签名
  • 定期审计类路径依赖项以查找已知的工具类
  • 考虑使用需要注册模式的 Kryo

通用实践:

  • 保持依赖项更新(漏洞利用针对特定版本)
  • 使用静态分析工具捕获安全问题
  • 记录安全相关事件以进行监控
  • 使用恶意输入进行测试,而不仅仅是正常路径
  • 假设一切都可以被攻击

有用的工具

SpotBugsFindSecBugs 插件可在构建时捕获常见安全问题:

<plugin>
    <groupId>com.github.spotbugs</groupId>
    <artifactId>spotbugs-maven-plugin</artifactId>
    <configuration>
        <plugins>
            <plugin>
                <groupId>com.h3xstream.findsecbugs</groupId>
                <artifactId>findsecbugs-plugin</artifactId>
                <version>1.12.0</version>
            </plugin>
        </plugins>
    </configuration>
</plugin>

OWASP Dependency-Check 识别易受攻击的依赖项:

<plugin>
    <groupId>org.owasp</groupId>
    <artifactId>dependency-check-maven</artifactId>
    <executions>
        <execution>
            <goals>
                <goal>check</goal>
            </goals>
        </execution>
    </executions>
</plugin>

SnykDependabot 在漏洞披露时自动更新依赖项。

思维模式的转变

安全不是你最后添加的功能。它是你从一开始就为之设计的约束。每次你接受外部输入时,问问自己:"攻击者利用这个能做的最坏的事情是什么?" 每次你反序列化数据时,问问:"我是否完全信任这个数据的来源?"

在代码审查中偏执是一种美德。当某人的 PR 包含反序列化或动态类加载时,积极地提出质疑。当缺少输入验证时,把它打回去。在代码审查中显得迂腐,也比在漏洞发生后显得疏忽要好。

运行时安全是关于减少信任。不要信任用户输入。不要信任插件。不要信任序列化数据。不要信任你的验证是完美的。层层设防,这样当一层失效时——它会的——其他层可以捕获攻击。

好消息是,一旦你内化了这些模式,它们就会成为第二天性。输入验证变得自动进行。你会本能地避免 Java 序列化。你会带着隔离的思想进行设计。安全成为你编码风格的一部分,而不是事后附加的东西。

有用资源


【注】本文译自:
Runtime Security in Java: Input Validation, Sandboxing, Safe Deserialization

Java 21 虚拟线程 vs 缓存线程池与固定线程池

探索 Java 并发如何从 Java 8 的增强发展到 Java 21 的虚拟线程,从而实现轻量级、可扩展且高效的多线程处理。

引言

并发编程仍然是构建可扩展、响应式 Java 应用程序的关键部分。多年来,Java 持续增强了其多线程编程能力。本文回顾了从 Java 8 到 Java 21 并发的演进,重点介绍了重要的改进以及 Java 21 中引入的具有重大影响的虚拟线程。

从 Java 8 开始,并发 API 出现了显著的增强,例如原子变量、并发映射以及集成 lambda 表达式以实现更具表现力的并行编程。

Java 8 引入的关键改进包括:

  • 线程与执行器
  • 同步与锁
  • 原子变量与 ConcurrentMap

Java 21 于 2023 年底发布,带来了虚拟线程这一重大演进,从根本上改变了 Java 应用程序处理大量并发任务的方式。虚拟线程为服务器应用程序提供了更高的可扩展性,同时保持了熟悉的"每个请求一个线程"的编程模型。

或许,Java 21 中最重要的特性就是虚拟线程。
在 Java 21 中,Java 的基本并发模型保持不变,Stream API 仍然是并行处理大型数据集的首选方式。
随着虚拟线程的引入,并发 API 现在能提供更好的性能。在当今的微服务和可扩展服务器应用领域,线程数量必须增长以满足需求。虚拟线程的主要目标是使服务器应用程序能够实现高可扩展性,同时仍使用简单的"每个请求一个线程"模型。

虚拟线程

在 Java 21 之前,JDK 的线程实现使用的是操作系统线程的薄包装器。然而,操作系统线程代价高昂:

  • 如果每个请求在其整个持续时间内消耗一个操作系统线程,线程数量很快就会成为可扩展性的瓶颈。
  • 即使使用线程池,吞吐量仍然受到限制,因为实际线程数量是有上限的。

虚拟线程的目标是打破 Java 线程与操作系统线程之间的 1:1 关系。
虚拟线程应用了类似于虚拟内存的概念。正如虚拟内存将大的地址空间映射到较小的物理内存一样,虚拟线程允许运行时通过将它们映射到少量操作系统线程来制造拥有许多线程的假象。

平台线程是操作系统线程的薄包装器。
而虚拟线程并不绑定到任何特定的操作系统线程。虚拟线程可以执行平台线程可以运行的任何代码。这是一个主要优势——现有的 Java 代码通常无需修改或仅需少量修改即可在虚拟线程上运行。虚拟线程由平台线程承载,这些平台线程仍然由操作系统调度。

例如,您可以像这样创建一个使用虚拟线程的执行器:

ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();

对比示例

虚拟线程仅在主动执行 CPU 密集型任务时才消耗操作系统线程。虚拟线程在其生命周期内可以在不同的载体线程上挂载或卸载。

通常,当虚拟线程遇到阻塞操作时,它会自行卸载。一旦该阻塞任务完成,虚拟线程通过挂载到任何可用的载体线程上来恢复执行。这种挂载和卸载过程频繁且透明地发生——不会阻塞操作系统线程。

示例 — 源代码

Example01CachedThreadPool.java

在此示例中,使用缓存线程池创建了一个执行器:

var executor = Executors.newCachedThreadPool()
package threads;

import java.time.Duration;
import java.util.concurrent.Executors;
import java.util.stream.IntStream;

/**
 *
 * @author Milan Karajovic <milan.karajovic.rs@gmail.com>
 *
 */

public class Example01CachedThreadPool {

    public void executeTasks(final int NUMBER_OF_TASKS) {

        final int BLOCKING_CALL = 1;
        System.out.println("Number of tasks which executed using 'newCachedThreadPool()' " + NUMBER_OF_TASKS + " tasks each.");

        long startTime = System.currentTimeMillis();

        try (var executor = Executors.newCachedThreadPool()) {

            IntStream.range(0, NUMBER_OF_TASKS).forEach(i -> {
                executor.submit(() -> {
                    // 模拟阻塞调用
                    Thread.sleep(Duration.ofSeconds(BLOCKING_CALL));
                    return i;
                });
            });

        } catch (Exception e) {
            throw new RuntimeException(e);
        }

        long endTime = System.currentTimeMillis();
        System.out.println("For executing " + NUMBER_OF_TASKS + " tasks duration is: " + (endTime - startTime) + " ms");
    }

}
package threads;

import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;

/**
 *
 * @author Milan Karajovic <milan.karajovic.rs@gmail.com>
 *
 */

@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class Example01CachedThreadPoolTest {

    @Test
    @Order(1)
    public void test_1000_tasks() {
        Example01CachedThreadPool example01CachedThreadPool = new Example01CachedThreadPool();
        example01CachedThreadPool.executeTasks(1000);
    }

    @Test
    @Order(2)
    public void test_10_000_tasks() {
        Example01CachedThreadPool example01CachedThreadPool = new Example01CachedThreadPool();
        example01CachedThreadPool.executeTasks(10_000);
    }

    @Test
    @Order(3)
    public void test_100_000_tasks() {
        Example01CachedThreadPool example01CachedThreadPool = new Example01CachedThreadPool();
        example01CachedThreadPool.executeTasks(100_000);
    }

    @Test
    @Order(4)
    public void test_1_000_000_tasks() {
        Example01CachedThreadPool example01CachedThreadPool = new Example01CachedThreadPool();
        example01CachedThreadPool.executeTasks(1_000_000);
    }

}

我 PC 上的测试结果:

Example02FixedThreadPool.java

使用固定线程池创建执行器:

var executor = Executors.newFixedThreadPool(500)
package threads;

import java.time.Duration;
import java.util.concurrent.Executors;
import java.util.stream.IntStream;

/**
 *
 * @author Milan Karajovic <milan.karajovic.rs@gmail.com>
 *
 */

public class Example02FixedThreadPool {

    public void executeTasks(final int NUMBER_OF_TASKS) {

        final int BLOCKING_CALL = 1;
        System.out.println("Number of tasks which executed using 'newFixedThreadPool(500)' " + NUMBER_OF_TASKS + " tasks each.");

        long startTime = System.currentTimeMillis();

        try (var executor = Executors.newFixedThreadPool(500)) {

            IntStream.range(0, NUMBER_OF_TASKS).forEach(i -> {
               executor.submit(() -> {
                   // 模拟阻塞调用
                  Thread.sleep(Duration.ofSeconds(BLOCKING_CALL));
                  return i;
               });
            });

        }   catch (Exception e) {
            throw new RuntimeException(e);
        }

        long endTime = System.currentTimeMillis();
        System.out.println("For executing " + NUMBER_OF_TASKS + " tasks duration is: " + (endTime - startTime) + " ms");
    }

}
package threads;

import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;

/**
 *
 * @author Milan Karajovic <milan.karajovic.rs@gmail.com>
 *
 */

@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class Example02FixedThreadPoolTest {

    @Test
    @Order(1)
    public void test_1000_tasks() {
        Example02FixedThreadPool example02FixedThreadPool = new Example02FixedThreadPool();
        example02FixedThreadPool.executeTasks(1000);
    }

    @Test
    @Order(2)
    public void test_10_000_tasks() {
        Example02FixedThreadPool example02FixedThreadPool = new Example02FixedThreadPool();
        example02FixedThreadPool.executeTasks(10_000);
    }

    @Test
    @Order(3)
    public void test_100_000_tasks() {
        Example02FixedThreadPool example02FixedThreadPool = new Example02FixedThreadPool();
        example02FixedThreadPool.executeTasks(100_000);
    }

    @Test
    @Order(4)
    public void test_1_000_000_tasks() {
        Example02FixedThreadPool example02FixedThreadPool = new Example02FixedThreadPool();
        example02FixedThreadPool.executeTasks(1_000_000);
    }

}

我 PC 上的测试结果:

Example03VirtualThread.java

使用虚拟线程每任务执行器创建执行器:

var executor = Executors.newVirtualThreadPerTaskExecutor()
package threads;

import java.time.Duration;
import java.util.concurrent.ExecutorService;
import java.util.concurrent.Executors;
import java.util.stream.IntStream;

/**
 *
 * @author Milan Karajovic <milan.karajovic.rs@gmail.com>
 *
 */

public class Example03VirtualThread {

    public void executeTasks(final int NUMBER_OF_TASKS) {

        final int BLOCKING_CALL = 1;
        System.out.println("Number of tasks which executed using 'newVirtualThreadPerTaskExecutor()' " + NUMBER_OF_TASKS + " tasks each.");

        long startTime = System.currentTimeMillis();

        try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {

            IntStream.range(0, NUMBER_OF_TASKS).forEach(i -> {
               executor.submit(() -> {
                   // 模拟阻塞调用
                  Thread.sleep(Duration.ofSeconds(BLOCKING_CALL));
                  return i;
               });
            });

        }   catch (Exception e) {
            throw new RuntimeException(e);
        }

        long endTime = System.currentTimeMillis();
        System.out.println("For executing " + NUMBER_OF_TASKS + " tasks duration is: " + (endTime - startTime) + " ms");
    }

}
package threads;

import org.junit.jupiter.api.MethodOrderer;
import org.junit.jupiter.api.Order;
import org.junit.jupiter.api.Test;
import org.junit.jupiter.api.TestMethodOrder;

/**
 *
 * @author Milan Karajovic <milan.karajovic.rs@gmail.com>
 *
 */

@TestMethodOrder(MethodOrderer.OrderAnnotation.class)
public class Example03VirtualThreadTest {

    @Test
    @Order(1)
    public void test_1000_tasks() {
        Example03VirtualThread example03VirtualThread = new Example03VirtualThread();
        example03VirtualThread.executeTasks(1000);
    }

    @Test
    @Order(2)
    public void test_10_000_tasks() {
        Example03VirtualThread example03VirtualThread = new Example03VirtualThread();
        example03VirtualThread.executeTasks(10_000);
    }

    @Test
    @Order(3)
    public void test_100_000_tasks() {
        Example03VirtualThread example03VirtualThread = new Example03VirtualThread();
        example03VirtualThread.executeTasks(100_000);
    }

    @Test
    @Order(4)
    public void test_1_000_000_tasks() {
        Example03VirtualThread example03VirtualThread = new Example03VirtualThread();
        example03VirtualThread.executeTasks(1_000_000);
    }

    @Test
    @Order(5)
    public void test_2_000_000_tasks() {
        Example03VirtualThread example03VirtualThread = new Example03VirtualThread();
        example03VirtualThread.executeTasks(2_000_000);
    }

}

我 PC 上的测试结果:

结论

您可以清楚地看到用于处理所有 NUMBER_OF_TASKS 的不同执行器实现之间的执行时间差异。值得尝试不同的 NUMBER_OF_TASKS 值以观察性能变化。

虚拟线程的优势在处理大量任务时变得尤其明显。当 NUMBER_OF_TASKS 设置为较高的数值时——例如 1,000,000——性能差距是显著的。如下表所示,虚拟线程在处理大量任务时效率要高得多:

我确信,在澄清这一点之后,如果您的应用程序使用并发 API 处理大量任务,您会认真考虑迁移到 Java 21 并利用虚拟线程。在许多情况下,这种转变可以显著提高应用程序的性能和可扩展性。

源代码:GitHub Repository – Comparing Threads in Java 21


【注】本文译自:Java 21 Virtual Threads vs Cached and Fixed Threads

Java数据库应用原型

一个使用 Spring Boot 和容器进行测试、Keycloak 提供安全、PostgreSQL 提供数据持久化的,带有 REST 和安全功能的 Java 数据库应用原型。

在工作中开发时,我多次需要一个简单应用的模板,以便基于此模板开始为手头的项目添加特定代码。
在本文中,我将创建一个简单的 Java 应用程序,它连接到数据库,暴露一些 REST 端点,并使用基于角色的访问来保护这些端点。
目的是拥有一个最小化且功能齐全的应用程序,然后可以针对特定任务进行定制。
对于数据库,我们将使用 PostgreSQL;对于安全,我们将使用 Keycloak,两者都通过容器部署。在开发过程中,我使用 podman 来测试容器是否正确创建(作为 docker 的替代品——它们在大多数情况下可以互换)作为一次学习体验。
应用程序本身是使用 Spring Boot 框架开发的,并使用 Flyway 进行数据库版本管理。
所有这些技术都是 Java EE 领域业界标准,在项目中被使用的可能性很高。

我们构建原型的核心需求是一个图书馆应用程序,它暴露 REST 端点,允许创建作者、书籍以及它们之间的关系。这将使我们能够实现一个多对多关系,然后可以将其扩展用于任何可以想象的目的。
完整可用的应用程序可以在 https://github.com/ghalldev/db_proto 找到。
本文中的代码片段取自该代码库

在创建容器之前,请确保使用您偏好的值定义以下环境变量(教程中故意省略了它们,以避免传播多个用户使用的默认值):

DOCKER_POSTGRES_PASSWORD
DOCKER_KEYCLOAK_ADMIN_PASSWORD
DOCKER_GH_USER1_PASSWORD

配置 PostgreSQL:

docker container create --name gh_postgres --env POSTGRES_PASSWORD=$DOCKER_POSTGRES_PASSWORD --env POSTGRES_USER=gh_pguser --env POSTGRES_INITDB_ARGS=--auth=scram-sha-256 --publish 5432:5432 postgres:17.5-alpine3.22
docker container start gh_postgres

配置 Keycloak:
首先是容器的创建并启动:

docker container create --name gh_keycloak --env DOCKER_GH_USER1_PASSWORD=$DOCKER_GH_USER1_PASSWORD --env KC_BOOTSTRAP_ADMIN_USERNAME=gh_admin --env KC_BOOTSTRAP_ADMIN_PASSWORD=$DOCKER_KEYCLOAK_ADMIN_PASSWORD --publish 8080:8080 --publish 8443:8443 --publish 9000:9000 keycloak/keycloak:26.3 start-dev
docker container start gh_keycloak

在容器启动并运行后,我们可以继续创建领域、用户和角色(这些命令必须在正在运行的容器内部执行):

cd $HOME/bin
./kcadm.sh config credentials --server http://localhost:8080 --realm master --user gh_admin --password $KC_BOOTSTRAP_ADMIN_PASSWORD
./kcadm.sh create realms -s realm=gh_realm -s enabled=true
./kcadm.sh create users -s username=gh_user1 -s email="gh_user1@email.com" -s firstName="gh_user1firstName" -s lastName="gh_user1lastName" -s emailVerified=true -s enabled=true -r gh_realm
./kcadm.sh set-password -r gh_realm --username gh_user1 --new-password $DOCKER_GH_USER1_PASSWORD
./kcadm.sh create roles -r gh_realm -s name=viewer -s 'description=Realm role to be used for read-only features'
./kcadm.sh add-roles --uusername gh_user1 --rolename viewer -r gh_realm
./kcadm.sh create roles -r gh_realm -s name=creator -s 'description=Realm role to be used for create/update features'
./kcadm.sh add-roles --uusername gh_user1 --rolename creator -r gh_realm
ID_ACCOUNT_CONSOLE=$(./kcadm.sh get clients -r gh_realm --fields id,clientId | grep -B 1 '"clientId" : "account-console"' | grep -oP '[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}')
./kcadm.sh update clients/$ID_ACCOUNT_CONSOLE -r gh_realm -s 'fullScopeAllowed=true' -s 'directAccessGrantsEnabled=true'

用户 gh_user1 在领域 gh_realm 中被创建,并拥有 viewercreator 角色。

您可能已经注意到,我们没有创建新的客户端,而是使用了 Keycloak 自带的一个默认客户端:account-console。这是为了方便起见,在实际场景中,您会创建一个特定的客户端,然后将其更新为具有 fullScopeAllowed(这会导致领域角色被添加到令牌中——默认情况下不添加)和 directAccessGrantsEnabled(允许通过 Keycloak 的 openid-connect/token 端点生成令牌,在我们的例子中使用 curl)。

创建的角色随后可以在 Java 应用程序内部使用,以根据我们约定的契约来限制对某些功能的访问——viewer 只能访问只读操作,而 creator 可以执行创建、更新和删除操作。当然,同样地,可以根据任何原因创建各种角色,只要约定的契约被明确定义并被所有人理解。
角色还可以进一步添加到组中,但本教程不包含这部分内容。

但是,在实际使用这些角色之前,我们必须告诉 Java 应用程序如何提取角色——这是必须的,因为 Keycloak 将角色添加到 JWT 的方式是其特有的,所以我们必须编写一段自定义代码,将其转换为 Spring Security 可以使用的东西:

@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
    // 遵循与 org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter 相同的模式
    Converter<Jwt, Collection<GrantedAuthority>> keycloakRolesConverter = new Converter<>() {
        private static final String DEFAULT_AUTHORITY_PREFIX = "ROLE_";
        //https://github.com/keycloak/keycloak/blob/main/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java#L901
        private static final String KEYCLOAK_REALM_ACCESS_CLAIM_NAME = "realm_access";
        private static final String KEYCLOAK_REALM_ACCESS_ROLES = "roles";

        @Override
        public Collection<GrantedAuthority> convert(Jwt source) {
            Collection<GrantedAuthority> grantedAuthorities = new ArrayList<>();
            Map<String, List<String>> realmAccess = source.getClaim(KEYCLOAK_REALM_ACCESS_CLAIM_NAME);
            if (realmAccess == null) {
                logger.warn("No " + KEYCLOAK_REALM_ACCESS_CLAIM_NAME + " present in the JWT");
                return grantedAuthorities;
            }
            List<String> roles = realmAccess.get(KEYCLOAK_REALM_ACCESS_ROLES);
            if (roles == null) {
                logger.warn("No " + KEYCLOAK_REALM_ACCESS_ROLES + " present in the JWT");
                return grantedAuthorities;
            }
            roles.forEach(
                    role -> grantedAuthorities.add(new SimpleGrantedAuthority(DEFAULT_AUTHORITY_PREFIX + role)));

            return grantedAuthorities;
        }

    };
    JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
    jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(keycloakRolesConverter);

    return jwtAuthenticationConverter;
}

AppConfiguration 类中还完成了其他重要配置,例如启用方法安全性和禁用 CSRF。

现在我们可以在 REST 控制器中使用 @org.springframework.security.access.prepost.PreAuthorize 注解来限制访问:

@PostMapping("/author")
@PreAuthorize("hasRole('creator')")
public void addAuthor(@RequestParam String name, @RequestParam String address) {
  authorService.add(new AuthorDto(name, address));
}

@GetMapping("/author")
@PreAuthorize("hasRole('viewer')")
public String getAuthors() {
  return authorService.allInfo();
}

通过这种方式,只有成功通过身份验证且拥有 hasRole 中列出的角色的用户才能调用端点,否则他们将收到 HTTP 403 Forbidden 错误。

在容器启动并配置完成后,Java 应用程序可以启动了,但在启动之前需要添加数据库密码——这可以通过环境变量完成(下面是一个 Linux shell 示例):

export SPRING_DATASOURCE_PASSWORD=$DOCKER_POSTGRES_PASSWORD

现在,如果一切正常启动并运行,我们可以使用 curl 来测试我们的应用程序(以下所有命令均为 Linux shell 命令)。

使用之前创建的用户 gh_user1 登录并提取身份验证令牌:

KEYCLOAK_ACCESS_TOKEN=$(curl -d 'client_id=account-console' -d 'username=gh_user1' -d "password=$DOCKER_GH_USER1_PASSWORD" -d 'grant_type=password' 'http://localhost:8080/realms/gh_realm/protocol/openid-connect/token' | grep -oP '"access_token":"\K[^"]*')

创建一个新作者(这将测试 creator 角色是否有效):

curl -X POST --data-raw 'name="GH_name1"&address="GH_address1"' -H "Authorization: Bearer $KEYCLOAK_ACCESS_TOKEN" 'localhost:8090/library/author'

检索库中的所有作者(这将测试 viewer 角色是否有效):

curl -X GET -H "Authorization: Bearer $KEYCLOAK_ACCESS_TOKEN" 'localhost:8090/library/author'

至此,您应该拥有了创建自己的 Java 应用程序所需的一切,可以根据需要对其进行扩展和配置。


【注】本文译自:Java Spring Boot Template With PostgreSQL, Keycloak Securit

单体架构中的事件驱动架构:Java应用程序的渐进式重构

传统观点认为事件驱动架构属于微服务架构范畴,服务通过消息代理进行异步通信。然而,事件驱动模式一些最具价值的应用恰恰发生在单体应用程序内部——在这些地方,紧密耦合已造成维护噩梦,而渐进式重构则提供了一条通往更好架构的路径,且无需分布式系统的运维复杂性。

为何在单体应用中使用事件有意义

传统的分层单体应用存在一个特定问题:直接的方法调用在组件之间创建了僵化的依赖关系。您的订单处理代码直接调用库存管理,库存管理又调用仓库系统,继而触发电子邮件通知。每个组件都了解其他几个组件,从而形成一个纠缠不清的网,更改其中一部分需要理解并测试它所触及的所有内容。

事件驱动模式引入了间接性。当下单时,订单服务发布一个"OrderPlaced"事件。其他对订单感兴趣的组件——库存、发货、通知——订阅此事件并独立响应。订单服务不知道也不关心谁在监听。即使这些组件存在于同一个代码库并共享同一个数据库,它们也变得松散耦合。

这种方法提供了立竿见影的好处,而无需将应用程序拆分为微服务。您在保持单体应用运维简单性的同时,获得了可测试性、灵活性和更清晰的边界。当您最终需要提取服务时,事件驱动的结构使得过渡更加平滑,因为组件已经通过定义良好的消息进行通信,而不是直接的方法调用。

起点:一个紧密耦合的订单系统

考虑一个使用 Spring Boot 构建的典型电子商务单体应用。订单创建流程如下所示:

@Service
@Transactional
public class OrderService {
    private final OrderRepository orderRepository;
    private final InventoryService inventoryService;
    private final PaymentService paymentService;
    private final ShippingService shippingService;
    private final LoyaltyService loyaltyService;
    private final EmailService emailService;
    private final AnalyticsService analyticsService;

    public OrderService(
        OrderRepository orderRepository,
        InventoryService inventoryService,
        PaymentService paymentService,
        ShippingService shippingService,
        LoyaltyService loyaltyService,
        EmailService emailService,
        AnalyticsService analyticsService
    ) {
        this.orderRepository = orderRepository;
        this.inventoryService = inventoryService;
        this.paymentService = paymentService;
        this.shippingService = shippingService;
        this.loyaltyService = loyaltyService;
        this.emailService = emailService;
        this.analyticsService = analyticsService;
    }

    public Order createOrder(CreateOrderRequest request) {
        // 验证库存
        for (OrderItem item : request.getItems()) {
            if (!inventoryService.checkAvailability(item.getProductId(), item.getQuantity())) {
                throw new InsufficientInventoryException(item.getProductId());
            }
        }

        // 处理支付
        PaymentResult payment = paymentService.processPayment(
            request.getCustomerId(),
            calculateTotal(request.getItems()),
            request.getPaymentDetails()
        );

        if (!payment.isSuccessful()) {
            throw new PaymentFailedException(payment.getErrorMessage());
        }

        // 创建订单
        Order order = new Order(
            request.getCustomerId(),
            request.getItems(),
            payment.getTransactionId()
        );
        order.setStatus(OrderStatus.CONFIRMED);
        Order savedOrder = orderRepository.save(order);

        // 预留库存
        for (OrderItem item : request.getItems()) {
            inventoryService.reserveInventory(item.getProductId(), item.getQuantity());
        }

        // 创建发货单
        shippingService.createShipment(savedOrder);

        // 更新忠诚度积分
        loyaltyService.addPoints(
            request.getCustomerId(),
            calculateLoyaltyPoints(savedOrder)
        );

        // 发送确认邮件
        emailService.sendOrderConfirmation(savedOrder);

        // 跟踪分析
        analyticsService.trackOrderPlaced(savedOrder);

        return savedOrder;
    }
}

这段代码可以工作,但存在严重问题。OrderService 知道七个不同的服务。测试需要模拟所有这些服务。添加新的订单后操作意味着要修改此方法。如果电子邮件服务缓慢,订单创建就会变慢。如果分析跟踪失败,整个订单就会失败并回滚。

事务边界也是错误的。所有操作都在单个数据库事务中发生,这意味着即使电子邮件服务临时停机也会阻止订单创建。库存预留和发货单创建在事务上耦合,尽管它们在逻辑上是独立的操作。

引入 Spring 应用事件

Spring Framework 提供了一个内置的事件系统,在单个 JVM 内工作。默认情况下它是同步的,这使得它易于推理和调试。首先定义领域事件:

public abstract class DomainEvent {
    private final Instant occurredAt;
    private final String eventId;

    protected DomainEvent() {
        this.occurredAt = Instant.now();
        this.eventId = UUID.randomUUID().toString();
    }

    public Instant getOccurredAt() {
        return occurredAt;
    }

    public String getEventId() {
        return eventId;
    }
}

public class OrderPlacedEvent extends DomainEvent {
    private final Long orderId;
    private final Long customerId;
    private final List<OrderItem> items;
    private final BigDecimal totalAmount;

    public OrderPlacedEvent(Order order) {
        super();
        this.orderId = order.getId();
        this.customerId = order.getCustomerId();
        this.items = new ArrayList<>(order.getItems());
        this.totalAmount = order.getTotalAmount();
    }

    // Getters
}

事件应该是不可变的,并包含订阅者需要的所有信息。避免直接传递实体——而是复制相关数据。这可以防止订阅者意外修改共享状态。

重构 OrderService 以发布事件,而不是直接调用服务:

@Service
@Transactional
public class OrderService {
    private final OrderRepository orderRepository;
    private final InventoryService inventoryService;
    private final PaymentService paymentService;
    private final ApplicationEventPublisher eventPublisher;

    public OrderService(
        OrderRepository orderRepository,
        InventoryService inventoryService,
        PaymentService paymentService,
        ApplicationEventPublisher eventPublisher
    ) {
        this.orderRepository = orderRepository;
        this.inventoryService = inventoryService;
        this.paymentService = paymentService;
        this.eventPublisher = eventPublisher;
    }

    public Order createOrder(CreateOrderRequest request) {
        // 验证库存
        for (OrderItem item : request.getItems()) {
            if (!inventoryService.checkAvailability(item.getProductId(), item.getQuantity())) {
                throw new InsufficientInventoryException(item.getProductId());
            }
        }

        // 处理支付
        PaymentResult payment = paymentService.processPayment(
            request.getCustomerId(),
            calculateTotal(request.getItems()),
            request.getPaymentDetails()
        );

        if (!payment.isSuccessful()) {
            throw new PaymentFailedException(payment.getErrorMessage());
        }

        // 创建并保存订单
        Order order = new Order(
            request.getCustomerId(),
            request.getItems(),
            payment.getTransactionId()
        );
        order.setStatus(OrderStatus.CONFIRMED);
        Order savedOrder = orderRepository.save(order);

        // 同步预留库存(仍在关键路径上)
        for (OrderItem item : request.getItems()) {
            inventoryService.reserveInventory(item.getProductId(), item.getQuantity());
        }

        // 为非关键操作发布事件
        eventPublisher.publishEvent(new OrderPlacedEvent(savedOrder));

        return savedOrder;
    }
}

现在 OrderService 仅依赖四个组件,而不是八个。更重要的是,它只了解对订单创建至关重要的操作——库存验证、支付处理和库存预留。其他所有操作都通过事件发生。

为解耦的操作创建事件监听器:

@Component
public class OrderEventListeners {
    private static final Logger logger = LoggerFactory.getLogger(OrderEventListeners.class);

    private final ShippingService shippingService;
    private final LoyaltyService loyaltyService;
    private final EmailService emailService;
    private final AnalyticsService analyticsService;

    public OrderEventListeners(
        ShippingService shippingService,
        LoyaltyService loyaltyService,
        EmailService emailService,
        AnalyticsService analyticsService
    ) {
        this.shippingService = shippingService;
        this.loyaltyService = loyaltyService;
        this.emailService = emailService;
        this.analyticsService = analyticsService;
    }

    @EventListener
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void handleOrderPlaced(OrderPlacedEvent event) {
        try {
            shippingService.createShipment(event.getOrderId());
        } catch (Exception e) {
            logger.error("Failed to create shipment for order {}", event.getOrderId(), e);
            // 不要重新抛出 - 其他监听器仍应执行
        }
    }

    @EventListener
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void updateLoyaltyPoints(OrderPlacedEvent event) {
        try {
            int points = calculatePoints(event.getTotalAmount());
            loyaltyService.addPoints(event.getCustomerId(), points);
        } catch (Exception e) {
            logger.error("Failed to update loyalty points for order {}", event.getOrderId(), e);
        }
    }

    @EventListener
    public void sendConfirmationEmail(OrderPlacedEvent event) {
        try {
            emailService.sendOrderConfirmation(event.getOrderId());
        } catch (Exception e) {
            logger.error("Failed to send confirmation email for order {}", event.getOrderId(), e);
        }
    }

    @EventListener
    public void trackAnalytics(OrderPlacedEvent event) {
        try {
            analyticsService.trackOrderPlaced(event.getOrderId(), event.getTotalAmount());
        } catch (Exception e) {
            logger.error("Failed to track analytics for order {}", event.getOrderId(), e);
        }
    }
}

每个监听器在它自己的事务中运行(在适当的时候)并独立处理故障。如果发送电子邮件失败,发货单创建仍然会发生。即使分析跟踪抛出异常,订单创建事务也会成功提交。

理解事务边界

@Transactional(propagation = Propagation.REQUIRES_NEW) 注解至关重要。没有它,所有监听器都会参与订单创建事务。如果任何监听器失败,整个订单都会回滚——这正是我们试图避免的情况。

使用 REQUIRES_NEW,每个监听器都会启动一个新的事务。当监听器运行时,订单已经提交。这意味着:

  • 监听器无法阻止订单创建
  • 监听器故障不会回滚订单
  • 每个监听器的工作是独立原子性的

但这有一个权衡。如果监听器失败,订单存在但某些后处理没有发生。您需要处理这些部分故障的策略:

@EventListener
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void handleOrderPlaced(OrderPlacedEvent event) {
    try {
        shippingService.createShipment(event.getOrderId());
    } catch (Exception e) {
        logger.error("Failed to create shipment for order {}", event.getOrderId(), e);

        // 记录失败以便重试
        failedEventRepository.save(new FailedEvent(
            event.getClass().getSimpleName(),
            event.getEventId(),
            "handleOrderPlaced",
            e.getMessage()
        ));
    }
}

一个单独的后台作业可以重试失败的事件:

@Component
public class FailedEventRetryJob {
    private final FailedEventRepository failedEventRepository;
    private final ApplicationEventPublisher eventPublisher;

    @Scheduled(fixedDelay = 60000) // 每分钟
    public void retryFailedEvents() {
        List failures = failedEventRepository.findRetryable();

        for (FailedEvent failure : failures) {
            try {
                // 重建并重新发布事件
                DomainEvent event = reconstructEvent(failure);
                eventPublisher.publishEvent(event);

                failure.markRetried();
                failedEventRepository.save(failure);
            } catch (Exception e) {
                logger.warn("Retry failed for event {}", failure.getEventId(), e);
                failure.incrementRetryCount();
                failedEventRepository.save(failure);
            }
        }
    }
}

这种模式提供了最终一致性——系统可能暂时不一致,但通过重试自行恢复。

转向异步事件

Spring 的 @EventListener 默认是同步的。事件处理发生在发布事件的同一线程中,发布者等待所有监听器完成。这提供了强有力的保证,但限制了可扩展性。

通过启用异步支持并注解监听器来使监听器异步:

@Configuration
@EnableAsync
public class AsyncConfig {
    @Bean(name = "eventExecutor")
    public Executor eventExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(4);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(100);
        executor.setThreadNamePrefix("event-");
        executor.initialize();
        return executor;
    }
}

@Component
public class OrderEventListeners {
    // ... 依赖 ...

    @Async("eventExecutor")
    @EventListener
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void handleOrderPlaced(OrderPlacedEvent event) {
        shippingService.createShipment(event.getOrderId());
    }

    @Async("eventExecutor")
    @EventListener
    public void sendConfirmationEmail(OrderPlacedEvent event) {
        emailService.sendOrderConfirmation(event.getOrderId());
    }
}

使用 @AsynccreateOrder() 方法在发布事件后立即返回。监听器在线程池中并发执行。这显著提高了响应时间——订单创建不再等待电子邮件发送或分析跟踪。

但异步事件引入了新的复杂性。当监听器执行时,订单创建事务可能尚未提交。监听器可能尝试从数据库加载订单,但由于事务仍在进行中而找不到它。

Spring 提供了 @TransactionalEventListener 来处理这种情况:

@Component
public class OrderEventListeners {
    @Async("eventExecutor")
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void handleOrderPlaced(OrderPlacedEvent event) {
        // 这仅在订单创建事务成功提交后运行
        shippingService.createShipment(event.getOrderId());
    }
}

AFTER_COMMIT 阶段确保监听器仅在发布事务成功提交后运行。如果订单创建失败并回滚,监听器永远不会执行。这可以防止处理实际上不存在的订单的事件。

实现事件存储

随着事件驱动架构的成熟,存储事件变得有价值。事件存储提供了审计日志,支持调试,并支持更复杂的模式,如事件溯源。

创建一个简单的事件存储:

@Entity
@Table(name = "domain_events")
public class StoredEvent {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String eventId;

    @Column(nullable = false)
    private String eventType;

    @Column(nullable = false, columnDefinition = "TEXT")
    private String payload;

    @Column(nullable = false)
    private Instant occurredAt;

    @Column(nullable = false)
    private Instant storedAt;

    @Column
    private String aggregateId;

    @Column
    private String aggregateType;

    // 构造器、getter、setter
}

@Repository
public interface StoredEventRepository extends JpaRepository<StoredEvent, Long> {
    List<StoredEvent> findByAggregateIdOrderByOccurredAt(String aggregateId);
    List<StoredEvent> findByEventType(String eventType);
}

拦截并存储所有领域事件:

@Component
public class EventStoreListener {
    private final StoredEventRepository repository;
    private final ObjectMapper objectMapper;

    public EventStoreListener(StoredEventRepository repository, ObjectMapper objectMapper) {
        this.repository = repository;
        this.objectMapper = objectMapper;
    }

    @EventListener
    @Order(Ordered.HIGHEST_PRECEDENCE) // 在其他监听器之前存储
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void storeEvent(DomainEvent event) {
        try {
            StoredEvent stored = new StoredEvent();
            stored.setEventId(event.getEventId());
            stored.setEventType(event.getClass().getSimpleName());
            stored.setPayload(objectMapper.writeValueAsString(event));
            stored.setOccurredAt(event.getOccurredAt());
            stored.setStoredAt(Instant.now());

            // 如果可用,提取聚合信息
            if (event instanceof OrderPlacedEvent) {
                OrderPlacedEvent orderEvent = (OrderPlacedEvent) event;
                stored.setAggregateId(orderEvent.getOrderId().toString());
                stored.setAggregateType("Order");
            }

            repository.save(stored);
        } catch (JsonProcessingException e) {
            throw new EventStoreException("Failed to serialize event", e);
        }
    }
}

现在,每个领域事件在业务逻辑处理之前都会持久化。您可以通过重放事件来重建系统中发生的情况:

@Service
public class OrderHistoryService {
    private final StoredEventRepository eventRepository;

    public List<OrderEvent> getOrderHistory(Long orderId) {
        List<StoredEvent> events = eventRepository.findByAggregateIdOrderByOccurredAt(
            orderId.toString()
        );

        return events.stream()
            .map(this::deserializeEvent)
            .collect(Collectors.toList());
    }

    private OrderEvent deserializeEvent(StoredEvent stored) {
        // 根据事件类型反序列化
        try {
            Class<?> eventClass = Class.forName("com.example.events." + stored.getEventType());
            return (OrderEvent) objectMapper.readValue(stored.getPayload(), eventClass);
        } catch (Exception e) {
            throw new EventStoreException("Failed to deserialize event", e);
        }
    }
}

这实现了强大的调试能力。当客户报告其订单问题时,您可以准确看到发生了什么事件以及发生的顺序。

Saga 和补偿操作

某些工作流需要跨多个步骤进行协调,其中每个步骤都可能失败。传统方法使用分布式事务,但这些方法扩展性不佳且增加了复杂性。Saga 使用编排事件和补偿操作提供了一种替代方案。

考虑一个更复杂的订单流程,您需要:

  1. 预留库存
  2. 处理支付
  3. 创建发货单

如果在预留库存后支付失败,您需要释放预留。通过补偿事件实现这一点:

public class InventoryReservedEvent extends DomainEvent {
    private final Long orderId;
    private final List<ReservationDetail> reservations;

    // 构造器、getter
}

public class PaymentFailedEvent extends DomainEvent {
    private final Long orderId;
    private final String reason;

    // 构造器、getter
}

@Component
public class InventorySagaHandler {
    private final InventoryService inventoryService;

    @EventListener
    public void handlePaymentFailed(PaymentFailedEvent event) {
        // 补偿操作:释放预留库存
        inventoryService.releaseReservation(event.getOrderId());
    }
}

Saga 通过事件而不是中央协调器进行协调:

@Service
public class OrderSagaService {
    private final ApplicationEventPublisher eventPublisher;
    private final InventoryService inventoryService;
    private final PaymentService paymentService;

    public void processOrder(Order order) {
        // 步骤 1: 预留库存
        List<ReservationDetail> reservations = inventoryService.reserve(order.getItems());
        eventPublisher.publishEvent(new InventoryReservedEvent(order.getId(), reservations));

        try {
            // 步骤 2: 处理支付
            PaymentResult payment = paymentService.processPayment(order);

            if (payment.isSuccessful()) {
                eventPublisher.publishEvent(new PaymentSucceededEvent(order.getId(), payment));
            } else {
                // 触发补偿
                eventPublisher.publishEvent(new PaymentFailedEvent(order.getId(), payment.getReason()));
                throw new PaymentException(payment.getReason());
            }
        } catch (Exception e) {
            // 触发补偿
            eventPublisher.publishEvent(new PaymentFailedEvent(order.getId(), e.getMessage()));
            throw e;
        }
    }
}

这种模式在没有分布式事务的情况下保持了一致性。每个步骤发布记录所发生事件的事件。当发生故障时,补偿事件会触发撤销先前步骤的操作。

桥接到外部消息代理

随着单体应用的增长,您可能希望与外部系统集成或为最终的服务提取做准备。Spring Cloud Stream 提供了对 RabbitMQ 或 Kafka 等消息代理的抽象,同时保持相同的事件驱动模式:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-stream</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-stream-binder-kafka</artifactId>
</dependency>

application.yml 中配置绑定:

spring:
  cloud:
    stream:
      bindings:
        orderPlaced-out-0:
          destination: order.placed
        orderPlaced-in-0:
          destination: order.placed
          group: order-processors
      kafka:
        binder:
          brokers: localhost:9092

创建内部事件和外部消息之间的桥接:

@Component
public class EventPublisher {
    private final StreamBridge streamBridge;

    public EventPublisher(StreamBridge streamBridge) {
        this.streamBridge = streamBridge;
    }

    @EventListener
    public void publishToExternalBroker(OrderPlacedEvent event) {
        // 将内部事件发布到外部消息代理
        streamBridge.send("orderPlaced-out-0", event);
    }
}

@Component
public class ExternalEventConsumer {
    private final ApplicationEventPublisher eventPublisher;

    public ExternalEventConsumer(ApplicationEventPublisher eventPublisher) {
        this.eventPublisher = eventPublisher;
    }

    @Bean
    public Consumer<OrderPlacedEvent> orderPlaced() {
        return event -> {
            // 将外部事件重新发布为内部事件
            eventPublisher.publishEvent(event);
        };
    }
}

这种模式让您可以选择性地将事件发布到外部,同时将内部事件保留在本地。关键的实时操作使用内部事件以实现低延迟。跨服务通信使用消息代理以实现可靠性和可扩展性。

监控与可观测性

事件驱动系统引入了新的可观测性挑战。理解正在发生的情况需要跨多个异步处理步骤跟踪事件。实施全面的日志记录和指标:

@Aspect
@Component
public class EventMonitoringAspect {
    private static final Logger logger = LoggerFactory.getLogger(EventMonitoringAspect.class);
    private final MeterRegistry meterRegistry;

    public EventMonitoringAspect(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
    }

    @Around("@annotation(org.springframework.context.event.EventListener)")
    public Object monitorEventListener(ProceedingJoinPoint joinPoint) throws Throwable {
        String listenerName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        DomainEvent event = (DomainEvent) args[0];

        Timer.Sample sample = Timer.start(meterRegistry);

        try {
            logger.info("Processing event {} in listener {}", 
                event.getEventId(), listenerName);

            Object result = joinPoint.proceed();

            sample.stop(Timer.builder("event.listener.duration")
                .tag("listener", listenerName)
                .tag("event_type", event.getClass().getSimpleName())
                .tag("status", "success")
                .register(meterRegistry));

            meterRegistry.counter("event.listener.processed",
                "listener", listenerName,
                "event_type", event.getClass().getSimpleName(),
                "status", "success"
            ).increment();

            return result;
        } catch (Exception e) {
            sample.stop(Timer.builder("event.listener.duration")
                .tag("listener", listenerName)
                .tag("event_type", event.getClass().getSimpleName())
                .tag("status", "failure")
                .register(meterRegistry));

            meterRegistry.counter("event.listener.processed",
                "listener", listenerName,
                "event_type", event.getClass().getSimpleName(),
                "status", "failure"
            ).increment();

            logger.error("Error processing event {} in listener {}", 
                event.getEventId(), listenerName, e);

            throw e;
        }
    }
}

这个切面自动跟踪每个事件监听器的执行时间和成功率。结合 Prometheus 和 Grafana 等工具,您可以可视化事件处理模式并识别瓶颈。

添加关联 ID 以跟踪系统中的事件:

public abstract class DomainEvent {
    private final Instant occurredAt;
    private final String eventId;
    private final String correlationId;

    protected DomainEvent(String correlationId) {
        this.occurredAt = Instant.now();
        this.eventId = UUID.randomUUID().toString();
        this.correlationId = correlationId != null ? correlationId : UUID.randomUUID().toString();
    }

    // Getters
}

通过事件链传播关联 ID:

@EventListener
public void handleOrderPlaced(OrderPlacedEvent event) {
    MDC.put("correlationId", event.getCorrelationId());

    try {
        // 执行工作

        // 发布具有相同关联 ID 的后续事件
        eventPublisher.publishEvent(new ShipmentCreatedEvent(
            event.getOrderId(),
            event.getCorrelationId()
        ));
    } finally {
        MDC.clear();
    }
}

现在,与单个订单流相关的所有日志消息共享一个关联 ID,使得跨多个异步操作跟踪整个工作流变得微不足道。

测试事件驱动代码

事件驱动架构需要不同的测试策略。传统的单元测试适用于单个监听器,但集成测试对于验证事件流变得更加重要:

@SpringBootTest
@TestConfiguration
public class OrderEventIntegrationTest {
    @Autowired
    private ApplicationEventPublisher eventPublisher;

    @Autowired
    private ShippingService shippingService;

    @Autowired
    private EmailService emailService;

    @Test
    public void shouldProcessOrderPlacedEventCompletely() throws Exception {
        // 给定
        Order order = createTestOrder();
        OrderPlacedEvent event = new OrderPlacedEvent(order);

        // 当
        eventPublisher.publishEvent(event);

        // 等待异步处理
        await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> {
            // 然后
            verify(shippingService).createShipment(order.getId());
            verify(emailService).sendOrderConfirmation(order.getId());
        });
    }
}

对于单元测试,注入一个间谍事件发布器以验证事件是否正确发布:

@ExtendWith(MockitoExtension.class)
public class OrderServiceTest {
    @Mock
    private OrderRepository orderRepository;

    @Mock
    private InventoryService inventoryService;

    @Mock
    private PaymentService paymentService;

    @Spy
    private ApplicationEventPublisher eventPublisher = new SimpleApplicationEventPublisher();

    @InjectMocks
    private OrderService orderService;

    @Test
    public void shouldPublishOrderPlacedEventAfterCreatingOrder() {
        // 给定
        CreateOrderRequest request = createValidRequest();

        when(inventoryService.checkAvailability(any(), anyInt())).thenReturn(true);
        when(paymentService.processPayment(any(), any(), any()))
            .thenReturn(PaymentResult.successful("txn-123"));
        when(orderRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        // 当
        orderService.createOrder(request);

        // 然后
        verify(eventPublisher).publishEvent(argThat(event -> 
            event instanceof OrderPlacedEvent
        ));
    }
}

迁移之旅

将单体应用重构为使用事件驱动架构并非全有或全无的命题。从一个工作流开始——通常是造成最多痛苦的那个。识别可以事件驱动的直接服务调用,并逐步引入事件。

从同步事件开始,以最小化行为变更。一旦事件正确流动,为非关键监听器切换到异步处理。当您需要审计跟踪或调试能力时,添加事件存储。仅当您需要跨服务通信或准备提取微服务时,才集成外部消息代理。

目标不是实现完美的事件驱动架构。而是减少耦合、提高可测试性,并在组件之间创建更清晰的边界。即使是部分采用也能提供价值——具有一些事件驱动模式的单体应用比完全没有的模式更易于维护。

这种渐进式方法使您能够持续交付价值,而不是投入一个需要数月时间、直到完全结束时才能交付任何成果的重构项目。您能够了解在特定领域和团队中哪些方法有效,根据实际经验而非理论理想来调整实施策略。

有用的资源


【注】本文译自: Event-Driven Architecture in Monoliths: Incremental Refactoring for Java Apps – Java Code Geeks