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

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

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

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

为何运行时安全被忽视

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

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

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

输入验证:万物皆不可信

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

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

糟糕的验证示例

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

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

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

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

  • 邮箱:"[email protected]<script>alert('xss')</script>"
  • 年龄:-1999999
  • 角色:"ADMIN"(提升自己的权限)

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

正确的输入验证

以下是正确的做法:

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

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

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

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

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

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

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

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

验证复杂对象

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

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

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

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

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

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

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

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

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

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

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

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

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

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

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

        return true;
    }
}

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

白名单与黑名单的陷阱

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

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

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

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

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

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

沙箱机制:限制损害

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

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

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

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

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

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

现代沙箱方法

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

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

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

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

        Process process = pb.start();

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

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

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

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

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

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

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

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

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

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

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

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

限制资源消耗:

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

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

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

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

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

真实世界的沙箱示例

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

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

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

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

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

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

            Process process = pb.start();

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

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

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

            return result;

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

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

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

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

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

为何反序列化是危险的

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

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

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

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

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

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

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

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

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

当你必须反序列化时

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

使用 ObjectInputFilter (Java 9+):

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

        ois.setObjectInputFilter(filter);

        return ois.readObject();
    }
}

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

验证对象图:

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

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

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

        String className = desc.getName();

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

        return super.resolveClass(desc);
    }

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

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

对序列化数据进行签名:

public class SignedSerializer {
    private final SecretKey signingKey;

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

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

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

        return buffer.array();
    }

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

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

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

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

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

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

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

替代序列化库

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

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

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

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

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

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

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

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

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

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

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

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

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

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

修复需要多次更改:

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

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

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

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

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

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

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

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

实用安全检查清单

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

输入验证:

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

沙箱机制:

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

反序列化:

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

通用实践:

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

有用的工具

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

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

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

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

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

思维模式的转变

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

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

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

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

有用资源


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

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

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

引言

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

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

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

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

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

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

虚拟线程

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

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

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

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

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

ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor();

对比示例

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

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

示例 — 源代码

Example01CachedThreadPool.java

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

var executor = Executors.newCachedThreadPool()
package threads;

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

/**
 *
 * @author Milan Karajovic <[email protected]>
 *
 */

public class Example01CachedThreadPool {

    public void executeTasks(final int NUMBER_OF_TASKS) {

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

        long startTime = System.currentTimeMillis();

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

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

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

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

}
package threads;

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

/**
 *
 * @author Milan Karajovic <[email protected]>
 *
 */

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

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

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

    @Test
    @Order(3)
    public void test_100_000_tasks() {
        Example01CachedThreadPool example01CachedThreadPool = new Example01CachedThreadPool();
        example01CachedThreadPool.executeTasks(100_000);
    }

    @Test
    @Order(4)
    public void test_1_000_000_tasks() {
        Example01CachedThreadPool example01CachedThreadPool = new Example01CachedThreadPool();
        example01CachedThreadPool.executeTasks(1_000_000);
    }

}

我 PC 上的测试结果:

Example02FixedThreadPool.java

使用固定线程池创建执行器:

var executor = Executors.newFixedThreadPool(500)
package threads;

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

/**
 *
 * @author Milan Karajovic <[email protected]>
 *
 */

public class Example02FixedThreadPool {

    public void executeTasks(final int NUMBER_OF_TASKS) {

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

        long startTime = System.currentTimeMillis();

        try (var executor = Executors.newFixedThreadPool(500)) {

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

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

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

}
package threads;

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

/**
 *
 * @author Milan Karajovic <[email protected]>
 *
 */

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

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

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

    @Test
    @Order(3)
    public void test_100_000_tasks() {
        Example02FixedThreadPool example02FixedThreadPool = new Example02FixedThreadPool();
        example02FixedThreadPool.executeTasks(100_000);
    }

    @Test
    @Order(4)
    public void test_1_000_000_tasks() {
        Example02FixedThreadPool example02FixedThreadPool = new Example02FixedThreadPool();
        example02FixedThreadPool.executeTasks(1_000_000);
    }

}

我 PC 上的测试结果:

Example03VirtualThread.java

使用虚拟线程每任务执行器创建执行器:

var executor = Executors.newVirtualThreadPerTaskExecutor()
package threads;

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

/**
 *
 * @author Milan Karajovic <[email protected]>
 *
 */

public class Example03VirtualThread {

