I've defined the following ArchUnit tests that verify
any class or method annotated with Spring's @Transactional
annotation must also be annotated with @Service
the annotation @jakarta.transaction.Transactional
is not allowed on any class or method
import com.tngtech.archunit.junit.ArchTest;
import com.tngtech.archunit.lang.ArchRule;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.*;
class TransactionRules {
@ArchTest // test rule 1 for classes
static final ArchRule transactionalServiceClasses = classes()
.that().areAnnotatedWith(Transactional.class)
.should().beAnnotatedWith(Service.class);
@ArchTest // test rule 1 for methods
static final ArchRule transactionalServiceMethods = methods()
.that().areAnnotatedWith(Transactional.class)
.should().beDeclaredInClassesThat().areAnnotatedWith(Service.class);
@ArchTest // test rule 2 for classes
static final ArchRule noJakartaTransactionClasses = noClasses()
.should().beAnnotatedWith(jakarta.transaction.Transactional.class);
@ArchTest // test rule 2 for methods
static final ArchRule noJakartaTransactionMethods = noMethods()
.should().beAnnotatedWith(jakarta.transaction.Transactional.class);
}
I had to write 4 tests because I need separate tests for classes and methods. Is there a way to make this more concise and write a single test that will apply to a class or a method?
For "the" first case
- any class or method annotated with Spring's
@Transactional
annotation must also be annotated with@Service
(which I think are really two structurally different cases: for @Transactional
methods, you want the declaring classes to be @Service
s, not the methods themselves), I don't see how you could combine them if you want to have classes and methods reported as individual violations in the corresponding cases.
If you didn't care, but really preferred to combine the rules, you could in this case (when you also accept that, for a class annotated with @Transactional
but not @Service
, you'll get a violation for every method – also none if the class doesn't have any methods) use:
@ArchTest
ArchRule transactionalServices = methods()
.that().areAnnotatedWith(Transactional.class)
.or().areDeclaredInClassesThat().areAnnotatedWith(Transactional.class)
.should().beDeclaredInClassesThat().areAnnotatedWith(Service.class);
(I've expressed that I wouldn't do that, haven't I?)
The second case
- the annotation
@jakarta.transaction.Transactional
is not allowed on any class or method
is something that could actually be unified as a single rule for classes or methods (regarding their common functionality that they can have annotations [and have a source code location each]), cf. §7.5 Rules with Custom Concepts of the ArchUnit User Guide:
// Intersection types are tricky, cf. https://stackoverflow.com/q/6643241 .
// Please let me know if you find a better solution for this:
<HAS_ANNOTATIONS extends HasAnnotations<?> & HasSourceCodeLocation>
ClassesTransformer<HAS_ANNOTATIONS> classesOrMethodsWhichCanHaveAnnotations() {
return new AbstractClassesTransformer<>("classes/methods") {
@Override
public Iterable<HAS_ANNOTATIONS> doTransform(JavaClasses classes) {
List<HAS_ANNOTATIONS> result = new ArrayList(classes); // shortcut 🤫
classes.forEach(javaClass ->
result.addAll((Set<HAS_ANNOTATIONS>) javaClass.getMethods())
);
return result;
}
};
}
@ArchTest
ArchRule noJakartaTransaction = no(classesOrMethodsWhichCanHaveAnnotations())
.should(beAnnotatedWith(jakarta.transaction.Transactional.class));
This uses the following static imports:
import static com.tngtech.archunit.lang.syntax.ArchRuleDefinition.no;
import static com.tngtech.archunit.lang.conditions.ArchConditions.beAnnotatedWith;