You are viewing a plain text version of this content. The canonical link for it is here.
Posted to commits@servicecomb.apache.org by li...@apache.org on 2020/02/12 03:40:20 UTC

[servicecomb-docs] branch master updated: [SCB-1764]add porter appliation to docs and fix known problems

This is an automated email from the ASF dual-hosted git repository.

liubao pushed a commit to branch master
in repository https://gitbox.apache.org/repos/asf/servicecomb-docs.git


The following commit(s) were added to refs/heads/master by this push:
     new 5dd9c27  [SCB-1764]add porter appliation to docs and fix known problems
5dd9c27 is described below

commit 5dd9c27e12c328c3e500b54fb0504139f87afae5
Author: liubao <bi...@qq.com>
AuthorDate: Wed Feb 12 11:29:11 2020 +0800

    [SCB-1764]add porter appliation to docs and fix known problems
---
 .gitignore                                         |   8 +-
 .../docs/general-development/local-develop-test.md |   4 +-
 .../en_US/docs/general-development/metrics.md      |   2 +-
 .../docs/featured-topics/application-porter.md     |  35 ++++
 .../application-porter/authentication.md           | 226 +++++++++++++++++++++
 .../featured-topics/application-porter/design.md   |  24 +++
 .../application-porter/file-service.md             |  80 ++++++++
 .../application-porter/gateway-service.md          | 147 ++++++++++++++
 .../featured-topics/application-porter/https.md    |  36 ++++
 .../application-porter/microservices.png           | Bin 0 -> 30451 bytes
 .../application-porter/porter-website.md           | 109 ++++++++++
 .../application-porter/user-service.md             | 146 +++++++++++++
 .../application-porter/user-story.md               |   6 +
 .../application-porter/usercase.png                | Bin 0 -> 16992 bytes
 .../docs/general-development/local-develop-test.md |   4 +-
 .../zh_CN/docs/general-development/metrics.md      |   4 +-
 java-chassis-reference/zh_CN/mkdocs.yml            |   1 +
 17 files changed, 825 insertions(+), 7 deletions(-)

diff --git a/.gitignore b/.gitignore
index 1a05e5d..1d48d9c 100644
--- a/.gitignore
+++ b/.gitignore
@@ -1,2 +1,8 @@
 /.idea
-/target
\ No newline at end of file
+/target
+
+# IntelliJ, based on http://devnet.jetbrains.net/docs/DOC-1186
+.idea/
+*.iml
+*.ipr
+*.iws
\ No newline at end of file
diff --git a/java-chassis-reference/en_US/docs/general-development/local-develop-test.md b/java-chassis-reference/en_US/docs/general-development/local-develop-test.md
index c0346b9..dcf5c6a 100644
--- a/java-chassis-reference/en_US/docs/general-development/local-develop-test.md
+++ b/java-chassis-reference/en_US/docs/general-development/local-develop-test.md
@@ -3,9 +3,9 @@
 
 This section describes how developers can locally develop and commission consumer and provider applications. Both service providers and consumers need to connect to the remote service center. Two methods of building Local  ServiceCenter for local microservice commissioning are as follows:
 
