构建复合AI系统以实现可扩展工作流

了解如何利用复合AI系统架构化模块化且安全的智能体工作流,以实现可扩展的企业自动化。

生成式AI、大语言模型和多智能体编排的融合催生了一个变革性的概念:复合AI系统。这些架构超越了单个模型或助手,代表了智能代理的生态系统,它们通过协作来大规模交付业务成果。随着企业追求超自动化、持续优化和个性化参与,设计智能体工作流已成为关键的差异化因素。

本文探讨复合AI系统的设计,重点聚焦模块化AI代理、安全编排、实时数据集成和企业治理。旨在为解决方案架构师、工程领导者和数字化转型高管提供一个实用的蓝图,用于在各个领域(包括客户服务、IT运营、营销和现场自动化)构建和扩展智能代理生态系统。

复合AI的兴起

传统的AI应用通常是孤立的,一个机器人专用于服务,另一个专注于分析,还有一个用于营销。然而,真实世界的工作流是相互关联的,需要共享上下文、移交意图并进行自适应协作。复合AI系统通过以下方式解决这一问题:

  • 启用自主但协作的代理(例如,规划器、检索器、执行器)
  • 促进多模态交互(文本、语音、事件)
  • 支持企业级的可解释性、隐私和控制指南

这反映了复杂系统在人类组织中的运作方式:每个单元(代理)都有其角色,但它们共同创造了一个价值链。

企业级智能体工作流的设计原则

设计有效的复合AI系统需要深思熟虑的方法,以确保模块化、可扩展性并与企业目标保持一致。以下是指导智能体工作流开发的关键原则:

1. 模块化代理设计

每个AI代理都应遵循单一职责原则,设计为具有特定、明确界定的职责。这种模块化使维护、测试和可扩展性变得更加容易。例如:

  • 规划器代理:将总体目标分解为可管理的子任务。
  • 检索器代理:从不同来源检索和收集相关数据。
  • 执行器代理:根据规划器的指令执行操作。
  • 评估器代理:评估结果并提供反馈以持续改进。

通过明确定义职责,代理可以独立运作,同时在系统内协同工作。

2. 事件驱动和以意图为中心的架构

从静态的、同步的工作流转向动态的、事件驱动的架构,可增强响应能力和适应性。实施以意图为中心的设计使系统能够有效解释用户或系统意图并据此行动。关键组件包括:

  • 意图路由器:对意图进行分类并将其引导至相应的代理。
  • 事件代理:通过事件消息促进代理之间的通信。
  • 记忆模块:随时间推移保存上下文,使代理能够基于历史数据做出明智决策。

这种架构实现了可扩展性和弹性,这对企业环境至关重要。

3. 企业数据集成与检索增强生成

集成结构化和非结构化数据源可确保AI代理在全面的上下文中运行。利用检索增强生成技术使代理能够访问外部知识库,从而提高其决策能力。策略包括:

  • 数据连接器:创建与企业数据库和API的安全连接。
  • 向量数据库:增强语义搜索和相关信息的检索。
  • 知识图谱:提供数据实体之间关系的结构化表示。

这种集成确保了代理信息灵通、具有上下文意识,并能提供准确的结果。

4. 安全与治理框架

确保智能体系统的安全性和合规性至关重要。实施强大的治理框架有助于维持信任和问责制。关键实践包括:

  • 访问控制:建立并强制执行数据和代理交互的权限。
  • 审计追踪:记录代理活动以实现透明度和合规性。
  • 合规性检查:根据GDPR和HIPAA等监管标准定期评估系统。

结构良好的治理模型可以防范风险,并确保AI的合乎道德的部署。

5. 可观测性与持续监控

实施可观测性实践能够实时监控和诊断代理行为及系统性能。关键组件包括:

  • 日志记录:记录代理行动和决策的全面日志。
  • 指标收集:收集性能指标,如响应时间和错误率。
  • 警报系统:及时向利益相关者通知异常或系统故障。

持续监控允许进行主动维护和持续改进。

6. 人在回路机制

纳入人工监督可确保AI代理在可接受的范围内运行,并适应细微的场景。HITL方法包括:

  • 审批工作流:确保关键决策或行动得到人工验证。
  • 反馈循环:使用户能够就代理性能提供输入,指导其未来行为。
  • 干预协议:允许人员在必要时修改或调整代理行动。

平衡自动化与人工判断可增强系统可靠性并建立用户信任。

7. 可扩展性与性能优化

设计能够有效扩展以处理不断增长工作负载的系统至关重要。实现这一目标的策略包括:

  • 负载均衡:在代理和资源之间均匀分配工作负载。
  • 异步处理:使代理能够独立运行,最大限度地减少瓶颈。
  • 资源管理:有效监控和分配计算资源以维持性能。

针对可扩展性进行优化可确保系统在需求增加时保持响应能力和有效性。

通过遵循这些设计原则,企业可以创建稳健、高效、可靠的智能体工作流,这些工作流既符合组织目标,又能适应不断变化的挑战。

实际应用案例:现场服务代理网格

场景:一家公用事业组织可以利用三个专门的AI代理来增强现场响应操作:

  • 规划器代理:评估收到的用户投诉并制定解决计划。
  • 检索器代理:获取资产位置、历史工单数据和合规性检查清单。
  • 执行器代理:安排技术人员并向移动服务团队发送警报。

影响:提高任务分配效率、缩短解决周期并提高技术人员生产率。

结论

复合AI系统正在通过促进智能、适应性强且可扩展的工作流来改变企业架构。设计模块化、可编排的智能体系统有助于组织:

  • 加速AI驱动的转型
  • 增强运营弹性和灵活性
  • 为客户和员工体验提供更好的结果

未来在于从孤立的AI任务转向复合的代理生态系统,这是一种将创新与强大治理及领域相关性相结合的战略。


【注】本文译自:Architecting Compound AI Systems for Scalable Workflows

Spring Boot WebSocket:使用 Java 构建多频道聊天系统

这是一个使用 WebFlux 和 MongoDB 构建响应式 Spring Boot WebSocket 聊天的分步指南,包括配置、处理程序和手动测试。


正如您可能已经从标题中猜到的,今天的主题将是 Spring Boot WebSockets。不久前,我提供了一个基于 Akka 工具包库的 WebSocket 聊天示例。然而,这个聊天将拥有更多一些功能,以及一个相当不同的设计。

我将跳过某些部分,以避免与上一篇文章的内容有太多重复。在这里您可以找到关于 WebSockets 更深入的介绍。请注意,本文中使用的所有代码也可以在 GitHub 仓库中找到。

Spring Boot WebSocket:使用的工具

让我们从描述将用于实现整个应用程序的工具开始本文的技术部分。由于我无法完全掌握如何使用经典的 Spring STOMP 覆盖来构建真正的 WebSocket API,我决定选择 Spring WebFlux 并使一切具有响应式特性。

  • Spring Boot – 基于 Spring 的现代 Java 应用程序离不开 Spring Boot;所有的自动配置都是无价的。
  • Spring WebFlux – 经典 Spring 的响应式版本,为处理 WebSocket 和 REST 提供了相当不错且描述性的工具集。我敢说,这是在 Spring 中实际获得 WebSocket 支持的唯一方法。
  • Mongo – 最流行的 NoSQL 数据库之一,我使用它来存储消息历史记录。
  • Spring Reactive Mongo – 用于以响应式方式处理 Mongo 访问的 Spring Boot 启动器。在一个地方使用响应式而在另一个地方不使用并不是最好的主意。因此,我决定也让数据库访问具有响应式特性。

让我们开始实现吧!

Spring Boot WebSocket:实现

依赖项与配置

pom.xml

<dependencies>
    <!--编译时依赖-->
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-webflux</artifactId>
    </dependency>
    <dependency>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-data-mongodb-reactive</artifactId>
    </dependency>
</dependencies>

application.properties

spring.data.mongodb.uri=mongodb://chats-admin:admin@localhost:27017/chats

我更喜欢 .properties 而不是 .yml——依我拙见,YAML 在较大规模上不可读且难以维护。

WebSocketConfig

@Configuration
class WebSocketConfig {

    @Bean
    ChatStore chatStore(MessagesStore messagesStore) {
        return new DefaultChatStore(Clock.systemUTC(), messagesStore);
    }

    @Bean
    WebSocketHandler chatsHandler(ChatStore chatStore) {
        return new ChatsHandler(chatStore);
    }

    @Bean
    SimpleUrlHandlerMapping handlerMapping(WebSocketHandler wsh) {
        Map<String, WebSocketHandler> paths = Map.of("/chats/{id}", wsh);
        return new SimpleUrlHandlerMapping(paths, 1);
    }

    @Bean
    WebSocketHandlerAdapter webSocketHandlerAdapter() {
        return new WebSocketHandlerAdapter();
    }
}

出乎意料的是,这里定义的四个 Bean 都非常重要。

  • ChatStore – 用于操作聊天的自定义 Bean,我将在后续步骤中详细介绍这个 Bean。
  • WebSocketHandler – 将存储所有与处理 WebSocket 会话相关逻辑的 Bean。
  • SimpleUrlHandlerMapping – 负责将 URL 映射到正确的处理器,此处理的完整 URL 看起来大致像这样:ws://localhost:8080/chats/{id}
  • WebSocketHandlerAdapter – 一种功能性的 Bean,它为 Spring Dispatcher Servlet 添加了 WebSocket 处理支持。

ChatsHandler

class ChatsHandler implements WebSocketHandler {

    private final Logger log = LoggerFactory.getLogger(ChatsHandler.class);

    private final ChatStore store;

    ChatsHandler(ChatStore store) {
        this.store = store;
    }

    @Override
    public Mono<Void> handle(WebSocketSession session) {
        String[] split = session.getHandshakeInfo()
            .getUri()
            .getPath()
            .split("/");
        String chatIdStr = split[split.length - 1];
        int chatId = Integer.parseInt(chatIdStr);
        ChatMeta chatMeta = store.get(chatId);
        if (chatMeta == null) {
            return session.close(CloseStatus.GOING_AWAY);
        }
        if (!chatMeta.canAddUser()) {
            return session.close(CloseStatus.NOT_ACCEPTABLE);
        }

        String sessionId = session.getId();
        store.addNewUser(chatId, session);
        log.info("New User {} join the chat {}", sessionId, chatId);
        return session
               .receive()
               .map(WebSocketMessage::getPayloadAsText)
               .flatMap(message -> store.addNewMessage(chatId, sessionId, message))
               .flatMap(message -> broadcastToSessions(sessionId, message, store.get(chatId).sessions()))
               .doFinally(sig -> store.removeSession(chatId, session.getId()))
               .then();
    }

    private Mono<Void> broadcastToSessions(String sessionId, String message, List<WebSocketSession> sessions) {
        return Flux.fromStream(sessions
                .stream()
                .filter(session -> !session.getId().equals(sessionId))
                .map(session -> session.send(Mono.just(session.textMessage(message)))))
                .then();
    }
}

正如我上面提到的,在这里您可以找到所有与处理 WebSocket 会话相关的逻辑。首先,我们从 URL 解析聊天的 ID 以获取目标聊天。根据特定聊天的上下文,响应不同的状态。

此外,我还将消息广播到与特定聊天相关的所有会话——以便用户实际交换消息。我还添加了 doFinally 触发器,它将从 chatStore 中清除已关闭的会话,以减少冗余通信。总的来说,这段代码是响应式的;我需要遵循一些限制。我试图使其尽可能简单和可读,如果您有任何改进的想法,我持开放态度。

ChatsRouter

@Configuration(proxyBeanMethods = false)
class ChatRouter {

    private final ChatStore chatStore;

    ChatRouter(ChatStore chatStore) {
        this.chatStore = chatStore;
    }

    @Bean
    RouterFunction<ServerResponse> routes() {
        return RouterFunctions
        .route(POST("api/v1/chats/create"), e -> create(false))
        .andRoute(POST("api/v1/chats/create-f2f"), e -> create(true))
        .andRoute(GET("api/v1/chats/{id}"), this::get)
        .andRoute(DELETE("api/v1/chats/{id}"), this::delete);
    }
}

WebFlux 定义 REST 端点的方法与经典 Spring 有很大不同。上面,您可以看到用于管理聊天的 4 个端点的定义。与 Akka 实现中的情况类似,我希望有一个用于管理聊天的 REST API 和一个用于实际处理聊天的 WebSocket API。我将跳过函数实现,因为它们非常简单;您可以在 GitHub 上查看它们。

ChatStore

首先,接口:

public interface ChatStore {
    int create(boolean isF2F);
    void addNewUser(int id, WebSocketSession session);
    Mono<String> addNewMessage(int id, String userId, String message);
    void removeSession(int id, String session);
    ChatMeta get(int id);
    ChatMeta delete(int id);
}

然后是实现:

public class DefaultChatStore implements ChatStore {

    private final Map<Integer, ChatMeta> chats;
    private final AtomicInteger idGen;
    private final MessagesStore messagesStore;
    private final Clock clock;

