In this article we will create a plugin for Speedment that generates serialization and deserialization logic using Gson to make it super easy to map between database entities and JSON strings. This will help to showcase the extendability of the Speedment code generation while at the same time explore some of the cool features of the Gson library.
Speedment is a code generation tool for java that connects to a database and use it as a reference for generating entity and manager files for your project. The tool is very modular, allowing you to write your own plugins that modify the way the resulting code will look. One thing several people have mentioned on the Gitter chat is that Speedment entities are declared abstract which prevents them from being automatically deserialized. In this article we will look at how you can deserialize Speedment entities using Gson by automatically generating a custom TypeAdapter for each table in the database. This will not only give us better performance when working with JSON representations of database content, but might also serve as a general example on how you can extend the code generator to solve your problems.
Step 1: Creating the Plugin Project
In a previous article I went into detail on how to create a new plugin for Speedment, so here is the short version. Create a new maven project and set Speedment and Gson as dependencies.
pom.xml
<name>Speedment Gson Plugin</name>
<description>
A plugin for Speedment that generates Gson Type Adapters for every
table in the database.
</description>
<properties>
<project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
<maven.compiler.source>1.8</maven.compiler.source>
<maven.compiler.target>1.8</maven.compiler.target>
<speedment.version>2.3.7</speedment.version>
</properties>
<dependencies>
<dependency>
<groupId>com.speedment</groupId>
<artifactId>speedment</artifactId>
<version>${speedment.version}</version>
</dependency>
<dependency>
<artifactId>gson</artifactId>
<groupId>com.google.code.gson</groupId>
<version>2.6.2</version>
</dependency>
</dependencies>
Step 2: Create a Translator Class for the Type Adapter
Next we need to create the translator that will generate the new type adapter for us. A translator is a class that describes what name, path and content a generated file will have. To that it has a lot of convenience methods to make it easier to generate the code. The basic structure of the translator is shown below.
GeneratedTypeAdapterTranslator.java
...
public GeneratedTypeAdapterTranslator(
Speedment speedment, Generator gen, Table table) {
super(speedment, gen, table, Class::of);
}
@Override
protected Class makeCodeGenModel(File file) {
return newBuilder(file, getClassOrInterfaceName())
.forEveryTable((clazz, table) -> {
// Code generation goes here
}).build();
}
@Override
protected String getClassOrInterfaceName() {
return "Generated" + getSupport().typeName() + "TypeAdapter";
}
@Override
protected String getJavadocRepresentText() {
return "A Gson Type Adapter";
}
@Override
public boolean isInGeneratedPackage() {
return true;
}
...
Every translator is built up using a builder pattern that can be invoked using the newBuilder()
-method. This becomes important later on when we want to modify an existing translator. The actual code is generated inside the builder’s forEveryTable()
-method. It is a callback that will be executed for every table that is encountered in the scope of interest. In this case, the translator will only execute on one table at a time so the callback will only be executed once.
For complete sources for the GeneratedTypeAdapterTranslator
-class, please go to this github page.
Step 3: Create Decorator for Modifying the Manager Interface
Generating a bunch of TypeAdapters
is not enough though. We want to integrate the new code into the already existing managers. To do this, we need to define a decorator that will be applied to every generated manager after the default logic has been executed.
GeneratedManagerDecorator.java
public final class GeneratedManagerDecorator
implements TranslatorDecorator<Table, Interface> {
@Override
public void apply(JavaClassTranslator<Table, Interface> translator) {
translator.onMake((file, builder) -> {
builder.forEveryTable(Translator.Phase.POST_MAKE,
(clazz, table) -> {
clazz.add(Method.of(
"fromJson",
translator.getSupport().entityType()
).add(Field.of("json", STRING)));
});
});
}
}
A decorator is similar to a translator, except it only defines the changes that should be done to an existing file. Every decorator executes in a specific phase. In our case we want to execute after the default code has been generated, so we select POST_MAKE
. The logic we want to add is simple. In the interface, we want an additional method fromJson(String)
to be required. We don’t need to define a toJson
since every Speedment manager already has that from an inherited interface.
Step 4: Create Decorator for Modifying the Manager Implementation
The manager implementation is a bit trickier to modify. We need to append it with a Gson instance as a member variable, a implementation for the new interface method we just added, an override for the toJson
-method that uses Gson instead of the built-in serializer and we need to modify the manager constructor to instantiate Gson using our new TypeAdapter
.
GeneratedManagerImplDecorator.java
public final class GeneratedManagerImplDecorator
implements TranslatorDecorator<Table, Class> {
@Override
public void apply(JavaClassTranslator<Table, Class> translator) {
final String entityName = translator.getSupport().entityName();
final String typeAdapterName = "Generated" + entityName +
"TypeAdapter";
final String absoluteTypeAdapterName =
translator.getSupport().basePackageName() + ".generated." +
typeAdapterName;
Final Type entityType = translator.getSupport().entityType();
translator.onMake((file, builder) -> {
builder.forEveryTable(Translator.Phase.POST_MAKE,
(clazz, table) -> {
// Make sure GsonBuilder and the generated type adapter
// are imported.
file.add(Import.of(Type.of(GsonBuilder.class)));
file.add(Import.of(Type.of(absoluteTypeAdapterName)));
// Add a Gson instance as a private member
clazz.add(Field.of("gson", Type.of(Gson.class))
.private_().final_()
);
// Find the constructor and define gson in it
clazz.getConstructors().forEach(constr -> {
constr.add(
"this.gson = new GsonBuilder()",
indent(".setDateFormat(\"" + DATE_FORMAT + "\")"),
indent(".registerTypeAdapter(" + entityName +
".class, new " + typeAdapterName + "(this))"),
indent(".create();")
);
});
// Override the toJson()-method
clazz.add(Method.of("toJson", STRING)
.public_().add(OVERRIDE)
.add(Field.of("entity", entityType))
.add("return gson.toJson(entity, " + entityName +
".class);"
)
);
// Override the fromJson()-method
clazz.add(Method.of("fromJson", entityType)
.public_().add(OVERRIDE)
.add(Field.of("json", STRING))
.add("return gson.fromJson(json, " + entityName +
".class);"
)
);
});
});
}
}
Step 5: Install All the New Classes into the Platform
Once we have created all the new classes, we need to create a component and a component installer that can be referenced from any project where we want to use the plugin.
GsonComponent.java
public final class GsonComponent extends AbstractComponent {
public GsonComponent(Speedment speedment) {
super(speedment);
}
@Override
public void onResolve() {
final CodeGenerationComponent code =
getSpeedment().getCodeGenerationComponent();
code.put(Table.class,
GeneratedTypeAdapterTranslator.KEY,
GeneratedTypeAdapterTranslator::new
);
code.add(Table.class,
StandardTranslatorKey.GENERATED_MANAGER,
new GeneratedManagerDecorator()
);
code.add(Table.class,
StandardTranslatorKey.GENERATED_MANAGER_IMPL,
new GeneratedManagerImplDecorator()
);
}
@Override
public Class<GsonComponent> getComponentClass() {
return GsonComponent.class;
}
@Override
public Software asSoftware() {
return AbstractSoftware.with("Gson Plugin", "1.0", APACHE_2);
}
@Override
public Component defaultCopy(Speedment speedment) {
return new GsonComponent(speedment);
}
}
GsonComponentInstaller.java
public final class GsonComponentInstaller
implements ComponentConstructor<GsonComponent> {
@Override
public GsonComponent create(Speedment speedment) {
return new GsonComponent(speedment);
}
}
Usage
When we want to use our new plugin in a project, we simply add it as a dependency both in the dependency section in the pom and as a dependency under the speedment maven plugin. We then add a configuration tag to the plugin like below:
<plugin>
<groupId>com.speedment</groupId>
<artifactId>speedment-maven-plugin</artifactId>
<version>${speedment.version}</version>
<dependencies>
<dependency>
<groupId>com.speedment.plugins</groupId>
<artifactId>gson</artifactId>
<version>1.0.0</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.39</version>
</dependency>
</dependencies>
<configuration>
<components>
<component implementation="com.speedment.plugins.gson.GsonComponentInstaller" />
</components>
</configuration>
</plugin>
We can then regenerate our code and we should then have access to the new serialization and deserialization logic.
final String pippi = "{" +
"\"id\":1," +
"\"bookId\":-8043771945249889258," +
"\"borrowedStatus\":\"AVAILABLE\"," +
"\"title\":\"Pippi Långström\"," +
"\"authors\":\"Astrid Lindgren\"," +
"\"published\":\"1945-11-26\"," +
"\"summary\":\"A story about the world's strongest little girl.\"" +
"}";
books.fromJson(pippi).persist();
Summary
In this article we have created a new Speedment plugin that generated Gson TypeAdapters
for every table in a database and integrates those adapters with the existing manager generation. If you want more examples on how you can use the Speedment code generator to increase your productivity, check out the GitHub page!
Until next time!