improve safety of Props.create by allowing Creator<T>, see #3377

Props constructors need to be deprecated instead of being mutated
because we cannot just start throwing exceptions in people’s existing
code. Props.withCreator is deprecated for similar reasons, but also
because Props are about the creators, so replacing that after the fact
is not good style.
This commit is contained in:
Roland 2013-05-28 10:39:38 +02:00
parent 58756be937
commit f8fa825e48
11 changed files with 222 additions and 45 deletions

View file

@ -0,0 +1,68 @@
/**
* Copyright (C) 2009-2013 Typesafe Inc. <http://www.typesafe.com>
*/
package akka.actor;
import static org.junit.Assert.*;
import org.junit.Test;
import akka.japi.Creator;
public class ActorCreationTest {
@Test
public void testWrongCreator() {
try {
Props.create(new Creator<Actor>() {
@Override
public Actor create() throws Exception {
return null;
}
});
assert false;
} catch(IllegalArgumentException e) {
assertEquals("cannot use non-static local Creator to create actors; make it static or top-level", e.getMessage());
}
}
static class C implements Creator<UntypedActor> {
@Override
public UntypedActor create() throws Exception {
return null;
}
}
static class D<T> implements Creator<T> {
@Override
public T create() {
return null;
}
}
static class E<T extends UntypedActor> implements Creator<T> {
@Override
public T create() {
return null;
}
}
@Test
public void testRightCreator() {
final Props p = Props.create(new C());
assertEquals(UntypedActor.class, p.actorClass());
}
@Test
public void testParametricCreator() {
final Props p = Props.create(new D<UntypedActor>());
assertEquals(Actor.class, p.actorClass());
}
@Test
public void testBoundedCreator() {
final Props p = Props.create(new E<UntypedActor>());
assertEquals(UntypedActor.class, p.actorClass());
}
}

View file