    public void executeTasks(final int NUMBER_OF_TASKS) {

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

        long startTime = System.currentTimeMillis();

        try (ExecutorService executor = Executors.newVirtualThreadPerTaskExecutor()) {

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

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

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

}
package threads;

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

/**
 *
 * @author Milan Karajovic <[email protected]>
 *
 */

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

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

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

    @Test
    @Order(3)
    public void test_100_000_tasks() {
        Example03VirtualThread example03VirtualThread = new Example03VirtualThread();
        example03VirtualThread.executeTasks(100_000);
    }

    @Test
    @Order(4)
    public void test_1_000_000_tasks() {
        Example03VirtualThread example03VirtualThread = new Example03VirtualThread();
        example03VirtualThread.executeTasks(1_000_000);
    }

    @Test
    @Order(5)
    public void test_2_000_000_tasks() {
        Example03VirtualThread example03VirtualThread = new Example03VirtualThread();
        example03VirtualThread.executeTasks(2_000_000);
    }

}

我 PC 上的测试结果:

结论

您可以清楚地看到用于处理所有 NUMBER_OF_TASKS 的不同执行器实现之间的执行时间差异。值得尝试不同的 NUMBER_OF_TASKS 值以观察性能变化。

虚拟线程的优势在处理大量任务时变得尤其明显。当 NUMBER_OF_TASKS 设置为较高的数值时——例如 1,000,000——性能差距是显著的。如下表所示,虚拟线程在处理大量任务时效率要高得多:

我确信,在澄清这一点之后,如果您的应用程序使用并发 API 处理大量任务,您会认真考虑迁移到 Java 21 并利用虚拟线程。在许多情况下,这种转变可以显著提高应用程序的性能和可扩展性。

源代码:GitHub Repository – Comparing Threads in Java 21


【注】本文译自:Java 21 Virtual Threads vs Cached and Fixed Threads

Java数据库应用原型

一个使用 Spring Boot 和容器进行测试、Keycloak 提供安全、PostgreSQL 提供数据持久化的,带有 REST 和安全功能的 Java 数据库应用原型。

在工作中开发时,我多次需要一个简单应用的模板,以便基于此模板开始为手头的项目添加特定代码。
在本文中,我将创建一个简单的 Java 应用程序,它连接到数据库,暴露一些 REST 端点,并使用基于角色的访问来保护这些端点。
目的是拥有一个最小化且功能齐全的应用程序,然后可以针对特定任务进行定制。
对于数据库,我们将使用 PostgreSQL;对于安全,我们将使用 Keycloak,两者都通过容器部署。在开发过程中,我使用 podman 来测试容器是否正确创建(作为 docker 的替代品——它们在大多数情况下可以互换)作为一次学习体验。
应用程序本身是使用 Spring Boot 框架开发的,并使用 Flyway 进行数据库版本管理。
所有这些技术都是 Java EE 领域业界标准,在项目中被使用的可能性很高。

我们构建原型的核心需求是一个图书馆应用程序,它暴露 REST 端点,允许创建作者、书籍以及它们之间的关系。这将使我们能够实现一个多对多关系,然后可以将其扩展用于任何可以想象的目的。
完整可用的应用程序可以在 https://github.com/ghalldev/db_proto 找到。
本文中的代码片段取自该代码库

在创建容器之前,请确保使用您偏好的值定义以下环境变量(教程中故意省略了它们,以避免传播多个用户使用的默认值):

DOCKER_POSTGRES_PASSWORD
DOCKER_KEYCLOAK_ADMIN_PASSWORD
DOCKER_GH_USER1_PASSWORD

配置 PostgreSQL:

docker container create --name gh_postgres --env POSTGRES_PASSWORD=$DOCKER_POSTGRES_PASSWORD --env POSTGRES_USER=gh_pguser --env POSTGRES_INITDB_ARGS=--auth=scram-sha-256 --publish 5432:5432 postgres:17.5-alpine3.22
docker container start gh_postgres

配置 Keycloak:
首先是容器的创建并启动:

docker container create --name gh_keycloak --env DOCKER_GH_USER1_PASSWORD=$DOCKER_GH_USER1_PASSWORD --env KC_BOOTSTRAP_ADMIN_USERNAME=gh_admin --env KC_BOOTSTRAP_ADMIN_PASSWORD=$DOCKER_KEYCLOAK_ADMIN_PASSWORD --publish 8080:8080 --publish 8443:8443 --publish 9000:9000 keycloak/keycloak:26.3 start-dev
docker container start gh_keycloak

在容器启动并运行后,我们可以继续创建领域、用户和角色(这些命令必须在正在运行的容器内部执行):

cd $HOME/bin
./kcadm.sh config credentials --server http://localhost:8080 --realm master --user gh_admin --password $KC_BOOTSTRAP_ADMIN_PASSWORD
./kcadm.sh create realms -s realm=gh_realm -s enabled=true
./kcadm.sh create users -s username=gh_user1 -s email="[email protected]" -s firstName="gh_user1firstName" -s lastName="gh_user1lastName" -s emailVerified=true -s enabled=true -r gh_realm
./kcadm.sh set-password -r gh_realm --username gh_user1 --new-password $DOCKER_GH_USER1_PASSWORD
./kcadm.sh create roles -r gh_realm -s name=viewer -s 'description=Realm role to be used for read-only features'
./kcadm.sh add-roles --uusername gh_user1 --rolename viewer -r gh_realm
./kcadm.sh create roles -r gh_realm -s name=creator -s 'description=Realm role to be used for create/update features'
./kcadm.sh add-roles --uusername gh_user1 --rolename creator -r gh_realm
ID_ACCOUNT_CONSOLE=$(./kcadm.sh get clients -r gh_realm --fields id,clientId | grep -B 1 '"clientId" : "account-console"' | grep -oP '[0-9a-f]{8}-([0-9a-f]{4}-){3}[0-9a-f]{12}')
./kcadm.sh update clients/$ID_ACCOUNT_CONSOLE -r gh_realm -s 'fullScopeAllowed=true' -s 'directAccessGrantsEnabled=true'

用户 gh_user1 在领域 gh_realm 中被创建,并拥有 viewercreator 角色。

您可能已经注意到,我们没有创建新的客户端,而是使用了 Keycloak 自带的一个默认客户端:account-console。这是为了方便起见,在实际场景中,您会创建一个特定的客户端,然后将其更新为具有 fullScopeAllowed(这会导致领域角色被添加到令牌中——默认情况下不添加)和 directAccessGrantsEnabled(允许通过 Keycloak 的 openid-connect/token 端点生成令牌,在我们的例子中使用 curl)。

创建的角色随后可以在 Java 应用程序内部使用,以根据我们约定的契约来限制对某些功能的访问——viewer 只能访问只读操作,而 creator 可以执行创建、更新和删除操作。当然,同样地,可以根据任何原因创建各种角色,只要约定的契约被明确定义并被所有人理解。
角色还可以进一步添加到组中,但本教程不包含这部分内容。

但是,在实际使用这些角色之前,我们必须告诉 Java 应用程序如何提取角色——这是必须的,因为 Keycloak 将角色添加到 JWT 的方式是其特有的,所以我们必须编写一段自定义代码,将其转换为 Spring Security 可以使用的东西:

@Bean
public JwtAuthenticationConverter jwtAuthenticationConverter() {
    // 遵循与 org.springframework.security.oauth2.server.resource.authentication.JwtGrantedAuthoritiesConverter 相同的模式
    Converter<Jwt, Collection<GrantedAuthority>> keycloakRolesConverter = new Converter<>() {
        private static final String DEFAULT_AUTHORITY_PREFIX = "ROLE_";
        //https://github.com/keycloak/keycloak/blob/main/services/src/main/java/org/keycloak/protocol/oidc/TokenManager.java#L901
        private static final String KEYCLOAK_REALM_ACCESS_CLAIM_NAME = "realm_access";
        private static final String KEYCLOAK_REALM_ACCESS_ROLES = "roles";

        @Override
        public Collection<GrantedAuthority> convert(Jwt source) {
            Collection<GrantedAuthority> grantedAuthorities = new ArrayList<>();
            Map<String, List<String>> realmAccess = source.getClaim(KEYCLOAK_REALM_ACCESS_CLAIM_NAME);
            if (realmAccess == null) {
                logger.warn("No " + KEYCLOAK_REALM_ACCESS_CLAIM_NAME + " present in the JWT");
                return grantedAuthorities;
            }
            List<String> roles = realmAccess.get(KEYCLOAK_REALM_ACCESS_ROLES);
            if (roles == null) {
                logger.warn("No " + KEYCLOAK_REALM_ACCESS_ROLES + " present in the JWT");
                return grantedAuthorities;
            }
            roles.forEach(
                    role -> grantedAuthorities.add(new SimpleGrantedAuthority(DEFAULT_AUTHORITY_PREFIX + role)));

            return grantedAuthorities;
        }

    };
    JwtAuthenticationConverter jwtAuthenticationConverter = new JwtAuthenticationConverter();
    jwtAuthenticationConverter.setJwtGrantedAuthoritiesConverter(keycloakRolesConverter);

    return jwtAuthenticationConverter;
}

AppConfiguration 类中还完成了其他重要配置,例如启用方法安全性和禁用 CSRF。

现在我们可以在 REST 控制器中使用 @org.springframework.security.access.prepost.PreAuthorize 注解来限制访问:

@PostMapping("/author")
@PreAuthorize("hasRole('creator')")
public void addAuthor(@RequestParam String name, @RequestParam String address) {
  authorService.add(new AuthorDto(name, address));
}

@GetMapping("/author")
@PreAuthorize("hasRole('viewer')")
public String getAuthors() {
  return authorService.allInfo();
}

通过这种方式,只有成功通过身份验证且拥有 hasRole 中列出的角色的用户才能调用端点,否则他们将收到 HTTP 403 Forbidden 错误。

在容器启动并配置完成后,Java 应用程序可以启动了,但在启动之前需要添加数据库密码——这可以通过环境变量完成(下面是一个 Linux shell 示例):

export SPRING_DATASOURCE_PASSWORD=$DOCKER_POSTGRES_PASSWORD

现在,如果一切正常启动并运行,我们可以使用 curl 来测试我们的应用程序(以下所有命令均为 Linux shell 命令)。

使用之前创建的用户 gh_user1 登录并提取身份验证令牌:

KEYCLOAK_ACCESS_TOKEN=$(curl -d 'client_id=account-console' -d 'username=gh_user1' -d "password=$DOCKER_GH_USER1_PASSWORD" -d 'grant_type=password' 'http://localhost:8080/realms/gh_realm/protocol/openid-connect/token' | grep -oP '"access_token":"\K[^"]*')

创建一个新作者(这将测试 creator 角色是否有效):

curl -X POST --data-raw 'name="GH_name1"&address="GH_address1"' -H "Authorization: Bearer $KEYCLOAK_ACCESS_TOKEN" 'localhost:8090/library/author'

检索库中的所有作者(这将测试 viewer 角色是否有效):

curl -X GET -H "Authorization: Bearer $KEYCLOAK_ACCESS_TOKEN" 'localhost:8090/library/author'

至此,您应该拥有了创建自己的 Java 应用程序所需的一切,可以根据需要对其进行扩展和配置。


【注】本文译自:Java Spring Boot Template With PostgreSQL, Keycloak Securit

单体架构中的事件驱动架构:Java应用程序的渐进式重构

传统观点认为事件驱动架构属于微服务架构范畴,服务通过消息代理进行异步通信。然而,事件驱动模式一些最具价值的应用恰恰发生在单体应用程序内部——在这些地方,紧密耦合已造成维护噩梦,而渐进式重构则提供了一条通往更好架构的路径,且无需分布式系统的运维复杂性。

为何在单体应用中使用事件有意义

传统的分层单体应用存在一个特定问题:直接的方法调用在组件之间创建了僵化的依赖关系。您的订单处理代码直接调用库存管理,库存管理又调用仓库系统,继而触发电子邮件通知。每个组件都了解其他几个组件,从而形成一个纠缠不清的网,更改其中一部分需要理解并测试它所触及的所有内容。

事件驱动模式引入了间接性。当下单时,订单服务发布一个"OrderPlaced"事件。其他对订单感兴趣的组件——库存、发货、通知——订阅此事件并独立响应。订单服务不知道也不关心谁在监听。即使这些组件存在于同一个代码库并共享同一个数据库,它们也变得松散耦合。

这种方法提供了立竿见影的好处,而无需将应用程序拆分为微服务。您在保持单体应用运维简单性的同时,获得了可测试性、灵活性和更清晰的边界。当您最终需要提取服务时,事件驱动的结构使得过渡更加平滑,因为组件已经通过定义良好的消息进行通信,而不是直接的方法调用。

起点:一个紧密耦合的订单系统

考虑一个使用 Spring Boot 构建的典型电子商务单体应用。订单创建流程如下所示:

@Service
@Transactional
public class OrderService {
    private final OrderRepository orderRepository;
    private final InventoryService inventoryService;
    private final PaymentService paymentService;
    private final ShippingService shippingService;
    private final LoyaltyService loyaltyService;
    private final EmailService emailService;
    private final AnalyticsService analyticsService;

