Let's connect
Let's connect

Transaction handling using an annotation processor

Picture of Jacek Dubikowski, Senior Software Engineer

Jacek Dubikowski

Senior Software Engineer

15 minutes read

console

Begin transaction
Participant: 'Participant[]' takes part in event: 'Event[]'
Commit transaction

java

@Singleton
public class ManualTransactionParticipationService implements ParticipationService {
    private final ParticipantRepository participantRepository;
    private final EventRepository eventRepository;
    private final TransactionManager transactionManager;


    // constructor

    @Override
    public void participate(ParticipantId participantId, EventId eventId) {
        try {
            transactionManager.begin();
            var participant = participantRepository.getParticipant(participantId);
            var event = eventRepository.findEvent(eventId);
            eventRepository.store(event.addParticipant(participant));

            System.out.printf("Participant: '%s' takes part in event: '%s'%n", participant, event);

            transactionManager.commit();
        } catch (Exception e) {
            rollback();
            throw new RuntimeException(e);
        }
    }

    private void rollback() {
        try {
            transactionManager.rollback();
        } catch (SystemException e) {
            throw new RuntimeException(e);
        }
    }
}

java

@Singleton
public class DeclarativeTransactionsParticipationService implements ParticipationService {
    private final ParticipantRepository participantRepository;
    private final EventRepository eventRepository;
    // constructor

    @Override
    @Transactional
    public void participate(ParticipantId participantId, EventId eventId) {
            var participant = participantRepository.getParticipant(participantId);
            var event = eventRepository.findEvent(eventId);
            eventRepository.store(event.addParticipant(participant));
            
            System.out.printf("Participant: '%s' takes part in event: '%s'%n", participant, event);
    }
}

java

@Singleton
public class RepositoryA {

    @Transactional
    void voidMethod() {
    }

    int intMethod() {
        return 1;
    }
}

java

@Singleton
class RepositoryA$Intercepted extends RepositoryA {
  private final TransactionManager transactionManager;

  RepositoryA$Intercepted(TransactionManager transactionManager) {
    super();
    this.transactionManager = transactionManager;
  }

  @Override
  void voidMethod() {
    // transaction handling code
  }
}

java

public class TransactionalPlugin implements ProcessorPlugin { // 7
   private TransactionalMessenger transactionalMessenger; // 3

   @Override
    public Collection<JavaFile> process(Set<? extends Element> annotated) { // 1
        Set<ExecutableElement> transactionalMethods = ElementFilter.methodsIn(annotated); // 2
        validateMethods(transactionalMethods); // 3
        Map<TypeElement, List<ExecutableElement>> typeToTransactionalMethods = transactionalMethods.stream() // 4
                .collect(groupingBy(element -> (TypeElement) element.getEnclosingElement())); // 4
        return typeToTransactionalMethods.entrySet()
                .stream()
                .map(this::writeTransactional) // 5
                .toList();
    }

    private void validateMethods(Set<ExecutableElement> transactionalMethods) { // 3
        raiseForPrivate(transactionalMethods);
        raiseForStatic(transactionalMethods);
        raiseForFinalMethods(transactionalMethods);
        raiseForFinalClass(transactionalMethods);
    }

    private JavaFile writeTransactional(Map.Entry<TypeElement, List<ExecutableElement>> typeElementListEntry) { // 5
        var transactionalType = typeElementListEntry.getKey();
        var transactionalMethods = typeElementListEntry.getValue();
        PackageElement packageElement = processingEnv.getElementUtils().getPackageOf(transactionalType);
        return new TransactionalInterceptedWriter(transactionalType, transactionalMethods, packageElement) // 6
                .createDefinition(processingEnv.getMessager()); // 6
    }
    
   // more methods ...
}

java

class TransactionalInterceptedWriter {
    private static final String TRANSACTION_MANAGER = "transactionManager";
    private static final Modifier[] PRIVATE_FINAL_MODIFIERS = {Modifier.PRIVATE, Modifier.FINAL};
    
    private final TypeElement transactionalElement; // 1 
    private final List<ExecutableElement> transactionalMethods; // 2
    private final PackageElement packageElement; // 3
}

java

class TransactionalInterceptedWriter {
    
    public JavaFile createDefinition(Messager messager) {
        TypeSpec typeSpec = TypeSpec.classBuilder("%s$Intercepted".formatted(transactionalElement.getSimpleName().toString())) // 1
                .addAnnotation(Singleton.class) // 2
                .superclass(transactionalElement.asType()) // 3
                .addSuperinterface(TypeName.get(Intercepted.class)) // 4
                .addMethod(interceptedTypeMethod()) // 4
                .addField(TransactionManager.class, TRANSACTION_MANAGER, PRIVATE_FINAL_MODIFIERS) // 5
                .addMethod(constructor(messager)) // 6
                .addMethods(transactionalMethodDefinitions()) // 7
                .build();
        return JavaFile.builder(packageElement.getQualifiedName().toString(), typeSpec).build(); // 8
    }
    
}

