初試Spring Boot:構建第一個Web程序

Spring Boot主要提供快速構建項目的功能。本文中我們會使用Spring Boot構建第一個Web程序,同時介紹Spring Boot最簡單的功能,例如運行單元測試,發佈與調用REST服務等。

1 Spring Boot介紹

我們先來了解一下Spring Boot項目。

1.1 Spring Boot簡介

開發一個全新的項目,需要先搭建開發環境,例如確定要使用的技術框架及版本,還要考慮各個框架之間的版本兼容問題。完成這些煩瑣的工作後,還要對新項目進行配置,測試其能否正常運行,最後才將搭建好的環境提交給項目組的其他成員使用。經常會出現的情形是,項目表面上已經成功運行,但部分項目組成員仍然無法運行項目。對於每一個項目,在初期都會浪費大量的時間做這些固定的事情。

受Ruby On Rails、Node.js等技術的影響,Java EE領域需要一種更為簡便的開發方式,來取代這些煩瑣的項目搭建工作。在此背景下,Spring推出了Spring Boot項目,該項目可以讓使用者更快速地搭建項目,從而使用者可以更專注於業務系統的開發。系統配置、基礎代碼、項目依賴的jar包,甚至開發時所用到的應用服務器等,Spring Boot都可以幫我們準備好。只要在建立項目時,使用構建工具加入相應的Spring Boot依賴包,項目即可運行,使用者無須關心版本兼容等問題。

Spring Boot支持Maven和Gradle這兩款構建工具。Gradle使用Groovy語言編寫構建腳本,與Maven、Ant等構建工具有良好的兼容性。鑑於筆者使用Maven較多,因此本文使用Maven作為項目構建工具。本文寫作時,Spring Boot最新的正式版本為2.0.1,其要求Maven版本為3.2或以上。

1.2 starter模塊

Spring Boot提供了許多starter模塊,starter為我們提供了“一站式”服務,在項目中另加入對應框架的starter的依賴,可以免去我們到處尋找依賴包的煩惱。只需要加入一個依賴,項目就可以運行,這就是starter的作用。例如,如果你想讓你的項目擁有訪問關係型數據庫的能力,則只需要在你的項目中加入spring-boot-starter-data-jpa依賴就可以實現:

<dependency>
<groupid>org.springframework.boot/<groupid>
<artifactid>spring-boot-starter-data-jpa/<artifactid>
<version>2.0.1.RELEASE/<version>
/<dependency>

Spring Boot官方提供的starter模塊,命名規則為“spring-boot-starter-*”,其中“*”代表對應的應用類型,這些starter的名稱,可以幫助我們快速地找到相應的依賴。如果想構建自己的starter模塊,官方建議命名規則為“project-spring-boot-starter”。

下面介紹一些比較常用的starter模塊。

  • spring-boot-starter-web:用於構建Web應用的starter模塊,加入該依賴後,會包含Spring MVC框架,默認內嵌Tomcat容器。
  • spring-boot-starter-jpa:用於構建Spring Data JPA應用,使用Hibernate作為ORM框架。
  • spring-boot-starter-mongodb:用於構建Spring Data MongoDB應用,底層使用MongoDB驅動操作MongoDB數據庫。
  • spring-boot-starter-redis:用於構建Spring Data Redis應用,使用Jedis框架操作Redis數據庫。
  • spring-boot-starter-thymeleaf:構建一個使用Thymeleaf作為視圖的Web應用。
  • spring-boot-starter-test:顧名思義,這個starter模塊主要用於進行單元測試。

2 構建第一個Spring Boot程序

這一節,我們使用Spring Boot構建一個最簡單的Web應用。

2.1 新建Maven項目

Spring Boot使用3.2以上版本的Maven,這裡我們使用的版本為3.5。在Eclipse的菜單中選擇“File→New→Other”命令,選中“Maven”下的“Maven Project”,單擊“Next”按鈕,在“New Maven Project”頁面中,選擇“Create a simple project”項,創建一個簡單的Maven項目。在彈出的創建項目頁面中,輸入相應的項目信息,如下圖。