    public OrderService(
        OrderRepository orderRepository,
        InventoryService inventoryService,
        PaymentService paymentService,
        ShippingService shippingService,
        LoyaltyService loyaltyService,
        EmailService emailService,
        AnalyticsService analyticsService
    ) {
        this.orderRepository = orderRepository;
        this.inventoryService = inventoryService;
        this.paymentService = paymentService;
        this.shippingService = shippingService;
        this.loyaltyService = loyaltyService;
        this.emailService = emailService;
        this.analyticsService = analyticsService;
    }

    public Order createOrder(CreateOrderRequest request) {
        // 验证库存
        for (OrderItem item : request.getItems()) {
            if (!inventoryService.checkAvailability(item.getProductId(), item.getQuantity())) {
                throw new InsufficientInventoryException(item.getProductId());
            }
        }

        // 处理支付
        PaymentResult payment = paymentService.processPayment(
            request.getCustomerId(),
            calculateTotal(request.getItems()),
            request.getPaymentDetails()
        );

        if (!payment.isSuccessful()) {
            throw new PaymentFailedException(payment.getErrorMessage());
        }

        // 创建订单
        Order order = new Order(
            request.getCustomerId(),
            request.getItems(),
            payment.getTransactionId()
        );
        order.setStatus(OrderStatus.CONFIRMED);
        Order savedOrder = orderRepository.save(order);

        // 预留库存
        for (OrderItem item : request.getItems()) {
            inventoryService.reserveInventory(item.getProductId(), item.getQuantity());
        }

        // 创建发货单
        shippingService.createShipment(savedOrder);

        // 更新忠诚度积分
        loyaltyService.addPoints(
            request.getCustomerId(),
            calculateLoyaltyPoints(savedOrder)
        );

        // 发送确认邮件
        emailService.sendOrderConfirmation(savedOrder);

        // 跟踪分析
        analyticsService.trackOrderPlaced(savedOrder);

        return savedOrder;
    }
}

这段代码可以工作,但存在严重问题。OrderService 知道七个不同的服务。测试需要模拟所有这些服务。添加新的订单后操作意味着要修改此方法。如果电子邮件服务缓慢,订单创建就会变慢。如果分析跟踪失败,整个订单就会失败并回滚。

事务边界也是错误的。所有操作都在单个数据库事务中发生,这意味着即使电子邮件服务临时停机也会阻止订单创建。库存预留和发货单创建在事务上耦合,尽管它们在逻辑上是独立的操作。

引入 Spring 应用事件

Spring Framework 提供了一个内置的事件系统,在单个 JVM 内工作。默认情况下它是同步的,这使得它易于推理和调试。首先定义领域事件:

public abstract class DomainEvent {
    private final Instant occurredAt;
    private final String eventId;

