It's Time to Abandon JavaPoet/KotlinPoet
Most Android developers are familiar with JavaPoet and KotlinPoet, both from the well-known Square. Typically, when generating source code at compile time using APT (Annotation Processing Toolkit) or KAPT, developers use JavaPoet for Java and KotlinPoet for Kotlin. At first glance, it seems pretty cool and sophisticated.
Generating Code with JavaPoet
As JavaPoet‘s introduction shows, to generate this code:
1 | package com.example.helloworld; |
You would write this with JavaPoet:
1 | MethodSpec main = MethodSpec.methodBuilder("main") |
Generating Code with KotlinPoet
Similar to JavaPoet, to generate this code:
1 | class Greeter(val name: String) { |
You would write this with KotlinPoet:
1 | val greeterClass = ClassName("", "Greeter") |
Readability and Maintainability
The two examples above are about as simple as it gets – fewer than 10 lines of source code including braces – yet the JavaPoet and KotlinPoet implementations are already lengthy. Without seeing the target output, figuring out what the generated code looks like from the builder calls alone takes real effort. If examples this trivial are already hard to read, imagine real-world complex projects. Even with code you wrote yourself, come back three months later and you will wonder if it was really you. And if the unlucky task of modifying someone else’s code falls on you, most people will read it while muttering:
What idiot wrote this garbage!
You will debug and curse your way through it, finally finishing after great effort. Months later, someone else takes over, and the scene replays with a different protagonist.
Template Engines
Frontend developers are likely familiar with template engines – for example, EJS (Embedded JavaScript template engine). The template engines Freemarker and Velocity used in early JSP technology both come from the renowned Apache Foundation. You might ask: what do template engines have to do with JavaPoet and KotlinPoet?
To be clear, template engines have no direct relationship with JavaPoet and KotlinPoet, but they are related to the problem we are solving. For code generation, the ultimate goal is “generating source code.” How is that fundamentally different from using a template engine to generate HTML? HTML is itself source code. If a template engine can generate HTML, why not use it to generate Java, Kotlin, Swift, and so on?
You might think of another question:
Why do template engines exist in the first place?
As a veteran in the industry, my first exposure to Web technology was ASP, then JSP, and I used PHP too. Early Web development did not separate frontend and backend – the source code was tightly coupled. Given the limited framework capabilities, frontend UI development meant embedding scripts in ASP/JSP/PHP. Taking JSP as an example, to generate HTML with a JSP script:
1 | <% |
If the page had complex business logic, it would be full of <% %> fragments, severely hurting readability. To make page code closer to HTML, JavaServer Pages Standard Tag Library (JSTL) was created. The famous Struts framework was built on this technology. With JSTL, the <% %> blocks were replaced by custom tags:
1 | <%@ taglib prefix="s" uri="/struts-tags" %> |
Even now with frontend-backend separation as the standard architecture, template engines remain popular. Take Vue.js, one of the most popular Web frameworks today:
1 | <script> |
From these examples, we can see that template engines decouple the final output from business logic through templates. With a template, we can easily see what the output will look like, without mentally simulating code execution to deduce the generated source code.
Code Reusability
With JavaPoet and KotlinPoet, you will notice that even across different projects, much of the code is similar – defining an AnnotationProcessor, overriding init and process methods, handling multi-round issues, and so on.
Furthermore, once you commit to JavaPoet for generating Java source code, switching to Kotlin later means reimplementing everything with KotlinPoet. The same applies in reverse. This is completely unreasonable for developers – the logic is the same; only the target language differs. If switching the target language requires a full reimplementation, the architecture is fundamentally wrong.
Template-Engine-Based Code Generation Framework
To address the problems above:
- Code maintainability
- Code readability
- Code reusability
We can fully separate the code generation logic from the source code content – that is, template + data model:
- Template - defines the source code to generate
- Data model - the data needed to render the template
This way, when generating source code at compile time, the main focus is on “how to build the data model.” With a template engine, as long as the data model is correct, we can switch target languages just by switching templates without rewriting any logic. This is the design philosophy behind codegen, which supports both Mustache and Velocity. For example, to generate a Factory class for a given type:
1 | interface Factory<T> { |
To generate Java source code, the mustache template looks like:
1 | package io.johnsonlee.codegen.generated; |
To generate Kotlin source code, the mustache template looks like:
1 | package io.johnsonlee.codegen.generated |
Both templates can reuse the same data model:
1 | data class AutoFactoryModel( |
For developers, all you need to do is build the data model and call the framework’s generate method:
1 | generate( |
After seeing this, do you still think JavaPoet and KotlinPoet are the way to go?
Codegen
Project repository: https://github.com/johnsonlee/codegen – don’t forget to star it!
- Blog Link: https://johnsonlee.io/2022/04/10/its-time-to-abandon-javapoet-kotlinpoet.en/
- Copyright Declaration: 著作权归作者所有。商业转载请联系作者获得授权,非商业转载请注明出处。
