আপনি যদি কিছুদিন ধরে Spring Boot অ্যাপ্লিকেশন তৈরি করে থাকেন, তাহলে নিশ্চয়ই লক্ষ্য করেছেন যে কিছু কাজ — যেমন logging, security check, transaction management, এবং performance monitoring — প্রায় প্রতিটি লেয়ারে বারবার লিখতে হয়। একই boilerplate কোড বারবার লেখার ফলে আপনার মূল business logic অগোছালো হয়ে যায়। এই সমস্যার সমাধানই হলো Aspect-Oriented Programming (AOP)

এই গাইডে আমরা Spring Boot-এ AOP কী, কীভাবে কাজ করে, প্রধান annotations এবং বাস্তব উদাহরণ নিয়ে বিস্তারিত আলোচনা করব।


AOP কী?

Aspect-Oriented Programming (AOP) হলো একটি programming paradigm যা আপনাকে cross-cutting concerns গুলোকে মূল business logic থেকে আলাদা করতে দেয়। Cross-cutting concern হলো এমন functionality যা অ্যাপ্লিকেশনের একাধিক লেয়ারে বিস্তৃত — যেমন logging, security, caching, error handling, এবং transaction management।

এই logic গুলো ডজন ডজন class-এ ছড়িয়ে দেওয়ার বদলে, AOP আপনাকে সেগুলো একটি নির্দিষ্ট module-এ সংজ্ঞায়িত করতে দেয় যাকে বলা হয় Aspect। এরপর এটি declaratively যেখানে দরকার সেখানে প্রয়োগ করা যায়।

Spring AOP প্রক্সি-ভিত্তিক এবং Spring IoC container-এর সাথে নির্বিঘ্নে কাজ করে।


AOP-এর মূল ধারণাসমূহ

কোডে যাওয়ার আগে গুরুত্বপূর্ণ পরিভাষাগুলো জেনে নেওয়া যাক:

ধারণা বিবরণ
Aspect Cross-cutting concern ধারণকারী module (যেমন: logging aspect)
Join Point Execution-এর যে বিন্দুতে aspect প্রয়োগ করা যায় (যেমন: method execution)
Advice Join point-এ aspect কর্তৃক গৃহীত পদক্ষেপ (যে কোড আসলে চলে)
Pointcut Join point-গুলো নির্বাচন করে — কোথায় advice প্রযোজ্য তা নির্ধারণ করে
Weaving Aspects-কে অন্য অবজেক্টের সাথে সংযুক্ত করার প্রক্রিয়া
Target Object এক বা একাধিক aspect দ্বারা advised অবজেক্ট
Proxy Spring AOP কর্তৃক তৈরি অবজেক্ট যা aspect চুক্তি বাস্তবায়ন করে

Spring Boot-এ AOP সেটআপ

Spring Boot-এ AOP সেটআপ অত্যন্ত সহজ। pom.xml-এ নিচের dependency যোগ করুন:

<dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-aop</artifactId>
</dependency>

ব্যস! Spring Boot AopAutoConfiguration-এর মাধ্যমে স্বয়ংক্রিয়ভাবে AOP কনফিগার করে। @EnableAspectJAutoProxy annotation আলাদাভাবে যোগ করার প্রয়োজন নেই (যদিও চাইলে যোগ করতে পারেন)।


Advice-এর প্রকারভেদ

Spring AOP পাঁচ ধরনের advice সাপোর্ট করে, প্রতিটি target method-এর সাথে সম্পর্কিত ভিন্ন সময়ে চলে:

১. @Before — Method-এর আগে চলে

@Aspect
@Component
public class LoggingAspect {

    private static final Logger log = LoggerFactory.getLogger(LoggingAspect.class);

    @Before("execution(* com.example.service.*.*(..))")
    public void logBeforeMethod(JoinPoint joinPoint) {
        log.info("Method call হচ্ছে: {}", joinPoint.getSignature().getName());
    }
}

Pre-condition check, method entry logging, বা security validation-এর জন্য ব্যবহার করুন।

২. @After — Method-এর পরে চলে (সবসময়)

@After("execution(* com.example.service.*.*(..))")
public void logAfterMethod(JoinPoint joinPoint) {
    log.info("Method execute হয়েছে: {}", joinPoint.getSignature().getName());
}

Method সফলভাবে শেষ হোক বা exception throw করুক — এটি সবসময় চলে। এটি finally block-এর মতো কাজ করে।

৩. @AfterReturning — সফল সম্পন্নের পরে চলে

@AfterReturning(
    pointcut = "execution(* com.example.service.UserService.findUser(..))",
    returning = "result"
)
public void logAfterReturning(JoinPoint joinPoint, Object result) {
    log.info("Method {} এর return value: {}",
        joinPoint.getSignature().getName(), result);
}

