11 月 19 - 20 日 Apache Pulsar 社区年度盛会来啦,立即报名! 了解详情
写点什么

Spring 4 支持的 Java 8 新特性一览

  • 2015-03-31
  • 本文字数:5134 字

    阅读完需:约 17 分钟

有众多新特性和函数库的 Java 8 发布之后,Spring 4.x 已经支持其中的大部分。有些 Java 8 的新特性对 Spring 无影响,可以直接使用,但另有些新特性需要 Spring 的支持。本文将带您浏览 Spring 4.0 和 4.1 已经支持的 Java 8 新特性。

Spring 4 支持 Java 6、7 和 8

Java 8 编译器编译过的代码生成的.class 文件需要在 Java 8 或以上的 Java 虚拟机上运行。由于 Spring 对反射机制和 ASM、CGLIB 等字节码操作函数库的重度使用,必须确保这些函数库能理解 Java 8 生成的新 class 文件。因此 Spring 将 ASM、CGLIB 等函数库通过 jar jar( https://code.google.com/p/jarjar/ ) 嵌入 Spring 框架中,这样 Spring 就可以同时支持 Java6、7 和 8 的字节码代码而不会触发运行时错误。

Spring 框架本身是由 Java 8 编译器编译的,编译时使用的是生成 Java 6 字节码的编译命令选项。因此你可以 Java6、7 或者 8 来编译运行 Spring 4.x 的应用。

Spring 和 Java 8 的 Lambda 表达式

Java 8 的设计者想保证它是向下兼容的,以使其 lambda 表达式能在旧版本的代码编译器中使用。向下兼容通过定义函数式接口概念实现。

基本上,Java 8 的设计者分析了现有的 Java 代码体系,注意到很多 Java 程序员用只有一个方法的接口来表示方法的思想。以下就是 JDK 和 Spring 中只有一个方法的接口的例子,也就是所谓的“函数式接口”。

JDK 里的函数式接口:

复制代码
public interface Runnable {
public abstract void run();}
public interface Comparable<T> {
public int compareTo(T o);}

Spring 框架里的函数式接口:

复制代码
public interface ConnectionCallback<T> {
T doInConnection(Connection con) throws SQLException, DataAccessException;}
public interface RowMapper<T>{
T mapRow(ResultSet rs, int rowNum) throws SQLException;}

在 Java 8 里,任何函数式接口作为方法的参数传入或者作为方法返回值的场合,都可以用 lambda 表达式代替。例如,Spring 的 JdbcTemplate 类里有一个方法定义如下:

复制代码
public <T> List<T> query(String sql, RowMapper<T> rowMapper)
throws DataAccessException

这个查询方法的第二个参数需要 RowMapper 接口的一个实例。在 Java 8 中我们可以写一个 lambda 表达式作为第二个参数的值传进去。

别把代码写成这样:

复制代码
jdbcTemplate.query("SELECT * from products", new RowMapper<Product>(){
@Override
public Product mapRow(ResultSet rs, int rowNum) throws SQLException {
Integer id = rs.getInt("id");
String description = rs.getString("description");
Integer quantity = rs.getInt("quantity");
BigDecimal price = rs.getBigDecimal("price");
Date availability = rs.getDate("available_date");
Product product = new Product();
product.setId(id);
product.setDescription(description);
product.setQuantity(quantity);
product.setPrice(price);
product.setAvailability(availability);
return product;
}});

我们这么写:

复制代码
jdbcTemplate.query("SELECT * from queries.products", (rs, rowNum) -> {
Integer id = rs.getInt("id");
String description = rs.getString("description");
Integer quantity = rs.getInt("quantity");
BigDecimal price = rs.getBigDecimal("price");
Date availability = rs.getDate("available_date");
{1}
Product product = new Product();
product.setId(id);
product.setDescription(description);
product.setQuantity(quantity);
product.setPrice(price);
product.setAvailability(availability);
{1}
return product;});

我们注意到 Java 8 中这段代码使用了 lambda 表达式,这比之前的版本中使用匿名内部类的方式紧凑、简洁得多。

涵盖 Java 8 中函数式接口的所有细节超出了本文的范畴,我们强烈建议您从别处详细学习函数式接口。本文想要传达的关键点在于Java 8 的lambda 表达式能传到那些用Java 7 或更早的JDK 编译的、接受函数式接口作为参数的方法中。

Spring 的代码里有很多函数式接口,因此 lambda 表达式很容易与 Spring 结合使用。即便 Spring 框架本身被编译成 Java 6 的.class 文件格式,你仍然可以用 Java 8 的 lambda 表达式编写应用代码、用 Java 8 编译器编译、并且在 Java 8 虚拟机上运行,你的应用可以正常工作。

总之,因为 Spring 框架早在 Java 8 正式给函数式接口下定义之前就已经实际使用了函数式接口,因此在 Spring 里使用 lambda 表达式非常容易。

Spring 4 和 Java 8 的时间与日期 API

Java 开发者们一直痛恨 java.util.Date 类的设计缺陷,终于,Java 8 带来了全新的日期与时间 API,解决了那些久被诟病的问题。这个新的日期与时间 API 值得用一整篇文章的篇幅来讲述,因此我们在本文不会详述其细节,而是重点关注新的java.time 包中引入的众多新类,如LocalDate、LocalTime 和 LocalDateTime。

Spring 有一个数据转换框架,它可以使字符串和 Java 数据类型相互转换。Spring 4 升级了这个转换框架以支持 Java 8 日期与时间 API 里的那些类。因此你的代码可以这样写:

复制代码
@RestController
public class ExampleController {
@RequestMapping("/date/{localDate}")
public String get(@DateTimeFormat(iso = ISO.DATE) LocalDate localDate)
{
return localDate.toString();
}}

上面的例子中,get 方法的参数是 Java 8 的 LocalDate 类型,Spring 4 能接受一个字符串参数例如 2014-02-01 并将它转换成 Java 8 LocalDate 的实例。

要注意的是 Spring 通常会与其它一些库一起使用实现特定功能,比如与 Hibernate 一起实现数据持久化,与 Jackson 一起实现 Java 对象和 JSON 的互相转换。

虽然 Spring 4 支持 Java 8 的日期与时间库,这并不表示第三方框架如 Hibernate 和 Jackson 等也能支持它。到本文发表时,Hibernate JIRA 里仍有一个开放状态的请求 HHH-8844 要求在 Hibernate 里支持 Java 8 日期与时间 API。

Spring 4 与重复注解

Java 8 增加了对重复注解的支持,Spring 4 也同样支持。特殊的是,Spring 4 支持对注解 @Scheduled 和 @PropertySource 的重复。例如,请注意如下代码片段中对 @PropertySource 注解的重复使用:

复制代码
@Configuration
@ComponentScan
@EnableAutoConfiguration
@PropertySource("classpath:/example1.properties")
@PropertySource("classpath:/example2.properties")public class Application {
@Autowired
private Environment env;
@Bean
public JdbcTemplate template(DataSource datasource) {
System.out.println(env.getProperty("test.prop1"));
System.out.println(env.getProperty("test.prop2"));
return new JdbcTemplate(datasource);
}
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}}

