Understanding bytecode enhancement

This recipe should show you why it is important to understand the basic concepts of bytecode enhancement and how it is leveraged in the Play framework. The average developer usually does not get in contact with bytecode enhancement. The usual build cycle is compiling the application, and then using the created class files.

Bytecode enhancement is an additional step between the compilation of the class files and the usage by the application. Basically this enhancement step allows you to change the complete behavior of the application by changing what is written in the class files at a very low level. A common use case for this is aspect oriented programming, is where you add certain features to methods after the class has been compiled. A classical use case for this is the measurement of method runtimes.

If you have already explored the source code of the persistence layer you might have noticed the use of bytecode enhancement. This is primarily to overcome a Java weakness: static methods cannot be inherited with class information about the inherited class, which seems pretty logical, but is a major obstacle. You have already used the Model class in most of your entities. It features the nice findAll() method, or its simpler companion, the count() method. However, if you defined a User entity extending the Model class, all invocations of User.findAll() or User.count() will always invoke the Model.findAll() or Model.count(), which would never return any user entity specific data.

This is exactly the place where the bytecode enhancement kicks in. When starting your Play application, every class which inherits from the Model class is enhanced to return entity specific data, when the static methods from findAll() or count() are executed. As this is not possible with pure java, bytecode enhancement is used to circumvent this problem.

If you take a look at the play.db.jpa.JPAEnhancer class, you will see that the code which is inserted by the enhancer is actually written inside quotes and does not represent real code inside the Java class, which is checked during compilation by the compiler. This has the problem of being error prone, as typos are caught only when the actual bytecode enhancement happens or, even worse, when the code is executed. There is currently no good way around this.

The source code of the example is available at examples/chapter5/bytecode-enhancement.

Getting ready

The example in this recipe will make use of the search module, which features fulltext search capabilities per entity. Make sure, it is actually installed by adding it to the conf/dependencies.yml file. You can get more information about the module at http://www.playframework.org/modules/search. This example will enhance every class with a method, which returns whether the entity is actually indexed or not.

This could also be solved with the reflection API of course by checking for annotations at the entity. This should just demonstrate how bytecode enhancement is supposed to work. So create an example application which features an indexed and a not indexed entity.

Write the Test which should be put into the application:

public class IndexedModelTest extends UnitTest {

    @Test
    public void testThatUserIsIndexed() {
       assertTrue(User.isIndexed());
       assertTrue(User.getIndexedFields().contains("name"));
       assertTrue(User.getIndexedFields().contains("descr"));
       assertEquals(2, User.getIndexedFields().size());
    }

    @Test
    public void testThatOrderIndexDoesNotExist() {
       assertFalse(Order.isIndexed());
       assertEquals(0, Order.getIndexedFields().size());
    }
}

How to do it...

When you have written the preceding test, you see the use of two entities, which need to be modeled. First the User entity:

@Entity
@Indexed
public class User extends IndexedModel {

   @Field
   public String name;
   @Field
   public String descr;
}

In case you are missing the Indexed and Field annotations, you should now really install the search module, which includes these as described some time back in this chapter. The next step is to create the Order entity:

@Entity(name="orders")
public class Order extends IndexedModel {

   public String title;
   public BigDecimal amount;
}

Note the change of the order table name as order is a reserved SQL word. As you can see, both entities do not extend from model, but rather extend from IndexedModel. This is a helper class which is included in the module we are about to create now. So create a new module named bytecode-module. Create the file bytecode-module/src/play.plugins with this content:

 1000:play.modules.searchhelp.SearchHelperPlugin

Create the IndexedModel class first in bytecode-module/src/play/modules/searchhelp/IndexedModel.java:

public abstract class IndexedModel extends Model {

   public static Boolean isIndexed() {
      return false;
   }
   
   public static List<String> getIndexedFields() {
      return Collections.emptyList();
   }
}

The next step is to create the bytecode enhancer, which is able to enhance a single entity class. So create bytecode-module/src/play/modules/searchhelp/SearchHelperEnhancer.java:

public class SearchHelperEnhancer extends Enhancer {

   @Override
   public void enhanceThisClass(ApplicationClass applicationClass) throws Exception {
      CtClass ctClass = makeClass(applicationClass);

        if (!ctClass.subtypeOf(classPool.get("play.modules.searchhelp.IndexedModel")) || 
              !hasAnnotation(ctClass, "play.modules.search.Indexed")) {
            return;
        }
        
        CtMethod isIndexed = CtMethod.make("public static Boolean isIndexed() { return Boolean.TRUE; }", ctClass);
        ctClass.addMethod(isIndexed);

        List<String> fields = new ArrayList<string>();
        for (CtField ctField : ctClass.getFields()) {
           if (hasAnnotation(ctField, "play.modules.search.Field")) {
              fields.add(""" + ctField.getName() + """);
           }
        }
        
        String method;
        if (fields.size() > 0) {
           String fieldStr = fields.toString().replace("[", "").replace("]", "");
           method = "public static java.util.List getIndexedFields() { return java.util.Arrays.asList(new String[]{" + fieldStr + "}); }";
           CtMethod count = CtMethod.make(method, ctClass);
           ctClass.addMethod(count);
        }
        
        applicationClass.enhancedByteCode = ctClass.toBytecode();
        ctClass.defrost();
   }
}

