讀古今文學網 > Spring Boot實戰 > 2.3 使用自動配置 >

2.3 使用自動配置

簡而言之,Spring Boot的自動配置是一個運行時(更準確地說,是應用程序啟動時)的過程,考慮了眾多因素,才決定Spring配置應該用哪個,不該用哪個。舉幾個例子,下面這些情況都是Spring Boot的自動配置要考慮的。

  • Spring的JdbcTemplate是不是在Classpath裡?如果是,並且有DataSource的Bean,則自動配置一個JdbcTemplate的Bean。

  • Thymeleaf是不是在Classpath裡?如果是,則配置Thymeleaf的模板解析器、視圖解析器以及模板引擎。

  • Spring Security是不是在Classpath裡?如果是,則進行一個非常基本的Web安全設置。

每當應用程序啟動的時候,Spring Boot的自動配置都要做將近200個這樣的決定,涵蓋安全、集成、持久化、Web開發等諸多方面。所有這些自動配置就是為了盡量不讓你自己寫配置。

有意思的是,自動配置的東西很難寫在書本裡。如果不能寫出配置,那又該怎麼描述並討論它們呢?

2.3.1 專注於應用程序功能

要為Spring Boot的自動配置博得好感,我可以在接下來的幾頁裡向你演示沒有Spring Boot的情況下需要寫哪些配置。但眼下已經有不少好書寫過這些內容了,再寫一次並不能讓我們更快地寫好閱讀列表應用程序。

既然知道Spring Boot會替我們料理這些事情,那麼與其浪費時間討論這些Spring配置,還不如看看如何利用Spring Boot的自動配置,讓我們專注於應用程序代碼。除了開始寫代碼,我想不到更好的辦法了。

1. 定義領域模型

我們應用程序裡的核心領域概念是讀者閱讀列表上的書。因此我們需要定義一個實體類來表示這個概念。代碼清單2-5演示了如何定義一本書。

代碼清單2-5 表示列表裡的書的Book

package readinglist;

import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.GenerationType;
import javax.persistence.Id;

@Entity
public class Book {

  @Id
  @GeneratedValue(strategy=GenerationType.AUTO)
  private Long id;
  private String reader;
  private String isbn;
  private String title;
  private String author;
  private String description;

  public Long getId {
    return id;
  }

  public void setId(Long id) {
    this.id = id;
  }

  public String getReader {
    return reader;
  }

  public void setReader(String reader) {
    this.reader = reader;
  }

  public String getIsbn {
    return isbn;
  }

  public void setIsbn(String isbn) {
    this.isbn = isbn;
  }

  public String getTitle {
    return title;
  }

  public void setTitle(String title) {
    this.title = title;
  }

  public String getAuthor {
    return author;
  }

  public void setAuthor(String author) {
    this.author = author;
  }

  public String getDescription {
    return description;
  }

  public void setDescription(String description) {
    this.description = description;
  }

}

  

如你所見,Book類就是簡單的Java對象,其中有些描述書的屬性,還有必要的訪問方法。@Entity註解表明它是一個JPA實體,id屬性加了@Id@GeneratedValue註解,說明這個字段是實體的唯一標識,並且這個字段的值是自動生成的。

2. 定義倉庫接口

接下來,我們就要定義用於把Book對像持久化到數據庫的倉庫了。4因為用了Spring Data JPA,所以我們要做的就是簡單地定義一個接口,擴展一下Spring Data JPA的JpaRepository接口:

4原文這裡寫的是ReadingList對象,但文中並沒有定義這個對象,看代碼應該是Book對象。——譯者注

package readinglist;

import java.util.List;
import org.springframework.data.jpa.repository.JpaRepository;

public interface ReadingListRepository extends JpaRepository<Book, Long> {

  List<Book> findByReader(String reader);

}

  

通過擴展JpaRepositoryReadingListRepository直接繼承了18個執行常用持久化操作的方法。JpaRepository是個泛型接口,有兩個參數:倉庫操作的領域對像類型,及其ID屬性的類型。此外,我還增加了一個findByReader方法,可以根據讀者的用戶名來查找閱讀列表。

如果你好奇誰來實現這個ReadingListRepository及其繼承的18個方法,請不用擔心,Spring Data提供了很神奇的魔法,只需定義倉庫接口,在應用程序啟動後,該接口在運行時會自動實現。

3. 創建Web界面

現在,我們定義好了應用程序的領域模型,還有把領域對像持久化到數據庫裡的倉庫接口,剩下的就是創建Web前端了。代碼清單2-6的Spring MVC控制器就能為應用程序處理HTTP請求。

