当Java 22遇到 SpringBoot 3.3.0(下)

2024-05-26 14:27:34 浏览数 (1)

7 隐式声明的类和实例main方法

这个预览功能是巨大的生活质量提升!尽管结果代码更小,而我非常欢迎它。不幸的是,它目前还与 Spring Boot 不兼容。基本概念是,总有一天你将能够只有一个顶层 main 方法,而不需要今天 Java 中的所有仪式。作为应用程序的入口点,这不是很好吗?没有 class 定义,没有 public static void,也没有不必要的 String[] 参数。

代码语言:javascript复制
void main() {
    System.out.println("Hello, world!");
}

8 父类之前的语句

这是一个不错的生活质量功能。基本上,Java 不允许你在子类中调用 super 构造函数前访问 this。其是为避免与无效状态相关的一类错误。但这有点过于严厉了,并迫使开发者在想在调用 super 方法前进行任何不一般的计算时,不得不转而使用 private static 辅助方法。这是有时所需的体操动作的一个例子。我从 the JEP 页面偷来了:

代码语言:javascript复制
class Sub extends Super {

    Sub(Certificate certificate) {
        super(prepareByteArray(certificate));
    }

    // 辅助方法
    private static byte[] prepareByteArray(Certificate certificate) {
        var publicKey = certificate.getPublicKey();
        if (publicKey == null)
            throw new IllegalArgumentException("null certificate");
        return switch (publicKey) {
            case RSAKey rsaKey -> ///...
            case DSAPublicKey dsaKey -> ...
            //...
            default -> //...
        };
    }

}

你可以看到这问题。这个新的 JEP,目前还是预览功能,将允许你将该方法直接内联在构造函数,增强可读性并消除代码冗余!

9 未命名的变量和模式

另一个提升生活质量的功能。已经交付!

当你在创建线程或使用 Java 8 的流和收集器时,你将创建很多 lambda。实际上,在 Spring 中有很多情况下你会用 lambdas。只需考虑所有的 *Template 对象,及其以回调为中心的方法。JdbcClientRowMapper<T> 也跳入脑海!

有趣的事实:Lambda 首次在 2014 年的 Java 8 版本中介绍。但它们的惊人品质是几乎前 20 年的 Java 代码在一夜之间如果方法期望单个方法接口实现就可以参与 lambdas。

Lambdas 是惊人的。它们在 Java 语言中引入了一个新的重用单元。最棒的部分是它们被设计为以某种方式嫁接到运行时的现有规则,包括自动将所谓的功能接口或 SAMs(单抽象方法)接口适应到 lambdas。我唯一的抱怨是,属于包含作用域的 lambda 中引用的东西必须设置为 final。这个问题已修复!现在必须拼出每个 lambda 参数,即使我根本没打算使用它,现在,有了 Java 22,那也得到修复了!这里是一个冗长的例子,仅为展示两处 _ 字符的使用。

代码语言:javascript复制
package com.example.demo;

import org.springframework.jdbc.core.simple.JdbcClient;
import org.springframework.stereotype.Component;

import javax.sql.DataSource;

@Component
class AnonymousLambdaParameters implements LanguageDemonstrationRunner {

    private final JdbcClient db;

    AnonymousLambdaParameters(DataSource db) {
        this.db = JdbcClient.create(db);
    }

    record Customer(Integer id, String name) {
    }

    @Override
    public void run() throws Throwable {
        var allCustomers = this.db.sql("select * from customer ")
                // 这里! 
            .query((rs, _) -> new Customer(rs.getInt("id"), rs.getString("name")))
            .list();
        System.out.println("all: "   allCustomers);
    }

}

该类使用 Spring 的 JdbcClient 查询底层数据库。它一页一页地翻阅结果,然后涉及我们的 lambda,它符合 RowMapper<Customer> 类型,以帮助我们将结果适应到与我的领域模型一致的记录。RowMapper<T> 接口,我们的 lambda 符合它,有一个方法 T mapRow(ResultSet rs, int rowNum) throws SQLException,期望两个参数:我将需要的 ResultSet及几乎不需要的 rowNum。现在,多亏 Java 22,我不需要指定它。就像在 Kotlin、TypeScript 中一样,只需插入 _ 即可。Good!

