A Gradle plugin adds an annotation class to the current project when applied

Gradle plugin users use this annotation class to mark classes that need to be processed at compile time. The target annotation class contains the following contents:

package com.test.javassist;

import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;

@Retention(RetentionPolicy.CLASS)
@Target(ElementType.TYPE)
public @interface JavassistAnnotation {
    String value(a);
}
Copy the code

Implementation: Javassist is used to generate the target annotation class

There are many ways to generate classes at compile time: Javapoet can generate uncompiled Java files, javAssist can generate compiled class files, because the Gradle plug-in already uses Javassist for subsequent bytecode processing, so we choose javAssist to generate class. The implementation (Kotlin version) is as follows:

  1. Create the target annotation class -> public @Interface JavassistAnnotation
val classPool = ClassPool.getDefault()
// className: full path to package name + className
val className = "com.test.javassist.JavassistAnnotation"
val ctClass = classPool.makeAnnotation(className)
Copy the code
  1. Add a method to the target class -> String value();
The return value String in the method needs to provide the full class name of the Java space. "value" is the method signature
val ctMethod = CtMethod(classPool.get(String::class.java.name), "value".null, ctClass)
ctClass.addMethod(ctMethod)
Copy the code
  1. Add meta annotation to Target CLASS -> @target (elementtype.type) @Retention(retentionPolicy.class)
// Create a constant pool
val cFile = ctClass.classFile
val cPool = cFile.constPool
val annotationAttributes = AnnotationsAttribute(cPool, AnnotationsAttribute.visibleTag)

// Set the meta annotation @target
val targetAnnotation = Annotation("java.lang.annotation.Target", cPool)
// @target's method signature is "value" and the return value is the enumeration type ElementType
// Use EnumMemberValue to set the value. Select the corresponding MemberValue class for return values of other types
targetAnnotation.addMemberValue("value", EnumMemberValue(cPool).apply {
    // Set the return type of the @target method
    type = "java.lang.annotation.ElementType"
    // Set the return value of the @target method
    value = "TYPE"
})

// Set meta annotation @retention
val retainAnnotation = Annotation("java.lang.annotation.Retention", cPool)
// @retention's method signature is "value" and the return value is enumerated RetentionPolicy
// Use EnumMemberValue to set the value. Select the corresponding MemberValue class for return values of other types
retainAnnotation.addMemberValue("value", EnumMemberValue(cPool).apply {
    // Set the return type of the @retention method
    type = "java.lang.annotation.RetentionPolicy"
    // Set the return value of the @retention method
    value = "CLASS"
})

// Add the set meta annotation
annotationAttributes.addAnnotation(retainAnnotation)
annotationAttributes.addAnnotation(targetAnnotation)
cFile.addAttribute(annotationAttributes)
Copy the code
  1. Generate the target class
// Note the output path of the class file. If not, write it directly to the root of the current project (e.g. app).
ctClass.writeFile(outputDirPath)
// Free memory
ctClass.detach()
Copy the code

3. Add-on: Where should the class files generated at compile time be placed

Because Javassist generates a class file that has already been compiled, it cannot be placed in the generate directory like Java files generated with Javapoet. The class file needs to be imported into the project in a dependent manner to be used.

How do I import it? This is the LIBS folder.

Implementation idea:

  1. Use Javassist to output the target file to the temporary directory tmpDir in the LIBS
  2. Zip tmpDir into the jar file destfile. jar and delete tmpDir
  3. Add the LIBS directory to project Repositories
  4. Add the destfile.jar dependency to the project dependencies
val libsDir = "${buildDir.parent}/libs"
val tmpDir = File(libsDir, "tmp")
val destFile = File(libsDir, "javassistAnnotation.jar")
JarUtil.jarFile(tmpDir, destFile)
tmpDir.deleteRecursively()

repositories.flatDir {
    it.dir(libsDir)
}
dependencies.add(
    "implementation",
    mapOf("name" to destFile.nameWithoutExtension, "ext" to destFile.extension)
)
Copy the code

Fourth, finally: other schemes

At this point, we have successfully generated the target annotation class and imported it into the project. However, by adding classes to a project in this way, a new JAR package will be generated in the libs directory, and CVS will prompt you to add the new file to CVS when the as default setting is set. Is there another supposedly more elegant solution?

  1. Generate Java classes using Javapoet, which can be exported directly to the Build /generate directory after BuildConfig task. In this way, there is no need to jar or add dependencies, and CVS prompts can be avoided without user awareness.
  2. Create a New Java project, place the target class, publish the project to the Maven repository, and import the dependencies directly with the Apply plugin. There is no need to generate classes (brawn for brawn), and adding dependencies through urls also avoids CVS prompts that the user will not notice.