Danny Briskin, QA Consultants Senior Automation Engineer
The issue
Project Lombok has its advantages (see previous article) but the drawbacks are present there too. The main one: you are limited to already created Lombok functionality.
When you need more flexibility, you may look at Aspect Oriented Programming (AOP)
The main idea of AOP is in usage of so-called aspects. Aspects are parts of code that can be automatically inserted (it is called “weaving“) into another part of code during compilation or even during runtime. You can define various rules of weaving aspects depend on your business logic
Aspects themselves do not do any specific business logic (like methods), moreover, there is no sense in using aspects standalone. They can be only weaved into another part of code (like a patch).
Thus, we can move our boilerplate code to aspects and it will be weaved automatically in our application.
Application areas of AOP are:
- Logging
- Data validation
- Authentication
- Performance measuring
We will use a popular library AspectJ to create and use aspects in our Java code. Useful links
- See this article to setup AspectJ in your project.
- You can find full Maven project code here (non Maven project code is here)
Definitions
- Aspect
- A modularization of a concern that cuts across multiple objects. Each aspect focuses on a specific crosscutting functionality. A good example of aspect is logging. You need the same logging functionality everywhere in the app.
- Join point
- A point during the execution of a script, such as the execution of a method or property access. A place where advice code will be inserted. It can be
- Before method is executed
- After method was executed
- After method was executed and had returned a value
- Around method (i.e., before + after join point together). You can wrap method execution in your code or even replace the execution with yours
- After exception was thrown
- Advice
- Action taken by an aspect at a particular join point. It is a method to weave(execute) when joint point is met. For example: let’s do logging of method name and actual parameters values before a method is executed
- Pointcut
- A regular expression that matches join points. An advice is associated with a pointcut expression and runs at any join point that matches the pointcut. You can think about it as a condition where to apply (weave) a certain part of code. For example:
- “apply it to all methods in all classes of a package”
- “all methods annotated with @AnnnotaionOne”
- “class methods with 2 parameters when first one is String” “all methods in packageOne or packageTwo except toString() method or annotated with @AnnnotaionOne”
Creation of an aspect
We will create an aspect to weave logging functionality to methods of our application
Aspect class
Let’s create a class with @Aspect annotation
@Aspect public class LogAspect {
Assuming you have configured Log4j, add a logger to use:
public static final Logger LOGGER = LogManager.getLogger("com.qaconsultants.core");
private static final Level LOGGING_LEVEL = Level.DEBUG;
Pointcut
Define a pointcut (see pointcut definition syntax here)
@Pointcut("execution(* org.example.core..*(..))")
public void pointcutOne() {
// it is a pointcut
}
The code in @Pointcut annotation means: run weaved code when a method is executed. Find all methods with any visibility (first asterisk) of org.example.core package, in any of package class (first two dots ..), any method name (second asterisk), any number and type of method parameters (second two dots).
Important! Don’t place Aspect class in the same package(s) where you want aspects’ code to be weawed. It will lead to stack overflow exception (because methods will call itself to be weawed to itself and so on… )
**Expand to see other examples of pointcuts**
Another (more complicated) pointcut @Pointcut("execution(* org.example.core..*(..)) && ! execution(* *.toBuilder(..))")
public void pointcutExecutionFramework() {
// it is a pointcut
}
@Pointcut("execution(* *.toString(..)) || execution(* *.lambda$*(..)) || execution(* *.hashCode(..)) || @annotation(org.example.aspects.NoAopInsideMethod) ")
public void noNeedMethods() {
// it is a pointcut
}
@Pointcut("@annotation(org.example.aspects.NoAopLog)")
public void noLog() {
// it is a pointcut
}
@Pointcut("@annotation(org.example.aspects.ReplaceAop)")
public void replaceAop() {
// it is a pointcut
}
Advices/Joint points
Let’s define an advice with join point
Before join point
@Before("pointcutExecutionFramework() && ! noLog()")
public void beforeLog(JoinPoint joinPoint) {
The code of this method (beforeLog(), an advice) will be executed before execution of a method where pointcut condition is met. I.e., before any method defined in pointcutExecutionFramework pointcut and not for those defined in noLog() pointcut.
The parameter JoinPoint joinPoint encapsulates all information needed from method where we weave the code (like method name and parameters values).
Let’s retrieve those values
Signature signature = joinPoint.getSignature();
String methodName = signature.getDeclaringType().getSimpleName() + "." + signature.getName();
Object[] arguments = joinPoint.getArgs();
And do the logging
LOGGER.log(LOGGING_LEVEL, () -> "[>>] " + methodName + "(" + Arrays.toString(arguments) + ")");
This advice will log a method name with parameters right before the method execution
After join point
Work with @After join point is the same as with @Before, the only difference is in place of weaving – it will in the end of method rather than beginning.
AfterReturning join point
There is a little difference in using @AfterRuturning join point. Usually it is used for methods returning a value (i.e. not void return type).
@AfterReturning(value = "pointcutExecutionFramework() && ! noLog()", returning = "result")
public void afterReturningLog(JoinPoint joinPoint, Object result) {
As you can see, it will weave exactly to the same methods as previous one but to the end of method. More specifically, it will be run after base methods is executed and returned a value.
You can see an additional parameter of @AfterReturning annotation: returning. The value of this parameter means the name of your advice method parameter: result.
In that parameter base method’s returned value will be placed. Knowing the signature of base method (retrieved from joinPoint parameter) and a little bit of “dark magic” of Java – Reflection API you can cast the value of Object type to its real type if you need to, or just convert it into String.
public void afterReturningLog(JoinPoint joinPoint, Object result) {
Signature signature = joinPoint.getSignature();
Method method = ((MethodSignature) signature).getMethod();
if (!method.getGenericReturnType().toString().equals("void")) {
String methodReturnedResultAsString = result.toString();
LOGGER.log(LOGGING_LEVEL, "[o<] [{}] <== {}::{}()"
, methodReturnedResultAsString
, signature.getDeclaringType().getSimpleName()
, signature.getName());}
Around join point
A combination of @Before and @After join points is @Around join point.
@Around(value = "replaceAop()")
public Object aroundReplace(ProceedingJoinPoint proceedingJoinPoint) {
LOGGER.log(LOGGING_LEVEL, "Before " + proceedingJoinPoint.getSignature().getName());
Object result = proceedingJoinPoint.proceed(proceedingJoinPoint.getArgs());
LOGGER.log(LOGGING_LEVEL, "After " + proceedingJoinPoint.getSignature().getName());
return result;
}
You can see that method parameter type is ProceedingJoinPoint not just JoinPoint as always. The idea is the same, but ProceedingJoinPoint can execute a proceed() method that in fact is a method where this advice was weaved.
So, this method will do logging instead of actual method execution
@Around(value = "replaceAop()")
public Object aroundReplace(ProceedingJoinPoint proceedingJoinPoint) {
LOGGER.log(LOGGING_LEVEL, "We start working instead of " + proceedingJoinPoint.getSignature().getName());
return null;
}
AfterThrowing join point
And the last annotation I would like to mention, is @AfterThrowing. This one is used for manipulation of methods that throw exceptions. It looks like @SneakyThrows from Lombok. The value of throwing parameter of the annotaion is the name of Throwable argument of the advice method.
@AfterThrowing(value = "pointcutExecutionFramework()", throwing = "e")
public void afterThrowingFramework(JoinPoint joinPoint, Throwable e) {
Using already known JoinPoint object and throwable object you can create a concise logging inside the method.
Drawbacks
There are a few hidden discrepancies in using AspectJ library:
- Some versions are not compatible with popular frameworks (like Spring and Lombok. The one used here (1.9.5) is fully compatible
- It is not easy to setup proper amount of logging. It is either too much or too few. A deep knowledge of logging technique is needed. In addition, you can create several annotations to disable/enable logging process and make your logging in advice methods dependent on it.
- Some objects are hard to log because their toString() method does not exist or does logging in inappropriate way. You need to know Java Reflection API well to be able to convert complex objects into String correctly.
- The main drawback of AOP is that program flow is obscured and a very fragile sometimes. One should consider using plugins to popular IDE to visualize weaving and make sure that all changes made will not break another part of application
Summary
AOP is a powerful and flexible tool to code less. On the other hand, the tool is much more low-level than Lombok. Knowledge in additional spheres is required in addition to Java core. In combination with Java Reflection API and Spring framework (will be discussed in next articles) one can create a reliable, automatically reusable, hidden (from business logic part) code instead of hundred times repeatable boilerplate code.