19 Reflection

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.

19.1 The basics

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:

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.

19.2 Obtaining ClassInfo instances

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.

19.3 Automatic type conversion

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.

19.4 Reflecting on properties

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.

19.5 Reflecting on methods

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.

19.6 Reflecting on constructors

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.

19.7 Reflecting on metadata tags

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.

19.8 Mapping classes to metadata tags

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:

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.

19.9 Working with ApplicationDomains

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.