初試Spring Boot:構建第一個Web程序

新建Maven項目

新建的Maven項目,其結構如下。

初試Spring Boot:構建第一個Web程序

Maven項目結構

src/main/java用於存放主應用程序的類,src/main/resources用於存放主應用程序的資源文件,src/test用於存放測試相關的Java類和資源,pom.xml則是Maven的構建腳本。

一般情況下,Maven腳本文件需要繼承“spring-boot-starter-parent”項目,並在腳本中根據需要聲明一個或多個starter。修改項目的pom.xml文件,如代碼清單2-1所示。

代碼清單2-1:codes\02\first-boot\pom.xml

<project> xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0
http://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelversion>4.0.0/<modelversion>
<groupid>org.crazyit.boot.c2/<groupid>
<artifactid>first-boot/<artifactid>
<version>0.0.1-SNAPSHOT/<version>


<parent>
<groupid>org.springframework.boot/<groupid>
<artifactid>spring-boot-starter-parent/<artifactid>
<version>2.0.1.RELEASE/<version>
/<parent>


<dependencies>
<dependency>
<groupid>org.springframework.boot/<groupid>
<artifactid>spring-boot-starter-web/<artifactid>
/<dependency>
/<dependencies>

/<project>

注意:在加入依賴後,如果Eclipse中的Maven項目存在錯誤,則可以選中項目,鼠標右擊,在彈出的菜單中選擇“Maven→Update Project”命令來解決問題。

加入依賴後,由於starter的作用,Maven會自動幫我們引用Spring框架的各個模塊,例如spring-context、spring-web等,還會引入嵌入式的Tomcat。具體會幫我們的項目加入哪些依賴包,我們在Eclipse下面看一下,有個大概印象即可。

2.2 編寫啟動類

編寫一個簡單的啟動類,就可以直接啟動Web服務,啟動類如代碼清單2-2所示。

代碼清單2-2:codes\02\first-boot\src\main\java\org\crazyit\boot\c2\FirstApp.java

@SpringBootApplication
public class FirstApp {

public static void main(String[] args) {
SpringApplication.run(FirstApp.class, args);
}

}

FirstApp類使用了@SpringBootApplication註解,該註解聲明該類是一個Spring Boot應用,該註解具有@SpringBootConfiguration、@EnableAutoConfiguration、@ComponentScan等註解的功能。直接運行MyApp的main方法,看到以下輸出信息後,證明啟動成功:

 2017-10-30 11:40:49.968 INFO 5916 --- [ main] o.s.j.e.a.AnnotationMBeanExporter : Registering beans for JMX exposure on startup
2017-10-30 11:40:50.199 INFO 5916 --- [ main] s.b.c.e.t.TomcatEmbeddedServletContainer : Tomcat started on port(s): 8080 (http)
2017-10-30 11:40:50.208 INFO 5916 --- [ main] org.crazyit.boot.c2.FirstApp : Started FirstApp in 6.226 seconds (JVM running for 6.888)

根據輸出信息可知,啟動的Tomcat端口為8080。打開瀏覽器訪問http://localhost:8080,可以看到錯誤頁面,表示應用已經成功啟動。

注意:在默認情況下,啟動端口為8080,如果需要修改Tomcat端口,則可以在src/main/ resources目錄下面,新建application.properties文件,通過設置server.port屬性來配置Tomcat端口。

2.3 編寫控制器

前面我們加入了spring-boot-starter-web模塊,默認集成了Spring MVC,因此只需要編寫一個Controller,即可實現一個最簡單的HelloWorld程序。代碼清單2-3為該控制器的代碼。

代碼清單2-3:codes\02\first-

@Controller
public class MyController {

@GetMapping("/hello")
@ResponseBody
public String hello() {
return "Hello World";
}
}

