Search This Blog

Wednesday 25 July 2012

Using Interception in Hibernate -1

As we saw in our earlier post, Hibernate allows us to lazily load columns in a table row using interception. Interception is used instead of proxies. Consider our Book- Shelf relation. If we wish to replace the Shelf proxy and instead using interception then the hbm files would have the following change:
<?xml version="1.0"?>
<!DOCTYPE hibernate-mapping PUBLIC "-//Hibernate/Hibernate Mapping DTD 3.0//EN"
"http://hibernate.sourceforge.net/hibernate-mapping-3.0.dtd">
<hibernate-mapping package="com.interception">
    <class name="Book" table="BOOK">
        <id name="id" type="integer">
            <column name="ID" />
            <generator class="native" />
        </id>
        <property name="name" type="string">
            <column name="Name" length="50" not-null="true" />
        </property>
        <many-to-one name="shelf" class="Shelf" foreign-key="BOOK_FK1"
            lazy="no-proxy">
            <column name="shelf_id"></column>
        </many-to-one>

    </class>
</hibernate-mapping>
The default value of lazy attribute (true) has been replaced by no-proxy. This tells Hibernate to use interception for the Shelf association instead of a Proxy. I wrote the below code to test the interception:
public static void testLoad() {
    Session session = sessionFactory.openSession();
    Transaction transaction = session.beginTransaction();
    Book book = (Book) session.load(Book.class, book1Id);
    System.out.println("book with id " + book.getId() + " is on "
            + book.getShelf().getId());
    System.out.println("shelf id is  " + book.getShelf().getId()); 
    System.out.println("Shelf class is " + book.getShelf().getClass());
    transaction.commit();
    session.close();
}
However this is not sufficient. For the code to work, Hibernate requires that the byte code of the class be instrumented after compilation. For this I moved my models and hbms into a simple java project.
The next step was to instrument the code. This was achieved using the build file below:
<?xml version="1.0" encoding="UTF-8"?>
<project name="buildModelJar" basedir="." default="jar">

    <property name="src.dir" value="${basedir}/src" />
    <property name="lib.dir" value="${basedir}/lib" />
    <property name="build.dir" value="${basedir}/build" />
    <property name="classes.dir" value="${build.dir}/classes" />
    <property name="jar.dir" value="${build.dir}/jar" />

    <path id ="project.toolpath">
        <fileset dir="${lib.dir}">
            <include name="**/*.jar" />
        </fileset>
    </path>

    <target name="clean">
        <echo message="Deleting build folders"/>
        <delete dir="${build.dir}" />
        <echo message="Deleting build folders complete "/>
    </target>

    <target name="compile" depends="clean">
        <echo message="Creating requisite folders"/>
        <mkdir dir="${build.dir}" />
        <mkdir dir="${classes.dir}" />
        <echo message="Compile entities"/>
        <javac srcdir="src" destdir="build/classes" />
        <copy todir="${classes.dir}">
            <fileset dir="${src.dir}" excludes="**/*.java" />
        </copy>
        <echo message="Copy hbm files"/>
    </target>

    <target name="instrument" depends="compile">
        <echo message="Instrument code..."/>
        <taskdef name="instrument" 
            classname="org.hibernate.tool.instrument.javassist.InstrumentTask">
            <classpath refid="project.toolpath"/>
        </taskdef>
        <instrument verbose="true">
            <fileset dir="${classes.dir}/com/interception">
                <include name="*.class" />
            </fileset>
        </instrument>
    </target>

    <target name="jar" depends="instrument">
        <echo message="Building jar"/>
        <mkdir dir="${jar.dir}" />
        <jar destfile="${jar.dir}/model.jar" basedir="${classes.dir}"/>
        <echo message="Jar creation complete"/>
    </target>

</project>
The instrument task is where all the magic happens. It is called after all the code is compiled but before creating the jar. Instead of Javassist, CGLIB can also be used to achieve the byte code instrumentation.
Unfortunately my task kept failing due to incompatible jars. As a last ditch effort I migrated to Hibernate 4 jars for getting the build task running. The task output is as below:
[mkdir] Created dir: D:\work\Hibernate\Hibernate workspace\InterceptionProje
ct\build\classes
     [echo] Compile entities
    [javac] Compiling 2 source files to D:\work\Hibernate\Hibernate workspace\In
terceptionProject\build\classes
     [copy] Copying 2 files to D:\work\Hibernate\Hibernate workspace\Interceptio
nProject\build\classes
     [echo] Copy hbm files
instrument:
     [echo] Instrument code...
[instrument] starting instrumentation
[instrument] processing class : com.interception.Shelf;  file = D:\work\Hibernat
e\Hibernate workspace\InterceptionProject\build\classes\com\interception\Shelf.c
lass
[instrument] processing class : com.interception.Book;  file = D:\work\Hibernate
\Hibernate workspace\InterceptionProject\build\classes\com\interception\Book.cla
ss
jar:
     [echo] Building jar
