簡而言之,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);
}
通過擴展JpaRepository
,ReadingListRepository
直接繼承了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}上的HTTPGET
請求,根據路徑裡指定的讀者,從(通過控制器的構造器注入的)倉庫獲取Book
列表。隨後將這個列表塞入模型,用的鍵是books
,最後返回readingList
作為呈現模型的視圖邏輯名稱。addToReadingList
:處理/{reader}上的HTTPPOST
請求,將請求正文裡的數據綁定到一個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裡必須要有DataSource
和EmbeddedDatabaseType
。如果它們不存在,條件就不成立,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的
LocalContainerEntityManagerFactoryBean
和JpaVendorAdapter
。因為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的重任,因此你能專注於編寫自己的應用程序。