    public DefaultChatStore(Clock clock, MessagesStore store) {
        this.chats = new ConcurrentHashMap<>();
        this.idGen = new AtomicInteger(0);
        this.clock = clock;
        this.messagesStore = store;
    }

    @Override
    public int create(boolean isF2F) {
        int newId = idGen.incrementAndGet();
        ChatMeta chatMeta = chats.computeIfAbsent(newId, id -> {
            if (isF2F) {
                return ChatMeta.ofId(id);
            }
            return ChatMeta.ofIdF2F(id);
        });
        return chatMeta.id;
    }

    @Override
    public void addNewUser(int id, WebSocketSession session) {
        chats.computeIfPresent(id, (k, v) -> v.addUser(session));
    }

    @Override
    public void removeSession(int id, String sessionId) {
        chats.computeIfPresent(id, (k, v) -> v.removeUser(sessionId));
    }

    @Override
    public Mono<String> addNewMessage(int id, String userId, String message) {
        ChatMeta meta = chats.getOrDefault(id, null);
        if (meta != null) {
            Message messageDoc = new Message(id, userId, meta.offset.getAndIncrement(), clock.instant(), message);
            return messagesStore.save(messageDoc)
                    .map(Message::getContent);
        }
        return Mono.empty();
    }
    // 省略部分
}

ChatStore 的基础是 ConcurrentHashMap,它保存所有开放聊天的元数据。接口中的大多数方法都不言自明,背后没有什么特别之处。

  • create – 创建一个新聊天,带有一个布尔属性,指示聊天是 f2f 还是群聊。
  • addNewUser – 向现有聊天添加新用户。
  • removeUser – 从现有聊天中移除用户。
  • get – 获取具有 ID 的聊天的元数据。
  • delete – 从 CMH 中删除聊天。

这里唯一复杂的方法是 addNewMessages。它增加聊天内的消息计数器,并将消息内容持久化到 MongoDB 中,以实现持久性。

MongoDB

消息实体

public class Message {
   @Id
   private String id;
   private int chatId;
   private String owner;
   private long offset;
   private Instant timestamp;
   private String content;
}

存储在数据库中的消息内容模型,这里有三个重要的字段:

  1. chatId – 表示发送特定消息的聊天。
  2. ownerId – 消息发送者的用户 ID。
  3. offset – 消息在聊天中的序号,用于检索排序。

MessageStore

public interface MessagesStore extends ReactiveMongoRepository<Message, String> {}

没什么特别的,经典的 Spring 仓库,但是以响应式方式实现,提供了与 JpaRepository 相同的功能集。它直接在 ChatStore 中使用。此外,在主应用程序类 WebsocketsChatApplication 中,我通过使用 @EnableReactiveMongoRepositories 来激活响应式仓库。没有这个注解,上面的 messageStore 将无法工作。好了,我们完成了整个聊天的实现。让我们测试一下!

Spring Boot WebSocket:测试

对于测试,我使用 Postman 和 Simple WebSocket Client。

  1. 我正在使用 Postman 创建一个新聊天。在响应体中,我得到了最近创建的聊天的 WebSocket URL。

图片:Postman 创建聊天请求的屏幕截图

  1. 现在是使用它们并检查用户是否可以相互通信的时候了。Simple Web Socket Client 在这里派上用场。因此,我在这里连接到新创建的聊天。

图片:Simple Web Socket Client 连接界面的屏幕截图

  1. 好了,一切正常,用户可以相互通信了。

图片:两个 WebSocket 客户端交换消息的屏幕截图
图片:两个 WebSocket 客户端交换消息的屏幕截图
图片:两个 WebSocket 客户端交换消息的屏幕截图

还有最后一件事要做。让我们花点时间看看哪些地方可以做得更好。

可以改进的地方

由于我刚刚构建的是最基础的聊天应用程序,有一些(或者实际上相当多)地方可以做得更好。下面,我列出了一些我认为值得改进的方面:

  • 身份验证和重新加入支持 – 目前,一切都基于 sessionId。这不是一个最优的方法。最好能有一些身份验证机制,并基于用户数据实现实际的重新加入。
  • 发送附件 – 目前,聊天仅支持简单的文本消息。虽然发消息是聊天的基本功能,但用户也喜欢交换图片和音频文件。
  • 测试 – 目前没有测试,但为什么要保持这样呢?测试总是一个好主意。
  • offset 溢出 – 目前,它只是一个简单的 int。如果我们要在非常长的时间内跟踪 offset,它迟早会溢出。

总结

好了!Spring Boot WebSocket 聊天已经实现,主要任务已完成。您对下一步要开发什么有了一些想法。

请记住,这个聊天案例非常简单,对于任何类型的商业项目,都需要大量的修改和开发。

无论如何,我希望您在阅读本文时学到了一些新东西。

感谢您的时间。


【注】本文译自:Spring Boot WebSocket: Building a Multichannel Chat in Java

无学历如何成为程序员

无学历如何成为程序员

要成为一名经过认证的软件开发人员,你并不需要文凭、学位甚至认证。
你需要的只是能力。

除了体育界之外,软件开发是世界上最以实力为基础的行业。如果你愿意投入时间和精力来培养你的技能,那么六位数的软件开发人员薪资正等待着你。

遵循以下五个步骤,你将培养出成为一名成功软件开发人员所需的纪律和能力:

  1. 当你投身软件开发职业时,设定切合实际的目标。
  2. 让自己接触多种编程语言。
  3. 围绕你最感兴趣的技术领域获取技能。
  4. 建立一个令人印象深刻的开发项目组合。
  5. 克服冒名顶替综合症,拓宽你的人脉网络,并申请软件开发人员职位。

写下你的职业目标

人类是目标导向的生物。当我们为自己设定目标时,我们的大脑会自动努力消除任何阻碍我们实现该目标的障碍。

如果你想成为一名软件开发人员,那么就全身心投入去实现这个目标。

列出一系列理由:你为什么想成为开发人员?一旦你实现了目标,你的生活将如何改变?实现这个目标将如何对你周围的人产生积极影响?

成为软件开发人员的第一步是写下你的新职业目标,并致力于实现它们。

了解软件开发领域概况

如果你刚刚起步,不知道如何编写代码,那么最好的起点是 Web 开发。请优先学习以下语言和工具。

  • 学习 HTML 和 CSS。
    使用这些语言,你可以构建网页并在 Web 浏览器中测试你编写的代码。
    基于 HTML 和 CSS 的开发非常直观——对代码的修改在刷新浏览器后立即可见。这种即时的视觉反馈能让你保持兴趣,并激励你学习更多。
    开始时不要试图精通 HTML 和 CSS。只需熟悉概念,然后转向 JavaScript。

  • 深入学习 JavaScript。
    JavaScript 是一种在 Web 浏览器中运行的正规编程语言。
    使用 JavaScript 来学习编程和 DevOps 基础知识,例如变量、循环和条件语句。
    创建一些基于浏览器的游戏,如"石头剪刀布"或猜数字游戏,当你熟悉基础知识后,离开 Web 层,学习一些关于数据和数据库的知识。

  • 使用 SQL 深入学习数据库。
    要成为软件开发人员,你必须理解如何与数据库交互,这意味着你需要了解一些结构化查询语言(SQL)的知识。
    安装一个像 MySQL 这样的开源数据库,学习如何创建数据库表。然后学习如何编写 SQL 语句,将数据保存到这些数据库表中,并从其中读取数据。

  • 进阶到 API。
    最后,学习如何编写一个中间层 API,允许运行在 Web 浏览器中的 JavaScript 与你的后端数据库进行交互。
    你可以使用 JavaScript、Python、Java 甚至 Rust 来编写这个最终的、将前端和后端粘合在一起的中间层组件。重要的是你要对现代技术栈的每个部分都有一点经验。

此阶段的目标仅仅是熟悉以下这些不同的开发技术:

  • HTML
  • CSS
  • JavaScript
  • SQL
  • API

在你成为软件开发人员的这个阶段,不要试图成为任何这些技术的专家。只需对每一项都有足够的了解,以理解它们是如何协同工作的。

选择一个专长领域

在你熟悉了前端、后端和中间层开发之后,应该会有某个方向吸引你。

  • 你最喜欢 HTML 和 CSS 吗?那么也许你应该专注于网页设计。
  • 你喜欢基于浏览器的 JavaScript 吗?也许你应该专注于 React、Vue 或 Angular 等 Web 开发框架。
  • SQL 和数据库设置是否激起了你的兴趣?也许你注定要成为一名后端工程师。
  • 如果你喜欢构建连接前端和后端的 API,那么中间层开发或许就是你的使命。

在你成为软件开发人员的这个阶段,选择最吸引你的编程语言和技术,并深入提升。

例如,如果你喜欢 Web 开发,可以了解更多关于 AngularJS 或 ReactJS 的知识:

  • 在 YouTube 上观看编程教程
  • 在 FreeCodeCamp 上注册课程
  • 了解生成式 AI、机器学习,并成为认证的 AI 从业者
  • 在 Udemy 或 Coursera 上购买评价良好的课程

构建项目组合

在学习过程中,记录你的学习历程,并建立一个你已完成项目的组合。

项目组合有助于你将成为软件开发人员的进展可视化。在雇主面试时,它也能突出你的技能。

没有大学学历?你仍然可以成为软件工程师。 学位或文凭绝对不是必需条件。

然而,为了获得工作面试机会,你的简历上需要有一些能够证明你知识的东西。这就是行业认证发挥作用的地方。

在你开始求职之前,尝试在你的简历中添加两到三个行业认证。我强烈推荐的三项认证是:

  • Oracle 的认证 Java 程序员,或其他语言的同等认证;
  • 专业 Scrum Master 认证,以证明你熟悉敏捷开发;以及
  • AWS 认证助理级,以证明你了解云计算基础知识。

谷歌在电子商务和数据分析领域提供免费证书,这些证书在简历上会非常亮眼。
你甚至可以参加免费的计算机课程,哈佛大学也有你可以在线学习的免费课程。完成后,哈佛大学提供 99 美元的经过验证的成就证书。一份来自哈佛的证书无疑是你开启软件开发职业生涯的好方法。

当你培养了编程技能、创建了软件开发人员作品集,并用几项 AWS、Azure 或 GCP 认证充实了简历后,就是时候开始申请工作了。
在没有学历的情况下获得软件开发工作的一种方法是用认证来充实你的简历

加强职业人脉并申请工作

许多新程序员在申请工作或回复 LinkedIn 上的面试请求之前会犹豫不决。他们担心自己没有完全受过培训或没有足够的资格成为软件开发人员。

不要让这种冒名顶替综合症限制你的职业发展。

许多组织需要年轻、有活力的开发人员,他们了解软件开发的基础知识,并且愿意在工作中学习新技术。

完善你的 LinkedIn 个人资料。通过 MeetUp 群组和 Twitter Circles 与同行建立联系。参与社区活动。

最重要的是,坚持不懈地申请开放的软件开发职位。你终将获得第一份工作,并成功成为一名软件开发人员。从那时起,你的软件开发工作将引领何方,你的职业生涯将走向何处,将完全由你掌控。

无学历的软件开发

没有学位或文凭,你同样可以成为软件开发人员。所需要的只是动力和干劲。

迈出第一步,投身于软件开发。构建你的项目组合,获得 AWS 或 GCP 认证,发展你的职业生涯,并享受你作为认证软件开发人员的生活。


【注】本文译自: How to become a software developer without a degree

Java包装类:你需要掌握的核心要点

自Java 21起,包装类在Java类型系统中扮演着日益复杂的角色。以下是关于虚拟线程、模式匹配等方面更新所需了解的全部信息。

你是否曾好奇Java如何无缝地将其基本数据类型与面向对象编程相结合?这就引入了包装类——一个重要但常被忽视的Java特性。这些特殊类在基本类型(如intdouble)与对象之间架起了桥梁,使您能够在集合中存储数字、处理空值、使用泛型,甚至在现代特性(如模式匹配)中处理数据。
无论您是在使用List<Integer>还是从字符串解析Double,Java的包装类都使其成为可能。在本文中,我们将介绍Java 21(当前Java的长期支持版本)中的包装类。我还会提供在使用包装类时的技巧、示例以及equals()hashCode()中需要避免的陷阱。

在深入探讨Java 21中包装类的新特性之前,我们先快速回顾一下。

包装类的定义和用途

Java包装类是最终(final)、不可变的类,它们将基本值“包装”在对象内部。每种基本类型都有一个对应的包装类:

  • Boolean
  • Byte
  • Character
  • Short
  • Integer
  • Long
  • Float
  • Double

这些包装类有多种用途:

  • 允许在需要对象的地方使用基本类型(例如,在集合和泛型中)。
  • 提供用于类型转换和操作的实用方法。
  • 支持空值(null),而基本类型不能。
  • 支持反射和其他面向对象的操作。
  • 通过对象方法实现一致的数据处理。

Java版本中包装类的演变