Java 8 的 Optional<> 与 Spring 4.1

忘记检查空值引用是应用代码中一类常见的 bug 来源。消除 NullPointerExceptions 的方式之一是确保方法总是返回一个非空值。例如如下方法:

复制代码
public interface CustomerRepository extends CrudRepository<Customer, Long> {
/**
* returns the customer for the specified id or
* null if the value is not found
*/
public Customer findCustomerById(String id);}

用如下有缺陷的代码来调用 CustomerRepository :

复制代码
Customer customer = customerRepository.findCustomerById(“123”);
customer.getName(); // 得到空指针错误

这段代码的正确写法应该是:

复制代码
Customer customer = customerRepository.findCustomerById(“123”);if(customer != null) {
customer.getName(); // 避免空指针错误

}

理想状态下,如果我们没有检查某个值能否为空,我们希望编译器及时发现。 java.util.Optional 类让我们可以像这样写接口:

复制代码
public interface CustomerRepository extends CrudRepository<Customer, Long> {
public Optional<Customer> findCustomerById(String id);}

这样一来,这段代码的有缺陷版本不会被编译,开发者必须显式地检查这个 Optional 类型对象是否有值,代码如下:

复制代码
Optional<Customer> optional =
customerRepository.findCustomerById(“123”);if(optional.isPresent()) {
Customer customer = optional.get();
customer.getName();}

