单体架构中的事件驱动架构: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

氛围编程:IT领导者须知

执行摘要

  • 氛围编程能加速开发与创新,但企业高管必须加强治理、安全与审查流程以保护业务。
  • 团队能快速测试想法并交付最小可行产品,从而缩短上市时间并提升对业务需求的响应能力。
  • 开发人员与非技术人员能更高效地协作,降低入门门槛并促进创新。

想象一下,您可以通过摩擦一盏神灯,用简单直白的语言向精灵描述您的需求,它就能为您生成一个功能齐全的应用程序。虽然神灯并不存在,但AI编程助手在很大程度上实现了这个愿望——无论其影响是好是坏。借助大语言模型,开发人员可以输入自然语言提示,并生成任何编程语言的代码。OpenAI联合创始人安德烈·卡帕西在2025年创造了"氛围编程"一词,用以描述"完全沉浸在感觉中,拥抱指数级增长,甚至忘记代码本身的存在"。

这种新范式标志着从深思熟虑、逐行编写代码,转向更流畅、更直观的人机意图与执行之间的协作。

氛围编程并非要取代开发人员,而是一种加速数字化转型的战略推动器,它能提高生产率,并且是打造快速上市工具的一种经济高效的选择。然而,IT高管必须将治理与赋能相结合,以最大化其价值,同时控制氛围编程带来的风险。

氛围编程如何运作?

开发人员首先选择一个AI编程助手,并描述他们想要的功能或特性。接着,AI会回应代码建议,开发人员可以审查、接受或优化这些建议。然后,开发人员继续迭代,通过向AI发出具体指令来添加新功能或进行调整,从而创建一个动态的、对话式的工作流程。

氛围编程 vs. 传统编程

传统上,编程过程非常结构化和有条不紊,而氛围编程描述的则是一种更具创造性或基于流程的方法。以下是这两种方法差异的细目分类:

氛围编程 传统编程
语言 自然语言 编程语言
焦点 宏观大局 / "感觉" 细节导向
审查流程 信任AI 同行代码审查
界面 AI代理 键入代码 / IDE
开发速度 几分钟到几小时 数天到数周甚至更长
入门门槛 无需具备代码知识 需要懂得如何编写所有代码
创作过程 探索与实验,如同即兴弹奏吉他 有计划、精确且可重复,如同创作交响乐

氛围编程的优势

氛围编程提供了若干关键优势,特别是对于那些希望快速将想法付诸实施并减少重复性任务的开发人员。

  • 更快的开发速度。 经验丰富的开发人员使用氛围编程可以在几小时内完成一个应用程序,而传统的开发时间则需要数天或数周。
  • 更低的入门门槛。 开发人员进行氛围编程所需的唯一语言是他们自己的自然口语。氛围编程使开发人员能够在不懂编码的情况下启动一个功能正常的项目。AI对于正在学习编码或理解应用程序工作原理的开发人员来说,也是一个强有力的工具。
  • 快速原型制作。 氛围编程的速度使开发团队能够快速创建功能性的最小可行产品。这使得氛围编程非常适合在争抢市场先机时向投资者展示项目。此外,它还通过实验实现了更快的功能迭代。
  • 爱好或内部项目。 如果无需考虑公共访问或安全问题,氛围编程是理想选择。其速度和易用性使开发人员能够快速解决问题并构建解决方案。
  • 多模态编程。 氛围编程将代码生成扩展到集成开发环境之外的键入方式,包括语音到文本提示。
  • 员工协作与生产力。 开发人员从编写代码转向审查和优化代码。其他员工,如分析师和产品经理,也可以对编程提供意见,从而实现业务和IT部门的跨职能协作。

氛围编程的局限性

氛围编程听起来是否好得令人难以置信?这取决于它的使用方式。使其成为小型应用程序和原型强大工具的特点,在大型代码库或安全性优先的场景中,却可能成为其负担。

  • 错误与幻觉。 生成代码的AI与任何其他流行的AI工具一样容易出现幻觉。几位计算机科学研究人员的一项研究发现,商业AI模型平均有5.2%的情况下会推荐不存在的软件包。相比之下,开源模型的这一比例跃升至21.7%。
  • 有限的技术复杂性。 提供给AI的每个提示都有一个有限的上下文窗口——类似于内存——其中包含大量关于您环境的数据,例如您打开的标签页内容。这为AI提供了上下文,使其能够做出明智的决策。然而,不同AI模型的上下文窗口大小不同,并且较大的上下文大小可能会影响AI的性能。项目越复杂,AI理解项目所需的上下文就越多。
  • 难以调试和维护。 未经审查就接受AI生成的代码,可能会导致创建一个无人理解代码作用及缘由的代码库。如果AI引入了其自身无法修复的错误,而开发人员又无法理解其输出,那么进展将完全受阻。
  • 缺乏原创性。 编码AI基于现有的代码示例进行训练,只能生成它所知道的内容。它无法完全靠自己提出革命性的过程或想法。

