The reflection library contains convenient classes to reflect on AS3 classes,
methods and properties without the need for cumbersome parsing of XML output from
describeType
.
We will use the flash.geom.Point
class from the core Player API to illustrate
the features of the Spicelib Reflection API. The following listing shows the output of
the flash.utils.describeType
method if invoked with Point
as the parameter:
<type name="flash.geom::Point" base="Class" isDynamic="true" isFinal="true" isStatic="true">
<extendsClass type="Class"/>
<extendsClass type="Object"/>
<method name="interpolate" declaredBy="flash.geom::Point" returnType="flash.geom::Point">
<parameter index="1" type="flash.geom::Point" optional="false"/>
<parameter index="2" type="flash.geom::Point" optional="false"/>
<parameter index="3" type="Number" optional="false"/>
</method>
<method name="polar" declaredBy="flash.geom::Point" returnType="flash.geom::Point">
<parameter index="1" type="Number" optional="false"/>
<parameter index="2" type="Number" optional="false"/>
</method>
<method name="distance" declaredBy="flash.geom::Point" returnType="Number">
<parameter index="1" type="flash.geom::Point" optional="false"/>
<parameter index="2" type="flash.geom::Point" optional="false"/>
</method>
<accessor name="prototype" access="readonly" type="*" declaredBy="Class"/>
<factory type="flash.geom::Point">
<extendsClass type="Object"/>
<constructor>
<parameter index="1" type="Number" optional="true"/>
<parameter index="2" type="Number" optional="true"/>
</constructor>
<method name="subtract" declaredBy="flash.geom::Point" returnType="flash.geom::Point">
<parameter index="1" type="flash.geom::Point" optional="false"/>
</method>
<method name="normalize" declaredBy="flash.geom::Point" returnType="void">
<parameter index="1" type="Number" optional="false"/>
</method>
<method name="toString" declaredBy="flash.geom::Point" returnType="String"/>
<method name="clone" declaredBy="flash.geom::Point" returnType="flash.geom::Point"/>
<method name="offset" declaredBy="flash.geom::Point" returnType="void">
<parameter index="1" type="Number" optional="false"/>
<parameter index="2" type="Number" optional="false"/>
</method>
<accessor name="length" access="readonly" type="Number" declaredBy="flash.geom::Point"/>
<method name="equals" declaredBy="flash.geom::Point" returnType="Boolean">
<parameter index="1" type="flash.geom::Point" optional="false"/>
</method>
<variable name="y" type="Number"/>
<method name="add" declaredBy="flash.geom::Point" returnType="flash.geom::Point">
<parameter index="1" type="flash.geom::Point" optional="false"/>
</method>
<variable name="x" type="Number"/>
</factory>
</type>
As you see you get information about superclasses, properties (<accessor>
and
<variable>
tags), the constructor and methods and their parameter types. The Spicelib
Reflection API builds on top of the output generated by describeType
and offers the
following features:
describeType
and XML parsing occur only once
for each class. Converter
instances for any number of target types. The following sections will explain each of these features.
Performance Improvements in Flash Player 10.1 and Higher
When using Flash Player 10.1 or newer the library supports
the new describeTypeJSON
function under the hood which is up to 4 times
faster than the old XML-based describeType
. It automatically detects
the availability of this function without the need for a configuration step.
The same SWC can be used in all versions of Flash Player 9 to 11.
The ClassInfo
class is the central entry point for all reflection features.
We chose the name ClassInfo as the name Class is already a top level type in AS3.
There are three ways to obtain an instance of ClassInfo
:
By class name | Example: ClassInfo.forName("flash.geom.Point"); |
By class reference | Example: ClassInfo.forClass(Point); |
By instance | Example: ClassInfo.forInstance(new Point()); |
Of course the last example would only make sense if you use an existing instance of a class
and don't know or don't want to determine the type of the instance first. Otherwise the second
example is the most efficient. If you invoke one of the above three static methods more than
once for the same type, the returned ClassInfo
instance will be taken from the internal
cache to avoid the overhead of parsing the XML returned by describeType
again.
The ClassInfo
class offers methods to obtain the superclasses and implemented interfaces
(getSuperclasses
and getInterfaces
) and the isType
method that checks if
the specified parameter is a superclass or one of the implemented interfaces of the class represented
by the ClassInfo
instance.
Furhermore the ClassInfo
class contains methods to reflect on properties and methods
which will be explained in the following sections,
but before that we will introduce the concept of automatic type conversion, as this concept is used
internally for some of the reflection features related to properties and methods.
The Converters
class of the org.spicefactory.lib.reflect
package includes a static
addConverter
method. This allows to register Converter
instances and map them to
particular types. Internally the Reflection Module will use these Converters to automatically convert
method parameters and property values if their type does not match the required type. This applies
to Property.setValue
, Method.invoke
and Constructor.newInstance
,
all of them explained in the following sections. They will also be used to convert attributes
of custom metadata tags to the properties of any registered custom metadata class.
The Reflection Module contains some builtin Converters for basic types like Boolean
,
String
, int
, etc., but you can easily add your own. Just implement the
Converter
interface and add it to the Reflection Module with Converters.addConverter
.
The Property
class allows to obtain information about the type of the property and
if it is readable and writable:
var ci:ClassInfo = ClassInfo.forClass(Point);
var p:Property = ci.getProperty("x");
trace("type: " + p.type.name);
trace("readable: " + p.readable);
trace("writable: " + p.writable);
The output for the code above would be:
type: Number
readable: true
writable: true
Furthermore you can also use the Property
class to read and write the value
of that property from/to a particular instance of the class that property belongs to:
var point:Point = new Point(7, 5);
var ci:ClassInfo = ClassInfo.forClass(Point);
var p:Property = ci.getProperty("x");
p.setValue(point, 12);
trace(point.x); // output: 12
When using Property.setValue
any necessary type conversion will be done as described
in 19.3 Automatic type conversion.
Reading and writing property values reflectively is usually not done in application code. It is most useful for developing frameworks and libraries that have to manage classes not known until runtime.
The Method
class allows to obtain information about the method parameter types and
if they are optional or not:
var ci:ClassInfo = ClassInfo.forClass(Point);
var m:Method = ci.getMethod("add");
var params:Array = m.parameters;
trace("param count: " + params.length);
var param:Parameter = params[0] as Parameter;
trace("param type: " + param.type.name);
trace("param required: " + param.required);
trace("return type: " + m.returnType.name);
The output for the code above would be:
param count: 1
param type: flash.geom::Point
param required: true
return type: flash.geom::Point
Furthermore you can also use the Method
class to reflectively invoke
the method on a particular instance of the class that the Method
instance belongs to:
var point:Point = new Point(7, 5);
var ci:ClassInfo = ClassInfo.forClass(Point);
var m:Method = ci.getMethod("add");
var result:Point = m.invoke(point, [new Point(3, 3)]);
trace(result.x); // output: 10
When using Method.invoke
any necessary type conversion for the method parameters
will be done as described in 19.3 Automatic type conversion.
Reflectively invoking methods is usually not done in application code. It is most useful for developing frameworks and libraries that have to manage classes not known until runtime.
The Constructor
class allows to obtain information about the method parameter types and
if they are optional or not:
var ci:ClassInfo = ClassInfo.forClass(Point);
var con:Constructor = ci.getConstructor();
var params:Array = con.parameters;
trace("param count: " + params.length);
var param:Parameter = params[0] as Parameter;
trace("param 0 type: " + param.type.name);
trace("param 0 required: " + param.required);
param = params[1] as Parameter;
trace("param 1 type: " + param.type.name);
trace("param 1 required: " + param.required);
The output for the code above would be:
param count: 2
param 0 type: Number
param 0 required: false
param 1 type: Number
param 1 required: false
Furthermore you can also use the Constructor
class to reflectively
create new instances:
var ci:ClassInfo = ClassInfo.forClass(Point);
var con:Constructor = ci.getConstructor();
var instance:Point = con.newInstance([2, 5]);
trace(instance.x); // output: 2
trace(instance.y); // output: 5
When using Constructor.newInstance
any necessary type conversion for the method parameters
will be done as described in 19.3 Automatic type conversion.
Unfortunately there is a bug in Flash Player 9 that causes the type information for the
constructor parameters to get lost under certain circumstances, usually if you create a
ClassInfo
instance for a class that has not been instantiated yet. If you run into
this bug it is usually sufficient to add a simple new MyClass()
statement anywhere
in your code before you start reflecting on that class. The bug is marked as "in progress"
in the Adobe Jira, so hopefully it will
be resolved for Player 10.
Introduced with version 1.1.0 the Metadata
class allows to reflect on metadata tags
added to classes, properties or methods. The ClassInfo
, Constructor
, Method
and Property
class now all extend MetadataAware
directly or indirectly, so that you
can retrieve information on any metadata tags added to one of those elements. (Note that for the
Constructor
class the metadata feature was added for "forward compatibility", currently
the Flash Player ignores metadata tags placed on constructors).
There are two ways to reflect on metadata. The one described in this section just provides untyped String-based access to tags and its attributes. The following section then explains how you can register custom classes mapping to metadata tags to allow for type-safe reflection on metadata.
Consider this simple class:
public class MyClass {
[CustomMetadata(customAttribute="foo")]
public function someMethod () : void {
trace("someMethod invoked");
}
}
When you compile classes with custom metadata make sure that you add it to the
-keep-as3-metadata
compiler option, as it will be ignored otherwise:
mxmlc -keep-as3-metadata+=CustomMetadata,OtherTag1,OtherTag2 ... [other options]
If you use this option when compiling an SWC all projects that use that SWC do not need to explicitly specify the metadata tags to keep as they will be included automatically.
Now you can obtain information about such a tag at runtime:
var ci:ClassInfo = ClassInfo.forClass(MyClass);
var m:Method = ci.getMethod("someMethod");
var tags:Array = m.getMetadata("CustomMetadata");
trace("number of metadata tags: " + tags.length);
var meta:Metadata = tags[0] as Metadata;
trace("customAttribute = " + meta.getArgument("customAttribute"));
The output for the above code would be:
number of metadata tags: 1
customAttribute = foo
Note that getMetadata
always returns an Array because multiple tags of the same
type can be place on the same element. If no such tag exists for a particular element an empty
Array will be returned.
While the API demonstrated in the previous section may be sufficient for simple use cases, it would be more convenient to work with metadata in a type-safe way if you are doing more than just simple lookups (like complex configuration tasks performed for custom metadata for example).
If you want to work with custom classes mapped to metadata tags you have to perform the following tasks:
[Metadata]
tag to this custom class, optionally specifying the types on which this tag
is allowed to occur. [DefaultProperty]
tag to one of the properties of that class. Metadata.registerMetadataClass
. -keep-as3-metadata
compiler option. We will walk you through all these steps with a concrete example. Consider the following class:
public class LoginController {
[EventHandler(name="login", type="com.foo.LoginEvent")]
public function handleLogin () : void {
trace("handleLogin invoked");
}
}
Let's assume you are building a framework that will interpret the EventHandler
tag
and invoke the annotated method whenever such an Event occurs (in fact Parsley includes
metadata-driven configuration options like this). The first step would be to create a class
that represents this tag:
[Metadata(name="EventHandler", types="method")]
public class EventHandlerMetadata {
[DefaultProperty]
public var name:String;
public var type:ClassInfo;
}
As you see, the two properties correspond to the two attributes in the metadata tags. You are
not limited to working with Strings here, the Spicelib will automatically convert the attributes
to the property type, as long as there is a builtin Converter for that type. If you are working
with properties that the Spicelib cannot convert out-of-the-box, you can register your own
Converter
instance as described in 19.3 Automatic type conversion, but you should
rarely have the need to do so. In addition to the usual type conversion the metadata support
handles an additional use case that is quite common: You can define a comma-separated value
for an attribute when the property type is Array, in this case the Spicelib will split the
value accordingly (but without type conversions for the individual elements).
Above the class declaration you have to add the [Metadata]
tag to declare
that this is a class that should be mapped to a custom metadata tag. The attribute specifies
the name of the tag (as we used it in the LoginController
example class). If this
attribute is omitted it will use the non-qualified name of the class as the tag name.
The second attribute specifies on which types the metadata tag should be mapped to this class.
Permitted values are class
, constructor
, method
and property
(but tags on the constructor are currently ignored by the Flex compilers, so this is included only
for eventual future use). If this attribute is omitted Spicelib will map the metadata tag for all
of these types. With this option you can for example map different classes for the same tag name,
in case you have a different set of attributes for annotated methods than for properties.
Finally one of the two properties was defined as [DefaultProperty]
. This means
that this property will be set whenever an attribute is specified without a key like
in the following example:
[EventHandler("login")]
Now that you have created the class that you want to map to metadata, you have to register it like this:
Metadata.registerMetadataClass(EventHandlerMetadata);
Make sure that you register the class before you reflect on a class that uses this tag
for the first time. Finally don't forget to add it to the -keep-as3-metadata
compiler option.
Now you can start reflecting on those tags:
var ci:ClassInfo = ClassInfo.forClass(LoginController);
var m:Method = ci.getMethod("handleLogin");
var tags:Array = m.getMetadata(EventHandlerMetadata);
trace("number of metadata tags: " + tags.length);
var meta:EventHandlerMetadata = tags[0] as EventHandlerMetadata;
trace("name = " + meta.name);
trace("type = " + meta.type.name);
The output for the above code would be:
number of metadata tags: 1
name = login
type = com.foo.LoginEvent
Note that you no longer use Strings as keys in your getMetadata
invocations,
you now use the class that represents the tag as the key (which is much better in terms of
type-safety).
If an error occurs while processing metadata tags (for example a type conversion that fails) it will be silently swallowed. This is because an illegal metadata tag should not prevent you from reflecting on the annotated method. We are considering adding a kind of optional "strict" mode to a future release that would throw an Error in such a case or any other means to explicitly validate tags.
Finally, also introduced with version 1.1.0, Spicelib now supports ApplicationDomains.
In case you work with Flex Modules or other SWF files that are loaded in a separate
ApplicationDomain you can now tell the Spicelib Reflection API explicitly to use this
domain. For this purpose the three methods you can use to obtain a ClassInfo
instance have been modified to support an optional ApplicationDomain
parameter:
static function forName (name:String, domain:ApplicationDomain = null) : ClassInfo
static function forClass (clazz:Class, domain:ApplicationDomain = null) : ClassInfo
static function forInstance (instance:Object, domain:ApplicationDomain = null) : ClassInfo
Note that if you use a different domain you would have to specifiy it for all of these
methods, not only the one that takes a String argument. This is because even if you pass
an existing Class
instance, the Spicelib might still need to know the domain to reflect
on dependent types, since it uses describeType
internally which provides type
information for superclasses, implemented interfaces, property types and method parameter types
in the form of Strings. To convert these Strings to actual Class
instances the
specified domain will be used internally. Unfortunately AS3 does not have a similar construct
like Javas getClassLoader
method which would make the ApplicationDomain
parameter obsolete for the second and third method.
If you omit the optional second parameter, the ClassInfo
class will work
like in previous versions, simply using ApplicationDomain.currentDomain
.