java

class TransactionalInterceptedWriter {

    private MethodSpec constructor(Messager messager) {
        Dependency dependency = new TypeDependencyResolver().resolve(transactionalElement, messager); // 1
        var typeNames = dependency.dependencies().stream().map(TypeName::get).toList(); // 1
        
        var constructorParameters = typeNames.stream() // 2
                .map(typeName -> ParameterSpec.builder(typeName, "$" + typeNames.indexOf(typeName)).build()) // 2
                .toList();
        
        var superCallParams = IntStream.range(0, typeNames.size()) // 3
                .mapToObj(integer -> "$" + integer) // 3
                .collect(Collectors.joining(", ")); // 3

        return MethodSpec.constructorBuilder()
                .addParameter(ParameterSpec.builder(TransactionManager.class, TRANSACTION_MANAGER).build()) // 2
                .addParameters(constructorParameters) // 2
                .addCode(CodeBlock.builder()
                        .addStatement("super($L)", superCallParams) // 3
                        .addStatement("this.$L = $L", TRANSACTION_MANAGER, TRANSACTION_MANAGER) // 4
                        .build())
                .build();
    }
}

java

class TestRepository$Intercepted {
   TestRepository$Intercepted(TransactionManager transactionManager,
                              ParticipantRepository $0, 
                              EventRepository $1) {
      super($0, $1);
      this.transactionManager = transactionManager;
   }
}

java

@Singleton
class RepositoryA$Intercepted extends RepositoryA { 
    
  @Override
  void voidMethod() {
    try {
      transactionManager.begin();
      super.voidMethod();
      transactionManager.commit();
    }
    catch (Exception e) {
      try {
        transactionManager.rollback();
      }
      catch (Exception innerException) {
        throw new RuntimeException(innerException);
      }
      throw new RuntimeException(e);
    }
  }

  @Override
  int intMethod() {
    try {
      transactionManager.begin();
      var intMethodReturnValue = (int) super.intMethod();
      transactionManager.commit();
      return intMethodReturnValue;
    }
    catch (Exception e) {
      try {
        transactionManager.rollback();
      }
      catch (Exception innerException) {
        throw new RuntimeException(innerException);
      }
      throw new RuntimeException(e);
    }
  }
}

java

class TransactionalInterceptedWriter {
   private MethodSpec generateTransactionalMethod(ExecutableElement executableElement) {
      var methodName = executableElement.getSimpleName().toString();
      var transactionalMethodCall = transactionalMethodCall(executableElement);
      var methodCode = tryClause(transactionalMethodCall, catchClause());
      return MethodSpec.methodBuilder(methodName)
              .addModifiers(executableElement.getModifiers())
              .addParameters(executableElement.getParameters().stream().map(ParameterSpec::get).toList())
              .addAnnotation(Override.class)
              .addCode(methodCode)
              .returns(TypeName.get(executableElement.getReturnType()))
              .addTypeVariables(getTypeVariableIfNeeded(executableElement).stream().toList())
              .build();
   }
}

java

class TransactionalInterceptedWriter {
   private CodeBlock transactionalMethodCall(ExecutableElement executableElement) {
      return executableElement.getReturnType().getKind() == TypeKind.VOID // 1
              ? transactionalVoidCall(executableElement)
              : returningTransactionalMethodCall(executableElement);
   }

   private CodeBlock transactionalVoidCall(ExecutableElement method) { // 2
      var params = translateMethodToSuperCallParams(method);
      return CodeBlock.builder()
              .addStatement(TRANSACTION_MANAGER + ".begin()")
              .addStatement("super.$L(%s)".formatted(params), method.getSimpleName())
              .addStatement(TRANSACTION_MANAGER + ".commit()")
              .build();
   }

   private CodeBlock returningTransactionalMethodCall(ExecutableElement method) { // 3
      var methodName = method.getSimpleName();
      var params = translateMethodToSuperCallParams(method);
      return CodeBlock.builder()
              .addStatement(TRANSACTION_MANAGER + ".begin()")
              .addStatement("var $LReturnValue = ($L) super.$L(%s)".formatted(params), methodName, method.getReturnType(), methodName)
              .addStatement(TRANSACTION_MANAGER + ".commit()")
              .addStatement("return $LReturnValue", methodName)
              .build();
   }

   private String translateMethodToSuperCallParams(ExecutableElement method) { 
      // just code
   }
}

java

@Singleton
public class DeclarativeTransactionsParticipationService implements ParticipationService {
    private final ParticipantRepository participantRepository;
    private final EventRepository eventRepository;