代碼清單2-6 作為閱讀列表應用程序前端的Spring MVC控制器

package readinglist;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.ui.Model;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;

import java.util.List;

@Controller
@RequestMapping("/")
public class ReadingListController {

  private ReadingListRepository readingListRepository;

  @Autowired
  public ReadingListController(
             ReadingListRepository readingListRepository) {
    this.readingListRepository = readingListRepository;
  }

  @RequestMapping(, method=RequestMethod.GET)
  public String readersBooks(
      @PathVariable("reader") String reader,
      Model model) {

    List<Book> readingList =
        readingListRepository.findByReader(reader);
    if (readingList != null) {
      model.addAttribute("books", readingList);
    }
    return "readingList";
  }

  @RequestMapping(, method=RequestMethod.POST)
  public String addToReadingList(
            @PathVariable("reader") String reader, Book book) {
    book.setReader(reader);
    readingListRepository.save(book);
    return "redirect:/{reader}";
  }

}

  

ReadingListController使用了@Controller註解,這樣組件掃瞄會自動將其註冊為Spring應用程序上下文裡的一個Bean。它還用了@RequestMapping註解,將其中所有的處理器方法都映射到了「/」這個URL路徑上。

該控制器有兩個方法。

  • readersBooks:處理/{reader}上的HTTP GET請求,根據路徑裡指定的讀者,從(通過控制器的構造器注入的)倉庫獲取Book列表。隨後將這個列表塞入模型,用的鍵是books,最後返回readingList作為呈現模型的視圖邏輯名稱。

  • addToReadingList:處理/{reader}上的HTTP POST請求,將請求正文裡的數據綁定到一個Book對像上。該方法把Book對象的reader屬性設置為讀者的姓名,隨後通過倉庫的save方法保存修改後的Book對象,最後重定向到/{reader}(控制器中的另一個方法會處理該請求)。

readersBooks方法最後返回readingList作為邏輯視圖名,為此必須創建該視圖。因為在項目開始之初我就決定要用Thymeleaf來定義應用程序的視圖,所以接下來就在src/main/ resources/templates裡創建一個名為readingList.html的文件,內容如代碼清單2-7所示。

代碼清單2-7 呈現閱讀列表的Thymeleaf模板

<html>
  <head>
    <title>Reading List</title>
    <link rel="stylesheet" th:href="@{/style.css}"></link>
  </head>

  <body>
    <h2>Your Reading List</h2>
    <p th:unless="${#lists.isEmpty(books)}">
      <dl th:each="book : ${books}">
        <dt>
          <span th:text="${book.title}">Title</span> by
          <span th:text="${book.author}">Author</span>
          (ISBN: <span th:text="${book.isbn}">ISBN</span>)
        </dt>
        <dd>
          <span th:if="${book.description}"
                th:text="${book.description}">Description</span>
          <span th:if="${book.description eq null}">
                No description available</span>
        </dd>
      </dl>
    </p>
    <p th:if="${#lists.isEmpty(books)}">
      <p>You have no books in your book list</p>
    </p>

    <hr/>

    <h3>Add a book</h3>
    <form method="POST">
      <label for="title">Title:</label>
        <input type="text" name="title" size="50"></input><br/>
      <label for="author">Author:</label>
        <input type="text" name="author" size="50"></input><br/>
      <label for="isbn">ISBN:</label>
        <input type="text" name="isbn" size="15"></input><br/>
      <label for="description">Description:</label><br/>
        <textarea name="description" cols="80" rows="5">
        </textarea><br/>
      <input type="submit"></input>
    </form>

  </body>
</html>

  

這個模板定義了一個HTML頁面,該頁面概念上分為兩個部分:頁面上方是讀者的閱讀列表中的圖書清單;下方是是一個表單,讀者可以從這裡添加新書。

為了美觀,Thymeleaf模板引用了一個名為style.css的樣式文件,該文件位於src/main/resources/ static目錄中,看起來是這樣的:

body {
    background-color: #cccccc;
    font-family: arial,helvetica,sans-serif;
}

.bookHeadline {
    font-size: 12pt;
    font-weight: bold;
}

.bookDescription {
    font-size: 10pt;
}

label {
    font-weight: bold;
}

  

這個樣式表並不複雜,也沒有過分追求讓應用程序變漂亮,但已經能滿足我們的需求了。很快你就會看到,它能用來演示Spring Boot的自動配置功能。

