全部产品
阿里云办公

Java

更新时间:2018-08-03 21:10:05

函数计算目前支持以下 Java 运行环境:

  • OpenJDK 1.8.0 (runtime = java8)

在函数计算服务使用 Java 编程,需要定义一个类并实现函数计算预定义的接口,一个最简单的函数定义如下:

  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,具体结构和使用在下面的 使用context 介绍。

  4. 返回值

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

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

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

在创建函数之前,用户需要将代码及其依赖的 fc-java-core 打包成 jar。关于如何打包,请参见 使用自定义的模块。 打包成 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 包,您可以使用 示例代码包 进行测试。

进阶使用

使用context

context 是函数计算在运行时生成的一个对象,其中包含一些运行时的信息,用户在代码中可以使用这些信息。context 的类型是 object,其定义如下,具体实现可以在 这里 找到:

  1. package com.aliyun.fc.runtime;
  2. public interface Context {
  3. public String getRequestId();
  4. public Credentials getExecutionCredentials();
  5. public FunctionParam getFunctionParam();
  6. public FunctionComputeLogger getLogger();
  7. }

可以看到 context 中包含了4个信息:

  1. RequestId: 本次调用请求的唯一 id,用户可以把它记录下来在出现问题的时候方便调查
  2. FunctionParam: 当前调用的函数的一些基本信息如函数名/函数入口/函数内存/超时时间
  3. ExecutionCredentials: 函数计算服务通过扮演用户提供的 服务角色获得的一组临时密钥,其有效时间是 5 分钟。用户可以在代码中使用它去访问相应的服务(例如 OSS),这就避免了用户把自己的 AK 信息写死在函数代码里。
  4. Logger: 函数计算封装过的 logger,见下面的 使用logging

例如下面的代码使用临时密钥,向 OSS 中上传了一个文件:

  1. package example;
  2. import com.aliyun.fc.runtime.Context;
  3. import com.aliyun.fc.runtime.Credentials;
  4. import com.aliyun.fc.runtime.StreamRequestHandler;
  5. import com.aliyun.oss.OSSClient;
  6. import java.io.ByteArrayInputStream;
  7. import java.io.IOException;
  8. import java.io.InputStream;
  9. import java.io.OutputStream;
  10. public class HelloFC implements StreamRequestHandler {
  11. @Override
  12. public void handleRequest(
  13. InputStream inputStream, OutputStream outputStream, Context context) throws IOException {
  14. String endpoint = "oss-cn-shanghai.aliyuncs.com";
  15. String bucketName = "my-bucket";
  16. Credentials creds = context.getExecutionCredentials();
  17. OSSClient client = new OSSClient(
  18. endpoint, creds.getAccessKeyId(), creds.getAccessKeySecret(), creds.getSecurityToken());
  19. client.putObject(bucketName, "my-object", new ByteArrayInputStream(new String("hello").getBytes()));
  20. outputStream.write(new String("done").getBytes());
  21. }
  22. }

使用logging

用户的函数通过 context.getLogger() 打印的内容会被收集到用户在创建 Service 时指定的 LogStore 中:

  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. context.getLogger().info("hello world");
  12. outputStream.write(new String("hello world").getBytes());
  13. }
  14. }

上面的代码输出的日志内容是:

  1. message:2017-07-05T05:13:35.920Z a72df088-f738-cee3-e0fe-323ad89118e5 [INFO] hello world

使用 context.getLogger().warncontext.getLogger().error 分别可以打包 WARN/ERROR 级别的日志。

函数接口

用户在使用 Java 编程时,必须要实现一个类,它要实现函数计算预定义的接口,目前有 2 个预定义的接口可以实现:

  1. StreamRequestHandler以流的方式接受调用输入(event)和返回执行结果,用户需要从inputStream中读取调用函数时的输入,处理完成后把函数执行结果写入到outputStream中来返回;本文最开始的例子使用的就是这个接口。

  2. PojoRequestHandler<I, O>通过泛型的方式,用户可以自定义输入和输出的类型,但是它们必须是POJO类型。下面将举例如何使用这个接口

  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. >>>

使用自定义的模块

如果用户需要使用自定义的模块,则需要在打包 jar 时,将它们与代码一起打包。这里分别使用 maven 和 IDEA 演示如何将 OSS java SDK 打包到 jar 中。

使用 maven 打包 jar

  1. 在 pom.xml 中添加 OSS java SDK:

    1. <dependencies>
    2. <dependency>
    3. <groupId>com.aliyun.fc.runtime</groupId>
    4. <artifactId>fc-java-core</artifactId>
    5. <version>1.0.0</version>
    6. </dependency>
    7. <dependency>
    8. <groupId>com.aliyun.oss</groupId>
    9. <artifactId>aliyun-sdk-oss</artifactId>
    10. <version>2.6.1</version>
    11. </dependency>
    12. </dependencies>
  2. 在 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>
  3. 打包

    1. mvn package

执行完毕后,依赖的第三方 jar 也会被一同打包到 jar 中。生成的 jar 会被存放在 target 目录下。

使用 IDEA 打包 jar

  1. 在 pom.xml 中添加 OSS java SDK:

    1. <dependencies>
    2. <dependency>
    3. <groupId>com.aliyun.fc.runtime</groupId>
    4. <artifactId>fc-java-core</artifactId>
    5. <version>1.0.0</version>
    6. </dependency>
    7. <dependency>
    8. <groupId>com.aliyun.oss</groupId>
    9. <artifactId>aliyun-sdk-oss</artifactId>
    10. <version>2.6.1</version>
    11. </dependency>
    12. </dependencies>
  2. 配置导出 jar 包的选项:

    java1

    java2

    java3

  3. 验证打包结果

    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/

使用 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

错误处理

用户的函数在执行过程如果抛出异常,那么函数计算会把异常捕获并将异常信息返回。例如下面的代码:

  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(InputStream inputStream, OutputStream outputStream, Context context) throws IOException {
  10. throw new IOException("oops");
  11. }
  12. }

调用时收到的响应为:

  1. >>> invk hello-java -f /tmp/a.json
  2. {
  3. "errorMessage" : "oops",
  4. "errorType" : "java.io.IOException",
  5. "errorCause" : "oops",
  6. "stackTrace" : [ "example.HelloFC.handleRequest(HelloFC.java:15)" ]
  7. }
  8. Error: Request id: 45dd8d90-6b78-cce3-087c-8bf4ebc6c9af. Error type: UnhandledInvocationError

发生异常时,函数调用的响应的HTTP header中会包含X-Fc-Error-Type: UnhandledInvocationError