在代碼清單2-3中,使用了@Controller註解來修飾MyController,由於啟動類中使用了@SpringBootApplication註解,該註解具有@ComponentScan的功能,因此@Controller會被掃描並註冊。在hello方法中使用了@GetMapping與@ResponseBody註解,以聲明hello方法的訪問地址及返回內容。重新運行啟動類,打開瀏覽器並訪問地址http://localhost:8080/hello,則可以看到控制器的返回內容。

至此,我們的第一個Spring Boot Web程序已經完成了,整個過程非常簡單。大家可以看到,使用Spring Boot後,使我們節省了很多搭建項目框架的時間,Spring Boot的starter提供了這種“一站式”的服務,幫助我們開發Web應用。

2.4 開發環境的熱部署

每次修改Java後,都需要重新運行main方法才能生效,這樣會降低開發效率。我們可以使用Spring Boot提供的開發工具來實現熱部署,為項目加上以下依賴:

 <dependency>
<groupid>org.springframework.boot/<groupid>
<artifactid>spring-boot-devtools/<artifactid>
/<dependency>

修改Java文件後,容器會重新加載本項目的Java類。

3 運行單元測試

單元測試對於程序來說非常重要,它不僅能增強程序的健壯性,而且也為程序的重構提供了依據,目前很多開源項目的測試覆蓋率高達90%以上,可見大家對單元測試的重視。Spring Boot運行Web應用,只需要執行main方法即可,那麼如何測試這個Web程序?如何測試Spring Boot中的組件呢?這一節,將簡單介紹Spring Boot的單元測試。

3.1 測試Web服務

Spring Boot提供了@SpringBootTest註解,可以讓我們在單元測試中測試Spring Boot的程序。先為我們的項目加入“spring-boot-starter-test”依賴:

 <dependency>
<groupid>org.springframework.boot/<groupid>
<artifactid>spring-boot-starter-test/<artifactid>
/<dependency>

新建測試類,用於測試第一個Spring Boot程序的“/hello”服務,測試類請見代碼清單3-1。

代碼清單3-1:codes\02\first-

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.RANDOM_PORT)
public class RandomPortTest {

@Autowired
private TestRestTemplate restTemplate;

@Test
public void testHello() {
// 測試hello方法
String result = restTemplate.getForObject("/hello", String.class);
assertEquals("Hello World", result);
}
}

在代碼清單3-1中,為測試類加入了@RunWith、@SpringBootTest註解,其中為SpringBootTest配置了webEnvironment屬性,表示在運行測試時,會為Web容器隨機分配端口。在測試方法中,使用@Test註解修飾,使用TestRestTemplate調用“/hello”服務。

這個TestRestTemplate對象,實際上是對RestTemplate進行了封裝,可以讓我們在測試環境更方便使用RestTemplate的功能,例如代碼清單3-1,我們不需要知道Web容器的端口是多少,就可以直接進行測試。

在代碼清單3-1中配置了隨機端口,如果想使用固定的端口,則可以將webEnvironment配置為WebEnvironment.DEFINED_PORT。使用該屬性,會讀取項目配置文件(例如application.properties)中的端口(server.port屬性)來啟動Web容器,如果沒有配置,則使用默認端口8080。

3.2 模擬Web測試

在設置@SpringBootTest的webEnvironment屬性時,不管設置為RANDOM_PORT還是設置為DEFINED_PORT,在運行單元測試時,都會啟動一個真實的Web容器。如果不想啟動真實的Web容器,則可以將webEnvironment屬性設置為WebEnvironment.MOCK,來啟動一個模擬的Web容器,如代碼清單3-2所示。

代碼清單3-2:codes\02\first-boot\src\test\java\org\crazyit\boot\c2\MockEnvTest.java

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.MOCK)
@AutoConfigureMockMvc
public class MockEnvTest {

@Autowired
private MockMvc mvc;

@Test
public void testHello() throws Exception {
ResultActions ra = mvc.perform(MockMvcRequestBuilders.get(new URI(
"/hello")));
MvcResult result = ra.andReturn();
System.out.println(result.getResponse().getContentAsString());
}
}

為測試類加入@AutoConfigureMockMvc註解,讓其進行mock相關的自動配置。在測試方法中,使用Spring的MockMvc進行模擬測試,向“/hello”發送請求並得到回應。

