明月松间照,清泉石上流。
概述
Spring Boot
是由Pivotal
团队提供的全新框架,其设计目的是用来简化Spring
应用的创建、运行、调试、部署等。它大大减少了基于Spring
开发的生产级应用程序的工作量。因此,开发人员能够真正专注于以业务为中心的功能。
本章我们将通过几个步骤演示如何使用Spring Boo
t构建RESTful
服务。我们将创建一个简单的客户服务CRUD
(也就是创建,读取,更新,删除)客户记录和每个客户拥有的银行帐户。
Spring
Initializr
Spring Initializr
是展开Spring Boot
的第一步。它用于创建Spring Boot
应用程序的项目结构。在开始Spring Boot
之前,我们需要弄清项目结构并确定将配置文件,属性文件和静态文件保留在何处。打开基于Web的界面开始。如下图所示,填写字段,然后单击“生成项目”按钮。
Group
:com.howtodoinjava.rest
Artifact
:customerservice
Name
:customerservice
Package Name
:com.howtodoinjava.rest.customerservice
Dependencies
:Web
,JPA
,H2
Spring Initializr
创建一个项目
项目目录结构
如下所示的POM
文件表示启动项目的依赖关系。在Spring Boot
中,不同的启动程序项目代表不同的Spring
模块,例如MVC
,ORM
等。开发人员主要要做的是在依赖项中添加启动程序项目,Spring Boot
将管理可传递的依赖项和版本。
<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 https://maven.apache.org/xsd/maven-4.0.0.xsd">
<modelVersion>4.0.0</modelVersion>
<parent>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-parent</artifactId>
<version>2.2.0.RELEASE</version>
<relativePath/> <!-- lookup parent from repository -->
</parent>
<groupId>com.howtodoinjava</groupId>
<artifactId>customerservice</artifactId>
<version>0.0.1-SNAPSHOT</version>
<name>customerservice</name>
<description>Demo project for Spring Boot</description>
<properties>
<java.version>1.8</java.version>
</properties>
<dependencies>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-data-jpa</artifactId>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-web</artifactId>
</dependency>
<dependency>
<groupId>com.h2database</groupId>
<artifactId>h2</artifactId>
<scope>runtime</scope>
</dependency>
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-test</artifactId>
<scope>test</scope>
<exclusions>
<exclusion>
<groupId>org.junit.vintage</groupId>
<artifactId>junit-vintage-engine</artifactId>
</exclusion>
</exclusions>
</dependency>
</dependencies>
<build>
<plugins>
<plugin>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-maven-plugin</artifactId>
</plugin>
</plugins>
</build>
</project>
如果我们运行mvnw dependency:tree
命令,则底层依赖关系层次结构将如下所示
[INFO] Scanning for projects...
[INFO]
[INFO] ----------------< com.codespeaks.rest:customerservice >-----------------
[INFO] Building customerservice 0.0.1-SNAPSHOT
[INFO] --------------------------------[ jar ]---------------------------------
[INFO]
[INFO] --- maven-dependency-plugin:3.0.2:tree (default-cli) @ customerservice ---
[INFO] com.codespeaks.rest:customerservice:jar:0.0.1-SNAPSHOT
[INFO] +- org.springframework.boot:spring-boot-starter-data-jpa:jar:2.0.6.RELEASE:compile
[INFO] | +- org.springframework.boot:spring-boot-starter:jar:2.0.6.RELEASE:compile
[INFO] | | +- org.springframework.boot:spring-boot:jar:2.0.6.RELEASE:compile
[INFO] | | +- org.springframework.boot:spring-boot-autoconfigure:jar:2.0.6.RELEASE:compile
[INFO] | | +- org.springframework.boot:spring-boot-starter-logging:jar:2.0.6.RELEASE:compile
[INFO] | | | +- ch.qos.logback:logback-classic:jar:1.2.3:compile
[INFO] | | | | \- ch.qos.logback:logback-core:jar:1.2.3:compile
[INFO] | | | +- org.apache.logging.log4j:log4j-to-slf4j:jar:2.10.0:compile
[INFO] | | | | \- org.apache.logging.log4j:log4j-api:jar:2.10.0:compile
[INFO] | | | \- org.slf4j:jul-to-slf4j:jar:1.7.25:compile
[INFO] | | +- javax.annotation:javax.annotation-api:jar:1.3.2:compile
[INFO] | | \- org.yaml:snakeyaml:jar:1.19:runtime
[INFO] | +- org.springframework.boot:spring-boot-starter-aop:jar:2.0.6.RELEASE:compile
[INFO] | | +- org.springframework:spring-aop:jar:5.0.10.RELEASE:compile
[INFO] | | \- org.aspectj:aspectjweaver:jar:1.8.13:compile
[INFO] | +- org.springframework.boot:spring-boot-starter-jdbc:jar:2.0.6.RELEASE:compile
[INFO] | | +- com.zaxxer:HikariCP:jar:2.7.9:compile
[INFO] | | \- org.springframework:spring-jdbc:jar:5.0.10.RELEASE:compile
[INFO] | +- javax.transaction:javax.transaction-api:jar:1.2:compile
[INFO] | +- org.hibernate:hibernate-core:jar:5.2.17.Final:compile
[INFO] | | +- org.jboss.logging:jboss-logging:jar:3.3.2.Final:compile
[INFO] | | +- org.hibernate.javax.persistence:hibernate-jpa-2.1-api:jar:1.0.2.Final:compile
[INFO] | | +- org.javassist:javassist:jar:3.22.0-GA:compile
[INFO] | | +- antlr:antlr:jar:2.7.7:compile
[INFO] | | +- org.jboss:jandex:jar:2.0.3.Final:compile
[INFO] | | +- com.fasterxml:classmate:jar:1.3.4:compile
[INFO] | | +- dom4j:dom4j:jar:1.6.1:compile
[INFO] | | \- org.hibernate.common:hibernate-commons-annotations:jar:5.0.1.Final:compile
.........omitted for brevity.................
Application
Properties
我们使用基于YAML
(一种标记语言)的属性文件将配置属性定义为比application.properties
更具可读性。
spring:application:name=customer-service
# 项目名称。spring:h2:console:enabled=true
# 启用嵌入式h2控制台。使用内存数据库spring:h2:console:path=/h2-console
#h2-console
的访问路径spring:jpa:show-sql=true
# 打印sql
server:port=8088
# 服务的端口.server:servlet:context-path=/restapi
#base URL
spring:
application:
name: customer-service
h2:
console:
enabled: true
path: /h2-console
jpa:
show-sql: true
server:
port: 8088
servlet:
context-path: /restapi
Domain
实体
在此示例中,我们定义JPA
实体以展示以下ER
图,其中Customer
实体与Account
实体具有一对多关系。Account.CustomerId
是引用Customer.CustomerId
的外键。
使用以下注解将这些类表示为JPA
实体
@Entity
表示该类是一个实体类。@Table
表示此实体映射到的数据库表。@Id
表示实体的主键@GeneratedValue
表示生成主键的策略,默认策略是AUTO
策略。@Column
表示实体属性的列映射。@ManyToOne
表示从帐户到客户的多对一个关系。此关系在本例中的实体Account
上指定。@JoinColumn
表示外键列@OnDelete
在此示例中表示级联删除操作。删除客户实体后,其所有帐户将同时被删除。@JsonIgnore
表示在序列化结束反序列化期间JSON解析器将忽略的属性。
package com.howtodoinjava.customerservice.domin;
import javax.persistence.*;
import java.io.Serializable;
import java.time.LocalDate;
@Table(name="CUSTOMER")
@Entity
public class Customer implements Serializable{
private static final long serialVersionUID = -6759774343110776659L;
@Id
@GeneratedValue
@Column(name="CUSTOMERID",updatable = false)
private Integer customerId;
@Column(name="NAME")
private String customerName;
@Column(name="DATEOFBIRTH" ,nullable=true)
private LocalDate dateofBirth;
@Column(name="PHONENUMBER")
private String phoneNumber;
public Integer getCustomerId() {
return customerId;
}
public void setCustomerId(Integer customerId) {
this.customerId = customerId;
}
public String getCustomerName() {
return customerName;
}
public void setCustomerName(String customerName) {
this.customerName = customerName;
}
public LocalDate getDateofBirth() {
return dateofBirth;
}
public void setDateofBirth(LocalDate dateofBirth) {
this.dateofBirth = dateofBirth;
}
public String getPhoneNumber() {
return phoneNumber;
}
public void setPhoneNumber(String phoneNumber) {
this.phoneNumber = phoneNumber;
}
}
package com.howtodoinjava.customerservice.domin;
import com.fasterxml.jackson.annotation.JsonIgnore;
import org.hibernate.annotations.OnDelete;
import org.hibernate.annotations.OnDeleteAction;
import javax.persistence.*;
import java.io.Serializable;
import java.math.BigDecimal;
import java.time.LocalDate;
@Table(name="ACCOUNT")
@Entity
public class Account implements Serializable {
@Id
@GeneratedValue
@Column(name="ACCOUNTNUMBER",updatable = false)
private Integer accountNumber;
@Column(name="ACCOUNTNAME")
private String accountName;
@Column(name="BALANCE")
private BigDecimal balance;
@Column(name="OPENINGDATE")
private LocalDate openingDate;
@ManyToOne(fetch = FetchType.LAZY, optional = false)
@JoinColumn(name = "CUSTOMERID", nullable = false)
@OnDelete(action = OnDeleteAction.CASCADE)
@JsonIgnore
private Customer customer;
private static final long serialVersionUID = -6380749575516426900L;
public Integer getAccountNumber() {
return accountNumber;
}
public void setAccountNumber(Integer accountNumber) {
this.accountNumber = accountNumber;
}
public String getAccountName() {
return accountName;
}
public void setAccountName(String accountName) {
this.accountName = accountName;
}
public BigDecimal getBalance() {
return balance;
}
public void setBalance(BigDecimal balance) {
this.balance = balance;
}
public LocalDate getOpeningDate() {
return openingDate;
}
public void setOpeningDate(LocalDate openingDate) {
this.openingDate = openingDate;
}
public Customer getCustomer() {
return customer;
}
public void setCustomer(Customer customer) {
this.customer = customer;
}
}
Repositories
Spring Data JPA
在关系数据库之上抽象了持久层,并大大减少了CRUD
操作和分页上的重复代码。通过扩展JPA
实体及其主键类型的JPARepository
接口,Spring Data
将检测该接口并在运行时自动创建实现。可从继承中轻松获得的CRUD
方法可以立即解决大多数数据访问用例。
package com.howtodoinjava.customerservice.repository;
import com.howtodoinjava.customerservice.domin.Customer;
import org.springframework.data.jpa.repository.JpaRepository;
public interface CustomerRepository extends JpaRepository<Customer, Integer> {
}
使用JPARepository
,我们还可以通过定义接口方法来创建自定义查询。Spring Data JPA
从方法名称派生查询,并在运行时实现查询逻辑。findByCustomerCustomerId
方法接受Pageable
类型的参数pageable
,并返Account
类的的Page
对象。
package com.howtodoinjava.customerservice.repository;
import com.howtodoinjava.customerservice.domin.Account;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.data.jpa.repository.JpaRepository;
public interface AccountRepository extends JpaRepository<Account, Integer> {
Page<Account> findByCustomerCustomerId(Integer customerId, Pageable pageable);
}
RESTful
控制器
在Spring MVC
(Model
-View
-Controller
)中使用@Controller
注解的控制器合并了业务逻辑和视图之间的数据流。在大多数情况下,控制器方法返回ModelAndView
对象以呈现视图。但有时控制器方法返回的值会以JSON/XML
格式显示给用户,而不是HTML
页面。要实现这一点,可以使用注释@ResponseBody
并自动将返回的值序列化为JSON/XML
,然后将其保存到HTTP
响应体中。annotation
@RestController
结合了前面的注释,为创建RESTful
控制器提供了更多的便利。
注解@GetMapping
,@PostMapping
,@PutMapping
和@DeleteMapping
比其前身@RequestMapping
更具HTTP
请求特定性,前者@RequestMapping
需要通过方法变量单独表示HTTP
请求方法。
这分别是与客户和帐户相关的操作的两个控制器类。
package com.howtodoinjava.customerservice.controller;
import com.howtodoinjava.customerservice.domin.Customer;
import com.howtodoinjava.customerservice.repository.CustomerRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/customers")
public class CustomerController {
@Autowired
private CustomerRepository customerRepository;
@PostMapping
@ResponseStatus(code = HttpStatus.CREATED)
public Customer save(@RequestBody Customer customer) {
return customerRepository.save(customer);
}
@GetMapping
public Page<Customer> all(Pageable pageable) {
return customerRepository.findAll(pageable);
}
@GetMapping(value = "/{customerId}")
public Customer findByCustomerId(@PathVariable Integer customerId) {
return customerRepository.findById(customerId).orElseThrow(() -> new RuntimeException("Customer [customerId=" + customerId + "] can't be found"));
}
@DeleteMapping(value = "/{customerId}")
public ResponseEntity<?> deleteCustomer(@PathVariable Integer customerId) {
return customerRepository.findById(customerId).map(customer -> {
customerRepository.delete(customer);
return ResponseEntity.ok().build();
}
).orElseThrow(() -> new RuntimeException("Customer [customerId=" + customerId + "] can't be found"));
}
@PutMapping(value = "/{customerId}")
public ResponseEntity<Customer> updateCustomer(@PathVariable Integer customerId, @RequestBody Customer newCustomer) {
return customerRepository.findById(customerId).map(customer -> {
customer.setCustomerName(newCustomer.getCustomerName());
customer.setDateofBirth(newCustomer.getDateofBirth());
customer.setPhoneNumber(newCustomer.getPhoneNumber());
customerRepository.save(customer);
return ResponseEntity.ok(customer);
}).orElseThrow(() -> new RuntimeException("Customer [customerId=" + customerId + "] can't be found"));
}
}
package com.howtodoinjava.customerservice.controller;
import com.howtodoinjava.customerservice.domin.Account;
import com.howtodoinjava.customerservice.domin.Customer;
import com.howtodoinjava.customerservice.repository.AccountRepository;
import com.howtodoinjava.customerservice.repository.CustomerRepository;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.Pageable;
import org.springframework.http.HttpStatus;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.*;
@RestController
@RequestMapping("/customers")
public class AccountController {
@Autowired
private CustomerRepository customerRepository;
@Autowired
private AccountRepository accountRepository;
@PostMapping(value = "/{customerId}/accounts")
@ResponseStatus(code = HttpStatus.CREATED)
public Account save(@PathVariable Integer customerId, @RequestBody Account account) {
return customerRepository.findById(customerId).map(customer -> {
account.setCustomer(customer);
return accountRepository.save(account);
}).orElseThrow(() -> new RuntimeException("Customer [customerId=" + customerId + "] can't be found"));
}
@GetMapping(value = "/{customerId}/accounts")
public Page<Account> all(@PathVariable Integer customerId, Pageable pageable) {
return accountRepository.findByCustomerCustomerId(customerId, pageable);
}
@DeleteMapping(value = "/{customerId}/accounts/{accountId}")
public ResponseEntity<?> deleteAccount(@PathVariable Integer customerId, @PathVariable Integer accountId) {
if (!customerRepository.existsById(customerId)) {
throw new RuntimeException("Customer [customerId=" + customerId + "] can't be found");
}
return accountRepository.findById(accountId).map(account -> {
accountRepository.delete(account);
return ResponseEntity.ok().build();
}).orElseThrow(() -> new RuntimeException("Account [accountId=" + accountId + "] can't be found"));
}
@PutMapping(value = "/{customerId}/accounts/{accountId}")
public ResponseEntity<Account> updateAccount(@PathVariable Integer customerId, @PathVariable Integer accountId, @RequestBody Account newAccount) {
Customer customer = customerRepository.findById(customerId).orElseThrow(() -> new RuntimeException("Customer [customerId=" + customerId + "] can't be found"));
return accountRepository.findById(accountId).map(account -> {
newAccount.setCustomer(customer);
accountRepository.save(newAccount);
return ResponseEntity.ok(newAccount);
}).orElseThrow(() -> new RuntimeException("Account [accountId=" + accountId + "] can't be found"));
}
}
在前面的控制器类中,我们如下定义了许多RESTful URI
/customers
HTTP Get
# 获得所有客户/customers
HTTP Post
# 创建新客户/customers/{customerId}
HTTP Get
# 获得一个客户/customers/{customerId}
HTTP Delete
# 删除客户/customers/{customerId}
HTTP Put
# 更新客户/customers/{customerId}/accounts
HTTP Post
# 为客户创建一个帐户/customers/{customerId}/accounts
HTTP Get
# 根据客户获取帐户/customers/{customerId}/accounts/{accountId}
HTTP Delete
# 根据客户删除账户/customers/{customerId}/accounts/{accountId}
HTTP Put
# 根据客户更新帐户
在关于REST
风格的API
设计指导原则,它超出了本文的范围。互联网上有一些不错的文章,大家可以自行查看。
测试
可以在Github
上找到RESTful
服务示例。如果你对Linux curl
命令不满意,我们可以通过简单地导入Postman
集合文件来使用Postman
调用RESTful
服务。
检查数据库中的数据,通过http://localhos:8088/restapi/h2-console/
访问H2
控制台,并提供以下详细信息。
Driver Class: org.h2.Driver
JDBC URL: jdbc:h2:mem:testdb
User Name: sa
Password: <blank>
总结
Spring Boot
并不与Spring
框架存在竞争。恰恰相反,它使Spring
更容易使用。在starter
项目中,Spring Boot
管理依赖项,使我们不必进行耗时且容易出错的依赖项管理,尤其是在应用程序复杂性增加的情况下。此外,Spring Boot
通过检查类路径为我们执行自动配置。例如,如果JPA
实现出现在类路径中,则Spring Boot
将配置DataSource
,TransactionManager
和EntityManagerFactory
等。
同时,覆盖Spring Boot
为我们所做的配置非常简单。
上述代码都可以在customerservice-RESTful上找到
🙂🙂🙂关注微信公众号java干货 不定期分享干货资料
原文链接:Build RESTful Services with Spring Boot 2.X in Few Steps