Spring 提示:Java 14(或者:你的 Java 能做到这个吗?)

工程 | Josh Long | 2020年3月11日 | ...

嗨,Spring 粉丝们!欢迎来到 Spring Tips 的另一期!在本期中,我们将了解 Java 14 中的新特性及其在构建基于 Spring Boot 的应用程序中的用法。

演讲者:Josh Long (@starbuxman)

首先,我们需要使用最新版本的 Java,即 Java 14,它尚未正式发布,预计在 2020 年初发布。您可以从 Java.net 下载早期访问版本。您也可以考虑使用 SDKManager (sdk),它使得安装新版本的 JVM 变得非常简单。

请记住,每 6 个月都会发布新的 Java 版本。这些新版本可在生产环境中使用,但只支持从一个版本发布到下一个版本之间的六个月。Java 项目偶尔也会发布长期支持 (LTS) 版本。当前的 LTS 版本是 Java 11。Java 14 仅在 Java 15 发布之前才适合用于生产环境。事实上,我们将讨论很多 *预览特性*,有人可能会认为这些特性根本不应该用于生产环境。我已经警告过你了!

如果您使用的是 SDKManager,您可以运行以下命令来安装 Java 14。

sdk install java 14.ea.36-open 

访问 Spring Initializr 并使用 Spring Boot 2.3 或更高版本生成一个新项目。您还需要选择 JDBCPostgreSQL

较旧版本的 Spring Boot 还不支持 Java 14 运行时。当然,为了编辑这个 Java 版本,您需要将其导入到您的 IDE 中。但是,在此之前,让我们修改 `pom.xml` 以配置构建以支持 Java 14。通常,当您访问 Spring Initializr 时,您还会指定一个 Java 版本。Java 14 尚未受支持,因此我们需要手动配置一些内容。

确保通过更改 `java.version` 属性来指定 Java 版本

<properties>
 <java.version>14</java.version>
</properties>

这允许我们的构建使用 Java 14 及该版本中所有已发布的功能,但要真正体验 Java 14 的新颖之处,我们需要启用 *预览功能* —— 在版本中发布但默认情况下未启用的功能。

在 `<plugins>...</plugins>` 代码段中,添加以下插件配置以启用 Java 14 的预览功能。

<plugin>
    <artifactId>maven-compiler-plugin</artifactId>
    <configuration>
        <release>14</release>
        <compilerArgs>
            <arg>--enable-preview</arg>
        </compilerArgs>
        <forceJavacCompilerUse>true</forceJavacCompilerUse>
        <parameters>true</parameters>
    </configuration>
</plugin>

<plugin>
    <artifactId>maven-surefire-plugin</artifactId>
    <configuration>
        <argLine>--enable-preview</argLine>
    </configuration>
</plugin>

现在您可以开始了!让我们来看一些 Java 代码。Spring Initializr 很贴心地为我们提供了一个项目和一个简单的入口类

package com.example.fourteen;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.context.event.ApplicationReadyEvent;
import org.springframework.context.event.EventListener;
import org.springframework.jdbc.core.JdbcTemplate;
import org.springframework.jdbc.core.PreparedStatementCreatorFactory;
import org.springframework.jdbc.core.RowMapper;
import org.springframework.jdbc.core.SqlParameter;
import org.springframework.jdbc.support.GeneratedKeyHolder;
import org.springframework.stereotype.Component;
import org.springframework.stereotype.Service;

import java.sql.Types;
import java.util.List;

@SpringBootApplication
public class FourteenApplication {

	public static void main(String[] args) {
		SpringApplication.run(FourteenApplication.class, args);
	}
}

我们将创建一个简单的基于 JDBC 的服务,它使用 SQL 将其数据写入数据库。我们需要一个对象来映射数据库表 `people` 中的数据。

