体验 Scala 2.12 支持的 Java 8 风格(SAM) Lambda

上一次关注 Scala 新版本特性还是在将近五年前,针对的是  Scala 2.10. 后来也一直在使用 Scala,基本上是 Scala 2.11,但对 Scala 2.11 所带来的新特性基本无知,大约有个 Macro 功能,没什么机会用上,应用 sbt 时稍有接触。还是老句老话,了解新特性最可靠的文档是每个版本的的 Release Notes, 比如 Scala 2.12.0 Release Notes.

其中 Scala 2.12 带来的主要特性在于对 Java 8 的充分支持:

  1. Scala 可以有方法实现的 trait 直接编译为带默认方法的 Java 接口
  2. Lambda 表达式无需生成相应的类,而是用到 invokedynamic 字节码指令(这个是 Java 7 加进来的新指令)
  3. 最方便的功能莫过于终于支持 Java  8 风格的 Lambda,即功能性接口的 SAM(Single Abstract Method)

Scala 的 Lambda 内部实现

这儿主要是体验 Scala 2.12 如何使用 Java 8 风格的 Lambda. 在 Scala 2.12 之前,Scala 对 Lambda 的支持是为你准备了一大堆的 trait 类,有

  1. Function0, Function1, ...... Function22 (接收多个参数,返回一个值)
  2. Product1, Product2, ...... Product22 (函数返回多个值,即 TupleX 时用的)

阅读全文 >>

Terraform 进阶 - 部署 Lambda 并创建相关资源

昨日刚刚体验了 Terraform 是一个什么鬼东西 Terraform 使用 - 从最简单例子开始,今天再进一步。将来尝试的是使用 Terraform 来部署一个 Lambda 应用,并创建相关的资源。

本例中的 Lambda 要由 Kinesis 来触发,并写数据到 S3 Bucket 中去,所以需要做的事情大致如下:

  1. 创建 IAM Role, 该 Role 要能访问 S3, Kinesis 和 CloudWatch
  2. 创建一个 Kinesis Stream (指定 Shard 数目)
  3. 创建一个 S3 Bucket
  4. 部署 Lambda (要指定能访问 S3 Bucket 的 Role, 并其他参数,如环境变量)
  5. 设置 Lambda 的 Kinesis 触发器 (指定源 Kinesis Stream 和  batchSize)

以下是 Lambda 的实现代码,从 Kinesis 读出字符串,逗号分割,第一部分作为 S3 Key, 第二部分作为文件内容写入到 S3 Bucket 中去。S3 Bucket 名称从环境变量中读取。 阅读全文 >>

AWS Lambda 按序处理同一个 Kinesis Shard 中的消息

当 AWS Lambda 由 Kinesis 消息来触发时,一个 Kinesis Shard 会相应启动一个 Lambda 实例,比如说 Kinesis Stream 有 5 个 Shards, 那同时只会启动 5 个 Lambda 实例。那么把多条消息发送到同一个 Kinesis Shard 中去,这些消息会被如何消费呢?答案是按顺消息,不管这些消息是否被不同的 Lambda 实例处理。本文就是关于怎么去理解 https://aws.amazon.com/lambda/faqs/ 的下面那段话的内容:

Q: How does AWS Lambda process data from Amazon Kinesis streams and Amazon DynamoDB Streams?
AWS Lambda 如何处理来自于 Amazon Kinesis 和 DynamoDB 的数据

The Amazon Kinesis and DynamoDB Streams records sent to your AWS Lambda function are strictly serialized, per shard. This means that if you put two records in the same shard, Lambda guarantees that your Lambda function will be successfully invoked with the first record before it is invoked with the second record. If the invocation for one record times out, is throttled, or encounters any other error, Lambda will retry until it succeeds (or the record reaches its 24-hour expiration) before moving on to the next record. The ordering of records across different shards is not guaranteed, and processing of each shard happens in parallel.
从 Kinesis 和 DynamoDB 单个 Shard 上的记录会被 Lambda 严格的按序处理。这意味着如果你送两条记录到相同的 Shard, Lambda 将会保证第一条记录成功处理后才会处理第二条记录。假如处理第一条记录时超时,或超过资源使用上限,或碰到任何错误, Lambda 将会不断重试直到成功(或记录在 24 小时后过期), 而后才会去处理下一条记录。跨 Shard 的记录不保证到达顺序,且是并行处理多个 Shard 来的记录。

