Spring 如何实现对 Java 6、7 和 8 的兼容

工程 | Stéphane Nicoll | 2015 年 4 月 3 日 | ...

从 Spring Framework 4.0 开始,Java 8 作为一级公民得到了支持,自那时以来,我们在 Spring 社区看到了一些困惑。毕竟,我们如何才能在支持 Java 8 的同时仍保持与 Java 6 和 Java 7 的兼容性?这篇博客文章提供了一些关于我们在框架代码库中如何处理此问题的信息。

Java 8 语言特性 vs. Java 8 API

首先,必须区分在给定的 Java 版本(例如 Java 8)中使用新语言特性和使用新 API。如果一个类使用了 Java 8 的语言特性(如 lambda 表达式),它必须使用 -source 1.8 -target 1.8 进行编译,因此整个编译单元将只能在 Java 8+ 上运行。然而,如果库中的某个特定类选择性地使用了新的 Java 8 接口(如 java.util.stream.Stream),只要该库使用例如 -source 1.6 -target 1.6 进行编译,并且对该特定基于 Stream 的类的使用被限定在只在实际运行于 Java 8+ 时才生效,那么该库仍然可以在之前的 Java 版本上运行。正如您可能猜到的那样,我们在 Spring Framework 代码库中广泛使用了这种安排!

我们宣传过 Spring Framework 4.0 如何自然地契合 Java 8 的 lambda 表达式。例如,使用 ConnectionCallback 检索给定 JDBC 连接的 catalog 可以写成如下形式

jdbcTemplate.execute(connection -> connection.getCatalog())

事实上,Spring Framework 多年前就已经有了现在所谓的函数式接口,我们无需更改任何这些 API 来符合 Java 8 对函数式接口的编译器规则。像上面这样的、调用 Spring API 的基于 lambda 的代码可以在任何 Spring 应用中使用——这显然需要 Java 8 运行时。然而,如果您选择使用传统的内部类方法编写此类代码,针对的是同一个 Spring 版本中的完全相同的 Spring API,您也可以使用 Java 6+ 运行时来实现。

jdbcTemplate.execute(new ConnectionCallback<String>() {
    @Override
    public String doInConnection(Connection con) throws SQLException {
        return con.getCatalog();
    }
});

关键在于选择权在您:我们精心设计了 Spring Framework 4.x,使其能自然地与 Java 6、7 和 8 兼容,使用相同的 Spring jar 包,无需特殊设置步骤。我们在自己的代码中不使用任何 Java 8 语言特性,因此我们可以使用 -source 1.6 -target 1.6 编译框架代码库,并在这种代码库安排中自动检测并激活许多 Java 8 API 特性(如果运行时可用)。您的应用程序代码随后可以选择使用 Java 6、7 或 8 的语言级别,与我们的框架安排交互,并自然地充分利用您正在使用的 JDK——无需任何额外设置,只需在运行时将 Spring 与您的 JDK 结合即可。

我们支持哪些 Java 8 API 特性?

我们专门支持一些 Java 8 特有的 API 特性,例如 java.util.Optionaljava.util.stream.Streamjava.time (JSR-310)、可重复注解、方法/构造函数参数名,甚至还有 java.util.Base64 工具类。当您选择在自己的应用程序类中使用这些特性时,它们会被反射性地检测到,Spring Framework 会有条件地激活对这些 Java 8 特性的支持,例如在运行时存在 Java 8 时,为 OptionalStream 注册默认的转换器。

让我们看一个例子。在即将发布的 Spring Framework 4.2 中,如果您定义了一个 Collection 类型或数组类型的值,您可以将其注入为 Stream,我们将为您进行转换。您可以在 github 上找到 StreamConverter 的完整代码,这里是一个片段

import java.util.*;
import java.util.stream.Collectors;
import java.util.stream.Stream;

import org.springframework.core.convert.*;
import org.springframework.lang.UsesJava8;

@UsesJava8
public class StreamConverter implements ConditionalGenericConverter {
    ....
}

StreamConverter 是一个使用 Java 8 特定 API 的独立类,因此我们现在需要做的是,如果在运行时存在 Java 8,则有条件地将 StreamConverter 添加到 DefaultConverterService 中。

public class DefaultConversionService extends GenericConversionService {

    /** Java 8's java.util.stream.Stream class available? */
    private static final boolean streamAvailable = ClassUtils.isPresent(
            "java.util.stream.Stream", 
            DefaultConversionService.class.getClassLoader());

    private static void addCollectionConverters(
            ConverterRegistry converterRegistry) {
        ...

        if (streamAvailable) {
            converterRegistry.addConverter(
                    new StreamConverter(conversionService));
        }
    }
}

我们有条件地检查 API 是否在运行时存在,并据此做出决定,作为用户,您默认只会体验到完全适应 Java 8 的设置。这与 Spring Boot 中的条件基础设施有些类似,只是它更底层且更偏向内部。

检查 Java 6 兼容性

由于我们在几个独立的地方使用了 Java 8 特定的 API,因此总体上我们需要 JDK 8 来编译框架代码库。因此,存在我们可能会在需要保持 Java 6 兼容性的地方意外引入 Java 8 特定 API 调用的风险。

幸运的是,我们的 CI 构建计划配置为在每次构建时执行 Animal Sniffer。它会对照给定的 Java API 签名(在我们的例子中是 Java 6 update 18)检查我们的代码,如果发现任何不正确的用法,则会导致构建失败。那么对于那些确实需要调用 Java 7 或 8 API 的合法用例怎么办?您可以配置 sniffer 来排除一个类列表,或者更好的是,提供一组注解来 标记 这些例外情况。

这正是 @UsesJava8 注解在 StreamConverter 上(见上文)所表明的:它将整个类标记为 Java 6 API 兼容性规则的一个例外。您可以类似地标记内部类甚至方法。通过查看我们对该注解的使用,我们可以知道在我们的代码库中所有使用 Java 7/8 特定 API 的地方。

Animal Sniffer 的配置相当简单:请查看我们的构建官方文档了解更多详情。

总结

我们选择在自己的代码库中不使用任何 Java 7 或 Java 8 语言特性,以便为您提供为 Java 6、7 或 8 编写 Spring 4 应用程序的灵活性。同时,如果您决定使用 Java 8,我们允许您体验一种非常自然的方式,在这种情况下,Spring Framework 对您来说基本上就像是基于 Java 8 的。

幸运的是,Java 8 的函数式接口约定对我们来说并不陌生。许多现有的 Spring API 可以与 Java 8 的 lambda 表达式无缝使用,因为它们自然遵循相同的约定。如果您选择在自己的代码中使用新的 Java 8 API,如 java.time (JSR-310)、OptionalStream,框架将自动支持它们。

从前瞻的角度来看,截至 4.2 版本,我们的代码库甚至已经在用早期的 JDK 9 构建进行检查了!一旦 JDK 9 明年普遍可用,这将导致框架出现一种独特的情况:在同一发布系列中支持 个 Java 版本——您可以选择 JDK 6、7、8 或 9,并结合使用同一代的 Spring Framework!

订阅 Spring 新闻快讯

保持与 Spring 新闻快讯的联系

订阅

先人一步

VMware 提供培训和认证,助您快速进步。

了解更多

获得支持

Tanzu Spring 通过一项简单的订阅,为 OpenJDK™、Spring 和 Apache Tomcat® 提供支持和二进制文件。

了解更多

近期活动

查看 Spring 社区的所有近期活动。

查看全部