Skip to main content

Build your own framework using a Java annotation processor

Picture of Jacek Dubikowski, Senior Software Engineer

Jacek Dubikowski

Senior Software Engineer
Jan 11, 2023|10 min read
Build_your_own_framework_using_a_Java_annotation_processor-min.jpg
1public class NoFrameworkApp {
2 public static void main(String[] args) {
3 ParticipationService participationService = new ManualTransactionParticipationService(
4 new ParticipantRepositoryImpl(),
5 new EventRepositoryImpl(),
6 new TransactionalManagerStub()
7 );
8 participationService.participate(new ParticipantId(), new EventId());
9 }
10}
1 input files: {io.jd.Data}
2 annotations: [io.jd.AllFieldsFinal]
3 last round: false
1 input files: {}
2 annotations: []
3 last round: true
1interface Water {
2 String name();
3}
4
5@Singleton
6class SparklingWater implements Water {
7
8 @Override
9 String name() {
10 return "Bubbles";
11 }
12}
13
14public class App {
15 public static void main(String[] args) {
16 BeanProvider provider = BeanProviderFactory.getInstance();
17 var bean = beanProvider.provider(SoftDrink.class);
18 System.out.println(bean.name()); // prints "Bubbles"
19 }
20}
1package io.jd.framework;
2
3public interface BeanDefinition<T> {
4 T create(BeanProvider beanProvider);
5
6 Class<T> type();
7}
1package io.jd.framework;
2
3public interface BeanProvider {
4 <T> T provide(Class<T> beanType);
5
6 <T> Iterable<T> provideAll(Class<T> beanType);
7}
1import javax.annotation.processing.AbstractProcessor;
2
3class BeanProcessor extends AbstractProcessor {
4
5}
1import javax.annotation.processing.AbstractProcessor;
2import javax.annotation.processing.SupportedAnnotationTypes;
3import javax.annotation.processing.SupportedSourceVersion;
4import javax.lang.model.SourceVersion;
5
6@SupportedAnnotationTypes({"jakarta.inject.Singleton"}) // 1
7@SupportedSourceVersion(SourceVersion.RELEASE_17) // 2
8class BeanProcessor extends AbstractProcessor {
9
10}
1 @Override
2 public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { // 1
3 try {
4 processBeans(roundEnv); // 2
5 } catch (Exception e) {
6 processingEnv.getMessager() // 3
7 .printMessage(ERROR, "Exception occurred %s".formatted(e));
8 }
9 return false; // 4
10 }
1 private void processBeans(RoundEnvironment roundEnv) {
2 Set<? extends Element> annotated = roundEnv.getElementsAnnotatedWith(Singleton.class); // 1
3 Set<TypeElement> types = ElementFilter.typesIn(annotated); // 2
4 var typeDependencyResolver = new TypeDependencyResolver(); // 3
5 types.stream().map(t -> typeDependencyResolver.resolve(t, processingEnv.getMessager())) // 4
6 .forEach(this::writeDefinition); // 5
7 }
1public class TypeDependencyResolver {
2
3 public Dependency resolve(TypeElement element, Messager messager) {
4 var constructors = ElementFilter.constructorsIn(element.getEnclosedElements()); // 1
5 return constructors.size() == 1 // 2
6 ? resolveDependency(element, constructors) // 3
7 : failOnTooManyConstructors(element, messager, constructors); // 4
8 }
9
10 private Dependency resolveDependency(TypeElement element, List<ExecutableElement> constructors) { // 5
11 ExecutableElement constructor = constructors.get(0);
12 return new Dependency(element, constructor.getParameters().stream().map(VariableElement::asType).toList());
13 }
14 ...
15}
1public final class Dependency {
2 private final TypeElement type;
3 private final List<TypeMirror> dependencies;
4
5 ...
6
7 public TypeElement type() {
8 return type;
9 }
10
11 public List<TypeMirror> dependencies() {
12 return dependencies;
13 }
14 ...
15 }
1 private void writeDefinition(Dependency dependency) {
2 JavaFile javaFile = new DefinitionWriter(dependency.type(), dependency.dependencies()).createDefinition(); // 1
3 writeFile(javaFile);
4 }
5
6 private void writeFile(JavaFile javaFile) { // 2
7 try {
8 javaFile.writeTo(processingEnv.getFiler());
9 } catch (IOException e) {
10 processingEnv.getMessager().printMessage(ERROR, "Failed to write definition %s".formatted(javaFile));
11 }
12 }
1@Singleton
2public class ServiceC {
3 private final ServiceA serviceA;
4 private final ServiceB serviceB;
5
6 public ServiceC(ServiceA serviceA, ServiceB serviceB) {
7 this.serviceA = serviceA;
8 this.serviceB = serviceB;
9 }
10}
1public class $ServiceC$Definition implements BeanDefinition<ServiceC> { // 1
2 private final ScopeProvider<ServiceC> provider = // 2
3 ScopeProvider.singletonScope(beanProvider -> new ServiceC(beanProvider.provide(ServiceA.class), beanProvider.provide(ServiceB.class)));
4
5 @Override
6 public ServiceC create(BeanProvider beanProvider) { // 3
7 return provider.apply(beanProvider);
8 }
9
10 @Override
11 public Class<ServiceC> type() { // 4
12 return ServiceC.class;
13 }
14}
1class DefinitionWriter {
2 private final TypeElement definedClass; // 1
3 private final List<TypeMirror> constructorParameterTypes; // 1
4 private final ClassName definedClassName; // 1
5
6 public JavaFile createDefinition() {
7 ParameterizedTypeName parameterizedBeanDefinition = ParameterizedTypeName.get(ClassName.get(BeanDefinition.class), definedClassName); // 3
8 var definitionSpec = TypeSpec.classBuilder("$%s$Definition".formatted(definedClassName.simpleName())) // 2
9 .addSuperinterface(parameterizedBeanDefinition) // 3
10 .addMethod(createMethodSpec()) // 4
11 .addMethod(typeMethodSpec()) // 5
12 .addField(scopeProvider()) // 6
13 .build();
14 return JavaFile.builder(definedClassName.packageName(), definitionSpec).build(); // 7
15 }
16
17 private MethodSpec createMethodSpec() { ... } // 4
18
19 private MethodSpec typeMethodSpec() { ... } // 5
20
21 private FieldSpec scopeProvider() { ... } // 6
22
23 private CodeBlock singletonScopeInitializer() { ... } // 6
24}
1public interface ScopeProvider<T> extends Function<BeanProvider, T> { // 1
2
3 static <T> ScopeProvider<T> singletonScope(Function<BeanProvider, T> delegate) { // 2
4 return new SingletonProvider<>(delegate);
5 }
6}
7
8final class SingletonProvider<T> implements ScopeProvider<T> { // 3
9 private final Function<BeanProvider, T> delegate;
10 private volatile T value;
11
12 SingletonProvider(Function<BeanProvider, T> delegate) {
13 this.delegate = delegate;
14 }
15
16 public synchronized T apply(BeanProvider beanProvider) {
17 if (value == null) {
18 value = delegate.apply(beanProvider);
19 }
20 return value;
21 }
22}
1public class BeanProviderFactory {
2
3 private static final QueryFunction<Store, Class<?>> TYPE_QUERY = SubTypes.of(BeanDefinition.class).asClass(); // 2
4
5 public static BeanProvider getInstance(String... packages) { // 1
6 ConfigurationBuilder reflectionsConfig = new ConfigurationBuilder() // 3
7 .forPackages("io.jd") // 4
8 .forPackages(packages) // 4
9 .filterInputsBy(createPackageFilter(packages)); // 4
10 var reflections = new Reflections(reflectionsConfig); // 5
11 var definitions = definitions(reflections); // 6
12 return new BaseBeanProvider(definitions); // 8
13 }
14
15 private static FilterBuilder createPackageFilter(String[] packages) { // 4
16 var filter = new FilterBuilder().includePackage("io.jd");
17 Arrays.asList(packages).forEach(filter::includePackage);
18 return filter;
19 }
20
21 private static List<? extends BeanDefinition<?>> definitions(Reflections reflections) { // 6
22 return reflections
23 .get(TYPE_QUERY)
24 .stream()
25 .map(BeanProviderFactory::getInstance) // 7
26 .toList();
27 }
28
29 private static BeanDefinition<?> getInstance(Class<?> e) { // 7
30 try {
31 return (BeanDefinition<?>) e.getDeclaredConstructors()[0].newInstance();
32 } catch (InstantiationException | IllegalAccessException | InvocationTargetException ex) {
33 throw new FailedToInstantiateBeanDefinitionException(e, ex);
34 }
35 }
36}
1class BaseBeanProvider implements BeanProvider {
2 private final List<? extends BeanDefinition<?>> definitions;
3
4 public BaseBeanProvider(List<? extends BeanDefinition<?>> definitions) {
5 this.definitions = definitions;
6 }
7
8 @Override
9 public <T> List<T> provideAll(Class<T> beanType) { // 1
10 return definitions.stream().filter(def -> beanType.isAssignableFrom(def.type()))
11 .map(def -> beanType.cast(def.create(this)))
12 .toList();
13 }
14
15 @Override
16 public <T> T provide(Class<T> beanType) { // 2
17 var beans = provideAll(beanType); // 2
18 if (beans.isEmpty()) { // 3
19 throw new IllegalStateException("No bean of given type: '%s'".formatted(beanType.getCanonicalName()));
20 } else if (beans.size() > 1) { // 4
21 throw new IllegalStateException("More than one bean of given type: '%s'".formatted(beanType.getCanonicalName()));
22 } else {
23 return beans.get(0); // 5
24 }
25 }
26}
1public class NoFrameworkApp {
2 public static void main(String[] args) {
3 ParticipationService participationService = new ManualTransactionParticipationService(
4 new ParticipantRepositoryImpl(),
5 new EventRepositoryImpl(),
6 new TransactionalManagerStub()
7 );
8 participationService.participate(new ParticipantId(), new EventId());
9 }
10}
1Begin transaction
2Participant: 'Participant[]' takes part in event: 'Event[]'
3Commit transaction
1public class FrameworkApp {
2 public static void main(String[] args) {
3 BeanProvider provider = BeanProviderFactory.getInstance();
4 ParticipationService participationService = provider.provide(ParticipationService.class);
5 participationService.participate(new ParticipantId(), new EventId());
6 }
7}
1Begin transaction
2Participant: 'Participant[]' takes part in event: 'Event[]'
3Commit transaction
4

Subscribe to our newsletter and never miss an article