在整个Java历史中,包装类经历了显著的演变:

  • Java 1.0 到 Java 1.4:引入了基本的包装类,并需要手动装箱和拆箱。
  • Java 5:增加了自动装箱和拆箱,极大地简化了代码。
  • Java 8:通过新的实用方法和函数式接口兼容性增强了包装类。
  • Java 9:弃用了包装类构造函数,推荐使用工厂方法。
  • Java 16 到 17:加强了弃用警告,并为移除包装类构造函数做准备。
  • Java 21:改进了包装类的模式匹配,并进一步优化了其在虚拟线程中的性能。

这种演变反映了Java在向后兼容性和集成现代编程范式之间持续的平衡。

Java 21类型系统中的包装类

从Java 21开始,包装类在Java类型系统中扮演着日益复杂的角色:

  • 增强的switchinstanceof模式匹配与包装类型无缝协作。
  • 与记录模式(record patterns)自然集成,实现更清晰的数据操作。
  • 优化了包装类型与虚拟线程系统之间的交互。
  • 改进了Lambda表达式和方法引用中包装类的类型推断。

Java 21中的包装类在承担其基本桥梁作用的同时,也融合了现代语言特性,使其成为当代Java开发的重要组成部分。

Java 21中的基本数据类型和包装类

Java为每种基本类型提供了一个包装类,为语言的基本值创建了完整的面向对象表示。以下是基本类型及其对应包装类的快速回顾,并附有创建示例:

基本类型 (Primitive type) 包装类 (Wrapper class) 创建示例 (Example creation)
boolean java.lang.Boolean Boolean.valueOf(true)
byte java.lang.Byte Byte.valueOf((byte)1)
char java.lang.Character Character.valueOf('A')
short java.lang.Short Short.valueOf((short)100)
int java.lang.Integer Integer.valueOf(42)
long java.lang.Long Long.valueOf(10000L)
float java.lang.Float Float.valueOf(3.14F)
double java.lang.Double Double.valueOf(2.71828D)

每个包装类都扩展了Object并实现了ComparableSerializable等接口。包装类提供了超越其基本对应物的附加功能,例如能够使用equals()方法进行比较。

包装类方法

Java的包装类提供了一组丰富的实用方法,超越了其装箱基本类型的主要角色。这些方法提供了方便的方式来解析字符串、转换类型、执行数学运算和处理特殊值。

类型转换方法

  • 字符串解析:Integer.parseInt("42"), Double.parseDouble("3.14")
  • 跨类型转换:intValue.byteValue(), intValue.doubleValue()
  • 进制转换:Integer.parseInt("2A", 16), Integer.toString(42, 2)
  • 无符号操作:Integer.toUnsignedLong()

实用方法

  • 最小/最大值函数:Integer.min(a, b), Long.max(x, y)
  • 比较:Double.compare(d1, d2)
  • 数学运算:Integer.sum(a, b), Integer.divideUnsigned(a, b)
  • 位操作:Integer.bitCount(), Integer.reverse()
  • 特殊值检查:Double.isNaN(), Float.isFinite()

valueOf()

另一个需要了解的重要方法是valueOf()。构造函数在Java 9中被弃用,并在Java 16中标记为待移除。不用构造函数的一种方法是改用工厂方法;例如,使用Integer.valueOf(42)而不是new Integer(42)valueOf()的优点包括:

  • 对基本类型包装器进行内存高效的缓存(IntegerShortLongByte缓存-128到127;Character缓存0-127;Boolean缓存TRUE/FALSE常量)。
  • FloatDouble由于其浮点值范围而不进行缓存。
  • 一些工厂方法对空输入有明确的行为定义。

模式匹配和虚拟线程的包装类更新

Java 21中的包装类针对模式匹配和虚拟线程进行了优化。Java中的模式匹配允许您测试对象的结构和类型,同时提取其组件。Java 21显著增强了switch语句的模式匹配,特别是在包装类方面。如下例所示,增强的模式匹配在处理多态数据时能够实现更简洁和类型安全的代码:

public String describeNumber(Number n) {
    return switch (n) {
        case Integer i when i < 0 -> "Negative integer: " + i;
        case Integer i -> "Positive integer: " + i;
        case Double d when d.isNaN() -> "Not a number";
        case Double d -> "Double value: " + d;
        case Long l -> "Long value: " + l;
        case null -> "No value provided";
        default -> "Other number type: " + n.getClass().getSimpleName();
    };
}

模式匹配的主要改进包括:

  • 空值处理(
    Null handling)
    :显式的空值case防止了意外的NullPointerException
  • 守卫模式(Guard patterns):when子句支持复杂的条件匹配。
  • 类型细化(Type refinement): 编译器现在理解每个case分支内的细化类型。
  • 嵌套模式(Nested patterns): 模式匹配现在支持涉及嵌套包装对象的复杂模式。
  • 穷举性检查(Exhaustiveness checking): 您现在可以获得编译器验证,确保覆盖了所有可能的类型。

这些特性使得包装类的处理更加类型安全和富有表现力,特别是在处理混合了基本类型和对象数据的代码中。

Java 21的虚拟线程特性也与包装类有几个重要的交互方式。首先,在并发上下文中的装箱开销减少了,如下所示:

// 使用虚拟线程高效处理大量数字流
void processNumbers(List<Integer> numbers) {
    try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
        numbers.forEach(num -> 
            executor.submit(() -> processNumber(num))
        );
    }
}

虚拟线程的其他更新包括:

  • JVM优化了涉及包装类的线程通信,减少了虚拟线程调度和交接的开销。
  • 线程本地缓存也得到了改进。包装类缓存(Integer等类型的-128到127)是按载体线程(carrier thread)而不是按虚拟线程维护的,防止了高并发场景下不必要的内存使用。
  • 还添加了身份保留(Identity preservation)。在单个虚拟线程内,包装类的身份被适当地维护,以用于同步和身份敏感的操作。

最后,对包装类进行了优化,以提高其在虚拟线程中的性能:

  • 虚拟线程使用栈遍历(stack walking)进行各种操作。包装类优化了这些交互。
  • 虚拟线程调度器队列中的包装类受益于内存效率的改进。
  • 通过优化的拆箱操作减少了线程固定(thread pinning)的风险。
  • 结构化并发模式与包装类值组合无缝协作。

包装类和虚拟线程之间的集成确保了包装类在Java 21引入的新并发编程模型中保持其有用性。这里描述的变化确保了包装类在Java中继续发挥作用,而不会出现在高吞吐量、虚拟线程密集型应用中可能发生的性能损失。

包装类中的Equals和hashcode实现

包装类重写了equals()方法,以执行基于值的比较,而不是Object.equals()使用的引用比较。在基于值的比较中,两个包装对象如果包含相同的基本值,则它们是相等的,无论它们是否是内存中不同的对象。这种比较类型具有类型特定性和空值安全性的优点:

  • 类型特异性: 仅当两个对象都是完全相同的包装类型时,比较才返回true。
  • 空值安全性: 所有包装类实现都能安全地处理空值比较。

在下面的例子中,Integer.equals()检查参数是否为Integer并且具有相同的int值:

public boolean equals(Object obj) {
    if (obj instanceof Integer) {
        return value == ((Integer)obj).intValue();
    }
    return false;
}

需要注意几个特殊情况:

  • FloatDouble 这些包装类一致地处理像NaN这样的特殊值。(与基本类型比较不同,在equals()NaN等于NaN。)
  • 自动装箱: 当使用==而不是equals()进行比较时,由于对某些值的缓存,自动装箱可能导致意外行为。

基于哈希的集合中的包装类

包装类以实现与其基本值直接对应的hashCode()方式,确保了在基于哈希的集合中的一致行为。这对于HashMapHashSetConcurrentHashMap等集合至关重要。考虑以下实现细节,然后我们将看几个例子。

  • IntegerShortByte:直接将基本值作为哈希码返回。
  • Long:将低32位与高32位进行异或操作:((int)(value ^ (value >>> 32)))
  • Float:使用Float.floatToIntBits()转换为原始位,以处理像NaN这样的特殊值。
  • Double:转换为原始位,然后对结果位使用Long的策略。
  • Character:将Unicode代码点作为哈希码返回。
  • Boolean:返回1231表示true1237表示false(任意但一致的值)。

在基于哈希的集合中使用包装类有几个优点:

  • 性能: 基于哈希的集合依赖分布良好的哈希码来实现O(1)的查找性能。
  • 一致性: hashCode()约定要求相等的对象产生相等的哈希码,包装类保证了这一点。
  • 特殊值处理: 正确处理边缘情况,如浮点类型中的NaN(两个NaN值在哈希码中是相等的,尽管使用equals()比较时不相等)。
  • 分布: 实现旨在最小化常见值模式的哈希冲突。
  • 不可变性: 由于包装对象是不可变的,它们的哈希码可以在首次计算后安全地缓存,从而提高性能。

这种谨慎的实现确保了包装类能够可靠地作为基于哈希的集合中的键,这是Java应用程序中的常见用例。

== 与 .equals() 包装类陷阱

我见过许多由使用==而不是.equals()比较包装对象引起的错误。这是一个经典的Java陷阱,甚至困扰着有经验的开发人员。你可以从这里看到是什么让它如此棘手:

Integer a = 100;
Integer b = 100;
System.out.println(a == b);      // 输出: true

Integer c = 200;
Integer d = 200;
System.out.println(c == d);      // 输出: false (等等,什么?)

这种令人困惑的行为发生是因为Java在内部缓存了常用值的Integer对象(通常是-128到127)。在这个范围内,Java重用相同的对象,而在缓存范围之外,你会得到新的对象。

这就是为什么黄金法则很简单:在比较包装对象时,始终使用.equals()。这个方法始终检查值相等性(value equality)而不是对象同一性(object identity):

// 无论缓存如何,这种方法都能可靠地工作
if (wrapperA.equals(wrapperB)) {
    // 值相等
}

空值拆箱陷阱

开发人员花费大量时间试图理解令人困惑的NullPointerException的起源,如下所示:

Integer wrapper = null;
int primitive = wrapper; // 运行时抛出 NullPointerException

这段看似无害的代码编译时没有警告,但在运行时崩溃。当Java尝试将空(null)包装器拆箱为其基本等效值时,它会尝试在空引用上调用intValue()等方法,从而导致NullPointerException

这个问题特别危险,因为它静默地通过编译,错误只在执行期间出现,并且通常发生在方法参数、数据库结果和集合处理中。为了保护你的代码,你可以使用以下防御策略:

  • 显式空值检查;例如,int primitive = (wrapper != null) ? wrapper : 0;
  • Java 21 模式匹配;例如,int value = (wrapper instanceof Integer i) ? i : 0;
  • 提供默认值;例如,int safe = Optional.ofNullable(wrapper).orElse(0);

在包装对象和基本类型之间转换时,尤其是在处理可能包含来自外部源或数据库查询的空值的数据时,务必小心。

包装类常量(不要重复造轮子)

每个Java开发人员可能都曾在某个时候写过类似“if (temperature > 100)”的代码。但是当你需要检查一个值是否超过整数的最大容量时呢?硬编码2147483647是滋生bug的温床。

相反,你可以使用带有内置常量的包装类:

// 这样清晰且自文档化
if (calculatedValue > Integer.MAX_VALUE) {
    logger.warn("Value overflow detected!");
}

最有用的常量分为两类。
数值限制有助于防止溢出错误:

  • Integer.MAX_VALUEInteger.MIN_VALUE
  • 需要更大范围时使用 Long.MAX_VALUE

浮点特殊值处理边缘情况:

  • Double.NaN 表示“非数字”结果。
  • 需要表示时使用 Double.POSITIVE_INFINITY

我发现这些在处理金融计算或处理科学数据(其中特殊值很常见)时特别有用。

包装类的内存和性能影响

理解包装类的内存和性能影响至关重要。首先,每个包装对象需要16字节的头部开销:12字节用于对象头部,4字节用于对象引用。我们还必须考虑实际的基本值存储(例如,Integer为4字节,Long为8字节等)。最后,集合中的对象引用增加了另一层内存使用,在大型集合中使用包装对象也比使用基本类型数组显著增加内存。

还有性能方面的考虑。首先,尽管有JIT优化,但在紧密循环中重复装箱和拆箱会影响性能。另一方面,像Integer这样的包装类缓存常用值(默认-128到127),减少了对象创建。此外,现代JVM有时可以在包装对象不“逃逸”方法边界时完全消除其分配。Valhalla项目旨在通过引入专门的泛型和值对象来解决这些低效问题。

考虑以下减少包装类性能和内存影响的最佳实践指南:

  • 对性能关键代码和大型数据结构使用基本类型。
  • 在需要对象行为时(例如,集合和可空性)利用包装类。
  • 考虑使用像Eclipse Collections这样的专门库来处理大量的“包装”基本类型集合。
  • 注意对包装对象进行身份比较(==)。
  • 始终使用Objectequals()方法来比较包装器。
  • 在优化之前进行分析,因为JVM对包装器的行为在不断改进。