注意:webEnvironment屬性的默認值是WebEnvironment.MOCK,只所以在代碼清單3-2中“多此一舉”,是為了展示該配置。

3.3 測試業務組件

前面都是針對Web容器進行測試,如果不想測試Web容器,只是想測試容器中的bean,則可以只啟動Spring的容器,請見代碼清單3-3。

代碼清單3-3:codes\02\first-boot\src\main\java\org\crazyit\boot\c2\MyService.java

codes\02\first-boot\src\test\java\org\crazyit\boot\c2\MyServiceTest.java

@Service
public class MyService {

public String helloService() {
return "hello";
}
}

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.NONE)
public class MyServiceTest {

@Autowired
private MyService myService;

@Test
public void testHello() {
String result = myService.helloService();
System.out.println(result);
}
}

在代碼清單3-3中,新建了一個MyService的服務類,MyServiceTest會對該類進行測試,直接在測試類中注入MyService的實例。注意,SpringBootTest的webEnvironment屬性被設置為NONE,因而Web容器將不會被啟動。

3.4 模擬業務組件

在實際應用中,我們的程序可能會操作數據庫,也有可能調用第三方接口,為了不讓這些外部的不穩定因素影響單元測試的運行結果,可以使用mock來模擬某些組件的返回結果,確保被測試組件代碼的健壯性。代碼清單3-4顯示了兩個業務組件。

代碼清單3-4:codes\02\first-boot\src\main\java\org\crazyit\boot\c2\MainService.java

codes\02\first-boot\src\main\java\org\crazyit\boot\c2\RemoteService.java

@Service
public class RemoteService {

public String call() {
return "hello";
}
}

@Service
public class MainService {

@Autowired
private RemoteService remoteService;

public void mainService() {
System.out.println("這是需要測試的業務方法");
String result = remoteService.call();
System.out.println("調用結果:" + result);
}
}

RemoteService的call方法在正常情況下會返回hello字符串,MainService中的mainService方法會調用call方法。假設call方法無法正常運行,為了能測試MainService,我們需要模擬call方法的返回結果。代碼清單3-5為MainService的測試方法。

代碼清單3-5:codes\02\first-boot\src\test\java\org\crazyit\boot\c2\MockTest.java

@RunWith(SpringRunner.class)
@SpringBootTest
public class MockTest {

@MockBean
private RemoteService remoteService;

@Autowired
private MainService mainService;

@Test
public void testMainService() {
// 模擬remoteService 的 call 方法返回 angus
BDDMockito.given(this.remoteService.call()).willReturn("angus");
mainService.mainService();
}
}

在測試類中,使用MockBean來修飾需要模擬的組件,在測試方法中使用了Mockito的API來模擬remoteService的call方法返回。在模擬中這個方法被調用後,將會返回“angus”字符串,運行代碼清單3-5,輸出結果如下:

這是需要測試的業務方法
調用結果:angus

根據結果可知,RemoteService的call方法被成功模擬。

這一節,簡單介紹瞭如何在Spring Boot中進行單元測試,本節的知識基本上能滿足大部分的需求,由於篇幅所限,在此不展開討論。我們下面介紹如何使用Spring Boot來發布和調用REST服務。

4 發佈與調用REST服務

在系統間進行通信,很多系統都會選擇SOAP協議,隨著REST的興起,現在很多系統在發佈與調用Web Service時,都首選REST。這一節,我們介紹如何在Spring Boot中發佈和調用REST服務。

4.1 REST

REST是英文Representational State Transfer的縮寫,一般翻譯為“表述性狀態轉移”,是Roy Thomas Fielding博士在他的論文“Architectural Styles and the Design of Network-based Software Architectures”中提出的一個術語。REST本身只是分佈式系統設計中的一種架構風格,並不是某種標準或者規範,而是一種輕量級的基於HTTP協議的Web Service風格。從另外一個角度看,REST更像是一種設計原則,更可以將其理解為一種思想。