不管你相不相信,以上就是一個完整的應用程序了——本章已經向你呈現了所有的代碼。等一下,回顧一下前幾頁的內容,你看到什麼配置了嗎?實際上,除了代碼清單2-1里的三行配置(這是開啟自動配置所必需的),你不用再寫任何Spring配置了。

雖然沒什麼Spring配置,但這已經是一個可以運行的完整Spring應用程序了。讓我們把它運行起來,看看會怎樣。

2.3.2 運行應用程序

運行Spring Boot應用程序有幾種方法。先前在2.5節裡,我們討論了如何通過Maven和Gradle來運行應用程序,以及如何構建並運行可執行JAR。稍後,在第8章裡你將看到如何構建WAR文件,並用傳統的方式部署到Java Web應用服務器裡,比如Tomcat。

假設你正使用Spring Tool Suite開發應用程序,可以在IDE裡選中項目,在Run菜單裡選擇Run As > Spring Boot App,通過這種方式來運行應用程序,如圖2-3所示。

圖 2-3 在Spring Tool Suite裡運行Spring Boot應用程序

如果一切正常,你的瀏覽器應該會展現一個空白的閱讀列表,下方有一個用於向列表添加新書的表單,如圖2-4所示。

圖 2-4 初始狀態下的空閱讀列表

接下來,通過表單添加一些圖書吧。隨後你的閱讀列表看起來就會像圖2-5這樣。

圖 2-5 添加了一些圖書後的閱讀列表

再多用用這個應用程序吧。你準備好之後,我們就來看一下Spring Boot是如何做到不寫Spring配置代碼就能開發整個Spring應用程序的。

2.3.3 剛剛發生了什麼

如我所說,在沒有配置代碼的情況下,很難描述自動配置。與其花時間討論那些你不用做的事情,不如在這一節裡關注一下你要做的事——寫代碼。

當然,某處肯定是有些配置的。配置是Spring Framework的核心元素,必須要有東西告訴Spring如何運行應用程序。

在向應用程序加入Spring Boot時,有個名為spring-boot-autoconfigure的JAR文件,其中包含了很多配置類。每個配置類都在應用程序的Classpath裡,都有機會為應用程序的配置添磚加瓦。這些配置類裡有用於Thymeleaf的配置,有用於Spring Data JPA的配置,有用於Spiring MVC的配置,還有很多其他東西的配置,你可以自己選擇是否在Spring應用程序裡使用它們。

所有這些配置如此與眾不同,原因在於它們利用了Spring的條件化配置,這是Spring 4.0引入的新特性。條件化配置允許配置存在於應用程序中,但在滿足某些特定條件之前都忽略這個配置。

在Spring裡可以很方便地編寫你自己的條件,你所要做的就是實現Condition接口,覆蓋它的matches方法。舉例來說,下面這個簡單的條件類只有在Classpath裡存在JdbcTemplate時才會生效:

package readinglist;
import org.springframework.context.annotation.Condition;
import org.springframework.context.annotation.ConditionContext;
import org.springframework.core.type.AnnotatedTypeMetadata;

public class JdbcTemplateCondition implements Condition {
  @Override
  public boolean matches(ConditionContext context,
                         AnnotatedTypeMetadata metadata) {
    try {
      context.getClassLoader.loadClass(
             "org.springframework.jdbc.core.JdbcTemplate");
      return true;
    } catch (Exception e) {
      return false;
    }
  }
}

  

當你用Java來聲明Bean的時候,可以使用這個自定義條件類:

@Conditional(JdbcTemplateCondition.class)
public MyService myService {
    ...
}

  

在這個例子裡,只有當JdbcTemplateCondition類的條件成立時才會創建MyService這個Bean。也就是說MyService Bean創建的條件是Classpath裡有JdbcTemplate。否則,這個Bean的聲明就會被忽略掉。

雖然本例中的條件相當簡單,但Spring Boot定義了很多更有趣的條件,並把它們運用到了配置類上,這些配置類構成了Spring Boot的自動配置。Spring Boot運用條件化配置的方法是,定義多個特殊的條件化註解,並將它們用到配置類上。表2-1列出了Spring Boot提供的條件化註解。

表2-1 自動配置中使用的條件化註解

條件化註解

配置生效條件

@ConditionalOnBean

配置了某個特定Bean

@ConditionalOnMissingBean

沒有配置特定的Bean

@ConditionalOnClass

Classpath裡有指定的類

@ConditionalOnMissingClass

