A code generator is a class extending io.vertx.codegen.Generator
loaded by a class implementing io.vertx.codegen.GeneratorLoader
declared as a META-INF/services/io.vertx.codegen.GeneratorLoader
JVM service.
There can be as many generators as you like.
A generator can create 3 different kinds of output: Java classes, resources or absolute files.
A generator declaring a filename not starting with /
' matching a Java FQN followed by .java
suffix will have its content generated as a Java class. This class will be automatically compiled by the same compiler (that's a Java compiler feature).
The generated files are handled by the Java compiler (-s
option), usually build tools configures the compiler to store them in a specific build location, for instance Maven by default uses the target/generated-sources/annotations
directory.
The following generators use it:
- Data object json/protobuf converters
- Service proxies
- RxJava-ified classes API
A file not starting with /
is considered as a java resource, its content generated is considered as a compiler resource. This resource will be stored in the generated sources directory and the generated class directory.
Generated files are handled by the Java compiler (-s
option), usually build tools configures the compiler to store them in a specific build location, for instance Maven by default uses the target/generated-sources/annotations
directory.
A file starting with /
will be written as an absolute file on the filesystem, this file is not managed by the java compiler.
You can configure the CodeGenProcessor
as any Java annotation processor, here is how to do with Maven:
<pluginManagement>
<plugins>
<!-- Configure the execution of the compiler to execute the codegen processor -->
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<version>3.8.1</version>
<configuration>
<release>11</release>
<encoding>${project.build.sourceEncoding}</encoding>
<!-- Important: there are issues with apt and incremental compilation in the maven-compiler-plugin -->
<useIncrementalCompilation>false</useIncrementalCompilation>
</configuration>
<executions>
<execution>
<id>default-compile</id>
<configuration>
<annotationProcessors>
<annotationProcessor>io.vertx.codegen.CodeGenProcessor</annotationProcessor>
</annotationProcessors>
<!-- It is new option since v3.5 to instruct compiler detect annotation processors classpath -->
<annotationProcessorPaths>
<path>
<groupId>io.vertx</groupId>
<artifactId>vertx-codegen</artifactId>
<version>${vertx.version}</version>
</path>
<!-- ... more path such as vertx-service-proxy/vertx-rx-java2 depends on what you want to generate ... -->
</annotationProcessorPaths>
</configuration>
</execution>
</executions>
</plugin>
</plugins>
</pluginManagement>
And here is a configuration example for Gradle (since Gradle 5.0
):
Gradle Groovy
dependencies {
compileOnly("io.vertx:vertx-codegen:4.0.2")
// more vertx-service-proxy/vertx-rx-java2
// compileOnly("io.vertx:vertx-rx-java2:4.0.2")
annotationProcessor("io.vertx:vertx-codegen:4.0.2")
}
task annotationProcessing(type: JavaCompile, group: 'other') { // codegen
source = sourceSets.main.java
classpath = configurations.compile
destinationDir = project.file('${project.buildDir}/generated/main/java')
options.annotationProcessorPath = configurations.compileClasspath
options.compilerArgs = [
"-proc:only",
"-processor", "io.vertx.codegen.CodeGenProcessor"
]
}
compileJava {
dependsOn annotationProcessing
}
sourceSets {
main {
java {
srcDirs += '${project.buildDir}/generated/main/java'
}
}
}
Gradle Kotlin
dependencies {
compileOnly("io.vertx:vertx-codegen:4.0.2")
// more vertx-service-proxy/vertx-rx-java2
// compileOnly("io.vertx:vertx-rx-java2:4.0.2")
annotationProcessor("io.vertx:vertx-codegen:4.0.2")
}
tasks.register<JavaCompile>("annotationProcessing") {
group = "other"
source = sourceSets.getByName(SourceSet.MAIN_SOURCE_SET_NAME).java
destinationDir = project.file("${project.buildDir}/generated/main/java")
classpath = configurations.compileClasspath.get()
options.annotationProcessorPath = configurations.compileClasspath.get()
options.compilerArgs = listOf(
"-proc:only",
"-processor", "io.vertx.codegen.CodeGenProcessor"
)
}
tasks.compileJava {
dependsOn(tasks.named("annotationProcessing"))
}
sourceSets {
main {
java {
srcDirs(project.file("${project.buildDir}/generated/main/java"))
}
}
}
Besides you can use the processor
classified dependency that declares the annotation processor as a
META-INF/services/javax.annotation.processing.Processor
, if you do so, code generation triggers automatically:
<dependency>
<groupid>io.vertx</groupId>
<artifactId>vertx-codegen</artifactId>
<classifier>processor</classifier>
</dependency>
The processor is configured by a the codegen.generators
option, a comma separated list of generators, each expression
is a regex, allow to filter undesired generators
In order for code generation to work effectively, certain constraints are put on the Java interfaces.
The constraints are
- The API must be described as a set of Java interfaces, classes are not permitted
- Nested interfaces are not permitted
- All interfaces to have generation performed on them must be annotated with the
io.vertx.codegen.annotations.VertxGen
annotation - Fluent methods (methods which return a reference to this) must be annotated with the
io.vertx.codegen.annotations.Fluent
annotation - Data object classes (classes which provide data (e.g. configuration) to methods) must be annotated with the
io.vertx.codegen.annotations.DataObject
annotation - Data object classes must provide a constructor which takes a single
io.vertx.core.json.JsonObject
orjava.lang.String
parameter. - Methods where the return value can be cached shall be annotated with the
io.vertx.codegen.annotations.CacheReturn
annotation - Types parameters or return value types for any API methods are constrainted (defined below).
- Enums should be annotated with
@VertxGen
, although this is not mandatory to allow the usage of existing Java enums JsonMapper
implementations must expose an instance as apublic static final [JsonDesdeType] INSTANCE
field
We define Basic
:
- any primitive type
- any boxed primitive type
java.lang.String
We define Json
:
*io.vertx.core.json.JsonObject
*io.vertx.core.json.JsonArray
We define DataObject
:
- The set of user defined API types which are defined in its own class and annotated with
@DataObject
- The set of types that have an associated mapper declared with a
json-mappers.properties
file
We define TypeVar
as the set of types variables where the variable is either declared by its generic method or its generic type
We define Api
as the set of user defined API types which are defined in its own interface and annotated with @VertxGen
We define JavaType
as the set of any Java type that does not belong to Basic
, Json
, DataObject
, TypeVar
and Api
, e.g java.net.Socket
.
We define Parameterized
as the set of user defined API types which are defined in its own interface and annotated with @VertxGen
where type parameters belong to:
- the type
java.lang.Void
- the set
Basic
- the set
Json
- the set
DataObject
- any enum type
- the set
Api
- the set
TypeVar
We define ContainerValueType
as the set of any Java type that belongs to:
- the set
Basic
- the set
Json
- any enum type
- the set
Api
- the set
DataObject
- the set
JavaType
(under restriction, see below) java.lang.Object
The following set Return
of types are permitted as return types from any API method:
void
- the set
Basic
- the set
Json
- the set
DataObject
- any enum type
java.lang.Throwable
- the set
TypeVar
java.lang.Object
- the set
Api
- the set
Parameterized
- the set
JavaType
(under restriction, see below) - type
java.util.List<C>
,java.util.Set<C>
orjava.util.Map<String, C>
whereC
belongs toContainerValueType
io.vertx.core.Future<HA>
whereHA
contains the setReturn
wherevoid
is interpreted asjava.lang.Void
minusjava.lang.Throwable
The following set Param
of types are permitted as parameters to any API method:
- the set
Basic
- the set
Json
- the set
DataObject
- any enum type
- the type
java.lang.Throwable
- the set
TypeVar
java.lang.Object
- the set
Api
- the set
JavaType
(under restriction, see below) - the set
Parameterized
- the type
java.lang.Class<T>
where<T>
is among- the set
Basic
- the set
Json
- the set
Api
- the set
JavaType
- the set
- type
java.util.List<C>
,java.util.Set<C>
orjava.util.Map<String, C>
whereC
belongs toContainerValueType
In addition,
java.util.function.Function<T, R>
whereT
containsReturn
andR
containsParam
java.util.function.Supplier<R>
whereR
containsParam
io.vertx.java.core.Handler<H>
whereH
contains the setReturn
wherevoid
is interpreted asjava.lang.Void
By default, method parameters shall declare types among the Param
set and return types among the Return
set.
However, methods can be annotated with @GenIgnore(GenIgnore.PERMITTED_TYPE)
to leave this restriction. Such method limit the translation of the method to other languages, so it should be used with care. It is useful to allow method previously annotated with @GenIgnore
to be available in code generator like RxJava that can handle Java types.
The io.vertx.codegen.annotations.Nullable
annotates types declarations to signal the type value can be null
.
Method return type can be io.vertx.codegen.annotations.Nullable
:
As well as method parameter type:
WARNING: type validation is a non goal of this feature, its purpose is to give hints to code generators
The following rules apply to nullable types:
- primitive types cannot be nullable
- method parameter type can be nullable
- non-fluent method return type can be nullable
io.vertx.core.Handler
type argument can be nullable,java.lang.Void
,java.lang.Object
and type variables are implicitly nullableio.vertx.core.Future
type argument can be nullable,java.lang.Void
,java.lang.Object
and type variables are implicitly nullable- the
java.lang.Object
type is always nullable - Liskov substitution principle
- a method overriding another method
inherits
the nullable usage of the overridden method, it should not declare it, but it is encouraged to do so - a method overriding another method cannot declare nullable in its types
- a method overriding another method
In addition, these rules apply to nullable type arguments:
- methods cannot declare generic api types with nullable type arguments, e.g.
<T> void method(GenericApi<Nullable T> api)
is not permitted - methods can declare nullable collection, e.g.
void method(List<Nullable String> list)
is allowed
You can declare methods in your interfaces, e.g.
interface MyInterface {
void doSomething(String foo);
}
Default method works as well
interface MyInterface {
default String doSomething(String foo) {
return foo != null ? new StringBuilder(foo).reverse().toString() : null;
}
}
Asynchronous operations are declared using a method returning a future.
@VertxGen
public interface SomeApi {
Future<Buffer> getValue();
}
Methods annotated with io.vertx.codegen.annotations.GenIgnore
are simply ignored by codegen, this is useful when the API provides Java specific methods, for instance a method uses a type not permitted by codegen.
You can declare static methods in your interfaces, e.g.
interface MyInterface {
static MyInterface newInterface(String foo) {
return new MyInterfaceImpl();
}
}
You can declare fields in your interfaces, e.g.
interface MyInterface {
int SOME_CONSTANT = 4;
}
Interfaces can extend other interfaces which also have the @VertxGen
annotation.
Interfaces annotated with @VertxGen
can either be concrete or abstract, such information is important
for languages not supporting multiple class inheritance like Groovy:
- interfaces annotated with
@VertxGen(concrete = false)
are meant to be extended by concrete interfaces and can inherit from abstract interfaces only. - interfaces annotated with
@VertxGen
or@VertxGen(concrete = true)
are implemented directly by Vertx and can inherit at most one other concrete interface and any abstract interface
If you do not wish a method to be used for generation you can annotate it with the @GenIgnore
annotation.
Generated types must belong to a module: a java package annotated with @ModuleGen
that defines a module. Such
file is created in a file package-info.java.
A module must define:
- a
name
used when generating languages that don't follow Java package naming, like JavaScript or Ruby. - a
groupPackage
to define the package of the group used for generating the generated package names (for Groovy, RxJava or Ceylon generation):
@ModuleGen(name = "acme", groupPackage="com.acme")
package com.acme.myservice;
The group package must be a prefix of the annotated module, it defines the naming of the generate packages o for the modules that belongs to the same group, in this case:
com.acme.rxjava...
for RxJava API
For this particular com.acme.myservice
module we have:
com.acme.rxjava.myservice
for RxJava API
Vert.x Apis uses the io.vertx
group package and vertx-XYZ
name, this naming is exclusively reserved
to Vert.x Apis.
NOTE: using Maven coordinates for name and group package is encouraged: the name corresponding to the
Maven artifactId and the group package corresponding to the groupId
.
A Data object is a type that can be converted back and forth to a Json type.
You can declare data objects by:
- Defining a mapper in the
META-INF/vertx/json-mappers.properties
file - Or annotating the type itself with
@DataObject
A json mapper for type T
is a method that maps any object or enum of type Type
, where J
can be:
JsonArray
orJsonObject
- a concrete type extending
Number
such asLong
orDouble
String
Boolean
Json mapped types can be used anywhere a json types used are.
A json mapper turns any Java type into a data object type.
You can declare them as public static methods:
package com.example;
public class MyMappers {
public static String serialize(ZonedDateTime date) {
return date.toString();
}
public static ZonedDateTime deserialize(String s) {
return ZonedDateTime.parse(s);
}
}
These mappers need to be declared in a META-INF/vertx/json-mappers.properties
file as follows:
java.time.ZonedDateTime.serializer=com.example.MyMappers#serializeZonedDateTime
java.time.ZonedDateTime.deserializer=com.example.MyMappers#deserializedZoneDateTime
Enum can be defined with values parameters passed to a constructor. In this use case, you can't use default behavior of codegen (#valueOf()
and #name()
), you need to define like Object serializer
and deserializer
.
package com.example;
public enum MyEnumWithCustomFactory {
DEV("dev", "development"), ITEST("itest", "integration-test");
private String[] names = new String[2];
MyEnumWithCustomFactory(String pShortName, String pLongName) {
names[0] = pShortName;
names[1] = pLongName;
}
public String getLongName() {
return names[1];
}
public String getShortName() {
return names[0];
}
public static MyEnumWithCustomFactory of(String pName) {
for (MyEnumWithCustomFactory item : MyEnumWithCustomFactory.values()) {
if (item.names[0].equalsIgnoreCase(pName) || item.names[1].equalsIgnoreCase(pName)
|| pName.equalsIgnoreCase(item.name())) {
return item;
}
}
return DEV;
}
}
You can declare them as public static methods:
public class MyEnumWithCustomFactory {
public static String serialize(MyEnumWithCustomFactory value) {
return value.getLongName();
}
public static MyEnumWithCustomFactory deserialize(String value) {
return MyEnumWithCustomFactory.of(value);
}
}
These mappers need to be declared in a META-INF/vertx/json-mappers.properties
file as follows:
com.example.MyEnumWithCustomFactory.serializer=com.example.MyEnumWithCustomFactory#serialize
com.example.MyEnumWithCustomFactory.deserializer=com.example.MyEnumWithCustomFactory#deserialize
A @DataObject
annotated type is a Java class with the only purpose to be a container for data.
A data object can be created from JSON with a constructor or a factory method:
.with a constructor
public class MyDataObject {
public MyDataObject(JsonObject json) {
// ...
}
}
.with a factory
public class MyDataObject {
public static MyDataObject fromJson(JsonObject json) {
// ...
}
}
A data object can be converted to JSON with a toJson()
method:
.with a factory
public class MyDataObject {
public JsonObject toJson() {
// ...
}
}
The data object/json conversion can be tedious and error-prone.
Vertx-codegen can automate it, generating for you an auxiliary class that implements the conversion logic. The generated converter handles the type mapping as well as the json naming convention.
Converters are generated when the data object is annotated with @JsonGen
. Generation happens for data object declared
properties, ancestor properties are omitted, unless inheritConverter
is set: @JsonGen(inheritConverter=true)
.
Converters are named by appending the Converter
suffix to the data object class name, e.g, ContactDetails
-> ContactDetailsConverter
. A generated converter declares two static methods:
public static void fromJson(JsonObject json, ContactDetails obj)
public static void toJson(ContactDetails obj, JsonObject json)
The former can be used in json constructors, the latter the toJson
methods.
@DataObject
@JsonGen
public class ContactDetails {
public ContactDetails(JsonObject json) {
this();
ContactDetailsConverter.fromJson(json, this);
}
public JsonObject toJson() {
JsonObject json = new JsonObject();
ContactDetailsConverter.toJson(this, json);
return json;
}
}
The json converter generator recognize the following types as member of any @DataObject
:
- the set
Basic
- these specific types
io.vertx.core.Buffer
java.time.Instant
- the set
Json
- any data object class annotated with
@DataObject
- type
java.util.List<C>
whereC
contains- the specific
io.vertx.core.Buffer
type - the set
Basic
- the set
Json
- any
@DataObject
- the Object type : the
List<Object>
acts like aJsonArray
- the specific
- type
java.util.Map<String, C>
whereC
contains- the specific
io.vertx.core.Buffer
type - the set
Basic
- the set
Json
- any
@DataObject
- the Object type : the
Map<String, Object>
acts like aJsonMap
- the specific
In addition a data object can also have multi-valued properties as a java.util.List<V>
/java.util.Set<V>
or a
java.util.Map<String, V>
where the <V>
is a supported single valued type or java.lang.Object
that stands for anything converted by io.vertx.core.json.JsonObject
and io.vertx.core.json.JsonArray
.
List/set multi-valued properties can be declared via a setter :
.a multi valued setter
@DataObject
@JsonGen
public class WebServerOptions {
...
public WebServerOptions setCertificates(List<String> certificates) {
this.certificates = certificates;
return this;
}
...
}
Or an adder :
.a multi valued adder
@DataObject
@JsonGen
public class WebServerOptions {
...
public WebServerOptions addCertificate(String certificate) {
this.certificates.add(certificate);
return this;
}
}
Map properties can only be declared with a setter.
NOTE: these examples uses a fluent return types for providing a better API, this is not mandatory but encouraged.
Enum types can be freely used in an API, custom enum types should be annotated with @VertxGen
to allow processing of the enum. This is not mandatory to allow the reuse the existing Java enums.
Enums can be processed for providing more idiomatic APIs in some languages.