所以 Optional 的关键点在于确保开发者不用查阅 Javadoc 就能知道某个方法可以返回 null,或者可以把一个 null 值传给某方法。编译器和方法签名有助于开发者明确知道某个值是 Optional 类型。关于 Optional 类思想的详细描述请参考这里

Spring 4.1 有两种方式支持 Java Optional。Spring 的 @Autowired 注解有一个属性"required",使用之后我们可以把如下代码:

复制代码
@Service
public class MyService {
@Autowired(required=false)
OtherService otherService;
public doSomething() {
if(otherService != null) {
// use other service
}
}}

替换成:

复制代码
public class MyService {
@Autowired
Optional<OtherService> otherService;
public doSomething() {
otherService.ifPresent( s -> {
// use s to do something
});
}}

另一个能用 Optional 的地方是 Spring MVC 框架,可以用于表示某个处理方法的参数是可选的。例如:

复制代码
@RequestMapping(“/accounts/{accountId}”,requestMethod=RequestMethod.POST)
void update(Optional<String> accountId, @RequestBody Account account)

这段代码会告诉 Spring 其 accountId 是可选参数。

总之,Java 8 的 Optional 类通过减少空指针错误相关的缺陷简化了代码编写,同时 Spring 能很好地支持 Java 8 的 Optional 类。

参数名发现机制

Java 8 支持在编译后的代码中保留方法的参数名。这意味着 Spring 4 可以从方法中提取参数名,从而使 SpringMVC 代码更为简洁。例如:

复制代码
@RequestMapping("/accounts/{id}")public Account getAccount(@PathVariable("id") String id)

可以改写为:

复制代码
@RequestMapping("/accounts/{id}")public Account getAccount(@PathVariable String id)

可以看到我们把 @PathVariable(“id”) 替换成 @PathVariable,因为 Spring 4 能从编译后的 Java 8 代码中获取参数名——id。只要在编译时指定了–parameters 标记,Java 8 编译器就会把参数名写入.class 文件中。在 Java 8 发布之前,Spring 也可以从使用 -debug 选项编译之后的代码中提取出参数名。

在 Java 7 及之前的版本中,-debug 选项不会保留抽象方法的参数名。这会导致 Spring Data 这类基于 Java 接口自动生成其资源库实现的工程就会出现问题。比如接口如下:

复制代码
interface CustomerRepository extends CrudRepository<Customer, Long> {
@Query("select c from Customer c where c.lastname = :lastname")
List<Customer> findByLastname(@Param("lastname") String lastname);}

我们能看到 findByLastname 仍然需要 @Param(“lastname”),这是因为 findByLastname 是个抽象方法,而在 Java 7 及之前的版本里就算用了 -debug 选项也不会保留其参数名。而在 Java 8 中,使用–parameters 选项后,Spring Data 就能自动找到抽象方法的参数名,我们可以把上例中的接口改写成:

复制代码
interface CustomerRepository extends CrudRepository<Customer, Long> {
@Query("select c from Customer c where c.lastname = :lastname")
List<Customer> findByLastname(String lastname);}

这里我们已经不再需要 @Param(“lastname”),让代码更简洁且易于阅读。所以使用 Java 8 编译代码时加上–parameters 标记是个好方法。

总结