Method-এর return value inspect বা log করতে এটি ব্যবহার করুন।

৪. @AfterThrowing — Exception throw হলে চলে

@AfterThrowing(
    pointcut = "execution(* com.example.service.*.*(..))" ,
    throwing = "exception"
)
public void logAfterThrowing(JoinPoint joinPoint, Throwable exception) {
    log.error("Method {}-এ Exception: {}",
        joinPoint.getSignature().getName(), exception.getMessage());
}

Centralized exception logging এবং monitoring-এর জন্য চমৎকার।

৫. @Around — সবচেয়ে শক্তিশালী Advice

@Around পুরো method execution-কে wrap করে। আপনার সম্পূর্ণ নিয়ন্ত্রণ থাকে — arguments পরিবর্তন করতে পারেন, return value বদলাতে পারেন, এমনকি method টি সম্পূর্ণ skip করতে পারেন।

@Around("execution(* com.example.service.*.*(..))")
public Object measureExecutionTime(ProceedingJoinPoint joinPoint) throws Throwable {
    long startTime = System.currentTimeMillis();

    Object result = joinPoint.proceed(); // আসল method execute করুন

    long duration = System.currentTimeMillis() - startTime;
    log.info("Method {} চলেছে {} ms",
        joinPoint.getSignature().getName(), duration);

    return result;
}

গুরুত্বপূর্ণ: @Around-এ joinPoint.proceed() অবশ্যই call করতে হবে। না হলে আসল method execute হবে না — প্রায়ই কোনো error ছাড়াই, যা খুব বিভ্রান্তিকর bug তৈরি করে।


Pointcut Expression লেখা

Pointcut expression হলো AOP-এর প্রাণ — এটি নির্ধারণ করে কোন method গুলো intercept হবে। Spring AOP AspectJ pointcut expression language ব্যবহার করে।

execution Designator

সবচেয়ে বেশি ব্যবহৃত pointcut designator:

execution([modifier] return-type [class].method-name(params) [throws])

উদাহরণ:

// UserService-এর সকল method
"execution(* com.example.service.UserService.*(..))"

// service package-এর সকল class-এর সকল method
"execution(* com.example.service.*.*(..)) "

// শুধু String return করা public method
"execution(public String com.example..*.*(..)) "

// যেকোনো সংখ্যক argument সহ method
"execution(* com.example.service.*.*(..)) "

@annotation Designator

নির্দিষ্ট annotation বহনকারী method টার্গেট করুন — custom aspect-এর জন্য অত্যন্ত শক্তিশালী:

// @Loggable দিয়ে annotate করা সকল method intercept করুন
"@annotation(com.example.annotation.Loggable)"

within Designator

// service package-এর সকল class-এর সকল method
"within(com.example.service.*)"

&&, ||, ! দিয়ে Pointcut সংযুক্ত করা

@Pointcut("execution(* com.example.service.*.*(..))")
public void serviceMethods() {}

@Pointcut("execution(* com.example.repository.*.*(..))")
public void repositoryMethods() {}

@Pointcut("serviceMethods() || repositoryMethods()")
public void serviceOrRepository() {}

বাস্তব জীবনের Use Cases

Use Case ১: Custom Annotation দিয়ে Method-Level Logging

Annotation তৈরি করুন:

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface Loggable {
    String value() default "";
}

Aspect তৈরি করুন:

@Aspect
@Component
public class LoggingAspect {

    private static final Logger log = LoggerFactory.getLogger(LoggingAspect.class);

    @Around("@annotation(loggable)")
    public Object logExecution(ProceedingJoinPoint joinPoint,
                               Loggable loggable) throws Throwable {
        String methodName = joinPoint.getSignature().getName();
        log.info("[শুরু] {} - {}", methodName, loggable.value());
        try {
            Object result = joinPoint.proceed();
            log.info("[শেষ] {} সফলভাবে সম্পন্ন হয়েছে", methodName);
            return result;
        } catch (Exception e) {
            log.error("[ত্রুটি] {} ব্যর্থ হয়েছে: {}", methodName, e.getMessage());
            throw e;
        }
    }
}

যেকোনো জায়গায় ব্যবহার করুন:

@Service
public class OrderService {

    @Loggable("নতুন অর্ডার প্রক্রিয়া করা হচ্ছে")
    public Order createOrder(OrderRequest request) {
        // business logic এখানে
    }
}

Use Case ২: Performance Monitoring

@Aspect
@Component
public class PerformanceAspect {

    private static final Logger log = LoggerFactory.getLogger(PerformanceAspect.class);

