测试必须学spring RESTful Service(上)
文末我会说说为什么测试必须学spring。
REST
REST,是指REpresentational State Transfer,有个精辟的解释什么是RESTful,
- 看url就知道要什么
- 看method就知道干什么
- 看status code就知道结果如何
实际上,RESTful API已经成为构建微服务的标准了,因为RESTful API容易build和consume。
为什么选择REST?REST信奉web的规则,包括架构、效益和其他一切。这并非偶然,因为spring的作者Roy Fielding参与了十几项web规则的定义,这些规则决定了web怎么运行。
效益是什么?web和它的核心协议,HTTP,提供了一系列特性,
- 适当的actions (
GET
,POST
,PUT
,DELETE
, …) - 缓存
- 重定向和转发
- 安全(加密和鉴权)
这些都是创建可伸缩性services的关键因素。但也不是全部。
web是由大量细小的规则构成的,所以不会存在什么“标准之争”,因此就能轻松的发展。
开发们(Javaer)可以使用第三方工具来实现这些不同的规则,在指尖就可以立即拥有client和server的技术。
所以建立在HTTP之上的REST APIs提供了创建灵活APIs的方法,
- 支持向后兼容
- 可扩展的API
- 可伸缩性的services
- 安全性的services
- 无状态到有状态的services
但是,REST并不是一个普遍的标准,而是一个方法,一个style,一系列架构上的约束,来帮你创建web-scale的系统,区别这一点很重要。
下载示例代码
到Spring Initializr这个网址选择,
- Web
- JPA
- H2
然后生成项目。下载.zip
文件。解压。就有了一个基于Maven的示例项目,包括一个pom.xml
文件。
Spirng Boot可以用任何IDE,包括Eclipse、IntelliJ IDEA、Netbeans等。
Eclipse可以使用一个工具STS(The Spring Tool Suite)。
先从非REST说起
我们先以最简单的示例开始。先抛弃REST的概念,后面再添加REST,在示例中感受到不同之处。
示例建模了一个简单的工资单service,管理公司employees。简言之,需要存储employee objects到一个H2内存数据库(Java编写的嵌入式数据库引擎),然后通过JPA(Java Persistence API,把实体对象持久化到数据库,是一种ORM规范,Hibernate是具体实现的框架)访问。它会被封装到Spring MVC layer进行远程访问。
nonrest/src/main/java/payroll/Employee.java
package payroll;
import java.util.Objects;
import javax.persistence.Entity;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
@Entity
class Employee {
private @Id @GeneratedValue Long id;
private String name;
private String role;
Employee() {}
Employee(String name, String role) {
this.name = name;
this.role = role;
}
public Long getId() {
return this.id;
}
public String getName() {
return this.name;
}
public String getRole() {
return this.role;
}
public void setId(Long id) {
this.id = id;
}
public void setName(String name) {
this.name = name;
}
public void setRole(String role) {
this.role = role;
}
@Override
public boolean equals(Object o) {
if (this == o)
return true;
if (!(o instanceof Employee))
return false;
Employee employee = (Employee) o;
return Objects.equals(this.id, employee.id) && Objects.equals(this.name, employee.name)
&& Objects.equals(this.role, employee.role);
}
@Override
public int hashCode() {
return Objects.hash(this.id, this.name, this.role);
}
@Override
public String toString() {
return "Employee{" + "id=" + this.id + ", name='" + this.name + '\'' + ", role='" + this.role + '\'' + '}';
}
}
代码不多,这个Java class包含了,
@Entity
是JPA注解,标记这个object是存储在基于JPA的数据库的。id
,name
, 和role
是domain object的属性,第一个被多个JPA注解标记的,是主键,通过JPA provider实现了自增。- 当创建新实例的时候,就会创建custom constructor,但是还没有id。
domain object定义好以后,就可以用Spring Data JPA来处理冗长的数据库交互。Spring Data repositories是一些接口,可以对后端数据库进行reading, updating, deleting, 和 creating记录。一些repositories也支持适当的data paging和sorting。Spring Data基于接口中的methods的命名约定来合成实现。
Spring Data JPA是Spring Data家族成员之一,只需要写repository接口,包括custom finder methods,Spring会自动提供实现。除了JPA之外,还有多种repository实现,如Spring Data MongoDB, Spring Data GemFire, Spring Data Cassandra等。
nonrest/src/main/java/payroll/EmployeeRepository.java
package payroll;
import org.springframework.data.jpa.repository.JpaRepository;
interface EmployeeRepository extends JpaRepository<Employee, Long> {
}
接口继承了Spring Data JPA的JpaRepository
,定义domain type为Employee
,id type为Long
。这个接口表面上是空的,但是支持,
- Creating new instances
- Updating existing ones
- Deleting
- Finding (one, all, by simple or complex properties)
Spring Data的repository solution可以避开数据存储细节,使用domain-specific术语来解决大部分问题。
不管你信不信,反正我信了!现在已经足够来启动一个应用了!Spring Boot应用至少有一个public static void main
entry-point,和@SpringBootApplication
注解。
nonrest/src/main/java/payroll/PayrollApplication.java
package payroll;
import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;
@SpringBootApplication
public class PayrollApplication {
public static void main(String... args) {
SpringApplication.run(PayrollApplication.class, args);
}
}
@SpringBootApplication
是元注解,引入了component scanning, autoconfiguration, 和property support。Spring Boot会启动一个servlet container,并为我们的service服务。
然而,没有数据的应用有点搞笑,先添点数据。下面这个类会由Spring自动加载,
nonrest/src/main/java/payroll/LoadDatabase.java
package payroll;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.boot.CommandLineRunner;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
@Configuration
class LoadDatabase {
private static final Logger log = LoggerFactory.getLogger(LoadDatabase.class);
@Bean
CommandLineRunner initDatabase(EmployeeRepository repository) {
return args -> {
log.info("Preloading " + repository.save(new Employee("Bilbo Baggins", "burglar")));
log.info("Preloading " + repository.save(new Employee("Frodo Baggins", "thief")));
};
}
}
Spring加载这个类的时候会发生什么?
- 一旦应用上下文加载后,Spring Boot就会运行所有的
CommandLineRunner
beans - runner会请求刚才创建的
EmployeeRepository
的copy - 然后创建2个对象,并存储
右键Run PayRollApplication
,
...
2018-08-09 11:36:26.169 INFO 74611 --- [main] payroll.LoadDatabase : Preloading Employee(id=1, name=Bilbo Baggins, role=burglar)
2018-08-09 11:36:26.174 INFO 74611 --- [main] payroll.LoadDatabase : Preloading Employee(id=2, name=Frodo Baggins, role=thief)
...
HTTP
为了用web layer封装repository,必须转Spring MVC。Spring Boot简化了这部分工作,基础代码只有一点点,从而把编码重心放到actions,
nonrest/src/main/java/payroll/EmployeeController.java
package payroll;
import java.util.List;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
@RestController
class EmployeeController {
private final EmployeeRepository repository;
EmployeeController(EmployeeRepository repository) {
this.repository = repository;
}
// Aggregate root
@GetMapping("/employees")
List<Employee> all() {
return repository.findAll();
}
@PostMapping("/employees")
Employee newEmployee(@RequestBody Employee newEmployee) {
return repository.save(newEmployee);
}
// Single item
@GetMapping("/employees/{id}")
Employee one(@PathVariable Long id) {
return repository.findById(id)
.orElseThrow(() -> new EmployeeNotFoundException(id));
}
@PutMapping("/employees/{id}")
Employee replaceEmployee(@RequestBody Employee newEmployee, @PathVariable Long id) {
return repository.findById(id)
.map(employee -> {
employee.setName(newEmployee.getName());
employee.setRole(newEmployee.getRole());
return repository.save(employee);
})
.orElseGet(() -> {
newEmployee.setId(id);
return repository.save(newEmployee);
});
}
@DeleteMapping("/employees/{id}")
void deleteEmployee(@PathVariable Long id) {
repository.deleteById(id);
}
}
@RestController
表明了每个方法返回的数据会直接写入到响应的body里面,而不是render一个template。- constructor注入了一个
EmployeeRepository
到controller(依赖注入)。 - 每个operations的路由 (
@GetMapping
,@PostMapping
,@PutMapping
和@DeleteMapping
, 对应HTTPGET
,POST
,PUT
, 和DELETE
)。 EmployeeNotFoundException
是当employee找不到时抛出的异常。
nonrest/src/main/java/payroll/EmployeeNotFoundException.java
package payroll;
class EmployeeNotFoundException extends RuntimeException {
EmployeeNotFoundException(Long id) {
super("Could not find employee " + id);
}
}
当抛出EmployeeNotFoundException
,Spring MVC configuration会render一个HTTP 404
,
nonrest/src/main/java/payroll/EmployeeNotFoundAdvice.java
package payroll;
import org.springframework.http.HttpStatus;
import org.springframework.web.bind.annotation.ControllerAdvice;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.bind.annotation.ResponseStatus;
@ControllerAdvice
class EmployeeNotFoundAdvice {
@ResponseBody
@ExceptionHandler(EmployeeNotFoundException.class)
@ResponseStatus(HttpStatus.NOT_FOUND)
String employeeNotFoundHandler(EmployeeNotFoundException ex) {
return ex.getMessage();
}
}
@ResponseBody
表示advice会直接render到response body。@ExceptionHandler
配置了只有抛EmployeeNotFoundException
异常的时候,advice才会响应。@ResponseStatus
表示发出HttpStatus.NOT_FOUND
, 比如 HTTP 404。- advice的body生成具体内容。示例中,返回了异常的message。
运行应用有多种方式,可以右键PayRollApplication
中的public static void main
,然后选择IDE的Run。
如果是Spring Initializr,可以输入命令行,
$ ./mvnw clean spring-boot:run
如果是本地安装了maven,可以输入命令行,
$ mvn clean spring-boot:run
一旦应用启动了,可以查看http通信,
$ curl -v localhost:8080/employees
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET /employees HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 200
< Content-Type: application/json;charset=UTF-8
< Transfer-Encoding: chunked
< Date: Thu, 09 Aug 2018 17:58:00 GMT
<
* Connection #0 to host localhost left intact
[{"id":1,"name":"Bilbo Baggins","role":"burglar"},{"id":2,"name":"Frodo Baggins","role":"thief"}]
能看到预加载的数据。
如果请求一个不存在的employee,
$ curl -v localhost:8080/employees/99
* Trying ::1...
* TCP_NODELAY set
* Connected to localhost (::1) port 8080 (#0)
> GET /employees/99 HTTP/1.1
> Host: localhost:8080
> User-Agent: curl/7.54.0
> Accept: */*
>
< HTTP/1.1 404
< Content-Type: text/plain;charset=UTF-8
< Content-Length: 26
< Date: Thu, 09 Aug 2018 18:00:56 GMT
<
* Connection #0 to host localhost left intact
Could not find employee 99
HTTP 404 error,并打印了message,Could not find employee 99。
使用-X
发个POST请求,创建新的Employee
,
$ curl -X POST localhost:8080/employees -H 'Content-type:application/json' -d '{"name": "Samwise Gamgee", "role": "gardener"}'
使用PUT
更新,
$ curl -X PUT localhost:8080/employees/3 -H 'Content-type:application/json' -d '{"name": "Samwise Gamgee", "role": "ring bearer"}'
使用Delete
删除,
$ curl -X DELETE localhost:8080/employees/3
$ curl localhost:8080/employees/3
Could not find employee 3
怎么变得RESTful
现在已经实现了基于web的service,但是是非REST的,
- 像
/employees/3
这种漂亮的URLs,不一定是REST - 只用了
GET
,POST
等,不一定是REST - 实现了所有CRUD操作,不一定是REST
那到底怎么样才算REST?
实际上现在创建的这个应该叫做RPC (Remote Procedure Call 远程过程调用)。因为并不知道以何种方式来和这个service交互。如果发布这个代码,还必须写个文档或者搞个开发门户网站,来把所有细节描述清楚。
看看Roy Fielding的这段话,是如何区别REST 和RPC的,
I am getting frustrated by the number of people calling any HTTP-based interface a REST API. Today’s example is the SocialSite REST API. That is RPC. It screams RPC. There is so much coupling on display that it should be given an X rating.
What needs to be done to make the REST architectural style clear on the notion that hypertext is a constraint? In other words, if the engine of application state (and hence the API) is not being driven by hypertext, then it cannot be RESTful and cannot be a REST API. Period. Is there some broken manual somewhere that needs to be fixed?
— Roy Fielding
https://roy.gbiv.com/untangled/2008/rest-apis-must-be-hypertext-driven
大概意思就是,应用状态引擎(API)不是超文本驱动的(我的理解是,像超文本一样携带一个地址,可以寻址定位信息,如超文本的link和id属性),就不是RESTful。
不包括hypermedia的坏处,就是clients必须硬编码URIs来导航API。这导致了电子商务兴起之前同样的脆弱特性。JSON output需要优化。
Spring HATEOAS,是一个spring项目,旨在帮你写hypermedia-driven outputs。
接下来RESTful开搞,先添加Spring HATEOAS到pom.xml,
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-hateoas</artifactId>
</dependency>
这个小library会提供定义RESTful service的constructs,然后以可接受的格式render,以便client消费。
对任何RESTful service来说,一个关键的要素,是给相关的操作添加links。
Getting a single item resource
@GetMapping("/employees/{id}")
EntityModel<Employee> one(@PathVariable Long id) {
Employee employee = repository.findById(id) //
.orElseThrow(() -> new EmployeeNotFoundException(id));
return EntityModel.of(employee, //
linkTo(methodOn(EmployeeController.class).one(id)).withSelfRel(),
linkTo(methodOn(EmployeeController.class).all()).withRel("employees"));
}
跟之前非REST有些类似,但也有不同,
- 方法的返回值从
Employee
变成了EntityModel<Employee>
。EntityModel<T>
是Spring HATEOAS的通用container,不仅包含data,也包含links集合。 linkTo(methodOn(EmployeeController.class).one(id)).withSelfRel()
让Spring HATEOAS创建link到EmployeeController
'sone()
方法,并标记为self link。linkTo(methodOn(EmployeeController.class).all()).withRel("employees")
让Spring HATEOAS创建link到aggregate root(聚合根),all()
,叫做"employees"。
创建link是什么意思?Spring HATEOAS的核心types之一就是Link
,包括一个 URI 和 一个 rel (relation)。正是Links改变了web。
在World Wide Web之前,其他的document systems会render information or links,但正是带有这种关系metadata的documents link把web连在了一起。
Roy Fielding鼓励使用相同的技术来创建APIs,links便是其中之一。
如果重启应用,查询employee Bilbo,会有一些不同,
RESTful representation of a single employee
{
"id": 1,
"name": "Bilbo Baggins",
"role": "burglar",
"_links": {
"self": {
"href": "http://localhost:8080/employees/1"
},
"employees": {
"href": "http://localhost:8080/employees"
}
}
}
不只有id
, name
and role
,还有 _links
,包括2个URLs。整个文档是采用HAL格式化的。
HAL是一个轻量的mediatype,不仅允许encoding data,也能hypermedia controls,提醒consumers到能导航到的API的其他部分。在本示例中,就是"self"(类似于代码里的this
) link和能返回到aggregate root的link。
为了让aggregate root也更RESTful,那么会希望包含top level links,和包含其他RESTful components,
Getting an aggregate root resource
@GetMapping("/employees")
CollectionModel<EntityModel<Employee>> all() {
List<EntityModel<Employee>> employees = repository.findAll().stream()
.map(employee -> EntityModel.of(employee,
linkTo(methodOn(EmployeeController.class).one(employee.getId())).withSelfRel(),
linkTo(methodOn(EmployeeController.class).all()).withRel("employees")))
.collect(Collectors.toList());
return CollectionModel.of(employees, linkTo(methodOn(EmployeeController.class).all()).withSelfRel());
}
我擦!之前只有一个方法repository.findAll()
!现在多了这么多代码!看不懂!不慌!排着队一个一个来!
CollectionModel<>
是Spring HATEOAS的另外一个container,用于封装集合,以及links。
封装集合?employees集合?
不完全是。
既然已经在说REST了,那么封装的是employee resources的集合。
这就是为什么获取了所有employees后,还需要转换为EntityModel<Employee>
的list。
重启之后,获取aggregate root,
{
"_embedded": {
"employeeList": [
{
"id": 1,
"name": "Bilbo Baggins",
"role": "burglar",
"_links": {
"self": {
"href": "http://localhost:8080/employees/1"
},
"employees": {
"href": "http://localhost:8080/employees"
}
}
},
{
"id": 2,
"name": "Frodo Baggins",
"role": "thief",
"_links": {
"self": {
"href": "http://localhost:8080/employees/2"
},
"employees": {
"href": "http://localhost:8080/employees"
}
}
}
]
},
"_links": {
"self": {
"href": "http://localhost:8080/employees"
}
}
}
这个aggregate root,提供了employee resources的集合,有一个top-level "self" link。 "collection"列在"_embedded"下面。这就是HAL怎么表示集合。
集合中每个独立的成员,都有information和关联的links。
添加links到底有什么意义?它使得随着时间的推移发展REST services成为可能。已存在的links能保留,新的links在未来被添加。新的clients可能用新的links,同时遗留clients仍然能用老的links。如果services需要重定位和移动,那这就会非常有用。只要link结构保留,clients就仍然能查找和交互。
后续内容请等待《测试必须学spring RESTful Service(下)》
参考资料
https://spring.io/guides/tutorials/rest/
测试为什么必须学spring
高级测试,需要懂架构,需要懂开发,需要能和开发在同一个Level交流。除了公司项目以外,业务时间是很少有合适的方式去学习一些开发技术的。尤其是对于我这种对代码不太敏感,对技术反应有些迟钝的。光靠自己零零散散的学习,是很难真正提升的。那么有一个比较好的方式,就是去看一些成熟的成体系的东西。对于Web来说,没有任何一个框架比得上Java Spring成熟。在spring里面可以了解到很多开发的技术点,这对了解整个技术栈是很有效的方式。虽然我平时写Python比较多(毕竟生产力很强大),但仍然喜欢学Java,这样才能接触到更完整的生态。让自己的测试眼界更宽广。
版权申明:本文为博主原创文章,转载请保留原文链接及作者。
测试必须学spring RESTful Service(上)的更多相关文章
- Wcf Restful Service服务搭建
目的 使用Wcf(C#)搭建一个Restful Service 背景 最近接到一个项目,客户要求使用Restful 方式接收到数据,并对数据提供对数据的统计显示功能,简单是简单,但必须要使用Restf ...
- 内容协商在视图View上的应用【享学Spring MVC】
每篇一句 人生很有意思:首先就得活得长.活得长才能够见自己,再长就可以见众生 前言 在经过 前两篇 文章了解了Spring MVC的内容协商机制之后,相信你已经能够熟练的运用Spring MVC提供的 ...
- (29)Spring boot 文件上传(多文件上传)【从零开始学Spring Boot】
文件上传主要分以下几个步骤: (1)新建maven java project: (2)在pom.xml加入相应依赖: (3)新建一个表单页面(这里使用thymleaf); (4)编写controlle ...
- spring如何创建RESTful Service
REST REST,是指REpresentational State Transfer,有个精辟的解释什么是RESTful, 看url就知道要什么 看method就知道干什么 看status code ...
- 使用spring boot+mybatis+mysql 构建RESTful Service
开发目标 开发两个RESTful Service Method Url Description GET /article/findAll POST /article/insert 主要使用到的技术 j ...
- 一起来学Spring Cloud | 第六章:服务网关 ( Zuul)
本章节,我们讲解springcloud重要组件:微服务网关Zuul.如果有同学从第一章看到本章的,会发现我们已经讲解了大部分微服务常用的基本组件. 已经讲解过的: 一起来学Spring Cloud | ...
- 一起来学Spring Cloud | 第三章:服务消费者 (负载均衡Ribbon)
一.负载均衡的简介: 负载均衡是高可用架构的一个关键组件,主要用来提高性能和可用性,通过负载均衡将流量分发到多个服务器,多服务器能够消除单个服务器的故障,减轻单个服务器的访问压力. 1.服务端负载均衡 ...
- 67. @Transactional的类注入失败【从零开始学Spring Boot】
[从零开始学习Spirng Boot-常见异常汇总] Spring的代理模式有两种:java自带的动态代理模式和cglib代理模式,cglib代码模式适用于没有接口的类,而java自带适用于接口类,默 ...
- 51. spring boot属性文件之多环境配置【从零开始学Spring Boot】
原本这个章节是要介绍<log4j多环境不同日志级别的控制的>但是没有这篇文章做基础的话,学习起来还是有点难度的,所以我们先一起了解下spring boot属性文件之多环境配置,当然文章中也 ...
随机推荐
- 部署java项目日志乱码求解!!!
springboot项项目打成war包放到tomcat9上,项目日志出现乱码,tomcat乱码已解决,这个不知道咋回事!!!!!! 这是项目的打包坐标 <parent> <group ...
- Improving RGB-D SLAM in dynamic environments: A motion removal approach
一.贡献 (1)提出一种针对RGB-D的新的运动分割算法 (2)运动分割采用矢量量化深度图像 (3)数据集测试,并建立RGB-D SLAM系统 二.Related work [1]R.K. Namde ...
- python1.1列表知识点:
#定义列表[]a=[1,2,3,4,5,6,7,"hello","world"]#列表索引从0开始,指定位置提取元素print(a[3])print(a) #列 ...
- 027_go语言中的通道选择器
代码演示 package main import "fmt" import "time" func main() { c1 := make(chan strin ...
- 简单认识JAVA内存划分
Java的内存划分为五个部分 那么又是哪五个部分呢?跟着我往下看! 介绍: 每个程序运行都需要内存空间,所以Java也不例外:而Java把从计算机中申请的这一块内存又进行了划分!而所在目的是为了让程序 ...
- Python爬虫教程:验证码的爬取和识别详解
今天要给大家介绍的是验证码的爬取和识别,不过只涉及到最简单的图形验证码,也是现在比较常见的一种类型. 很多人学习python,不知道从何学起.很多人学习python,掌握了基本语法过后,不知道在哪里寻 ...
- 连通图算法详解之① :Tarjan 和 Kosaraju 算法
相关阅读: 双连通分量 ,割点和桥 简介 在阅读下列内容之前,请务必了解 图论相关概念 中的基础部分. 强连通的定义是:有向图 G 强连通是指,G 中任意两个结点连通. 强连通分量(Strongly ...
- subprocess.CalledProcessError: Command '['ninja', '-v']' returned non-zero exit status 1.
将anaconda环境下的 lib/python3.5/dist-packages/torch/utils/cpp_extension.py文件 将['ninja','-v']改成['ninja', ...
- String类常用的方法
(1)int length():返回字符串的长度,例如: String s1="hello"; System.out.println(s1.length());//显示为5 (2) ...
- C#-用Winform制作一个简单的密码管理工具
为什么要做? 首先是为了练习一下c#. 想必大家都有过记不起某个平台的账号密码的经历,那种感受着实令人抓狂.那这么多账号密码根本记不住!我之前用python写过一个超级简单(连账号信息都写在代码里那种 ...