AOP is a programming paradigm that enables the modularization of cross-cutting concerns in software applications.
Cross-cutting concerns are aspects of an application that affect multiple parts of the codebase — logging, security, transactions, error handling. AOP lets you separate these from the core logic, keeping code more maintainable and less cluttered.
Key terminology
- Aspect — a module that encapsulates a cross-cutting concern. Contains advice and pointcuts.
- Advice — the code that runs when a pointcut matches. Types include “before,” “after,” “around,” and “after throwing.”
- Pointcut — an expression defining where an aspect’s advice should apply — it selects specific join points.
- Join point — a specific point in program execution, such as a method call, constructor invocation, or field access.
Creating aspects
Define a class annotated @Aspect, containing advice methods:
import org.aspectj.lang.annotation.Aspect;import org.aspectj.lang.annotation.Before;
@Aspectpublic class LoggingAspect {
@Before("execution(* com.example.demo.*.*(..))") public void logBefore() { System.out.println("Before method execution"); }}LoggingAspect has a @Before advice that logs before any method execution in the
com.example.demo package.
Defining pointcuts
Pointcuts define where advice applies, via an expression matching join points. In
"execution(* com.example.demo.*.*(..))":
execution— this is a pointcut for method execution.*— matches any return type.*.*(..)— matches any method name with any number of arguments.
Pointcut expressions can be highly customized to target specific methods and classes.
Writing advice
@Before— runs before the join point. Good for logging, security checks, pre-processing.@After— runs after the join point. Good for cleanup or finalizing operations.@Around— wraps the join point entirely, letting you control its execution: modify input/output, act before and after, or even prevent it from running. The most powerful advice type.@AfterThrowing— runs if the join point throws an exception. Good for handling or logging exceptions.
Worked example: all advice types
A HelloService bean whose single method gets intercepted by every advice type in one aspect:
@Servicepublic class HelloService {
public String hello(String name) { String message = "Hello ".concat(name); System.out.println(message); return message; }}@Aspect@Componentpublic class HelloServiceAspect {
// the annotations on aspect methods are called join points; the "execution(...)" // expression in each annotation's brackets is the pointcut
@Before("execution(* dev.thesarfo.service.HelloService.hello(..))") public void before() { System.out.println("Before This method has been called"); }
// runs whether an exception is thrown or not @After("execution(* dev.thesarfo.service.HelloService.hello(..))") public void after() { System.out.println("After this method has been called"); }
// runs only when no exception is thrown @AfterReturning("execution(* dev.thesarfo.service.HelloService.hello(..))") public void afterReturning() { System.out.println("After this method has been called without an exception"); }
// runs only when the method throws @AfterThrowing("execution(* dev.thesarfo.service.HelloService.hello(..))") public void afterThrowing() { System.out.println("After this method has been called with an exception"); }
// lets you fully control before, during, and after execution // return type is Object because aspects are decoupled from the app, so you usually // don't know the return type of the method being intercepted; the ProceedingJoinPoint // is the instance of the method being intercepted @Around("execution(* dev.thesarfo.service.HelloService.hello(..))") public Object around(ProceedingJoinPoint joinPoint) { System.out.println("A"); Object result = null; try { result = joinPoint.proceed(); System.out.println("B"); } catch (Throwable throwable) { throwable.printStackTrace(); } return result; }}@Configuration@ComponentScan(basePackages = {"dev.thesarfo.service", "dev.thesarfo.aspects"})@EnableAspectJAutoProxypublic class ProjectConfig {}public class App { public static void main(String[] args) { try (AnnotationConfigApplicationContext c = new AnnotationConfigApplicationContext(ProjectConfig.class)) { HelloService service = c.getBean(HelloService.class); String message = service.hello("Ernest"); System.out.println("Result is " + message); } }}Calling service.hello("Ernest") here triggers, in order: @Around’s “A”, @Before, the actual
method body, @Around’s “B”, @AfterReturning, then @After.
Enabling AOP
Add @EnableAspectJAutoProxy to the main application class (or a @Configuration class, as
above) to enable AspectJ-based proxy creation, letting Spring weave aspects into the application’s
components:
@SpringBootApplication@EnableAspectJAutoProxypublic class DemoApplication { public static void main(String[] args) { SpringApplication.run(DemoApplication.class, args); }}Advanced AOP techniques
AspectJ with custom annotations — define your own annotation and match on it in a pointcut:
@Target(ElementType.METHOD)@Retention(RetentionPolicy.RUNTIME)public @interface Loggable {}
@Aspectpublic class CustomAspect { @Before("@annotation(Loggable)") public void logMethod(JoinPoint joinPoint) { // Log method execution }}Any method annotated @Loggable gets logged by logMethod.
More expressive pointcuts — match on arguments too, not just method signatures:
@Around("execution(* com.example.service.*.*(..)) && args(arg1, arg2)")public Object customAroundAdvice(ProceedingJoinPoint joinPoint, Object arg1, Object arg2) { // Custom around advice logic return joinPoint.proceed();}Aspect ordering — advice within an aspect defaults to order 0; use @Order to control
execution order when multiple aspects apply to the same join point (lower values run first).
Exception handling — @AfterThrowing can log or otherwise handle exceptions thrown by
matched methods:
@Aspectpublic class ExceptionHandlingAspect {
@AfterThrowing(pointcut = "execution(* com.example.service.*.*(..))", throwing = "ex") public void handleException(Throwable ex) { // Handle the exception }}AOP vs. OOP
Object-Oriented Programming
- Class-based — the class is the primary unit, encapsulating data and behavior; objects are instances of classes.
- Inheritance & polymorphism — build class hierarchies for code reuse and overriding; treat objects as instances of a parent type for flexible, interchangeable code.
- Encapsulation — hide an object’s internal state behind public methods, promoting modularity and reducing dependencies.
- Entities & relationships — design revolves around objects and how they interact.
Aspect-Oriented Programming
- Aspect-based — the aspect is the primary unit of modularity, encapsulating a cross-cutting concern (logging, security, transactions) via advice and pointcuts.
- Separation of concerns — cross-cutting concerns are pulled out of the main business logic entirely, instead of tangled into it.
- Join points & advice — join points are specific execution points (method calls, field access); advice is the code that runs at them (before/after/around).
- Pointcuts — expressions matching which join points an aspect applies to.
- Weaving — the process of integrating aspects into the codebase, at compile time, load time, or runtime depending on the framework (AspectJ, Spring AOP).
In short: OOP structures code around objects and their interactions; AOP structures it around join points and advice, cleanly separating concerns that would otherwise cut across many objects. The two are complementary rather than competing — most Spring apps lean on both.