Skip to content

2.云引擎 2 网站托管

jwfing edited this page Jun 2, 2018 · 1 revision

网站托管是云引擎的一个子模块,允许你用 Java 开发一个 Web 程序,提供云函数和 Hook,还可以提供静态文件的托管和自定义的路由、绑定你自己的域名。你可以用它为你的移动应用提供一个介绍和下载页、开发一个管理员控制台或完整的网站,或者运行一些必须在服务器端运行的自定义逻辑。

如果你还不知道如何创建云引擎项目,本地调试并部署到云端,可以先阅读一下 云引擎快速入门。其他相关文档包括:

这篇文档以 Java 为例,但云引擎还支持其他多种语言,你可以选择自己熟悉的技术栈进行开发:

Java 运行环境对内存的使用较多,所以建议:

  • 示例项目 起步的应用,建议使用 512 MB 或以上规格的实例。
  • 使用 Spring Boot 的应用,建议使用 1 GB 或以上规格的实例。
  • 本地启动并模拟完成主要业务流程操作,待应用充分初始化后,根据 Java 进程内存占用量选择相应的实例规格,需要注意保留一定的余量用以应对请求高峰。
如果云引擎 [实例规格](leanengine_plan.html#选择实例规格) **选择不当**,可能造成应用启动时因为内存溢出(OOM)导致部署失败,或运行期内存溢出导致应用频繁重启。

项目骨架

你的项目需要遵循一定格式才会被云引擎识别并运行。

云引擎 Java 运行环境使用 Maven 进行构建,所以 云引擎 Java 项目必须有 $PROJECT_DIR/pom.xml 文件,该文件为整个项目的配置文件。构建完成后云引擎会尝试到 $PROJECT_DIR/target 目录下寻找可以使用的包:

  • WAR:如果项目打包成 WAR 文件,则云引擎会将其放入 Servlet 容器(当前是 Jetty 9.x)来运行。
  • JAR:如果项目打包成 JAR 文件,则云引擎会通过 java -jar <packageName>.jar 来运行。

我们建议使用示例项目做为起步,因为一些细节的开发环境的配置会让开发调试方便很多:

Java 云引擎只支持 1.8 运行环境和 war 包运行

本地运行和调试

打包成 WAR 文件的项目

首先确认项目 pom.xml 中配置了 jetty plugin,并且 web server 的端口通过环境变量 LEANCLOUD_APP_PORT 获取,具体配置可以参考我们的 示例代码

然后使用 Maven 安装依赖并打包:

mvn package

以下有几种方式可以本地启动:

命令行工具启动应用

lean up

更多有关命令行工具和本地调试的内容请参考 命令行工具使用指南

提示:相对于其他启动方式,命令行工具有 多应用管理 功能,可以方便的切换不同应用环境。

命令行设置环境变量启动

通过以下命令将云引擎运行需要的环境变量设置到当前命令行环境中,并使用 jetty 插件启动应用:

eval "$(lean env)"
mvn jetty:run

提示:命令 lean env 可以输出当前应用所需环境变量的设置语句,外层的 eval 是直接执行这些语句。

使用 Eclipse 启动应用

首先确保 Eclipse 已经安装 Maven 插件,并将项目以 Maven Project 方式导入 Eclipse 中,在 Package Explorer 视图右键点击项目,选择 Run As > Maven build...,将 Main 标签页的 Goals 设置为 jetty:run,将 Environment 标签页增加以下环境变量和相应的值:

  • LEANCLOUD_APP_ENV = development
  • LEANCLOUD_APP_ID = {{appid}}
  • LEANCLOUD_APP_KEY = {{appkey}}
  • LEANCLOUD_APP_MASTER_KEY = {{masterkey}}
  • LEANCLOUD_APP_PORT = 3000

然后点击 run 按钮启动应用。

打包成 JAR 文件的项目

使用 Maven 正常的安装依赖并打包:

mvn package

以下有几种方式可以本地启动:

命令行设置环境变量启动

通过以下命令将云引擎运行需要的环境变量设置到当前命令行环境中,并启动应用:

eval "$(lean env)"
java -jar target/{打包好的 jar 文件}

提示:命令 lean env 可以输出当前应用所需环境变量的设置语句,外层的 eval 是直接执行这些语句。

使用 Eclipse 启动应用

首先确保 Eclipse 已经安装 Maven 插件,并将项目以 Maven Project 方式导入 Eclipse 中,在 Package Explorer 视图右键点击项目,选择 Run As > Run Configurations...,选择 Application,设置 Main class: (示例项目为 cn.leancloud.demo.todo.Application),将 Environment 标签页增加以下环境变量和相应的值:

  • LEANCLOUD_APP_ENV = development
  • LEANCLOUD_APP_ID = {{appid}}
  • LEANCLOUD_APP_KEY = {{appkey}}
  • LEANCLOUD_APP_MASTER_KEY = {{masterkey}}
  • LEANCLOUD_APP_PORT = 3000

然后点击 run 按钮启动应用。

命令行工具启动应用

很抱歉,命令行工具暂不支持 JAR 项目的启动。

Web 框架

云引擎 Java 依赖 Servlet 3.1.0 ,你可以使用任何基于 Servlet 3.1.0 的 Web 框架。

LeanCloud SDK

云引擎使用 LeanEngine Java SDK 来代替 Java 存储 SDK 。前者依赖了后者,并增加了云函数和 Hook 函数的支持,因此开发者可以直接使用 LeanCloud 的存储服务 来存储自己的数据。

如果使用项目框架作为基础开发,LeanEngine Java SDK 默认是配置好的,可以根据示例程序的方式直接使用。

如果是自定义项目,则需要自己配置:

	<dependencies>
		<dependency>
			<groupId>cn.leancloud</groupId>
			<artifactId>leanengine</artifactId>
			<version>5.0.0-snapshot</version>
		</dependency>
	</dependencies>
  • 初始化:在正式使用数据存储之前,你需要使用自己的应用 key 进行初始化中间件:
import com.avos.avoscloud.internal.impl.JavaRequestSignImplementation;
import cn.leancloud.LeanEngine;

// 从 LEANCLOUD_APP_ID 这个环境变量中获取应用 app id 的值
String appId = System.getenv("LEANCLOUD_APP_ID");

// 从 LEANCLOUD_APP_KEY 这个环境变量中获取应用 app key 的值                
String appKey = System.getenv("LEANCLOUD_APP_KEY");

// 从 LEANCLOUD_APP_MASTER_KEY 这个环境变量中获取应用 master key 的值        
String appMasterKey = System.getenv("LEANCLOUD_APP_MASTER_KEY");   

LeanEngine.initialize(appId, appKey, appMasterKey);

// 如果不希望使用 masterKey 权限,可以将下面一行删除
JavaRequestSignImplementation.instance().setUseMasterKey(true);

健康监测

你的应用在启动时,云引擎的管理程序会每秒去检查你的应用是否启动成功,如果 30 秒 仍未启动成功,即认为启动失败;在之后应用正常运行的过程中,也会有定期的「健康监测」,以确保你的应用正常运行,如果健康监测失败,云引擎管理程序会自动重启你的应用。

健康检查的 URL 包括你的应用首页(/)和 Java SDK 负责处理的 /__engine/1/ping,只要 两者之一 返回了 HTTP 200 的响应,就视作成功。因此请确保你的程序使用了 Java SDK,或你的应用 首页能够正常地返回 HTTP 200 响应。

除此之外,为了支持云引擎的云函数和 Hook 功能,管理程序会使用 /1.1/functions/_ops/metadatas 这个 URL 和 Java SDK 交互,请确保将这个 URL 交给 Java SDK 处理,或 返回一个 HTTP 404 表示不使用云函数 和 Hook 相关的功能。

如果未使用 LeanEngine Java SDK,则需要自己实现该 URL 的处理,比如这样:

//健康监测 router
@WebServlet(name = "LeanEngineHealthServlet", urlPatterns = {"/__engine/1/ping"})
public class LeanEngineHealthCheckServlet extends HttpServlet {

  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp)
      throws ServletException, IOException {
    resp.setHeader("content-type", "application/json; charset=UTF-8");
    JSONObject result = new JSONObject();
    result.put("runtime", System.getProperty("java.version"));
    result.put("version", "custom");
    resp.getWriter().write(result.toJSONString());
  }
}

