[关闭]
@yanbo-ai 2016-01-13T15:54:40.000000Z 字数 9010 阅读 4228

在 Docker 上运行一个 RESTful 风格的微服务

Microservice RESTful Docker

Author: Andy Ai
Weibo: NinetyH
GitHub: https://github.com/aiyanbo/docker-restful-demo


实现构思
1. 使用 Maven 进行项目构建
2. 使用 Jersey 实现一个 RESTful 风格的微服务
3. 在 Docker 里面执行 mvn package 对项目打包
4. 在 Docker 容器里运行这个微服务

实现一个 RESTful 风格的微服务

如果你对 RESTful 风格的 API 设计有疑惑,可以参考我的文章 RESTful Best Practices

场景 & 需求

在 Maven 仓库里面有许多的组件,我们现在暂且称之为 Stack。在我们模拟的系统里面有下面2个需求:
1. 列出仓库里的所有 Stack
2. 根据 StackID 找到某一个组件,如果没有找到则返回 Not Found

现在,我们就根据这个需求一起踏入 Jersey 打造微服务的奇幻之旅。

Step0. 准备

使用 mvn 命令创建一个简单工程

  1. mvn archetype:generate -DgroupId=org.jmotor -DartifactId=docker-restful-demo -DinteractiveMode=false

pom.xml 加入 Jersey 等依赖

  1. <properties>
  2. <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
  3. <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding>
  4. <junit.version>4.12</junit.version>
  5. <jersey.version>2.18</jersey.version>
  6. <javax.servlet.version>3.1.0</javax.servlet.version>
  7. </properties>
  8. <dependencies>
  9. <dependency>
  10. <groupId>junit</groupId>
  11. <artifactId>junit</artifactId>
  12. <version>${junit.version}</version>
  13. <scope>test</scope>
  14. </dependency>
  15. <dependency>
  16. <groupId>org.glassfish.jersey.containers</groupId>
  17. <artifactId>jersey-container-grizzly2-http</artifactId>
  18. <version>${jersey.version}</version>
  19. </dependency>
  20. <dependency>
  21. <groupId>org.glassfish.jersey.media</groupId>
  22. <artifactId>jersey-media-json-jackson</artifactId>
  23. <version>${jersey.version}</version>
  24. </dependency>
  25. </dependencies>

Step1. 构建 Model

Stack 包含了以下几个属性: id, groupId, artifactId, version。同时,Stack 类里面包含了一个 Builder 用来比较方便地创建一个 Stack 对象。这些都可以使用 IDE 自动生成,无需手动编写。

  1. package org.jmotor.model;
  2. /**
  3. * Component:
  4. * Description:
  5. * Date: 2015/6/18
  6. *
  7. * @author Andy Ai
  8. */
  9. public class Stack {
  10. private Integer id;
  11. private String groupId;
  12. private String artifactId;
  13. private String version;
  14. ...getter and setter...
  15. public static class Builder {
  16. private Integer id;
  17. private String groupId;
  18. private String artifactId;
  19. private String version;
  20. public Builder id(Integer id) {
  21. this.id = id;
  22. return this;
  23. }
  24. ...
  25. public Stack build() {
  26. Stack stack = new Stack();
  27. stack.setId(id);
  28. ...
  29. return stack;
  30. }
  31. public static Builder newBuilder() {
  32. return new Builder();
  33. }
  34. }
  35. }

Step2 创建一个 Restlet

刚刚我们已经把 Model 做好了,现在我们就开始使用 Jersey 实现一个 Service,在 JAX-RS 中这样的一个服务称为 Resource。在这里,这个 Service(Resource) 提供了一个 RESTful 风格的接口访问。所以我们称之为 restletrestlet 在 JAX-RS 或 Jersey 中并没有这个概念,这是我们附加上去的用法。

  1. import javax.ws.rs.Path;
  2. @Path("/v1/stacks")
  3. public class StacksRestlet {}