虽然包装类与基本类型相比会产生开销,但Java的持续发展在保持面向对象范式优势的同时,正在不断缩小这一差距。

包装类的一般最佳实践

理解何时使用基本类型 versus 包装类对于编写高效且可维护的Java代码至关重要。虽然基本类型提供更好的性能,但包装类在某些场景下提供了灵活性,例如处理空值或使用Java的泛型类型。通常,您可以遵循以下准则:

在以下情况下使用基本类型:

  • 局部变量
  • 循环计数器和索引
  • 性能关键代码
  • 返回值(当null没有意义时)

在以下情况下使用包装类:

  • 可以为空的类字段
  • 泛型集合(例如,List<Integer>
  • 返回值(当null具有含义时)
  • 泛型中的类型参数
  • 使用反射时

结论

Java包装类是基本类型与Java面向对象生态系统之间的重要桥梁。从它们在Java 1.0中的起源到Java 21中的增强,这些不可变类使基本类型能够参与集合和泛型,同时提供丰富的转换和计算实用方法。它们谨慎的实现确保了在基于哈希的集合中的一致行为,并提供了提高代码正确性的重要常量。

虽然包装类与基本类型相比会产生一些内存开销,但现代JVM通过缓存和JIT编译优化了它们的使用。最佳实践包括使用工厂方法代替已弃用的构造函数,使用.equals()进行值比较,以及为性能关键代码选择基本类型。随着Java 21模式匹配改进和虚拟线程集成,包装类在保持向后兼容性的同时继续发展,巩固了它们在Java开发中的重要性。

软件开发中的 8 个伦理问题示例

软件开发中的 8 个伦理问题示例

随着软件在人类生活的方方面面根深蒂固,开发者对其客户负有伦理责任。我来来探讨如何承担这一责任。


传统上,伦理实践并非软件开发的一部分。软件并非总是对日常生活有直接影响,且开发速度缓慢。
在现代社会中,人们在生活的各个方面都会遇到软件。人工智能 (AI)、大数据和数据分析 (data analytics) 都会对个人产生切实的影响。
尽管软件开发人员主要在企业的幕后工作,但他们在项目过程中的决定,在合规性 (compliance)、公平性 (fairness)、诚信 (integrity) 和信任 (trust) 方面,可能对世界产生超乎寻常的影响——无论是好是坏。行业中的每个人都应该意识到软件开发中的社会与伦理问题。

以下是一些伦理问题的示例以及开发者可以如何解决它们:

  • 成瘾性设计 (Addictive design)。
  • 企业拥有个人数据 (Corporate ownership of personal data)。
  • 算法偏见 (Algorithmic bias)。
  • 薄弱的网络安全和个人身份信息 (PII) 保护 (Weak cybersecurity and personally identifiable information (PII) protection)。
  • 过度强调功能 (Overemphasis on features)。
  • 缺乏透明度 (Lack of transparency)。
  • 环境影响 (Environmental impact)。
  • 人权影响 (Human rights impact)。

1. 成瘾性设计 (Addictive design)

每位开发者都渴望创建人们喜欢使用的程序——这只是良好的用户体验 (UX) 设计。问题在于,有些团队设计的应用程序让人们爱不释手。这引发了关于社交媒体等数字平台角色的伦理担忧。
人道技术中心 (Center for Humane Technology) 的 Tristan Harris 等批评者认为,社交媒体公司从愤怒、困惑、成瘾和抑郁中获利——从而将我们的福祉和民主置于风险之中。值得注意的是,Harris 在谷歌工作时,因其关于推动成瘾性技术设计以及公司在社会中的道德责任的演讲而走红。
在消费者喜爱的产品和劫持他们注意力的产品之间取得伦理平衡,更像是一门艺术而非科学。在产品创建和更新中,请问以下问题:

  • 谁受益?
  • 他们如何受益?
  • 他们在多大程度上受益?
  • 是否有保障用户健康和理智的措施?
  • 包括通过 AI 和机器学习 (machine learning) 进行的货币化 (monetization) 以及客户数据收集和使用有多公开?这些实践有多透明?

技术诚信委员会 (Technology Integrity Council) 创始执行董事 David K. Bain 将 Duolingo 和 TikTok 作为应用程序设计的两个对比示例。这两个应用程序都为它们的创造者带来了增长和收入,但它们对用户的益处性质不同。
Duolingo 的客户获得语言技能,并通过促进神经元生长 (neuronal growth) 和大脑可塑性 (brain plasticity) 的活动受到挑战。TikTok 用户获得文化知识,并通过让大脑沐浴在令人陶醉的神经递质 (neurotransmitters) 中的视频内容获得即时满足感。“基于此,许多成年人会说 Duolingo 的真正用户收益大于 TikTok,”Bain 说,但他补充道,他十几岁的女儿会不同意。
这两个应用程序对旨在防范成瘾性依赖的使用限制持有不同态度。Duolingo 鼓励一致性,并有力论证其使用与优化的学习曲线 (learning curves) 相关。Duolingo 肯定会揪着用户的衣领 (grabs users by the lapels) 要求他们完成每日配额 (daily quota) 并保持连续表现 (performance streaks)。但一旦每日活动完成,Duolingo 就会释放用户。相比之下,TikTok 通过本质上无限量的可消费媒体 (consumable media) 自助餐来吸引用户留下。
应用程序通常包含用户操纵 (user manipulation)、货币化方法 (monetization methods)、用于企业用途的用户数据收集 (user data collection for corporate use) 以及用于增强应用程序的机器学习算法 (machine learning algorithms)。透明的应用程序提供者会让用户对这些实践有一定程度的了解和理解。
以下是这一伦理方面在这两个示例应用程序中的体现:“Duolingo 的用户显然是强制每日计划的自愿受害者,但几乎可以肯定没有意识到广告和使用数据连接到一个更大的广告生态系统,”Bain 说。“TikTok 的用户,尤其是年轻用户,我非常肯定在很大程度上是快乐地没有意识到他们成瘾的方法和后果。”

2. 存疑的个人数据所有权 (Questionable personal data ownership)

随着设备和软件的发展,基于 AI 的生物识别 (biometric) 和其他关于客户的上下文数据 (contextual data) 处理有所增加。软件可以以令人恐惧的详细程度对用户进行画像 (profile users) 并预测行为。
“通常,伦理问题是关于如何处理这些数据,”广告验证和欺诈预防平台 TrafficGuard 的首席产品官 Miguel Lopes 说。这种伦理问题对于各种业务的开发者来说都是一个困境——不仅仅是那些登上新闻的社交媒体巨头。
算法 (algorithm) 指导信息收集和画像构建 (profile building),但随后的行动是故意的。开发者通常清楚这些数据在特定情境下的力量。
开发者可以帮助公司内其他角色理解技术选择对伦理考量的影响。
Lopes 说,伦理担忧的根本原因之一与企业如何产生收入以及如何激励开发者和业务经理有关。在许多情况下,公司将用户数据视为一种有价值的货币,并希望将其存储的数据货币化。“这些因素可能导致这些组织不道德地共享其用户数据,”他说。
开发者在个人数据和软件设计方面面临艰难抉择。他们可以在理解责任在于组织的前提下创建利用用户数据的系统,或者他们可以提出担忧,但面临因违背项目目标而可能受到惩罚的风险。现代技术公司的工作文化应让开发者能够毫无畏惧地提出个人数据所有权的担忧。
这类担忧在 Lopes 工作过的不同组织中激起了丰富的讨论,这些组织决定不提供免费服务层级。“我们分析了其中的含义,更倾向于通过销售我们的服务而不是用户数据来维持运营,并且不让我们的开发团队面临这些艰难的选择,”Lopes 说。公司内部的透明度是一个关键因素。开发者应该了解他们正在参与的项目的整个背景,而不仅仅是他们需要完成的模块。
公司应该让开发者能够轻松地提出担忧。人力资源 (HR) 部门可以创建机制,让开发者能够表达他们的担忧而无需担心报复,例如用于伦理问题的匿名热线。然后,组织应跟进并独立识别该用例是否违反了隐私、法律或伦理政策。

3. 算法偏见 (Algorithmic bias)

技术会放大现有的偏见。“当今开发者面临的一个更紧迫的伦理问题是偏见 (bias),”业务自动化平台 Pegasystems 的首席客户主管 Spencer Lentz 说。
偏见常常在未被察觉的情况下进入系统——Lentz 将偏见比作病毒。计算机本身没有内在的道德框架。软件只能反映其创造者的偏见。因此,开发者和数据科学家 (data scientists) 必须从训练数据 (training data) 和他们构建的算法中清除偏见。Lentz 说,从开发者的角度来看,偏见通常集中在出于错误的原因消除选项上。
当在不完整和有偏见的数据上训练时,AI 可能会产生有偏见的结果
当在不完整和有偏见的数据上训练时,AI 可能会产生有偏见的结果
近年的报道和研究说明了软件系统中的偏见如何能对特定人群延续系统性种族主义 (systemic racism),这会造成机会丧失、恶化医疗护理并增加监禁率。例如,在《Race After Technology》一书中,Ruha Benjamin 对一个案例提出了担忧:开发者未能将黑人的声音纳入训练 AI 语音识别算法中,理由是认为使用该应用程序的黑人较少。
高管、数据科学家和开发者必须创建一种组织文化,制定伦理准则,并赋予业务任何层级的个人在看到有问题时发声的权力。
“时至今日,模型中的偏见已众所周知,以至于 LLM 幻觉 (LLM hallucination) 已成为一个主流概念,”数据科学平台 Anaconda 的首席 AI 和创新官兼联合创始人 Peter Wang 说。“如今最大的风险是,人们被炒作和害怕落后的恐惧冲昏了头脑,以至于没有花时间勤奋地构建评估机制和实施治理 (governance)。作为一个行业,我们需要更加透明地说明企业 AI 项目的失败率有多高,这样管理者和高管才不会感到有必要在一致性 (alignment)、准确性 (accuracy) 和安全性 (safety) 等极其重要的话题上仓促行事。”
Wang 主张,是时候为 AI 提供商创建一个管理机构了,类似于美国医学会 (American Medical Association) 之于医生。该机构可以制定全行业的伦理准则和最佳实践 (best practices)。“这些技术在商业环境中仍然相对较新,我们都将从源于我们集体智慧和投入的伦理标准中受益,而不是让每个个体或组织自行决定,”他说。

4. 薄弱的安全性和 PII 保护 (Weak security and PII protection)

随着软件在我们的线上和线下环境中扮演越来越重要的角色,应用程序安全 (Application security) 的重要性日益增长。
开发者可能只在代码发布后才处理安全性问题,而不是在开发过程中。因此,软件社区缺乏安全的开发标准 (secure development standards)。
“重点几乎完全放在将产品推向市场上,”软件开发咨询公司 Bit Developers 的创始人兼首席软件架构师 Randolph Morris 说。一旦软件产品公开发布,重点就会转移到新功能和性能优化上,因此安全性仍然很少受到重视。
黑客 (Hackers) 和其他恶意行为者 (malicious actors) 会对真实的人造成切实的损害。一种被动应对 (reactionary approach) 应用程序安全的方法——在发现漏洞 (vulnerabilities) 时才进行修补——既不实用也不可行。
为了履行对客户安全的这一伦理责任,开发者需要接受教育,但通常只有专门的网络安全 (cybersecurity) 课程涉及这些主题。首先,让你的团队了解网络安全故障,例如 2015 年具有里程碑意义的 Anthem 医疗数据泄露事件 (Anthem medical data breach),其中 PII 以纯文本 (plain text) 形式存储在数据库中。“如果这些信息被加密 (encrypted),就不会那么容易使用和有价值地分发,”Morris 说。
此外,行业需要修订安全标准 (security standards)。组织可以采取更多措施来采用旨在保护 PII 的标准。支付卡行业数据安全标准 (Payment Card Industry Data Security Standard, PCI DSS) 和针对医疗保健应用程序的 HIPAA 是一个良好的开端,但开发者还应考虑其他形式的 PII——以及保护它的软件设计。
探索企业在应用程序设计中应负责任处理的不同类型的个人信息
探索企业在应用程序设计中应负责任处理的不同类型的个人信息

5. 优先考虑功能而非影响 (Prioritizing features over impact)**

许多伦理问题的核心在于一个决定:软件发布中的功能 (capabilities) 比它们可能产生的影响更重要。但仅仅因为你能够做某事,并不意味着你应该做。
“如果开发团队是根据其功能开发速度来衡量的,那么在设计或实施阶段,特定实施的伦理问题很可能不会被优先考虑,”应用安全平台 Black Duck 的软件供应链风险策略负责人 Tim Mackey 说。
企业本身必须为其软件中的伦理标准定下基调。以下是企业可以实现这一目标的一些方法:

  • 在整个软件生命周期(从设计到运营)中体现伦理优先事项。
  • 就开源软件许可和使用等伦理选择对员工进行培训。
  • 教导开发者、架构师、测试人员和其他软件团队成员符合法规和客户期望的数据管理实践 (data management practices)。
    Mackey 指出,开发者并不总是关注客户使用其软件的司法管辖区的最新立法行动新闻,但企业必须确保他们知情。
    工程领导层 (engineering leadership) 与法律团队之间的协作有助于避免伦理缺陷。例如,企业应关注客户的个人数据访问 (personal data access) 和保留 (retention)。数据访问控制 (Data access controls) 和日志记录机制 (logging mechanisms) 是在软件实施时启用的。负责创建功能性、用户友好型产品的开发者可能认为数据访问限制是另一个团队的责任。相反,应确保数据保护 (data protection) 是软件设计中包含的一项功能,从根本上防止未经授权的访问 (unauthorized access)。

6. AI 透明度的幻象 (Mirage of AI transparency)**

大型语言模型 (Large language models, LLMs) 在软件开发中扮演着越来越重要的角色,涉及生成代码和支持非结构化数据处理 (unstructured data processing) 等任务。由于 LLM 的复杂性,很容易忽视这些系统是如何被训练、配置和部署的——以及这对用户意味着什么。
“软件公司应该始终披露他们如何训练其 AI 引擎,”Lopes 说。“用户数据被收集的方式——通常是静默地收集并输入 LLM——引发了关于同意 (consent)、安全和自动化伦理界限的严重问题。”
已经出现了一些引人注目的案例,其中用户在平台上的互动被用于在没有任何通知的情况下静默训练 AI。“我们看到公司在未经同意的情况下收集行为数据 (behavioral data),本质上将用户变成了无偿贡献者,而这些模型有一天可能会取代他们的工作,”他继续说道。
一个训练有素的 AI 代理 (AI agent) 需要深度配置 (deep configuration)、监督 (supervision) 和昂贵的人力人才。“你认为通过跳过适当开发而节省的成本,几乎总是被一个专业化程度低的代理所造成的损害所掩盖——无论是安全风险、错误信息 (misinformation) 还是客户信任的丧失,”Lopes 说。
AI 伦理框架 (AI ethics frameworks) 旨在缓解上述部分问题
AI 伦理框架 (AI ethics frameworks) 旨在缓解上述部分问题

7. 环境影响 (Environmental impact)**

随着对气候变化影响(包括气温上升、洪水、火灾和其他不利天气条件)认识的提高,对各种活动环境影响的担忧日益增长。技术公司的活动也可能减少清洁水的获取、污染空气并减少生物多样性。
AI 使用量的增长带来了显著增加能源消耗以及随之而来的碳排放 (carbon emissions) 的风险。它还可能增加用于冷却数据中心的水系统压力,从而损害当地社区。云提供商也开始探索碳中和能源 (carbon-neutral energy sources),如核裂变电厂 (nuclear fission plants),同时掩盖了处理乏放射性燃料 (spent radioactive fuel) 相关的仍未解决的环境成本。
这些都是通常超出软件开发周期的大局考量,但在决定扩展由新 LLM 驱动的应用程序的潜在影响时,值得考虑。其他方面包括新软件应用程序可能鼓励不良环境选择的潜力。一个快时尚应用程序 (fast-fashion app) 可能会以产生更多浪费为代价来推动收入。

8. 社会与人权影响 (Social and human rights impact)**

考虑软件开发实践对人权影响的多个维度包括其对劳动力和社区的潜在影响。
在劳动力方面,一个担忧是所谓的“数据标注血汗工厂” (data labeling sweatshops) 的增长,其中涉及让工人接触有毒内容 (toxic content) 以改进 AI 系统中的内容审核 (content moderation)。尽管大多数企业并未直接参与此过程,但他们可能忽视了其 AI 和数据系统供应商和承包商所采用的做法。
此外,必须考虑优化应用程序在相对容易量化的方面(如仓库吞吐量 (warehouse throughput))的潜在影响,与在更难以量化的方面(如工人健康或心理健康)的潜在影响。风险在于,某些类型的生产力优化 (productivity optimizations) 可能对工人的生活及其对家庭和社区的贡献产生不利影响。
AI 系统在软件开发中的兴起推动了数据标注行业的增长,但通常缺乏监督。新的应用程序也有可能破坏社区的社会结构 (social fabric)。

合乎伦理的软件开发最佳实践 (Best practices for ethical software development)

以下是培养具有积极社会影响的实践的几种方法:

  • 主动性 (Proactivity):对软件工程选择对合乎伦理的软件开发乃至整个世界的影响和背景保持好奇。
  • 诚实 (Honesty):考虑软件工程选择如何可能与伦理原则相冲突,即使这对你个人或公司来说是不舒服的。
  • 问责制 (Accountability):确定在公司内部衡量和沟通伦理问题的方法,以确保每个人都达成共识。
  • 平衡社会责任与技术能力 (Balance social responsibility with technical ability):记住开发者可以帮助公司内其他角色理解技术选择对伦理考量的影响。

【注】本文译自:8 examples of ethical issues in software development

Spring框架中的Component与Bean注解

Spring Boot 中的 @Bean 与 @Component

Spring 的 @Component@Bean 注解的关键区别在于:@Bean 注解可用于暴露您自己编写的 JavaBeans,而 @Component 注解可用于暴露源代码由他人维护的 JavaBeans。
Spring 框架的核心是其控制反转 (IoC) 容器,它管理着应用程序中最重要的 JavaBeans 的生命周期。然而,IoC 容器并不管理应用程序可能需要的每一个 JavaBean。它只管理您明确要求它管理的 JavaBeans 的生命周期。
何时使用 Spring 的 @Bean 注解?
如果您自己编写了一个 JavaBean,可以直接在源代码中添加 Spring 的 @Bean 注解。这里我们要求 Spring 的 IoC 容器管理 Score 类所有实例的生命周期。

@Bean
public class Score {
    int wins, losses, ties;
}

何时使用 Spring 的 @Component 注解?
但是,如果您想让 Spring 的 IoC 容器管理来自 Jackson API 的 ObjectMapper,或者来自 JDBC API 的 DataSource 组件呢?您不能简单地编辑 JDK 中的代码并在标准 API 的类上添加 @Bean 注解。这就是 @Component 注解的用武之地。
如果您希望 Spring 管理一个您无法控制其代码的 JavaBean,您可以创建一个返回该 JavaBean 实例的方法,然后用 @Component 注解装饰该方法,如下例所示:

@Configuration
public class MyConfig {
    @Component
    public DataSource getMyHikariDataSource() {
        HikariDataSource ds = new HikariDataSource();
        ds.setJdbcUrl("jdbc:h2:mem:roshambo");
        return ds;
    }
    @Component
    public ObjectMapper getMyObjectMapper() {
        ObjectMapper mapper = new ObjectMapper();
        mapper.enable(SerializationFeature.INDENT_OUTPUT);
        return mapper;
    }
}

在此示例中,我们使用了 @Component 注解来告诉 Spring IoC 容器管理 DataSourceObjectMapper bean 的生命周期。
这些组件来自 Jackson 和 JDBC API,因此我们无法编辑其源代码。这就是为什么我们不能直接在类声明上方添加 @Bean 注解的原因。但是,我们可以使用 @Component 注解,并结合放在类文件本身的 @Configuration 注解,来告诉 Spring 管理这些外部提供的资源。
用 @Component 代替 @Bean?
@Component 注解并不仅限于与外部 API 一起使用。开发者完全允许使用 @Component 注解代替 @Bean 注解来暴露他们自己编写的 JavaBeans。
如果我们从上方的 Score 类中移除 @Bean 注解,我们可以像下面代码中看到的那样,通过使用 @Component 注解来通过 IoC 容器暴露 Score

@Configuration
public class MyRoshamboConfig {
    @Component
    public Score getTheScore() {
        return new Score();
    }
}

何时使用 @Component vs @Bean?
在具有一定规模的 Spring Boot 项目中,我实际上更倾向于使用 @Component 注解而不是 @Bean 注解。这样,配置被限制在单个文件中,而您编写的 JavaBeans 不会被那些将您的源代码紧密绑定到 Spring 框架的注解所充斥。
在较小的项目和原型中?我完全支持使用 @Bean 注解。它更容易使用,并且如果您的项目不需要大量配置,它可以帮助您更快地启动和运行您的微服务。


【注】本文译自:Component vs. Bean annotations in Spring

Java中的多态与继承

Java中的多态与继承

开始学习Java中的多态及如何在多态方法调用中进行方法调用

多态——即对象根据其类型执行特定操作的能力——是Java代码灵活性的核心。四人组(Gang Of Four)创建的许多设计模式都依赖于某种形式的多态,包括命令模式。本文将介绍Java多态的基础知识及如何在程序中使用它。

关于Java多态需要了解的内容

  • 多态与Java继承
  • 为何多态重要
  • 方法重写中的多态
  • 核心Java类中的多态
  • 多态方法调用与类型转换
  • 保留关键字与多态
  • 多态的常见错误
  • 关于多态需要记住的要点

多态与Java继承

我们将重点探讨多态与Java继承的关系。需记住的核心点是:多态需要继承或接口实现。以下示例通过Duke和Juggy展示这一点:

public abstract class JavaMascot {
    public abstract void executeAction();
}

public class Duke extends JavaMascot {
    @Override
    public void executeAction() {
        System.out.println("Punch!");
    }
}

public class Juggy extends JavaMascot {
    @Override
    public void executeAction() {
        System.out.println("Fly!");
    }
}

public class JavaMascotTest {
    public static void main(String... args) {
        JavaMascot dukeMascot = new Duke();
        JavaMascot juggyMascot = new Juggy();
        dukeMascot.executeAction();
        juggyMascot.executeAction();
    }
}

代码输出为:

Punch!
Fly!

由于各自的具体实现,Duke和Juggy的动作均被执行。

为何多态重要

使用多态的目的是将客户端类与实现代码解耦。客户端类通过接收具体实现来执行所需操作,而非硬编码。这种方式下,客户端类仅需了解执行操作的必要信息,这是松耦合的典范。

为了更好地理解多态的优势,请观察以下SweetCreator

public abstract class SweetProducer {
    public abstract void produceSweet();
}

public class CakeProducer extends SweetProducer {
    @Override
    public void produceSweet() {
        System.out.println("Cake produced");
    }
}

public class ChocolateProducer extends SweetProducer {
    @Override
    public void produceSweet() {
        System.out.println("Chocolate produced");
    }
}

public class CookieProducer extends SweetProducer {
    @Override
    public void produceSweet() {
        System.out.println("Cookie produced");
    }
}

public class SweetCreator {
    private List<SweetProducer> sweetProducer;

    public SweetCreator(List<SweetProducer> sweetProducer) {
        this.sweetProducer = sweetProducer;
    }

    public void createSweets() {
        sweetProducer.forEach(sweet -> sweet.produceSweet());
    }
}

public class SweetCreatorTest {
    public static void main(String... args) {
        SweetCreator sweetCreator = new SweetCreator(
            Arrays.asList(
                new CakeProducer(),
                new ChocolateProducer(),
                new CookieProducer()
            )
        );
        sweetCreator.createSweets();
    }
}

此例中,SweetCreator类仅知晓SweetProducer类,而不了解每个甜点的具体实现。这种分离使类能灵活更新和重用,并大幅提升代码可维护性。设计代码时,应始终寻求使其尽可能灵活和可维护。多态是编写可重用Java代码的强力技术。

提示@Override注解强制程序员使用必须被重写的相同方法签名。若方法未被重写,将产生编译错误。

方法重载是多态吗?

许多程序员对多态与方法重写、重载的关系感到困惑。但只有方法重写是真正的多态。重载共享相同方法名但参数不同。多态是广义术语,因此相关讨论将持续存在。

方法重写中的多态

若返回类型是协变类型,则允许修改重写方法的返回类型。协变类型本质上是返回类型的子类。示例如下:

public abstract class JavaMascot {
    abstract JavaMascot getMascot();
}

public class Duke extends JavaMascot {
    @Override
    Duke getMascot() {
        return new Duke();
    }
}

由于DukeJavaMascot的子类,我们可在重写时修改返回类型。

核心Java类中的多态

我们在核心Java类中频繁使用多态。一个简单示例是实例化ArrayList类时声明List接口为类型:

List<String> list = new ArrayList<>();

进一步观察以下未使用多态的Java集合API代码:

public class ListActionWithoutPolymorphism {
    // 无多态的示例
    void executeVectorActions(Vector<Object> vector) {/* 此处代码重复 */}
    void executeArrayListActions(ArrayList<Object> arrayList) {/* 此处代码重复 */}
    void executeLinkedListActions(LinkedList<Object> linkedList) {/* 此处代码重复 */}
    void executeCopyOnWriteArrayListActions(CopyOnWriteArrayList<Object> copyOnWriteArrayList)
    { /* 此处代码重复 */}
}

public class ListActionInvokerWithoutPolymorphism {
    listAction.executeVectorActions(new Vector<>());
    listAction.executeArrayListActions(new ArrayList<>());
    listAction.executeLinkedListActions(new LinkedList<>());
    listAction.executeCopyOnWriteArrayListActions(new CopyOnWriteArrayList<>());
}

这段代码很糟糕,不是吗?想象维护它的难度!现在观察使用多态的相同示例:

public static void main(String … polymorphism) {
    ListAction listAction = new ListAction();    
    listAction.executeListActions();
}
public class ListAction {
    void executeListActions(List<Object> list) {
        // 对不同列表执行操作
    }
}
public class ListActionInvoker {
    public static void main(String... masterPolymorphism) {
        ListAction listAction = new ListAction();
        listAction.executeListActions(new Vector<>());
        listAction.executeListActions(new ArrayList<>());
        listAction.executeListActions(new LinkedList<>());
        listAction.executeListActions(new CopyOnWriteArrayList<>());
    }
}

多态的优势在于灵活性和扩展性。我们无需创建多个不同方法,只需声明一个接收通用List类型的方法。

多态方法调用与类型转换

可以在多态调用中调用特定方法,但会牺牲灵活性。示例如下:

public abstract class MetalGearCharacter {
    abstract void useWeapon(String weapon);
}
public class BigBoss extends MetalGearCharacter {
    @Override
    void useWeapon(String weapon) {
        System.out.println("Big Boss is using a " + weapon);
    }
    void giveOrderToTheArmy(String orderMessage) {
        System.out.println(orderMessage);
    }
}
public class SolidSnake extends MetalGearCharacter {
    void useWeapon(String weapon) {
        System.out.println("Solid Snake is using a " + weapon);
    }
}
public class UseSpecificMethod {
    public static void executeActionWith(MetalGearCharacter metalGearCharacter) {
        metalGearCharacter.useWeapon("SOCOM");
        // 以下行无法工作
        // metalGearCharacter.giveOrderToTheArmy("Attack!");
        if (metalGearCharacter instanceof BigBoss) {
            ((BigBoss) metalGearCharacter).giveOrderToTheArmy("Attack!");
        }
    }
    public static void main(String... specificPolymorphismInvocation) {
        executeActionWith(new SolidSnake());
        executeActionWith(new BigBoss());
    }
}

此处使用的技术是类型转换(casting),即在运行时显式改变对象类型。

注意:只有将通用类型强制转换为具体类型后,才能调用特定方法。这相当于明确告诉编译器:“我知道自己在做什么,因此要将对象转换为具体类型并使用特定方法。”

在上述示例中,编译器拒绝接受特定方法调用的原因很重要:传入的类可能是SolidSnake。在此情况下,编译器无法确保每个MetalGearCharacter的子类都声明了giveOrderToTheArmy方法。

保留关键字

注意保留字instanceof。在调用特定方法前,我们需检查MetalGearCharacter是否为BigBoss的实例。若BigBoss实例,将收到以下异常信息:

Exception in thread "main" java.lang.ClassCastException: com.javaworld.javachallengers.polymorphism.specificinvocation.SolidSnake cannot be cast to com.javaworld.javachallengers.polymorphism.specificinvocation.BigBoss

若需引用Java超类的属性或方法,可使用保留字super。例如:

public class JavaMascot {
    void executeAction() {
        System.out.println("The Java Mascot is about to execute an action!");
    }
}
public class Duke extends JavaMascot {
    @Override
    void executeAction() {
        super.executeAction();
        System.out.println("Duke is going to punch!");
    }
    public static void main(String... superReservedWord) {
        new Duke().executeAction();
    }
}

在Duke的executeAction方法中使用super可调用超类方法,再执行Duke的特定动作。因此输出如下:

The Java Mascot is about to execute an action!
Duke is going to punch!

多态的常见错误

  • 常见错误是认为无需类型转换即可调用特定方法。
  • 另一个错误是在多态实例化类时不确认将调用哪个方法。需记住:被调用的方法是所创建实例的方法。
  • 还需注意方法重写不同于方法重载
  • 若参数不同,则无法重写方法。若返回类型是超类方法的子类,则可以修改重写方法的返回类型。

关于多态需要记住的要点

  • 所创建的实例将决定使用多态时调用哪个方法。
  • @Override注解强制程序员使用重写方法;否则将产生编译错误。
  • 多态可用于普通类、抽象类和接口。
  • 大多数设计模式依赖某种形式的多态。
  • 调用多态子类中特定方法的唯一方式是使用类型转换。
  • 可通过多态设计强大的代码结构。

接受Java多态挑战!

让我们测试你对多态和继承的理解。在此挑战中,你需要根据Matt Groening的辛普森一家代码推断每个类的输出。首先仔细分析以下代码:

public class PolymorphismChallenge {
    static abstract class Simpson {
        void talk() {
            System.out.println("Simpson!");
        }
        protected void prank(String prank) {
            System.out.println(prank);
        }
    }
    static class Bart extends Simpson {
        String prank;
        Bart(String prank) { this.prank = prank; }
        protected void talk() {
            System.out.println("Eat my shorts!");
        }
        protected void prank() {
            super.prank(prank);
            System.out.println("Knock Homer down");
        }
    }
    static class Lisa extends Simpson {
        void talk(String toMe) {
            System.out.println("I love Sax!");
        }
    }
    public static void main(String... doYourBest) {
        new Lisa().talk("Sax :)");
        Simpson simpson = new Bart("D'oh");
        simpson.talk();
        Lisa lisa = new Lisa();
        lisa.talk();
        ((Bart) simpson).prank();
    }
}

你认为最终输出是什么?不要使用IDE!重点是提升代码分析能力,请自行推断结果。

选项:
A)

