实现构思
使用 Maven 进行项目构建
使用 Jersey 实现一个 RESTful 风格的微服务
在 Docker 里面执行 mvn package
对项目打包
在 Docker 容器里运行这个微服务
如果你对 RESTful 风格的 API 设计有疑惑,可以参考我的文章 RESTful Best Practices。
在 Maven 仓库里面有许多的组件,我们现在暂且称之为 Stack
。在我们模拟的系统里面有下面2个需求:
列出仓库里的所有 Stack
根据 Stack
的 ID
找到某一个组件,如果没有找到则返回 Not Found
现在,我们就根据这个需求一起踏入 Jersey 打造微服务的奇幻之旅。
使用 mvn
命令创建一个简单工程
mvn archetype:generate -DgroupId=org.jmotor -DartifactId=docker-restful-demo -DinteractiveMode=false
在 pom.xml
加入 Jersey 等依赖
<properties> <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding> <project.reporting.outputEncoding>UTF-8</project.reporting.outputEncoding> <junit.version>4.12</junit.version> <jersey.version>2.18</jersey.version> <javax.servlet.version>3.1.0</javax.servlet.version></properties><dependencies> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>${junit.version}</version> <scope>test</scope> </dependency> <dependency> <groupId>org.glassfish.jersey.containers</groupId> <artifactId>jersey-container-grizzly2-http</artifactId> <version>${jersey.version}</version> </dependency> <dependency> <groupId>org.glassfish.jersey.media</groupId> <artifactId>jersey-media-json-jackson</artifactId> <version>${jersey.version}</version> </dependency></dependencies>
Stack
包含了以下几个属性: id
, groupId
, artifactId
, version
。同时,Stack 类里面包含了一个 Builder 用来比较方便地创建一个 Stack 对象。这些都可以使用 IDE 自动生成,无需手动编写。
package org.jmotor.model;/** * Component: * Description: * Date: 2015/6/18 * * @author Andy Ai */public class Stack { private Integer id; private String groupId; private String artifactId; private String version; ...getter and setter... public static class Builder { private Integer id; private String groupId; private String artifactId; private String version; public Builder id(Integer id) { this.id = id; return this; } ... public Stack build() { Stack stack = new Stack(); stack.setId(id); ... return stack; } public static Builder newBuilder() { return new Builder(); } }}
刚刚我们已经把 Model 做好了,现在我们就开始使用 Jersey 实现一个 Service,在 JAX-RS 中这样的一个服务称为 Resource。在这里,这个 Service(Resource) 提供了一个 RESTful 风格的接口访问。所以我们称之为 restlet
。restlet
在 JAX-RS 或 Jersey 中并没有这个概念,这是我们附加上去的用法。
import javax.ws.rs.Path;@Path("/v1/stacks")public class StacksRestlet {}
我们需要使用 javax.ws.rs.Path
这个注解来申明 Restlet 的根路径是什么。在上面的代码中, Restlet 的跟路径是 /v1/stacks
。
在 Jersey 里实现一个服务接口非常简单,你只需要创建一个 public 的方法就可以了。
接口1:
@GET@Produces("application/json")public List<Stack> stacks() { return Arrays.asList( Stack.Builder.newBuilder().id(1).groupId("javax.servlet").artifactId("javax.servlet-api").version("3.1.0").build(), Stack.Builder.newBuilder().id(2).groupId("com.google.guava").artifactId("guava").version("18.0").build() );}
我们使用 javax.ws.rs.GET
这个注解来申明接口接受的是 HTTP 请求的 GET 方法。javax.ws.rs.Produces("application/json")
用来表示我们这个接口返回的是 application/json
类型的数据。
接口2:
@GET@Path("{id}")@Produces("application/json")public Stack filterByArtifactId(@NotNull @PathParam("id") Integer id) { switch (id) { case 1: return Stack.Builder.newBuilder().id(1).groupId("javax.servlet").artifactId("javax.servlet-api").version("3.1.0").build(); case 2: return Stack.Builder.newBuilder().id(2).groupId("com.google.guava").artifactId("guava").version("18.0").build(); default: throw new WebApplicationException("Stack not found, id: " + id, 404); }}
在上面的示例中:
@Path("{id}")
表示 id 是一个 url 上的动态参数,因为 id 是变化的,所以我们要做成一个 url 变量。然后在方法里面使用 @PathParam("id")
来获得这个参数。JAX-RS 可以有许多类型的参数,例如:QueryParam
用来获取 url 问号?
后面的查询参数。你可以在 javax.ws.rs
这个包中找到其他的参数!
使用 WebApplicationException
抛一个任何状态的异常,例如: 404 或 500。
这里,我们使用 Jersey 的内置的 Grizzly 容器运行。
final URI uri = UriBuilder.fromUri("http://localhost/").port(9998).build();final ResourceConfig config = new ResourceConfig();config.packages("org.jmotor.restlet");final HttpServer server = GrizzlyHttpServerFactory.createHttpServer(uri, config);Runtime.getRuntime().addShutdownHook(new Thread() { @Override public void run() { server.shutdown(); }});try { server.start();} catch (IOException e) { e.printStackTrace(); System.exit(1);}
获取 Stacks 列表
$ curl http://localhost:9998/v1/stacks[{"id":1,"groupId":"javax.servlet","artifactId":"javax.servlet-api","version":"3.1.0"},{"id":2,"groupId":"com.google.guava","artifactId":"guava","version":"18.0"}]
根据 ID 获取 Stack
$ curl http://localhost:9998/v1/stacks/1{"id":1,"groupId":"javax.servlet","artifactId":"javax.servlet-api","version":"3.1.0"}
找不到的 Stack
$ curl -I http://localhost:9998/v1/stacks/5HTTP/1.1 404 Not FoundContent-Length: 0Date: Tue, 23 Jun 2015 06:04:19 GMT
首先,我们需要安装一个 Docker 环境,你可以从 Docker Docs 上找到如何安装它。
安装完成后,我们需要把我们的微服务打包成一个 Docker Image
。下面,我们就使用 Dockerfile 来构建我们的 Docker Image。
刚刚我们已经成功地在 IDE 中运行了我们的微服务。但是如果需要让它能独立运行,我们需要把我们的工程通过 mvn 做成一个可以运行的包。但是因为我们在 Docker 中运行,所以我们只需要把相关的依赖复制到一个特地的地方就可以了。在下面的代码中,我们把依赖放到了 ${project.build.directory}/lib
下。
在 pom.xml
加入下面的代码:
<build> <plugins> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-dependency-plugin</artifactId> <executions> <execution> <id>copy-dependencies</id> <phase>package</phase> <goals> <goal>copy-dependencies</goal> </goals> <configuration> <excludeScope>provided</excludeScope> <outputDirectory>${project.build.directory}/lib</outputDirectory> </configuration> </execution> </executions> </plugin> <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-compiler-plugin</artifactId> <configuration> <source>1.7</source> <target>1.7</target> </configuration> </plugin> </plugins></build>
FROM jamesdbloom/docker-java8-mavenMAINTAINER Andy Ai "yanbo.ai@gmail.com"WORKDIR /codeADD pom.xml /code/pom.xmlADD src /code/srcADD settings.xml /root/.m2/settings.xmlRUN ["mvn", "package"]CMD ["java", "-cp", "target/lib/*:target/docker-restful-demo-1.0-SNAPSHOT.jar", "org.jmotor.StackMicroServices"]EXPOSE 9998
Tips
ADD settings.xml /root/.m2/settings.xml
,这是加入了本地的 maven settings。在这个文件里面你可能会使用到一些特定的配置,例如:maven 仓库的代理镜像。代理镜像可以加快你的 Docker Build。
EXPOSE 9998
Docker 对外暴露的端口需要跟服务的端口是一致的。
cd docker-restful-demodocker build -t docker-restful-demo .
上面代码中,-t
是在 Docker Build 的时候指定 Image Tag。
docker run -d -p 9998:9998 docker-restful-demo
Tips
-p
是发布一个 Docker 容器的端口到 Docker 运行的主机上。
检查 Docker 容器是否在运行
$ docker psCONTAINER ID IMAGE COMMAND CREATED STATUS PORTS NAMESbdda2408484a docker-restful-demo:latest "java -cp target/lib 31 seconds ago Up 29 seconds 0.0.0.0:9998->9998/tcp fervent_swartz
检查 Docker 容器内的服务是否已经启动:
登录到 Docker 容器:
docker exec -i -t bdda2408484a bash
查看服务端口信息
$ ss -aNetid State Recv-Q Send-Q Local Address:Port Peer Address:Portnl UNCONN 0 0 rtnl:kernel *nl UNCONN 4352 0 tcpdiag:ss/92 *nl UNCONN 768 0 tcpdiag:kernel *nl UNCONN 0 0 6:kernel *nl UNCONN 0 0 10:kernel *nl UNCONN 0 0 12:kernel *nl UNCONN 0 0 15:kernel *nl UNCONN 0 0 16:kernel *u_str ESTAB 0 0 * 9590 * 0tcp LISTEN 0 128 ::ffff:127.0.0.1:9998 :::*
测试接口
$ curl -i http://localhost:9998/v1/stacksHTTP/1.1 200 OKContent-Type: application/jsonDate: Tue, 23 Jun 2015 07:51:15 GMTContent-Length: 163[{"id":1,"groupId":"javax.servlet","artifactId":"javax.servlet-api","version":"3.1.0"},{"id":2,"groupId":"com.google.guava","artifactId":"guava","version":"18.0"}]
退出 Docker 容器
exit
如果你使用的是 boot2docker, 需要拿到 boot2docker 虚拟机的IP
$ boot2docker ip192.168.59.103
调用远程接口
$ curl http://192.168.59.103:9998/v1/stackscurl: (7) Failed to connect to 192.168.59.103 port 9998: Connection refused
如果遇到上面的错误,我们可以通过2种方式去解决它:
方法1: 将程序绑定全零IP的端口
检查 Docker 容器绑定的端口:
$ docker port bdda2408484a9998/tcp -> 0.0.0.0:9998
我们看到的是 9998 这个端口绑定在 0.0.0.0 上,这时需要把 Jersey 容器的 URI 改成 0.0.0.0 就可以,像这样:
final URI uri = UriBuilder.fromUri("http://localhost/").port(9998).build();--->final URI uri = UriBuilder.fromUri("http://0.0.0.0/").port(9998).build();
方法2: 将程序绑定到非回路的IP端口上
查看 Docker 容器 IP 地址的方法:
boot2docker sshdocker inspect --format '{{.NetworkSettings.IPAddress}}' $container_id
使用 Java 接口获取本机的非回路IP地址:
final URI uri = UriBuilder.fromUri("http://localhost/").port(9998).build();--->InetAddress inetAddress = localInet4Address();String host = "0.0.0.0";if (inetAddress != null) { host = inetAddress.getHostAddress();}final URI uri = UriBuilder.fromUri("http://" + host + "/").port(9998).build(); private static InetAddress localInet4Address() throws SocketException { Enumeration<NetworkInterface> networkInterfaces = NetworkInterface.getNetworkInterfaces(); while (networkInterfaces.hasMoreElements()) { NetworkInterface networkInterface = networkInterfaces.nextElement(); Enumeration<InetAddress> inetAddresses = networkInterface.getInetAddresses(); while (inetAddresses.hasMoreElements()) { InetAddress inetAddress = inetAddresses.nextElement(); if (!inetAddress.isLoopbackAddress() && inetAddress instanceof Inet4Address) { return inetAddress; } } } return null;}
使用上面的任何一种方法,服务都能正常调用:
$ curl -i http://192.168.59.103:9998/v1/stacksHTTP/1.1 200 OKContent-Type: application/jsonDate: Tue, 23 Jun 2015 07:53:24 GMTContent-Length: 163[{"id":1,"groupId":"javax.servlet","artifactId":"javax.servlet-api","version":"3.1.0"},{"id":2,"groupId":"com.google.guava","artifactId":"guava","version":"18.0"}]
在撰写此文的时候,我使用的是 DaoColud 的镜像在做加速。 具体步骤请查看 DaoColud Mirror 文档。
如果你在 Windows 上使用 Boot2Docker, 可以按照下列步骤设置你的 Docker 镜像仓库:
boot2docker sshsudo suecho "EXTRA_ARGS=\"--registry-mirror=http://98bc3dca.m.daocloud.io\"" >> /var/lib/boot2docker/profileexitboot2docker restart
联系客服