javaannotation-processingjavapoet

How to add a parameterized super interface in JavaPoet?


I am writing an annotation processor that generates JSON serialization code. Here's my annotation that I use to identify the POJOs that need a serializer

@Target(ElementType.TYPE)
@Retention(RetentionPolicy.SOURCE)
public @interface JsonSerialize {

}

And here's the base interface of my serializer

public interface JsonSerializer<T> {

    String serialize(T t);

}

Here's the annotation processor code that looks for that annotation and generates the serializer code

@Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) {
        for (Element element : roundEnv.getElementsAnnotatedWith(JsonSerialize.class)) {
            if (element.getKind() == ElementKind.CLASS) {

                MethodSpec serializeMethod = MethodSpec
                        .methodBuilder("serialize")
                        .addModifiers(Modifier.PUBLIC)
                        .addParameter(ParameterSpec.builder(TypeName.get(element.asType()), "obj", Modifier.FINAL).build())
                        .returns(String.class)
                        .addStatement("return \"dummy string\"")
                        .build();

                TypeSpec serializer = TypeSpec
                        .classBuilder(element.getSimpleName().toString() + "JsonSerializer")
                        .addSuperinterface(JsonSerializer.class) // THIS LINE IS WRONG
                        .addModifiers(Modifier.PUBLIC)
                        .addMethod(serializeMethod)
                        .build();

                try {
                    JavaFile.builder(processingEnv.getElementUtils().getPackageOf(element).toString(), serializer)
                            .build()
                            .writeTo(processingEnv.getFiler());
                } catch (IOException e) {
                    e.printStackTrace();
                }

            }
        }
        return true;
    }

But I get a compile error, because my generated class is not specifying the generic parameter in it's inheritance. How can I specify that?


Solution

  • Instead of passing a java.lang.Class to the addSuperinterface method, you'll need to pass something with the specific type details you have in mind. This method has two overloads - one which takes java.lang.reflect.Type (and Class is a subtype of this), and another which one which takes com.squareup.javapoet.TypeName). Technically either works, though since you are already using JavaPoet, I'd encourage trying to create the TypeName instance.

    TypeName has a number of subclasses, ClassName, ParameterizedTypeName are probably the main ones to focus on here. In an annotation processor, they have some big advantages over using a Class instance - mostly that you don't need to actually be able to load or reference the class you are talking about - kind of like how you are using element.getSimpleName().toString() elsewhere in your code.

    These classes have static methods to create them, which can be based on a variety of things. The one we're interested in here is this:

      /** Returns a parameterized type, applying {@code typeArguments} to {@code rawType}. */
      public static ParameterizedTypeName get(ClassName rawType, TypeName... typeArguments)
    

    In you code, you would use it roughly like this:

        ...
        .addSuperinterface(ParameterizedTypeName.get(
                ClassName.get(JsonSerializer.class),//rawType
                ClassName.get(whateverTShouldBe)    //the value for T
        ))
        ...
    

    Chance are excellent that T could eventually be generic here too, like List<String>, so you should take care to properly build the type which is passed in there - it might itself be a ParameterizedTypeName. Keep an eye on the various methods in TypeName for this too - the TypeName.get(TypeMirror) overload for example will take an already-parameterized declared type mirror and give you the expected ParameterizedTypeName back again.

    With that said, according to your other code, T today cannot be generic - you look for the @JsonSerialize annotation on an Element, which means it would be the equivelent of List<T> rather than the usage of it, List<String>. Then, in this line, you make the Element into a TypeMirror to build the type name as I've described above:

        .addParameter(ParameterSpec.builder(TypeName.get(element.asType()), "obj", Modifier.FINAL).build())
    

    This means the final code would probably be

        ...
        .addSuperinterface(ParameterizedTypeName.get(
                ClassName.get(JsonSerializer.class),//rawType
                TypeName.get(element.asType())    //the value for T
        ))
        ...