I love Sax!  
 D'oh  
 Simpson!  
 D'oh  

B)

Sax :)  
 Eat my shorts!  
 I love Sax!  
 D'oh  
 Knock Homer down  

C)

Sax :)  
 D'oh  
 Simpson!  
 Knock Homer down  

D)

I love Sax!  
 Eat my shorts!  
 Simpson!  
 D'oh  
 Knock Homer down

解答挑战
对于以下方法调用:

new Lisa().talk("Sax :)");

输出为“I love Sax!”,因为我们向方法传递了字符串且Lisa类有此方法。

下一调用:

Simpson simpson = new Bart("D'oh");
simpson.talk();

输出为“Eat my shorts!”,因为我们用Bart实例化了Simpson类型。

以下调用较为复杂:

Lisa lisa = new Lisa();
lisa.talk();

此处通过继承使用了方法重载。由于未向talk方法传递参数,因此调用Simpsontalk方法,输出为:

"Simpson!"

最后一个调用:

((Bart) simpson).prank();

此例中,prank字符串在实例化Bart时通过new Bart("D'oh")传入。此时首先调用super.prank方法,再执行Bart的特定prank方法。输出为:

"D'oh"
"Knock Homer down"

因此正确答案是D。输出为:

I love Sax!
Eat my shorts! 
Simpson!
D'oh
Knock Homer down