-* Starting [Local Service Center](#section2945986191314)。
+* Starting [Local Service Center](#starting-local-servicecenter).
 
-* Starting Local Service Center [Mock mechanism](#section960893593759)。
+* Starting Local Service Center [Mock mechanism](#mock-mechanism-start-service-center).
 
 ### Local debugging by setting up environmental information
 
diff --git a/java-chassis-reference/en_US/docs/general-development/metrics.md b/java-chassis-reference/en_US/docs/general-development/metrics.md
index d54a409..b846991 100644
--- a/java-chassis-reference/en_US/docs/general-development/metrics.md
+++ b/java-chassis-reference/en_US/docs/general-development/metrics.md
@@ -49,7 +49,7 @@ _Note: Please change the version field to the actual version number; if the vers
 ### 3. Slow call detection
   After slow call detection is enabled, if there is a slow call, the corresponding log will be output immediately:
 ```
-2019-04-02 23:01:09,103[WARN][pool-7-thread-74][5ca37935c00ff2c7-350076] - slow(40 ms) invocation, CONSUMER highway perf1.impl.syncQuery
+2019-04-02 23:01:09,103\[WARN]\[pool-7-thread-74]\[5ca37935c00ff2c7-350076] - slow(40 ms) invocation, CONSUMER highway perf1.impl.syncQuery
   http method: GET
   url        : /v1/syncQuery/{id}/
   server     : highway://192.168.0.152:7070?login=true
diff --git a/java-chassis-reference/zh_CN/docs/featured-topics/application-porter.md b/java-chassis-reference/zh_CN/docs/featured-topics/application-porter.md
new file mode 100644
index 0000000..a6ee34b
--- /dev/null
+++ b/java-chassis-reference/zh_CN/docs/featured-topics/application-porter.md
@@ -0,0 +1,35 @@
+# 示例项目:porter
+
+示例项目包含如下章节:
+
+  * [User Story](application-porter/user-story.md)
+  * [设计微服务](application-porter/design.md)
+  * [开发界面\(porter-website\)](application-porter/porter-website.md)
+  * [开发文件上传功能\(file-service\)](application-porter/file-service.md)
+  * [开发网关\(gateway-service\)](application-porter/gateway-service.md)
+  * [使用MyBatis访问数据库\(user-service\)](application-porter/user-service.md)
+  * [进行认证和鉴权设计](application-porter/authentication.md)
+  * [网关HTTPS安全配置](application-porter/https.md)
+
+示例项目的出发点是帮助开发者开发一个完整的微服务应用。通过一个典型的应用场景,展现一个微服务应用需要解决那些问题,在不同的章节里面,会详细解释解决解决这些问题的技术原理和实现过程。
+
+这个应用场景,是通过收集了一些用户的真实业务场景提取出来的。具体包括:
+
+1. 一个推荐的微服务设计方案;
+
+2. 认证鉴权;
+
+3. 使用mybatis访问数据库;
+
+4. 使用html+js提供界面服务;
+
+5. 上传文件;
+
+6. 使用网关和配置HTTPS;
+
+在这个应用中,尽可能让服务小、每个微服务完全独立,没有代码上的依赖,服务之间通过REST接口相互访问。为了达到这个目的,可能会有些重复代码(包括配置类文件如pom.xml、数据模型类文件等)。开发者可以结合实际情况选择是否提供公共模块,来避免这种情况。在这个项目中选择的是用重复代码来换取自由度的方案。
+
+在实际的代码中,我们还会遵循其他一些和微服务开发有关的原则,包括无状态设计等。这里的例子的目的是搭建一个商业可用的微服务,因此我们会在架构设计、方案设计上也给出一定的建议以及说明这样处理的目的。
+
+本专题的涉及的代码均托管在github,参考 [Porter应用](https://github.com/apache/servicecomb-samples/tree/master/porter_lightweight) 。开发者可以clone一份供学习使用,或者作为正式项目的模板。
+
diff --git a/java-chassis-reference/zh_CN/docs/featured-topics/application-porter/authentication.md b/java-chassis-reference/zh_CN/docs/featured-topics/application-porter/authentication.md
new file mode 100644
index 0000000..d459eb6
--- /dev/null
+++ b/java-chassis-reference/zh_CN/docs/featured-topics/application-porter/authentication.md
@@ -0,0 +1,226 @@
+传统的WEB容器都提供了会话管理,在微服务架构下,这些会话管理存在很多的限制,如果需要做到弹性扩缩容,则需要做大量的定制。 在porter中,我们使用user-service做会话管理,可以通过login和session两个接口创建和获取会话信息。会话信息持久化到数据库中,从而实现微服务本身的无状态,微服务可以弹性扩缩容。在更大规模并发或者高性能要求的情况下,可以考虑将会话信息存储到高速缓存。
+
+```
+@PostMapping(path = "/login", produces = MediaType.APPLICATION_JSON_VALUE)
+
+public SessionInfo login(@RequestParam(name = "userName") String userName,
+
+@RequestParam(name = "password") String password)
+
+
+
+@GetMapping(path = "/session", produces = MediaType.APPLICATION_JSON_VALUE)
+
+public SessionInfo getSession(@RequestParam(name = "sessionId") String sessionId)
+```
+
+同时新增了会话管理的数据表设计:
+
+    CREATE TABLE `T_SESSION` (
+      `ID`  INTEGER(8) NOT NULL AUTO_INCREMENT COMMENT '唯一标识',
+      `SESSION_ID`  VARCHAR(64) NOT NULL COMMENT '临时会话ID',
+      `USER_NAME`  VARCHAR(64) NOT NULL COMMENT '用户名称',
+      `ROLE_NAME`  VARCHAR(64) NOT NULL COMMENT '角色名称',
+      `CREATION_TIME`  TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP COMMENT '创建时间',
+      `ACTIVE_TIME`  TIMESTAMP NOT NULL DEFAULT CURRENT_TIMESTAMP ON UPDATE CURRENT_TIMESTAMP COMMENT '最近活跃时间',
+      PRIMARY KEY (`ID`)
+    );
+
+会话管理和认证都在gateway-service进行,鉴权则需要使用到用户信息。为了让微服务获取用户信息的时候,不至于再查询user-service,我们利用了Context机制,在Context里面存储了session信息,所有的微服务都可以直接从Context里面取到session信息,非常方便和灵活。完成这个功能有如下几个关键步骤:
+
+* gateway-service进行HTTP协议到Invocation的转换
+
+这个通过重载EdgeInvocation的createInvocation实现。将会话ID通过Context传递给handler。如果开发者需要实现诸如增加响应头,设计Cookie等操作,则可以通过重载sendResponse来实现。
+
+```
+EdgeInvocation invoker = new EdgeInvocation() {
+  // 认证鉴权:构造Invocation的时候,设置会话信息。如果是认证请求,则添加Cookie。
+  protected void createInvocation(Object[] args) {
+    super.createInvocation(args);
+    // 既从cookie里面读取会话ID,也从header里面读取,方便各种独立的测试工具联调
+    String sessionId = context.request().getHeader("session-id");
+    if (sessionId != null) {
+      this.invocation.addContext("session-id", sessionId);
+    } else {
+      Cookie sessionCookie = context.getCookie("session-id");
+      if (sessionCookie != null) {
+        this.invocation.addContext("session-id", sessionCookie.getValue());
+      }
+    }
+  }
+};
+```
+
+* 通过handler来进行认证和会话管理
+
+对于ui界面,不提供认证,用户可以直接访问。对于REST接口需要进行认证,因此我们将认证和会话管理的功能在Hanlder中实现。下面的代码对user-service的login接口直接转发请求,其他请求先经过会话校验,再进行转发。
+
+***注意***: 在网关执行的Hanlder逻辑,是reactive模式的,不能使用阻塞调用,否则会导致线程阻塞。
+
+```
+public class AuthHandler implements Handler {
+  private UserServiceClient userServiceClient = BeanUtils.getBean("UserServiceClient");
+
+  // session expires in 10 minutes, cache for 1 seconds to get rid of concurrent scenarios.
+  private Cache<String, String> sessionCache = CacheBuilder.newBuilder()
+      .expireAfterAccess(30, TimeUnit.SECONDS)
+      .build();
+
+  @Override
+  public void handle(Invocation invocation, AsyncResponse asyncResponse) throws Exception {
+    if (invocation.getMicroserviceName().equals("user-service")
+        && (invocation.getOperationName().equals("login")
+            || (invocation.getOperationName().equals("getSession")))) {
+      // login:return session id, set cookie by javascript
+      invocation.next(asyncResponse);
+    } else {
+      // check session
+      String sessionId = invocation.getContext("session-id");
+      if (sessionId == null) {
+        throw new InvocationException(403, "", "session is not valid.");
+      }
+
+      String sessionInfo = sessionCache.getIfPresent(sessionId);
+      if (sessionInfo != null) {
+        try {
+          // session info stored in InvocationContext. Microservices can get it. 
+          invocation.addContext("session-id", sessionId);
+          invocation.addContext("session-info", sessionInfo);
+          invocation.next(asyncResponse);
+        } catch (Exception e) {
+          asyncResponse.complete(Response.failResp(new InvocationException(500, "", e.getMessage())));
+        }
+        return;
+      }
+
+      // In edge, handler is executed in reactively. Must have no blocking logic.
+      CompletableFuture<SessionInfo> result = userServiceClient.getGetSessionOperation().getSession(sessionId);
+      result.whenComplete((info, e) -> {
+        if (result.isCompletedExceptionally()) {
+          asyncResponse.complete(Response.failResp(new InvocationException(403, "", "session is not valid.")));
+        } else {
+          if (info == null) {
+            asyncResponse.complete(Response.failResp(new InvocationException(403, "", "session is not valid.")));
+            return;
+          }
+          try {
+            // session info stored in InvocationContext. Microservices can get it. 
+            invocation.addContext("session-id", sessionId);
+            String sessionInfoStr = JsonUtils.writeValueAsString(info);
+            invocation.addContext("session-info", sessionInfoStr);
+            invocation.next(asyncResponse);
+            sessionCache.put(sessionId, sessionInfoStr);
+          } catch (Exception ee) {
+            asyncResponse.complete(Response.failResp(new InvocationException(500, "", ee.getMessage())));
+          }
+        }
+      });
+    }
+  }
+}
+```
+
+启用该Hanlder,需要增加cse.handler.xml文件
+
+```
+<config>
+  <handler id="auth"
+    class="org.apache.servicecomb.samples.porter.gateway.AuthHandler" />
+</config>
+```
+
+并且在microservice.yaml中启用auth,将新增加的auth处理链放到流控之后。
+
+```
+servicecomb:
+  handler:
+    chain:
+      Consumer:
+        default: internalAccess,auth,qps-flowcontrol-consumer,loadbalance
+```
+
+* 给删除文件增加鉴权
+
+在上面的步骤中,已经将会话信息设置到Context里面,file-service可以方便的使用这些信息进行鉴权操作。
+
+```
+@DeleteMapping(path = "/delete", produces = MediaType.APPLICATION_JSON_VALUE)
+public boolean deleteFile(@RequestParam(name = "id") String id) {
+    String session = ContextUtils.getInvocationContext().getContext("session-info");
+    if (session == null) {
+        throw new InvocationException(403, "", "not allowed");
+    } else {
+        SessionInfo sessionInfo = null;
+        try {
+            sessionInfo = JsonUtils.readValue(session.getBytes("UTF-8"), SessionInfo.class);
+        } catch (Exception e) {
+            throw new InvocationException(403, "", "session not allowed");
+        }
+        if (sessionInfo == null || !sessionInfo.getRoleName().equals("admin")) {
+            throw new InvocationException(403, "", "not allowed");
+        }
+    }
+    return fileService.deleteFile(id);
+}
+```
+
+到这里为止,认证、会话管理和鉴权的逻辑基本已经完成了。可以通过Postman等工具进行流程相关的测试。
+
+```
+#### 会话管理接口调用示例,调用删除文件接口。使用guest用户的会话的情况。
+
+#Request
+DELETE http://localhost:9090/api/file-service/delete?id=ba6bd8a2-d31a-42cd-a1be-9fb3d6ab4c82
+
+session-id: 1be646c0-50cb-4c0a-968d-2a512775f5e8
+
+#Response
+{
+    "message": "not allowed"
+}
+```
+
+
+
+# 开发JS脚本管理会话
+
+首先需要提供登陆框,让用户输入用户名密码:
+
+```
+<div class="form">
+    <h2>登录</h2>
+    <input id="username" type="text" name="Username" placeholder="Username">
+    <input id="paasword" type="password" name="Password" placeholder="Password" >
+    <input type="button" value="Login" onclick="loginAction()">
+</div>
+```
+
+实现登陆逻辑。登陆首先调用后台登陆接口,登陆成功后设置会话cookie:
+
+```
+function loginAction() {
+     var username = document.getElementById("username").value;
+     var password = document.getElementById("paasword").value;
+     var formData = {};
+     formData.userName = username;
+     formData.password = password;
+
+     $.ajax({
+        type: 'POST',
+        url: "/api/user-service/login",
+        data: formData,
+        success: function (data) {
+            setCookie("session-id", data.sessiondId, false);
+            window.alert('登陆成功!');
+        },
+        error: function(data) {
+            console.log(data);
+            window.alert('登陆失败!' + data);
+        },
+        async: true
+    });
+}
+```
+
+
+
diff --git a/java-chassis-reference/zh_CN/docs/featured-topics/application-porter/design.md b/java-chassis-reference/zh_CN/docs/featured-topics/application-porter/design.md
new file mode 100644
index 0000000..0ed37b0
--- /dev/null
+++ b/java-chassis-reference/zh_CN/docs/featured-topics/application-porter/design.md
@@ -0,0 +1,24 @@
+根据User Story,应用至少包含用户管理、文件管理等微服务,每个微服务都行使不一样的功能。下面是分解后设计的微服务结构:
+
+![](microservices.png)
+
+
+
+**网关:**负责进行请求转发、用户认证以及其他内容,比如解决跨站访问、设置HTTP安全消息头等。通过设置防火墙,所有的请求都必须经过网关,这样就将内部服务与外部用户隔离起来,防止内部服务被非法访问。
+
+**文件管理:**提供文件上传、删除等文件管理功能。
+
+**用户管理:**提供认证、角色和权限管理等功能。
+
+**界面: ** 采用静态页面技术, html+js+css实现。界面可以作为一个单独的微服务,也可以直接放到网关服务里面。
+
+为了可靠性,这些服务都应该支持分布式集群部署。因此在业务逻辑中涉及到并发和负载均衡的场景,都需要考虑无状态设计。可以给网关配置域名或者在上层再挂一个弹性负载均衡器,实现网关的多实例部署。
+
+微服务设计好以后,可以通过已有项目快速搭建项目架子。可以从:
+
+ [Porter应用](https://github.com/apache/servicecomb-samples/tree/master/porter_lightweight) 
+
+下载该项目。
+
+初始的项目是一个maven项目,主要内容包括pom.xml文件、microservice.yam文件和一个Main函数。microservice.yam文件配置了微服务的基本信息和访问服务中心的地址。
+
diff --git a/java-chassis-reference/zh_CN/docs/featured-topics/application-porter/file-service.md b/java-chassis-reference/zh_CN/docs/featured-topics/application-porter/file-service.md
new file mode 100644
index 0000000..9973374
--- /dev/null
+++ b/java-chassis-reference/zh_CN/docs/featured-topics/application-porter/file-service.md
@@ -0,0 +1,80 @@
+文件上传功能、用户管理功能都只需要提供REST接口,在技术选型上,使用轻量级REST框架。开发新的微服务都涉及到配置微服务信息,写一个新的Main函数,这些公共步骤在文档前面已经描述,后续文档会省略这些内容。
+
+* 增加依赖关系
+
+```
+    <dependency>
+      <groupId>org.apache.servicecomb</groupId>
+      <artifactId>solution-basic</artifactId>
+    </dependency>
+```
+
+* 定义服务接口
+
+服务接口定义上有3种选择:RPC、Spring MVC、JAX RS。 这里选择了Sping MVC,相比RPC需要额外增加Annotation,灵活性在于接口即可以通过RPC的方式在服务内部访问,也可以通过浏览器访问,期望前台js脚本开发者也能够对照生成的契约完成开发。
+
+```
+@RestSchema(schemaId = "file")
+@RequestMapping(path = "/")
+public class FileServiceEndpoint {
+    @Autowired
+    private FileService fileService;
+
+    /**
+     * 上传文件接口,用户上传一个文件,返回文件ID。
+     */
+    @PostMapping(path = "/upload", produces = MediaType.TEXT_PLAIN_VALUE)
+    public String uploadFile(@RequestPart(name = "fileName") MultipartFile file) {
+        return fileService.uploadFile(file);
+    }
+
+    /**
+     * 删除文件接口。指定ID,返回删除成功还是失败.
+     */
+    @DeleteMapping(path = "/delete", produces = MediaType.APPLICATION_JSON_VALUE)
+    public boolean deleteFile(@RequestParam(name = "id") String id) {
+        return fileService.deleteFile(id);
+    }
+}
+```
+
+为了实现不同方式的文件存储,将实现抽象出来FileService。为了简单,当前只提供了本地文件实现。这个实现限制了该服务无法进行多实例部署。可以考虑使用对象存储服务器、分布式文件系统等满足存储要求。
+
+* 设置临时目录
+
+需要在microservice.yaml中增加servicecomb.uploads.directory配置项,指定临时目录的路径。需要保证目录有写权限。默认情况下如果没设置临时目录,不允许启用上传功能。如果使用网关,网关也需要增加这个配置项。
+
+* 开发测试HTML,访问上传服务
+
+为了测试开发的接口,可以写一个HTML程序。注意name参数需要和接口定义的名称一样。
+
+```
+<!DOCTYPE html>
+<html>
+<head>
+<title>Upload Example</title>
+</head>
+<body>
+    <h2>Upload Example</h2>
+    <hr/>
+    <form method="POST" enctype="multipart/form-data"
+        action="http://localhost:9091/upload">
+        <p>
+            File Name: <input type="file" name="fileName" />
+        </p>
+        <p>
+            <input type="submit" value="Upload" />
+        </p>
+    </form>
+</body>
+</html>
+```
+
+可以使用Postman等工具测试删除接口:
+
+```
+DELETE http://localhost:9091/delete?id=ba6bd8a2-d31a-42cd-a1be-9fb3d6ab4c82
+```
+
+还可以开发一个客户端测试这些接口,对于自动化测试用例是非常有用的。这里不再详细说明。
+
diff --git a/java-chassis-reference/zh_CN/docs/featured-topics/application-porter/gateway-service.md b/java-chassis-reference/zh_CN/docs/featured-topics/application-porter/gateway-service.md
new file mode 100644
index 0000000..4aa3d53
--- /dev/null
+++ b/java-chassis-reference/zh_CN/docs/featured-topics/application-porter/gateway-service.md
@@ -0,0 +1,147 @@
+这个章节中,介绍如何通过网关转发请求。java-chassis提供了非常灵活的网关服务,开发者能够非常简单的实现微服务之间的转发,网关拥有客户端一样的服务治理能力。同时,开发者可以使用vert.x暴漏的HTTP API,实现非常灵活的转发控制。
+
+网关服务由一系列的VertxHttpDispatcher组成,开发者通过继承AbstractEdgeDispatcher,来实现自己的转发机制。
+
+为了实现gateway-service将请求转发到file-service,定义了如下规则:
+
+* 直接请求file-service: DELETE [http://localhost:9091/delete](http://localhost:9091/delete)
+
+* 通过网关:DELETE [http://localhost:9090/api/file-service/delete](http://localhost:9090/api/file-service/delete)
+
+达到这个目的的代码如下,在请求处理的时候,使用EdgeInvocation,可以实现请求转发,并开启各种治理功能。下面代码的核心内容是定义转发规则regex。
+
+```
+public class ApiDispatcher extends AbstractEdgeDispatcher {
+    @Override
+    public int getOrder() {
+        return 10002;
+    }
+
+    @Override
+    public void init(Router router) {
+        String regex = "/api/([^\\/]+)/(.*)";
+        router.routeWithRegex(regex).handler(CookieHandler.create());
+        router.routeWithRegex(regex).handler(createBodyHandler());
+        router.routeWithRegex(regex).failureHandler(this::onFailure).handler(this::onRequest);
+    }
+
+    protected void onRequest(RoutingContext context) {
+        Map<String, String> pathParams = context.pathParams();
+        String microserviceName = pathParams.get("param0");
+        String path = "/" + pathParams.get("param1");
+
+        EdgeInvocation invoker = new EdgeInvocation();
+        invoker.init(microserviceName, context, path, httpServerFilters);
+        invoker.edgeInvoke();
+    }
+}
+```
+
+为了实现gateway-service将请求转发到porter-website,定义了如下规则:
+
+* 直接请求porter-website: GET [http://localhost:9093/index.html](http://localhost:9093/index.html)
+
+* 通过网关:GET [http://localhost:9090/ui/porter-website/index.html](http://localhost:9090/ui/porter-website/index.html)
+
+UI静态页面信息不需要实现治理能力(服务治理能力需要契约,静态页面不存在接口契约),因此直接使用vert.x的API实现请求转发。在下面的代码中,还使用java chassis API做了服务发现,并实现了一个简单的RoundRobin负载均衡策略,从而允许porter-website也进行多实例部署。
+
+```
+public class UiDispatcher extends AbstractEdgeDispatcher {
+    private static Logger LOGGER = LoggerFactory.getLogger(UiDispatcher.class);
+
+    private static Vertx vertx = VertxUtils.getOrCreateVertxByName("web-client", null);
+
+    private static HttpClient httpClient = vertx.createHttpClient(new HttpClientOptions());
+
+    private Map<String, DiscoveryTree> discoveryTrees = new ConcurrentHashMapEx<>();
+
+    private AtomicInteger counter = new AtomicInteger(0);
+
+    @Override
+    public int getOrder() {
+        return 10001;
+    }
+
+    @Override
+    public void init(Router router) {
+        String regex = "/ui/([^\\/]+)/(.*)";
+        router.routeWithRegex(regex).failureHandler(this::onFailure).handler(this::onRequest);
+    }
+
+    protected void onRequest(RoutingContext context) {
+        Map<String, String> pathParams = context.pathParams();
+
+        String microserviceName = pathParams.get("param0");
+        String path = "/" + pathParams.get("param1");
+
+        URI uri = chooseServer(microserviceName);
+
+        if (uri == null) {
+            context.response().setStatusCode(404);
+            context.response().end();
+            return;
+        }
+
+        // 使用HttpClient转发请求
+        HttpClientRequest clietRequest =
+            httpClient.request(context.request().method(),
+                    uri.getPort(),
+                    uri.getHost(),
+                    "/" + path,
+                    clientResponse -> {
+                        context.request().response().setChunked(true);
+                        context.request().response().setStatusCode(clientResponse.statusCode());
+                        context.request().response().headers().setAll(clientResponse.headers());
+                        clientResponse.handler(data -> {
+                            context.request().response().write(data);
+                        });
+                        clientResponse.endHandler((v) -> context.request().response().end());
+                    });
+        clietRequest.setChunked(true);
+        clietRequest.headers().setAll(context.request().headers());
+        context.request().handler(data -> {
+            clietRequest.write(data);
+        });
+        context.request().endHandler((v) -> clietRequest.end());
+    }
+
+    private URI chooseServer(String serviceName) {
+        URI uri = null;
+
+        DiscoveryContext context = new DiscoveryContext();
+        context.setInputParameters(serviceName);
+        DiscoveryTree discoveryTree = discoveryTrees.computeIfAbsent(serviceName, key -> {
+            return new DiscoveryTree();
+        });
+        VersionedCache serversVersionedCache = discoveryTree.discovery(context,
+                RegistryUtils.getAppId(),
+                serviceName,
+                DefinitionConst.VERSION_RULE_ALL);
+        Map<String, MicroserviceInstance> servers = serversVersionedCache.data();
+        String[] endpoints = asArray(servers);
+        if (endpoints.length > 0) {
+            int index = Math.abs(counter.getAndIncrement() % endpoints.length);
+            String endpoint = endpoints[index];
+            try {
+                uri = new URI(endpoint);
+            } catch (URISyntaxException e) {
+                LOGGER.error("", e);
+            }
+        }
+        return uri;
+    }
+
+    private String[] asArray(Map<String, MicroserviceInstance> servers) {
+        List<String> endpoints = new LinkedList<>();
+        for (MicroserviceInstance instance : servers.values()) {
+            endpoints.addAll(instance.getEndpoints());
+        }
+        return endpoints.toArray(new String[endpoints.size()]);
+    }
+}
+```
+
+完成VertxHttpDispatcher开发后,需要通过SPI的方式加载到系统中,需要增加META-INF/services/org.apache.servicecomb.transport.rest.vertx.VertxHttpDispatcher配置文件,并将增加的两个实现写入该配置文件中。
+
+网关服务开发完成后,所有的用户请求都可以通过网关来发送。开发者通过通过设置防火墙等机制,限制用户直接访问内部服务,保证内部服务的安全。
+
diff --git a/java-chassis-reference/zh_CN/docs/featured-topics/application-porter/https.md b/java-chassis-reference/zh_CN/docs/featured-topics/application-porter/https.md
new file mode 100644
index 0000000..2b488d9
--- /dev/null
+++ b/java-chassis-reference/zh_CN/docs/featured-topics/application-porter/https.md
@@ -0,0 +1,36 @@
+HTTP协议已逐渐被标记为不安全,配置HTTPS可以防止用户数据被窃取和篡改,提升了安全性。考虑到性能的影响,我们只在网关使用HTTPS接入,内部服务之间仍然使用HTTP。
+
+使用HTTPS之前,需要准备证书。通常是向权威机构申请,这样的证书才会被浏览器等设备标记为可信。在这个例子中,我们使用通过工具已经生成好的证书。并且将自己的证书通过PKCS12格式存储在server.p12文件中,将CA的证书使用JKS格式存储在trust.jks中。
+
+网关启用HTTP只需要在监听的端口中增加sslEnabled配置项:
+
+```
+servicecomb:
+  rest:
+    address: 0.0.0.0:9090 ?sslEnabled=true
+```
+
+然后增加ssl相关的配置。下面的配置包含了TLS的协议、是否认证对端以及证书和密码信息。其中```EdgeSSLCustom```用于证书路径和证书密码的转换,不实现的时候,默认从当前目录读取证书文件,证书的密码明文存储。当业务需要做一些高级安全特性,比如密码保护的时候,可以通过扩展这个类实现。
+
+```
+ssl.protocols: TLSv1.2
+ssl.authPeer: false
+ssl.checkCN.host: false
+ssl.trustStore: trust.jks
+ssl.trustStoreType: JKS
+ssl.trustStoreValue: Changeme_123
+ssl.keyStore: server.p12
+ssl.keyStoreType: PKCS12
+ssl.keyStoreValue: Changeme_123
+ssl.crl: revoke.crl
+ssl.sslCustomClass: org.apache.servicecomb.samples.porter.gateway.EdgeSSLCustom
+```
+
+开发完成后,访问界面就可以通过https进行了
+
+```
+https://localhost:9090/ui/porter-website/index.html
+```
+
+
+
diff --git a/java-chassis-reference/zh_CN/docs/featured-topics/application-porter/microservices.png b/java-chassis-reference/zh_CN/docs/featured-topics/application-porter/microservices.png
new file mode 100644
index 0000000..f9eb802
Binary files /dev/null and b/java-chassis-reference/zh_CN/docs/featured-topics/application-porter/microservices.png differ
diff --git a/java-chassis-reference/zh_CN/docs/featured-topics/application-porter/porter-website.md b/java-chassis-reference/zh_CN/docs/featured-topics/application-porter/porter-website.md
new file mode 100644
index 0000000..69fbe4c
--- /dev/null
+++ b/java-chassis-reference/zh_CN/docs/featured-topics/application-porter/porter-website.md
@@ -0,0 +1,109 @@
+# 开发界面
+
+在技术选择上,界面完全由html+js+css等静态网页技术构成,不采用动态页面技术。采用静态页面技术构建界面,使得整个微服务系统更加具备弹性,能够非常容易的进行扩容,相关开发成果也能够更好的被其他应用继承。
+
+采用静态页面技术,也使得界面服务部署更加灵活多样:
+
+* 将静态页面部署到nginx,nginx将REST请求转发到gateway-service。
+* 将静态页面直接部署到gateway-service。
+* 静态页面通过Tomcat、Spring Boot等Web服务器部署,并注册到服务中心,gateway-service将请求转发到对应的应用服务器上。
+* 静态页面由第三方开发,第三方直接通过gateway-service访问REST接口。由第三方选择界面的开发技术。
+
+这几种方式都被广泛使用。
+
+## 将静态页面直接部署到gateway-service
+在[porter_lightweight](https://github.com/apache/servicecomb-samples/tree/master/porter_lightweight)项目中,采用了将静态页面直接部署到gateway-service的方式,这种方式简洁高效。这种方式的核心代码是StaticWebpageDispatcher。它采用vert.x提供的静态页面功能,直接挂载了静态页面服务。
+
+```
+public class StaticWebpageDispatcher implements VertxHttpDispatcher {
+  private static final Logger LOGGER = LoggerFactory.getLogger(StaticWebpageDispatcher.class);
+
+  private static final String WEB_ROOT = DynamicPropertyFactory.getInstance()
+      .getStringProperty("gateway.webroot", "/var/static")
+      .get();
+
+  @Override
+  public int getOrder() {
+    return Integer.MAX_VALUE;
+  }
+
+  @Override
+  public void init(Router router) {
+    String regex = "/ui/(.*)";
+    StaticHandler webpageHandler = StaticHandler.create();
+    webpageHandler.setWebRoot(WEB_ROOT);
+    LOGGER.info("server static web page for WEB_ROOT={}", WEB_ROOT);
+    router.routeWithRegex(regex).failureHandler((context) -> {
+      LOGGER.error("", context.failure());
+    }).handler(webpageHandler);
+  }
+
+}
+```
+
+## 静态页面通过Tomcat、Spring Boot等Web容器部署,并注册
+
+在架构图中,界面的请求需要被网关转发,并且需要支持多实例部署,因此界面服务需要增加的功能是服务注册和发现。有两种方法集成和使用J2EE:
+
+1. 运行于独立的web服务器中,如tomcat等。
+
+2. 运行于Spring Boot的Embedded Tomcat中。
+
+
+在Spring Boot中提供静态页面服务,核心问题是解决服务注册、发现能力。在Spring Boot的Embeded Tomcat中使用ServiceComb的服务注册发现,需要完成如下步骤:
+
+* 增加依赖关系
+
+依赖关系定义了对于Spring Boot的依赖和java-chassis的依赖。
+
+```
+<dependency>
+
+  <groupId>org.apache.servicecomb</groupId>
+
+  <artifactId>java-chassis-spring-boot-starter-servlet</artifactId>
+
+</dependency>
+```
+
+* 配置微服务信息\(microservice.yaml\)
+
+需要注意配置项servicecomb.rest.address的端口与application.yml的server.port保持一致。application.yml是Spring Boot的配置文件,用于指定Embeded Tomcat的监听端口。microservice.yam的信息用于服务注册。另外也需要注意一下配置项servicecomb.rest.servlet.urlPattern,当使用@EnableServiceComb时,会加载REST框架org.apache.servicecomb.transport.rest.servlet. RestServlet,而且默认接管了/\*的请求。在我们的场景下,仅仅需要提供web页面,不需要提供REST服务,这个配置项的含义就是将它的路径改为一个和静态页面不冲突的路径,以保证静态页面能够被正常访问。
+
+```
+APPLICATION_ID: porter
+service_description:
+  name: porter-website
+  version: 0.0.1
+
+servicecomb:
+  rest:
+    address: 0.0.0.0:9093
+    servlet:
+      urlPattern: /servicecomb/rest/*
+```
+
+* 增加静态页面
+
+按照Spring Boot的惯例,静态页面需要放到源代码的resources/static目录。项目开始前,增加了如下静态页面和目录:
+
+```
+css
+js
+index.html
+```
+
+* 使用@EnableServiceComb启用注册发现
+
+```
+@SpringBootApplication
+@EnableServiceComb
+public class WebsiteMain {
+    public static void main(final String[] args) {
+        SpringApplication.run(WebsiteMain.class, args);
+    }
+}
+```
+
+经过以上的步骤,界面服务就开发完成了。通过运行WebsiteMain,就可以通过[http://localhost:9093](http://localhost:9093) 来访问。
+
diff --git a/java-chassis-reference/zh_CN/docs/featured-topics/application-porter/user-service.md b/java-chassis-reference/zh_CN/docs/featured-topics/application-porter/user-service.md
new file mode 100644
index 0000000..ee7970f
--- /dev/null
+++ b/java-chassis-reference/zh_CN/docs/featured-topics/application-porter/user-service.md
@@ -0,0 +1,146 @@
+访问数据库可以使用第三方提供的组件。这里选择了MyBatis说明如何访问数据库。开发者也可以直接参考:
+
+[http://www.mybatis.org/spring/zh/index.html](http://www.mybatis.org/spring/zh/index.html)
+
+这里给出一个快速集成参考。在本章中涉及到建表等数据库操作的时候,数据库选用MySQL。
+
+# 设计表
+
+本应用提供了非常简单的用户管理和基于角色的鉴权机制。因此我们设计了非常简单的用户表,表格包含了用户名称及用户所属的角色。为了测试的目的,还插入了两个用户数据,其中密码采用SHA256进行单向加密保存。
+
+    CREATE DATABASE IF NOT EXISTS porter_user_db;
+
+    USE porter_user_db;
+
+    DROP TABLE IF EXISTS T_USER;
+
+    CREATE TABLE `T_USER` (
+      `ID`  INTEGER(20) NOT NULL COMMENT '用户ID',
+      `USER_NAME`  VARCHAR(64) NOT NULL COMMENT '用户名称',
+      `PASSWORD`  VARCHAR(64) NOT NULL COMMENT '用户密码',
+      `ROLE_NAME`  VARCHAR(64) NOT NULL COMMENT '角色名称',
+      PRIMARY KEY (`ID`)
+    );
+
+    insert into T_USER(ID, USER_NAME, PASSWORD, ROLE_NAME) values(1, "admin", "n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg=", "admin");
+    insert into T_USER(ID, USER_NAME, PASSWORD, ROLE_NAME) values(2, "guest", "n4bQgYhMfWWaL+qgxVrQFaO/TxsrC4Is0V1sFbDwCgg=", "guest");
+
+# 使用MyBatis
+
+* 引用MyBatis的相关依赖
+
+依赖包含了MyBatis和Spring、DBCP2数据库连接池管理相关组件。这些组件都是使用Spring和MyBatis必须的。
+
+```
+<dependency>
+  <groupId>org.mybatis</groupId>
+  <artifactId>mybatis</artifactId>
+  <version>3.4.5</version>
+</dependency>
+<dependency>
+  <groupId>mysql</groupId>
+  <artifactId>mysql-connector-java</artifactId>
+</dependency>
+<dependency>
+  <groupId>org.apache.commons</groupId>
+  <artifactId>commons-dbcp2</artifactId>
+</dependency>
+<dependency>
+  <groupId>org.mybatis</groupId>
+  <artifactId>mybatis-spring</artifactId>
+  <version>1.3.0</version>
+</dependency>
+<dependency>
+  <groupId>org.springframework</groupId>
+  <artifactId>spring-jdbc</artifactId>
+  <scope>compile</scope>
+</dependency>
+<dependency>
+  <groupId>org.springframework</groupId>
+  <artifactId>spring-aop</artifactId>
+</dependency>
+<dependency>
+  <groupId>org.springframework</groupId>
+  <artifactId>spring-context-support</artifactId>
+</dependency>
+<dependency>
+  <groupId>org.springframework</groupId>
+  <artifactId>spring-tx</artifactId>
+</dependency>
+```
+
+* 配置数据源和SqlSessionFactory
+
+数据源使用DBCP2。SqlSessionFactory里面指定了dataSource和configLocation两个属性,并新增加了mybatis-config.xml文件,用于配置mapper文件的路径。 在本微服务场景中,只需要使用简单的数据库连接和简单事务管理,如果需要使用复杂的事务管理,还需要配置XA数据源和相关的事务管理器。 有关MyBatis的Configuration更加详细的信息可以参考:[http://www.mybatis.org/mybatis-3/configuration.html](http://www.mybatis.org/mybatis-3/configuration.html) 。
+
+```
+<bean id="dataSource" class="org.apache.commons.dbcp2.BasicDataSource" destroy-method="close">
+  <property name="driverClassName" value="${db.url:com.mysql.jdbc.Driver}" />
+  <property name="url" value="${db.url:jdbc:mysql://localhost/porter_user_db}" />
+  <property name="username" value="${db.username:root}" />
+  <property name="password" value="${db.password:}" />
+</bean>
+
+<bean id="sqlSessionFactory" class="org.mybatis.spring.SqlSessionFactoryBean">
+  <property name="dataSource" ref="dataSource" />
+  <property name="configLocation" value="classpath:/config/mybatis-config.xml"></property>
+</bean>
+```
+
+* 书写Mapper文件
+
+涉及到JAVA的Mapper定义UserMapper,XML中定义SQL与JAVA的映射关系UserMapper.xml。定义完成后,需要将内容配置到Mybatis的扫描路径和Spring的扫描路径中,涉及文件mybatis-config.xml和user.bean.xml。
+
+```
+### mybatis-config.xml
+<configuration>
+    <mappers>
+        <mapper resource="config/UserMapper.xml"/>
+    </mappers>
+</configuration>
+
+### user.bean.xml
+<bean id="userMapper" class="org.mybatis.spring.mapper.MapperFactoryBean">
+    <property name="mapperInterface"
+        value="org.apache.servicecomb.samples.porter.user.dao.UserMapper" />
+    <property name="sqlSessionFactory" ref="sqlSessionFactory" />
+</bean>
+```
+
+## 设计用户服务
+
+经过上面的配置,数据库访问相关开发已经完成了。 结合User Story,可以先设计一个login的服务接口。 这个服务在UserServiceEndpoint里面进行定义。
+
+```
+@PostMapping(path = "/login", produces = MediaType.APPLICATION_JSON_VALUE)
+public SessionInfo login(@RequestParam(name = "userName") String userName, 
+    @RequestParam(name = "password") String password)
+```
+
+接口会返回SessionInfo,这些必要的信息,会在后续的鉴权、认证操作中起到很大的方便。
+
+经过以上的开发,就可以启动用户服务,配置数据库和插入相关数据,从界面访问这个接口。
+
+```
+#### 访问login接口的HTTP请求和响应
+
+#Request
+POST http://localhost:9090/api/user-service/v1/user/login
+
+Content-Type: application/x-www-form-urlencoded
+
+userName=admin&password=test
+
+#Response
+{
+    "id": 0,
+    "sessiondId": "1be646c0-50cb-4c0a-968d-2a512775f5e8",
+    "userName": "guest",
+    "roleName": "guest",
+    "creationTime": null,
+    "activeTime": null
+}
+```
+
+
+
diff --git a/java-chassis-reference/zh_CN/docs/featured-topics/application-porter/user-story.md b/java-chassis-reference/zh_CN/docs/featured-topics/application-porter/user-story.md
new file mode 100644
index 0000000..61f01e3
--- /dev/null
+++ b/java-chassis-reference/zh_CN/docs/featured-topics/application-porter/user-story.md
@@ -0,0 +1,6 @@
+开始前,给应用取一个名字porter。
+
+![](usercase.png)
+
+
+
diff --git a/java-chassis-reference/zh_CN/docs/featured-topics/application-porter/usercase.png b/java-chassis-reference/zh_CN/docs/featured-topics/application-porter/usercase.png
new file mode 100644
index 0000000..00106ac
Binary files /dev/null and b/java-chassis-reference/zh_CN/docs/featured-topics/application-porter/usercase.png differ
diff --git a/java-chassis-reference/zh_CN/docs/general-development/local-develop-test.md b/java-chassis-reference/zh_CN/docs/general-development/local-develop-test.md
index d58d56f..b249478 100644
--- a/java-chassis-reference/zh_CN/docs/general-development/local-develop-test.md
+++ b/java-chassis-reference/zh_CN/docs/general-development/local-develop-test.md
@@ -2,9 +2,9 @@
 
 本小节介绍如何在开发者本地进行消费者/提供者应用的开发调试。开发服务提供者请参考3 开发服务提供者章节,开发服务消费者请参考4 开发服务消费者。服务提供者和消费提供者均需要连接到在远程的服务中心,为了本地微服务的开发和调试,本小节介绍了两种搭建本地服务中心的方法进行本地微服务调试:
 
-* 启动[本地服务中心](#section2945986191314);
+* 启动本地服务中心
 
-* 通过local file模拟启动服务中心\([Mock机制](#section960893593759)\)。
+* 通过local file模拟启动服务中心Mock机制
 
 * 通过设置环境信息方便本地调试
 
diff --git a/java-chassis-reference/zh_CN/docs/general-development/metrics.md b/java-chassis-reference/zh_CN/docs/general-development/metrics.md
index d925555..5dbcb39 100644
--- a/java-chassis-reference/zh_CN/docs/general-development/metrics.md
+++ b/java-chassis-reference/zh_CN/docs/general-development/metrics.md
@@ -47,8 +47,9 @@ _注:请将version字段修改为实际版本号;如果版本号已经在dep
 
 ### 3.慢调用检测
   开启慢调用检测后,如果存在慢调用,则会立即输出相应日志:
+
 ```
-2019-04-02 23:01:09,103[WARN][pool-7-thread-74][5ca37935c00ff2c7-350076] - slow(40 ms) invocation, CONSUMER highway perf1.impl.syncQuery
+2019-04-02 23:01:09,103\[WARN]\[pool-7-thread-74]\[5ca37935c00ff2c7-350076] - slow(40 ms) invocation, CONSUMER highway perf1.impl.syncQuery
   http method: GET
   url        : /v1/syncQuery/{id}/
   server     : highway://192.168.0.152:7070?login=true
@@ -65,6 +66,7 @@ _注:请将version字段修改为实际版本号;如果版本号已经在dep
     client filters response: 0.2 ms
     handlers response      : 0.0 ms (SlowInvocationLogger.java:121)
 ```
+
   其中5ca37935c00ff2c7-350076是${traceId}-${invocationId}的结构,在log4j2或logback的输出格式中通过%marker引用
 
 ### 4.通过RESTful访问
diff --git a/java-chassis-reference/zh_CN/mkdocs.yml b/java-chassis-reference/zh_CN/mkdocs.yml
index 2f14bec..8c21b1c 100644
--- a/java-chassis-reference/zh_CN/mkdocs.yml
+++ b/java-chassis-reference/zh_CN/mkdocs.yml
@@ -6,6 +6,7 @@ nav:
     - 微服务系统架构: 'start/architecture.md'
     - 安装本地开发环境: 'start/development-environment.md'
     - 开发第一个微服务: start/first-sample.md
+    - 完整例子-porter应用: featured-topics/application-porter.md
 - 开发服务提供者: 
     - 服务定义: build-provider/definition/service-definition.md
     - 定义服务契约: build-provider/define-contract.md