پنجشنبه، خرداد ۲۷، ۱۳۸۹

A very dynamic Java persistence application

Since I have started my work at my current company about one and a half years age, there were a huge emphasis on MDA and code generation. Though I don't believe that we can build a magic box for generating applications for all different kind of domains, it might be helpful in certain domains. Also generating the redundant and mundane parts of the application like CRUDs can be a real time saver.

One of the problems we had back then was the static nature of the Java language itself. We had to generate Java classes from different models and then compile and run it to see the result. Need to change model? You must go through the tedious generate-compile-run cycle. I didn't like it at all!

In some dynamic languages like Ruby, you can define classes at run time and even change them. If our framework were based on something like Ruby, there were no need for the generation/compilation phase. The framework could read models at startup and define classes on the fly. Unlike ours, there are no intermediate artifacts in this approach.

Recently I have found a way to do a similar thing with Java. Although it is not quite similar. In a language like Ruby, we don't have static type checking so if you know the call of a method in string format, you can send the method's name as a message to the Ruby object for the execution, but in Java you must use the Reflection API for this, which is supposed to be slower than normal method call.

So how we can define classes in Java at runtime? The key is a library named Javassist. Javassist is really easy to use! Love it :)

Enough for the history and theory, let's get our hand dirty and write some code to generate a class named Person using javassist. I have used the version 3.8 of this library which is also available in maven repositories.

ClassPool cp = ClassPool.getDefault();
CtClass ctClass = cp.makeClass("test.Person");

CtField idField = new CtField(cp.get(Long.class.getName()), "id", ctClass);
ctClass.addField(idField);

CtField nameField = new CtField(cp.get(String.class.getName()), "name", ctClass);
ctClass.addField(nameField);

CtField ageField = new CtField(cp.get(int.class.getName()), "age", ctClass);
ctClass.addField(ageField);

Class personClass = ctClass.toClass();


The code is self-explanatory, but if you have further questions, please ask it in comments. Also there is a good tutorial available here if you are interested to learn more. Basically the code above, defines a class named "Person" which resides in the "test" package. It has three field: id of type Long, name of type String and age of type int. the last line creates the actual Java class and loads it in JVM.

So now we have the Java class, like any other statically written one. How can we instantiate it and set its properties? The answer lies in the standard Java Reflection API.

Object person = personClass.newInstance();
Field field = personClass.getDeclaredField("name");
field.set(person, "Wickoo");

If the main class that runs these lines of code is also in the "test" package, the code runs successfully, otherwise you get the java.lang.IllegalAccessException exception, because the name field has no access modifier yet (package access). To overcome this, you have multiple choices. The simpler and more obvious one is to call

field.setAccessible(true);

before accessing the field. Or you can generate a public field in the first place.

CtField nameField = new CtField(cp.get(String.class.getName()), "name", ctClass);
nameField.setModifiers(Modifier.PUBLIC);

Or you can generate a setter:

CtMethod setNameMethod = CtNewMethod.make("public void setName(String name) {this.name = name;}", ctClass);
ctClass.addMethod(setNameMethod);

And then invoke the setter method:

Object person = personClass.newInstance();
Method method = personClass.getDeclaredMethod("setName", String.class);
method.invoke(person, "Wickoo");

I think now you must have a good idea about what Javassist is capable of. The aim of this post is to generate a class at runtime, instantiate it and the persist it.

I'm going to use JPA and Hibernate as its implementation here. For JPA you need to put a persistence.xml file inside META-INF folder in the class path. The content of persistence.xml are shown below:


xmlns="http://java.sun.com/xml/ns/persistence"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/persistence
http://java.sun.com/xml/ns/persistence/persistence_1_0.xsd"
version="1.0">

org.hibernate.ejb.HibernatePersistence













In order to be able to persist a Java class using JPA, you need to annotate the class with @Entity or write a XML mapping file. Personally I like annotations. Also it shows the power of Javassit for generating annotations. So before proceeding further with the persistence let us generate an @Entity annotation for our class:

AnnotationsAttribute attr = new AnnotationsAttribute(classFile
.getConstPool(), AnnotationsAttribute.visibleTag);
Annotation entityAnnotation = new Annotation(Entity.class.getName(), classFile.getConstPool());
attr.addAnnotation(entityAnnotation);
classFile.addAttribute(attr);

We also need two more annotations to go. @Id and @GeneratedValue on the id field. All other fields will be persisted to the corresponding columns with the same name by convention. So we change how we make idField to this:

AnnotationsAttribute attribute = new AnnotationsAttribute(classFile
.getConstPool(), AnnotationsAttribute.visibleTag);

Annotation idAnnotation = new Annotation(Id.class.getName(), classFile.getConstPool());
attribute.addAnnotation(idAnnotation);

Annotation gvAnnotation = new Annotation(GeneratedValue.class.getName(), classFile.getConstPool());
attribute.addAnnotation(gvAnnotation);

CtField idField = new CtField(cp.get(Long.class.getName()), "id", ctClass);
idField.getFieldInfo().addAttribute(attribute);
ctClass.addField(idField);

Now we have everything in place. Let's try to persist it:

EntityManagerFactory emf = Persistence.createEntityManagerFactory("dtest");
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
Object person = personClass.newInstance();
em.persist(person);
em.getTransaction().commit();

After running this code, you'll get the "java.lang.IllegalArgumentException: Unknown entity: test.Person" exception. The reason for this is obvious! Hibernate can not find the entity's class anywhere. You must add it manually to Hibernate:

Ejb3Configuration configuration = new Ejb3Configuration();
configuration.addAnnotatedClass(personClass);
configuration.configure("dtest", null);
EntityManagerFactory emf = configuration.buildEntityManagerFactory();
EntityManager em = emf.createEntityManager();
em.getTransaction().begin();
Object person = personClass.newInstance();
em.persist(person);
em.getTransaction().commit();

Now you can check the database to see it with your own eyes that it works! Use the reflection methods described above to set person fields' values if you don't want to persist an empty object.

Please put your thoughts about this approach in the comments.

۱ نظر: