在互联网早期的相当长一段时间内,WEB应用都是”单体应用(monolithic)“。也就是说所有的API和前端展示层代码都被封装在一个独立的、自给自足的应用当中。业务逻辑,校验,数据获取及计算,持久化,安全,UI都封装成一个大的包,部署在应用服务器或者web服务器上,比如说Tomcat, Apache或者Microsoft IIS。这个方法过去有效,未来也仍将有效,只不过当你的应用到达一定规模之后,就会面临诸多挑战:
当然还会碰到其它问题,但这些就已经够让开发人员、项目经理、运维人员头疼的了。长久以来,大家不得不去处理这些事情。
微服务并不是万能的,但在许多场景下还是非常有用的。我们已经介绍了“为什么”需要微服务,现在来介绍下”如何“实现微服务。
目前市面上已经有不少微服务框架了,再造一个新的似乎没这个必要,不过Oracle还真就这么做了,这个项目便是Helidon。光看项目名字你可能就知道Oracle为什么要创建这个项目了:Helidon在希腊语中是燕子的意思——一种小巧、灵活的鸟类,它们天然就适合在云端翱翔。因此,这个项目的发起人应该是想开发出一款无需应用服务器且能被用于Java SE应用的轻量级框架。
Helidon有两种版本:SE和MP。Helidon SE算是一个微框架(microframework),比较简单、轻量,采用了函数式编程、响应式编程的思想,运行在自带的Netty web服务器上。它比较类似于Javalin、Micronaut或者Spark Java这样的框架。而Helidon MP实现了MicroProfile的规范,采用了Java EE/Jakarta EE开发人员所熟知的注解和组件的技术,比如说JAX-RS/Jersey, JSON-P以及CDI。它和Open Liberty, Payara还有Thorntail (正式名称是 WildFly Swarm)的定位差不多。我们先从Helidon SE开始,来了解一下这个框架。
新工具的学习就是在摸着石头过河,不过Helidon不存在这个问题。只需要安装一些必要的依赖软件(JDK 8+,Maven 3.5+)就可以开始使用了。使用Docker或者Kubernetes的话,能让容器的创建和部署更加容易。那还需要安装Docker 18.02或更新的版本,以及Kubernetes 1.7.4+。(可以使用Minikube或Docket Desktop在桌面操作系统上运行你的Kubernetes集群)。
确认下软件的版本:
$ java --version $ mvn --version $ docker --version $ kubectl version --short
一旦安装完成,便能够通过Helidon提供的Maven项目模板(原型,Archetype)来快速生成一个工程。可能你对Maven Archetype还不太了解,它其实就是一些项目模板,可以用来搭建某个框架的启动工程以便快速使用。Oracle提供了两套项目模板:Helidon SE和Helidon MP各一个。
mvn archetype:generate -DinteractiveMode=false / -DarchetypeGroupId=io.helidon.archetypes / -DarchetypeArtifactId=helidon-quickstart-se / -DarchetypeVersion=0.10.2 / -DgroupId=[io.helidon.examples] / -DartifactId=[quickstart-se] / -Dpackage=[io.helidon.examples.quickstart.se]
项目模板在 Maven的中央仓库 中,在这里你可以找到最新发布的版本。前面方括号内的值是和具体项目相关的,可以根据你的需要来进行编辑。本文中的示例将使用下面的命令来创建完成:
$ mvn archetype:generate -DinteractiveMode=false / -DarchetypeGroupId=io.helidon.archetypes / -DarchetypeArtifactId=helidon-quickstart-se / -DarchetypeVersion=0.10.2 / -DgroupId=codes.recursive / -DartifactId=helidon-se-demo / -Dpackage=codes.recursive.helidon.se.demo
完成之后,一个完整的示例工程就会在新生成的目录当中了,目录名便是artifactId参数里所指定的。这是一个完整的可运行的工程,可以编译打包一下:
$ mvn package
这个命令会把所有生成的测试用例全执行一遍,并在target/libs目录下生成应用的jar包。这个框架还自带了一个内嵌的web服务器,现在你可以通过下述的命令来运行一下:
$ java -jar target/helidon-se-demo.jar
可以看到应用程序启动起来了,工作在8080端口上:
[DEBUG] (main) Using Console logging 2018.10.18 14:34:10 INFO io.netty.util.internal.PlatformDependent Thread[main,5,main]: Your platform does not provide complete low-level API for accessing direct buffers reliably. Unless explicitly requested, heap buffer will always be preferred to avoid potential system instability. 2018.10.18 14:34:10 INFO io.helidon.webserver.netty.NettyWebServer Thread[nioEventLoopGroup-2-1,10,main]: Channel '@default' started: [id: 0x3002c88a, L:/0:0:0:0:0:0:0:0:8080] WEB server is up! http://localhost:8080
但是访问根路径会报错,因为这个模板并没有在根路径下配置路由。你可以访问http://localhost:8080/greet,它会返回一个JSON格式的”Hello Wrold“的信息。
到目前为止,除了执行了几个maven命令并启动应用之外,我们没写过一行代码,却已经有了一个搭建好的完整的可运行的应用程序。当然了,未来还是要和代码打交道的,不过在这之前我们先来看下Helidon为Docker提供了什么样的支持。
我们先通过ctrl+c把程序停掉。在target目录中,我们可以看到运行mvn package命令时额外生成了一些文件。Helidon生成了一个可以用来构建Docker容器的Dockerfile,以及一个用于部署到Kubernetes的application.yaml。这两文件虽然简单,但有了它们可以很快把应用部署起来。
下面是这个demo工程的Dockerfile(为了简洁起见,授权信息就去掉了):
FROM openjdk:8-jre-alpine RUN mkdir /app COPY libs /app/libs COPY helidon-se-demo.jar /app CMD ["java", "-jar", "/app/helidon-se-demo.jar"]
也许你是第一次接触Dockerfile,在首行它声明了一个基础的镜像。这里用的是8-jre-alpine的openjdk镜像,这是基于Alpine Linux的包含了Java 8 JRE的一个非常轻量级的镜像。两行之后,Dockerfile创建了一个app目录来存储应用程序。接下来这行将libs目录中的文件拷贝到app/libs下,然后将jar也包复制到app下。最后一行告诉Docker执行java jar命令来启动应用。
我们在工程的根目录下运行下面的命令来测试一下这个Dockerfile:
(注:如果你用的是kubemini,在运行后面的docker build命令前,一定要先执行下:
eval $(minikube docker-env)
否则后面kubernetes会找不到镜像。)
$ docker build -t helidon-se-demo target
它会告诉Docker使用target目录下的Dockerfile去创建一个tag为helidon-se-demo的镜像。执行完docker builld的输出结果大概是这样的:
Sending build context to Docker daemon 5.231MB Step 1/5 : FROM openjdk:8-jre-alpine ---> 0fe3f0d1ee48 Step 2/5 : RUN mkdir /app ---> Using cache ---> ab57483b1f76 Step 3/5 : COPY libs /app/libs ---> 6ac2b96f4b9b Step 4/5 : COPY helidon-se-demo.jar /app ---> 7d2135433bcc Step 5/5 : CMD ["java", "-jar", "/app/helidon-se-demo.jar"] ---> Running in 5ab71094a72f Removing intermediate container 5ab71094a72f ---> 7e81289d5267 Successfully built 7e81289d5267 Successfully tagged helidon-se-demo:latest
运行下这个命令确认下结果是否ok:
docker images helidon-se-demo
你可以在目录下找到一个叫helidon-se-demo的容器文件。我这里生成的文件大小是88.2MB。通过下面的命令来启动这个容器:
$ docker run -d -p 8080:8080 helidon-se-demo
docker run命令加上-d开关后会在后台运行容器实例,-p开关用来指定端口。最后是要运行的镜像名,这里是helidon-se-demo。
如果想看下你的系统中有哪些容器在运行,可以使用这个命令:
$ docker ps -a
你也可以使用像 Kitematic 或 Portainer 这样的GUI工具。我个人比较喜欢Portainer,现在用它来看下运行状态,结果如图一所示。
当然你也可以访问http:localhost:8080/greet来确认下应用程序是否还在本地运行着(只不过这次它是运行在Docker里了)。
了解完Helidon对Docker的支持度后,我们再来看看它对Kubernetes支持得怎么样。先kill掉Docker容器(命令行或GUI工具都可以)。然后看一下生成的target/app.yaml文件。它的内容如下:
kind: Service apiVersion: v1 metadata: name: helidon-se-demo labels: app: helidon-se-demo spec: type: NodePort selector: app: helidon-se-demo ports: - port: 8080 targetPort: 8080 name: http --- kind: Deployment apiVersion: extensions/v1beta1 metadata: name: helidon-se-demo spec: replicas: 1 template: metadata: labels: app: helidon-se-demo version: v1 spec: containers: - name: helidon-se-demo image: helidon-se-demo imagePullPolicy: IfNotPresent ports: - containerPort: 8080 ---
这里我不再详细介绍配置细节,你可以用它来快速地将应用部署到Kubernetes中,Kubernetes则提供了容器管理和编排的能力。通过下述命令将它部署到Kubernetes集群里(同样的,也是工程根目录下执行,否则的话需要更新下app.yaml的路径):
$ kubectl create -f target/app.yaml
如果一切正常,应该能看到这样的结果:
service/helidon-se-demo created deployment.extensions/helidon-se-demo created
可以通过kubectl get deployments来确认下部署情况,kubectl get services可以用来检查服务状态:
NAME TYPE CLUSTER-IP EXTERNAL-IP PORT(S) helidon-se-demo NodePort 10.105.215.173 <none> 8080:32700/TCP
可以看到现在服务运行在32700端口上,你可以在浏览器中访问该地址确认一下。
目前为止我们已经搭建好一个应用、生成Docker容器,并且部署到了Kubernetes中——仍然没有写过一行代码。
那现在我们就换一换,来看一下代码。打开src/main/java/Main.java,看一看startServer()方法中Helidon SE是如何初始化内嵌的Netty服务器的:
protected static WebServer startServer() throws IOException { // load logging configuration LogManager.getLogManager().readConfiguration( Main.class.getResourceAsStream("/logging.properties")); // By default this will pick up application.yaml from // the classpath Config config = Config.create(); // Get web server config from the "server" section of // application.yaml ServerConfiguration serverConfig = ServerConfiguration.fromConfig(config.get("server")); WebServer server = WebServer.create(serverConfig, createRouting()); // Start the server and print some info. server.start().thenAccept(ws -> { System.out.println( "WEB server is up! http://localhost:" + ws.port()); }); // Server threads are not demon. NO need to block. Just react. server.whenShutdown().thenRun(() -> System.out.println("WEB server is DOWN. Goodbye!")); return server; }
代码中生成的注释已经解释的很清楚了,总结一下:
createRouting()方法是这样注册服务的:
private static Routing createRouting() { return Routing.builder() .register(JsonSupport.get()) .register("/greet", new GreetService()) .build(); }
这里我们注册了"/greet"服务,指向了GreetService。会看到有几个类变量通过Config从前面提到的application.yaml文件中获取配置值。
private static final Config CONFIG = Config.create().get("app"); private static String greeting = CONFIG.get("greeting").asString("Ciao");
GreetService类实现了Service接口并重写了update()方法,里面定义了子路径/greet的实现。
@Override public final void update(final Routing.Rules rules) { rules .get("/", this::getDefaultMessage) .get("/{name}", this::getMessage) .put("/greeting/{greeting}", this::updateGreeting); }
update()方法接收Routing.Rules的实例对象,Routing.Rules的方法分别对应着不同的HTTP请求——get(),post(),put(),head(),options()和trace()——还有一些比较有用的方法比如any(),它可以用来兜底,实现一些日志或安全类的功能。
这里我注册了三个endpoint:/greet/, /greet/{name}和/greet/greeting。每个endpoint都有一个指向服务方法的引用。注册成endpoint的方法接收两个参数:request和response。这样设计的话,你可以从request中获取参数,比如请求头及参数,也可以往response中设置响应头及响应体。getDefaultMessage()方法的内容如下:
private void getDefaultMessage(final ServerRequest request, final ServerResponse response) { String msg = String.format("%s %s!", greeting, "World"); JsonObject returnObject = Json.createObjectBuilder() .add("message", msg) .build(); response.send(returnObject); }
这是个非常简单的例子,但是也能看出服务方法的基本实现结构。getMessage()方法是一个动态路径参数({name}参数是在URL路径中注册进来的)的例子,你可以从URL中获取参数。
private void getMessage(final ServerRequest request, final ServerResponse response) { String name = request.path().param("name"); String msg = String.format("%s %s!", greeting, name); JsonObject returnObject = Json.createObjectBuilder() .add("message", msg) .build(); response.send(returnObject); }
http://localhost:8080/greet/todd的结果如图二所示。
下面要讲的updateGreeting()方法和getMessage()有很大的不同,需要注意的是这里只能调用Put方法而不是get,因为在update()里就是这样注册的。
private void updateGreeting(final ServerRequest request, final ServerResponse response) { greeting = request.path().param("greeting"); JsonObject returnObject = Json.createObjectBuilder() .add("greeting", greeting) .build(); response.send(returnObject); }
Helidon SE还包含很多东西,包括异常处理、静态内容、metrics以及健康度。强烈推荐阅读下 项目文档 来了解更多特性。
Helidon MP是MicroProfile规范的实现版本。如果你使用过Java EE的话应该不会觉得陌生。前面也提到,你可能会看到像JAX-RS/Jersey, JSON-P以及CDI这些常用的东西。
和Helidon SE一样,我们先通过Helidon MP的项目模板来快速创建一个工程:
$ mvn archetype:generate -DinteractiveMode=false / -DarchetypeGroupId=io.helidon.archetypes / -DarchetypeArtifactId=helidon-quickstart-mp / -DarchetypeVersion=0.10.2 / -DgroupId=codes.recursive / -DartifactId=helidon-mp-demo / -Dpackage=codes.recursive.helidon.mp.demo
看一下Main.java类,你会发现它比Helidon SE还要简单。
protected static Server startServer() throws IOException { // load logging configuration LogManager.getLogManager().readConfiguration( Main.class.getResourceAsStream("/logging.properties")); // Server will automatically pick up configuration from // microprofile-config.properties Server server = Server.create(); server.start(); return server; }
应用的定义在GreetApplication类中,它的getClasses()方法中注册了路由资源。
@ApplicationScoped @ApplicationPath("/") public class GreetApplication extends Application { @Override public Set<Class<?>> getClasses() { Set<Class<?>> set = new HashSet<>(); set.add(GreetResource.class); return Collections.unmodifiableSet(set); } }
Helidon MP中的GreetResource和Helidon SE中的GreetService的角色差不多。不过它不用单独去注册路由信息,你可以使用注解来表示endpoint、HTTP方法和content-type头。
@Path("/greet") @RequestScoped public class GreetResource { private static String greeting = null; @Inject public GreetResource(@ConfigProperty(name = "app.greeting") final String greetingConfig) { if (this.greeting == null) { this.greeting = greetingConfig; } } @Path("/") @GET @Produces(MediaType.APPLICATION_JSON) public JsonObject getDefaultMessage() { String msg = String.format("%s %s!", greeting, "World"); JsonObject returnObject = Json.createObjectBuilder() .add("message", msg) .build(); return returnObject; } @Path("/{name}") @GET @Produces(MediaType.APPLICATION_JSON) public JsonObject getMessage(@PathParam("name") final String name){ String msg = String.format("%s %s!", greeting, name); JsonObject returnObject = Json.createObjectBuilder() .add("message", msg) .build(); return returnObject; } @Path("/greeting/{greeting}") @PUT @Produces(MediaType.APPLICATION_JSON) public JsonObject updateGreeting(@PathParam("greeting") final String newGreeting) { this.greeting = newGreeting; JsonObject returnObject = Json.createObjectBuilder() .add("greeting", this.greeting) .build(); return returnObject; } }
Helidon MP和Helidon SE的区别还不止这些,但它们的目标都是一致的,即降低微服务的使用门槛。Helidon是一个功能非常强大的框架,能够帮助你快速开发微服务应用。如果你不希望使用容器技术,你也可以像部署传统jar一样去部署它。如果你的团队使用容器技术,它内建的支持能够帮忙你快速地部署到任何云上或自有的Kubernetes集群中。由于Helidon是Oracle公司开发的,因此团队后续会计划将它集成到Oracle Cloud上。如果你已经在使用Oracle Cloud部署应用,或者最近有计划要迁移到上面,那么Helidon将是你的不二选择。
英文原文链接