企业高管应将氛围编程生成的代码视为快速原型。然而,程序仍然需要经过审查。对于面向客户的服务,必须进行审查;如果涉及其他敏感数据,则必须检查其是否符合法规要求。

氛围编程的安全顾虑

一位名叫Leo的开发人员在X上宣布,他发布了一个完全通过氛围编程构建的SaaS应用程序。两天内,他的应用程序就遭到了黑客的攻击,Leo发帖称出现了各种随机问题。在整个项目中如此重度依赖AI会导致安全问题层出不穷。原因如下:

  • LLM或平台中的漏洞。 任何依赖外部组件的软件产品都会继承潜在的漏洞。AI编码平台也不例外。最近,安全研究人员在氛围编程平台Base44中发现了暴露的API端点,使得攻击者能够使用非机密的app_id值创建新账户来访问私有应用程序,从而绕过所有身份验证机制。
  • 开发人员错误。 氛围编程工具会精确地生成开发人员所要求的内容。如果开发人员在其提示中未包含安全实践,AI将不会生成遵循最佳安全实践的代码。
  • 数据隐私。 LLM通过摄取数据作为训练数据来改进模型。如果项目涉及敏感数据,例如支付信息、健康记录、专有代码或商业机密,则AI工具必须实施严格的数据隔离,以防止AI在其他应用程序中使用受保护的信息。

如何实施氛围编程

考虑到其局限性,在将氛围编程集成到项目中时最好谨慎行事,以充分利用其优势。

  1. 规划项目。 氛围编程和传统编程共有的一个特点是,两者在从一开始就有清晰计划指导时最为有效。确定您要构建什么,并将步骤分解为易于消化的小部分。牢记您希望为项目采用的安全和代码标准。
  2. 决定您的"氛围"策略。 真正的氛围编程定义为将所有决定交给AI。AI辅助编码是一种混合方法,开发人员向AI提示代码,然后在批准前仔细检查输出。找到最符合您优先事项的平衡点。
  3. 选择AI编程助手。 并非所有模型都构建得一样。有些专门用于代码生成,而其他则能解决更复杂的问题。不同模型在数据隔离、隐私以及成本方面有不同的政策。请仔细选择最适合您项目的AI代理。
  4. 使用源代码控制。 这对任何类型的编码都是个好主意,但对于氛围编程尤其重要。当您的项目处于良好工作状态时,为自己创建检查点,以便您可以根据需要轻松调整。
  5. 迭代。 一次创建一个功能,并在每个提示中提供尽可能多的细节和上下文。优化和重构您的代码,直到它符合您的设想。
  6. 测试。 确保您的项目在每个步骤中都正常工作。AI非常擅长生成自动化测试,但请确保您也执行手动测试,包括依赖项验证和自动化测试,以阻止合并未知/无效的软件包。
  7. 设定防护措施。 务必建立安全审查和编码标准。氛围编程项目仍应审查其准确性和合规意识,因此审批工作流是必要的。

IT高管可追踪的指标

这些指标应衡量交付速度、缺陷率和生产率的改进。以下是高管可用于追踪氛围编程的几个指标示例:

  • 原型制作时间(引入氛围编程工具前后对比)。

  • AI生成的拉取请求在自动化关卡失败的比例,例如测试、代码 lint 检查和软件成分分析。

  • 幻觉检测率,包括无效软件包或不良依赖项。

  • 每月归因于AI生成代码的安全事件

  • 每个正常工作的原型的成本,以客观显示投资回报率。

Java有哪些优势?

Java 的价值

