全部产品

Java 函数入口

更新时间:2019-07-04 06:37:53

在函数计算服务使用 Java 编程,需要定义一个 Java 函数作为入口。Java 运行环境根据是否支持 HTTP 触发器分为 普通函数入口HTTP 触发器函数入口 两种函数入口,为函数设置 HTTP 触发器后的函数入口形式会不同,这是为了方便处理发来的 HTTP request 请求并允许用户返回自定义 HTTP header。其中 普通函数入口 又分为 处理函数入口initializer入口

本文对 普通函数入口HTTP 触发器函数入口 进行详细介绍:

普通函数入口

处理函数入口

用户在使用 Java 编程时,必须要实现函数计算提供的接口类,对于普通函数入口目前有 2 个预定义接口可选择:

  1. StreamRequestHandler以流的方式接受调用输入event和返回执行结果,用户需要从inputStream中读取调用函数时的输入,处理完成后把函数执行结果写入到outputStream中来返回。

  2. PojoRequestHandler通过泛型的方式,用户可以自定义输入和输出的类型,但是它们必须是POJO类型。

StreamRequestHandler

一个最简单的处理函数定义如下:

  1. package example;
  2. import com.aliyun.fc.runtime.Context;
  3. import com.aliyun.fc.runtime.StreamRequestHandler;
  4. import java.io.IOException;
  5. import java.io.InputStream;
  6. import java.io.OutputStream;
  7. public class HelloFC implements StreamRequestHandler {
  8. @Override
  9. public void handleRequest(
  10. InputStream inputStream, OutputStream outputStream, Context context) throws IOException {
  11. outputStream.write(new String("hello world").getBytes());
  12. }
  13. }
1. 包名/类名

包名和类名可以是任意的,但是需要与创建函数时的 “Handler” 字段相对应:上面的例子包名是 “example”,类名是 “HelloFC”,那么创建函数时指定的 Handler 为 example.HelloFC::handleRequest,”Handler” 的格式为 {package}.{class}::{method}

2. 实现的接口

用户的代码中必须要实现函数计算预定义的接口。上面的例子中实现了StreamRequestHandler,其中的 inputStream 参数是调用函数时传入的数据,outputStream 用于返回函数的执行结果。

3. context参数

context 参数中包含一些函数的运行时信息(例如 request id/临时 AK 等)。其类型是 com.aliyun.fc.runtime.Context

4. 返回值

实现StreamRequestHandler接口的函数通过outputStream参数返回执行结果

5. 引入接口库

其中用到的com.aliyun.fc.runtime这个包的依赖可以通过下面的 pom.xml 引用:

  1. <dependency>
  2. <groupId>com.aliyun.fc.runtime</groupId>
  3. <artifactId>fc-java-core</artifactId>
  4. <version>1.2.1</version>
  5. </dependency>

通过 maven 仓库 可以获取fc-java-core最新的版本号。

在创建函数之前,用户需要将代码及其依赖的 fc-java-core 打包成 jar。关于如何打包,请参见 Java 代码打包。 打包成 jar 后就可以使用 Fun 或者控制台上传代码,以 Fun 为例:

在项目根目录创建一个 template.yml 的文件:

  1. ROSTemplateFormatVersion: '2015-09-01'
  2. Transform: 'Aliyun::Serverless-2018-04-03'
  3. Resources:
  4. FunDemo:
  5. Type: 'Aliyun::Serverless::Service'
  6. javademo:
  7. Type: 'Aliyun::Serverless::Function'
  8. Properties:
  9. Handler: example.HelloFC::handleRequest
  10. Runtime: java8
  11. CodeUri: './example.jar'

这个 template.yml 的含义如下:声明一个名为 FunDemo 的 服务,并在这个服务下,再声明一个名为 javademo 的 函数,配置函数入口为 example.HelloFC::handleRequest,以及函数的 runtime 为 java8。并且,我们指定了 CodeUri./example.jar。在部署时,Fun 会将 CodeUri 指定的目录或文件打包上传。更多的配置规则 请参考

示例代码包是示例中的 hello world 代码打包成的 jar 包,您可以直接使用 示例代码包 进行测试。

使用 Fun 部署:

  1. fun deploy

执行成功时,会看到相关日志:

  1. using region: cn-hangzhou
  2. using accountId: ***********3557
  3. using accessKeyId: ***********r3Ra
  4. using timeout: 300
  5. Waiting for service FunDemo to be deployed...
  6. Waiting for function javademo to be deployed...
  7. Waiting for packaging function javademo code...
  8. package function javademo code done
  9. function javademo deploy success
  10. service FunDemo deploy success

然后就可以登陆控制台查看或直接调用了。

PojoRequestHandler

一个最简单的处理函数定义如下:

  1. // HelloFC.java
  2. package example;
  3. import com.aliyun.fc.runtime.Context;
  4. import com.aliyun.fc.runtime.PojoRequestHandler;
  5. public class HelloFC implements PojoRequestHandler<SimpleRequest, SimpleResponse> {
  6. @Override
  7. public SimpleResponse handleRequest(SimpleRequest request, Context context) {
  8. String message = "Hello, " + request.getFirstName() + " " + request.getLastName();
  9. return new SimpleResponse(message);
  10. }
  11. }
  1. // SimpleRequest.java
  2. package example;
  3. public class SimpleRequest {
  4. String firstName;
  5. String lastName;
  6. public String getFirstName() {
  7. return firstName;
  8. }
  9. public void setFirstName(String firstName) {
  10. this.firstName = firstName;
  11. }
  12. public String getLastName() {
  13. return lastName;
  14. }
  15. public void setLastName(String lastName) {
  16. this.lastName = lastName;
  17. }
  18. public SimpleRequest() {}
  19. public SimpleRequest(String firstName, String lastName) {
  20. this.firstName = firstName;
  21. this.lastName = lastName;
  22. }
  23. }
  1. // SimpleResponse.java
  2. package example;
  3. public class SimpleResponse {
  4. String message;
  5. public String getMessage() {
  6. return message;
  7. }
  8. public void setMessage(String message) {
  9. this.message = message;
  10. }
  11. public SimpleResponse() {}
  12. public SimpleResponse(String message) {
  13. this.message = message;
  14. }
  15. }

准备调用的输入文件:

  1. {
  2. "firstName": "FC",
  3. "lastName": "aliyun"
  4. }

使用 fcli 调用结果:

  1. >>> invk hello-java -f /tmp/a.json
  2. {"message":"Hello, FC aliyun"}
  3. >>>

initializer 入口

无论您的函数使用流式输入还是通过泛型的方式自定义输入和输出,当需要在 Java runtime 中添加 initializer 接口时,都需在原有的基础上额外实现 initializer 预定义的接口。

initializer预定义接口如下:

  1. package com.aliyun.fc.runtime;
  2. import java.io.IOException;
  3. public interface FunctionInitializer {
  4. /**
  5. * The interface to handle a function compute initialize request
  6. *
  7. * @param context The function compute initialize environment context object.
  8. * @throws IOException IOException during I/O handling
  9. */
  10. void initialize(Context context) throws IOException;
  11. }

一个简单的流式输入的函数和 initializer 结合的 demo 如下:

  1. package aliyun.serverless.test.example;
  2. import com.aliyun.fc.runtime.Context;
  3. import com.aliyun.fc.runtime.FunctionComputeLogger;
  4. import com.aliyun.fc.runtime.StreamRequestHandler;
  5. import com.aliyun.fc.runtime.FunctionInitializer;
  6. import java.io.IOException;
  7. import java.io.InputStream;
  8. import java.io.OutputStream;
  9. public class InitializerAndStreamRequest implements StreamRequestHandler, FunctionInitializer {
  10. @Override
  11. public void initialize(Context context) {
  12. FunctionComputeLogger logger = context.getLogger();
  13. logger.debug(String.format("RequestID is %s %n", context.getRequestId()));
  14. }
  15. @Override
  16. public void handleRequest(InputStream input, OutputStream output, Context context) throws IOException {
  17. FunctionComputeLogger logger = context.getLogger();
  18. logger.debug(String.format("RequestID is %s %n", context.getRequestId()));
  19. output.write(new String("hello world!").getBytes());
  20. output.flush();
  21. }
  22. }

针对 InitializerAndStreamRequest 中新增加的 initialize 方法即是 initializer 接口,特性如下:

1. 包名/类名

initializer 所属包名和类名和处理函数一致,都可以是任意的。”initializer” 的格式同为 {package}.{class}::{method},与 “handler” 不同的是 handler 中的 method 为 handleRequest,initializer 的 method 为 initialize。根据定义可知此示例的 initializer 为 aliyun.serverless.test.example.InitializerAndStreamRequest::initialize

2. 实现的接口

用户的代码中必须要实现函数计算预定义的接口。上面示例中 initializer 接口实现了FunctionInitializer,initializer 接口只有一个 context 参数。

3. context参数

context 参数中包含一些函数的运行时信息(例如 request id/临时 AK 等)。其类型是 com.aliyun.fc.runtime.Context

4. 返回值

实现FunctionInitializer接口的函数无返回结果。

泛型的方式输入的函数和 initializer 结合的 demo 如下:

  1. package aliyun.serverless.test.example;
  2. import com.aliyun.fc.runtime.Context;
  3. import com.aliyun.fc.runtime.PojoRequestHandler;
  4. import com.aliyun.fc.runtime.FunctionInitializer;
  5. import com.aliyun.fc.runtime.FunctionComputeLogger;
  6. public class InitializerAndPojoRequest implements FunctionInitializer,PojoRequestHandler<SimpleRequest, SimpleResponse> {
  7. @Override
  8. public void initialize(Context context) {
  9. FunctionComputeLogger logger = context.getLogger();
  10. logger.debug(String.format("RequestID is %s %n", context.getRequestId()));
  11. }
  12. @Override
  13. public SimpleResponse handleRequest(SimpleRequest request, Context context) {
  14. FunctionComputeLogger logger = context.getLogger();
  15. logger.debug(String.format("RequestID is %s %n", context.getRequestId()));
  16. String message = "Hello, " + request.getFirstName() + " " + request.getLastName();
  17. return new SimpleResponse(message);
  18. }
  19. }

HTTP 触发器函数入口

关于 HTTP 触发器 的使用请参考:HTTP 触发器

HTTP 触发器函数接口

函数计算提供基于 Servlet 协议的 HTTP 触发器入口,具体接口形式如下:

  1. public interface HttpRequestHandler {
  2. /**
  3. * The entrance function of fc http trigger
  4. * @param request The servlet request
  5. * @param response The servlet response
  6. * @param context The fc context
  7. * @throws IOException If IO exception happened
  8. * @throws ServletException If servlet exception happened
  9. */
  10. public void handleRequest(HttpServletRequest request, HttpServletResponse response, Context context) throws IOException, ServletException;
  11. }

使用 HTTP 触发器需将 fc-java-core 库版本升级到 1.3.0 及以上。

HTTP 触发器的简单示例

  1. package com.aliyun.fc.example;
  2. import java.io.IOException;
  3. import java.io.OutputStream;
  4. import javax.servlet.ServletException;
  5. import javax.servlet.http.HttpServletRequest;
  6. import javax.servlet.http.HttpServletResponse;
  7. import com.aliyun.fc.runtime.Context;
  8. import com.aliyun.fc.runtime.HttpRequestHandler;
  9. public class Hello implements HttpRequestHandler {
  10. public void handleRequest(HttpServletRequest request, HttpServletResponse response, Context context)
  11. throws IOException, ServletException {
  12. String requestPath = (String) request.getAttribute("FC_REQUEST_PATH");
  13. String requestURI = (String) request.getAttribute("FC_REQUEST_URI");
  14. String requestClientIP = (String) request.getAttribute("FC_REQUEST_CLIENT_IP");
  15. response.setStatus(200);
  16. response.setHeader("header1", "value1");
  17. response.setHeader("header2", "value2");
  18. String body = String.format("Path: %s\n Uri: %s\n IP: %s\n", requestPath, requestURI, requestClientIP);
  19. OutputStream out = response.getOutputStream();
  20. out.write((body).getBytes());
  21. out.flush();
  22. out.close();
  23. }
  24. }
1. HttpServletRequest

函数计算 HTTP 触发器的接口直接使用标准的 Servlet 协议。 用户的请求会封装成HttpServletRequest对象,用户请求参数、请求header等均可通过此对象获取。 除此之外,函数计算在HttpServletRequest中预封装了一些属性, 可通过 getAttribute 方法来获取, 具体包括:

  • FC_REQUEST_PATH

    获取请求的 Path

  • FC_REQUEST_URI

    获取请求的 URI

  • FC_REQUEST_CLIENT_IP

    获取请求的 ClientIP

2. HttpServletResponse

用户可通过标准的HttpServletResponse协议对象来返回响应 header 和 body.

3. context参数

context 参数中包含一些函数的运行时信息(例如 request id/临时 AK 等)。其类型是 com.aliyun.fc.runtime.Context

HTTP 触发器支持传统web应用

基于 Servlet 协议的传统 web 应用能很方便的迁移到函数计算平台,目前支持的主流框架包括 Spring 、 SpringBoot 、 Struts2 等。以下示例如何通过函数计算提供的 fc-java-common 库来加载 web 应用.

  1. 打包您的 web 工程,生成 demo.war 包

  2. 上传 demo.war 包到 OSS,比如为 demo-bucket 下的 demo.war

  3. 创建函数,并设置函数初始化入口和函数入口。

    • 函数示例代码如下:

      1. package com.aliyun.fc.example;
      2. import java.io.IOException;
      3. import javax.servlet.ServletException;
      4. import javax.servlet.http.HttpServletRequest;
      5. import javax.servlet.http.HttpServletResponse;
      6. import com.aliyun.fc.runtime.Context;
      7. import com.aliyun.fc.runtime.FcAppLoader;
      8. import com.aliyun.fc.runtime.FunctionComputeLogger;
      9. import com.aliyun.fc.runtime.FunctionInitializer;
      10. import com.aliyun.fc.runtime.HttpRequestHandler;
      11. public class HelloWeb implements FunctionInitializer, HttpRequestHandler {
      12. private FcAppLoader fcAppLoader = new FcAppLoader();
      13. private String ossEndPoint = "YourOSSEndPoint";
      14. private String bucket = "YourOSSBucket";
      15. private String key = "YourWarName";
      16. private String userContextPath = "/2016-08-15/proxy/{YourServiceName}/{YourFunctionName}";
      17. @Override
      18. public void initialize(Context context) throws IOException {
      19. FunctionComputeLogger fcLogger = context.getLogger();
      20. fcAppLoader.setFCContext(context);
      21. // Load code from OSS
      22. fcAppLoader.loadCodeFromOSS(ossEndPoint, bucket, key);
      23. // Init webapp from code
      24. fcAppLoader.initApp(userContextPath, HelloWeb.class.getClassLoader());
      25. }
      26. @Override
      27. public void handleRequest(HttpServletRequest request, HttpServletResponse response, Context context)
      28. throws IOException, ServletException {
      29. try {
      30. fcAppLoader.forward(request, response);
      31. } catch (Exception e) {
      32. e.printStackTrace();
      33. }
      34. }
      35. }
    • 引用 Maven 库

      1. <dependency>
      2. <groupId>com.aliyun.fc.runtime</groupId>
      3. <artifactId>fc-java-core</artifactId>
      4. <version>1.3.0</version>
      5. </dependency>
      6. <dependency>
      7. <groupId>com.aliyun.fc.runtime</groupId>
      8. <artifactId>fc-java-common</artifactId>
      9. <version>1.0.0</version>
      10. </dependency>