// 云函数列表
@WebServlet(name = "LeanEngineMetadataServlet", urlPatterns = {"/1.1/functions/_ops/metadatas"})
public class LeanEngineMetadataServlet extends HttpServlet {

  @Override
  protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException,
      IOException {
    resp.setContentType("application/json; charset=UTF-8");
    resp.getWriter().write("{\"result\":[]}");
  }
}

部署

命令行部署

在你的项目根目录运行:

lean deploy

使用命令行工具可以非常方便地部署、发布应用,查看应用状态,查看日志,甚至支持多应用部署。具体使用请参考 命令行工具指南

依赖缓存

云引擎实现了一个缓存机制来加快构建的速度,所谓构建就是指你的应用在云引擎上安装依赖的过程,在每次构建时,如果 package.json 或其他用于定义依赖的文件没有发生修改,那么就直接使用上次安装的依赖,只将新的应用代码替换上去。

如果你遇到了与依赖安装有关的问题,可以在控制台部署时勾选「下载最新依赖」,或通过命令行工具部署时添加 --no-cache 选项;依赖缓存也会因为很多原因失效,因此也不保证每次构建都可以利用上缓存。

lean deploy --no-cache

Git 部署

除此之外,还可以使用 git 仓库部署。你需要将项目提交到一个 git 仓库,我们并不提供源码的版本管理功能,而是借助于 git 这个优秀的分布式版本管理工具。我们推荐你使用 GitHubCoding 或者 OSChina 这样第三方的源码托管网站,也可以使用你自己搭建的 git 仓库(比如 Gitlab)。

你需要先在这些平台上创建一个项目(如果已有代码,请不需要选择「Initialize this repository with a README」),在网站的个人设置中填写本地机器的 SSH 公钥(以 GitHub 为例,在 Settings => SSH and GPG keys 中点击 New SSH key),然后在项目目录执行:

git remote add origin git@github.com:<username>/<repoName>.git
git push -u origin master

然后到云引擎的设置界面填写你的 Git 仓库地址,如果是公开仓库建议填写 https 地址,例如 https://github.com/<username>/<repoName>.git

如果是私有仓库需要填写 ssh 地址 git@github.com:<username>/<repoName>.git,还需要你将云引擎分配给你的公钥填写到第三方托管平台的 Deploy keys 中,以 GitHub 为例,在项目的 Settings => Deploy keys 中点击 Add deploy key。

设置好之后,今后需要部署代码时就可以在云引擎的部署界面直接点击「部署」了,默认会部署 master 分支的代码,你也可以在部署时填写分支、标签或具体的 Commit。

预备环境和生产环境

默认情况,云引擎只有一个「生产环境」,对应的域名是 {应用的域名}.leanapp.cn。在生产环境中有一个「体验实例」来运行应用。

当生产环境的体验实例升级到「标准实例」后会有一个额外的「预备环境」,对应域名 stg-{应用的域名}.leanapp.cn,两个环境所访问的都是同样的数据,你可以用预备环境测试你的云引擎代码,每次修改先部署到预备环境,测试通过后再发布到生产环境;如果你希望有一个独立数据源的测试环境,建议单独创建一个应用。

如果访问云引擎遇到「Application not Found」的错误,通常是因为对应的环境还没有部署代码。例如应用可能没有预备环境,或应用尚未发布代码到生产环境。

有些时候你可能需要知道当前云引擎运行在什么环境(开发环境、预备环境或生产环境),从而做不同的处理:

String env = System.getenv("LEANCLOUD_APP_ENV");
if (env.equals("development")) {
    // 当前环境为「开发环境」,是由命令行工具启动的
} else if (env.equals("production")) {
    // 当前环境为「生产环境」,是线上正式运行的环境
} else {
    // 当前环境为「预备环境」
}

在客户端 SDK 调用云函数时,可以通过 REST API 的特殊的 HTTP 头 X-LC-Prod 来区分调用的环境。

  • X-LC-Prod: 0 表示调用预备环境
  • X-LC-Prod: 1 表示调用生产环境
