Spring Tips: 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 版本发布。这些新版本可以在生产环境中使用,但只在两次发布之间的 6 个月内获得支持。偶尔,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、toStringequals 实现的编译器合成对象。我甚至可能会略带不情愿地提及其他语言能够轻松完成这种繁琐工作的能力。Scala 支持*case class*。Kotlin 支持*data class*。

Java 14 支持*record*。

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

不错吧?这个语法非常强大!它为我们提供了一个包含构造函数和构造函数参数、属性、equalstoString 实现等的新对象。我们可以像实例化任何其他对象一样实例化这个对象。尝试解引用对象中的属性,你会发现我们的构造函数属性变成了 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 String,他们还会经常使用 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 方法。

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

enum EmotionalState {
	SAD, HAPPY, NEUTRAL
}

我想,这是一个开始。让我们进入实现。立刻我们就有了使用 Java 14 中另一个很棒的新特性的机会:*更智能的 switch 表达式*。switch 表达式提供了一种从 switch 分支返回值,然后将其赋值给变量的方式。其语法*几乎*与我们之前使用的完全相同,不同之处在于每个 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 类型,而不是 IntegerDouble 或任何更具体的类型。这给了我们一个机会使用 Java 14 中另一个有趣的新特性,智能类型转换(smart casting)。智能类型转换允许我们在 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<ApplicationReadyEvent> 来运行代码。


@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 社区所有即将举行的活动。

查看全部