使用Spring Boot 2.X构建RESTful服务

2019/10/30 SpringBoot

明月松间照,清泉石上流。

https://raw.githubusercontent.com/longfeizheng/longfeizheng.github.io/master/images/springboot/springboot18.jpg

概述

Spring Boot是由Pivotal团队提供的全新框架,其设计目的是用来简化Spring应用的创建、运行、调试、部署等。它大大减少了基于Spring开发的生产级应用程序的工作量。因此,开发人员能够真正专注于以业务为中心的功能。

本章我们将通过几个步骤演示如何使用Spring Boot构建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

https://raw.githubusercontent.com/longfeizheng/longfeizheng.github.io/master/images/springboot/springboot19.png

Spring Initializr创建一个项目

https://raw.githubusercontent.com/longfeizheng/longfeizheng.github.io/master/images/springboot/springboot20.png

项目目录结构

如下所示的POM文件表示启动项目的依赖关系。在Spring Boot中,不同的启动程序项目代表不同的Spring模块,例如MVCORM等。开发人员主要要做的是在依赖项中添加启动程序项目,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的外键。

https://raw.githubusercontent.com/longfeizheng/longfeizheng.github.io/master/images/springboot/springboot21.jpeg

使用以下注解将这些类表示为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>

https://raw.githubusercontent.com/longfeizheng/longfeizheng.github.io/master/images/springboot/springboot22.png

https://raw.githubusercontent.com/longfeizheng/longfeizheng.github.io/master/images/springboot/springboot23.png

总结

Spring Boot并不与Spring框架存在竞争。恰恰相反,它使Spring更容易使用。在starter项目中,Spring Boot管理依赖项,使我们不必进行耗时且容易出错的依赖项管理,尤其是在应用程序复杂性增加的情况下。此外,Spring Boot通过检查类路径为我们执行自动配置。例如,如果JPA实现出现在类路径中,则Spring Boot将配置DataSourceTransactionManagerEntityManagerFactory等。 同时,覆盖Spring Boot为我们所做的配置非常简单。

上述代码都可以在customerservice-RESTful上找到


https://niocoder.com/assets/images/qrcode.jpg

🙂🙂🙂关注微信公众号java干货 不定期分享干货资料

原文链接:Build RESTful Services with Spring Boot 2.X in Few Steps

Show Disqus Comments

Search

    Post Directory