当具有开创性的 Java 白皮书在 1995 年推出该语言时,它列出了七项使其超越竞争对手的核心价值。如今,Java 为在 AWS 和 Google Cloud 等主要云上运行的大规模系统提供动力,这使得这些价值对于现代部署和认证路径更具现实意义。
那份白皮书撰写至今已过去近 30 年,虽然其中许多价值仍然有效,但在 2025 年,选择 Java 作为您的部署平台的理由比以往任何时候都多。如果您关注 Java 路线图或热门技术博客,您会看到 Java 出现在云架构师、开发人员和数据领域的各个路径中。

Java 的优势

以下是 Java、JVM 和 JDK 的十大现代优势:

  1.  Java 是开源的
  2.  Java 是由社区驱动的
  3.  Java 快速且高性能
  4.  Java 易于学习
  5.  Java 是静态类型的
  6.  Java 拥有专家领导
  7.  Java 功能添加迅速
  8.  Java 是面向对象的
  9.  Java 支持函数式编程
  10. Java 优先考虑向后兼容性

    Java 是开源的

    Java 自 2011 年起已开源。任何人都可以查看 JDK 的源代码并创建定制化和优化的构建版本。这种开放性与 AWS 开发者GCP 专业云开发者等云学习路径非常契合,在这些路径中,基于 Java 的微服务很常见。
    流行的 OpenJDK 和 JVM 发行版包括:

    •   Azul 的高性能实现
    •   Oracle 的授权版本
    •   AdoptOpenJDK(现称为 Adoptium)
    •   IBM 的 Java 运行时
    •   Amazon Corretto
    •   Red Hat 的 OpenJDK 发行版
    •   微软构建的 OpenJDK
    •   高性能的 GraalVM
      谷歌甚至不惜借用 Java 源代码来构建自己的移动操作系统。这样做在道德上可能值得商榷,但美国最高法院表示,为构建 Android 操作系统而侵犯 Oracle 的版权是完全公平合理的。

      Java 是由社区驱动的

      Oracle 拥有 Java 商标这一事实在技术社区中引发了无休止的、任性的焦虑。然而,事实是 Java 通过 Java 社区进程向前发展,而非根据 Larry Ellison 的个人意愿。社区驱动的学习也体现在认证项目中,如 AWS 云从业者AWS 解决方案架构师GCP 助理云工程师
      JCP 是向 Java 编程语言添加新功能、新规范和新 API 的途径。在过去的 20 年里,JCP 完成了以下工作:

    •   增加了 1000 多名成员
    •   欢迎了 200 多家公司
    •   鼓励独立开发人员加入

社区支持和贡献是 Java 为软件开发社区带来的巨大优势之一,这种精神您同样可以在 AWS DevOps 工程师GCP DevOps 工程师圈子里找到。

Java 快速且高性能

Java 虚拟机是一个抽象层,使得 Java 程序能够跨平台运行。这种可移植性与 AWS 安全专家AWS 数据工程师GCP 专业数据工程师路径中的云工作负载非常匹配。
JVM 架构中立是 Java 的一大优势,但人们总是担心所需的抽象层可能会严重影响性能。但事实并非如此
在 JVM 上运行的 Java 可能无法达到与 C++ 或 Rust 等编译语言相同的性能。然而,垃圾收集器工作方式的改进、即时编译器的使用以及大量其他底层优化为 Java 平台带来了接近原生的性能。

Java 易于学习

1995 年的 Java 白皮书曾夸耀 Java 易于学习,因为它采用了该语言发布时流行的、类似 C 的熟悉语法。如果您喜欢结构化的目标和问责制,来自 Scrumtuous 的 Scrum 式冲刺可以帮助您规划 Java 学习节奏。
2023 年,JDK 拥有了 JShell,这使得 Java 对 Python 和 JavaScript 开发人员来说变得熟悉且易于学习。应试耐力可以通过像 Udemy 实践考试合集这样的题库来培养,即使它针对的是 AWS。这种训练方法可以很好地迁移到 Java 考试和云认证中。
此外,像 Replit 和 OneCompiler 这样的在线编译器允许学习者无需安装 IDE 或配置 JAVA_HOME 即可开始使用 Java。如果您的最终目标包括云角色,请参阅基础的 AWS 云从业者GCP 助理云工程师页面。

Java 是静态类型的