【注】本文译自:Polymorphism and inheritance in Java | InfoWorld

Netflix系统架构解析

Netflix系统架构解析

Netflix架构旨在高效可靠地同时为数百万用户提供内容。以下是其特性和组件的详细分析。


是否曾好奇Netflix如何让您目不转睛地享受无中断的流畅播放体验?幕后功臣正是Netflix架构,它负责提供吸引全球观众的无缝流媒体体验。Netflix的系统架构强调了决定未来内容形态的重要性。让我们一起探索Netflix流媒体宇宙的幕后故事!
Netflix已成为娱乐、追剧和尖端流媒体服务的代名词。其迅速崛起可归因于庞大的内容库、全球覆盖以及弹性创新的架构。
从1997年的DVD租赁服务发展为全球流媒体巨头,Netflix始终运用前沿技术革新着媒体消费方式。
Netflix架构旨在高效可靠地同时为数百万用户提供内容。鉴于其在190多个国家拥有超过2亿会员,基础设施的可扩展性至关重要。
让我们深入探究Netflix架构的复杂性,揭示其如何持续塑造我们享受喜爱节目的方式。

理解Netflix系统架构的重要性

理解Netflix系统架构至关重要,原因包括:
首先,它揭示了Netflix如何为全球数百万用户提供无瑕疵的流媒体体验。通过探索架构细节,我们能更好地理解其成功背后的技术与策略。
此外,其他行业可将Netflix设计作为开发可扩展、可靠且高效系统的蓝图。其设计原则和最佳实践为构建复杂分布式系统提供了重要经验。
理解Netflix架构还能让我们认识到推动数字媒体消费发展的持续创新。

理解系统设计需求

系统设计对开发复杂软件或技术基础设施至关重要。这些规范是构建整个系统的基础,驱动决策并塑造最终产品。那么系统设计的先决条件是什么?为何如此重要?让我们进行探讨。

功能性需求

功能性需求规定了系统必须包含的功能和能力。这些规范概述系统主要目标,并详述各部件如何交互。以Netflix为例的流媒体平台功能性需求包括但不限于:

  1. 账户创建: 用户应能轻松创建账户,提供注册所需信息。
  2. 用户登录: 注册用户应能通过认证凭证安全登录。
  3. 内容推荐: 平台应根据用户偏好和观看历史提供个性化建议。
  4. 视频播放: 用户应能无缝播放视频,支持播放控制功能。

非功能性需求

非功能性需求定义系统在不同场景下的行为,确保满足特定质量要求。涵盖性能、可扩展性、可靠性、安全性和合规性等方面。以Netflix为例包括但不限于:

  1. 性能需求: 高负载时保持低延迟和高吞吐量。
  2. 合规需求: 遵守数据保护法规标准。
  3. 扩展性需求: 基础设施需支持用户增长而不影响性能。
  4. 安全需求: 实施强认证和加密防止未授权访问。
  5. 可靠性需求: 包含故障转移方法并保证高正常运行时间。

Netflix架构:拥抱云原生

2008年8月因数据库损坏遭遇重大挫折后,Netflix得出关键结论:必须摆脱单点故障,转向高可靠、水平可扩展的云解决方案。Netflix选择AWS作为云供应商,2015年将多数服务迁移至云端。经过七年努力,2016年1月初完成云迁移,关闭了最后的数据中心组件。
上云并非易事。Netflix采用云原生策略,彻底改革运营模式和技术栈:采用NoSQL数据库、反规范化数据模型、从单体应用转向数百个微服务。文化变革也不可或缺,如采用DevOps流程、持续交付和自助式工程环境。尽管困难重重,此转型使Netflix成为云原生企业,为在线娱乐领域的未来扩展和创新奠定基础。

Netflix架构三要素

由客户端、后端和内容分发网络(CDN)构成的强大架构三要素,共同保障无瑕疵用户体验。面对全球数百万观众,每个组件对内容交付都至关重要。

客户端

客户端架构是Netflix体验的核心,涵盖用户访问的各种设备(电脑、智能电视、智能手机)。Netflix混合使用Web界面和原生应用确保跨平台一致体验。无论设备类型,这些客户端管理播放控制、用户交互与界面渲染,提供统一体验。得益于响应式优化,用户可轻松浏览内容库并享受连续播放。

后端架构

后端架构是幕后运营的支柱。用户账户管理、内容目录、推荐算法、计费系统等由复杂的服务器、数据库和微服务网络处理。
后端不仅处理用户数据与内容交付,还运用大数据分析和机器学习优化内容交付与个性化推荐,提升用户满意度。
Netflix后端架构历经重大演变:2007年迁移至云基础设施,2018年采用Spring Boot作为主要Java框架。结合AWS的可扩展性和可靠性,Ribbon、Eureka和Hystrix等专有技术有效协调后端运营。

内容分发网络(CDN)

CDN完善架构三角。Netflix运营名为Open Connect的CDN,通过战略部署的全球服务器网络,以最高可靠性和最小延迟交付内容。
通过在靠近用户的站点缓存内容,减少缓冲并确保流畅播放。即使在高峰期,通过全球服务器分发内容减少拥塞并最大化带宽利用率。这种去中心化方式提升全球观看体验,降低缓冲时间并提高流媒体质量。

客户端组件

Web界面

近年Netflix Web界面经历重大转型,从Silverlight转向HTML5流式传输视频内容。此举消除了浏览器插件需求,简化用户体验。自采用HTML5后,提升了对Chrome、Safari、Firefox等浏览器的兼容性。
Netflix对HTML5的应用不仅限于基础播放,还借此支持行业标准与技术进步。

移动应用

通过iOS和Android应用将流媒体体验延伸至移动用户。结合原生开发与平台优化,为各类移动设备提供流畅界面。
凭借个性化推荐、无缝播放和离线下载等功能,满足移动观众需求。用户可随时随地观看喜爱的内容,Netflix通过频繁升级提供引人入胜的移动体验。

智能电视应用

电视应用基于复杂架构,包含Gibbon渲染层、动态更新的JavaScript应用和原生SDK。通过定制版React-Gibbon确保跨电视平台的流畅UI渲染与响应。
性能优化聚焦每秒帧数与输入响应等指标,通过减少属性迭代等方法提升渲染效率,样式优化与自定义组件开发进一步优化性能。