    protected DomainEvent() {
        this.occurredAt = Instant.now();
        this.eventId = UUID.randomUUID().toString();
    }

    public Instant getOccurredAt() {
        return occurredAt;
    }

    public String getEventId() {
        return eventId;
    }
}

public class OrderPlacedEvent extends DomainEvent {
    private final Long orderId;
    private final Long customerId;
    private final List<OrderItem> items;
    private final BigDecimal totalAmount;

    public OrderPlacedEvent(Order order) {
        super();
        this.orderId = order.getId();
        this.customerId = order.getCustomerId();
        this.items = new ArrayList<>(order.getItems());
        this.totalAmount = order.getTotalAmount();
    }

    // Getters
}

事件应该是不可变的,并包含订阅者需要的所有信息。避免直接传递实体——而是复制相关数据。这可以防止订阅者意外修改共享状态。

重构 OrderService 以发布事件,而不是直接调用服务:

@Service
@Transactional
public class OrderService {
    private final OrderRepository orderRepository;
    private final InventoryService inventoryService;
    private final PaymentService paymentService;
    private final ApplicationEventPublisher eventPublisher;

    public OrderService(
        OrderRepository orderRepository,
        InventoryService inventoryService,
        PaymentService paymentService,
        ApplicationEventPublisher eventPublisher
    ) {
        this.orderRepository = orderRepository;
        this.inventoryService = inventoryService;
        this.paymentService = paymentService;
        this.eventPublisher = eventPublisher;
    }