可以做几个试验,下面的代码可以保证消息总是被发送到同一个 Kinesis Shard,因为 PartitionKey 参数是一个常量 阅读全文 >>

AWS Java Lambda 与环境变量

一句话概要:对 Lambda 环境变量的任何改动都会引起一次 Lambda 的冷启动,大可放心在 handleRequest(...) 方法外使用环境变量。

AWS 上 Java Lambda 应用记要 中,我学到了 Lambda 的实例是跨请求共享的,所以为使用 Lambda 配置的环境变量时曾写出了下面复杂而多余的  AWS  Lambda 代码:

public class Handler implements RequestHandler<SNSEvent, String> {

    private int threadPoolSize = getThreadPoolSizeFromEnv();
    private ExecutorService threadPool = Executors.newFixedThreadPool(threadPoolSize);

    @Override
    public String handleRequest(SNSEvent snsEvent, Context context) {

        int configuredThreadPoolSize = getThreadPoolSizeFromEnv();
        if(configuredThreadPoolSize != threadPoolSize) {
            threadPoolSize = configuredThreadPoolSize;
            threadPool = Executors.newFixedThreadPool(threadPoolSize);
        }

        return "Hello Lambda";
    }

    private int getThreadPoolSizeFromEnv() {
        return Integer.parseInt(System.getenv().getOrDefault("threadpool_size", "50"));
    }
}

这段代码看起来很在理,既然 Lambda 实例是共享的,那么在必变环境变量之后就可能不会重新初始始化实例,所以在每次的请求方法中对比如果环境变量值改动了就重新用最新的配置值来初始化线程池。然而上面的代码结结实实是多余的,真是把 Lambda 想得太简单了,如果是很多环境变量岂不是逐一判断。 阅读全文 >>

AWS Java Lambda 使用 Logback 记录日志

直接一句话:去掉 Log4J 的依赖,把  Slf4J, Logback, 和 log4j-over-slf4j 依赖加进来就行了,配置文件换成 logback.xml,这就完了,不要往下看了,都是些废话。

当我们用 Serverless 命令 sls create -t aws-java-maven -p hello-lambda 创建的示例项目中直接用的是 Log4J 日志组件,而且也没用像  Slf4j, 或 Apache Common Logging 更上一层的通用日志框架。查看了几个 AWS 本身的组件 S3, SNS, 和 Kinesis 的 SDK, 它们内部是用的 Apache Common Logging 声明的日志变量

import org.apache.commons.logging.LogFactory;
import org.apache.commons.logging.Logger;

private static final Log log = LogFactory.getLog(AmazonKinesis.class)

而我们自己的组件中通用日志组件是 Slf4j, 底层实现为 Logback, 所以我们希望在 Lambda 中使用 Logback 来写日志。

选用一个通用日志框架总是明智之举,因为一个项目经常杂糅了多种日志实现,使用 Slf4J 或 Apache Common Logging 可以把它们(Log4J, Logback, 或更多)输出到共同的目的地,并且有统一的日志输出接口。而我们认为通用日志框架还是 Slf4J 要先进些,所以我们在 Java Lambda 中的日志方案是 Slf4J + Logback,还需要把 Log4J 的日志桥接到 Slf4J 上来,再经由 Logback 输出。

回到前面创建的 hello-lambda 项目,看其中怎么用的 Log4J,先瞧瞧 pom.xml 文件怎么引入的 Log4J, 它是间接通过一个 AWS 定义的 Log4J Appender 引入的 阅读全文 >>

AWS 上 Java Lambda 应用记要

AWS 的 Lambda 给了那些不想自己管理 EC2 服务器和配置负载人员很大的便利,所以 Lambda 被描述为 Serverless。真正的只关注业务就行,怎么调度,同时有多少个实例运行交给亚马逊去处理就是了。运行 Lambda 的环境也是亚马逊内部的 EC2 服务器,镜像是 Amazon Linux, 所以如果想运行系统命令,那是 Linux 的。Lambda 支持多种语言 Node.js, Python, C#(.net core), 还有 Java 8,我们就选择了 Java 8, 一开始还担心它与别的语言比起来会多大劣势,其实不然。而且所谓的 Java 8, 并非单指 Java 语言,而是指 JVM 平台,所以也可以用 Scala, Clojure, Groovy, Kotlin 来写。