    @Around("execution(* com.example.service.*.*(..))")
    public Object trackPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
        StopWatch stopWatch = new StopWatch();
        stopWatch.start();

        Object result = joinPoint.proceed();

        stopWatch.stop();
        if (stopWatch.getTotalTimeMillis() > 1000) {
            log.warn("ধীর Method সনাক্ত: {} সময় নিয়েছে {} ms",
                joinPoint.getSignature().toShortString(),
                stopWatch.getTotalTimeMillis());
        }
        return result;
    }
}

Use Case ৩: Role-Based Security Check

@Target(ElementType.METHOD)
@Retention(RetentionPolicy.RUNTIME)
public @interface RequiresRole {
    String value();
}
@Aspect
@Component
@RequiredArgsConstructor
public class SecurityAspect {

    private final SecurityContext securityContext;

    @Before("@annotation(requiresRole)")
    public void checkRole(JoinPoint joinPoint, RequiresRole requiresRole) {
        String requiredRole = requiresRole.value();
        if (!securityContext.getCurrentUser().hasRole(requiredRole)) {
            throw new AccessDeniedException(
                "অ্যাক্সেস অস্বীকৃত। প্রয়োজনীয় role: " + requiredRole
            );
        }
    }
}

Spring AOP বনাম AspectJ: পার্থক্য কী?

বৈশিষ্ট্য Spring AOP AspectJ
Weaving Runtime (proxy-based) Compile-time বা load-time
Scope শুধু Spring-managed bean যেকোনো Java class
Performance সামান্য ধীর (proxy overhead) দ্রুত
Setup সহজ, Spring-এ অন্তর্নির্মিত AspectJ compiler/agent প্রয়োজন
ব্যবহার বেশিরভাগ enterprise অ্যাপ Non-Spring code বা fine-grained

বেশিরভাগ Spring Boot অ্যাপ্লিকেশনের জন্য Spring AOP-ই সঠিক পছন্দ। শুধুমাত্র non-Spring-managed অবজেক্ট intercept করতে হলে বা compile-time weaving দরকার হলে full AspectJ ব্যবহার করুন।


সাধারণ ভুলগুলো এড়িয়ে চলুন

১. Self-invocation AOP trigger করে না Spring AOP প্রক্সির মাধ্যমে কাজ করে, তাই একই class-এর মধ্যে this দিয়ে method call করলে প্রক্সি bypass হয় এবং advice কাজ করে না। ApplicationContext থেকে প্রক্সি নিন অথবা আলাদা bean-এ refactor করুন।

২. Private method intercept হয় না Spring AOP শুধুমাত্র public method intercept করতে পারে। কখনো private method-এ @Around কাজ করবে বলে আশা করবেন না।

৩. @Around-এ joinPoint.proceed() ভুলবেন না @Around advice-এ proceed() call করতে ভুলে গেলে আসল method execute হবে না — প্রায়ই কোনো error ছাড়াই, যা অত্যন্ত বিভ্রান্তিকর bug তৈরি করে।

৪. অতিরিক্ত বিস্তৃত Pointcut Expression execution(* *.*(..)) এর মতো সব কিছুকে match করে এমন pointcut performance মারাত্মকভাবে নষ্ট করে। সবসময় যতটা সম্ভব নির্দিষ্ট থাকুন।


সারসংক্ষেপ

Spring Boot-এ AOP হলো সেই feature যা একবার ভালোভাবে বুঝলে আপনার অ্যাপ্লিকেশন architecture সম্পূর্ণ বদলে যায়। Service class-গুলোকে logging, security এবং monitoring কোড দিয়ে ভারাক্রান্ত করার বদলে সেই concerns গুলো clean, reusable aspect-এ externalize করুন।

সংক্ষিপ্ত পুনরাবৃত্তি:

  • Aspect declare করতে @Aspect + @Component ব্যবহার করুন
  • সঠিক advice type বেছে নিন: @Before, @After, @AfterReturning, @AfterThrowing, অথবা @Around
  • Pointcut expression আয়ত্ত করুন — বিশেষত execution() এবং @annotation()
  • Expressive, declarative API তৈরিতে custom annotation ব্যবহার করুন
  • Proxy-এর সীমাবদ্ধতা মনে রাখুন (self-invocation নয়, private method নয়)

AOP আপনার কোড পরিষ্কার রাখে, concerns আলাদা রাখে এবং architecture maintainable রাখে। ছোট থেকে শুরু করুন — আপনার পরবর্তী প্রজেক্টে একটি logging aspect যোগ করুন এবং দেখুন আপনার service layer কতটা পরিষ্কার হয়ে যায়।

Share