piątek, 21 października 2011

Mixins aka traits implementation in java

Mixins, also known as traits in some programming languages, are very useful language constructions. They cover most of the use cases where one's might want to use multi-inheritance but conceptually are much simpler. Unfortunately java does not support mixins. Nonetheless with some help from one of bytecode generation libraries like CGLib or javassit it is possible to implement simple mixins support in less then 100 lines of code.

The idea is to use java interface to define mixin methods. Additionally such interface should be marked with @Mixin annotation which will provide information about the class which implements mixin interface and acts as, possibly incomplete, mixin implementation.

@Retention(RetentionPolicy.RUNTIME)
@Target(TYPE)
public @interface Mixin {
  /** 
   * Class implementing mixin interface.
   */
  Class<?> impl();
}

The class which wants to extend mixin must be defined as abstract and must implement mixin interface. Class might extend more then one mixin.

The actual, non abstract implementation for mixin methods is build by helper class which here is called MixinBuilder.

Simple yet complete implementation of MixinBuilder using javassit library is presented below. It does not perform any kind of validation of the setup. Moreover it does not handle the case when more then one mixin defines and implement the same method or two mixins depend on each other. But all those limitations are quite easy extensions to presented code.

import java.lang.reflect.Method;
import java.lang.reflect.Modifier;
import java.util.HashMap;
import java.util.Map;

import javassist.util.proxy.MethodHandler;
import javassist.util.proxy.ProxyFactory;

public class MixinBuilder {
 private static Object invoke(Method method, Object target, 
    Object[] args) throws Exception {
  return target.getClass().getMethod(method.getName(),
    method.getParameterTypes()).invoke(target, args);
 }
 
 @SuppressWarnings("unchecked")
 private static <T> T newMixinInstance(final Object parent, 
    final Class<T> mixinImpl) {
  try {
   final ProxyFactory proxy = new ProxyFactory();
   proxy.setSuperclass(mixinImpl);
   return (T) proxy.create(null, null, new MethodHandler() {
    public Object invoke(Object target, Method method, 
      Method superMethod, Object[] args) 
      throws Throwable {
     // Delegate invocations of abstract methods 
     // to parent object.
     if (Modifier.isAbstract(method.getModifiers())) {
      return MixinBuilder.invoke(method, parent, args);
     }
     return superMethod.invoke(target, args);
    }
   });
  } catch (Exception e) {
   throw new IllegalArgumentException(e);
  }
 }
 
 @SuppressWarnings("unchecked")
 public static <T> T newInstance(final Class<T> mainClass) {
  try {
   final ProxyFactory proxy = new ProxyFactory();
   proxy.setSuperclass(mainClass);
   return (T) proxy.create(null, null, new MethodHandler() {
    private Map<Class<?>,Object> mixinMap = 
      new HashMap<Class<?>, Object>();
    
    public Object invoke(Object target, Method method, 
        Method superMethod, Object[] args) 
        throws Throwable {
     // Delegate invocations of abstract methods 
     // to mixin.
     if (Modifier.isAbstract(method.getModifiers())) {
      Class<?> mixinInterface = method.getDeclaringClass();
      if (!mixinMap.containsKey(mixinInterface)) {
       Mixin annotation = mixinInterface.getAnnotation(
          Mixin.class);
       final Class<?> mixinClass = annotation.impl();
       mixinMap.put(mixinInterface, 
          newMixinInstance(target, mixinClass));
      }
      Object mixin = mixinMap.get(mixinInterface);
      return MixinBuilder.invoke(method, mixin, args);
     }
     return superMethod.invoke(target, args);
    }
   });
  } catch (Exception e) {
   throw new IllegalArgumentException(e);
  }
 }

Sample usage of MixinBuilder might look like this:

@Mixin(impl = MixinAImpl.class)
 public interface MixinA {
  void a();
  void doA();
 }
 
 public abstract class MixinAImpl implements MixinA {
  public void a() {
   System.out.println("MixinAImpl.a()");
   doA();
  }
 }
 
 @Mixin(impl = MixinBImpl.class)
 public interface MixinB {
  void b();
  void doB();
 }
 
 public abstract class MixinBImpl implements MixinB {
  public void b() {
   System.out.println("MixinBImpl.b()");
   doB();
  }
 }
 
 public abstract class Test implements MixinA, MixinB {
  public void doA() {
   System.out.println("Test.doA()");
   b();
  }
  public void doB() {
   System.out.println("Test.doB()");
  }
  public void test() {
   System.out.println("Test.test()");
   a();
  }
 
  public static void main(String[] args) {
   MixinBuilder.newInstance(Test.class).test();
  }
 }

When executed it will display following output:

Test.test()
MixinAImpl.a()
Test.doA()
MixinBImpl.b()
Test.doB()

We can clearly see how execution of single method on Test class causes MixinAImpl execution which indirectly calls Test and MixinBImpl.

If you need more complete ready to use solution check out: Multiple Inheritance and Traits in Java

Brak komentarzy:

Prześlij komentarz