User Konstantin
Konstantin
Level 36
Odesa

What is AOP? Principles of aspect-oriented programming

Published in the Random group
Hi, guys and gals! Without understanding the basic concepts, it is quite difficult to delve into frameworks and approaches to building functionality. So today we will talk about one such concept — AOP, aka aspect-oriented programming.What is AOP? Principles of aspect-oriented programming - 1This topic isn't easy and is rarely used directly, but many frameworks and technologies use it under the hood. And of course, sometimes during interviews, you may be asked to describe in general terms what sort of beast this is and where it can be applied. So let's take a look at the basic concepts and some simple examples of AOP in Java. Now then, AOP stands for aspect-oriented programming, which is a paradigm intended to increase the modularity of the different parts of an application by separating cross-cutting concerns. To accomplish this, additional behavior is added to the existing code without making changes to the original code. In other words, we can think of it as hanging additional functionality on top of methods and classes without altering to the modified code. Why is this necessary? Sooner or later, we conclude that the typical object-oriented approach cannot always effectively solve certain problems. And when that moment arrives, AOP comes to the rescue and gives us additional tools for building applications. And additional tools mean increased flexibility in software development, which means more options for solving a particular problem.

Applying AOP

Aspect-oriented programming is designed to perform cross-cutting tasks, which can be any code that may be repeated many times by different methods, which cannot be completely structured into a separate module. Accordingly, AOP lets us keep this outside of the main code and declare it vertically. An example is using a security policy in an application. Typically, security runs through many elements of an application. What's more, the application's security policy should be applied equally to all existing and new parts of the application. At the same time, a security policy in use can itself evolve. This is the perfect place to use AOP. Also, another example is logging. There are several advantages to using the AOP approach to logging rather than manually adding logging functional:
  1. The code for logging is easy to add and remove: all you need to do is add or remove a couple of configurations of some aspect.

  2. All the source code for logging is kept in one place, so you don't need to manually hunt down all the places where it is used.

  3. Logging code can be added anywhere, whether in methods and classes that have already been written or in new functionality. This reduces the number of coding errors.

    Also, when removing an aspect from a design configuration, you can be sure that all the tracing code is gone and that nothing was missed.

  4. Aspects are separate code that can be improved and used again and again.
What is AOP? Principles of aspect-oriented programming - 2AOP is also used for exception handling, caching, and extracting certain functionality to make it reusable.

Basic principles of AOP

To move further in this topic, let's first get to know the main concepts of AOP. Advice — Additional logic or code called from a join point. Advice can be performed before, after, or instead of a join point (more about them below). Possible types of advice:
  1. Before — this type of advice is launched before target methods, i.e. join points, are executed. When using aspects as classes, we use the @Before annotation to mark the advice as coming before. When using aspects as .aj files, this will be the before() method.

  2. After — advice that is executed after execution of methods (join points) is complete, both in normal execution as well as when throwing an exception.

    When using aspects as classes, we can use the @After annotation to indicate that this is advice that comes after.

    When using aspects as .aj files, this is the after() method.

  3. After Returning — this advice is performed only when the target method finishes normally, without errors.

    When aspects are represented as classes, we can use the @AfterReturning annotation to mark the advice as executing after successful completion.

    When using aspects as .aj files, this will be the after() returning (Object obj) method.

  4. After Throwing — this advice is intended for instances when a method, that is, join point, throws an exception. We can use this advice to handle certain kinds of failed execution (for example, to roll back an entire transaction or log with the required trace level).

    For class aspects, the @AfterThrowing annotation is used to indicate that this advice is used after throwing an exception.

    When using aspects as .aj files, this will be the after() throwing (Exception e) method.

  5. Around — perhaps one of the most important types of advice. It surrounds a method, that is, a join point that we can use to, for example, choose whether or not to perform a given join point method.

    You can write advice code that runs before and after the join point method is executed.

    The around advice is responsible for calling the join point method and the return values if the method returns something. In other words, in this advice, you can simply simulate the operation of a target method without calling it, and return whatever you want as a return result.

    Given aspects as classes, we use the @Around annotation to create advice that wraps a join point. When using aspects in the form of .aj files, this method will be the around() method.

