Skip to content

Commit

Permalink
improve userguide
Browse files Browse the repository at this point in the history
* replace reference to @deprecated methods getType() & getParameters()
* replace reference to @deprecated ImportOptions DONT_INCLUDE_JARS & DONT_INCLUDE_TESTS
* domain-overview diagram:
  - indicate that JavaStaticInitializer does not have a ThrowsClause
  - add multiplicities
* mention ArchRuleDefinition's other methods to compose member rules
* remove a bunch of (AFAIK) superfluous commas ;-)

Signed-off-by: Manfred Hanke <Manfred.Hanke@tngtech.com>
  • Loading branch information
hankem committed Mar 29, 2019
1 parent ce6618a commit 85c5d06
Show file tree
Hide file tree
Showing 8 changed files with 159 additions and 147 deletions.
15 changes: 8 additions & 7 deletions docs/userguide/005_Ideas_and_Concepts.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -9,13 +9,14 @@ section will explain these layers in more detail.

=== Core

Much of ArchUnit's core API resembles the Java Reflection API. There are classes
like `JavaMethod`, `JavaField`, and more, and the public API consists of methods like
`getName()`, `getMethods()`, `getType()` or `getParameters()`. Additionally ArchUnit extends
this API for concepts needed to talk about dependencies between code, like `JavaMethodCall`,
`JavaConstructorCall` or `JavaFieldAccess`. For example, it is possible to programmatically
iterate over `javaClass.getAccessesFromSelf()` and react to the imported accesses between this
Java class and other Java classes.
Much of ArchUnit's core API resembles the Java Reflection API.
There are classes like `JavaMethod`, `JavaField`, and more,
and the public API consists of methods like `getName()`, `getMethods()`,
`getRawType()` or `getRawParameterTypes()`.
Additionally ArchUnit extends this API for concepts needed to talk about dependencies between code,
like `JavaMethodCall`, `JavaConstructorCall` or `JavaFieldAccess`.
For example, it is possible to programmatically iterate over `javaClass.getAccessesFromSelf()`
and react to the imported accesses between this Java class and other Java classes.

To import compiled Java class files, ArchUnit provides the `ClassFileImporter`, which can
for example be used to import packages from the classpath:
Expand Down
53 changes: 27 additions & 26 deletions docs/userguide/006_The_Core_API.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -33,7 +33,7 @@ is imported. This can be achieved by specifying `ImportOptions`:
ImportOption ignoreTests = new ImportOption() {
@Override
public boolean includes(Location location) {
return !location.contains("/test/"); // ignore any URI to sources, that contains '/test/'
return !location.contains("/test/"); // ignore any URI to sources that contains '/test/'
}
};
Expand All @@ -52,21 +52,21 @@ there already exist predefined `ImportOptions`:
[source,java,options="nowrap"]
----
new ClassFileImporter()
.withImportOption(ImportOption.Predefined.DONT_INCLUDE_JARS)
.withImportOption(ImportOption.Predefined.DONT_INCLUDE_TESTS)
.withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_JARS)
.withImportOption(ImportOption.Predefined.DO_NOT_INCLUDE_TESTS)
.importClasspath();
----

==== Dealing with Missing Classes

While importing the requested classes (e.g. `target/classes` or `target/test-classes`)
it can happen, that a class within the scope of the import has a reference to a class outside of the
it can happen that a class within the scope of the import has a reference to a class outside of the
scope of the import. This will naturally happen, if the classes of the JDK are not imported,
since then for example any dependency on `Object.class` will be unresolved within the import.

At this point ArchUnit needs to decide how to treat these classes that are missing from the
import. By default, ArchUnit searches within the classpath for missing classes and if found
imports them. This obviously has the advantage, that information about those classes
imports them. This obviously has the advantage that information about those classes
(which interfaces they implement, how they are annotated) is present during rule evaluation.

On the downside this additional lookup from the classpath will cost some performance and in some
Expand Down Expand Up @@ -115,18 +115,19 @@ class JavaFieldAccess
class JavaConstructorCall
class JavaMethodCall
JavaPackage *--* JavaClass : has
JavaClass *-- JavaMember : has
JavaPackage *--* "1..*" JavaClass : has
JavaClass *-- "0..*" JavaMember : has
JavaMember <|-- JavaField : extends
JavaMember <|-- JavaCodeUnit : extends
JavaCodeUnit *-left- ThrowsClause : has
JavaCodeUnit <|-- JavaConstructor : extends
JavaCodeUnit <|-- JavaMethod : extends
JavaCodeUnit <|-- JavaStaticInitializer : extends
JavaConstructor *-- "1" ThrowsClause : has
JavaMethod *-- "1" ThrowsClause : has
JavaCodeUnit *-- JavaFieldAccess : has
JavaCodeUnit *-- JavaMethodCall : has
JavaCodeUnit *-- JavaConstructorCall : has
JavaCodeUnit *-- "0..*" JavaFieldAccess : has
JavaCodeUnit *-- "0..*" JavaMethodCall : has
JavaCodeUnit *-- "0..*" JavaConstructorCall : has
----

