自動生成によるクラス作成の補助

  • 38
    Like
  • 0
    Comment
More than 1 year has passed since last update.

フィールドを持つクラスを定義するときに、

private int id;

このように一つのフィールドを追加する度に、

public int getId() {
    return id;
}

public void setId(int id) {
    this.id = id;
}

7行のコードの追加が必要になります。getterとsetter以外にも、反復的で定型なequals、hashCode、toStringを実装したりしますよね。

IDEのテンプレートやショートカットで生成することもできますが、書かれたコードはレビュワー、コミッターおよび将来のメンテナーとって継続的な負担になります。そしてJavaのこの言語仕様はDRY原則の違反を余儀なくさせています。

Lombok を使うという方法もあります。Lombokを使うことで簡潔にクラスを定義することができますが、LombokはDirty Hackをしています。コンパイラのハックは非標準なため将来的に壊れる可能性があります。Java島とLombok島のように、Lombokを使って書かれたコードはもはやJavaではないのです。

Screen Shot 2015-05-08 at 08.43.54.png

できればもっと安全な方法にしたいですね。

Auto

GoogleはAutoと呼ばれるコードジェネレーターのリポジトリを持っています。
Auto - A collection of source code generators for Java.

Autoはいくつかのサブプロジェクトで構成されています。

  • AutoFactory - JSR-330-compatible factories
  • AutoService - Provider-configuration files for ServiceLoader
  • AutoValue - Immutable value-type code generation for Java 1.6+.
  • Common - Helper utilities for writing annotation processors.

その中の AutoValue を使うと、

@AutoValue
public abstract class Recipe {
    public abstract int id();
    public abstract String title();
    public abstract User user();
}

このようなクラス定義から、

@javax.annotation.Generated("com.google.auto.value.processor.AutoValueProcessor")
final class AutoValue_Recipe extends Recipe {
  private final int id;
  private final String title;
  private final User user;

  AutoValue_Recipe(
      int id,
      String title,
      User user) {
    this.id = id;
    if (title == null) {
      throw new NullPointerException("Null title");
    }
    this.title = title;
    if (user == null) {
      throw new NullPointerException("Null user");
    }
    this.user = user;
  }

  @Override
  public int id() {
    return id;
  }

  @Override
  public String title() {
    return title;
  }

  @Override
  public User user() {
    return user;
  }

  @Override
  public String toString() {
    return "Recipe{"
        + "id=" + id
        + ", title=" + title
        + ", user=" + user
        + "}";
  }

  @Override
  public boolean equals(Object o) {
    if (o == this) {
      return true;
    }
    if (o instanceof Recipe) {
      Recipe that = (Recipe) o;
      return (this.id == that.id())
          && (this.title.equals(that.title()))
          && (this.user.equals(that.user()));
    }
    return false;
  }

  @Override
  public int hashCode() {
    int h = 1;
    h *= 1000003;
    h ^= id;
    h *= 1000003;
    h ^= title.hashCode();
    h *= 1000003;
    h ^= user.hashCode();
    return h;
  }
}

Annotation Processorによってこのようなクラスが生成されます。Gradleを使っているのであればファイルは build/generated/source/apt 以下に生成されるので、リポジトリには含まれません(レビュワーの目に触れることはありません)
では、保存したりモジュール間で受け渡すためにSerialize/Deserializeするにはどうしたらいいでしょうか。

AutoValue + Gson

Jake WhartonがAutoValueとGsonを組み合わせて使うときの提案をしています。
A Gson TypeAdapterFactory which allows serialization of @AutoValue types

新しいアノテーションを定義して、

@Target(TYPE)
@Retention(RUNTIME)
public @interface AutoGson {
}

生成前のクラス(Recipe)から生成後のクラス(AutoValue_Recipe)にクラスを変換してTypeAdapterを生成するようにしています。

public final class AutoValueAdapterFactory implements TypeAdapterFactory {
    @SuppressWarnings("unchecked")
    @Override
    public <T> TypeAdapter<T> create(Gson gson, TypeToken<T> type) {
        Class<? super T> rawType = type.getRawType();
        if (!rawType.isAnnotationPresent(AutoGson.class)) {
            return null;
        }

        String packageName = rawType.getPackage().getName();
        String className = rawType.getName().substring(packageName.length() + 1).replace('$', '_');
        String autoValueName = packageName + ".AutoValue_" + className;

        try {
            Class<?> autoValueType = Class.forName(autoValueName);
            return (TypeAdapter<T>) gson.getAdapter(autoValueType);
        } catch (ClassNotFoundException e) {
            throw new RuntimeException("Could not load AutoValue type " + autoValueName, e);
        }
    }
}

これによって @AutoGson を付けることでGsonでSerialize/Deserializeすることができるようになります。

@AutoValue @AutoGson
public abstract class Recipe {
    public abstract int id();
    public abstract String title();
    public abstract User user();

    public static Recipe create(int id, String title, User user) {
        return new AutoValue_Recipe(id, title, user);
    }
}

このクラスをJSONにすると以下のようになります。

{"id":1,"title":"sushi","user":{...}}

オブジェクトとJSONのキーのマッピングはフィールド名で行われます。つまり、APIのレスポンスで "image_url" のようなキーに対して imageUrl のようなフィールド名に対応させることができません。

@SerializedName はメソッド名に使うことはできませんし、できたとしても生成されたクラスには影響を及ぼすことはできません。

// Recipe.java
@SerializedName("image_url")
public abstract imageUrl(); // '@SerializedName' not applicable to method

自分でアノテーションを作ればできそうですが、ちょっと大変そうなので別の方法を検討しました。

AutoValue + Jackson

Jacksonは @JsonCreator でデシリアライズするときのメソッドを指定することができます。

@AutoValue
public abstract class Recipe {
    @JsonProperty("id")
    public abstract int id();
    @JsonProperty("title")
    public abstract String title();
    @JsonProperty("user")
    public abstract User user();

    @JsonCreator
    public static Recipe create(@JsonProperty("id") int id,
                                @JsonProperty("title") String title,
                                @JsonProperty("user") User user) {
        return new AutoValue_Recipe(id, title, user);
    }
}

@JsonProperty の値を変えることで任意のキーのマッピングをすることができます。
なので、APIのレスポンスのエンティティにAutoValueを使うなら、オブジェクトのマッピングにはJacksonを選択した方が良さそうです。