与 Python 或 JavaScript 等语言不同,Java 是静态类型的。
在 Java 中,您需要指定变量是 float、double、int、Integer、char 还是 String。这比动态类型语言提供了两个显著好处:

  •   它使得管理大型代码库更加容易,这对于 AWS 解决方案架构师GCP 云架构师非常重要。
  •   它使得优化运行时环境成为可能,这对 AWS 数据工程师GCP 数据库工程师等数据密集型角色有所帮助。
    Java 在 Python 和 JavaScript 失败的情况下仍能扩展的原因,通常可以追溯到 Java 的静态类型特性。

    Java 语言的静态类型特性是其主要优势。

    Java 拥有专家领导

    虽然该语言通过 Java 社区进程向前发展,但有两位杰出的软件架构师在 Oracle 内部指导着 Java 平台的演进。领导力和管理也是云项目中的主题,例如 AWS 专业级解决方案架构师以及专注于安全的路径,如 AWS 安全专家GCP 安全工程师

    功能采纳迅速

    与其他语言相比,Java 的优势之一是采纳新功能和响应社区需求的速度非常快。同样的迭代速度也反映在实践角色中,如 AWS DevOps 工程师GCP DevOps 工程师,这些角色会持续部署 Java 服务。

    Java 是面向对象的

    Java 用户认为这是理所当然的,但讨论 Java 的优势不能忽视 Java 是完全面向对象的,它实现了重要的 OOA&D 概念,例如:

  •   继承
  •   组合
  •   多态
  •   封装
  •   接口

对于使用 Scrum 主管产品负责人角色等框架组织工作的团队来说,Java 的对象建模自然地契合了映射到领域驱动设计的待办事项项。

Java 支持函数式编程

软件开发行业出现了向函数式编程的重大转变,而 Java 一直是这一趋势的重要组成部分。如果您旨在将 ML 服务与 Java 微服务融合,请探索 AWS 机器学习AWS AI 从业者路径。
函数式编程和不可变类型的使用可以使程序更快、更简洁且更易于理解。Java 在 Java 8 中进行了重大转变,引入了 Java Streams 和 lambda 表达式,这开启了 Java 函数式编程的新时代。您可以使用该语言同时进行函数式编程和面向对象编程,这是一个主要优势。

向后兼容性

随着 Java 社区推动 API 的重大更改,该语言的维护者始终优先考虑向后兼容性和非破坏性功能的添加。稳定性是 Java 在准备 AWS 助理级解决方案架构师GCP 专业云架构师角色的架构师中保持首选的原因之一。
即使引入了作为函数式编程的默认接口和 lambda 表达式,Java 平台也保持了向后兼容性。早期版本编写的代码可以在更新的环境中运行,无需重新编译。
在 2025 年,Java 的价值众多,因为 JDK 和 JVM 对于包含 AWS 云从业者解决方案架构师开发者数据工程师安全专家,以及高级角色如 AWS 专业级解决方案架构师,还有 GCP 路径如 GCP 数据从业者GCP 专业云网络工程师GCP Workspace 管理员GCP 机器学习工程师GCP 生成式 AI 负责人GCP 数据库工程师在内的多云职业而言,比以往任何时候都更具现实意义。

Java、JVM 和 JDK 的诸多优势持续推动着该编程语言的采用。


【注】本文译自:What are the advantages of Java?

AI智能体是加速器,而非开发者替代品

将AI集成到应用开发中的核心挑战,不在于其协助能力,而在于我们能在多大程度上放心地将控制权委托给它。

尽管AI智能体可以完美地执行那些曾被认为人类专属的任务,但它们同样可能在紧接着的下一段代码中犯下令人震惊的错误。

这些错误尖锐地提醒我们,即使是最先进的AI编程助手,仍然缺乏对世界运行方式的理解。这一根本区别将当前的生成式AI与通用人工智能(AGI)的愿景区分开来。考虑到这一点,让我们来看看AI智能体如何成为出色的开发加速器,却无法取代人类开发者。

LLM的推理并非逻辑推理

即使是复杂的智能体AI——构建于拥有日益庞大的上下文窗口和复杂工作流程的大型语言模型(LLM)之上——也依赖于语义模式匹配。它们无法对底层的因果关系和相互依赖提供真正的洞见。

让人类难以理解这一点的是,LLM在阐述其决策过程时具有令人信服的方式,常常模仿一种逻辑递进,暗示其对因果关系的理解,而实际上它们并不具备这种理解。它们通过拼凑统计上可能性高的人类推理文本片段来实现这一点。虽然这看起来像是逻辑推理,但它基于从训练数据中得出的概率计算,而非对步骤之间因果关系的直接理解。