重塑播放体验:现代化之旅

过去十年Netflix彻底改变了数字媒体消费方式。尽管持续推出创新功能,但自2013年以来播放界面的视觉设计与用户控制鲜有变化。认识到需要更新后,Web UI团队着手重新设计。
团队聚焦三大画布:播放前、视频播放和播放后,目标是提升客户满意度。通过React.js和Redux等技术加速开发与提升性能,革新了播放界面。

后端基础设施

内容分发网络(CDN)

Netflix基础设施依赖Open Connect CDN,轻松向全球数百万观众交付内容。全球分布的CDN对确保各地高质量流媒体至关重要。
通过名为OCA的服务器战略部署于ISP和用户附近,在高峰期降低延迟并保障性能。通过在ISP网络预置内容,最大化带宽利用率并减少对骨干网络的依赖。
可扩展性是CDN的核心特性。全球约1000个地点部署OCA(包括偏远地区),满足各地增长需求。
向合格ISP提供OCA,使其直接从自身网络提供内容,既提升质量又降低ISP成本,建立双赢关系。

视频处理转型:微服务革命

通过实施微服务改造视频处理流水线,实现无与伦比的可扩展性和灵活性。从单体平台转向微服务平台开启了敏捷性和功能开发速度的新纪元。
视频处理流程的每一步由独立微服务代表,实现简化编排与解耦功能。从视频检测到编码,这些服务共同产出优质视频资产。微服务通过快速迭代适应业务需求变化,取得显著成效。

Open Connect播放流程

全球客户能够享受丝滑无暇的观看体验得益于Netflix Open Connect 的播放流程。其运作方式如下:

  1. 健康状态报告: 开放连接设备(OCAs)定期向亚马逊云服务(AWS)中的缓存控制服务汇报其学习到的路由信息、内容可用性及整体运行状况。
  2. 用户请求: 用户通过客户端设备上托管在AWS的Netflix应用程序请求播放电视剧或电影。
  3. 授权与文件选择: 在验证用户授权和许可后,AWS播放应用程序服务会精确选择处理播放请求所需的文件。
  4. 导向服务: AWS导向服务根据缓存控制服务保存的数据,选择用于提供文件的OCA设备。播放应用程序服务从导向服务获取这些OCA设备信息并构建其URL地址。
  5. 内容传输: 播放应用程序服务将相关OCA的URL发送至客户端设备。当请求的文件通过HTTP/HTTPS协议传输至客户端时,选定的OCA设备即开始提供服务。

下方图示展示了完整的播放流程:

数据库架构

利用Amazon S3实现无缝媒体存储

Netflix在2022年4月21日AWS服务中断期间的表现,充分证明了其云基础设施的价值,特别是对Amazon S3数据存储服务的依赖。通过整合SimpleDB、S3和Cassandra等服务,Netflix构建了能够承受此类中断的健壮系统。
作为基础设施的核心支柱,Netflix采用Amazon S3(简单存储服务)存储海量影视内容与原创作品。为服务全球数亿用户,平台需要管理PB级数据,而S3提供的可扩展、高可靠且易访问的存储特性成为理想选择。
内容库持续扩张时,S3使Netflix无需担忧硬件扩容或复杂存储架构维护,完美契合其"不牺牲用户体验"的扩展需求。

拥抱NoSQL实现弹性扩展

面对分布式架构的结构化存储需求,Netflix在发现传统关系型数据库的局限性后,全面转向NoSQL分布式数据库。技术栈中Cassandra, Hadoop/HBase, 和SimpleDB三大核心方案各具优势。

Amazon SimpleDB

迁移至AWS云时,SimpleDB凭借强大的查询能力、跨可用区自动复制和高持久性成为首选。其托管特性有效降低了运维成本,符合Netflix将非核心业务外包给云服务商的策略。

Apache HBase

作为Hadoop生态的高性能解决方案,HBase通过动态分区策略实现负载均衡与集群扩展,完美应对Netflix的数据增长挑战。分布式计数器、范围查询和数据压缩等功能,进一步强化了其一致性架构的健壮性。

Apache Cassandra

这款开源NoSQL数据库以性能、弹性和灵活性见长。动态集群扩展能力满足Netflix无限扩容需求,自适应一致性机制与灵活数据模型使其成为跨区域部署、避免单点故障的理想选择。
虽然需要面对学习曲线和运维成本,但NoSQL在可扩展性、可用性和性能方面的优势,使其成为Netflix长期云战略的关键支柱。

计费系统中的MySQL实践

Netflix计费系统作为向AWS云原生架构全面迁移的一部分经历了重大转型。由于Netflix运营高度依赖计费系统,此次迁移被谨慎处理以确保对会员体验的影响最小化,同时严格遵守严格的财务标准。
跟踪计费周期、监控支付状态以及向财务系统提供报告数据只是Netflix计费基础设施处理的众多任务中的几项。计费工程团队管理着一个包含批处理任务、API、与其他服务的连接器以及数据管理的复杂生态系统来实现这些功能。
数据库技术的选择是迁移过程中最重要的决策之一。由于支付处理需要可扩展性和ACID事务支持,MySQL被选为数据库解决方案。
构建健壮的工具链、优化代码和清理不必要数据都是迁移过程的一部分,以适应新的云架构。在转移现有会员数据前,团队使用代理和重定向器处理流量重定向,并采用干净数据集进行了全面测试流程。
将计费系统迁移至AWS上的MySQL是个复杂过程,需要周密规划、系统实施以及持续测试和迭代。尽管存在困难,迁移最终顺利完成,使Netflix能够利用AWS云服务的可扩展性和可靠性来支持其计费系统。
总之,将Netflix计费系统切换至AWS上的MySQL涉及大量工程工作并产生广泛影响。Netflix的系统架构已更新其计费系统,并采用基于云的解决方案为数字领域的未来发展做好准备。
以下是Netflix迁移后的架构:

Netflix架构中的内容处理流水线

Netflix内容处理流水线是处理内容合作伙伴提供的数字资产的系统化方法。主要包含三个阶段:内容摄取、转码和封装。

内容摄取

在摄取阶段,音频、定时文本或视频等源文件会经过严格的准确性和合规性检查。这些验证包括:语义信号域检查、文件格式验证、压缩码流可解码性验证、符合Netflix交付标准以及数据传输完整性检查。

转码与封装

通过摄取阶段的源文件会进行转码处理,生成输出基本流。随后这些流会被加密并封装至可分发的流式容器中。

通过Netflix金丝雀模型确保无缝流媒体体验

由于客户端应用是用户与品牌互动的主要方式,它们必须保持卓越品质。Netflix系统架构投入大量资源确保对更新版本进行全面评估。然而,由于Netflix需要在数千种设备上运行,并依赖数百个独立部署的微服务,全面内部测试变得困难。因此,必须依靠更新过程中获取的可靠现场数据来支持发布决策。
为加速客户端应用更新评估,Netflix系统架构组建了专门团队从现场挖掘健康信号。这项系统投资提高了开发速度,改善了应用质量和开发流程。

  1. 客户端应用: Netflix通过两种方式更新客户端应用:直接下载和应用商店部署。直接下载提高了分发控制力。
  2. 部署策略: 虽然定期增量发布的优势众所周知,但软件更新仍存在挑战。由于每个用户设备都以流形式传输数据,高效信号采样至关重要。Netflix采用定制部署策略应对各类设备和复杂微服务的独特挑战。策略因客户端类型而异(如智能电视与移动应用)。新版本通过分阶段发布逐步推出,提供快速故障处理和智能后端服务扩展。发布过程中监控客户端错误率和采用率可确保部署的一致性和有效性。
  3. 分阶段发布: 为降低风险并合理扩展后端服务,分阶段发布需要逐步部署新版本。
  4. AB测试/客户端金丝雀: Netflix采用强化的A/B测试变体"客户端金丝雀",通过完整应用测试确保数小时内完成及时更新。
  5. 编排: 编排减少了频繁部署和分析的工作量,有效管理A/B测试和客户端金丝雀。

总之,得益于Netflix采用客户端金丝雀模型,数百万用户能享受无瑕疵的流媒体体验,该模型确保了应用的频繁更新。

Netflix架构图示

Netflix系统架构是一个复杂生态系统:后端服务采用Python和Java(Spring Boot),数据处理和实时事件流使用Apache Kafka和Flink。前端采用Redux、React.js和HTML5提供沉浸式用户体验。多种数据库(包括Cassandra、HBase、SimpleDB、MySQL和Amazon S3)提供实时分析并处理海量媒体内容。Jenkins和Spinnaker实现持续集成和部署,AWS为整个基础设施提供可扩展性、可靠性和全球覆盖能力。
这些技术仅占Netflix庞大技术栈的一小部分,体现了其为全球观众提供完美娱乐体验的决心。

Netflix架构总结

Netflix系统架构彻底改变了娱乐行业。从DVD租赁服务发展为全球流媒体巨头,其技术基础设施是成功的关键。
依托AWS支持的Netflix架构确保全球用户的无中断流媒体体验,通过客户端、后端和内容分发网络(CDN)实现跨设备的无瑕疵内容传输。
HTML5的创新应用和个性化推荐提升了用户体验。
尽管面临挑战,向云原生架构的转型使Netflix更加强大。通过采用微服务、NoSQL数据库和云解决方案,Netflix在快速发展的在线娱乐领域为未来创新做好准备。任何技术企业都能从理解Netflix系统中获益。
简而言之,Netflix系统架构不仅关乎技术,更旨在改变我们的媒体消费方式。当观众追剧时,这套架构在幕后确保一切顺畅运行,提升每个人的娱乐享受。


【注】本文译自: A Look Into Netflix System Architecture

如何在Java程序中使用泛型

如何在Java程序中使用泛型

泛型可以使你的代码更灵活、更易读,并能帮助你在运行时避免ClassCastExceptions。让我们通过这篇结合Java集合框架的泛型入门指南,开启你的泛型之旅。

Java 5引入的泛型增强了代码的类型安全性并提升了可读性。它能帮助你避免诸如ClassCastException(当尝试将对象强制转换为不兼容类型时引发的异常)这类运行时错误。

本教程将解析泛型概念,通过三个结合Java集合框架的实例演示其应用。同时我们将介绍原始类型(raw types),探讨选择使用原始类型而非泛型的场景及其潜在风险。

Java编程中的泛型

  • 为何使用泛型?
  • 如何利用泛型保障类型安全
  • Java集合框架中的泛型应用
  • Java泛型类型示例
  • 原始类型与泛型对比

为何使用泛型?

泛型在Java集合框架中被广泛用于java.util.List、java.util.Set和java.util.Map等接口。它们也存在于Java其他领域,如java.lang.Class、java.lang.Comparable 和java.lang.ThreadLocal。

在泛型出现前,Java代码常缺乏类型安全保障。以下是非泛型时代Java代码的典型示例:

List integerList = new ArrayList();
integerList.add(1);
integerList.add(2);
integerList.add(3);

for (Object element : integerList) {
    Integer num = (Integer) element; // 必须显式类型转换
    System.out.println(num);
}

这段代码意图存储Integer对象,但没有任何机制阻止你添加其他类型(如字符串):

integerList.add("Hello");

当尝试将String强制转换为Integer时,这段代码会在运行时抛出ClassCastException。

利用泛型保障类型安全

为解决上述问题并避免ClassCastExceptions,我们可以使用泛型指定列表允许存储的对象类型。此时无需手动类型转换,代码更安全且更易理解:

List<Integer> integerList = new ArrayList<>();

integerList.add(1);
integerList.add(2);
integerList.add(3);

for (Integer num : integerList) {
    System.out.println(num);
}

List表示"存储Integer对象的列表"。基于此声明,编译器确保只有Integer对象能被添加至列表,既消除了类型转换需求,也预防了类型错误。

Java集合框架中的泛型

泛型深度集成于Java集合框架,提供编译时类型检查并消除显式类型转换需求。当使用带泛型的集合时,你需指定集合可容纳的元素类型。Java编译器基于此规范确保你不会意外插入不兼容对象,从而减少错误并提升代码可读性。

为演示泛型在Java集合框架中的使用,让我们观察几个实例。

List和ArrayList的泛型应用

前例已简要展示ArrayList的基本用法。现在让我们通过List接口的声明深入理解这一概念:

public interface List<E> extends SequencedCollection<E> { … }

此处声明泛型变量为"E",该变量可被任何对象类型替代。注意变量E代表元素(Element)。

接下来演示如何用具体类型替换变量。下例中将替换为

List<String> list = new ArrayList<>();
list.add("Java");
list.add("Challengers");
// list.add(1); // 此行会导致编译时错误

List声明该列表仅能存储String对象。如代码最后一行所示,尝试添加Integer将引发编译错误。

Set和HashSet的泛型应用

Set接口与List类似:

public interface Set<E> extends Collection<E> { … }