我们需要使用 javax.ws.rs.Path 这个注解来申明 Restlet 的根路径是什么。在上面的代码中, Restlet 的跟路径是 /v1/stacks

Step3. 实现服务接口

在 Jersey 里实现一个服务接口非常简单,你只需要创建一个 public 的方法就可以了。

接口1:

  1. @GET
  2. @Produces("application/json")
  3. public List<Stack> stacks() {
  4. return Arrays.asList(
  5. Stack.Builder.newBuilder().id(1).groupId("javax.servlet").artifactId("javax.servlet-api").version("3.1.0").build(),
  6. Stack.Builder.newBuilder().id(2).groupId("com.google.guava").artifactId("guava").version("18.0").build()
  7. );
  8. }

我们使用 javax.ws.rs.GET 这个注解来申明接口接受的是 HTTP 请求的 GET 方法。javax.ws.rs.Produces("application/json") 用来表示我们这个接口返回的是 application/json 类型的数据。

接口2:

  1. @GET
  2. @Path("{id}")
  3. @Produces("application/json")
  4. public Stack filterByArtifactId(@NotNull @PathParam("id") Integer id) {
  5. switch (id) {
  6. case 1:
  7. return Stack.Builder.newBuilder().id(1).groupId("javax.servlet").artifactId("javax.servlet-api").version("3.1.0").build();
  8. case 2:
  9. return Stack.Builder.newBuilder().id(2).groupId("com.google.guava").artifactId("guava").version("18.0").build();
  10. default:
  11. throw new WebApplicationException("Stack not found, id: " + id, 404);
  12. }
  13. }

在上面的示例中:
1. @Path("{id}") 表示 id 是一个 url 上的动态参数,因为 id 是变化的,所以我们要做成一个 url 变量。然后在方法里面使用 @PathParam("id") 来获得这个参数。JAX-RS 可以有许多类型的参数,例如:QueryParam 用来获取 url 问号? 后面的查询参数。你可以在 javax.ws.rs 这个包中找到其他的参数!
2. 使用 WebApplicationException 抛一个任何状态的异常,例如: 404 或 500。

Step4. 运行微服务

这里,我们使用 Jersey 的内置的 Grizzly 容器运行。

  1. final URI uri = UriBuilder.fromUri("http://localhost/").port(9998).build();
  2. final ResourceConfig config = new ResourceConfig();
  3. config.packages("org.jmotor.restlet");
  4. final HttpServer server = GrizzlyHttpServerFactory.createHttpServer(uri, config);
  5. Runtime.getRuntime().addShutdownHook(new Thread() {
  6. @Override
  7. public void run() {
  8. server.shutdown();
  9. }
  10. });
  11. try {
  12. server.start();
  13. } catch (IOException e) {
  14. e.printStackTrace();
  15. System.exit(1);
  16. }

Step5. 测试