Join Point — the point in a running program (i.e. method call, object creation, variable access) where the advice should be applied. In other words, this is a kind of regular expression used to find places for code injection (places where advice should be applied). Pointcut — a set of join points. A pointcut determines whether given advice is applicable to a given join point. Aspect — a module or class that implements cross-cutting functionality. Aspect changes the behavior of the remaining code by applying advice at join points defined by some pointcut. In other words, it is a combination of advice and join points. Introduction — changing the structure of a class and/or changing the inheritance hierarchy to add the aspect's functionality to foreign code. Target — the object to which the advice will be applied. Weaving — the process of linking aspects to other objects to create advised proxy objects. This can be done at compile time, load time, or run time. There are three types of weaving:
  • Compile-time weaving — if you have the aspect's source code and the code where you use the aspect, then you can compile the source code and the aspect directly using the AspectJ compiler;

  • Post-compile weaving (binary weaving) — if you cannot or do not want to use source code transformations to weave aspects into the code, you can take previously compiled classes or jar files and inject aspects into them;

  • Load-time weaving — this is just binary weaving that is delayed until the classloader loads the class file and defines the class for the JVM.

    One or more weaving class loaders are required to support this. They are either explicitly provided by the runtime or activated by a "weaving agent."

AspectJ — A specific implementation of the AOP paradigm that implements the ability to perform cross-cutting tasks. The documentation can be found here.

Examples in Java

Next, for a better understanding of AOP, we will look at small "Hello World"-style examples. Right of the bat, I'll note that our examples will use compile-time weaving. First, we need to add the following dependency in our pom.xml file:

<dependency>
  <groupId>org.aspectj</groupId>
  <artifactId>aspectjrt</artifactId>
  <version>1.9.5</version>
</dependency>
As a rule, the special ajc compiler is how we use aspects. IntelliJ IDEA doesn't include it by default, so when choosing it as the application compiler, you must specify the path to the 5168 75AspectJ distribution. This was the first way. The second, which is the one I used, is to register the following plugin in the pom.xml file:

<build>
  <plugins>
     <plugin>
        <groupId>org.codehaus.mojo</groupId>
        <artifactId>aspectj-maven-plugin</artifactId>
        <version>1.7</version>
        <configuration>
           <complianceLevel>1.8</complianceLevel>
           <source>1.8</source>
           <target>1.8</target>
           <showWeaveInfo>true</showWeaveInfo>
           <<verbose>true<verbose>
           <Xlint>ignore</Xlint>
           <encoding>UTF-8</encoding>
        </configuration>
        <executions>
           <execution>
              <goals>
                 <goal>compile</goal>
                 <goal>test-compile</goal>
              </goals>
           </execution>
        </executions>
     </plugin>
  </plugins>
</build>
After this, it's a good idea to reimport from Maven and run mvn clean compile. Now let's proceed directly to the examples.

Example No. 1

Let's create a Main class. In it, we will have an entry point and a method that prints a passed name on the console:

public class Main {
 
  public static void main(String[] args) {
  printName("Tanner");
  printName("Victor");
  printName("Sasha");
  }
 
  public static void printName(String name) {
     System.out.println(name);
  }
}
There's nothing complicated here. We passed a name and display it on the console. If we run the program now, we'll see the following on the console:
Tanner Victor Sasha
Now then, it's time to take advantage of the power of AOP. Now we need to create an aspect file. They are of two kinds: the first has the .aj file extension. The second is an ordinary class that uses annotations to implement AOP capabilities. Let's first look at the file with the .aj extension:

public aspect GreetingAspect {
 
  pointcut greeting() : execution(* Main.printName(..));
 
  before() : greeting() {
     System.out.print("Hi, ");
  }
}
This file is kinda like a class. Let's see what is happening here: pointcut is a set of join points; greeting() is the name of this pointcut; : execution indicates to apply it during the execution of all (*) calls of the Main.printName(...) method. Next comes a specific advice — before() — which is executed before the target method is called. : greeting() is the cutpoint that this advice responds to. Well, and below we see the body of the method itself, which is written in the Java language, which we understand. When we run main with this aspect present, we will get this console output:
Hi, Tanner Hi, Victor Hi, Sasha
We can see that every call to the printName method has been modified thanks to an aspect. Now let's take a look at what the aspect would look like as a Java class with annotations:

@Aspect
public class GreetingAspect{
 
  @Pointcut("execution(* Main.printName(String))")
  public void greeting() {
  }
 
  @Before("greeting()")
  public void beforeAdvice() {
     System.out.print("Hi, ");
  }
}
After the .aj aspect file, everything becomes more obvious here:
  • @Aspect indicates that this class is an aspect;
  • @Pointcut("execution(* Main.printName(String))") is the cutpoint that is triggered for all calls to Main.printName with an input argument whose type is String;
  • @Before("greeting()") is advice that is applied before calling the code specified in the greeting() cutpoint.
Running main with this aspect does not change the console output:
Hi, Tanner Hi, Victor Hi, Sasha

Example No. 2

Suppose we have some method that performs some operations for clients, and we call this method from main:

public class Main {
 
