A blog about software development and other software related matters

Blog Archive

Saturday, August 30, 2008

AMF serialization from Java to Flex and back

There are some scenarios in which you want to pass objects to and from a Flex application to a Java back end, if your using an Adobe framework (LCDS or BlazeDS) than you get this functionality out of the box however there are some cases that you wish to escape Adobe's grip and uses some third party technology.
Some solutions that may come to mind are JSon or XML, the problem with these solutions is that they may require a lot of bandwidth and even lots of computing power for complex objects graphs, AMF to the rescue.
AMF is a binary protocol which is used natively by the flash player, with the release of BlazeDS it was made accessible to any Java client that wishes to use it, in order to use AMF we need to serialize an object into bytes and pass it as the payload of any protocol that we choose, the easiest way to do so is to encode the resulting AMF bytes into BASE64 encoding (a format that transforms byte arrays into a readable string form) and append it to the existing payload.
Ill start with the Java side first:


import com.google.inject.Inject;
import flex.messaging.endpoints.BaseHTTPEndpoint;
import flex.messaging.io.SerializationContext;
import flex.messaging.io.amf.Amf3Input;
import flex.messaging.io.amf.Amf3Output;
import java.io.ByteArrayInputStream;
import java.io.ByteArrayOutputStream;
import java.io.IOException;
import java.io.InputStream;
import sun.misc.BASE64Decoder;
import sun.misc.BASE64Encoder;

/**
*
* @author ronen
*/
public class AmfSerializer {

@Inject
private SerializationContext context;

public <T> String toAmf(final T source) throws IOException {
final StringBuffer buffer = new StringBuffer();
final ByteArrayOutputStream bout = new ByteArrayOutputStream();
final Amf3Output amf3Output = new Amf3Output(context);
amf3Output.setOutputStream(bout);
amf3Output.writeObject(source);
amf3Output.flush();
amf3Output.close();
final BASE64Encoder encoder = new BASE64Encoder();
return encoder.encode(bout.toByteArray());
}

public <T> T fromAmf(final String amf) throws ClassNotFoundException, IOException {
final BASE64Decoder decoder = new BASE64Decoder();
byte[] input = decoder.decodeBuffer(amf);
InputStream bIn = new ByteArrayInputStream(input);
Amf3Input amf3Input = new Amf3Input(context);
amf3Input.setInputStream(bIn);
return (T) amf3Input.readObject();
}
}


This code uses BlazeDS API (the messaging-core, messaging-common and messaging-remoting jars) in order to serialize an object of type T into AMF byte array the bytes are encoded to a Base64 string, encoding result of an instance of the following VO:


package com.jdftm.vo;

import java.util.Date;

public class CurrentDayVO {

private Date now;

public CurrentDayVO() {
}

public void setNow(Date now) {
this.now = now;
}

public Date getNow() {
return now;
}
}


might look something like ChMzY29tLmpkZnRtLnZvLkN1cnJlbnREYXlWTwdub3cIAUJrjUw54AAA.

Now lets turn to the Flex side:


package com.jdftm.stomp.interop {
import flash.utils.ByteArray;
import mx.utils.Base64Encoder;
import mx.utils.Base64Decoder;

public class AMFSerializer {
public function serializeToString(value:Object):String{
if(value==null){
throw new Error("null isn't a legal serialization candidate");
}
var bytes:ByteArray = new ByteArray();
bytes.writeObject(value);
bytes.position = 0;
var be:Base64Encoder = new Base64Encoder();
be.encodeBytes(bytes);
var res:String = be.toString();
be.reset();
return res;
}

public function readObjectFromStringBytes(value:String):Object{
var dec:Base64Decoder=new Base64Decoder();
dec.decode(value);
var result:ByteArray=dec.drain();
result.position=0;
return result.readObject();
}
}
}


The logic is quite similar to the Java side code however when using the AMFSerializer we must register the serialized classes prior to serializing them:


registerClassAlias("com.jdftm.vo.CurrentDayVO", CurrentDayVO);


Failing to so will cause the de-serialization process (on both sides) to fail since it uses the alias in order to create the resulting instance object, its also required that the serialized classes on both ends to be in the same package as implemented in the CurrentDayVO Flex class:


package com.jdftm.vo{
[Bindable]
[RemoteClass(alias="com.jdftm.vo.CurrentDayVO")]
public class CurrentDayVO{
private var _now:Date;

public function get now():Date{
return _now;
}

public function set now(value:Date):void{
_now=value;
}
}
}


As youv seen AMF isn't to hard to use and may prove to be a powerful contender in the crowded integration protocols market.

Updated 2/09/08
Here is the SerializationContext implementation which is used in the Java serialization class


import com.google.inject.Provider;
import flex.messaging.io.SerializationContext;

