2. Externalizers
Overview
To (de)serialize private or protected fields you must use an externalization mechanism. See Flex 2 documentation. Granite DS provides a way to avoid the Java coding overload of writing your entity beans as externalizable. Furthermore, it provides a way to make externalizable standard Java classes.
Externalized JavaBeans must have their ActionScript3 counterpart (i.e., classes that implement the flash.utils.IExternalizable interface). In those ActionScript3 beans, readExternal and writeExternal methods must read and write fields in the exact same order as Java ones. In order to avoid the tedious task of manually writing your IExternalizable beans, GraniteDS provides an ActionScript3 code generator that automatically writes the ActionScript3 mirror of your entire Java model. See ActionScript 3 Beans Generation.
Externalization is field-based rather than property-based. This means that all non-static non-transient fields, even private or protected, with no getter/setter will be externalized, while getters/setters with no corresponding field will be ignored. There is, however, a way to externalize the returned value of a public getter without a corresponding field by annotating it with the @ExternalizedProperty annotation. See below about the @ExternalizedBean and @ExternalizedProperty annotations.
Sample Java & ActionScript3 Beans (EJB 3)
Let's say we have a basic entity bean that represents a person. The following code shows its implementation using EJB 3 annotations:
package test.granite.ejb3.entity; import java.io.Serializable; import javax.persistence.Basic; import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.Id; import javax.persistence.Version; @Entity public class Person implements Serializable { private static final long serialVersionUID = 1L; @Id @GeneratedValue private Integer id; @Version private Integer version; @Basic private String firstName; @Basic private String lastName; public Integer getId() { return id; } public String getFirstName() { return firstName; } public void setFirstName(String firstName) { this.firstName = firstName; } public String getLastName() { return lastName; } public void setLastName(String lastName) { this.lastName = lastName; } }
This sample entity bean has one read-only property (id), one completely private property (version) and two read/write properties (firstName, lastName). With standard serialization, we would not be able to send the id and version fields to the Flex client code. One solution would be to make them public with getters and setters, but this would obviously expose these fields to manual and erroneous modifications. Another solution would be to make the person bean implement java.io.Externalizable instead of java.io.Serializable, but it would require implementing and maintaining the readExternal and writeExternal methods. This is at least an annoyance, a source of errors, and might even be impossible if you do not have access to the source code to the Java entities.
With GraniteDS automated externalization and without any modification made to our bean, we may serialize all Person properties, private or not. Furthermore, thanks to the Gas3 code generator, we do not even have to write the ActionScript3 bean by ourselves. Here is a sample generated bean implementation:
/** * Generated by Gas3 v2.0.0 (Granite Data Services). * * WARNING: DO NOT CHANGE THIS FILE. IT MAY BE OVERWRITTEN EACH TIME YOU USE * THE GENERATOR. INSTEAD, EDIT THE INHERITED CLASS (Person.as). */ package test.granite.ejb3.entity { import flash.utils.IDataInput; import flash.utils.IDataOutput; import flash.utils.IExternalizable; import org.granite.collections.IPersistentCollection; import org.granite.meta; use namespace meta; [Bindable] public class PersonBase implements IExternalizable { private var __initialized:Boolean = true; private var __detachedState:String = null; private var _firstName:String; private var _id:Number; private var _lastName:String; private var _version:Number; meta function isInitialized(name:String = null):Boolean { if (!name) return __initialized; var property:* = this[name]; return ( (!(property is Person) || (property as Person).meta::isInitialized()) && (!(property is IPersistentCollection) || (property as IPersistentCollection).isInitialized()) ); } public function set firstName(value:String):void { _firstName = value; } public function get firstName():String { return _firstName; } public function get id():Number { return _id; } public function set lastName(value:String):void { _lastName = value; } public function get lastName():String { return _lastName; } public function readExternal(input:IDataInput):void { __initialized = input.readObject() as Boolean; __detachedState = input.readObject() as String; if (meta::isInitialized()) { _firstName = input.readObject() as String; _id = function(o:*):Number { return (o is Number ? o as Number : Number.NaN) } (input.readObject()); _lastName = input.readObject() as String; _version = function(o:*):Number { return (o is Number ? o as Number : Number.NaN) } (input.readObject()); } else { _id = function(o:*):Number { return (o is Number ? o as Number : Number.NaN) } (input.readObject()); } } public function writeExternal(output:IDataOutput):void { output.writeObject(__initialized); output.writeObject(__detachedState); if (meta::isInitialized()) { output.writeObject(_firstName); output.writeObject(_id); output.writeObject(_lastName); output.writeObject(_version); } else { output.writeObject(_id); } } } }
This AS3 bean reproduces all properties found in the JavaBean equivalent and adds two extra properties, (__initialized and __detachedState), only required for JPA lazy initialization support. See the Lazy Initialization section for details on this feature. Note that properties access (get/set) reproduces exactly those found in the Java entity bean, even if all fields are serialized between the client and the server, only firstName and lastName are modifiable and id is kept read-only.
Standard Configuration
In order to externalize our Person.java entity bean, we must provide a special rule in granite-config.xml file:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE granite-config PUBLIC "-//Granite Data Services//DTD granite-config internal//EN" "http://www.graniteds.org/public/dtd/2.0.0/granite-config.dtd"> <granite-config> <class-getter type="org.granite.hibernate.HibernateClassGetter"/> <externalizers> <externalizer type="org.granite.hibernate.HibernateExternalizer"> <include type="test.granite.ejb3.entity.Person"/> </externalizer> </externalizers> </granite-config>
This instructs GraniteDS to externalize all test.granite.ejb3.entity.Person beans by using the org.granite.hibernate.HibernateExternalizer. Note that the HibernateClassGetter configuration is only required if you use Hibernate Proxies (lazy-initialized beans). See more about this feature in the Lazy Initialization section.
When using TopLink, the configuration is similar but uses org.granite.toplink.TopLinkExternalizer and org.granite.toplink.TopLinkClassGetter.
If you use an abstract entity bean as a parent to all your entity beans you could use this declaration, but note that type in the example above is replaced by instance-of:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE granite-config PUBLIC "-//Granite Data Services//DTD granite-config internal//EN" "http://www.graniteds.org/public/dtd/2.0.0/granite-config.dtd"> <granite-config> <class-getter type="org.granite.hibernate.HibernateClassGetter"/> <externalizers> <externalizer type="org.granite.hibernate.HibernateExternalizer"> <include instance-of="test.granite.ejb3.entity.AbstractEntity"/> </externalizer> </externalizers> </granite-config>
This will avoid the overload of writing externalization instructions for all your beans, and all instances of AbstractEntity will be automatically externalized.
You may even use a annotatedwith attribute as follows:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE granite-config PUBLIC "-//Granite Data Services//DTD granite-config internal//EN" "http://www.graniteds.org/public/dtd/2.0.0/granite-config.dtd"> <granite-config> <class-getter type="org.granite.hibernate.HibernateClassGetter"/> <externalizers> <externalizer type="org.granite.hibernate.HibernateExternalizer"> <include annotated-with="javax.persistence.Entity"/> <include annotated-with="javax.persistence.MappedSuperclass"/> <include annotated-with="javax.persistence.Embeddable"/> </externalizer> </externalizers> </granite-config>
Of course, you may mix different attributes as you want.
Note, however, that there are precedence rules for these three configuration options: type has precedence over annotated-with and annotated-with has precedence over instance-of. Playing with rule precedence provides a way to override general rules with more specific rules.
Autoscan Configuration
Instead of configuring externalizers with the above method, you may use the autoscan feature:
<?xml version="1.0" encoding="UTF-8"?> <!DOCTYPE granite-config PUBLIC "-//Granite Data Services//DTD granite-config internal//EN" "http://www.graniteds.org/public/dtd/2.0.0/granite-config.dtd"> <granite-config scan="true"/>
With this very short configuration, GraniteDS will scan at startup all classes available in the classpath, actually all classes found in the GraniteConfig class ClassLoader instance, and discover all externalizers (classes that implements the GDS Externalizer interface).
See the Standard Externalizers section below to know how scanned Externalizers are applied to your beans.
Standard Externalizers
GraniteDS comes with seven standard externalizers:
- org.granite.messaging.amf.io.util.externalizer.DefaultExternalizer: this externalizer may be used with any POJO bean. When autoscan is enabled, it will be used with all beans annotated with the @ExternalizedBean annotation; assuming that the type attribute of the annotation is left empty. See below.
- org.granite.messaging.amf.io.util.externalizer.EnumExternalizer: this externalizer may be used with Java enum types. When autoscan is enabled, it will be automatically used for all enum types.
- org.granite.hibernate.HibernateExternalizer: This externalizer may be used with all JPA/Hibernate entities (i.e., all classes annotated with @Entity, @MappedSuperclass or @Embeddable annotations). Include granite-hibernate.jar in your classpath in order to use this feature.
- org.granite.toplink.TopLinkExternalizer: this externalizer may be used with all JPA/TopLink entities (i.e., all classes annotated with @Entity, @MappedSuperclass or @Embeddable annotations). Include granite-toplink.jar in your classpath in order to use this feature.
- org.granite.eclipselink.EclipseLinkExternalizer: this externalizer will be used with the new version of TopLink (renamed EclipseLink). Include granite-eclipselink.jar in your classpath in order to use this feature.
- org.granite.openjpa.OpenJpaExternalizer: this externalizer may be used with all JPA/OpenJPA (formerly WebLogic Kodo) entities (mainly in WebLogic environments). Include granite-openjpa.jar in your classpath in order to use this feature.
- org.granite.datanucleus.DataNucleusExternalizer: this externalizer may be used with all JPA/DataNucleus entities. Include granite-datanucleus.jar in your classpath in order to use this feature.
Notes:
- With standard configuration (scan set to false), you must use the appropriate class getter together with the persistence externalizer (eg. org.granite.openjpa.OpenJpaClassGetter with org.granite.openjpa.OpenJpaExternalizer).
- With all persistence externalizers, provided that you have added the relevant jar to your application classpath, you may use the auto scan feature: <granite-config scan="true"> without anything else (no class getter or externalizer configuration): your entities will be automatically externalized according to the underlying JPA engine.
- You can't use several persistence externalizers in the same application (this doesn't make sense anyway).
Custom Externalizers
If you want to write your own externalizer, you must implement the org.granite.messaging.amf.io.util.externalizer.Externalizer interface, or extend the DefaultExternalizer class. There is no particular use case for this extension; it depends on your specific needs and you should look at the standard externalizer implementations to figure out how to write your custom code.
If you use autoscan configuration, make sure your class is packaged in a jar accessible via the GraniteConfig class loader (granite.jar classpath), put a META-INF/granite-config.properties in your jar, even if its empty, and put relevant code in the accept method:
public int accept(Class<?> clazz) { return clazz.isAnnotationPresent(MySpecialAnnotation.class) ? 1 : -1; }
You may, of course, use any kind of conditional expression, based on annotations, inheritance, etc. The returned integer is a weight used when GDS tries to figure out what externalizer it should use when it encounters a Java bean at serialization time: -1 means "do not use this externalizer", 0 or more means "use this externalizer if there is no other externalizer that returns a superior weight for this bean". DefaultExternalizer as a weight of 0, EnumExternalizer and HibernateExternalizer a weight of 1. If your class would normally be externalized by the HibernateExternalizer, you may, for example, use a weight of 2 when you want to supercede the default serialization for particular entities.
@ExternalizedBean & @ExternalizedProperty
Two standard annotations are available in GDS that give you more control over the externalization process:
- @ExternalizedBean: This class annotation may be used to instruct GDS to externalize the annotated bean with the DefaultExternalizer or any other externalizer specified in the type attribute. For example, you could annotate a Java class with:
@ExternalizedBean(type=path.to.MyExternalizer.class) public class MyExternalizedBean { ... } - @ExternalizedProperty: This method annotation may be used on a public getter when you want to externalize a property with no corresponding field (i.e., a computed property). For example:
public class MyBean { private int value; ... @ExternalizedProperty public int getSquare() { return value * value; } }
Of course, this annotation will only be used if the MyBean class is configured for externalization. Note that externalized properties are read only: a setSquare(...) will never be used in the Flex to Java serialization. Note also that Gas3 uses this annotation when it generates ActionScript3 bean so you'll find an extra square member field in your generated MyBean.as.