I then created a separate java project and added the newly created jar to the class path for execution.
The next step was to execute the earlier test method:
Hibernate: 
    /* sequential select
        com.interception.Book */  
        select
            book_.Name as Name0_,
            book_.shelf_id as shelf3_0_ 
        from
            BOOK book_ 
        where
            book_.ID=?
Hibernate: 
    /* load com.interception.Shelf */ 
    select
        shelf0_.ID as ID1_0_,
        shelf0_.CODE as CODE1_0_ 
    from
        SHELF shelf0_ 
    where
        shelf0_.ID=?
book with id 1 is on 1
shelf id is  1
Shelf class is class com.interception.Shelf
If proxy had been used instead the output would be as below:
Hibernate: 
    /* load com.interception.Book */  
    select
        book0_.ID as ID0_0_,
        book0_.Name as Name0_0_,
        book0_.shelf_id as shelf3_0_0_ 
    from
        BOOK book0_ 
    where
        book0_.ID=?
book with id 1 is on 1
shelf id is  1
Shelf class is class com.interception.Shelf$$EnhancerByCGLIB$$512b15bf
As can be seen, in case of interception there are no proxies involved. Even the SQL select queries are slightly different.
Also when we accessed the id of shelf, an sql query was fired in case of interception. The same is not the case with proxy. The object is loaded only when non-identity fields are accessed. Thus proxies are more lazy than Interception.
Interception however is necessary if we want to work with lazy properties. I decided to try that out here.
This is the code that I managed to decompile from the instrumented class file. It does not seem to make much sense, in fact it does not even compile. I have just added here:
package com.interception;

import java.util.HashSet;
import java.util.Set;

import org.hibernate.bytecode.internal.javassist.FieldHandled;
import org.hibernate.bytecode.internal.javassist.FieldHandler;

public class Shelf implements FieldHandled {

    private Integer id;
    private String code;
    private Set<Book> allBooks;
    private transient FieldHandler $JAVASSIST_READ_WRITE_HANDLER;

    public Shelf() {
        $javassist_write_allBooks(new HashSet());
    }

    public Integer getId() {
        return $javassist_read_id();
    }

    public void setId(Integer paramInteger) {
        $javassist_write_id(paramInteger);
    }

    public String getCode() {
        return $javassist_read_code();
    }

    public void setCode(String paramString) {
        $javassist_write_code(paramString);
    }

    public Set<Book> getAllBooks() {
        return $javassist_read_allBooks();
    }

    public void setAllBooks(Set<Book> paramSet) {
        $javassist_write_allBooks(paramSet);
    }

    public FieldHandler getFieldHandler() {
        return this.$JAVASSIST_READ_WRITE_HANDLER;
    }

    public void setFieldHandler(FieldHandler paramFieldHandler) {
        this.$JAVASSIST_READ_WRITE_HANDLER = paramFieldHandler;
    }

    public Integer $javassist_read_id() {
        if (getFieldHandler() == null)
            return this.jdField_id_of_type_JavaLangInteger;
    }

    public void $javassist_write_id(Integer paramInteger) {
        if (getFieldHandler() == null) {
            this.jdField_id_of_type_JavaLangInteger = paramInteger;
            return;
        }
        this.jdField_id_of_type_JavaLangInteger = ((Integer) getFieldHandler()
                .writeObject(this, "id",
                        this.jdField_id_of_type_JavaLangInteger, paramInteger));
    }

    public String $javassist_read_code() {
        if (getFieldHandler() == null)
            return this.jdField_code_of_type_JavaLangString;
    }

    public void $javassist_write_code(String paramString) {
        if (getFieldHandler() == null) {
            this.jdField_code_of_type_JavaLangString = paramString;
            return;
        }
        this.jdField_code_of_type_JavaLangString = ((String) getFieldHandler()
                .writeObject(this, "code",
                        this.jdField_code_of_type_JavaLangString, paramString));
    }

    public Set $javassist_read_allBooks() {
        if (getFieldHandler() == null)
            return this.jdField_allBooks_of_type_JavaUtilSet;
    }

    public void $javassist_write_allBooks(Set paramSet) {
        if (getFieldHandler() == null) {
            this.jdField_allBooks_of_type_JavaUtilSet = paramSet;
            return;
        }
        this.jdField_allBooks_of_type_JavaUtilSet = ((Set) getFieldHandler()
                .writeObject(this, "allBooks",
                        this.jdField_allBooks_of_type_JavaUtilSet, paramSet));
    }
}
That is what was inside the instrumented class.

No comments:

Post a Comment