全部产品

Java 函数入口

更新时间:2019-05-07 16:02:40

在函数计算服务使用 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 后就可以使用 fcli 或者控制台上传代码,以 fcli 为例:

  1. rockuw-MBP:hello-java (master) $ ls -lrt
  2. total 16
  3. -rw-r--r-- 1 rockuw staff 7690 Aug 31 19:45 hellofc.jar
  4. >>> mkf hello-java -t java8 -h example.HelloFC::handleRequest -d ./functions/hello-java
  5. >>> invk hello-java
  6. hello world
  7. >>>

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

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/{YourServideName}/{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/