    public DeclarativeTransactionsParticipationService(
            ParticipantRepository participantRepository, 
            ventRepository eventRepository
    ) {
        this.participantRepository = participantRepository;
        this.eventRepository = eventRepository;
    }

    @Override
    @Transactional
    public void participate(ParticipantId participantId, EventId eventId) {
            var participant = participantRepository.getParticipant(participantId);
            var event = eventRepository.findEvent(eventId);
            eventRepository.store(event.addParticipant(participant));
            
            System.out.printf("Participant: '%s' takes part in event: '%s'%n", participant, event);
    }
}

java

public interface ProcessorPlugin {
    void init(ProcessingEnvironment processingEnv); // 1

    Collection<JavaFile> process(Set<? extends Element> annotated); // 2

    Class<? extends Annotation> reactsTo(); // 3
}

java

public class BeanProcessor extends AbstractProcessor {
    private List<ProcessorPlugin> plugins = List.of();

    @Override
    public synchronized void init(ProcessingEnvironment processingEnv) { // 1
        super.init(processingEnv);
        plugins = List.of(new TransactionalPlugin());
        plugins.forEach(processorPlugin -> processorPlugin.init(processingEnv));
    }

    @Override
    public boolean process(Set<? extends TypeElement> annotations, RoundEnvironment roundEnv) { // 2
        try {
            runPluginsProcessing(roundEnv);
            // rest of the processing 
        } catch (Exception e) {
           // exception handling
        }
        // return
    }

    private void runPluginsProcessing(RoundEnvironment roundEnv) { // 3
        plugins.stream().map(processorPlugin -> processorPlugin.process(roundEnv.getElementsAnnotatedWith(processorPlugin.reactsTo())))
                .flatMap(Collection::stream)
                .forEach(this::writeFile); // 4
    }
    
    private void writeFile(JavaFile javaFile) {} // 4
}

java

public class TransactionalPlugin implements ProcessorPlugin { 
    @Override
    public void init(ProcessingEnvironment processingEnv) { // 1
        this.processingEnv = processingEnv;
        this.transactionalMessenger = new TransactionalMessenger(processingEnv.getMessager());
    }

    @Override
    public Class<? extends Annotation> reactsTo() { //2
        return Transactional.class;
    }
}

java

public interface Intercepted {
    Class<?> interceptedType();
}

java

@Singleton
public class RepositoryA {
    // some @Transactional methods
}

java

@Singleton
class RepositoryA$Intercepted extends RepositoryA {
   @Override
   public Class interceptedType() {
      return RepositoryA.class;
   }
   // Overridden transactional methods
}

java

class TransactionalInterceptedWriter {
   private MethodSpec interceptedTypeMethod() {
      return MethodSpec.methodBuilder("interceptedType")
              .addAnnotation(Override.class)
              .addModifiers(PUBLIC)
              .addStatement("return $T.class", TypeName.get(transactionalElement.asType()))
              .returns(ClassName.get(Class.class))
              .build();
   }
}

java

class BaseBeanProvider implements BeanProvider {
   @Override
   public <T> List<T> provideAll(Class<T> beanType) {
      var allBeans = definitions.stream().filter(def -> beanType.isAssignableFrom(def.type()))
              .map(def -> beanType.cast(def.create(this)))
              .toList(); // 1
      var interceptedTypes = allBeans.stream().filter(bean -> Intercepted.class.isAssignableFrom(bean.getClass()))
              .map(bean -> ((Intercepted) bean).interceptedType())
              .toList(); // 2
      return allBeans.stream().filter(not(bean -> interceptedTypes.contains(bean.getClass()))).toList(); // 3
   }
}

Liked the article?

Share it with others!

explore more on

Take the first step to a sustained competitive edge for your business

Get your free consultation

VirtusLab's work has met the mark several times over, and their latest project is no exception. The team is efficient, hard-working, and trustworthy. Customers can expect a proactive team that drives results.

Stephen Rooke
Stephen RookeDirector of Software Development @ Extreme Reach

VirtusLab's engineers are truly Strapi extensions experts. Their knowledge and expertise in the area of Strapi plugins gave us the opportunity to lift our multi-brand CMS implementation to a different level.

facile logo
Leonardo PoddaEngineering Manager @ Facile.it

VirtusLab has been an incredible partner since the early development of Scala 3, essential to a mature and stable Scala 3 ecosystem.

Martin_Odersky
Martin OderskyHead of Programming Research Group @ EPFL

The VirtusLab team's in-depth knowledge, understanding, and experience of technology have been invaluable to us in developing our product. The team is professional and delivers on time – we greatly appreciated this efficiency when working with them.

Michael_Grant
Michael GrantDirector of Development @ Cyber Sec Company