    public Order createOrder(CreateOrderRequest request) {
        // 验证库存
        for (OrderItem item : request.getItems()) {
            if (!inventoryService.checkAvailability(item.getProductId(), item.getQuantity())) {
                throw new InsufficientInventoryException(item.getProductId());
            }
        }

        // 处理支付
        PaymentResult payment = paymentService.processPayment(
            request.getCustomerId(),
            calculateTotal(request.getItems()),
            request.getPaymentDetails()
        );

        if (!payment.isSuccessful()) {
            throw new PaymentFailedException(payment.getErrorMessage());
        }

        // 创建并保存订单
        Order order = new Order(
            request.getCustomerId(),
            request.getItems(),
            payment.getTransactionId()
        );
        order.setStatus(OrderStatus.CONFIRMED);
        Order savedOrder = orderRepository.save(order);

        // 同步预留库存(仍在关键路径上)
        for (OrderItem item : request.getItems()) {
            inventoryService.reserveInventory(item.getProductId(), item.getQuantity());
        }

        // 为非关键操作发布事件
        eventPublisher.publishEvent(new OrderPlacedEvent(savedOrder));

        return savedOrder;
    }
}

现在 OrderService 仅依赖四个组件,而不是八个。更重要的是,它只了解对订单创建至关重要的操作——库存验证、支付处理和库存预留。其他所有操作都通过事件发生。

为解耦的操作创建事件监听器:

@Component
public class OrderEventListeners {
    private static final Logger logger = LoggerFactory.getLogger(OrderEventListeners.class);

    private final ShippingService shippingService;
    private final LoyaltyService loyaltyService;
    private final EmailService emailService;
    private final AnalyticsService analyticsService;

    public OrderEventListeners(
        ShippingService shippingService,
        LoyaltyService loyaltyService,
        EmailService emailService,
        AnalyticsService analyticsService
    ) {
        this.shippingService = shippingService;
        this.loyaltyService = loyaltyService;
        this.emailService = emailService;
        this.analyticsService = analyticsService;
    }

    @EventListener
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void handleOrderPlaced(OrderPlacedEvent event) {
        try {
            shippingService.createShipment(event.getOrderId());
        } catch (Exception e) {
            logger.error("Failed to create shipment for order {}", event.getOrderId(), e);
            // 不要重新抛出 - 其他监听器仍应执行
        }
    }

    @EventListener
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void updateLoyaltyPoints(OrderPlacedEvent event) {
        try {
            int points = calculatePoints(event.getTotalAmount());
            loyaltyService.addPoints(event.getCustomerId(), points);
        } catch (Exception e) {
            logger.error("Failed to update loyalty points for order {}", event.getOrderId(), e);
        }
    }

    @EventListener
    public void sendConfirmationEmail(OrderPlacedEvent event) {
        try {
            emailService.sendOrderConfirmation(event.getOrderId());
        } catch (Exception e) {
            logger.error("Failed to send confirmation email for order {}", event.getOrderId(), e);
        }
    }

    @EventListener
    public void trackAnalytics(OrderPlacedEvent event) {
        try {
            analyticsService.trackOrderPlaced(event.getOrderId(), event.getTotalAmount());
        } catch (Exception e) {
            logger.error("Failed to track analytics for order {}", event.getOrderId(), e);
        }
    }
}

每个监听器在它自己的事务中运行(在适当的时候)并独立处理故障。如果发送电子邮件失败,发货单创建仍然会发生。即使分析跟踪抛出异常,订单创建事务也会成功提交。

理解事务边界

@Transactional(propagation = Propagation.REQUIRES_NEW) 注解至关重要。没有它,所有监听器都会参与订单创建事务。如果任何监听器失败,整个订单都会回滚——这正是我们试图避免的情况。

使用 REQUIRES_NEW,每个监听器都会启动一个新的事务。当监听器运行时,订单已经提交。这意味着:

  • 监听器无法阻止订单创建
  • 监听器故障不会回滚订单
  • 每个监听器的工作是独立原子性的

但这有一个权衡。如果监听器失败,订单存在但某些后处理没有发生。您需要处理这些部分故障的策略:

@EventListener
@Transactional(propagation = Propagation.REQUIRES_NEW)
public void handleOrderPlaced(OrderPlacedEvent event) {
    try {
        shippingService.createShipment(event.getOrderId());
    } catch (Exception e) {
        logger.error("Failed to create shipment for order {}", event.getOrderId(), e);

        // 记录失败以便重试
        failedEventRepository.save(new FailedEvent(
            event.getClass().getSimpleName(),
            event.getEventId(),
            "handleOrderPlaced",
            e.getMessage()
        ));
    }
}

一个单独的后台作业可以重试失败的事件:

@Component
public class FailedEventRetryJob {
    private final FailedEventRepository failedEventRepository;
    private final ApplicationEventPublisher eventPublisher;