我们将用替换,使集合只能存储Double值:

Set<Double> doubles = new HashSet<>();
doubles.add(1.5);
doubles.add(2.5);
// doubles.add("three"); // 编译时错误

double sum = 0.0;
for (double d : doubles) {
    sum += d;
}

Set确保只有Double值能被添加至集合,防止因错误类型转换引发的运行时错误。

Map和HashMap的泛型应用

我们可以声明任意数量的泛型类型。以键值数据结构Map为例,K代表键(Key),V代表值(Value):

public interface Map<K, V> { … }

现在用String替换K作为键类型,用Integer替换V作为值类型:

Map<String, Integer> map = new HashMap<>();
map.put("Duke", 30);
map.put("Juggy", 25);
// map.put(1, 100); // 此行会导致编译时错误

此例展示将String键映射到Integer值的HashMap。添加Integer类型的键将不被允许并导致编译错误。

泛型命名规范

我们可以在任何类中声明泛型类型。虽然可以使用任意名称,但建议遵循命名规范:

  • E 代表元素(Element)
  • K 代表键(Key)
  • V 代表值(Value)
  • T 代表类型(Type)

应避免使用无意义的"X"、"Y"或"Z"等名称。

Java泛型类型使用示例

现在通过更多示例深入演示Java中泛型类型的声明与使用。

创建通用对象容器

我们可以在自定义类中声明泛型类型,不必局限于集合类型。下例中,Box类通过声明泛型类型E来操作任意元素类型。注意泛型类型E声明于类名之后,随后即可作为属性、构造器、方法参数和返回类型使用:

// 定义带泛型参数E的Box类
public class Box<E> {
    private E content; // 存储E类型对象

    public Box(E content) { this.content = content; }
    public E getContent() { return content; }
    public void setContent(E content) { 
        this.content = content;
    }

    public static void main(String[] args) {
        // 创建存储Integer的Box
        Box<Integer> integerBox = new Box<>(123);
        System.out.println("整数盒内容:" + integerBox.getContent());

        // 创建存储String的Box
        Box<String> stringBox = new Box<>("Hello World");
        stringBox.setContent("Java Challengers");
        System.out.println("字符串盒内容:" + stringBox.getContent());
    }
}

输出结果:

整数盒内容:123
字符串盒内容:Java Challengers

代码要点:

  • Box类使用类型参数E作为容器存储对象的占位符,允许Box处理任意对象类型
  • 构造器初始化Box实例时接受指定类型对象,确保类型安全
  • getContent返回与实例创建时指定的泛型类型匹配的对象,无需类型转换
  • setContent通过类型参数E确保只能设置正确类型的对象
  • main方法创建了存储Integer和String的Box实例
  • 每个Box实例操作特定数据类型,展现泛型在类型安全方面的优势

此例展示了Java泛型的基础实现,演示了如何以类型安全方式创建和操作任意类型对象。

处理多数据类型

我们可以声明多个泛型类型。以下Pair类包含<K, V>泛型值。如需更多泛型参数,可扩展为<K, V, V1, V2, V3>等,代码仍可正常编译。

Pair类示例:

class Pair<K, V> {
    private K key;
    private V value;

    public Pair(K key, V value) {
        this.key = key;
        this.value = value;
    }

    public K getKey() { return key; }
    public V getValue() { return value; }

    public void setKey(K key) { this.key = key; }
    public void setValue(V value) { this.value = value; }
}

public class GenericsDemo {
    public static void main(String[] args) {
        Pair<String, Integer> person = new Pair<>("Duke", 30);

        System.out.println("姓名:" + person.getKey());
        System.out.println("年龄:" + person.getValue());

        person.setValue(31);
        System.out.println("更新后年龄:" + person.getValue());
    }
}

输出结果:

姓名:Duke
年龄:30
更新后年龄:31

代码要点:

  • Pair<K, V>类包含两个类型参数,适用于任意数据类型组合
  • 构造器与方法使用类型参数实现严格类型检查
  • 创建存储String(姓名)和Integer(年龄)的Pair对象
  • 访问器和修改器方法操作Pair数据
  • Pair类可存储管理关联信息而不受特定类型限制,展现泛型的灵活性与强大功能

此例展示泛型如何创建支持多数据类型的可复用类型安全组件,提升代码复用性和可维护性。

让我们再看一个示例。

方法级泛型声明

泛型类型可直接在方法中声明,无需在类级别定义。若某个泛型类型仅用于特定方法,可在方法签名返回类型前声明:

public class GenericMethodDemo {

    // 声明泛型类型<T>并打印指定类型数组
    public static <T> void printArray(T[] array) {
        for (T element : array) {
            System.out.print(element + " ");
        }
        System.out.println();
    }

    public static void main(String[] args) {
        Integer[] intArray = {1, 2, 3, 4};
        printArray(intArray);

        String[] stringArray = {"Java", "Challengers"};
        printArray(stringArray);
    }
}

输出结果:

1 2 3 4
Java Challengers

原始类型与泛型对比

原始类型指未指定类型参数的泛型类或接口名称。在Java 5引入泛型前,原始类型被广泛使用。现今开发者通常仅在与遗留代码兼容或与非泛型API交互时使用原始类型。即使使用泛型,仍需了解如何识别和处理原始类型。

典型原始类型示例——未指定类型参数的List声明:

 List rawList = new ArrayList();

此处List rawList声明了一个未指定泛型参数的列表。rawList可存储任意类型对象(Integer、String、Double等)。由于未指定类型,编译器不会对添加至列表的对象类型进行检查。

使用原始类型的编译警告

Java编译器会对原始类型使用发出警告,提醒开发者可能存在的类型安全隐患。当使用泛型时,编译器会检查集合(如List、Set)中存储的对象类型、方法返回类型和参数是否匹配声明类型,从而预防如ClassCastException的常见错误。

使用原始类型时,由于未指定存储对象类型,编译器无法进行类型检查,因此会发出警告提示你绕过了泛型提供的类型安全机制。

编译警告示例

以下代码演示编译器如何对原始类型发出警告:

List list = new ArrayList(); // 警告:原始使用参数化类'List'
list.add("hello");
list.add(1);

编译时通常会显示:

注意:SomeFile.java使用了未经检查或不安全的操作。
注意:使用-Xlint:unchecked重新编译以获取详细信息。

使用-Xlint:unchecked参数编译将显示更详细警告:

warning: [unchecked] unchecked call to add(E) as a member of the raw type List
    list.add("hello");
            ^
  where E is a type-variable:
    E extends Object declared in interface List

若确信使用原始类型不会引入风险,或处理无法重构的遗留代码,可使用@SuppressWarnings("unchecked")注解抑制警告。但需谨慎使用,避免掩盖真实问题。

使用原始类型的后果

尽管原始类型有助于向后兼容,但存在两大缺陷:类型安全性缺失和维护成本增加。

  • 类型安全性缺失:泛型的核心优势是类型安全,使用原始类型将丧失这一优势。编译器不进行类型正确性检查,可能导致运行时ClassCastException。
  • 维护成本增加:使用原始类型的代码缺乏泛型提供的明确类型信息,维护难度加大,易产生仅在运行时暴露的错误。

类型安全问题示例:使用原始类型List而非泛型List时,编译器允许添加任意类型对象。当从列表检索元素并尝试强制转换为String时,若实际为其他类型将导致运行时错误。

泛型知识要点回顾

泛型以高度灵活性提供类型安全保障。以下回顾关键要点:

泛型是什么?为何使用?

  • code.Java 5引入泛型以提升代码类型安全性和灵活性
  • 主要优势在于帮助避免ClassCastException等运行时错误
  • 泛型广泛应用于Java集合框架,也见于Class、Comparable、ThreadLocal等组件
  • 通过阻止不兼容类型插入实现类型安全

Java集合中的泛型

  • List和ArrayList:List允许指定元素类型E,确保列表类型专一
  • Set和HashSet:Set限定元素为类型E,保持一致性
  • Map和HashMap:Map<K,V>定义键值类型,提升类型安全性和代码清晰度

泛型使用优势

  • 通过阻止不兼容类型插入减少错误
  • 明确类型关联提升代码可读性和可维护性
  • 便于以类型安全方式创建和管理集合等数据结构

AI时代的非人类身份安全

AI时代的非人类身份安全

随着AI在企业中的崛起,攻击面也在不断扩展。了解如何保护非人类身份(Non-Human Identities, NHIs)并防止未经授权的访问。

AI时代的非人类身份安全


非人类身份(NHIs)近期成为焦点并非偶然——随着AI工具和自主代理的快速普及,企业的NHI数量正呈爆炸式增长。这一趋势也引发了关于机器身份与治理的大量研究和讨论。

与系统的普通用户类似,NHI(如AI代理、机器人、脚本和云工作负载)通过密钥(secrets)进行操作。这些凭证赋予其访问敏感系统和数据的权限,可能以多种形式存在,且必须从创建到销毁全程受控。然而,机器无法使用多因素认证或通行密钥(passkeys),而开发者在部署应用时可能生成数百个此类凭证。


AI加速NHI的扩张与风险

企业AI的采用速度惊人,迫使开发者以前所未有的速度推出NHI。AI虽能提升效率,但也带来隐私泄露、密钥暴露和不安全代码等风险。大型语言模型(LLMs)的应用场景令人兴奋,但需谨记:技术引入越多,攻击面越大——尤其是当AI代理被赋予自主权时。


AI代理带来的NHI风险

1. AI代理与密钥泛滥(Secrets Sprawl)

“AI代理”是基于LLM的系统,可自主决策如何完成任务。它们不同于传统的确定性机器人(仅按开发者预设的步骤执行),而是能访问内部数据源、搜索互联网,并代表用户与其他应用交互。

例如,一个AI采购代理可以分析需求、通过电商平台比价、与AI聊天机器人议价,甚至自主下单。每个安全通信都需要凭证,而这类代理需通过DevOps流程部署,导致更多跨环节的身份验证需求。密钥往往在系统、日志和仓库中意外散落。

企业常为AI代理赋予比传统机器人更广泛的读写、甚至创建和删除权限。由于AI代理的自主性,若权限限制过严,其任务可能受阻;但宽松权限又易导致过度授权。

风险点:任一密钥泄露都可能引发数据泄露或未经授权的交易。需通过最小权限访问、API密钥保护和审计日志来强化NHI治理,并关注密钥存储之外的暴露风险。

2. 孤立的API密钥(Orphaned API Keys)

孤立API密钥指不再与用户账户关联的密钥(如员工离职后未被删除的密钥)。在NHI场景中,密钥的“归属权”模糊(开发者?运维团队?),导致其极易被遗忘却仍有效。

关键问题:谁应对这些密钥引发的安全漏洞负责?

3. 基于提示的架构与敏感数据暴露

AI助手(如ChatGPT、Gemini、GitHub Copilot)依赖提示(prompt)架构,通过上下文、命令和数据与LLM交互。这种模式虽简化了开发,但也可能导致敏感信息(如API密钥)被写入提示或日志。

案例:财务团队用AI聊天机器人处理发票时,若提示中包含API key ABC123,该密钥可能被明文记录。若日志未加密,攻击者可借此入侵发票系统。

防护措施:需阻止开发者及用户将敏感数据嵌入提示或日志,并扫描LLM输出中的异常信息。

4. AI代理与数据收集风险

AI代理常从以下来源收集数据:

  • 云存储(如AWS S3、Google Drive)
  • 企业应用(如Jira、Confluence、Salesforce)
  • 通信系统(如Slack、Microsoft Teams)

风险:若AI代理可访问这些系统中的任何数据,攻击者亦可滥用其NHI权限。需定期轮换所有内部系统的密钥,并清理日志。

5. AI生成代码与嵌入式密钥

GitHub Copilot、Amazon CodeWhisperer等AI编码工具已被超50%的开发者使用。然而,AI生成的代码可能诱导开发者硬编码密钥(如API密钥、数据库凭证)。

案例:开发者要求Copilot生成调用云服务的代码时,可能得到:

import requests  
API_KEY = "sk_live_ABC123XYZ"  
response = requests.get("https://api.example.com/data", headers={"Authorization": f"Bearer {API_KEY}"})  

若匆忙中替换为真实密钥并提交至代码仓库,凭证可能被泄露。

防护:通过预提交钩子(pre-commit hooks)等工具扫描代码,防止密钥泄露。


未来方向:如何保护非人类身份

  1. 发现密钥:自动识别企业环境中的所有AI代理凭证(包括存储库内外)。
  2. 评估风险:明确密钥的用途、访问范围及关联的关键系统。
  3. 动态防护:实时监控提示和日志,防止敏感数据嵌入。

让NHI治理跟上AI速度

AI代理的部署速度与复杂性并存,既带来效率提升,也伴随风险。随着AI普及,保护机器身份已非可选,而是必需。唯有通过系统化的密钥管理、权限控制和持续监测,才能在AI时代实现安全与创新的平衡。


【注】本文译自:
Non-Human Identity Security in the Age of AI