ほげにっき

hogedigoの日記

JAVA Compiler APIを利用してBeanのgetter、setterを不要にする

以前から、「javaのgetter、setterは面倒だ」と書いてきた。
2007-10-12 - ほげにっき


JDK7から1st class propertyてのがサポートされてgetter、setterが不要になるはず・・だったのだが、どうやらなくなりそうみたいでガクリorz。


まあ、個人的にはpublic fieldアクセスでいいじゃん!てなカンジなのだが、フレームワークやユーティリティによってはJava Beansのproperty仕様に依存している物が多くていろいろ不便だ。*1


てなわけで、最近ちょっと触ってみたCompiler APIを利用してpublic fieldをJava Beansのpropertyとして扱えるようにする仕組みを考えてみた。仕掛けは単純で、public fieldのみの構造体クラスを引数に渡すとgetter、setterを動的に組み込んだサブクラスをコンパイル・ロードして生成してくれる。これで、コーディングはfield直アクセスして、フレームワーク・ユーティリティーはプロパティアクセスできる。

ソース

とりあえず実験コードなので、実用には耐えられません。悪しからず。

public class DynaBeanFactory {
  
  public static <T> T create(Class<T> clazz) throws Exception {
    return getDynaClass(clazz).newInstance();
  }

  public static <T> Class<? extends T> getDynaClass(Class<T> originalClass) throws Exception {

    String packageName = originalClass.getPackage().getName();
    String simpleName = originalClass.getSimpleName() + "___DynaBean";
    String fullName = packageName + "." + simpleName;
    
    StringBuilder src = new StringBuilder();
    src.append("package " + packageName + ";\n" + "public class "
        + simpleName + " extends " + originalClass.getName() + " {\n");

    for (Field field : originalClass.getFields()) {
      String fieldName = field.getName();
      String capitalized = capitalize(fieldName);
      String fieldType = field.getType().getName();
      src.append("  public " + fieldType + " get" + capitalized + "() {\n"
          + "    return this." + fieldName + ";\n"
          + "  }\n"
          + "  public void set" + capitalized + "(" + fieldType + " val) {\n"
          + "    this." + fieldName + "=val;\n"
          + "  }\n");
    }

    src.append("}\n");

    System.out.println(src);
    
    JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
    DiagnosticCollector<JavaFileObject> collector = new DiagnosticCollector<JavaFileObject>();
    JavaFileManager fileManager = new ClassFileManager(compiler, collector);

    try {

      List<JavaFileObject> jfiles = new ArrayList<JavaFileObject>();
      jfiles.add(new StringJavaFileObject(fullName, src.toString()));

      JavaCompiler.CompilationTask task = compiler.getTask(null,
          fileManager, collector, null, null, jfiles);

      if (!task.call()) {
        throw new IllegalStateException("Error!");
      }

      ClassLoader cl = fileManager.getClassLoader(null);
      return (Class<? extends T>) cl.loadClass(fullName);
    } finally {
      fileManager.close();
    }
  }

  private static String capitalize(String name) {
    return Character.toUpperCase(name.charAt(0))
        + (name.length() > 1 ? name.substring(1) : "");
  }

  private static class StringJavaFileObject extends SimpleJavaFileObject {
    private String content;

    public StringJavaFileObject(String className, String content) {

      super(URI.create("string:///" + className.replace('.', '/')
          + Kind.SOURCE.extension), Kind.SOURCE);

      this.content = content;
    }

    @Override
    public CharSequence getCharContent(boolean ignoreEncodingErrors) {
      return content;
    }
  }

  private static class JavaClassObject extends SimpleJavaFileObject {

    public JavaClassObject(String name, Kind kind) {
      super(URI.create("string:///" + name.replace('.', '/')
          + kind.extension), kind);
    }

    protected final ByteArrayOutputStream bos = new ByteArrayOutputStream();

    @Override
    public OutputStream openOutputStream() throws IOException {
      return bos;
    }

    public byte[] getBytes() {
      return bos.toByteArray();
    }

    private Class<?> clazz = null;

    public void setDefinedClass(Class<?> c) {
      clazz = c;
    }

    public Class<?> getDefinedClass() {
      return clazz;
    }
  }

  private static class ClassFileManager extends
      ForwardingJavaFileManager<JavaFileManager> {

    public ClassFileManager(JavaCompiler compiler,
        DiagnosticListener<? super JavaFileObject> listener) {
      super(compiler.getStandardFileManager(listener, null, null));
    }

    private JavaClassObject jclassObject;

    @Override
    public JavaFileObject getJavaFileForOutput(Location location,
        String className, Kind kind, FileObject sibling)
        throws IOException {
      jclassObject = new JavaClassObject(className, kind);
      return jclassObject;
    }

    protected ClassLoader loader = null;

    @Override
    public ClassLoader getClassLoader(Location location) {
      return new SecureClassLoader() {
        @Override
        protected Class<?> findClass(String name)
            throws ClassNotFoundException {
          Class<?> c = jclassObject.getDefinedClass();
          if (c == null) {
            byte[] b = jclassObject.getBytes();
            c = super.defineClass(name, b, 0, b.length);
            jclassObject.setDefinedClass(c);
          }
          return c;
        }
      };
    }
  }
}

テスト↓↓

package test.bean;
public class TestBean {
  public String hoge;
  public String moke;
}
public class DynaBeanFactoryTest extends TestCase {

  public void testCreate() throws Exception {
    TestBean bean = DynaBeanFactory.create(TestBean.class);
    
    bean.hoge = "hoge1!";
    bean.moke = "moke1!";
    
    assertEquals("hoge1!", PropertyUtils.getProperty(bean, "hoge"));
    assertEquals("moke1!", PropertyUtils.getProperty(bean, "moke"));
    
    PropertyUtils.setProperty(bean, "hoge", "hoge2!");
    PropertyUtils.setProperty(bean, "moke", "moke2!");

    assertEquals("hoge2!", bean.hoge);
    assertEquals("moke2!", bean.moke);
  }
}

でけた!(^o^)/


しかし・・・最近はツールがgetter, setterを生成してくれるから・・・あんまりメリットないかな。

*1:SAStrutsはpublic fieldをpropertyの様に扱えるようだ