Most objects resemble the Java Reflection API, including inheritance relations. Thus a `JavaClass`
Expand All @@ -137,7 +138,7 @@ calls 'code unit', and is in fact either a method, a constructor (including the
or a static initializer of a class (e.g. a `static { ... }` block, a static field assignment,
etc.).

Furthermore one of the most interesting features of ArchUnit, that exceeds the Java Reflection API,
Furthermore one of the most interesting features of ArchUnit that exceeds the Java Reflection API,
is the concept of accesses to another class. On the lowest level accesses can only take place
from a code unit (as mentioned, any block of executable code) to either a field (`JavaFieldAccess`),
a method (`JavaMethodCall`) or constructor (`JavaConstructorCall`).
Expand All @@ -146,8 +147,8 @@ ArchUnit imports the whole graph of classes and their relationship to each other
the accesses *from* a class is pretty isolated (the bytecode offers all this information),
checking accesses *to* a class requires the whole graph to be built first. To distinguish which
sort of access is referred to, methods will always clearly state *fromSelf* and *toSelf*.
For example, every `JavaField` allows to call `JavaField#getAccessesToSelf()`, to retrieve all
code units within the graph, that access this specific field. The resolution process through
For example, every `JavaField` allows to call `JavaField#getAccessesToSelf()` to retrieve all
code units within the graph that access this specific field. The resolution process through
inheritance is not completely straight forward. Consider for example

[plantuml, "resolution-example"]
Expand Down Expand Up @@ -180,7 +181,7 @@ ClassAccessing o-- ClassBeingAccessed

The bytecode will record a field access from `ClassAccessing.accessField()` to
`ClassBeingAccessed.accessedField`. However, there is no such field, since the field is
actually declared in the superclass. This is the reason, that a `JavaFieldAccess`
actually declared in the superclass. This is the reason why a `JavaFieldAccess`
has no `JavaField` as its target, but a `FieldAccessTarget`. In other words, ArchUnit models
the situation, as it is found within the bytecode, and an access target is not an actual
member within another class. If a member is queried for `accessesToSelf()` though, ArchUnit
Expand Down Expand Up @@ -223,14 +224,14 @@ ConstructorCallTarget "1" -- "0..1" JavaConstructor : resolves to

Two things might seem strange at the first look.

First, why can a target resolve to zero matching members? The reason is, that the set of classes
First, why can a target resolve to zero matching members? The reason is that the set of classes
that was imported does not need to have all classes involved within this resolution process.
Consider the above example, if `SuperClassBeingAccessed` would not be imported, ArchUnit would
have no way of knowing, where the actual targeted field resides. Thus in this case the
have no way of knowing where the actual targeted field resides. Thus in this case the
resolution would return zero elements.

Second, why can there be more than one resolved methods for method calls?
The reason for this is, that a call target might indeed match several methods in those
The reason for this is that a call target might indeed match several methods in those
cases, for example:

[plantuml, "diamond-example"]
Expand Down Expand Up @@ -265,21 +266,21 @@ D -right- C : calls targetMethod()
----

While this situation will always be resolved in a specified way for a real program,
ArchUnit can not do the same. Instead, the resolution will report all candidates that match a
ArchUnit cannot do the same. Instead, the resolution will report all candidates that match a
specific access target, so in the above example, the call target `C.targetMethod()` would in fact
resolve to two `JavaMethods`, namely `A.targetMethod()` and `B.targetMethod()`. Likewise a check
of either `A.targetMethod.getCallsToSelf()` or `B.targetMethod.getCallsToSelf()` would return
the same call from `D.callTargetMethod()` to `C.targetMethod()`.

==== Domain Objects, Reflection and the Classpath

ArchUnit tries to offer a lot of information from the bytecode, for example a `JavaClass`
provides details like if it is an Enum or an Interface, modifiers like `public` or `abstract`,
ArchUnit tries to offer a lot of information from the bytecode. For example, a `JavaClass`
provides details like if it is an enum or an interface, modifiers like `public` or `abstract`,
but also the source, where this class was imported from (namely the URI mentioned in the first
section). However, if information if missing, and the classpath is correct, ArchUnit offers
some convenience to rely on the reflection API for extended details. For this reason, most
`Java*`-Objects offer a method `reflect()`, which will in fact try to resolve the respective
object from the Reflection API. For example
`Java*` objects offer a method `reflect()`, which will in fact try to resolve the respective
object from the Reflection API. For example:

[source,java,options="nowrap"]
----
Expand All @@ -299,8 +300,8 @@ Method lengthMethod = javaMethod.reflect();
However, this will throw an `Exception`, if the respective classes are missing on the classpath
(e.g. because they were just imported from some file path).

This restriction also applies to handling Annotations in a more convenient way.
Consider some Annotation
This restriction also applies to handling annotations in a more convenient way.
Consider the following annotation:

[source,java,options="nowrap"]
----
Expand All @@ -309,7 +310,7 @@ Consider some Annotation
}
----