public class SerializationContextProvider implements Provider<SerializationContext> {

@Override
public SerializationContext get() {
SerializationContext serializationContext = SerializationContext.getSerializationContext();// Threadlocal SerializationContent
serializationContext.enableSmallMessages = true;
serializationContext.instantiateTypes = true;
serializationContext.supportRemoteClass = true;// use _remoteClass field
serializationContext.legacyCollection = false;// false Legacy Flex 1.5 behavior was to return a java.util.Collection for Array, New Flex 2+ behavior is to return Object[] for AS3 Array
serializationContext.legacyMap = false;// false Legacy flash.xml.XMLDocument Type
serializationContext.legacyXMLDocument = false;// true New E4X XML Type
serializationContext.legacyXMLNamespaces = false;// determines whether the constructed Document is name-space aware
serializationContext.legacyThrowable = false;
serializationContext.legacyBigNumbers = false;
serializationContext.restoreReferences = false;
serializationContext.logPropertyErrors = false;
serializationContext.ignorePropertyErrors = true;
return serializationContext;

/*
serializationContext.enableSmallMessages = serialization.getPropertyAsBoolean(ENABLE_SMALL_MESSAGES, true);
serializationContext.instantiateTypes = serialization.getPropertyAsBoolean(INSTANTIATE_TYPES, true);
serializationContext.supportRemoteClass = serialization.getPropertyAsBoolean(SUPPORT_REMOTE_CLASS, false);
serializationContext.legacyCollection = serialization.getPropertyAsBoolean(LEGACY_COLLECTION, false);
serializationContext.legacyMap = serialization.getPropertyAsBoolean(LEGACY_MAP, false);
serializationContext.legacyXMLDocument = serialization.getPropertyAsBoolean(LEGACY_XML, false);
serializationContext.legacyXMLNamespaces = serialization.getPropertyAsBoolean(LEGACY_XML_NAMESPACES, false);
serializationContext.legacyThrowable = serialization.getPropertyAsBoolean(LEGACY_THROWABLE, false);
serializationContext.legacyBigNumbers = serialization.getPropertyAsBoolean(LEGACY_BIG_NUMBERS, false);
boolean showStacktraces = serialization.getPropertyAsBoolean(SHOW_STACKTRACES, false);
if (showStacktraces && Log.isWarn())
log.warn("The " + SHOW_STACKTRACES + " configuration option is deprecated and non-functional. Please remove this from your configuration file.");
serializationContext.restoreReferences = serialization.getPropertyAsBoolean(RESTORE_REFERENCES, false);
serializationContext.logPropertyErrors = serialization.getPropertyAsBoolean(LOG_PROPERTY_ERRORS, false);
serializationContext.ignorePropertyErrors = serialization.getPropertyAsBoolean(IGNORE_PROPERTY_ERRORS, true);
*/
}
}



This is the guice provider which is used when creating such contexts.

24 comments:

Heiner said...

Hello

I found your article very useful. Now we are planing to migrate our gaming service from a text-based messaging protocol to a binary based one. However we are having trouble recreating an object on the flash side, we use Socket.readObject() method but we get: RangeError: Error #2006: The supplied index is out of bounds..
Maybe you know about this and could post something about it.

Thanks

ronen said...

Try to eliminate some of the object graph, i was told once that Flex had some issues with converting certain Java collection types maybe it has something to do with it, can you publish the serialized object source?

Heiner said...

I think that our problem is that we were assuming that Socket.readObject() base64-decodes the stream before converting. So we are going to decode it first but we don't have mx.utils.Base64Encoder nor
mx.utils.Base64Decoder so we are looking for a 3rd party codec. If that's the problem we cannot use readObject()directly from Socket class because the protocol is base64 encoded.

Thanks a lot.

Oscar said...

For my project I need to convert a java object to amf, I found your article on it and it looks like I'll need to use the amf3Output class.

I downloaded the blazeDS source files but I wasn't able to find
flex.messaging.io.amf.Amf3Output
anywhere.
Where is it...? Or am I doing something wrong?
Thanks in advance =)

ronen said...

Well iv found it under the3.2 tag.

It there a reason your using the source instead of the jar?

Oscar said...

I could only find 2 downloads, one with the source, and one with a war. I didn't know what a war was, but you pointed me in the right direction, I just managed to extract its contents and get the jar =)
Thanks a lot, I really appreciate it.
And good tutorial too =)

ronen said...

Cool, glad it helped

Oscar said...

Heh, sorry, looks like I'm in need of even more help. I've now tried to convert an object to amf3 in java but I'm having problems.
I Suppose these 3 lines are basically what I need:
amf3Output = new Amf3Output(context);
amf3Output.setOutputStream(bout);
amf3Output.writeObject(source);
the second and third lines make sense, it's the first that confuses me. "context" what are you supposed to pass in there? I've tried finding the answer for quite a while now but can't seem to find it.
Any help is very appreciated.
Thanks in advance

ronen said...

The example shown uses guice which is an IOC framework, the SerializationContext is injected into the context data member.