LLM模仿逻辑推理,但无法掌握因果关系。

将这比作一位主演医疗电视剧的演员,他多年来记住了数千小时的对话、纪录片和真实咨询记录。他可以完美地进行鉴别诊断,像经验丰富的医生一样自信地用专业词汇滔滔不绝地说出症状、检测结果和治疗方案。他知道"向左臂放射的胸痛"通常出现在关于心脏病的场景中,"全血细胞计数和代谢指标组"跟在"我们来做些检查"之后,而担忧的表情伴随着关于肿瘤的讨论。

一位对医疗话题表现出表面理解的医疗剧演员,是比喻AI无法掌握因果关系的绝佳例子。

他们的表演如此令人信服,以至于任何观众都会相信他们懂医。但他们根本不知道阿司匹林为什么能稀释血液,心脏病发作时会发生什么,或者为什么一种治疗有效而另一种会致命。他们只是在背诵他们记住的各种医疗对话的变体,拼凑那些在统计上共同出现的片段,却不理解这些模式代表了真实的生物过程,其中顺序和因果关系 literally 意味着生与死。翻译到应用开发中,这通常意味着出色的结果之后紧接着灾难性的失败,反之亦然。

统计模式而非因果真相

LLM非常擅长在难以想象的大量文本中寻找并连接模式。尽管这些文本中有许多描述了世界的运作方式,但LLM并不理解这些描述的实际含义。相反,它将文本转换成数字——向量——这些数字捕获的是统计关系,而非因果真相。然后,模型将这些数字翻译回人类语言,而在这一切之下,它始终只是在跟踪和 shuffling 数字,而不是意义。例如,"charge"、"payment"和"credit card"这些词可能在向量空间中位置接近,因为它们经常在文本中共同出现,而"profile"、"lookup"和"fetch"则形成另一个集群——但模型实际上并不知道一组涉及金钱,而另一组不涉及。

LLM只处理词组之间的统计关系。

事物并非表面所见

由于编程语言是高度结构化的,这种数值上的 shuffling 可以产生优秀的代码。虽然AI模型并不像开发者那样"理解"代码,但它可以可靠地将输入模式映射到输出,将框架映射到样板代码,将语法映射到语义,其方式常常看起来与人类代码无异。例如,当被要求"用Python和Flask构建一个REST API"时,模型无法推理HTTP或数据库——它只是回忆起@app.route通常出现在函数定义之前,GET请求常常映射到返回jsonify,而错误处理经常涉及try/except块。结果往往是结构良好的Flask代码,即使它源于模式回忆而非真正的理解。

人类需要保持在循环中,以应对AI缺失的上下文和推理能力。

例如,为重试逻辑加固微服务听起来很简单——然而实际情况并非如此。要求AI助手"在失败时添加重试",你可能会得到一段在任何错误时都重试所有操作的代码。这对于幂等的(或无状态的)读取操作(例如"获取配置文件")来说没问题,因为重复调用只会返回相同的数据。
将相同的逻辑应用于非幂等操作——扣款、创建订单、发送电子邮件、查询数据库——你就会招致灾难:重复扣款、重复订单、通知风暴、数据库中的重复记录。解决方法并非魔术,而是判断力。人类首先对操作进行分类——幂等与非幂等——仅在瞬态错误时重试,并且对于任何有副作用的操作,都需要幂等性密钥和服务端去重。虽然这仍然为人类开发者节省大量时间,但他们仍然需要将其技能和专业知知融入其中,否则灾难可能并且将会随机发生。

理解模式匹配的局限性很棘手

原则上,模式匹配难道不能识别出对信用卡扣款需要采用与检索客户资料或产品信息不同的API调用重试方法吗?是的,它可以,但人类无法事先知道这一点,因为这取决于该特定模型的训练数据是否包含了执行标准POST或GET请求的重试函数。
模型未能建立操作类型与其现实后果之间的联系;它仅仅回忆统计关联。为了让模型避免这个错误,训练数据需要包含清晰、一致且重复出现的配对,将操作类型与重试策略及其后果联系起来。
理想情况下,数据会明确对比可以安全重试的代码与必须避免重试的代码。或许它还包括了事后分析或警告,描述了误用重试时发生的情况。然而,模型是否摄入了足够的训练数据来做出这种区分,我们人类无法确定。更棘手的是,由于其概率性质,模型可能在某一次做出了区分,但在接下来的三次尝试中却没有。
这个例子说明了为什么简单地添加更多训练数据通常不是答案,因为必要的数据可能并不以书面形式存在。或者更糟的是,训练数据可能包含了强化错误概括的内容。无论哪种情况,人类用户都无法知道是否如此,并且需要全面理解特定问题应如何解决。