The last part is to create the plugin, which actually invokes the enhancer on startup of the application. The plugin also features an additional output in the status pages. Put the file into bytecode-module/src/play/modules/searchhelp/SearchHelperPlugin.java:

public class SearchHelperPlugin extends PlayPlugin {

   private SearchHelperEnhancer enhancer = new SearchHelperEnhancer();

   @Override
   public void enhance(ApplicationClass applicationClass) throws Exception {
      enhancer.enhanceThisClass(applicationClass);
   }

   @Override
   public JsonObject getJsonStatus() {
      JsonObject obj = new JsonObject();
      List<ApplicationClass> classes = Play.classes.getAssignableClasses(IndexedModel.class);

      for (ApplicationClass applicationClass : classes) {
         if (isIndexed(applicationClass)) {
            List<String> fieldList = getIndexedFields(applicationClass);
            JsonArray fields = new JsonArray();

            for (String field :fieldList) {
               fields.add(new JsonPrimitive(field));
            }

            obj.add(applicationClass.name, fields);
         }
      }

      return obj;
   }


   @Override
   public String getStatus() {
      String output = "";

      List<ApplicationClass> classes = Play.classes.getAssignableClasses(IndexedModel.class);

      for (ApplicationClass applicationClass : classes) {
         if (isIndexed(applicationClass)) {
            List<String> fieldList = getIndexedFields(applicationClass);
            output += "Entity " + applicationClass.name + ": " + fieldList +  "
";
         }
      }

      return output;
   }

   private List<String> getIndexedFields(ApplicationClass applicationClass) {
      try {
         Class clazz = applicationClass.javaClass;
         List<String> fieldList = (List<String>) clazz.getMethod("getIndexedFields").invoke(null);
         return fieldList;
      } catch (Exception e) {}
      
      return Collections.emptyList();
   }
   
   private boolean isIndexed(ApplicationClass applicationClass) {
      try {
         Class clazz = applicationClass.javaClass;
         Boolean isIndexed = (Boolean) clazz.getMethod("isIndexed").invoke(null);
         return isIndexed;
      } catch (Exception e) {}
      
      return false;
   }
}

After this, build the module, add the module dependency to the application which includes the test at the beginning of the recipe, and then go to the test page at http://localhost:9000/@tests and check if the test is running successfully.

How it works...

Though the module consists of three classes, only two should need further explanation. The SearchHelperEnhancer basically checks whether @Indexed and @Field annotations exist, and overwrites the statically defined methods of the IndexedModel class with methods that actually return true in case of isIndexed() and a list of indexed fields in case of getIndexedFields() .

As already mentioned in the overview of the modules, the enhance() method inside of any Play plugin allows you to include your own enhancers and is basically a one liner in the plugin, as long as you do type checking in the enhancer itself.

As you can see in the source, the code which is actually enhanced, is written as text string inside of the CtMethod.make() method in the enhancer. This is error prone, as typos or other mistakes cannot be detected at compile time, but only on runtime. Currently, this is the way to go. You could possibly try out other bytecode enhancers such as JBoss AOP, if this is a big show stopper for you. You can read more about JBoss AOP at http://www.jboss.org/jbossaop:

This recipe shows another handy plugin feature. The plugin also implements getStatus() and getJsonStatus() methods. If you run play status in the directory of your application while it is running, you will get the following output at the end:

SearchHelperPlugin:
~~~~~~~~~~~~~~~~~~~
Entity models.User: [name, descr]

There's more...

As writing your own enhancers inside of your own plugins is quite simple, you should check out the different persistence modules, where enhancers are used for the finders. If you want to get more advanced, you should also check out the enhancement of controllers.

Overriding toString() via annotation

Peter Hilton has written a very good article on how one can configure the output of the toString() method of an entity with the help of an annotation by using bytecode enhancement. You can check it out at http://www.lunatech-research.com/archives/2011/01/11/declarative-model-class-enhancement-play.

See also

In the next chapter the recipe Adding annotations via bytecode enhancements will add annotations to classes via bytecode enhancement. This can be used to prevent repeating placing annotations all over your model classes.

..................Content has been hidden....................

You can't read the all page of ebook, please click here login for view all page.
Reset
3.144.15.154