    @Scheduled(fixedDelay = 60000) // 每分钟
    public void retryFailedEvents() {
        List failures = failedEventRepository.findRetryable();

        for (FailedEvent failure : failures) {
            try {
                // 重建并重新发布事件
                DomainEvent event = reconstructEvent(failure);
                eventPublisher.publishEvent(event);

                failure.markRetried();
                failedEventRepository.save(failure);
            } catch (Exception e) {
                logger.warn("Retry failed for event {}", failure.getEventId(), e);
                failure.incrementRetryCount();
                failedEventRepository.save(failure);
            }
        }
    }
}

这种模式提供了最终一致性——系统可能暂时不一致,但通过重试自行恢复。

转向异步事件

Spring 的 @EventListener 默认是同步的。事件处理发生在发布事件的同一线程中,发布者等待所有监听器完成。这提供了强有力的保证,但限制了可扩展性。

通过启用异步支持并注解监听器来使监听器异步:

@Configuration
@EnableAsync
public class AsyncConfig {
    @Bean(name = "eventExecutor")
    public Executor eventExecutor() {
        ThreadPoolTaskExecutor executor = new ThreadPoolTaskExecutor();
        executor.setCorePoolSize(4);
        executor.setMaxPoolSize(10);
        executor.setQueueCapacity(100);
        executor.setThreadNamePrefix("event-");
        executor.initialize();
        return executor;
    }
}

@Component
public class OrderEventListeners {
    // ... 依赖 ...

    @Async("eventExecutor")
    @EventListener
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void handleOrderPlaced(OrderPlacedEvent event) {
        shippingService.createShipment(event.getOrderId());
    }

    @Async("eventExecutor")
    @EventListener
    public void sendConfirmationEmail(OrderPlacedEvent event) {
        emailService.sendOrderConfirmation(event.getOrderId());
    }
}

使用 @AsynccreateOrder() 方法在发布事件后立即返回。监听器在线程池中并发执行。这显著提高了响应时间——订单创建不再等待电子邮件发送或分析跟踪。

但异步事件引入了新的复杂性。当监听器执行时,订单创建事务可能尚未提交。监听器可能尝试从数据库加载订单,但由于事务仍在进行中而找不到它。

Spring 提供了 @TransactionalEventListener 来处理这种情况:

@Component
public class OrderEventListeners {
    @Async("eventExecutor")
    @TransactionalEventListener(phase = TransactionPhase.AFTER_COMMIT)
    public void handleOrderPlaced(OrderPlacedEvent event) {
        // 这仅在订单创建事务成功提交后运行
        shippingService.createShipment(event.getOrderId());
    }
}

AFTER_COMMIT 阶段确保监听器仅在发布事务成功提交后运行。如果订单创建失败并回滚,监听器永远不会执行。这可以防止处理实际上不存在的订单的事件。

实现事件存储

随着事件驱动架构的成熟,存储事件变得有价值。事件存储提供了审计日志,支持调试,并支持更复杂的模式,如事件溯源。

创建一个简单的事件存储:

@Entity
@Table(name = "domain_events")
public class StoredEvent {
    @Id
    @GeneratedValue(strategy = GenerationType.IDENTITY)
    private Long id;

    @Column(nullable = false)
    private String eventId;

    @Column(nullable = false)
    private String eventType;

    @Column(nullable = false, columnDefinition = "TEXT")
    private String payload;

    @Column(nullable = false)
    private Instant occurredAt;

    @Column(nullable = false)
    private Instant storedAt;

    @Column
    private String aggregateId;

    @Column
    private String aggregateType;

    // 构造器、getter、setter
}

@Repository
public interface StoredEventRepository extends JpaRepository<StoredEvent, Long> {
    List<StoredEvent> findByAggregateIdOrderByOccurredAt(String aggregateId);
    List<StoredEvent> findByEventType(String eventType);
}

拦截并存储所有领域事件:

@Component
public class EventStoreListener {
    private final StoredEventRepository repository;
    private final ObjectMapper objectMapper;

    public EventStoreListener(StoredEventRepository repository, ObjectMapper objectMapper) {
        this.repository = repository;
        this.objectMapper = objectMapper;
    }

    @EventListener
    @Order(Ordered.HIGHEST_PRECEDENCE) // 在其他监听器之前存储
    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public void storeEvent(DomainEvent event) {
        try {
            StoredEvent stored = new StoredEvent();
            stored.setEventId(event.getEventId());
            stored.setEventType(event.getClass().getSimpleName());
            stored.setPayload(objectMapper.writeValueAsString(event));
            stored.setOccurredAt(event.getOccurredAt());
            stored.setStoredAt(Instant.now());

            // 如果可用,提取聚合信息
            if (event instanceof OrderPlacedEvent) {
                OrderPlacedEvent orderEvent = (OrderPlacedEvent) event;
                stored.setAggregateId(orderEvent.getOrderId().toString());
                stored.setAggregateType("Order");
            }

            repository.save(stored);
        } catch (JsonProcessingException e) {
            throw new EventStoreException("Failed to serialize event", e);
        }
    }
}

现在,每个领域事件在业务逻辑处理之前都会持久化。您可以通过重放事件来重建系统中发生的情况:

@Service
public class OrderHistoryService {
    private final StoredEventRepository eventRepository;

    public List<OrderEvent> getOrderHistory(Long orderId) {
        List<StoredEvent> events = eventRepository.findByAggregateIdOrderByOccurredAt(
            orderId.toString()
        );

        return events.stream()
            .map(this::deserializeEvent)
            .collect(Collectors.toList());
    }

