文末我会说说为什么测试必须学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, 对应HTTP GET, 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的这段话,是如何区别RESTRPC的,

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 's one() 方法,并标记为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(上)的更多相关文章

  1. Wcf Restful Service服务搭建

    目的 使用Wcf(C#)搭建一个Restful Service 背景 最近接到一个项目,客户要求使用Restful 方式接收到数据,并对数据提供对数据的统计显示功能,简单是简单,但必须要使用Restf ...

  2. 内容协商在视图View上的应用【享学Spring MVC】

    每篇一句 人生很有意思:首先就得活得长.活得长才能够见自己,再长就可以见众生 前言 在经过 前两篇 文章了解了Spring MVC的内容协商机制之后,相信你已经能够熟练的运用Spring MVC提供的 ...

  3. (29)Spring boot 文件上传(多文件上传)【从零开始学Spring Boot】

    文件上传主要分以下几个步骤: (1)新建maven java project: (2)在pom.xml加入相应依赖: (3)新建一个表单页面(这里使用thymleaf); (4)编写controlle ...

  4. spring如何创建RESTful Service

    REST REST,是指REpresentational State Transfer,有个精辟的解释什么是RESTful, 看url就知道要什么 看method就知道干什么 看status code ...

  5. 使用spring boot+mybatis+mysql 构建RESTful Service

    开发目标 开发两个RESTful Service Method Url Description GET /article/findAll POST /article/insert 主要使用到的技术 j ...

  6. 一起来学Spring Cloud | 第六章:服务网关 ( Zuul)

    本章节,我们讲解springcloud重要组件:微服务网关Zuul.如果有同学从第一章看到本章的,会发现我们已经讲解了大部分微服务常用的基本组件. 已经讲解过的: 一起来学Spring Cloud | ...

  7. 一起来学Spring Cloud | 第三章:服务消费者 (负载均衡Ribbon)

    一.负载均衡的简介: 负载均衡是高可用架构的一个关键组件,主要用来提高性能和可用性,通过负载均衡将流量分发到多个服务器,多服务器能够消除单个服务器的故障,减轻单个服务器的访问压力. 1.服务端负载均衡 ...

  8. 67. @Transactional的类注入失败【从零开始学Spring Boot】

    [从零开始学习Spirng Boot-常见异常汇总] Spring的代理模式有两种:java自带的动态代理模式和cglib代理模式,cglib代码模式适用于没有接口的类,而java自带适用于接口类,默 ...

  9. 51. spring boot属性文件之多环境配置【从零开始学Spring Boot】

    原本这个章节是要介绍<log4j多环境不同日志级别的控制的>但是没有这篇文章做基础的话,学习起来还是有点难度的,所以我们先一起了解下spring boot属性文件之多环境配置,当然文章中也 ...

随机推荐

  1. 部署java项目日志乱码求解!!!

    springboot项项目打成war包放到tomcat9上,项目日志出现乱码,tomcat乱码已解决,这个不知道咋回事!!!!!! 这是项目的打包坐标 <parent> <group ...

  2. 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 ...

  3. python1.1列表知识点:

    #定义列表[]a=[1,2,3,4,5,6,7,"hello","world"]#列表索引从0开始,指定位置提取元素print(a[3])print(a) #列 ...

  4. 027_go语言中的通道选择器

    代码演示 package main import "fmt" import "time" func main() { c1 := make(chan strin ...

  5. 简单认识JAVA内存划分

    Java的内存划分为五个部分 那么又是哪五个部分呢?跟着我往下看! 介绍: 每个程序运行都需要内存空间,所以Java也不例外:而Java把从计算机中申请的这一块内存又进行了划分!而所在目的是为了让程序 ...

  6. Python爬虫教程:验证码的爬取和识别详解

    今天要给大家介绍的是验证码的爬取和识别,不过只涉及到最简单的图形验证码,也是现在比较常见的一种类型. 很多人学习python,不知道从何学起.很多人学习python,掌握了基本语法过后,不知道在哪里寻 ...

  7. 连通图算法详解之① :Tarjan 和 Kosaraju 算法

    相关阅读: 双连通分量 ,割点和桥 简介 在阅读下列内容之前,请务必了解 图论相关概念 中的基础部分. 强连通的定义是:有向图 G 强连通是指,G 中任意两个结点连通. 强连通分量(Strongly ...

  8. 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', ...

  9. String类常用的方法

    (1)int length():返回字符串的长度,例如: String s1="hello"; System.out.println(s1.length());//显示为5 (2) ...

  10. C#-用Winform制作一个简单的密码管理工具

    为什么要做? 首先是为了练习一下c#. 想必大家都有过记不起某个平台的账号密码的经历,那种感受着实令人抓狂.那这么多账号密码根本记不住!我之前用python写过一个超级简单(连账号信息都写在代码里那种 ...