在这一点上,我通常会使用我的 IDE 的代码生成功能来编写 Javabean 对象,或者我会使用 Lombok 来添加注解,生成一个编译器合成的对象,该对象具有 getter、setter、`toString` 和 `equals` 的实现。我甚至可能会不情愿地提到其他语言能够使这种乏味的工作变得微不足道。Scala 支持 *case classes*。Kotlin 支持 *data classes*。

而 Java 14 支持 _记录_。

record Person(Integer id, String name, int emotionalState) {
}

不错吧?这种语法非常强大!它为我们提供了一个带有构造函数和构造函数参数、属性、`equals` 和 `toString` 实现等等的新对象。我们可以像使用任何其他对象一样实例化此对象的一个实例。尝试取消引用对象中的属性,您会发现我们的构造函数属性已变成 `id()`/`id(int)`、`name()`/`name(String)` 和 `emotionalState()`/`emotionalState(int)`。这么小的代码就能做到这么多,真不错!

让我们看看 `PeopleService` 的实现。

`PeopleService` 使用 `JdbcTemplate` 来简化将数据库查询结果转换为 Java 对象的工作。如果您曾经使用过 `JdbcTemplate`(谁没用过呢?),这应该相当简单。我留下了一些未实现的部分,以便我们可以直接回顾它们。


@Service
class PeopleService {

	private final JdbcTemplate template;

	//todo
	private final String findByIdSql = null;

	private final String insertSql = null; 

	private final RowMapper<Person> personRowMapper =
		(rs, rowNum) -> new Person(rs.getInt("id"), rs.getString("name"), rs.getInt("emotional_state"));

	PeopleService(JdbcTemplate template) {
		this.template = template;
	}

	public Person create(String name, EmotionalState state) {
		 //todo
	}

	public Person findById(Integer id) {
		return this.template.queryForObject(this.findByIdSql, new Object[]{id}, this.personRowMapper);
	}
}


首先,我们将使用一些 SQL 查询。我一生都在努力避免在 Java 代码中键入 SQL 查询。我的天哪,如果人们知道他们可以将 SQL 查询巧妙地表达为 Java `Strings`,他们会有多少次会使用 ORM 呢?对于任何稍微复杂一点的内容,我都将 SQL 查询提取到属性文件中,然后使用 Spring 的配置属性机制加载它们。

但是,在 Java 14 中我们可以做得更好!多行字符串终于来到 Java 了!它现在加入了 Python、Ruby、C++、C#、Rust、PHP、Kotlin、Scala、Groovy、Go、JavaScript、Clojure 和其他十多种语言的行列。我很高兴它终于来了!

将 `sql` 变量替换为以下声明。

private final String findByIdSql =
    """
            select * from PEOPLE 
            where ID = ? 
    """;

	private final String insertSql =
    """
        insert into PEOPLE(name, emotional_state)
        values (?,?);
    """;

太棒了!有一些方法可以用来修剪边距等等。您还可以使用反斜杠转义序列 (\) 在每一行的末尾表示下一行应从那里开始,否则换行符将按字面解释。

让我们看看 `create` 方法。

将 `Person` 的 `emotionalState` 作为 `int` 存储在数据库中是一个实现细节。我宁愿不必将其传递给用户。让我们使用枚举来描述每个 `Person` 的情绪状态

enum EmotionalState {
	SAD, HAPPY, NEUTRAL
}

我想这是一个开始。让我们开始实现。我们立即有机会使用 Java 14 中的另一个不错的功能:*更智能的 switch 表达式*。switch 表达式为我们提供了一种从 switch case 的分支返回一个值然后将其赋值给变量的方法。语法与我们之前使用的几乎相同,只是每个 case 都用箭头 `->` 与分支隔开,而不是 `:`,并且不需要 `break` 语句。

在下面的示例中,我们将 `int` 值赋值给变量 `index`,由于最近 Java 版本中的另一个好特性,即使用 `var` 进行自动类型推断,我们不需要指定其类型。

	public Person create(String name, EmotionalState state) {
		var index = switch (state) {
			case SAD -> -1;
			case HAPPY -> 1;
			case NEUTRAL -> 0;
        };
        // todo 
	}

