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,那么只要该类使用 e.g. -source 1.6 -target 1.6 进行编译,并且对该特定 Stream 的使用进行了保护,以确保它仅在实际运行时于 Java 8+ 上触发,那么该库仍然可以在之前的 Java 版本上运行。正如您可能猜到的,我们在 Spring Framework 的代码库中大量使用了这种安排!

我们已经宣传了 Spring Framework 4.0 如何自然地与 Java 8 lambda 表达式兼容。例如,使用 ConnectionCallback 检索给定 JDBC 连接的目录可以这样写:

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 的独立类,因此我们现在需要有条件地将 StreamConverter 添加到 DefaultConverterService 中,前提是运行时存在 Java 8。

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 中的 conditions 基础设施,只是它更底层、更内部。

检查 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 来排除类列表,或者更好的是,提供一组注解来标记这些例外情况。

这正是上面看到的 StreamConverter 上的 @UsesJava8 注解所表示的:它将整个类标记为 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 社区所有即将举行的活动。

查看所有