客户端 SDK 都有类似于 `setProduction` 的方法,比如 [JavaScript SDK API 的 AV.setProduction(production)](https://leancloud.github.io/javascript-sdk/docs/AV.html#.setProduction),其中 `production` 设置为 `0` 则该 SDK 将请求预备环境;设置为 `1` 将请求生产环境,默认为 `1`。

设置域名

你可以在 云引擎 > 设置 的「Web 主机域名」部分,填写一个自定义的二级域名,例如你设置了 myapp,那么你就可以通过我们的二级域名来访问你的网站了:

  • http://myapp.leanapp.cn(中国区)
  • http://myapp.avosapps.us(美国区)
DNS 可能需要等待几个小时后才能生效。

如果你的应用在 30 天内无访问且没有购买标准实例,二级域名有可能会被回收。

用户状态管理

云引擎提供了一个 EngineSessionCookie 组件,用 Cookie 来维护用户(AVUser)的登录状态,要使用这个组件可以在初始化时添加下列代码:

// 加载 cookieSession 以支持 AV.User 的会话状态
LeanEngine.addSessionCookie(new EngineSessionCookie(3600000,true));

EngineSessionCookie 的构造函数参数包括:

  • maxAge:设置 Cookie 的过期时间。

  • fetchUser是否自动 fetch 当前登录的 AV.User 对象。默认为 false。 如果设置为 true,每个 HTTP 请求都将发起一次 LeanCloud API 调用来 fetch 用户对象。如果设置为 false,默认只可以访问 AVUser.getCurrentUser()id_User 表记录的 ObjectId)和 sessionToken 属性,你可以在需要时再手动 fetch 整个用户。

  • 在云引擎方法中,通过 AVUser.getCurrentUser() 获取用户信息。

  • 在网站托管中,通过 AVUser.getCurrentUser() 获取用户信息。

  • 在后续的方法调用显示传递 user 对象。

你可以这样简单地实现一个具有登录功能的站点:

登录

@WebServlet(name = "LoginServlet", urlPatterns = {"/login"})
public class LoginServlet extends HttpServlet {


  protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException,
      IOException {
    req.getRequestDispatcher("/login.jsp").forward(req, resp);
  }


  protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException,
      IOException {
    String username = req.getParameter("username");
    String passwd = req.getParameter("password");
    try {
      AVUser.logIn(username, passwd);
      req.getRequestDispatcher("/profile").forward(req, resp);
    } catch (AVException e) {
      resp.setStatus(HttpServletResponse.SC_BAD_REQUEST);
      resp.setContentType("application/json; charset=UTF-8");
      JSONObject result = new JSONObject();
      result.put("code", e.getCode());
      result.put("error", e.getMessage());
      resp.getWriter().write(result.toJSONString());
      e.printStackTrace();
    }
  }
}

登出

@WebServlet(name = "LogoutServlet", urlPatterns = {"/logout"})
public class LogoutServlet extends HttpServlet {
  protected void doPost(HttpServletRequest req, HttpServletResponse resp) throws ServletException,
      IOException {
    AVUser user = AVUser.getCurrentUser();
    if (user != null) {
      user.logOut();
    }
    req.getRequestDispatcher("/profile").forward(req, resp);
  }
}

Profile页面

@WebServlet(name = "ProfileServlet", urlPatterns = {"/profile"})
public class ProfileServlet extends HttpServlet {

  protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException,
      IOException {
    if (AVUser.getCurrentUser() == null) {
      req.getRequestDispatcher("/login").forward(req, resp);
    } else {
      resp.setContentType("application/json; charset=UTF-8");
      JSONObject result = new JSONObject();
      result.put("currentUser", AVUser.getCurrentUser());
      resp.getWriter().write(result.toJSONString());
    }
  }
}

一个简单的登录页面(login.jsp)可以是这样:

<html>
    <head></head>
    <body>
      <form method="post" action="/login">
        <label>Username</label>
        <input name="username"></input>
        <label>Password</label>
        <input name="password" type="password"></input>
        <input class="button" type="submit" value="登录">
      </form>
    </body>
  </html>

实现常见功能

发送 HTTP 请求

云引擎 Java 环境可以使用 URL 或者是 HttpClient 等基础类 ,不过我们推荐使用 okhttp 等第三方库来处理 HTTP 请求。

    Request.Builder builder = new Request.Builder();
    builder.url(url).get();
    OkHttpClient client  = new OkHttpClient();
    Call call = client.newCall(buidler.build());
    try{
      Response response = call.execute();
    }catch(Exception e){
    }