获取 Stacks 列表

  1. $ curl http://localhost:9998/v1/stacks
  2. [{"id":1,"groupId":"javax.servlet","artifactId":"javax.servlet-api","version":"3.1.0"},{"id":2,"groupId":"com.google.gua
  3. va","artifactId":"guava","version":"18.0"}]

根据 ID 获取 Stack

  1. $ curl http://localhost:9998/v1/stacks/1
  2. {"id":1,"groupId":"javax.servlet","artifactId":"javax.servlet-api","version":"3.1.0"}

找不到的 Stack

  1. $ curl -I http://localhost:9998/v1/stacks/5
  2. HTTP/1.1 404 Not Found
  3. Content-Length: 0
  4. Date: Tue, 23 Jun 2015 06:04:19 GMT

在 Docker 中运行

首先,我们需要安装一个 Docker 环境,你可以从 Docker Docs 上找到如何安装它。

安装完成后,我们需要把我们的微服务打包成一个 Docker Image。下面,我们就使用 Dockerfile 来构建我们的 Docker Image。

Step0. 准备

刚刚我们已经成功地在 IDE 中运行了我们的微服务。但是如果需要让它能独立运行,我们需要把我们的工程通过 mvn 做成一个可以运行的包。但是因为我们在 Docker 中运行,所以我们只需要把相关的依赖复制到一个特地的地方就可以了。在下面的代码中,我们把依赖放到了 ${project.build.directory}/lib 下。

pom.xml 加入下面的代码:

  1. <build>
  2. <plugins>
  3. <plugin>
  4. <groupId>org.apache.maven.plugins</groupId>
  5. <artifactId>maven-dependency-plugin</artifactId>
  6. <executions>
  7. <execution>
  8. <id>copy-dependencies</id>
  9. <phase>package</phase>
  10. <goals>
  11. <goal>copy-dependencies</goal>
  12. </goals>
  13. <configuration>
  14. <excludeScope>provided</excludeScope>
  15. <outputDirectory>${project.build.directory}/lib</outputDirectory>
  16. </configuration>
  17. </execution>
  18. </executions>
  19. </plugin>
  20. <plugin>
  21. <groupId>org.apache.maven.plugins</groupId>
  22. <artifactId>maven-compiler-plugin</artifactId>
  23. <configuration>
  24. <source>1.7</source>
  25. <target>1.7</target>
  26. </configuration>
  27. </plugin>
  28. </plugins>
  29. </build>

Step1. Dockerfile

  1. FROM jamesdbloom/docker-java8-maven
  2. MAINTAINER Andy Ai "yanbo.ai@gmail.com"
  3. WORKDIR /code
  4. ADD pom.xml /code/pom.xml
  5. ADD src /code/src
  6. ADD settings.xml /root/.m2/settings.xml
  7. RUN ["mvn", "package"]
  8. CMD ["java", "-cp", "target/lib/*:target/docker-restful-demo-1.0-SNAPSHOT.jar", "org.jmotor.StackMicroServices"]
  9. EXPOSE 9998

Tips
1. ADD settings.xml /root/.m2/settings.xml,这是加入了本地的 maven settings。在这个文件里面你可能会使用到一些特定的配置,例如:maven 仓库的代理镜像。代理镜像可以加快你的 Docker Build。
2. EXPOSE 9998 Docker 对外暴露的端口需要跟服务的端口是一致的。

Step2. Build Image

  1. cd docker-restful-demo
  2. docker build -t docker-restful-demo .

上面代码中,-t 是在 Docker Build 的时候指定 Image Tag。

Step3. 运行 Image

  1. docker run -d -p 9998:9998 docker-restful-demo

Tips
-p 是发布一个 Docker 容器的端口到 Docker 运行的主机上。

Step4. Docker 容器测试

检查 Docker 容器是否在运行

  1. $ docker ps
  2. CONTAINER ID IMAGE COMMAND CREATED STATUS PORTS
  3. NAMES
  4. bdda2408484a docker-restful-demo:latest "java -cp target/lib 31 seconds ago Up 29 seconds 0.0.0.0:9
  5. 998->9998/tcp fervent_swartz

检查 Docker 容器内的服务是否已经启动:

  1. docker exec -i -t bdda2408484a bash
  1. $ ss -a
  2. Netid State Recv-Q Send-Q Local Address:Port Peer Address:Port
  3. nl UNCONN 0 0 rtnl:kernel *
  4. nl UNCONN 4352 0 tcpdiag:ss/92 *
  5. nl UNCONN 768 0 tcpdiag:kernel *
  6. nl UNCONN 0 0 6:kernel *
  7. nl UNCONN 0 0 10:kernel *
  8. nl UNCONN 0 0 12:kernel *
  9. nl UNCONN 0 0 15:kernel *
  10. nl UNCONN 0 0 16:kernel *
  11. u_str ESTAB 0 0 * 9590 * 0
  12. tcp LISTEN 0 128 ::ffff:127.0.0.1:9998 :::*
  1. $ curl -i http://localhost:9998/v1/stacks
  2. HTTP/1.1 200 OK
  3. Content-Type: application/json
  4. Date: Tue, 23 Jun 2015 07:51:15 GMT
  5. Content-Length: 163
  6. [{"id":1,"groupId":"javax.servlet","artifactId":"javax.servlet-api","version":"3.1.0"},{"id":2,"groupId":"com.google.gua
  7. va","artifactId":"guava","version":"18.0"}]
  1. exit

Step5. 远程调用测试

  1. $ boot2docker ip
  2. 192.168.59.103
  1. $ curl http://192.168.59.103:9998/v1/stacks
  2. curl: (7) Failed to connect to 192.168.59.103 port 9998: Connection refused

如果遇到上面的错误,我们可以通过2种方式去解决它:
方法1: 将程序绑定全零IP的端口

检查 Docker 容器绑定的端口:

  1. $ docker port bdda2408484a
  2. 9998/tcp -> 0.0.0.0:9998

我们看到的是 9998 这个端口绑定在 0.0.0.0 上,这时需要把 Jersey 容器的 URI 改成 0.0.0.0 就可以,像这样:

  1. final URI uri = UriBuilder.fromUri("http://localhost/").port(9998).build();
  2. --->
  3. final URI uri = UriBuilder.fromUri("http://0.0.0.0/").port(9998).build();

方法2: 将程序绑定到非回路的IP端口上

查看 Docker 容器 IP 地址的方法:

  1. boot2docker ssh
  2. docker inspect --format '{{.NetworkSettings.IPAddress}}' $container_id

使用 Java 接口获取本机的非回路IP地址:

  1. final URI uri = UriBuilder.fromUri("http://localhost/").port(9998).build();
  2. --->
  3. InetAddress inetAddress = localInet4Address();
  4. String host = "0.0.0.0";
  5. if (inetAddress != null) {
  6. host = inetAddress.getHostAddress();
  7. }
  8. final URI uri = UriBuilder.fromUri("http://" + host + "/").port(9998).build();
  9. private static InetAddress localInet4Address() throws SocketException {
  10. Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces();
  11. while (networkInterfaces.hasMoreElements()) {
  12. NetworkInterface networkInterface = networkInterfaces.nextElement();
  13. Enumeration<InetAddress> inetAddresses = networkInterface.getInetAddresses();
  14. while (inetAddresses.hasMoreElements()) {
  15. InetAddress inetAddress = inetAddresses.nextElement();
  16. if (!inetAddress.isLoopbackAddress() && inetAddress instanceof Inet4Address) {
  17. return inetAddress;
  18. }
  19. }
  20. }
  21. return null;
  22. }

使用上面的任何一种方法,服务都能正常调用:

  1. $ curl -i http://192.168.59.103:9998/v1/stacks
  2. HTTP/1.1 200 OK
  3. Content-Type: application/json
  4. Date: Tue, 23 Jun 2015 07:53:24 GMT
  5. Content-Length: 163
  6. [{"id":1,"groupId":"javax.servlet","artifactId":"javax.servlet-api","version":"3.1.0"},{"id":2,"groupId":"com.google.gua
  7. va","artifactId":"guava","version":"18.0"}]

加速器

在撰写此文的时候,我使用的是 DaoColud 的镜像在做加速。 具体步骤请查看 DaoColud Mirror 文档。

如果你在 Windows 上使用 Boot2Docker, 可以按照下列步骤设置你的 Docker 镜像仓库:

  1. boot2docker ssh
  2. sudo su
  3. echo "EXTRA_ARGS=\"--registry-mirror=http://98bc3dca.m.daocloud.io\"" >> /var/lib/boot2docker/profile
  4. exit
  5. boot2docker restart

参考资料

https://dashboard.daocloud.io/mirror
http://martinfowler.com/articles/microservices.html
https://jersey.java.net/documentation/latest/index.html
https://blog.giantswarm.io/getting-started-with-java-development-on-docker/

未经同意不可转载, 转载需保留原文链接与作者署名。

添加新批注
在作者公开此批注前,只有你和作者可见。
回复批注