有了 `index`,我们可以创建执行针对数据库的 SQL 语句所需的 `PreparedStatement`。我们可以执行该预处理语句并传入一个 `KeyHolder`,它将用于收集从新插入的行返回的生成的键。


	public Person create(String name, EmotionalState state) {
		var index = switch (state) {
			case SAD -> -1;
			case HAPPY -> 1;
			case NEUTRAL -> 0;
		};
        var declaredParameters = List.of(
            new SqlParameter(Types.VARCHAR, "name"), 
            new SqlParameter(Types.INTEGER, "emotional_state"));
		var pscf = new PreparedStatementCreatorFactory(this.insertSql, declaredParameters) {
			{
				setReturnGeneratedKeys(true);
				setGeneratedKeysColumnNames("id");
			}
		};
		var psc = pscf.newPreparedStatementCreator(List.of(name, index));
		var kh = new GeneratedKeyHolder();
		this.template.update(psc, kh);
		// todo
    }

唯一的问题是返回的键是 `Number`,而不是 `Integer` 或 `Double` 或任何更具体的东西。这使我们有机会使用 Java 14 中另一个有趣的新特性,智能转换。智能转换允许我们在 `instanceof` 测试中测试类型后避免冗余转换。它更进一步,它为我们提供了一个变量名,我们可以通过它在测试范围内引用自动转换的变量。


	public Person create(String name, EmotionalState state) {
		var index = switch (state) {
			case SAD -> -1;
			case HAPPY -> 1;
			case NEUTRAL -> 0;
		};
        var declaredParameters = List.of(
            new SqlParameter(Types.VARCHAR, "name"), 
            new SqlParameter(Types.INTEGER, "emotional_state"));
		var pscf = new PreparedStatementCreatorFactory(this.insertSql, declaredParameters) {
			{
				setReturnGeneratedKeys(true);
				setGeneratedKeysColumnNames("id");
			}
		};
		var psc = pscf.newPreparedStatementCreator(List.of(name, index));
		var kh = new GeneratedKeyHolder();
		this.template.update(psc, kh);
		if (kh.getKey() instanceof Integer id) {
			return findById(id);
		}
		throw new IllegalArgumentException("we couldn't create the " + Person.class.getName() + "!");
    }

我们需要一个 `int` 才能将其传递给 `findById(Integer)`,而此方法将为我们完成这项工作。方便吧?

一切正常,让我们用一个简单的 `ApplicationListener` 来运行代码


@Component
class Runner {

	private final PeopleService peopleService;

	Runner(PeopleService peopleService) {
		this.peopleService = peopleService;
	}

	@EventListener(ApplicationReadyEvent.class)
	public void exercise() throws Exception {
		var elizabeth = this.peopleService.create("Elizabeth", EmotionalState.SAD);
		System.out.println(elizabeth);
	}
}

运行它,您将看到该对象已写入数据库,最棒的是——在打印生成的 `Person` 对象时,您得到了一个漂亮的新 `toString()` 结果!

我们只是触及了 Java 14 中所有新特性的表面!语言中有很多新特性我们已经在这个视频中开始介绍,运行时本身还有更多关于安全性和性能的特性。我强烈建议您找到一种方法摆脱旧版本的 JDK(看看你们这些 Java 8 用户!)并迁移到最新版本。

获取 Spring 新闻通讯

与 Spring 新闻通讯保持联系

订阅

领先一步

VMware 提供培训和认证,以加速您的进步。

了解更多

获取支持

Tanzu Spring 在一个简单的订阅中提供对 OpenJDK™、Spring 和 Apache Tomcat® 的支持和二进制文件。

了解更多

即将举行的活动

查看 Spring 社区中所有即将举行的活动。

查看全部