@ -12,9 +12,12 @@ import akka.util.Reflect
import scala.annotation.varargs import scala.annotation.varargs
import Deploy.{ NoDispatcherGiven, NoMailboxGiven } import Deploy.{ NoDispatcherGiven, NoMailboxGiven }
import scala.collection.immutable import scala.collection.immutable
import scala.language.existentials import scala.language.existentials
import java.lang.reflect.Constructor import java.lang.reflect.Constructor
import java.lang.reflect.Modifier
import scala.annotation.tailrec
import java.lang.reflect.ParameterizedType
import java.lang.reflect.TypeVariable
/** /**
* Factory for Props instances. * Factory for Props instances.
@ -40,16 +43,16 @@ object Props {
*/ */
final val defaultDeploy = Deploy() final val defaultDeploy = Deploy()
/**
* The default Props instance, uses the settings from the Props object starting with default*.
*/
final val default = new Props()
/** /**
* A Props instance whose creator will create an actor that doesn't respond to any message * A Props instance whose creator will create an actor that doesn't respond to any message
*/ */
final val empty = Props[EmptyActor] final val empty = Props[EmptyActor]
/**
* The default Props instance, uses the settings from the Props object starting with default*.
*/
final val default = new Props()
/** /**
* INTERNAL API * INTERNAL API
* *
@ -85,7 +88,7 @@ object Props {
/** /**
* The deprecated legacy constructor. * The deprecated legacy constructor.
*/ */
@deprecated("give class and arguments instead", "2.2") @deprecated("use Props.withDispatcher and friends", "2.2")
def apply( def apply(
creator: () Actor = Props.defaultCreator, creator: () Actor = Props.defaultCreator,
dispatcher: String = Dispatchers.DefaultDispatcherId, dispatcher: String = Dispatchers.DefaultDispatcherId,
@ -98,6 +101,13 @@ object Props {
if (d2 != Props.defaultDeploy) p.withDeploy(d2) else p if (d2 != Props.defaultDeploy) p.withDeploy(d2) else p
} }
/**
* The deprecated legacy extractor.
*/
@deprecated("use three-argument version", "2.2")
def unapply(p: Props)(dummy: Int = 0): Option[(() Actor, String, RouterConfig, Deploy)] =
Some((p.creator, p.dispatcher, p.routerConfig, p.deploy))
/** /**
* Scala API: create a Props given a class and its constructor arguments. * Scala API: create a Props given a class and its constructor arguments.
*/ */
@ -108,6 +118,30 @@ object Props {
*/ */
@varargs @varargs
def create(clazz: Class[_], args: AnyRef*): Props = apply(defaultDeploy, clazz, args.toVector) def create(clazz: Class[_], args: AnyRef*): Props = apply(defaultDeploy, clazz, args.toVector)
/**
* Create new Props from the given [[Creator]].
*/
def create[T <: Actor](creator: Creator[T]): Props = {
if ((creator.getClass.getModifiers & Modifier.STATIC) == 0)
throw new IllegalArgumentException("cannot use non-static local Creator to create actors; make it static or top-level")
val cc = classOf[Creator[_]]
val ac = classOf[Actor]
@tailrec def findType(c: Class[_]): Class[_] = {
c.getGenericInterfaces collectFirst {
case t: ParameterizedType if cc.isAssignableFrom(t.getRawType.asInstanceOf[Class[_]])
t.getActualTypeArguments.head match {
case c: Class[_] c // since T <: Actor
case v: TypeVariable[_]
v.getBounds collectFirst { case c: Class[_] if ac.isAssignableFrom(c) && c != ac c } getOrElse ac
}
} match {
case Some(x) x
case None findType(c.getSuperclass)
}
}
apply(defaultDeploy, classOf[CreatorConsumer], findType(creator.getClass) :: creator :: Nil)
}
} }
/** /**
@ -183,7 +217,7 @@ final case class Props(deploy: Deploy, clazz: Class[_], args: immutable.Seq[Any]
* non-serializable inner classes, making them also * non-serializable inner classes, making them also
* non-serializable * non-serializable
*/ */
@deprecated("use Props.create", "2.2") @deprecated("use Props.create()", "2.2")
def this(factory: UntypedActorFactory) = this(Props.defaultDeploy, classOf[UntypedActorFactoryConsumer], Vector(factory)) def this(factory: UntypedActorFactory) = this(Props.defaultDeploy, classOf[UntypedActorFactoryConsumer], Vector(factory))
/** /**
@ -227,6 +261,7 @@ final case class Props(deploy: Deploy, clazz: Class[_], args: immutable.Seq[Any]
* *
* The creator must not return the same instance multiple times. * The creator must not return the same instance multiple times.
*/ */
@deprecated("use Props(...).withDeploy(other.deploy)", "2.2")
def withCreator(c: Actor): Props = copy(clazz = classOf[CreatorFunctionConsumer], args = (() c) :: Nil) def withCreator(c: Actor): Props = copy(clazz = classOf[CreatorFunctionConsumer], args = (() c) :: Nil)
/** /**
@ -239,8 +274,8 @@ final case class Props(deploy: Deploy, clazz: Class[_], args: immutable.Seq[Any]
* non-serializable inner classes, making them also * non-serializable inner classes, making them also
* non-serializable * non-serializable
*/ */
@deprecated("use Props.create(clazz, args ...) instead", "2.2") @deprecated("use Props.create(clazz, args ...).withDeploy(other.deploy) instead", "2.2")
def withCreator(c: Creator[Actor]): Props = copy(clazz = classOf[CreatorConsumer], args = c :: Nil) def withCreator(c: Creator[Actor]): Props = copy(clazz = classOf[CreatorConsumer], args = classOf[Actor] :: c :: Nil)
/** /**
* Returns a new Props with the specified creator set. * Returns a new Props with the specified creator set.
@ -248,7 +283,7 @@ final case class Props(deploy: Deploy, clazz: Class[_], args: immutable.Seq[Any]
* @deprecated use Props.create(clazz) instead; deprecated since it duplicates * @deprecated use Props.create(clazz) instead; deprecated since it duplicates
* another API * another API
*/ */
@deprecated("use Props(clazz, args).withDeploy(other.deploy)", "2.2") @deprecated("use Props.create(clazz, args).withDeploy(other.deploy)", "2.2")
def withCreator(c: Class[_ <: Actor]): Props = copy(clazz = c, args = Nil) def withCreator(c: Class[_ <: Actor]): Props = copy(clazz = c, args = Nil)
/** /**
@ -342,8 +377,8 @@ private[akka] class CreatorFunctionConsumer(creator: () ⇒ Actor) extends Indir
/** /**
* INTERNAL API * INTERNAL API
*/ */
private[akka] class CreatorConsumer(creator: Creator[Actor]) extends IndirectActorProducer { private[akka] class CreatorConsumer(clazz: Class[_ <: Actor], creator: Creator[Actor]) extends IndirectActorProducer {
override def actorClass = classOf[Actor] override def actorClass = clazz
override def produce() = creator.create() override def produce() = creator.create()
} }

