MENU
Contact
Contact

Transaction handling using an annotation processor

Picture of Jacek Dubikowski, Senior Software Engineer

Jacek Dubikowski

Senior Software Engineer
Jan 11, 2023|15 min read
Image Alt
1Begin transaction
2Participant: 'Participant[]' takes part in event: 'Event[]'
3Commit transaction
1@Singleton
2public class ManualTransactionParticipationService implements ParticipationService {
3 private final ParticipantRepository participantRepository;
4 private final EventRepository eventRepository;
5 private final TransactionManager transactionManager;
6
7
8 // constructor
9
10 @Override
11 public void participate(ParticipantId participantId, EventId eventId) {
12 try {
13 transactionManager.begin();
14 var participant = participantRepository.getParticipant(participantId);
15 var event = eventRepository.findEvent(eventId);
16 eventRepository.store(event.addParticipant(participant));
17
18 System.out.printf("Participant: '%s' takes part in event: '%s'%n", participant, event);
19
20 transactionManager.commit();
21 } catch (Exception e) {
22 rollback();
23 throw new RuntimeException(e);
24 }
25 }
26
27 private void rollback() {
28 try {
29 transactionManager.rollback();
30 } catch (SystemException e) {
31 throw new RuntimeException(e);
32 }
33 }
34}
1@Singleton
2public class DeclarativeTransactionsParticipationService implements ParticipationService {
3 private final ParticipantRepository participantRepository;
4 private final EventRepository eventRepository;
5 // constructor
6
7 @Override
8 @Transactional
9 public void participate(ParticipantId participantId, EventId eventId) {
10 var participant = participantRepository.getParticipant(participantId);
11 var event = eventRepository.findEvent(eventId);
12 eventRepository.store(event.addParticipant(participant));
13
14 System.out.printf("Participant: '%s' takes part in event: '%s'%n", participant, event);
15 }
16}
1@Singleton
2public class RepositoryA {
3
4 @Transactional
5 void voidMethod() {
6 }
7
8 int intMethod() {
9 return 1;
10 }
11}
1@Singleton
2class RepositoryA$Intercepted extends RepositoryA {
3 private final TransactionManager transactionManager;
4
5 RepositoryA$Intercepted(TransactionManager transactionManager) {
6 super();
7 this.transactionManager = transactionManager;
8 }
9
10 @Override
11 void voidMethod() {
12 // transaction handling code
13 }
14}
1public class TransactionalPlugin implements ProcessorPlugin { // 7
2 private TransactionalMessenger transactionalMessenger; // 3
3
4 @Override
5 public Collection<JavaFile> process(Set<? extends Element> annotated) { // 1
6 Set<ExecutableElement> transactionalMethods = ElementFilter.methodsIn(annotated); // 2
7 validateMethods(transactionalMethods); // 3
8 Map<TypeElement, List<ExecutableElement>> typeToTransactionalMethods = transactionalMethods.stream() // 4
9 .collect(groupingBy(element -> (TypeElement) element.getEnclosingElement())); // 4
10 return typeToTransactionalMethods.entrySet()
11 .stream()
12 .map(this::writeTransactional) // 5
13 .toList();
14 }
15
16 private void validateMethods(Set<ExecutableElement> transactionalMethods) { // 3
17 raiseForPrivate(transactionalMethods);
18 raiseForStatic(transactionalMethods);
19 raiseForFinalMethods(transactionalMethods);
20 raiseForFinalClass(transactionalMethods);
21 }
22
23 private JavaFile writeTransactional(Map.Entry<TypeElement, List<ExecutableElement>> typeElementListEntry) { // 5
24 var transactionalType = typeElementListEntry.getKey();
25 var transactionalMethods = typeElementListEntry.getValue();
26 PackageElement packageElement = processingEnv.getElementUtils().getPackageOf(transactionalType);
27 return new TransactionalInterceptedWriter(transactionalType, transactionalMethods, packageElement) // 6
28 .createDefinition(processingEnv.getMessager()); // 6
29 }
30
31 // more methods ...
32}
1class TransactionalInterceptedWriter {
2 private static final String TRANSACTION_MANAGER = "transactionManager";
3 private static final Modifier[] PRIVATE_FINAL_MODIFIERS = {Modifier.PRIVATE, Modifier.FINAL};
4
5 private final TypeElement transactionalElement; // 1
6 private final List<ExecutableElement> transactionalMethods; // 2
7 private final PackageElement packageElement; // 3
8}
1class TransactionalInterceptedWriter {
2
3 public JavaFile createDefinition(Messager messager) {
4 TypeSpec typeSpec = TypeSpec.classBuilder("%s$Intercepted".formatted(transactionalElement.getSimpleName().toString())) // 1
5 .addAnnotation(Singleton.class) // 2
6 .superclass(transactionalElement.asType()) // 3
7 .addSuperinterface(TypeName.get(Intercepted.class)) // 4
8 .addMethod(interceptedTypeMethod()) // 4
9 .addField(TransactionManager.class, TRANSACTION_MANAGER, PRIVATE_FINAL_MODIFIERS) // 5
10 .addMethod(constructor(messager)) // 6
11 .addMethods(transactionalMethodDefinitions()) // 7
12 .build();
13 return JavaFile.builder(packageElement.getQualifiedName().toString(), typeSpec).build(); // 8
14 }
15
16}
1class TransactionalInterceptedWriter {
2
3 private MethodSpec constructor(Messager messager) {
4 Dependency dependency = new TypeDependencyResolver().resolve(transactionalElement, messager); // 1
5 var typeNames = dependency.dependencies().stream().map(TypeName::get).toList(); // 1
6
7 var constructorParameters = typeNames.stream() // 2
8 .map(typeName -> ParameterSpec.builder(typeName, "$" + typeNames.indexOf(typeName)).build()) // 2
9 .toList();
10
11 var superCallParams = IntStream.range(0, typeNames.size()) // 3
12 .mapToObj(integer -> "$" + integer) // 3
13 .collect(Collectors.joining(", ")); // 3
14
15 return MethodSpec.constructorBuilder()
16 .addParameter(ParameterSpec.builder(TransactionManager.class, TRANSACTION_MANAGER).build()) // 2
17 .addParameters(constructorParameters) // 2
18 .addCode(CodeBlock.builder()
19 .addStatement("super($L)", superCallParams) // 3
20 .addStatement("this.$L = $L", TRANSACTION_MANAGER, TRANSACTION_MANAGER) // 4
21 .build())
22 .build();
23 }
24}
1class TestRepository$Intercepted {
2 TestRepository$Intercepted(TransactionManager transactionManager,
3 ParticipantRepository $0,
4 EventRepository $1) {
5 super($0, $1);
6 this.transactionManager = transactionManager;
7 }
8}
1@Singleton
2class RepositoryA$Intercepted extends RepositoryA {
3
4 @Override
5 void voidMethod() {
6 try {
7 transactionManager.begin();
8 super.voidMethod();
9 transactionManager.commit();
10 }
11 catch (Exception e) {
12 try {
13 transactionManager.rollback();
14 }
15 catch (Exception innerException) {
16 throw new RuntimeException(innerException);
17 }
18 throw new RuntimeException(e);
19 }
20 }
21
22 @Override
23 int intMethod() {
24 try {
25 transactionManager.begin();
26 var intMethodReturnValue = (int) super.intMethod();
27 transactionManager.commit();
28 return intMethodReturnValue;
29 }
30 catch (Exception e) {
31 try {
32 transactionManager.rollback();
33 }
34 catch (Exception innerException) {
35 throw new RuntimeException(innerException);
36 }
37 throw new RuntimeException(e);
38 }
39 }
40}
1class TransactionalInterceptedWriter {
2 private MethodSpec generateTransactionalMethod(ExecutableElement executableElement) {
3 var methodName = executableElement.getSimpleName().toString();
4 var transactionalMethodCall = transactionalMethodCall(executableElement);
5 var methodCode = tryClause(transactionalMethodCall, catchClause());
6 return MethodSpec.methodBuilder(methodName)
7 .addModifiers(executableElement.getModifiers())
8 .addParameters(executableElement.getParameters().stream().map(ParameterSpec::get).toList())
9 .addAnnotation(Override.class)
10 .addCode(methodCode)
11 .returns(TypeName.get(executableElement.getReturnType()))
12 .addTypeVariables(getTypeVariableIfNeeded(executableElement).stream().toList())
13 .build();
14 }
15}
1class TransactionalInterceptedWriter {
2 private CodeBlock transactionalMethodCall(ExecutableElement executableElement) {
3 return executableElement.getReturnType().getKind() == TypeKind.VOID // 1
4 ? transactionalVoidCall(executableElement)
5 : returningTransactionalMethodCall(executableElement);
6 }
7
8 private CodeBlock transactionalVoidCall(ExecutableElement method) { // 2
9 var params = translateMethodToSuperCallParams(method);
10 return CodeBlock.builder()
11 .addStatement(TRANSACTION_MANAGER + ".begin()")
12 .addStatement("super.$L(%s)".formatted(params), method.getSimpleName())
13 .addStatement(TRANSACTION_MANAGER + ".commit()")
14 .build();
15 }
16
17 private CodeBlock returningTransactionalMethodCall(ExecutableElement method) { // 3
18 var methodName = method.getSimpleName();
19 var params = translateMethodToSuperCallParams(method);
20 return CodeBlock.builder()
21 .addStatement(TRANSACTION_MANAGER + ".begin()")
22 .addStatement("var $LReturnValue = ($L) super.$L(%s)".formatted(params), methodName, method.getReturnType(), methodName)
23 .addStatement(TRANSACTION_MANAGER + ".commit()")
24 .addStatement("return $LReturnValue", methodName)
25 .build();
26 }
27
28 private String translateMethodToSuperCallParams(ExecutableElement method) {
29 // just code
30 }
31}
1@Singleton
2public class DeclarativeTransactionsParticipationService implements ParticipationService {
3 private final ParticipantRepository participantRepository;
4 private final EventRepository eventRepository;
5
6 public DeclarativeTransactionsParticipationService(
7 ParticipantRepository participantRepository,
8 ventRepository eventRepository
9 ) {
10 this.participantRepository = participantRepository;
11 this.eventRepository = eventRepository;
12 }
13
14 @Override
15 @Transactional
16 public void participate(ParticipantId participantId, EventId eventId) {
17 var participant = participantRepository.getParticipant(participantId);
18 var event = eventRepository.findEvent(eventId);
19 eventRepository.store(event.addParticipant(participant));
20
21 System.out.printf("Participant: '%s' takes part in event: '%s'%n", participant, event);
22 }
23}
1public interface ProcessorPlugin {
2 void init(ProcessingEnvironment processingEnv); // 1
3
4 Collection<JavaFile> process(Set<? extends Element> annotated); // 2
5
6 Class<? extends Annotation> reactsTo(); // 3
7}
1public class BeanProcessor extends AbstractProcessor {
2 private List<ProcessorPlugin> plugins = List.of();
3
4 @Override
5 public synchronized void init(ProcessingEnvironment processingEnv) { // 1
6 super.init(processingEnv);
7 plugins = List.of(new TransactionalPlugin());
8 plugins.forEach(processorPlugin -> processorPlugin.init(processingEnv));
9 }
10
11 @Override
12 public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { // 2
13 try {
14 runPluginsProcessing(roundEnv);
15 // rest of the processing
16 } catch (Exception e) {
17 // exception handling
18 }
19 // return
20 }
21
22 private void runPluginsProcessing(RoundEnvironment roundEnv) { // 3
23 plugins.stream().map(processorPlugin -> processorPlugin.process(roundEnv.getElementsAnnotatedWith(processorPlugin.reactsTo())))
24 .flatMap(Collection::stream)
25 .forEach(this::writeFile); // 4
26 }
27
28 private void writeFile(JavaFile javaFile) {} // 4
29}
1public class TransactionalPlugin implements ProcessorPlugin {
2 @Override
3 public void init(ProcessingEnvironment processingEnv) { // 1
4 this.processingEnv = processingEnv;
5 this.transactionalMessenger = new TransactionalMessenger(processingEnv.getMessager());
6 }
7
8 @Override
9 public Class<? extends Annotation> reactsTo() { //2
10 return Transactional.class;
11 }
12}
1public interface Intercepted {
2 Class<?> interceptedType();
3}
1@Singleton
2public class RepositoryA {
3 // some @Transactional methods
4}
1@Singleton
2class RepositoryA$Intercepted extends RepositoryA {
3 @Override
4 public Class interceptedType() {
5 return RepositoryA.class;
6 }
7 // Overridden transactional methods
8}
1class TransactionalInterceptedWriter {
2 private MethodSpec interceptedTypeMethod() {
3 return MethodSpec.methodBuilder("interceptedType")
4 .addAnnotation(Override.class)
5 .addModifiers(PUBLIC)
6 .addStatement("return $T.class", TypeName.get(transactionalElement.asType()))
7 .returns(ClassName.get(Class.class))
8 .build();
9 }
10}
1class BaseBeanProvider implements BeanProvider {
2 @Override
3 public <T> List<T> provideAll(Class<T> beanType) {
4 var allBeans = definitions.stream().filter(def -> beanType.isAssignableFrom(def.type()))
5 .map(def -> beanType.cast(def.create(this)))
6 .toList(); // 1
7 var interceptedTypes = allBeans.stream().filter(bean -> Intercepted.class.isAssignableFrom(bean.getClass()))
8 .map(bean -> ((Intercepted) bean).interceptedType())
9 .toList(); // 2
10 return allBeans.stream().filter(not(bean -> interceptedTypes.contains(bean.getClass()))).toList(); // 3
11 }
12}

Subscribe to our newsletter and never miss an article