  public static void main(String[] args) {
  performSomeOperation("Tanner");
  }
 
  public static void performSomeOperation(String clientName) {
     System.out.println("Performing some operations for Client " + clientName);
  }
}
Let's use the @Around annotation to create a "pseudo-transaction":

@Aspect
public class TransactionAspect{
 
  @Pointcut("execution(* Main.performSomeOperation(String))")
  public void executeOperation() {
  }

  @Around(value = "executeOperation()")
  public void beforeAdvice(ProceedingJoinPoint joinPoint) {
     System.out.println("Opening a transaction...");
     try {
        joinPoint.proceed();
        System.out.println("Closing a transaction...");
     }
     catch (Throwable throwable) {
        System.out.println("The operation failed. Rolling back the transaction...");
     }
  }
  }
With the proceed method of the ProceedingJoinPoint object, we call the wrapping method to determine its location in the advice. Therefore, the code in the method above joinPoint.proceed(); is Before, while the code below it is After. If we run main, we get this in the console:
Opening a transaction... Performing some operations for Client Tanner Closing a transaction...
But if we throw and exception in our method (to simulate a failed operation):

public static void performSomeOperation(String clientName) throws Exception {
  System.out.println("Performing some operations for Client " + clientName);
  throw new Exception();
}
Then we get this console output:
Opening a transaction... Performing some operations for Client Tanner The operation failed. Rolling back the transaction...
So what we ended up with here is a kind of error handling capability.

Example No. 3

In our next example, let's do something like logging to the console. First, take a look at Main, where we've added some pseudo business logic:

public class Main {
  private String value;
 
  public static void main(String[] args) throws Exception {
     Main main = new Main();
     main.setValue("<some value>");
     String valueForCheck = main.getValue();
     main.checkValue(valueForCheck);
  }
 
  public void setValue(String value) {
     this.value = value;
  }
 
  public String getValue() {
     return this.value;
  }
 
  public void checkValue(String value) throws Exception {
     if (value.length() > 10) {
        throw new Exception();
     }
  }
}
In main, we use setValue to assign a value to the value instance variable. Then we use getValue to get the value, and then we call checkValue to see if it is longer than 10 characters. If so, then an exception will be thrown. Now let's look at the aspect we will use to log the work of the methods:

@Aspect
public class LogAspect {
 
  @Pointcut("execution(* *(..))")
  public void methodExecuting() {
  }
 
  @AfterReturning(value = "methodExecuting()", returning = "returningValue")
  public void recordSuccessfulExecution(JoinPoint joinPoint, Object returningValue) {
     if (returningValue != null) {
        System.out.printf("Successful execution: method — %s method, class — %s class, return value — %s\n",
              joinPoint.getSignature().getName(),
              joinPoint.getSourceLocation().getWithinType().getName(),
              returningValue);
     }
     else {
        System.out.printf("Successful execution: method — %s, class — %s\n",
              joinPoint.getSignature().getName(),
              joinPoint.getSourceLocation().getWithinType().getName());
     }
  }
 
  @AfterThrowing(value = "methodExecuting()", throwing = "exception")
  public void recordFailedExecution(JoinPoint joinPoint, Exception exception) {
     System.out.printf("Exception thrown: method — %s, class — %s, exception — %s\n",
           joinPoint.getSignature().getName(),
           joinPoint.getSourceLocation().getWithinType().getName(),
           exception);
  }
}
What's going on here? @Pointcut("execution(* *(..))") will join all calls of all methods. @AfterReturning(value = "methodExecuting()", returning = "returningValue") is advice that will be executed after successful execution of the target method. We have two cases here:
  1. When the method has a return value — if (returningValue! = Null) {
  2. When there is no return value — else {
@AfterThrowing(value = "methodExecuting()", throwing = "exception") is advice that will be triggered in case of an error, that is, when the method throws an exception. And accordingly, by running main, we will get a kind of console-based logging:
Successful execution: method — setValue, class — Main Successful execution: method — getValue, class — Main, return value — <some value> Exception thrown: method — checkValue, class — Main exception — java.lang.Exception Exception thrown: method — main, class — Main, exception — java.lang.Exception
And since we didn't handle the exceptions, we'll still get a stack trace:What is AOP? Principles of aspect-oriented programming - 3You can read about exceptions and exception handling in these articles: Exceptions in Java and Exceptions: catching and handling. That's all for me today. Today we got acquainted with AOP, and you were able to see that this beast isn't as scary as some people make it out to be. Goodbye, everyone!What is AOP? Principles of aspect-oriented programming - 4
Comments
TO VIEW ALL COMMENTS OR TO MAKE A COMMENT,
GO TO FULL VERSION