View file

@ -166,5 +166,5 @@ abstract class UntypedActor extends Actor {
/** /**
* Factory closure for an UntypedActor, to be used with 'Actors.actorOf(factory)'. * Factory closure for an UntypedActor, to be used with 'Actors.actorOf(factory)'.
*/ */
@deprecated("use Props.create(clazz, args) instead", "2.2") @deprecated("use Creator<T> instead", "2.2")
trait UntypedActorFactory extends Creator[Actor] with Serializable trait UntypedActorFactory extends Creator[Actor] with Serializable

View file

@ -14,7 +14,6 @@ import java.util.concurrent.TimeoutException
import java.util.concurrent.atomic.AtomicInteger import java.util.concurrent.atomic.AtomicInteger
import akka.pattern.ask import akka.pattern.ask
import scala.reflect.ClassTag import scala.reflect.ClassTag
import akka.dispatch.{ UnboundedDequeBasedMailbox, RequiresMessageQueue }
trait Creators { this: ActorDSL.type trait Creators { this: ActorDSL.type

View file

@ -48,7 +48,8 @@ trait Effect {
/** /**
* A constructor/factory, takes no parameters but creates a new value of type T every call. * A constructor/factory, takes no parameters but creates a new value of type T every call.
*/ */
trait Creator[T] { @SerialVersionUID(1L)
trait Creator[T] extends Serializable {
/** /**
* This method must return a different instance upon every call. * This method must return a different instance upon every call.
*/ */

View file

@ -49,6 +49,7 @@ import akka.actor.IndirectActorProducer;
import akka.actor.OneForOneStrategy; import akka.actor.OneForOneStrategy;
//#import-props //#import-props
import akka.actor.Props; import akka.actor.Props;
import akka.japi.Creator;
//#import-props //#import-props
import akka.actor.SupervisorStrategy; import akka.actor.SupervisorStrategy;
import akka.actor.SupervisorStrategy.Directive; import akka.actor.SupervisorStrategy.Directive;
@ -88,14 +89,35 @@ public class UntypedActorDocTest {
private final ActorSystem system = actorSystemResource.getSystem(); private final ActorSystem system = actorSystemResource.getSystem();
//#creating-props-config
static class MyActorC implements Creator<MyActor> {
@Override public MyActor create() {
return new MyActor("...");
}
}
//#creating-props-config
@SuppressWarnings("unused") @SuppressWarnings("unused")
@Test @Test
public void createProps() { public void createProps() {
//#creating-props-config //#creating-props-config
Props props1 = Props.create(MyUntypedActor.class); Props props1 = Props.create(MyUntypedActor.class);
Props props2 = Props.create(MyActor.class, "..."); Props props2 = Props.create(MyActor.class, "...");
Props props3 = Props.create(new MyActorC());
//#creating-props-config //#creating-props-config
} }
//#parametric-creator
static class ParametricCreator<T extends MyActor> implements Creator<T> {
@Override public T create() {
// ... fabricate actor here
//#parametric-creator
return null;
//#parametric-creator
}
}
//#parametric-creator
@SuppressWarnings("deprecation") @SuppressWarnings("deprecation")
@Test @Test

View file

@ -51,12 +51,20 @@ dispatcher to use, see more below). Here are some examples of how to create a
.. includecode:: code/docs/actor/UntypedActorDocTest.java#import-props .. includecode:: code/docs/actor/UntypedActorDocTest.java#import-props
.. includecode:: code/docs/actor/UntypedActorDocTest.java#creating-props-config .. includecode:: code/docs/actor/UntypedActorDocTest.java#creating-props-config
The last line shows how to pass constructor arguments to the :class:`Actor` The second line shows how to pass constructor arguments to the :class:`Actor`
being created. The presence of a matching constructor is verified during being created. The presence of a matching constructor is verified during
construction of the :class:`Props` object, resulting in an construction of the :class:`Props` object, resulting in an
:class:`IllegalArgumentEception` if no or multiple matching constructors are :class:`IllegalArgumentEception` if no or multiple matching constructors are
found. found.
The third line demonstrates the use of a :class:`Creator<T extends Actor>`. The
creator class must be static, which is verified during :class:`Props`
construction. The type parameters upper bound is used to determine the
produced actor class, falling back to :class:`Actor` if fully erased. An
example of a parametric factory could be:
.. includecode:: code/docs/actor/UntypedActorDocTest.java#parametric-creator
Deprecated Variants Deprecated Variants
^^^^^^^^^^^^^^^^^^^ ^^^^^^^^^^^^^^^^^^^

View file

@ -18,17 +18,57 @@ Deprecated Closure-Taking Props
are usually created in-line and thus carry a reference to their enclosing are usually created in-line and thus carry a reference to their enclosing
object; this is not well known among programmers, in particular it can be object; this is not well known among programmers, in particular it can be
surprising that innocent-looking actor creation should not be serializable, surprising that innocent-looking actor creation should not be serializable,
e.g. if the enclosing class is an actor. e.g. if the enclosing class is an actor. Another issue which came up often
during reviews is that these actor creators inadvertedly close over the Actors
``this`` reference for calling methods on it, which is inherently unsafe.
Thus we have decided to deprecate ``Props(new MyActor(...))`` and Another reason for changing the underlying implementation is that Props now
:class:`UntypedActorFactory` in favor of basing :class:`Props` on a carries information about which class of actor will be created, allowing the
:class:`Class` and a sequence of constructor arguments. This has the added extraction of mailbox type requirements (e.g. when using the Stash) before
benefit of allowing easier integration with dependency injection frameworks, trying to create the actor. Being based on the actor class and a list of
see :ref:`actor-create-factory`. constructor arguments also allows these arguments to be serialized according to
the configured serializer bindings instead of mandating Java serialization
(which was used previously).
The deprecated methods will be retained until the possibility of reintroducing What changes for Java?
a similar syntax in a safe fashion has been properly researched (in case of ----------------------
Scala it might be possible to use macros to this effect).
A new method ``Props.create`` has been introduced with two overloads::
Props.create(MyActor.class, arg1, arg2, ...);
// or
Props.create(new MyActorCreator(args ...));
In the first case the existence of a constructor signature matching the
supplied arguments is verified at Props construction time. In the second case
it is verified that ``MyActorCreator`` (which must be a ``akka.japi.Creator<?
extends Actor>``) is a static class. In both cases failure is signaled by
throwing a :class:`IllegalArgumentException`.
The constructors of :class:`Props` have been deprecated to facilitate migration.
The :meth:`withCreator` methods have been deprecated. The functionality is
available by using ``Props.create(...).withDeploy(oldProps.deploy());``.
:class:`UntypedActorFactory` has been deprecated in favor of the more precisely
typed :class:`Creator<T>`.
What changes for Scala?
-----------------------
The case class signature of Props has been changed to only contain a
:class:`Deploy`, a :class:`Class[_]` and an immutable :class:`Seq[Any]` (the
constructor arguments for the class). The old factory and extractor methods
have been deprecated.
Properly serializable :class:`Props` can now be created for actors which take
constructor arguments by using ``Props(classOf[MyActor], arg1, arg2, ...)``.
In a future update—possibly within the 2.2.x timeframe—we plan to introduce a
macro which will transform the by-name argument to ``Props(new MyActor(...))``
into a call to the former.
The :meth:`withCreator` methods have been deprecated. The functionality is
available by using ``Props(...).withDeploy(oldProps.deploy)``.
Immutable everywhere Immutable everywhere
==================== ====================

View file

@ -85,18 +85,21 @@ for a migration period):
.. includecode:: code/docs/actor/ActorDocSpec.scala#creating-props-deprecated .. includecode:: code/docs/actor/ActorDocSpec.scala#creating-props-deprecated
The last one is deprecated because its functionality is available in full The first one is deprecated because the case class structure changed between
through :meth:`Props.apply()`. Akka 2.1 and 2.2.
The first three are deprecated because the captured closure is a local class The two variants in the middle are deprecated because :class:`Props` are
which means that it implicitly carries a reference to the enclosing class. This primarily concerned with actor creation and thus the “creator” part should be
can easily make the resulting :class:`Props` non-serializable, e.g. when the explicitly set when creating an instance. In case you want to deploy one actor
enclosing class is an :class:`Actor`. Akka advocates location transparency, in the same was as another, simply use
meaning that an application written with actors should just work when it is ``Props(...).withDeploy(otherProps.deploy)``.
deployed over multiple network nodes, and non-serializable actor factories
would break this principle. In case indirect actor creation is needed—for The last one is not technically deprecated, but it is not recommended because
example when using dependency injection—there is the possibility to use an it encourages to close over the enclosing scope, resulting in non-serializable
:class:`IndirectActorProducer` as described below. :class:`Props` and possibly race conditions (breaking the actor encapsulation).
We will provide a macro-based solution in a future release which allows similar
syntax without the headaches, at which point this variant will be properly
deprecated.
There were two use-cases for these methods: passing constructor arguments to There were two use-cases for these methods: passing constructor arguments to
the actor—which is solved by the newly introduced the actor—which is solved by the newly introduced

View file

@ -243,19 +243,19 @@ class ActorDocSpec extends AkkaSpec(Map("akka.loglevel" -> "INFO")) {
//#creating-props //#creating-props
//#creating-props-deprecated //#creating-props-deprecated
// DEPRECATED: encourages to close over enclosing class // DEPRECATED: old case class signature
val props4 = Props( val props4 = Props(
creator = { () new MyActor }, creator = { () new MyActor },
dispatcher = "my-dispatcher") dispatcher = "my-dispatcher")
// DEPRECATED: encourages to close over enclosing class // DEPRECATED due to duplicate functionality with Props.apply()
val props5 = props1.withCreator(new MyActor) val props5 = props1.withCreator(new MyActor)
// DEPRECATED: encourages to close over enclosing class
val props6 = Props(new MyActor)
// DEPRECATED due to duplicate functionality with Props.apply() // DEPRECATED due to duplicate functionality with Props.apply()
val props7 = props1.withCreator(classOf[MyActor]) val props6 = props1.withCreator(classOf[MyActor])
// NOT RECOMMENDED: encourages to close over enclosing class
val props7 = Props(new MyActor)
//#creating-props-deprecated //#creating-props-deprecated
} }

View file

@ -136,7 +136,8 @@ object AkkaBuild extends Build {
dependencies = Seq(testkit % "compile;test->test"), dependencies = Seq(testkit % "compile;test->test"),
settings = defaultSettings ++ scaladocSettings ++ Seq( settings = defaultSettings ++ scaladocSettings ++ Seq(
publishArtifact in Compile := false, publishArtifact in Compile := false,
libraryDependencies ++= Dependencies.actorTests libraryDependencies ++= Dependencies.actorTests,
testOptions += Tests.Argument(TestFrameworks.JUnit, "-v", "-a")
) )
) )
@ -972,7 +973,7 @@ object Dependencies {
val testkit = Seq(Test.junit, Test.scalatest) val testkit = Seq(Test.junit, Test.scalatest)
val actorTests = Seq(Test.junit, Test.scalatest, Test.commonsCodec, Test.commonsMath, Test.mockito, Test.scalacheck, protobuf) val actorTests = Seq(Test.junit, Test.scalatest, Test.commonsCodec, Test.commonsMath, Test.mockito, Test.scalacheck, protobuf, Test.junitIntf)
val remote = Seq(netty, protobuf, uncommonsMath, Test.junit, Test.scalatest) val remote = Seq(netty, protobuf, uncommonsMath, Test.junit, Test.scalatest)