Spring 4 支持 Java 6、7 和 8,开发者可以随意使用 Java 6、7 或 8 来编写自己的应用代码。如果使用的是 Java 8,那么只要有函数式接口的地方就可以使用 lambda 表达式,使代码更为简洁易读。

Java 8 对某些库做了改进,比如新的 java.time 包和 Optional 类,Optional 类使得用 Spring 编写的代码更加简单明了。

最后,用–parameters 选项编译 Java 8 代码会在编译时保留方法的参数名,使得开发者得以编写更为紧凑的 Spring MVC 方法和 Spring Data 查询方法。

如果你已经准备在项目中使用 Java 8,你会发现 Spring 4 是个很好地利用了 Java 8 新特性的出色框架。

查看英文原文 Spring 4 and Java 8


感谢崔康对本文的审校。

给InfoQ 中文站投稿或者参与内容翻译工作,请邮件至 editors@cn.infoq.com 。也欢迎大家通过新浪微博( @InfoQ @丁晓昀),微信(微信号: InfoQChina )关注我们,并与我们的编辑和其他读者朋友交流。

2015-03-31 10:3520678

评论

发布
暂无评论
发现更多内容

运维与云

yann [扬] :曹同学

Yii2.0 RESTful API 之速率限制

Middleware

php RESTful Yii2

简述 HTTP 缓存相关的首部及其行为

黄耗子皮

缓存 HTTP

RocketMQ broker.properties

李绍俊

RocketMQ

技术工作的一二三之价值观方法论

拖地先生

个人成长 方法论

[JVM] String#intern 面试必会

猴哥一一 cium

Java JVM string pool string Java 25 周年

JUC整理笔记三之测试工具jcstress

JFound

Java

Yii2.0 RESTful API 基础配置教程

Middleware

php RESTful Yii2

Python 沙盒环境配置

黄耗子皮

介绍一下自研开源NLP工具库---MYNLP

陈吉米

自然语言处理 中文分词 mynlp nlp

Kubernetes in action 笔记

FeiLong

Kubernetes 容器

Yii2.0 RESTful API 认证教程

Middleware

php RESTful Yii2

Yii2.0 RESTful API 之版本控制

Middleware

php RESTful Yii2

一周信创舆情观察(5.18~5.24)

统小信uos

基础软件 操作系统

一个前端的 Windows10 开发环境

Gadzan

大前端 windows Windows Terminal 环境安装 开发工具

【译】并不存在的普通用户(面向极端用户的设计)

Yukun

设计思维 可用性

钱从哪里来 - 中国家庭的财富方案

石云升

读书笔记 工作 财富 买房 资产配置

技术工作的一二三之内功

拖地先生

个人成长

阅读对写作的好处

董一凡

写作

JVM最佳学习笔记---总览

Loubobooo

Java JVM

ESP8266远程控制+MicroPython 固件初体验

黄耗子皮

物联网 esp8266

运维那点事 - jenkins流水线

yann [扬] :曹同学

JVM最佳学习笔记<一>---Java内存区域与内存溢出异常

Loubobooo

Java JVM

JVM最佳学习笔记<四>---虚拟机类加载机制

Loubobooo

Java JVM

最长回文算法(马拉车算法)分析

Gadzan

Java 算法 LeetCode

到底谁是你老板

Neco.W

工作 创业心态

企业也有中年危机?探讨数字化与永续经营

FinClip

数字化转型 小程序生态

JVM最佳学习笔记<三>---虚拟机性能监控与故障处理工具

Loubobooo

Java JVM

技术工作的一二三之快餐

拖地先生

项目管理 软件开发 技术管理 软件开发流程

Java 异步编程:从 Future 到 Loom

理帆

Java 并发编程 kotlin Netty

JVM最佳学习笔记<二>---垃圾收集器与内存分配策略

Loubobooo

Java JVM

Spring 4支持的Java 8新特性一览_语言 & 开发_Adib Saikali_InfoQ精选文章