The SerializationContextProvider is the factory from which context instances are created (see the get method).

You don't have to use guice, but it does make the code cleaner.

Oscar said...

Okay...
So what do I need to do to get amf3Output to work? Either without or with guice i guess.

I do see you pass in the variable "context" when creating the amf3Output, but I don't even see where it's initialized or anything, just where it's declared, so I'm confused =S

Thanks again...

ronen said...

This should work http://gist.github.com/148668

Oscar said...

Thanks a lot for the time you're taking trying to help me.
I made a test-app and put that code you provided me with in the main-function, but it still spits out an error.
Could you take a look at it and see where I've gone wrong?
The error is at line 40

http://gist.github.com/148763

Thank You

ronen said...

Try removing line 22 (the braces don't seem to be balanced),
In any case iv already published the entire original example here.

It uses maven, in order to build it you will need a maven proxy server (artifactory).

Oscar said...

Well, the braces are balanced. And it's not a syntax or compiler error, it's a runtime error at that line.
Thanks, I'll take a look at your code when i get home and see if I can adopt it to my project. But.. Well, I don't really know what maven is. Would I need to use maven just to get that part working? All I want to do is convert a simple object to a amf-bytearray, I figured that coulnd't be so complicated but it looks like I was wrong...

Oscar said...

I've done some more research now, firstly I've found this working example using blazeDS.
And secondly I've found out graniteDS can also convert objects in java to AMF3. I've looked at the API documentation and it looks to me I just need to create a amf3serializer object and pass a input-object and a output-bytearray.
I'll compare the performance of these two when i get home.
Hopefully, and problaby I'll manage to do this with these 2 options.
Thanks a lot for all your help, Ronen =)

Caius said...

Hello,
I have been working on your examples for a while but i cannot make them to work.
I have created a socket server and whenever i send data to the server i get extra caracters and if i run fromAmf(String).. it totally decodes it wrong.
Let's say i solve the extra caracters problem, how can i extract the object from the Amf3Input amf3Input ?
Thanks,

ronen said...

amf3Input.readObject();// reads out the object.

The best advice that i can give is to take a look into this, its the complete example on both ends.

Caius said...

Firstly the example is very useful to understand, thanks.
But what i don understant is why you decode and encode the data that you are sending back and forth, does it reduce the amount of data ?
I have been working with your example, and asked around because it had problems. I asled the guy from RED5 server and he told me:

"but why would you serialize a binary format to string and send the string instead of the binary data ?"
and " amf serialization is player native. just make sure to use Socket.writeObject and Socket.readObject (also available in ByteArray so you collect your data in a ByteArray and write the resulting bytes to the socket using Socket.writeBytes)"

So I dont think you need to encode/decode and also you should send the data in raw format, after you call writeObject. What do you think ?

Oscar said...

Caius, decoding it does not recude the data, it increases it. I removed the base64 decoding for my code. Normally it's a waste of both cpu and bandwidth, but if you are using SmartFoxServer for instance, you need to decode it since it only handles strings and not byteArrays.

By the way, Ronen.. The code i showed you which i cut out from yours and edited was correct and it did work.
The reason I couldn't get it to work was because I had missed to add one of the needed libraries, heh stupid me.
I assumed I would get a compile-time error for that, I'm not that experienced with Java.

By the way, here's another working implementation of BlazeDS-serialization
http://snipplr.com/view.php?codeview&id=7820

Caius said...

Oscar, you seem to have some experience working with socket servers. What server would you recommand i use for a online poker? Should i create my own server from scratch or i ca modify an existing one like red5 ?
Thanks.

Oscar said...

Yeah, "some" not a lot though.
Personally I would not create my own socket server, I don't really see a reason not to an existing one like red5, SmartFoxServer, Flash Media Interactive Server, Project Darkstar, etc.

red5 I can't say much about.

SmartFoxServer, also Java, is quite nice but it has some limits, and its not free either.

Flash Media Interactive Server is cool, and easy to use. It uses actionscript on the server side.
But it's expensive and has no scalabilty features.

I'd go with Project Darkstar which is open source, that's what I'm using now and I love it, also Java.
I's still under heavy development and currently only supports one node, but it's working real good and keeps getting better.

ronen said...

The reason iv used BASE64 encoding was due to the fact that the example uses the stomp Java library which send strings, if your using a bare socket than its possible to send the binary data without the encoding.

Id like also to recommend you both to take a look on the Spring BlazeDS integration project.

My example is good when you are trying to use bare (or custom) protocols (like stomp or XMPP), not for so called "standard" integrations.

morris said...

If I'm simply trying to read an AMF message in binary format in from disk and perform Amf3Input.readObject() on the input stream, what do I need to do to initialize the SerializableContext? I keep getting classloader exceptions when trying to instantiate Amf3Input.

ronen said...

Can you post the snippet that your are trying to run?
I haven't tried to persist the AMF output but don't see a reason why it cannot be done.