If you need to access this annotation, without this annotation on the classpath you must rely on
If you need to access this annotation without it being on the classpath, you must rely on

[source,java,options="nowrap"]
----
Expand Down
74 changes: 39 additions & 35 deletions docs/userguide/007_The_Lang_API.adoc
Original file line number Diff line number Diff line change
Expand Up @@ -33,11 +33,11 @@ for (JavaClass service : services) {

What we want to express, is the rule _"no classes that reside in a package 'service' should
access classes that reside in a package 'controller'"_. Nevertheless, it's hard to read through
that code and distill that information. And the same process has to be done every time, someone
that code and distill that information. And the same process has to be done every time someone
needs to understand the semantics of this rule.

To solve this shortcoming, ArchUnit offers a high level API to express architectural concepts
in a concise way. In fact, we can write code, that is almost equivalent to the prose rule text
in a concise way. In fact, we can write code that is almost equivalent to the prose rule text
mentioned before:

[source,java,options="nowrap"]
Expand All @@ -49,9 +49,9 @@ ArchRule rule = ArchRuleDefinition.noClasses()
rule.check(importedClasses);
----

The only difference to colloquial language, are the ".." in the package notation,
The only difference to colloquial language is the ".." in the package notation,
which refers to any number of packages. Thus "..service.." just expresses
"any package that contains some sub-package 'service'", e.g. `com.myapp.service.any`.
_"any package that contains some sub-package 'service'"_, e.g. `com.myapp.service.any`.
If this test fails, it will report an `AssertionError` with the following message:

[source,bash]
Expand Down Expand Up @@ -82,8 +82,8 @@ rule.check(importedClasses);
=== Composing Member Rules

In addition to a predefined API to write rules about Java classes and their relations, there is
an extended API to define rules for members of Java classes. This might be relevant for example,
if methods in a certain context need to be annotated with a specific annotation or return
an extended API to define rules for members of Java classes. This might be relevant, for example,
if methods in a certain context need to be annotated with a specific annotation, or return
types implementing a certain interface. The entry point is again `ArchRuleDefinition`, e.g.

[source,java,options="nowrap"]
Expand All @@ -96,6 +96,9 @@ ArchRule rule = ArchRuleDefinition.methods()
rule.check(importedClasses);
----

Besides `methods()`, `ArchRuleDefinition` offers the methods `members()`, `fields()`, `codeUnits()`, `constructors()`
– and the corresponding negations `noMembers()`, `noFields()`, `noMethods()`, etc.

=== Creating Custom Rules

In fact, most architectural rules take the form
Expand All @@ -105,10 +108,10 @@ In fact, most architectural rules take the form
classes that ${PREDICATE} should ${CONDITION}
----

In other words, we always want to limit imported classes to a relevant subset, and then
evaluate some condition to see that all those classes satisfy it.
ArchUnit's API allows you, to do just that, by exposing the concepts of `DescribedPredicate`
and `ArchCondition`. So the rule above, is just an application of this generic API:
In other words, we always want to limit imported classes to a relevant subset,
and then evaluate some condition to see that all those classes satisfy it.
ArchUnit's API allows you to do just that, by exposing the concepts of `DescribedPredicate` and `ArchCondition`.
So the rule above is just an application of this generic API:

[source,java,options="nowrap"]
----
Expand All @@ -119,8 +122,9 @@ noClasses().that(resideInAPackageService)
.should(accessClassesThatResideInAPackageController);
----

Thus, if the predefined API does not allow to express some concept, it is possible to extend
it in any custom way, for example:
Thus, if the predefined API does not allow to express some concept,
it is possible to extend it in any custom way.
For example:

[source,java,options="nowrap"]
----
Expand Down Expand Up @@ -160,10 +164,9 @@ classes that have a field annotated with @Payload should only be accessed by @Se

=== Predefined Predicates and Conditions

Often custom predicates and conditions like in the last section can be composed from
predefined elements. ArchUnit's basic convention for predicates is, that they are defined
in an inner class `Predicates` within the type they target. For example, one can find the
predicate to check for the simple name of a `JavaClass` as
Custom predicates and conditions like in the last section can often be composed from predefined elements.
ArchUnit's basic convention for predicates is that they are defined in an inner class `Predicates` within the type they target.
For example, one can find the predicate to check for the simple name of a `JavaClass` as

[source,java,options="nowrap"]
----
Expand All @@ -184,9 +187,10 @@ DescribedPredicate<JavaClass> serializableNamedFoo =
----

Note that for some properties, there exist interfaces with predicates defined for them.
For example the property to have a name is represented by the interface `HasName`, consequently
the predicate to check the name of a `JavaClass`, is the same as the predicate to check the name
of a `JavaMethod` and resides within
For example the property to have a name is represented by the interface `HasName`;
consequently the predicate to check the name of a `JavaClass`
is the same as the predicate to check the name of a `JavaMethod`,
and resides within

[source,java,options="nowrap"]
----
Expand All @@ -210,12 +214,12 @@ DescribedPredicate<JavaClass> name = HasName.Predicates.name("").forSubType();
name.and(JavaClass.Predicates.type(Serializable.class));
----

This behavior is somewhat tedious, but unfortunately it is a shortcoming of the Java
type system, that cannot be circumvented in a satisfying way.
This behavior is somewhat tedious, but unfortunately it is a shortcoming of the Java type system
that cannot be circumvented in a satisfying way.

Just like predicates, there exist predefined conditions, that can be combined in a similar
way. Since `ArchCondition` is a less generic concept, all predefined conditions can be found
within `ArchConditions`:
Just like predicates, there exist predefined conditions that can be combined in a similar way.
Since `ArchCondition` is a less generic concept, all predefined conditions can be found within `ArchConditions`.
Examples:

[source,java,options="nowrap"]
----
Expand All @@ -229,7 +233,7 @@ ArchCondition<JavaClass> callEqualsOrHashCode = callEquals.or(callHashCode);

=== Rules with Custom Concepts

Earlier we stated, that most architectural rules take the form
Earlier we stated that most architectural rules take the form

[source]
----
Expand All @@ -240,7 +244,7 @@ However, we do not always talk about classes, if we express architectural concep
have custom language, we might talk about modules, about slices, or on the other hand more
detailed about fields, methods or constructors. A generic API will never be able to support
every imaginable concept out of the box. Thus ArchUnit's rule API has at its foundation
a more generic API, that controls the types of objects that our concept targets.
a more generic API that controls the types of objects that our concept targets.

[plantuml, "import-vs-lang"]
----
Expand All @@ -262,7 +266,7 @@ CustomObjects -right->[passed to] "ArchRule
<i>and ArchCondition<CustomObject></i>"
----

To achieve this, any rule definition is based on a `ClassesTransformer` that defines, how
To achieve this, any rule definition is based on a `ClassesTransformer` that defines how
`JavaClasses` are to be transformed to the desired rule input. In many cases, like the ones
mentioned in the sections above, this is the identity transformation, passing classes on to the rule
as they are. However, one can supply any custom transformation to express a rule about a
Expand Down Expand Up @@ -307,18 +311,18 @@ all(businessModules).that(dealWithOrders).should(beIndependentOfPayment);

If the rule is straight forward, the rule text that is created automatically should be
sufficient in many cases. However, for rules that are not common knowledge, it is good practice
to document the reason for this rule. This can be done the following way:
to document the reason for this rule. This can be done in the following way:

[source,java,options="nowrap"]
----
classes().that(haveAFieldAnnotatedWithPayload).should(onlyBeAccessedBySecuredMethods)
.because("@Secured methods will be intercepted, checking for increased priviledges " +
.because("@Secured methods will be intercepted, checking for increased privileges " +
"and obfuscating sensitive auditing information");
----

Nevertheless sometimes the generated rule text might not convey the real intention
concisely enough (e.g. if multiple predicates or conditions are joined). In those cases
it is possible, to completely override the rule text:
Nevertheless, the generated rule text might sometimes not convey the real intention
concisely enough, e.g. if multiple predicates or conditions are joined.
It is possible to completely overwrite the rule description in those cases:

[source,java,options="nowrap"]
----
Expand All @@ -329,10 +333,10 @@ classes().that(haveAFieldAnnotatedWithPayload).should(onlyBeAccessedBySecuredMet
=== Ignoring Violations

In legacy projects there might be too many violations to fix at once. Nevertheless, that code
should be covered completely by architecture tests, to ensure that no further violations will
should be covered completely by architecture tests to ensure that no further violations will
be added to the existing code. One approach to ignore existing violations is
to tailor the `that(..)` clause of the rules in question, to ignore certain violations.
A more generic approach is, to ignore violations based on simple regex matches.
to tailor the `that(..)` clause of the rules in question to ignore certain violations.
A more generic approach is to ignore violations based on simple regex matches.
For this one can put a file named `archunit_ignore_patterns.txt` in the root of the classpath.
Every line will be interpreted as a regular expression and checked against reported violations.
Violations with a message matching the pattern will be ignored. If no violations are left,
Expand Down
Loading

0 comments on commit 85c5d06

Please sign in to comment.