Classpath裡缺少指定的類

@ConditionalOnExpression

給定的Spring Expression Language(SpEL)表達式計算結果為true

@ConditionalOnJava

Java的版本匹配特定值或者一個範圍值

@ConditionalOnJndi

參數中給定的JNDI位置必須存在一個,如果沒有給參數,則要有JNDI InitialContext

@ConditionalOnProperty

指定的配置屬性要有一個明確的值

@ConditionalOnResource

Classpath裡有指定的資源

@ConditionalOnWebApplication

這是一個Web應用程序

@ConditionalOnNotWebApplication

這不是一個Web應用程序

一般來說,無需查看Spring Boot自動配置類的源代碼,但為了演示如何使用表2-1里的註解,我們可以看一下DataSourceAutoConfiguration裡的這個片段(這是Spring Boot自動配置庫的一部分):

@Configuration
@ConditionalOnClass({ DataSource.class, EmbeddedDatabaseType.class })
@EnableConfigurationProperties(DataSourceProperties.class)
@Import({ Registrar.class, DataSourcePoolMetadataProvidersConfiguration.class })
public class DataSourceAutoConfiguration {

...

}

  

如你所見,DataSourceAutoConfiguration添加了@Configuration註解,它從其他配置類裡導入了一些額外配置,還自己定義了一些Bean。最重要的是,DataSourceAutoConfiguration上添加了@ConditionalOnClass註解,要求Classpath裡必須要有DataSourceEmbeddedDatabaseType。如果它們不存在,條件就不成立,DataSourceAutoConfiguration提供的配置都會被忽略掉。

DataSourceAutoConfiguration裡嵌入了一個JdbcTemplateConfiguration類,自動配置了一個JdbcTemplate Bean

@Configuration
@Conditional(DataSourceAutoConfiguration.DataSourceAvailableCondition.class)
protected static class JdbcTemplateConfiguration {

  @Autowired(required = false)
  private DataSource dataSource;

  @Bean
  @ConditionalOnMissingBean(JdbcOperations.class)
  public JdbcTemplate jdbcTemplate {
    return new JdbcTemplate(this.dataSource);
  }

...

}

  

JdbcTemplateConfiguration使用了@Conditional註解,判斷DataSourceAvailableCondition條件是否成立——基本上就是要有一個DataSource Bean或者要自動配置創建一個。假設有DataSource Bean,使用了@Bean註解的jdbcTemplate方法會配置一個JdbcTemplate Bean。這個方法上還加了@ConditionalOnMissingBean註解,因此只有在不存在JdbcOperations(即JdbcTemplate實現的接口)類型的Bean時,才會創建JdbcTemplate Bean。

此處看到的只是DataSourceAutoConfiguration的冰山一角,Spring Boot提供的其他自動配置類也有很多知識沒有提到。但這已經足以說明Spring Boot如何利用條件化配置實現自動配置。

自動配置會做出以下配置決策,它們和之前的例子息息相關。

  • 因為Classpath裡有H2,所以會創建一個嵌入式的H2數據庫Bean,它的類型是javax.sql.DataSource,JPA實現(Hibernate)需要它來訪問數據庫。

  • 因為Classpath裡有Hibernate(Spring Data JPA傳遞引入的)的實體管理器,所以自動配置會配置與Hibernate相關的Bean,包括Spring的LocalContainerEntityManagerFactoryBeanJpaVendorAdapter

  • 因為Classpath裡有Spring Data JPA,所以它會自動配置為根據倉庫的接口創建倉庫實現。

  • 因為Classpath裡有Thymeleaf,所以Thymeleaf會配置為Spring MVC的視圖,包括一個Thymeleaf的模板解析器、模板引擎及視圖解析器。視圖解析器會解析相對於Classpath根目錄的/templates目錄裡的模板。

  • 因為Classpath裡有Spring MVC(歸功於Web起步依賴),所以會配置Spring的DispatcherServlet並啟用Spring MVC。

  • 因為這是一個Spring MVC Web應用程序,所以會註冊一個資源處理器,把相對於Classpath根目錄的/static目錄裡的靜態內容提供出來。(這個資源處理器還能處理/public、/resources和/META-INF/resources的靜態內容。)

  • 因為Classpath裡有Tomcat(通過Web起步依賴傳遞引用),所以會啟動一個嵌入式的Tomcat容器,監聽8080端口。

由此可見,Spring Boot自動配置承擔起了配置Spring的重任,因此你能專注於編寫自己的應用程序。