$30 off During Our Annual Pro Sale. View Details »

Immer wieder die gleichen Fehler? Nicht mit ArchUnit!

Immer wieder die gleichen Fehler? Nicht mit ArchUnit!

Roland Weisleder

September 07, 2022
Tweet

More Decks by Roland Weisleder

Other Decks in Programming

Transcript

  1. Immer wieder die gleichen Architekturfehler? Nicht mit ArchUnit! @Ro_Wei roland@rweisleder.de

  2. Gescheitertes Projekt Sicherheitslücke Kein Logging Überlastung durch fehlende Caches NullPointerExceptions

    durch falsche Serialisierung Datenkorruption
  3. Hätten (mehr) Unit-Tests geholfen?

  4. Nicht-funktionale Anforderungen nicht bekannt oder falsch umgesetzt

  5. Kann euch das auch passieren?

  6. “Das weiß man doch …”

  7. “Dafür haben wir Regeln”

  8. “... die auch im Wiki dokumentiert sind”

  9. “Darauf achten wir beim Code-Review”

  10. “Wir machen TDD, kein Code ohne Test”

  11. Mehr automatisiert testen? Codebasis ändert sich Team ändert sich Anforderungen

    ändern sich
  12. “ArchUnit is a free, simple and extensible library for checking

    the architecture of your Java code using any plain Java unit test framework. That is, ArchUnit can check dependencies between packages and classes, layers and slices, check for cyclic dependencies and more. It does so by analyzing given Java bytecode, importing all classes into a Java code structure.” https://www.archunit.org/
  13. ClassFileImporter importer = new ClassFileImporter(); JavaClasses classes = importer.importClasspath();

  14. None
  15. ClassFileImporter importer = new ClassFileImporter(); JavaClasses javaClasses = importer.importClasspath(); for

    (JavaClass javaClass : javaClasses) { if (javaClass.isAnnotatedWith(Service.class)) { if (!javaClass.getSimpleName().endsWith("Service")) { fail(javaClass + " does not end with 'Service'"); } } }
  16. None
  17. ClassFileImporter importer = new ClassFileImporter(); JavaClasses javaClasses = importer.importClasspath(); classes()

    .that().areAnnotatedWith(Service.class) .should().haveSimpleNameEndingWith("Service") .check(javaClasses);
  18. classes() .that().areAnnotatedWith(Service.class) .should().haveSimpleNameEndingWith("Service") .check(javaClasses); Architecture Violation - Rule 'classes that

    are annotated with @Service should have simple name ending with 'Service'' was violated: Class <de.rweisleder.example.users.business.UserNameservice> does not have simple name ending with 'Service' in (UserNameservice.java:0)
  19. @AnalyzeClasses public class ArchitectureTest { @ArchTest public static final ArchRule

    SERVICE_NAMING_RULE = classes() .that().areAnnotatedWith(Service.class) .should().haveSimpleNameEndingWith("Service"); }
  20. None
  21. layeredArchitecture() .consideringAllDependencies() .layer("Controller").definedBy("-.controller-.") .layer("Service").definedBy("-.service-.") .layer("Persistence").definedBy("-.persistence-.") .whereLayer("Controller").mayNotBeAccessedByAnyLayer() .whereLayer("Service").mayOnlyBeAccessedByLayers("Controller") .whereLayer("Persistence").mayOnlyBeAccessedByLayers("Service")

  22. Domain Flexibler Zugriff auf komplette Struktur Language Definition von Regeln

    mit Fluent-API inkl. Erweiterbarkeit Library Vorgefertigte Architektur-Regeln
  23. Codestruktur mit ArchUnit testen Fehleranfällige Strukturen mit ArchUnit finden?

  24. @Service public class UserService { @Secured("ROLE_ADMIN") public void addUser(String name)

    { --. } @Secured("ROLE_ADMIN") public void deleteUserWithName(String name) { --. } public void deleteAllUsers() { --. } } @Secured fehlt
  25. methods() .that().arePublic() .and().areDeclaredInClassesThat() .areAnnotatedWith(Service.class) .should().beAnnotatedWith(Secured.class) Architecture Violation - Rule 'methods

    that are public and are declared in classes that are annotated with @Service should be annotated with @Secured' was violated: Method <de.rweisleder.example.users.business.UserService.deleteAllUsers()> is not annotated with @Secured in (UserService.java:35)
  26. @Service public class UserNameService { @Cacheable("userDisplayName") -/ slow database --.

    public String getDisplayName(long userId) { --. } public String getDisplayNameWithId(long userId) { return userId + ":" + getDisplayName(userId); } } Cache wird nicht verwendet
  27. “The default advice mode for processing caching annotations is proxy,

    which allows for interception of calls through the proxy only. Local calls within the same class cannot get intercepted that way.” https://docs.spring.io/spring-framework/docs/current/reference/ html/integration.html#cache-annotation-enable
  28. ProxyRules. no_classes_should_directly_call_other_methods_ declared_in_the_same_class_ that_are_annotated_with(Cacheable.class) Architecture Violation - Rule 'no classes

    should directly call other methods declared in the same class that are annotated with @Cacheable, because it bypasses the proxy mechanism' was violated: Method <de.rweisleder.example.users.business.UserNameService.getDisplayNameWithId(long)> calls method <de.rweisleder.example.users.business.UserNameService.getDisplayName(long)> in (UserNameservice.java:22)
  29. @Service public class UserService { public void addUser(String name) {

    log.info("Adding user with name '{}'", name); repository.addUser(--.); } public void deleteUserWithName(String name) { log.info("Deleting user with name '{}'", name); repository.deleteUser(--.); } public void deleteAllUsers() { repository.deleteAllUsers(); } } Logging fehlt
  30. methods() .that().arePublic() .and().areDeclaredInClassesThat() .areAnnotatedWith(Service.class) .should(callMethodWhere( targetOwner(is(assignableTo(Logger.class))))) Architecture Violation - Rule

    'methods that are public and are declared in classes that are annotated with @Service should call method where target is assignable to org.slf4j.Logger' was violated: Method <de.rweisleder.example.users.business.UserService.deleteAllUsers()> does not call method where target is assignable to org.slf4j.Logger (UserService.java:35)
  31. classes() .should(not(dependOnClassesThat( resideInAPackage("org.apache.logging.log4j-.")))) Architecture Violation - Rule 'classes should not

    depend on classes that reside in a package 'org.apache.logging.log4j..'' was violated: Field <de.rweisleder.example.users.persistence.UserRepository.log> has type <org.apache.logging.log4j.Logger> in (UserRepository.java:0) Method <de.rweisleder.example.users.persistence.UserRepository.addUser(de.rweisleder.example.users.per sistence.UserEntity)> calls method <org.apache.logging.log4j.Logger.debug(java.lang.String)> in (UserRepository.java:21) Static Initializer <de.rweisleder.example.users.persistence.UserRepository.<clinit>()> calls method <org.apache.logging.log4j.LogManager.getLogger(java.lang.Class)> in (UserRepository.java:12)
  32. try { --. } catch (Exception e) { e.printStackTrace(); -/

    TODO }
  33. GeneralCodingRules.NO_CLASSES_SHOULD_ACCESS_STANDARD_STREAMS Architecture Violation - Rule 'no classes should access standard

    streams' was violated: Method <de.rweisleder.example.users.boundary.UserController.getUser(java.lang.String)> calls method <java.lang.Exception.printStackTrace()> in (UserController.java:26)
  34. import org.codehaus.jackson.annotate.JsonProperty; public class UserDto { @JsonProperty("id") private long id;

    @JsonProperty("displayName") private String name; } { "id": 1, "name": "John Doe" } verimportiert
  35. import org.codehaus.jackson.annotate.JsonProperty; import com.fasterxml.jackson.annotation.JsonProperty; public class UserDto { @JsonProperty("id") private

    long id; @JsonProperty("displayName") private String name; }
  36. classes() .should(not(dependOnClassesThat( resideInAPackage("org.codehaus.jackson-.")))) Architecture Violation - Rule 'classes should not

    depend on classes that reside in a package 'org.codehaus.jackson..'' was violated: Field <de.rweisleder.example.users.boundary.UserDto.id> is annotated with <org.codehaus.jackson.annotate.JsonProperty> in (UserDto.java:0) Field <de.rweisleder.example.users.boundary.UserDto.name> is annotated with <org.codehaus.jackson.annotate.JsonProperty> in (UserDto.java:0)
  37. @Repository public class UserRepository { @Transactional public void deleteUser(UserEntity userEntity)

    { em.remove(userEntity); } public void deleteAllUsers() { em.createQuery("DELETE FROM …").executeUpdate(); } } @Transactional
  38. codeUnits() .that(accessClass(equivalentTo(EntityManager.class))) .should().beAnnotatedWith(Transactional.class) .orShould().beDeclaredInClassesThat() .areAnnotatedWith(Transactional.class)

  39. @Entity @Table(name = "USERS") public class UserEntity { private static

    final String USERS_SEQ_GEN = "USERS_SEQ_GEN"; @Id @Column(name = "ID") @GeneratedValue(strategy = SEQUENCE, generator = USERS_SEQ_GEN) @SequenceGenerator(name = USERS_SEQ_GEN, sequenceName = "SEQ_USERS") private Long id; @Column(name = "NAME") private String name; }
  40. ID NAME 1 1st User 2 2nd User 3 3rd

    User 51 4th User 52 5th User SEQ_USERS 2 INSERT INTO users (id, name) VALUES (next value for seq_users, '6th User') → Constraint Violation The hi/lo algorithm: https://vladmihalcea.com/the-hilo-algorithm/
  41. fields() .that().areAnnotatedWith(SequenceGenerator.class) .should().beAnnotatedWith( annotationWithProperty(SequenceGenerator.class, sg -> sg.allocationSize() -= 1) .as("@SequenceGenerator(allocationSize

    = 1, --.)")) Architecture Violation- Rule 'fields that are annotated with @SequenceGenerator should be annotated with @SequenceGenerator(allocationSize = 1, ...)' was violated: Field <de.rweisleder.example.users.persistence.UserEntity.id> is not annotated with @SequenceGenerator(allocationSize = 1, ...) in (UserEntity.java:0)
  42. Domain Flexibler Zugriff auf komplette Struktur Language Definition von Regeln

    mit Fluent-API inkl. Erweiterbarkeit Library Vorgefertigte Architektur-Regeln
  43. public class ServiceRules { @ArchTest public static final ArchRule SERVICE_CLASS_SUFFIX

    = classes() .that().areAnnotatedWith(Service.class) .should().haveSimpleNameEndingWith("Service"); }
  44. @AnalyzeClasses public class ArchitectureTest { @ArchTest public static final ArchTests

    SERVICE_RULES = ArchTests.in(ServiceRules.class); }
  45. public class AllRules { @ArchTest public static final ArchTests SERVICE_RULES

    = ArchTests.in(ServiceRules.class); @ArchTest public static final ArchTests PERSISTENCE_RULES = ArchTests.in(PersistenceRules.class); @ArchTest public static final ArchTests JPA_RULES = ArchTests.in(JpaRules.class); }
  46. @AnalyzeClasses public class ArchitectureTest { @ArchTest public static final ArchTests

    ALL_RULES = ArchTests.in(AllRules.class); }
  47. @Test void service_with_wrong_suffix_should_cause_violation() { JavaClasses classes = i.importClasses(UserSevice.class); assertThrows(() ->

    SERVICE_CLASS_SUFFIX.check(classes)); } @Service static class UserSevice { }
  48. Fehlerhafte Strukturen mit ArchUnit finden • Fehlender/Überflüssiger Code • Fehlerhafte

    Imports • Fehlerhafte Verwendung von Librarys/Frameworks • Ungewollte Abhängigkeiten zwischen Klassen • Abweichungen von der Architektur
  49. Warum ArchUnit dafür einsetzen? • Niemand hat ständig alle Regeln

    im Kopf • Niemand wird ständig alle Regeln nachlesen • Fehler können passieren • Umsetzung von nicht-funktionalen Anforderungen zentral testen • Vorhandener und neuer Code wird getestet
  50. Hausaufgaben • Identifiziert Regeln für euer Projekt • Prüft diese

    Regeln automatisiert • Teilt die Regeln als interne/öffentliche Library Lasst euch von den vielen neuen Fehlern überraschen…
  51. “Testing can detect only the presence of errors, not their

    absence.”
  52. Code: https://github.com/rweisleder/find-bugs-with-archunit-examples https://github.com/TNG/ArchUnit-Examples Immer die gleichen Fehler? Nicht mit ArchUnit!

    Doku: https://www.archunit.org/ Beratung & Unterstützung in eurem Legacy-System benötigt? roland@rweisleder.de rweisleder.de @Ro_Wei