10 聚集者

另一个在预览中也很好的功能。Viktor Klang,他在 Akka 上的了不起工作以及他在 Lightbend 期间对 Scala futures 的贡献。如今,他是 Oracle 的一名 Java 语言架构师,他一直在研究的就是新的 Gatherer API。Stream API 也是在 Java 8 中引入的,这给了 Javaer 一个机会,与 lambdas 一起,大大简化和现代化他们现有的代码,并向更多函数式编程方向发展。

它构建了一个在值的流上进行一系列转换的模型。然而,这个抽象模型并不尽完美。Streams API 提供大量便利方法,几乎满足 99% 场景,但当你遇到找不到合适方法的case时,会感到极大挫败感,因为之前并没有一种简易方式可直接扩展新操作。过去10年,关于为 Streams API 引入新操作的提案数不胜数,甚至在最初 lambda 表达式提案中,就有讨论和妥协,目的是让编程模型有足够灵活性来支持新操作的加入。现在,这一目标作为一个预览性质功能终于实现。

Gatherers 提供了一个稍微更底层的抽象层次,使你能在不需要将 Stream 具体化为 Collection 的情况下,在 Streams 上引入多种新操作。以下是一个我毫不掩饰地直接从 Viktor 和他的团队那里取得的示例。

代码语言:javascript复制
package com.example.demo;

import org.springframework.stereotype.Component;

import java.util.Locale;
import java.util.function.BiFunction;
import java.util.function.Supplier;
import java.util.stream.Gatherer;
import java.util.stream.Stream;

@Component
class Gatherers implements LanguageDemonstrationRunner {

    private static <T, R> Gatherer<T, ?, R> scan(
            Supplier<R> initial,
             BiFunction<? super R, ? super T, ? extends R> scanner) {

        class State {
            R current = initial.get();
        }
        return Gatherer.<T, State, R>ofSequential(State::new,
                Gatherer.Integrator.ofGreedy((state, element, downstream) -> {
                    state.current = scanner.apply(state.current, element);
                    return downstream.push(state.current);
                }));
    }

    @Override
    public void run() {
        var listOfNumberStrings = Stream
                .of(1, 2, 3, 4, 5, 6, 7, 8, 9)
                .gather(scan(() -> "", (string, number) -> string   number)
                        .andThen(java.util.stream.Gatherers.mapConcurrent(10, s -> s.toUpperCase(Locale.ROOT)))
                )
                .toList();
        System.out.println(listOfNumberStrings);
    }

}

该段代码的重点在于,这里描述了一个名为 scan 的方法,它返回一个 Gatherer<T,?,R> 类型的实现。每个 Gatherer<T,O,R> 对象都需要一个初始化函数和一个整合函数。虽然这种实现自带默认的合并函数和完成函数,但你也可以自行覆盖它们。它通过读取所有的数字条目,并为每一个条目逐步构造一个字符串,字符串随着数字的增加不断累积。结果就像这样:先是 1,然后是 12,接着是 123,直到 1234 等等。上述例子还展示了 gatherers 是可以组合使用的。在这里,我们实际上操作了两个 Gatherer 对象:一个用于执行扫描过程,另一个则把每个元素转成大写,并且这一转换是并发进行的。如果您还没能完全理解,没关系,对于大多数人而言,这部分内容可能会有些深奥。大多数人可能无需自己编写 Gatherers。但是,如果你想挑战一下,也是可以试试的。我的朋友 Gunnar Morling 就在前几天完成了这样的工作。Gatherers 方法的巧妙之处在于,它使社区能够根据自己的需求去设计解决方案。我很好奇这对于 Eclipse Collections、Apache Commons Collections 或者 Guava 这样的著名项目会带来什么影响?它们是否会推出 Gatherers?还有其他什么项目会加入这一趋势?我期待看到很多实用的 gatherers 能够聚集到同一个地方。

11 Class Parsing API

