フィールドを持つクラスを定義するときに、
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ではないのです。
できればもっと安全な方法にしたいですね。
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を選択した方が良さそうです。