通过手动调用构造函数来创建 props 对象是否安全且值得推荐?

Is creating a props object by calling a constructor manually safe and recommended?

我正在尝试熟悉 Akka actors,但我无法解决这两个问题: 首先,如 here 所述,闭包会导致序列化问题。下面的示例包含一个不可序列化的 Props 对象,因为它关闭了一个不可序列化的对象:

case class Helper(name: String)

object MyNonserializableObject {

   val helper = Helper("the helper")

   val props7 = Props(new MyActor(helper))
}

所以建议不要创建这样的Actor。上面的答案与 Akka 文档 dangerous variants 有关。 另一方面,当我们处理 value 类 作为构造函数参数时 Akka 文档 recommends 通过手动调用构造函数来创建道具 props3下面的代码是一个例子:

class Argument(val value: String) extends AnyVal

class ValueClassActor(arg: Argument) extends Actor {
  def receive = { case _ => () }
}

object ValueClassActor {
  def props1(arg: Argument) = Props(classOf[ValueClassActor], arg) // fails at runtime
  def props2(arg: Argument) = Props(classOf[ValueClassActor], arg.value) // ok
  def props3(arg: Argument) = Props(new ValueClassActor(arg)) // ok
}

这两个概念在我看来是矛盾的。 顺便说一句,由于我的排名,我无法将此问题创建为评论。

如果您了解 JVM 的工作原理,这将更容易理解。如果您使用 classOf[ValueClassActor] 和参数列表实例化对象,JVM 必须从 Class 对象中提取 Constructor,然后使用 Java 反射 API.

与此同时,如果您看一下 AnyVal 是什么,您会看到 class 需要 AnyVal

class Argument(val value: String) extends AnyVal

class ValueClassActor(arg: Argument)

编译为:

Compiled from "test.scala"
public class ValueClassActor {
  public ValueClassActor(java.lang.String);
    Code:
       0: aload_0
       1: invokespecial #14                 // Method java/lang/Object."<init>":()V
       4: return
    LineNumberTable:
      line 3: 0
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0       5     0  this   LValueClassActor;
          0       5     1   arg   Ljava/lang/String;
}

所以 Argument 类型只存在于编译时(好吧,大多数情况下,sometimes Scala instantiates it),如果你想调用 JVM 实际看到的构造函数,你需要传递 String代替 Argument。这就是为什么你有这种行为的原因:

  def props1(arg: Argument) = Props(classOf[ValueClassActor], arg) // fails at runtime
  def props2(arg: Argument) = Props(classOf[ValueClassActor], arg.value) // ok

为避免处理此问题,您可以使用不依赖运行时反射的Props creator

def apply[T <: Actor](creator: => T)(implicit arg0: ClassTag[T]): Props

危险吗?文档说:

CAVEAT: Required mailbox type cannot be detected when using anonymous mixin composition when creating the instance. For example, the following will not detect the need for DequeBasedMessageQueueSemantics as defined in Stash:

'Props(new Actor with Stash { ... })

Instead you must create a named class that mixin the trait, e.g. class MyActor extends Actor with Stash.

这意味着只要您简单地使用 named class 并且只向它提供参数而不在匿名子 classes 上使用任何 minxins,您就消除了一个潜在的问题。为避免闭包问题,您可以执行 exactly said in documentation 并在伴随对象中创建 Prop 构造。

问题是,当您尝试创建 Prop 时,如果您通过 Internet 将它发送到应用程序的另一部分,如果您有,它可能会被序列化。阿卡集群。如果您尝试序列化一个函数(这里:匿名 Function 即 `new ValueClassActor(arg)),如果您尝试序列化它,它会获取它的整个闭包。由于 Java 的工作方式,此函数将有一个指向创建它的父对象的指针。

如果你有

class Foo(s: => String)

object Foo {
  def hello: Foo = new Foo("test") // "test" is by-name so it has closure
}

你看看生成的字节码你会发现有

Compiled from "foo.scala"
public class Foo {
  public static Foo hello();
    Code:
       0: getstatic     #16                 // Field Foo$.MODULE$:LFoo$;
       3: invokevirtual #18                 // Method Foo$.hello:()LFoo;
       6: areturn

  public Foo(scala.Function0<java.lang.String>);
    Code:
       0: aload_0
       1: invokespecial #25                 // Method java/lang/Object."<init>":()V
       4: return
    LineNumberTable:
      line 3: 0
      line 1: 4
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0       5     0  this   LFoo;
          0       5     1     s   Lscala/Function0;
}

Compiled from "foo.scala"
public final class Foo$ {
  public static final Foo$ MODULE$;

  public static {};
    Code:
       0: new           #2                  // class Foo$
       3: dup
       4: invokespecial #17                 // Method "<init>":()V
       7: putstatic     #19                 // Field MODULE$:LFoo$;
      10: return
    LineNumberTable:
      line 3: 0

  public Foo hello();
    Code:
       0: new           #23                 // class Foo
       3: dup
       4: invokedynamic #44,  0             // InvokeDynamic #0:apply:()Lscala/Function0;
       9: invokespecial #47                 // Method Foo."<init>":(Lscala/Function0;)V
      12: areturn
    LineNumberTable:
      line 4: 0
    LocalVariableTable:
      Start  Length  Slot  Name   Signature
          0      13     0  this   LFoo$;

  public static final java.lang.String $anonfun$hello();
    Code:
       0: ldc           #50                 // String test
       2: areturn
    LineNumberTable:
      line 4: 0
}

这意味着:

  • 当您调用 Foo.hello 并创建一个 lambda new Foo("test") 时,该函数的闭包只是伴随对象实例
  • 伴随对象正在实现 Serializable
  • 因此它满足函数的闭包是可序列化的要求

object MyNonserializableObject 是开箱即用的解释的捷径 objects 可序列化的,你必须对它们做一些奇怪的事情才能使它们不可序列化。例如。如果你这样做了

trait Bar {

  object Baz {
    def hello: Foo = new Foo("test")  // "test" is by-name so it has closure
  }
}

闭包将持有对 Baz 的引用,后者将持有对 Bar 的引用,如果扩展 Bar 的内容不可序列化,则闭包也不会。但是,如果您将在顶级 object 中生成您的 lambda(不嵌套在其他 class 等中),那么您的闭包只能依赖于可序列化的东西(因为 object 自己有空构造函数并实现 Serializable 接口),因此可以自己序列化。

同样的原则适用于 Props 和别名参数。如果您在顶层(或以其他方式保证可序列化)的伴生对象中使用别名参数创建一个 Prop,那么闭包也将是可序列化的,并且使用是安全的。就像文档推荐说的那样。

长话短说:

class ValueClassActor(arg: Argument) extends Actor {
  def receive = { case _ => () }
}

object ValueClassActor {
  def props(arg: Argument) = Props(new ValueClassActor(arg))
}

安全。