获取客户端 IP

如果你想获取客户端的 IP,可以直接从用户请求的 HTTP 头的 x-real-ip 字段获取,示例代码如下:

EngineRequestContext.getRemoteAddress();

文件上传

托管在 云引擎 的网站项目可以直接使用内置的 LeanCloud Java SDK 的 API 文件相关的接口直接处理文件的上传。

假设前端 HTML 代码如下:

<form enctype="multipart/form-data" method="post" action="/upload">
  <input type="file" name="iconImage">
  <input type="submit" name="submit" value="submit">
</form>

接下来定义文件上传的处理函数,构建一个 Form 对象,并将 req 作为参数进行解析,会将请求中的文件保存到临时文件目录,并构造 files 对象:

@WebServlet("/upload")
@MultipartConfig
public class UploadServlet extends HttpServlet {

  @Override
  protected void doPost(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
    String description = request.getParameter("description"); // Retrieves <input type="text" name="description">
    Part filePart = request.getPart("iconImage"); // Retrieves <input type="file" name="file">
    String fileName = filePart.getSubmittedFileName();
    InputStream fileContent = filePart.getInputStream();
    // ... (do your job here)
  }
}

Session

Java 暂不支持。

CSRF Token

如果你的云引擎应用使用 Cookie 作为鉴权方式的话(例如使用 SDK 的 CookieSession 中间件),那就有受到 CSRF 攻击的风险,将会允许其他站点伪造带有正确 Cookie 的恶意请求。

业界通常使用 CSRF Token 来防御 CSRF 统计,你需要传递给客户端一个随机字符串(即 CSRF Token,可通过 Cookie 传递),客户端在每个有副作用的请求中都要将 CSRF 包含在请求正文或 Header 中,服务器端需要校验这个 CSRF Token 是否正确。

LeanCache

关于 LeanCache 的更多使用方法请看 LeanCache 使用指南

重定向到 HTTPS

为了安全性,我们可能会为网站加上 HTTPS 加密传输。我们的 云引擎 支持网站托管,同样会有这样的需求。

因此我们在 云引擎 中提供了一个新的 middleware 来强制让你的 {应用的域名}.leanapp.cn 的网站通过 https 访问,你只要这样:

LeanEngine.setHttpsRedirectEnabled(true);

部署并发布到生产环境之后,访问你的 云引擎 网站二级域名都会强制通过 HTTPS 访问。

线上环境

系统依赖

leanengine.yaml 的 systemDependencies 部分中可以指定系统依赖,类似:

systemDependencies:
  - imagemagick

目前支持的选项包括:

  • ffmpeg 一个音视频处理工具库
  • imagemagick 一个图片处理工具库
  • fonts-wqy 文泉驿点阵宋体、文泉驿微米黑,通常和 phantomjs 配合来显示中文。
  • phantomjs 一个无 UI 的 WebKit 浏览器

注意添加系统依赖将会拖慢部署速度,因此请不要添加未用到的依赖。

环境变量

云引擎平台默认提供下列环境变量供应用使用:

变量名 说明
LEANCLOUD_APP_ID 当前应用的 App Id
LEANCLOUD_APP_KEY 当前应用的 App Key
LEANCLOUD_APP_MASTER_KEY 当前应用的 Master Key
LEANCLOUD_APP_ENV 当前的应用环境:
  • 开发环境没有该环境变量,或值为 development(一般指本地开发)
  • 预备环境值为 stage
  • 生产环境值为 production