又一个令人期待的预览性特性,这是 JDK 新增的部分,非常适合框架和基础架构开发人员。它可以解答例如如何构建 .class 文件和如何读取 .class 文件的问题。目前市场上有很多好用但不兼容,总是稍微有点落后的工具,比如 ASM(这个领域里的重量级解决方案),ByteBuddy,CGLIB 等。JDK 本身在其代码库中就包含了三种此类解决方案!这类库在整个行业中随处可见,并且对于像 Spring 这样的框架的开发来说至关重要,Spring 动态地在运行时创建类来支持业务逻辑。你可以将它看作是一个反射 API,但它作用于 .class 文件——硬盘上实际的字节码,而不是加载进 JVM 的对象。这是一个简单的例子,展示了如何把一个 .class 文件加载进一个 byte[] 数组,并对其进行分析。

代码语言:javascript复制
package com.example.demo;

import org.springframework.aot.hint.RuntimeHints;
import org.springframework.aot.hint.RuntimeHintsRegistrar;
import org.springframework.context.annotation.ImportRuntimeHints;
import org.springframework.core.io.ClassPathResource;
import org.springframework.core.io.Resource;
import org.springframework.stereotype.Component;

import java.lang.classfile.ClassFile;
import java.lang.classfile.FieldModel;
import java.lang.classfile.MethodModel;

@Component
@ImportRuntimeHints(ClassParsing.Hints.class)
class ClassParsing implements LanguageDemonstrationRunner {

    static class Hints implements RuntimeHintsRegistrar {

        @Override
        public void registerHints(RuntimeHints hints, ClassLoader classLoader) {
            hints.resources().registerResource(DEFAULT_CUSTOMER_SERVICE_CLASS);
        }

    }

    private final byte[] classFileBytes;

    private static final Resource DEFAULT_CUSTOMER_SERVICE_CLASS = new ClassPathResource(
            "/simpleclassfile/DefaultCustomerService.class");

    ClassParsing() throws Exception {
        this.classFileBytes = DEFAULT_CUSTOMER_SERVICE_CLASS.getContentAsByteArray();
    }

    @Override
    public void run() {
        // this is the important logic
        var classModel = ClassFile.of().parse(this.classFileBytes);
        for (var classElement : classModel) {
            switch (classElement) {
                case MethodModel mm -> System.out.printf("Method %s%n", mm.methodName().stringValue());
                case FieldModel fm -> System.out.printf("Field %s%n", fm.fieldName().stringValue());
                default -> {
                    // ... 
                }
            }
        }
    }

}

这个例子稍微复杂一些,因为它涉及到了运行时读取资源。为了应对这个过程,我实现了一个名为 Spring AOT RuntimeHintsRegistrar 的组件,它能生成一个 .json 文件。这个 JSON 文件记录着我正在读取的资源信息,比如具体来说就是 DefaultCustomerService.class 文件的数据。不过,这些都是幕后的技术细节,主要是为了在 GraalVM 上进行本地镜像编译的时候使用。而代码底部的部分则颇有意思,我们对 ClassElement 实例进行了枚举,并通过模式匹配的方法一一提取了各个要素。这真是太棒了!

12 String Templates

又一项预览特性的加入,String templates 为 Java 带来了字符串插值功能!Java 中的多行字符串(String)已经使用了一段时间。这个新功能允许开发者将编译后字符串中可见的变量直接嵌入到字符串值里面。最精彩的部分?从理论上讲,这个机制还可以自定义!不满意现有的语法?你完全可以创造一个属于你自己的版本。

代码语言:javascript复制
package com.example.demo;

import org.springframework.stereotype.Component;

@Component
class StringTemplates implements LanguageDemonstrationRunner {

    @Override
    public void run() throws Throwable {
        var name = "josh";
        System.out.println(STR.""" 
            name: {name.toUpperCase()}
            """);
    }

}

13 总结

作为一名 Java 和 Spring 开发者,现在是一个前所未有的好时机!我一直强调这一点。我们仿佛获得了一个崭新的语言和运行时环境,这一进步奇妙地保持了对历史版本的兼容。这是我目睹 Java 社区所开展的最具雄心壮志的软件项目之一,我们很幸运能够见证其成果的诞生。从现在起,我打算将 Java 22 和支持 Java 22 的 GraalVM 用于我的所有开发工作,我希望您也能跟我一起。

参考:

  • GraalVM 开发者倡导者 Alina Yurenko

0 人点赞