在互聯網早期的至關長一段時間內,WEB應用都是」單體應用(monolithic)「。也就是說全部的API和前端展現層代碼都被封裝在一個獨立的、自給自足的應用當中。業務邏輯,校驗,數據獲取及計算,持久化,安全,UI都封裝成一個大的包,部署在應用服務器或者web服務器上,好比說Tomcat, Apache或者Microsoft IIS。這個方法過去有效,將來也仍將有效,只不過當你的應用到達必定規模以後,就會面臨諸多挑戰:前端
固然還會碰到其它問題,但這些就已經夠讓開發人員、項目經理、運維人員頭疼的了。長久以來,你們不得不去處理這些事情。java
微服務並非萬能的,但在許多場景下仍是很是有用的。咱們已經介紹了「爲何」須要微服務,如今來介紹下」如何「實現微服務。react
目前市面上已經有很多微服務框架了,再造一個新的彷佛沒這個必要,不過Oracle還真就這麼作了,這個項目即是Helidon。光看項目名字你可能就知道Oracle爲何要建立這個項目了:Helidon在希臘語中是燕子的意思——一種小巧、靈活的鳥類,它們自然就適合在雲端翱翔。所以,這個項目的發起人應該是想開發出一款無需應用服務器且能被用於Java SE應用的輕量級框架。web
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開始,來了解一下這個框架。docker
新工具的學習就是在摸着石頭過河,不過Helidon不存在這個問題。只須要安裝一些必要的依賴軟件(JDK 8+,Maven 3.5+)就能夠開始使用了。使用Docker或者Kubernetes的話,能讓容器的建立和部署更加容易。那還須要安裝Docker 18.02或更新的版本,以及Kubernetes 1.7.4+。(可使用Minikube或Docket Desktop在桌面操做系統上運行你的Kubernetes集羣)。編程
確認下軟件的版本:api
$ 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以及健康度。強烈推薦閱讀下項目文檔(https://helidon.io/docs/latest/#/about/01_introduction)來了解更多特性。
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將是你的不二選擇。