今天聊聊我在试行的一套流程
先看一个故事 Steve Yegge对Amazon和Google平台的长篇大论
http://blog.jobbole.com/5052/
有一天,Jeff Bezos下了一份命令。当然,他总是这么干,这些命令对人们来说就像用橡皮槌敲击蚂蚁一样。这个命令大概是2002年,我想误差应该是在正负1年内 —— 这个命令发布的范围非常地广,设想很大,让人眼珠子鼓出来的那种,这种惊讶程度和其他的命令相比,就好像突然收到奖金一样。
这份大命令大概有如下几个要点:(陈皓注:这里是本篇文章的要点!如果这真是Bezos发出来的,那么太赞了,Bezos完全就是一个系统架构大师啊,那可是2002年左右啊。作者调侃Bezos完全是正话反说啊)
1) 所有团队的程序模块都要以透过Service Interface 方式将其数据与功能开放出来。
2) 团队间的程序模块的信息通信,都要透过这些接口。
3) 除此之外没有其它的通信方式。其他形式一概不允许:不能使用直接链结程序、不能直接读取其他团队的数据库、不能使用共享内存模式、不能使用别人模块的后门、等等,等等,唯一允许的通信方式只能是能过call Service Interface。
4) 任何技术都可以使用。比如:HTTP、Corba、Pubsub、自定义的网络协议、等等,都可以,Bezos不管这些。
5) 所有的Service Interface,毫无例外,都必须从骨子里到表面都要设计成能对外界开放的。也就是说,团队必须做好规划与设计,以便把接口开放给全世界的程序员,没有例外。
6) 不这样的做的人会被炒鱿鱼。
一切以服务为基础,当决定采用服务化的架构时,就注定了好多复杂的事情会接踵而至。我们的业务场景没有那么高的并发,但是要求绝对的准确。 然后我们有兼容多种开发语言。所以我们的调用方式从Avro改成了http+json。
核心支付系统也进行了服务化的应用拆分。
我有很多问题啊
这些不同的系统之间通过契约(http接口)来完成交互。每个系统本身都可能是服务消费方与服务提供方。
1. 接口的定义如何更合理呢?
2. 定义出的接口如果可以消费方与提供方并行的开发,同时都会遵守契约呢?
3. 接口文档怎么样才能漂亮清楚些,老大满意? 接口定义文档是否可能不仅仅是查阅使用!?业务发展迅速,系统升级改造后如何去保证文档的即时性,最好自动生成?
4. 集成测试 是否一定需要部署所有的系统,如果不这样 靠打桩stub来模拟测试。如果保证你的桩同服务提供方的返回一致?
5. 当然了上述都是内部系统业务需求可控的情况,如果是对外提供的服务,我的服务会被多个客户端访问,每个客户端自身业务独特性,又会要求我改接口满足他,但是我改了其他人怎么办呢有没有影响呢?
6. 一个服务提供方定义接口的输入与输出要素,每个服务消费方所需要的往往不一致。
7. 服务存在的意义就是为消费者所使用! 微服务架构下不同的模块往往负责人不同,也可能技术语言不同,那怎么保证双方对服务的理解一致?如果去确保 凡是某一方需求发生了迭代变更。可以即时的通知到所有的参与方?
8. 系统的建设过程中,需要产品,研发,测试多方的协作。是否有什么策略保证所有人的理解一致?
9. 微服务架构下的测试环境搭建,是否需要把所有的相关系统全部都进行部署?
解决办法?
契约: 最好可以是服务的提供方与服务的消费方 两方所有的参与人员(产品,研发,测试)当然最重要的是包括机器都可以读懂使用的内容格式!!!!有么?下面引入Json schema!
JSON Schema(模式)是一种基于 JSON 格式定义 JSON 数据结构的规范。它被写在 IETF 草案下并于 2011 年到期。JSON 模式:
描述现有数据格式。
干净的人类和机器可读的文档。
完整的结构验证,有利于自动化测试。
完整的结构验证,可用于验证客户端提交的数据。
上面说过理房通使用http+json做为RPC的选择。研发测试人员与机器当然都可以看懂JSON,产品呢? 感谢下 知识图谱开放社区吧
http://www.kgopen.com/json-schema-editor
![]()
![]()
为我们提供了可视化表格型的json Schema编辑器。使用它 我们可以生成如下的jsonSchema
{
“$schema”: “http://json-schema.org/schema#“,
“id”: “http://www.kgopen.com/schema/unpublish“,
“title”: “”,
“description”: “”,
“type”: “object”,
“properties”: {
“name”: {
“id”: “http://www.kgopen.com/schema/unpublish/name“,
“type”: “string”,
“maxLength”: 1,
“pattern”: “[a-z]+”,
“default”: “a”,
“name”: “name”,
“title”: “名称”,
“description”: “名称”
},
“url”: {
“id”: “http://www.kgopen.com/schema/unpublish/url“,
“type”: “string”,
“minLength”: 2,
“format”: “uri”,
“default”: “http://baike.baidu.com/subview/2679/5443091.htm“,
“name”: “url”,
“title”: “网址”,
“description”: “可以识别唯一实体的url”
},
“alternateName”: {
“id”: “http://www.kgopen.com/schema/unpublish/alternateName“,
“type”: “array”,
“maxItems”: 10,
“uniqueItems”: true,
“items”: {
“type”: “string”,
“maxLength”: 1,
“default”: “A”
},
“default”: null,
“name”: “alternateName”,
“title”: “别名”,
“description”: “别名”
}
},
“required”: [
“url”,
“name”
]
}
生成好的JsonSchema我们就把它称之为我们的契约模板吧! 确定了以何种格式为契约下一步就开始讨论业务定义服务吧! 服务定义的方式?
https://martinfowler.com/articles/consumerDrivenContracts.html
服务契约三件套——提供者契约、消费者契约及消费者驱动的契约——中的一员,它从期望与约束的角度描述了服务提供者与服务消费者之间的关系:
提供者契约(Provider contracts)——提供者契约是我们最为熟悉的一种的服务契约,参考 WSDL+XML Schema+WS-Policy。顾名思义,提供者契约是以提供者为中心的。提供者规定了它要提供什么;然后,各消费者便将自己绑定到这个一成不变的契约上。不论消费者实际需要多少功能,消费者接受了提供者契约,就将自己与该提供者的全体功能耦合起来了。
消费者契约(Consumer contracts)——另一方面,消费者契约是对一个消费者的需求更为精确的描述。消费者契约描述了,在一次具体交互场合下,提供者功能中消费者需要的特定部分。消费者契约可被用来标注一个现有的提供者契约,另外消费者契约也有助于发现一个现今尚未规定的提供者契约。
消费者驱动的契约(Consumer-driven contracts)——消费者驱动的契约描述的是服务提供者向其所有当前消费者承诺遵守的约束。一旦各消费者把自己的具体期望告知提供者,消费者驱动的契约就被创建了。在提供者方面创建的约束,确定了一个消费者驱动的契约。若提供者接受了一个消费者驱动的契约,那么它只需保证已有约束仍能得到满足,即可自行改进与修改其服务。
我所理解的三种方式:
1. 提供者契约: 接口的定义完全取决于服务提供方,我定义好我的接口有100个输入200个返回。怎么使用是消费者的问题了。
2. 消费者契约: 这个我理解就是对提供者契约针对不同场景消费者的使用描述。你有100个输入200个返回,可我消费者A只用到10个输入20个返回。消费者B用到100个输入3个返回?
3. 消费者驱动的契约: Consumer Driven Contracts?
这个有意思了,我理解服务存在的意义就是要被消费者消费!所以从服务定义时候开始,提供方就应该广开言路。。。去问各个不同的服务方你们到底想要什么?之后提供方要做的就是 你们所有人的需求我都知道了,我会满足你们的!但是这之外无论我做什么变化只要我们的契约不变,那么消费方就完全可以无感知也无需知道,或者我提供方为了消费方A做的一次改版升级,只要提供方确保同B,C,D的契约仍然满足那么B,C,D的系统就无需变动!下面引入:
Spring cloud contract
What you always need is confidence in pushing new features into a new application or service in a distributed system. This project provides support for Consumer Driven Contracts and service schemas in Spring applications, covering a range of options for writing tests, publishing them as assets, asserting that a contract is kept by producers and consumers, for HTTP and message-based interactions.
提供了对Consumer Driven Contracts 的支持,可以编写测试用例来同时保证提供方与消费方均遵守契约。
它主要包含了3部分:
1. Spring Cloud Contract WireMock 使用WireMock来真正开启一个servers为服务消费方充当服务提供方的stub(挡板)。
2. Spring RestDocs 整合用于生成 stub文件,contract文件。
3. Spring Cloud Contract Verifier 。
Just to make long story short - Spring Cloud Contract Verifier is a tool that enables Consumer Driven Contract (CDC) development of JVM-based applications. It is shipped with Contract Definition Language (DSL). Contractdefinitions are used to produce following resources:
1)JSON stub definitions to be used by WireMock when doing integration testing on the client code (client tests). Test code must still be written by hand, test data is produced by Spring Cloud Contract Verifier.
2)Messaging routes if you’re using one. We’re integrating with Spring Integration, Spring Cloud Stream, Spring AMQP and Apache Camel. You can however set your own integrations if you want to
3)Acceptance tests (in JUnit or Spock) used to verify if server-side implementation of the API is compliant with the contract (server tests). Full test is generated by Spring Cloud Contract Verifier.
Spring Cloud Contract Verifier moves TDD to the level of software architecture.
Spring cloud Contract使用Groovy语言来编写contract DSL(契约)。最主要的两点 使用契约生成stub文件来供消费方进行测试开发使用。同时使用契约在服务提供方进行自动的单元测试确保服务方符合契约的约束!
回到上面的JsonSchema。我们有三个地方会继续使用它。
1. 做为单元测试的测试通过条件,对request及response进行matcher匹配。
package com.ehomepay;
import static org.springframework.test.util.AssertionErrors.assertTrue;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.ResultMatcher;
import com.jayway.restassured.module.jsv.JsonSchemaValidator;
/**
* @author xuwei
*/
public class JsonSchemaResultMatcher implements ResultMatcher {
private String reqschemaPath;
private String resschemaPath;
public JsonSchemaResultMatcher(String reqschemaPath, String resschemaPath) {
this.reqschemaPath = reqschemaPath;
this.resschemaPath = resschemaPath;
}
@Override
public void match(MvcResult result) throws Exception {
JsonSchemaValidator reqvalidator = JsonSchemaValidator.matchesJsonSchemaInClasspath(reqschemaPath);
JsonSchemaValidator resvalidator = JsonSchemaValidator.matchesJsonSchemaInClasspath(resschemaPath);
MockHttpServletRequest mrequest = result.getRequest() == null ? new MockHttpServletRequest()
: result.getRequest();
MockHttpServletResponse mresponse = result.getResponse() == null ? new MockHttpServletResponse()
: result.getResponse();
if (mrequest.getContentLength() > 0) {
StringBuilder inputline = new StringBuilder(mrequest.getContentLength());
String line = null;
while ((line = mrequest.getReader().readLine()) != null) {
inputline.append(line);
}
assertTrue("result‘s request content not match schema", reqvalidator.matches(inputline.toString()));
}
if (mresponse.getContentLength() > 0) {
assertTrue("result‘s response content not match schema",
resvalidator.matches(mresponse.getContentAsString()));
}
}
}
2, 可以结合jsonschema2pojo用其简化java代码的编写,自动生成所需要的JavaBean。
<dependency>
<groupId>org.jsonschema2pojo</groupId>
<artifactId>jsonschema2pojo-core</artifactId>
<version>0.4.33</version>
</dependency>
package com.ehomepay.account;
import java.io.File;
import java.io.IOException;
import java.net.URL;
import org.jsonschema2pojo.DefaultGenerationConfig;
import org.jsonschema2pojo.GenerationConfig;
import org.jsonschema2pojo.Jackson2Annotator;
import org.jsonschema2pojo.SchemaGenerator;
import org.jsonschema2pojo.SchemaMapper;
import org.jsonschema2pojo.SchemaStore;
import org.jsonschema2pojo.rules.RuleFactory;
import com.sun.codemodel.JCodeModel;
/**
* @author xuwei
*/
public class JsonSchematoPojo {
public static void main(String args[]) throws IOException {
JCodeModel codeModel = new JCodeModel();
URL source = Thread.currentThread().getContextClassLoader().getResource("mySchema.json");
GenerationConfig config = new DefaultGenerationConfig() {
@Override
public boolean isGenerateBuilders() { // set config option by
// overriding method
return true;
}
};
SchemaMapper mapper = new SchemaMapper(
new RuleFactory(config, new Jackson2Annotator(config), new SchemaStore()), new SchemaGenerator());
mapper.generate(codeModel, "ClassName", "com.ehomepay.account.domain", source);
codeModel.build(new File("C:\\work\\output"));
}
}
3, 可以使用其为模版,通过可视化界面编辑生成符合schema规范要求的json。
生成的json有两个用途。
3.1 配合MockMvc, ResultHandler,Spring RestDoc来完成Spring cloud contract的Contract DSL自动生成。
package com.ehomepay;
import javax.servlet.ServletOutputStream;
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.mock.web.MockHttpServletResponse;
import org.springframework.test.web.servlet.MvcResult;
import org.springframework.test.web.servlet.ResultHandler;
/**
* @author xuwei
*/
public class MyResultHandler implements ResultHandler {
@Override
public void handle(MvcResult result) throws Exception {
//自定义Contract的request部分
MockHttpServletRequest request=result.getRequest();
request.setRequestURI("/resource3");
result.getResponse().setOutputStreamAccessAllowed(true);
//自定义Contract的reponse部分
MockHttpServletResponse response = result.getResponse();
response.setCommitted(false);
response.resetBuffer();
ServletOutputStream outStream = response.getOutputStream();
outStream.println("{\"2id\":\"21\",\"2message\":\"2Hello World\"}");
response.setCommitted(true);
}
}
@Test
public void autoContractDSL() throws Exception {
mockMvc.perform( // 用于触发一个下述post请求
post("/resource"))
.andDo(new MyResultHandler())
.andDo(verify() // 调用WireMockRestDocs生成 wiremock的stub文件
.wiremock(WireMock.post(urlEqualTo("/resource3")).withRequestBody(matchingJsonPath("$.id")))
.stub("post-resource2"))
.andDo(document("index2", SpringCloudContractRestDocs.dslContract()));//生成 contractDsl文件。
}
3.2 配合showDoc利用json自动生成请求参数返回参数从而完成接口文档的自动生成。
当通过上述程序自动生成Contract DSL之后,可以在提供方程序中添加依赖如下:
<dependency>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-starter-contract-verifier</artifactId>
<scope>test</scope>
</dependency>
<plugin>
<groupId>org.springframework.cloud</groupId>
<artifactId>spring-cloud-contract-maven-plugin</artifactId>
<version>${spring-cloud-contract.version}</version>
<extensions>true</extensions>
<configuration> <packageWithBaseClasses>com.example.fraud</packageWithBaseClasses>
</configuration>
</plugin>
package com.example.fraud;
import org.junit.Before;
import com.jayway.restassured.module.mockmvc.RestAssuredMockMvc;
public class FraudBase {
@Before
public void setup() {
RestAssuredMockMvc.standaloneSetup(new FraudDetectionController(),
new FraudStatsController(stubbedStatsProvider()));
}
private StatsProvider stubbedStatsProvider() {
return fraudType -> {
switch (fraudType) {
case DRUNKS:
return 100;
case ALL:
return 200;
}
return 0;
};
}
public void assertThatRejectionReasonIsNull(Object rejectionReason) {
assert rejectionReason == null;
}
}
定义所有plugin自动生成测试类的基类。服务提供方完成所有的单元测试之后 本地执行 mvn clean deploy 进行部署包的上传maven私服 此处使用artifactory。 之后可以两种测试方式:
1. 本地测试wiremock运行stub。
2. 搭建一个公用服务来自动从maven私服获取多个stub运行,同时可以设置参数使其自动注册到eureka注册中心。以便消费方通过服务发现调用。
最终