4.2 發佈REST服務

在Spring Boot中發佈REST服務非常簡單,只需要在控制器中使用@RestController即可。下面我們來看一個示例。新建一個rest-server的Maven項目,加入“spring-boot-starter-web”依賴,將啟動類和控制器寫入同一個類中,請見代碼清單4-1。

代碼清單4-1:codes\02\rest-server\src\main\java\org\crazyit\boot\c2\RestApp.java

@SpringBootApplication
@RestController
public class RestApp {

public static void main(String[] args) {
SpringApplication.run(RestApp.class, args);
}

@GetMapping(value = "/person/{name}", produces = MediaType.APPLICATION_JSON_VALUE)
public Person person(@PathVariable String name) {
Person p = new Person();
p.setName(name);
p.setAge(33);
return p;
}

static class Person {

String name;
Integer age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}

}
}

在代碼清單4-1中,發佈了一個“/person/name”的服務,調用該服務後,會返回一個Person實例的JSON字符串,該服務對應的方法使用了組合註解@GetMapping,該註解的作用相當於@RequestMapping(method = RequestMethod.GET)。運行代碼清單4-1,在瀏覽器中訪問:http://localhost:8080/person/angus,則返回以下信息:{"name":"angus","age":33}。

很簡單的一個註解就幫我們完成了發佈REST服務的工作,這再一次展示了Spring Boot的便捷。如果不使用Spring Boot,估計你還要為尋找依賴包而疲於奔命。

4.3 使用RestTemplate調用服務

下面,我們使用Spring的RestTemplate來調用服務。RestTemplate是Spring Framework的一個類,其主要用來調用REST服務,它提供了攔截器機制,我們可以對它進行個性化定製。另外,在Spring Cloud中也可以使用RestTemplate來調用服務,而且還可以實現負載均衡的功能,有興趣的朋友可參考筆者的另外一本書《瘋狂Spring Cloud微服務架構實戰》。

我們來看一個例子。

新建一個rest-client的Maven項目,加入“spring-boot-starter-web”與“spring-boot-starter-test”的依賴,新建一個最普通的main方法,直接調用前面的服務,請見代碼清單4-2。

代碼清單4-2:codes\02\rest-client\src\main\java\org\crazyit\boot\c2\RestTemplateMain.java

/**
* 在main方法中使用RestTemplate
* @author 楊恩雄
*
*/
public class RestTemplateMain {

public static void main(String[] args) {
RestTemplate tpl = new RestTemplate();
Person p = tpl.getForObject("http://localhost:8080/person/angus", Person.class);
System.out.println(p.getName() + "---" + p.getAge());
}
}

在main方法中,直接創建RestTemplate的實例並調用服務,操作非常簡單。如果想在Spring的bean裡面使用RestTemplate,則可以使用RestTemplateBuilder,請見代碼清單4-3。

代碼清單4-3:codes\02\rest-client\src\main\java\org\crazyit\boot\c2\MyService.java

@Service
public class MyService {

@Autowired
private RestTemplateBuilder builder;

@Bean
public RestTemplate restTemplate() {
return builder.rootUri("http://localhost:8080").build();
}

/**

* 使用 RestTemplateBuilder 來創建template
*/
public Person useBuilder() {
Person p = restTemplate().getForObject("/person/angus", Person.class);
return p;
}
}

在我們自已的bean裡面注入RestTemplateBuilder,創建一個RestTemplate的bean。在創建RestTemplate實例時,使用RestTemplateBuilder的rootUri方法設置訪問的URI。除了rootUri方法外,RestTemplateBuilder還提供了很多方法用於設置RestTemplate,在此不再贅述。接下來,編寫一個單元測試類,來測試我們這個MyService的bean,請見代碼清單4-4。

代碼清單4-4:codes\02\rest-client\src\test\java\org\crazyit\boot\c2\MyServiceTest.java