Java 与脚本语言如 Node.js, Python 相比给人一个明显的感觉是启动慢,还有人用统计数据来比划 AWS Lambda cold start(pseudeo-)benchmark. 不过真不用担心,人家说的是冷启动,也就发生在部署后第一次执行启动会比较慢。要是我们的 Lambda 经常被调用,或每天触发比较集中,Lambda 在任务到来之前处理待续状态,就不会有冷启动的耗时过程。或者是每次任务要执行 3 分钟左右,又何必在乎毫秒级的冷启动时间。

说到底就是别理会下面的数据

  • 20ms startup time for Python ~ $0.041675
  • 40ms startup time for Node.js ~ $0.08335
  • 80ms startup time for Java ~ $0.1667

Lambda 实例重用

Java 的 Lambda 就是一个微服务,在首次触发时微服务冷启动有些慢,但一旦启动之后就可以用这个微服务实例接受后续的请求,只有在比较长的一段时间内未被触发 AWS 才会把这个微服务杀掉。 阅读全文 >>

Java8 Lambda 表达式与 Checked Exception

当我们在使用 Java 8 的 Lambda 表达式时,表达式内容需要抛出异常,也许还会想当然的让当前方法再往外抛来解决编译问题,如下面的代码

main() 方法抛出 Exception 还是不解决决编译错误,仍然提示 "Unhandled exception: java.io.FileNotFoundException"。

因为我们可能保持着惯性思维,忽略了 Lambda 本身就是一个功能性接口方法的实现,所以把上面的代码还原为匿名类的方式

那么对于上面那种情况应该如何处理呢? 阅读全文 >>

Java 8 返回集合中第一个匹配的元素

在 Java 8 之前如果我们要找到集合中第一个匹配元素,要使用外部循环,如下面方法 findFirstMatch() 如果找到一个大于 3 的数字立即返回它,否则返回 null

  public Integer findFirstMatch() {
    List<Integer> integers = Arrays.asList(1, 4, 2, 5, 6, 3);
    for(int i: integers) {
      if(i > 3) return i;
    }
    return null;
  }

因为在 for 循环中找到第一个大于 3 的数字是 4, 并且立即返回,所以不管集合 integers 再大,也不会遍历整个集合。

注:不要纠结于上面示例方法的实际用途,实际上集体和匹配条件都该通过参数传入方法的,这里只作演示循环。

那么我们来到 Java 8 之后用 Stream API 该如何实现,翻遍了 Stream API, 能过滤元素的操作也就是 filter 方法,于是尝试这样的写法 阅读全文 >>

Java 8 Lambda 捕获外部变量

可能会把捕获外部变量的 Lambda 表达式称为闭包,那么 Java 8 的 Lambda 可以捕获什么变量呢?

  1. 捕获实例或静态变量是没有限制的(可认为是通过 final 类型的局部变量 this 来引用前两者)
  2. 捕获的局部变量必须显式的声明为 final 或实际效果的的 final 类型

回顾一下我们在 Java 8 之前,匿名类中如果要访问局部变量的话,那个局部变量必须显式的声明为  final,例如下面的代码放在 Java 7 中是编译不过的

java7-annymous-capture-local-variable

Java 7 要求 version 这个局部变量必须是 final  类型的,否则在匿名类中不可引用。 阅读全文 >>

Java 8 Lambda 写法与简化

Java 8 的 Lambda 表达式的实现方式还是基于已有的字节码指令,由 Lambda 表达式的方法签名结合上下文并经由 SAM 推断出正确的类型来。Java 8 的 Lambda 完整书写格式是

(type parameter1 [type parameter2, ...type parametern]) -> { statements here }

这种标准格式所表示的就是方法签名。

虽不及其他语言的 Lambda 表达式,像 Swift, Scala 可省略参数部分,可用默认参数名 $0, $1, 或 _, 但 Java 8 的 Lambda 还是可以进行酌情简化

  1. 参数类型总是可省略   --     (x, y) -> { x + y; }
  2. 参数为一个时,参数括号可省略  --    x -> { System.out.println(x); }
  3. 语句为一条时,可省略大括号, 并且语句后不要分号 --  x -> System.out.println(x)
  4. 上面更进一步,如果是单条 return 语句,必须把 return 关键字去掉  --  x -> "Hello " + x
  5. 就差一点,参数部分总是不能省,无参必须写成 () -> System.out.println("hi")
  6. Java 8 中若要近似的实现无参数部分写法,那就是方法引用了 -- System.out::println

阅读全文 >>