Java 代码打包

使用 maven 打包 jar

  1. 在 pom.xml 中添加添加 maven-assembly-plugin 插件

    1. <build>
    2. <plugins>
    3. <plugin>
    4. <artifactId>maven-assembly-plugin</artifactId>
    5. <version>3.1.0</version>
    6. <configuration>
    7. <descriptorRefs>
    8. <descriptorRef>jar-with-dependencies</descriptorRef>
    9. </descriptorRefs>
    10. <appendAssemblyId>false</appendAssemblyId> <!-- this is used for not append id to the jar name -->
    11. </configuration>
    12. <executions>
    13. <execution>
    14. <id>make-assembly</id> <!-- this is used for inheritance merges -->
    15. <phase>package</phase> <!-- bind to the packaging phase -->
    16. <goals>
    17. <goal>single</goal>
    18. </goals>
    19. </execution>
    20. </executions>
    21. </plugin>
    22. <plugin>
    23. <groupId>org.apache.maven.plugins</groupId>
    24. <artifactId>maven-compiler-plugin</artifactId>
    25. <configuration>
    26. <source>1.8</source>
    27. <target>1.8</target>
    28. </configuration>
    29. </plugin>
    30. </plugins>
    31. </build>
  2. 打包

    1. mvn package

执行完毕后,生成的 jar 会被存放在 target 目录下。

使用 maven 打包 jar,并将依赖以 jar 的形式存放在 /lib 目录中

随着项目依赖的增加,jar 的体积会变得越来越大。而用户上传的 jar 或者 zip 代码,在执行前,首先会被解压缩,然后才会被加载、执行。因此在刚才的两种实现中,存在的一个问题是我们打包的 jar 中包含了大量的 class 文件,这无疑会增加解压缩的时间,进而增加函数的首次启动时间。

一个更好的实践是将第三方依赖以 jar 的形式,存放于 /lib 目录。

这里提供一种使用 maven-dependency-plugin 实现的方案:

  1. <plugin>
  2. <groupId>org.apache.maven.plugins</groupId>
  3. <artifactId>maven-dependency-plugin</artifactId>
  4. <executions>
  5. <execution>
  6. <id>copy-dependencies</id>
  7. <phase>prepare-package</phase>
  8. <goals>
  9. <goal>copy-dependencies</goal>
  10. </goals>
  11. <configuration>
  12. <outputDirectory>${project.build.directory}/classes/lib</outputDirectory>
  13. <includeScope>runtime</includeScope>
  14. </configuration>
  15. </execution>
  16. </executions>
  17. </plugin>

执行完 mvn package,打包好的 jar 的目录结构为:

  1. */*.class
  2. lib/*.jar

使用 IDEA 打包 jar

  1. 配置导出 jar 包的选项:

    java1

    java2

    java3

  2. 验证打包结果

    1. rockuw-MBP:hello-java (master) $ ls -lrth
    2. total 6520
    3. -rw-r--r-- 1 rockuw staff 3.2M Aug 31 21:03 hellofc.jar
    4. rockuw-MBP:hello-java (master) $ jar -tf hellofc.jar | head
    5. Picked up _JAVA_OPTIONS: -Duser.language=en
    6. META-INF/MANIFEST.MF
    7. example/
    8. example/HelloFC.class
    9. example/SimpleRequest.class
    10. example/SimpleResponse.class
    11. META-INF/
    12. META-INF//
    13. org/
    14. org//
    15. org/apache/