    private OrderEvent deserializeEvent(StoredEvent stored) {
        // 根据事件类型反序列化
        try {
            Class<?> eventClass = Class.forName("com.example.events." + stored.getEventType());
            return (OrderEvent) objectMapper.readValue(stored.getPayload(), eventClass);
        } catch (Exception e) {
            throw new EventStoreException("Failed to deserialize event", e);
        }
    }
}

这实现了强大的调试能力。当客户报告其订单问题时,您可以准确看到发生了什么事件以及发生的顺序。

Saga 和补偿操作

某些工作流需要跨多个步骤进行协调,其中每个步骤都可能失败。传统方法使用分布式事务,但这些方法扩展性不佳且增加了复杂性。Saga 使用编排事件和补偿操作提供了一种替代方案。

考虑一个更复杂的订单流程,您需要:

  1. 预留库存
  2. 处理支付
  3. 创建发货单

如果在预留库存后支付失败,您需要释放预留。通过补偿事件实现这一点:

public class InventoryReservedEvent extends DomainEvent {
    private final Long orderId;
    private final List<ReservationDetail> reservations;

    // 构造器、getter
}

public class PaymentFailedEvent extends DomainEvent {
    private final Long orderId;
    private final String reason;

    // 构造器、getter
}

@Component
public class InventorySagaHandler {
    private final InventoryService inventoryService;

    @EventListener
    public void handlePaymentFailed(PaymentFailedEvent event) {
        // 补偿操作:释放预留库存
        inventoryService.releaseReservation(event.getOrderId());
    }
}

Saga 通过事件而不是中央协调器进行协调:

@Service
public class OrderSagaService {
    private final ApplicationEventPublisher eventPublisher;
    private final InventoryService inventoryService;
    private final PaymentService paymentService;

    public void processOrder(Order order) {
        // 步骤 1: 预留库存
        List<ReservationDetail> reservations = inventoryService.reserve(order.getItems());
        eventPublisher.publishEvent(new InventoryReservedEvent(order.getId(), reservations));

        try {
            // 步骤 2: 处理支付
            PaymentResult payment = paymentService.processPayment(order);

            if (payment.isSuccessful()) {
                eventPublisher.publishEvent(new PaymentSucceededEvent(order.getId(), payment));
            } else {
                // 触发补偿
                eventPublisher.publishEvent(new PaymentFailedEvent(order.getId(), payment.getReason()));
                throw new PaymentException(payment.getReason());
            }
        } catch (Exception e) {
            // 触发补偿
            eventPublisher.publishEvent(new PaymentFailedEvent(order.getId(), e.getMessage()));
            throw e;
        }
    }
}

这种模式在没有分布式事务的情况下保持了一致性。每个步骤发布记录所发生事件的事件。当发生故障时,补偿事件会触发撤销先前步骤的操作。

桥接到外部消息代理

随着单体应用的增长,您可能希望与外部系统集成或为最终的服务提取做准备。Spring Cloud Stream 提供了对 RabbitMQ 或 Kafka 等消息代理的抽象,同时保持相同的事件驱动模式:

<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-stream</artifactId>
</dependency>
<dependency>
    <groupId>org.springframework.cloud</groupId>
    <artifactId>spring-cloud-stream-binder-kafka</artifactId>
</dependency>

application.yml 中配置绑定:

spring:
  cloud:
    stream:
      bindings:
        orderPlaced-out-0:
          destination: order.placed
        orderPlaced-in-0:
          destination: order.placed
          group: order-processors
      kafka:
        binder:
          brokers: localhost:9092

创建内部事件和外部消息之间的桥接:

@Component
public class EventPublisher {
    private final StreamBridge streamBridge;

    public EventPublisher(StreamBridge streamBridge) {
        this.streamBridge = streamBridge;
    }

    @EventListener
    public void publishToExternalBroker(OrderPlacedEvent event) {
        // 将内部事件发布到外部消息代理
        streamBridge.send("orderPlaced-out-0", event);
    }
}

@Component
public class ExternalEventConsumer {
    private final ApplicationEventPublisher eventPublisher;

    public ExternalEventConsumer(ApplicationEventPublisher eventPublisher) {
        this.eventPublisher = eventPublisher;
    }

    @Bean
    public Consumer<OrderPlacedEvent> orderPlaced() {
        return event -> {
            // 将外部事件重新发布为内部事件
            eventPublisher.publishEvent(event);
        };
    }
}

这种模式让您可以选择性地将事件发布到外部,同时将内部事件保留在本地。关键的实时操作使用内部事件以实现低延迟。跨服务通信使用消息代理以实现可靠性和可扩展性。

监控与可观测性

事件驱动系统引入了新的可观测性挑战。理解正在发生的情况需要跨多个异步处理步骤跟踪事件。实施全面的日志记录和指标:

@Aspect
@Component
public class EventMonitoringAspect {
    private static final Logger logger = LoggerFactory.getLogger(EventMonitoringAspect.class);
    private final MeterRegistry meterRegistry;

    public EventMonitoringAspect(MeterRegistry meterRegistry) {
        this.meterRegistry = meterRegistry;
    }

