Over a million developers have joined DZone.
{{announcement.body}}
{{announcement.title}}

Enforcing Your Architecture With ArchUnit

DZone's Guide to

Enforcing Your Architecture With ArchUnit

Want to learn how to enforce an architecture that easily resolves issues with the @Sensitive annotation? Click here to learn how with the ArchUnit.

· Java Zone ·
Free Resource

Get the Edge with a Professional Java IDE. 30-day free trial.

Recently, I was working on an application that relied heavily on a variety of data sources. Our application actually combines, interprets, and visualizes the data.

Not everyone is allowed to see all the data. We needed a simple way to protect access to our resources, though not solely based on the typical users and roles. Access to data should be protected on a more granularized level. However, on the one hand, access rules are defined by a set of mutually excluding parameters. We also wanted to keep the code as clean and maintainable as possible.

Since our application already relied on Spring Boot, our choice fell on spring-security and to be more precise it's@PreAuthorize annotation.

We could have chosen to copy/paste/adapt the enclosed SPeL expression; however, this would quickly introduce maintenance issues, e.g. given we only have two distinct parameters that are used to grant access to a piece of data. Then, we would still be copying the SPeL expressions throughout the code, introducing maintenance issues whenever the access rules change.

@Component
public class SomeSecuredResource {

    @PreAuthorize("hasAccess(#foo)")
    public SensitiveData showMe(Foo foo){
        return new SensitiveData();
    }

    @PreAuthorize("isAllowedToSee(#bar)")
    public SensitiveData showMeToo(Bar bar){
        return new SensitiveData();
    }
}


So, we created a simple annotation for that:

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
@Inherited
@Documented
@PreAuthorize(Sensitive.EXPRESSION)

public @interface Sensitive {

    String EXPRESSION = "#foo != null ? hasAccess(#foo) : (#bar != null ? isAllowedToSee(#bar) : false)";

}


Which, we can now apply to the following:

@Component
public class SomeSecuredResource {

    @Sensitive
    public SensitiveData showMe(Foo foo){
        return new SensitiveData();
    }

    @Sensitive
    public SensitiveData showMeToo(Bar bar){
        return new SensitiveData();
    }
}


Fine. Problem solved, one would think...

That's only partially true. We isolated the logic, which grants access to a method given a specific method parameter, but we introduced other less visible issues.

Suppose a new developer joins the team. And, they see these  @Sensitive annotations protecting access to resources. They apply them to the following code:

@Component
public class NewDevsClass {

    @Sensitive
    public SensitiveData doSomething(long someParameter){
       return new SensitiveData(); 
    }

    @Sensitive
    public SensitiveData doOtherStuff(String foo){
        return new SensitiveData();
    }
}


Now, they are breaking the implicit rules here. As we have seen, the implementation of our @Sensitive annotation relies on a parameter of type Foo or  Bar .

Actually, they are now breaking our architectural rule, which is:

Methods annotated with the  @Sensitive  annotation must have a parameter of type  Foo  or Bar .

So, how do we solve this? Do we need to keep an extensive list of rules and let everyone sift through it? Or, do we configure a sonar rule and collect the reports every night? No, what about embedding those rules in a fast-executing unit test and getting immediate feedback?

And now, please welcome ArchUnit

ArchUnit is a Java architecture test library to specify and assert architecture rules in plain Java

So, let's dig in and write a test that ensures correct usage of our annotation.

public class SensitiveAnnotationUsageTest {

    DescribedPredicate<JavaClass> haveAFieldAnnotatedWithSensitive =
            new DescribedPredicate<JavaClass>("have a field annotated with @Sensitive"){
                @Override
                public boolean apply(JavaClass input) {
                    // note : simplified version which inspects all classesreturn true;
                }
            };

    ArchCondition<JavaClass> mustContainAParameterOfTypeFooOrBar =
            new ArchCondition<JavaClass>("must have parameter of type 'com.example.post.Foo' or 'com.example.post.Bar'") {
                @Override
                public void check(JavaClass item, ConditionEvents events) {
                    List<JavaMethod> collect = item.getMethods().stream()
                            .filter(method -> method.isAnnotatedWith(Sensitive.class)).collect(Collectors.toList());

                    for(JavaMethod method: collect){

                        List<String> names = method.getParameters().getNames();

                        if(!names.contains("com.example.post.Foo") && !names.contains("com.example.post.Bar"))  {
                            String message = String.format(
                                    "Method %s bevat geen parameter met type 'Foo' of 'Bar", method.getFullName());
                            events.add(SimpleConditionEvent.violated(method, message));
                        }
                    }
                }
            };

    @Test
    public void checkArchitecturalRules(){
        JavaClasses importedClasses = new ClassFileImporter().importPackages("com.example.post");

        ArchRule rule = ArchRuleDefinition.classes()
                .that(haveAFieldAnnotatedWithSensitive)
                .should(mustContainAParameterOfTypeFooOrBar);


        rule.check(importedClasses);
    }
}


Executing this test on our two classes results in the following result:

java.lang.AssertionError: Architecture Violation [Priority: MEDIUM] - Rule 'classes that have a field annotated with @Sensitive should must have parameter of type 'com.example.post.Foo' was violated (2 times):
Method com.example.post.NewDevsClass.doOtherStuff(java.lang.String) bevat geen parameter met type 'Foo' of 'Bar
Method com.example.post.NewDevsClass.doSomething(long) bevat geen parameter met type 'Foo' of 'Bar


And, voila! We have a test in place that can enforce a correct application of our architectural rules.

Are you curious to find out more about the ArchUnit? Make sure to check out their user guide and examples.

Get the Java IDE that understands code & makes developing enjoyable. Level up your code with IntelliJ IDEA. Download the free trial.

Topics:
architecture ,java ,tutorial ,archunit ,annotations

Opinions expressed by DZone contributors are their own.

{{ parent.title || parent.header.title}}

{{ parent.tldr }}

{{ parent.urlSource.name }}