LEANCLOUD_APP_PORT 当前应用开放给外网的端口,只有监听此端口,用户才可以访问到你的服务。
LEANCLOUD_API_SERVER 访问存储服务时使用的地址(类似于 https://api.leancloud.cn)。该值会因为所在数据中心等原因导致不一样,所以使用 REST API 请求存储服务或 LeanCloud 其他服务时请使用此环境变量的值。在云引擎中不要直接使用 https://api.leancloud.cn
LEANCLOUD_AVAILABLE_CPUS 云引擎实例可用 CPU 的数量,高规格实例(如:2CPU 1024MB 内存)时该值大于 1。应用可以根据该值启动对应数量的线程或者进程。在云引擎中不要直接使用操作系统 CPU 核数,否则可能启动过多的进程导致超出实例规格而异常退出。
LEANCLOUD_APP_GROUP 云引擎实例所在的组。当使用云引擎 组管理 功能时,该值为组的名称。
LEANCLOUD_REGION 云引擎服务所在区域,值为 CNUS,分别表示国内节点和美国节点。
旧版云引擎使用的以 `LC_` 开头的环境变量(如 `LC_APP_ID`)已经被弃用。为了保证代码兼容性,`LC_` 变量在一段时间内依然有效,但未来可能会完全失效。为了避免报错,建议使用 `LEANCLOUD_` 变量来替换。

你也可以在 云引擎 > 设置 > 自定义环境变量 页面中添加自定义的环境变量。其中名字必须是字母、数字、下划线且以字母开头,值必须是字符串,修改环境变量后会在下一次部署时生效。

按照一般的实践,可以将一些配置项存储在环境变量中,这样可以在不修改代码的情况下,修改环境变量并重新部署,来改变程序的行为;或者可以将一些第三方服务的 Secret Key 存储在环境变量中,避免这些密钥直接出现在代码中。

云引擎运行环境默认提供的环境变量,无法被自定义环境变量覆盖(覆盖无效)。

负载均衡

在云引擎上,用户的请求会先经过一个负载均衡组件,再到达你的应用。负载均衡组件会处理 HTTPS 加密、对响应进行压缩等一般性工作,因此你不必在你的应用中添加 HTTPS 或 gzip 相关的功能。

负载均衡同时限制了请求不能超过 100M(包括直接上传文件到云引擎)、请求处理不得超过 60 秒,WebSocket 60 秒无数据会被断开连接。

文件系统

你可以向 /home/leanengine/tmp 目录写入临时文件,最多不能超过 1G。

云引擎每次部署都会产生一个新的容器,即使不部署系统偶尔也会进行一些自动调度,这意味着你 不能将本地文件系统当作持久的存储,只能用作临时存储。

如果你写入的文件体积较大,建议在使用后自动删除他们,否则如果占用磁盘空间超过 1G,继续写入文件可能会收到类似 Disk quota exceeded 的错误,这种情况下你可以重新部署一下,这样文件就会被清空了。

日志

在控制台的 云引擎 / 日志 中可以查看云引擎的部署和运行日志,还可以通过日志级别进行筛选。

你还可以 通过命令行工具来导出 最近七天的日志到本地文件,方便进行进一步的分析和统计。

应用的日志可以直接输出到「标准输出」或者「标准错误」,这些信息会分别对应日志的 infoerror 级别,比如下列代码会在 info 级别记录参数信息:

日志单行最大 4096 个字符,多余部分会被丢弃;日志输出频率大于 600 行/分钟,多余的部分会被丢弃。

云引擎的访问日志(Access Log)可在应用控制台中导出,通过控制台下载经过打包的日志。

时区

在云引擎的中国区系统默认使用北京时间(Asia/Shanghai),美国区默认使用 UTC 时间。

备案和自定义域名

如果需要绑定自己的域名,进入应用控制台,按照步骤填写资料即可。

国内节点绑定独立域名需要有 ICP 备案,美国节点不需要。

只有主域名需要备案,二级子域名不需要备案;如果没有 ICP 备案,请进入 应用控制台 > 账号设置 > 域名备案,按照步骤填写资料进行备案。

备案之前要求云引擎已经部署,并且网站内容和备案申请的内容一致。仅使用云引擎托管静态文件、未使用其他 LeanCloud 服务的企业用户,需要自行完成域名备案工作。

出入口 IP 地址

如果开发者希望在第三方服务平台(如微信开放平台)上配置 IP 白名单而需要获取云引擎的入口或出口 IP 地址,请进入 控制台 > 云引擎 > 设置 > 出入口 IP 来自助查询。

我们会尽可能减少出入口 IP 的变化频率,但 IP 突然变换的可能性仍然存在。因此在遇到与出入口 IP 相关的问题,我们建议先进入控制台来核实一下 IP 列表是否有变化。