    @Around("@annotation(org.springframework.context.event.EventListener)")
    public Object monitorEventListener(ProceedingJoinPoint joinPoint) throws Throwable {
        String listenerName = joinPoint.getSignature().getName();
        Object[] args = joinPoint.getArgs();
        DomainEvent event = (DomainEvent) args[0];

        Timer.Sample sample = Timer.start(meterRegistry);

        try {
            logger.info("Processing event {} in listener {}", 
                event.getEventId(), listenerName);

            Object result = joinPoint.proceed();

            sample.stop(Timer.builder("event.listener.duration")
                .tag("listener", listenerName)
                .tag("event_type", event.getClass().getSimpleName())
                .tag("status", "success")
                .register(meterRegistry));

            meterRegistry.counter("event.listener.processed",
                "listener", listenerName,
                "event_type", event.getClass().getSimpleName(),
                "status", "success"
            ).increment();

            return result;
        } catch (Exception e) {
            sample.stop(Timer.builder("event.listener.duration")
                .tag("listener", listenerName)
                .tag("event_type", event.getClass().getSimpleName())
                .tag("status", "failure")
                .register(meterRegistry));

            meterRegistry.counter("event.listener.processed",
                "listener", listenerName,
                "event_type", event.getClass().getSimpleName(),
                "status", "failure"
            ).increment();

            logger.error("Error processing event {} in listener {}", 
                event.getEventId(), listenerName, e);

            throw e;
        }
    }
}

这个切面自动跟踪每个事件监听器的执行时间和成功率。结合 Prometheus 和 Grafana 等工具,您可以可视化事件处理模式并识别瓶颈。

添加关联 ID 以跟踪系统中的事件:

public abstract class DomainEvent {
    private final Instant occurredAt;
    private final String eventId;
    private final String correlationId;

    protected DomainEvent(String correlationId) {
        this.occurredAt = Instant.now();
        this.eventId = UUID.randomUUID().toString();
        this.correlationId = correlationId != null ? correlationId : UUID.randomUUID().toString();
    }

    // Getters
}

通过事件链传播关联 ID:

@EventListener
public void handleOrderPlaced(OrderPlacedEvent event) {
    MDC.put("correlationId", event.getCorrelationId());

    try {
        // 执行工作

        // 发布具有相同关联 ID 的后续事件
        eventPublisher.publishEvent(new ShipmentCreatedEvent(
            event.getOrderId(),
            event.getCorrelationId()
        ));
    } finally {
        MDC.clear();
    }
}

现在,与单个订单流相关的所有日志消息共享一个关联 ID,使得跨多个异步操作跟踪整个工作流变得微不足道。

测试事件驱动代码

事件驱动架构需要不同的测试策略。传统的单元测试适用于单个监听器,但集成测试对于验证事件流变得更加重要:

@SpringBootTest
@TestConfiguration
public class OrderEventIntegrationTest {
    @Autowired
    private ApplicationEventPublisher eventPublisher;

    @Autowired
    private ShippingService shippingService;

    @Autowired
    private EmailService emailService;

    @Test
    public void shouldProcessOrderPlacedEventCompletely() throws Exception {
        // 给定
        Order order = createTestOrder();
        OrderPlacedEvent event = new OrderPlacedEvent(order);

        // 当
        eventPublisher.publishEvent(event);

        // 等待异步处理
        await().atMost(5, TimeUnit.SECONDS).untilAsserted(() -> {
            // 然后
            verify(shippingService).createShipment(order.getId());
            verify(emailService).sendOrderConfirmation(order.getId());
        });
    }
}

对于单元测试,注入一个间谍事件发布器以验证事件是否正确发布:

@ExtendWith(MockitoExtension.class)
public class OrderServiceTest {
    @Mock
    private OrderRepository orderRepository;

    @Mock
    private InventoryService inventoryService;

    @Mock
    private PaymentService paymentService;

    @Spy
    private ApplicationEventPublisher eventPublisher = new SimpleApplicationEventPublisher();

    @InjectMocks
    private OrderService orderService;

    @Test
    public void shouldPublishOrderPlacedEventAfterCreatingOrder() {
        // 给定
        CreateOrderRequest request = createValidRequest();

        when(inventoryService.checkAvailability(any(), anyInt())).thenReturn(true);
        when(paymentService.processPayment(any(), any(), any()))
            .thenReturn(PaymentResult.successful("txn-123"));
        when(orderRepository.save(any())).thenAnswer(inv -> inv.getArgument(0));

        // 当
        orderService.createOrder(request);

        // 然后
        verify(eventPublisher).publishEvent(argThat(event -> 
            event instanceof OrderPlacedEvent
        ));
    }
}

迁移之旅

将单体应用重构为使用事件驱动架构并非全有或全无的命题。从一个工作流开始——通常是造成最多痛苦的那个。识别可以事件驱动的直接服务调用,并逐步引入事件。

从同步事件开始,以最小化行为变更。一旦事件正确流动,为非关键监听器切换到异步处理。当您需要审计跟踪或调试能力时,添加事件存储。仅当您需要跨服务通信或准备提取微服务时,才集成外部消息代理。

目标不是实现完美的事件驱动架构。而是减少耦合、提高可测试性,并在组件之间创建更清晰的边界。即使是部分采用也能提供价值——具有一些事件驱动模式的单体应用比完全没有的模式更易于维护。

这种渐进式方法使您能够持续交付价值,而不是投入一个需要数月时间、直到完全结束时才能交付任何成果的重构项目。您能够了解在特定领域和团队中哪些方法有效,根据实际经验而非理论理想来调整实施策略。

有用的资源


【注】本文译自: Event-Driven Architecture in Monoliths: Incremental Refactoring for Java Apps – Java Code Geeks

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系统的设计,重点聚焦模块化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框架中的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

如何在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