AI的价值是真实的,开发团队可以受益

只要清楚地理解其局限性,AI智能体可以显著提高人类开发者在整个开发生命周期中的生产力。从收集需求并将其转化为用户故事,一直到检测并部署应用程序,AI智能体可以为人类提供建议、自动化验证和快速原型设计,从而显著缩短迭代周期。
AI智能体应被视为力量倍增器,可以处理开发的机械性方面,例如基于现有示例和文档生成样板代码、编写测试用例和记录API。另一方面,人类则负责真正理解业务影响、决定架构权衡,以及解决需要应用抽象逻辑能力的复杂问题。

AI对SDLC的生产力影响

下表分析了AI对软件开发生命周期(SDLC)中不同活动的生产力影响,以及AI对每项活动的能力、所需的人力参与程度和每项活动的风险水平。

AI对SDLC活动的生产力影响

活动 AI的生产力影响 AI能力描述 人力参与需求 风险水平
需求收集 低 – 中 根据笔记、会议记录、电子邮件和其他材料生成用户故事。 高 – 确保故事在成本、风险和回报方面与当前业务优先级保持一致。 高 – 被误解的需求将贯穿整个项目。
架构与设计 建议模式、识别瓶颈并生成初始图表,作为人类构建的坚实起点。 关键 – 考虑系统范围影响、做出战略权衡并监控技术趋势。 高 – 糟糕的架构决策难以逆转且成本高昂。
代码生成 构建定义良好的样板代码并解决精确定义的问题。保持文档更新。 中 – 掌控业务逻辑和边缘情况。 中 – 通常难以完全掌控AI编写的代码。
代码审查 捕获语法错误、发现安全漏洞、发现性能问题并建议优化。 高 – AI会遗漏依赖于上下文的问题和架构问题。 中 – 人类需要对审查负全责。
测试 创建单元测试、集成测试、自动化回归测试并发现边缘情况。 低 – 对于测试生成,但 高 – 对于测试策略。 中 – 人类必须对测试的完整性和相关性负责。
调试 分析堆栈跟踪并就已知错误建议修复方法。 中 – 指导调试过程。 低 – 错误的修复通常很容易发现。
文档编写 生成API文档、自述文件、内联注释、用户指南和变更日志。 低 – 对于面向用户的文档。 低 – 不正确的文档通常可以在没有重大影响的情况下得到纠正。
部署与CI/CD 创建部署清单、构建IaC模板、生成流水线配置。 高 – 生产部署需要仔细检查。 高 – 任何问题都会直接影响生产环境。
监控 添加检测、分析日志并生成警报规则。 中 – AI在没有上下文的情况下难以确定优先级。 中 – 误报会浪费时间。

来源:Torsten Volk, Omdia • 获取数据 • 使用 Datawrapper 创建

结论

宣布AI智能体正在接管开发者工作的技术领导者们,对AI当前能力产生了不切实际的期望。这导致许多企业高管认为开发者工时不再是他们所能构建内容的限制因素。金融分析师可以创建自己的投资组合再平衡工具;医疗保健管理员可以构建患者排班系统;供应链经理可以开发库存优化仪表板;或者营销总监可以构建个性化的活动自动化平台,而无需编写一行代码。虽然他们可以为许多此类业务任务实现概念验证,但架构、开发和交付企业级软件仍然极大地依赖于人类开发者的技能和经验。
然而,AI智能体可以通过为人类开发者完成大量基础性工作来显著加速SDLC。创建测试用例、用监控代理自动检测复杂软件、记录数万行主机代码以及精确定义复杂的基础设施清单,仅仅是AI智能体可以帮助人类开发者的几个例子。
人类与AI智能体之间的SDLC必须是协作的、迭代的并接受持续监督。确定如何最优地调整流程、开发工具和企业文化以满足这些要求,是智能体辅助应用开发的下一个前沿领域。弄清楚如何为人类编码者提供最佳AI支持,其回报有望带来显著的生产力提升,使人类开发团队能够更快、更高质量地交付更多功能。


[注]本文译自: AI agents are accelerators, not developer replacements

构建复合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