@RunWith(SpringRunner.class)
@SpringBootTest(webEnvironment = WebEnvironment.NONE)
public class MyServiceTest {

@Autowired
private MyService myService;

@Test
public void testUserTemplate() {
Person p = myService.useBuilder();
System.out.println(p.getName() + "===" + p.getAge());
}
}

與前面的單元測試類似,直接注入MyService即可。

注意:在運行單元測試時,項目中一定要有Spring Boot的啟動類,否則會得到以下異常:java.lang.IllegalStateException: Unable to find a @SpringBootConfiguration, you need to use @ContextConfiguration or @SpringBootTest(classes=...) with your test

Spring的RestTemplate只是眾多REST客戶端中的一個。接下來,我們介紹另外一個REST客戶端Feign。

4.4 使用Feign調用服務

Feign是Github上的一個開源項目,其目的是簡化Web Service客戶端的開發。Spring Cloud項目將Feign整合進來,讓其作為REST客戶端。這一節,我們來了解如何使用Feign框架調用REST服務。在rest-client項目中加入以下依賴:

 <dependency>
<groupid>io.github.openfeign/<groupid>
<artifactid>feign-core/<artifactid>
<version>9.5.0/<version>
/<dependency>
<dependency>
<groupid>io.github.openfeign/<groupid>
<artifactid>feign-gson/<artifactid>
<version>9.5.0/<version>
/<dependency>

新建PersonClient接口,請見代碼清單4-5。

代碼清單4-5:codes\02\rest-client\src\main\java\org\crazyit\boot\c2\feign\PersonClient.java

/**
* Feign客戶端接口
* @author 楊恩雄
*/
public interface PersonClient {

@RequestLine("GET /person/{name}")
Person getPerson(@Param("name") String name);

}

在接口中,使用了@RequestLine和@Param註解,這兩個註解是Feign的註解。使用註解修飾後,getPerson方法被調用,然後使用HTTP的GET方法向“/person/name”服務發送請求。接下來編寫客戶端運行類,請見代碼清單4-6。

代碼清單4-6:codes\02\rest-client\src\main\java\org\crazyit\boot\c2\feign\FeignMain.java

public class FeignMain {

public static void main(String[] args) {
// 調用Hello接口
PersonClient pc = Feign.builder()
.decoder(new GsonDecoder())
.target(PersonClient.class, "http://localhost:8080/");
Person p = pc.getPerson("angus");
System.out.println(p.getName() + "---" + p.getAge());
}
}

在代碼清單4-6中,使用Feign來創建PersonClient接口的實例,最後通過調用接口方法來訪問服務。熟悉AOP的朋友可能已經猜到,Feign實際上幫助我們動態生成了代理類,Feign使用的是JDK的動態代理,代理類會將請求的信息封裝,最終使用java.netHttpURLConnection來發送HTTP請求。如果想將這裡的PersonClient作為bean放到Spring容器中,則可以添加一個創建該實例的方法:

 @Bean
public PersonClient personClient() {
return Feign.builder()
.decoder(new GsonDecoder())
.target(PersonClient.class, "http://localhost:8080/");
}

使用Feign來調用REST服務,使人感覺更加面向對象了。除了RestTemplate和Feign之外,還可以使用諸如Restlet、CXF等框架來調用REST服務,在此不再贅述。

5 本文小結

本文主要運行了第一個Spring Boot程序,通過這個程序,大家可以瞭解什麼是Spring Boot,在此過程中,大家應該也能感受到Spring Boot的便捷。除了這個簡單的Spring Boot程序外,還介紹瞭如何在Spring Boot環境中運行單元測試,包括對Web應用的測試、對Spring組件的模擬測試。最後,介紹瞭如何在Spring Boot中發佈和調用REST服務,其中重點介紹了RestTemplate和Feign框架。本文作為Spring Boot入門的文章,涉及的知識較為簡單,在

《Spring Boot 2+Thymeleaf企業應用實戰》一書中我們會繼續學習Spring Boot。


本文選自《Spring Boot 2+Thymeleaf企業應用實戰》,作者楊恩雄,電子工業出版社9月出版。本書詳情請點擊瞭解更多


分享到:


相關文章: