1. 程式人生 > >Spring 4支援的Java 8新特性一覽

Spring 4支援的Java 8新特性一覽

轉載於:http://www.infoq.com/cn/articles/spring-4-java-8?utm_source=infoq&utm_medium=related_content_link&utm_campaign=relatedContent_articles_clk

有眾多新特性和函式庫的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");

    Product product = new Product();
    product.setId(id);
    product.setDescription(description);
    product.setQuantity(quantity);
    product.setPrice(price);
    product.setAvailability(availability);

    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新特性的出色框架。