Fading Coder

One Final Commit for the Last Sprint

Home > Tech > Content

Understanding Spring AOP: From Annotation Loading to Proxy Activation

Tech May 12 2

Spring AOP (Aspect-Oriented Programming) is a powerful feature that allows developers to modularize cross-cutting concerns. In this article, we'll dive into how Spring AOP annotations are loaded and how they become active through proxy creation. We'll examine the underlying mechanisms that make AOP work, starting with a practical example and then exploring the source code.

Let's begin with a simple caching aspect implementation that demonstrates common AOP usage:

@Aspect
@Component
public class CacheAspect {

    @Autowired
    private RedisTemplate<String, Object> redisTemplate;

    private final Map<String, CacheResultParser> parserRegistry = new ConcurrentHashMap<>();
    private final Map<String, KeyGenerator> generatorRegistry = new ConcurrentHashMap<>();
    private final Map<String, AtomicBoolean> processingFlags = new ConcurrentHashMap<>();

    @Pointcut("@annotation(com.example.annotation.Cachable)")
    public void cacheableMethods() {}

    @Around("cacheableMethods() && @annotation(cachable)")
    public Object handleCache(ProceedingJoinPoint joinPoint, Cachable cachable) throws Throwable {
        String cacheKey = "";
        Object cachedValue = null;
        
        try {
            cacheKey = generateCacheKey(cachable, joinPoint);
            cachedValue = redisTemplate.opsForValue().get(cacheKey);
        } catch (Exception e) {
            Logger.error("Failed to retrieve cache for key {}: {}", cacheKey, e.getMessage());
        } finally {
            if (cachedValue == null) {
                cachedValue = retrieveAndCache(joinPoint, cachable, cacheKey);
            }
            return parseResult(cachable, cachedValue, joinPoint);
        }
    }

    private Object parseResult(Cachable cachable, Object cachedValue, ProceedingJoinPoint joinPoint) 
            throws InstantiationException, IllegalAccessException {
        if (cachedValue == null) {
            return null;
        }
        
        String parserName = cachable.parser().getName();
        CacheResultParser parser = parserRegistry.computeIfAbsent(parserName, 
            name -> cachable.parser().newInstance());
        
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Class<?> returnType = signature.getReturnType();
        return parser.parse(cachedValue, returnType);
    }

    private String retrieveAndCache(String cacheKey, ProceedingJoinPoint joinPoint, 
            Cachable cachable) {
        String resultValue = "";
        
        // Random delay to prevent cache stampede
        try {
            Thread.sleep((long)(Math.random() * 100 + 100));
        } catch (InterruptedException e) {
            Logger.error("Cache synchronization delay interrupted", e);
        }
        
        while (StringUtils.isEmpty(resultValue = 
                (String) redisTemplate.opsForValue().get(cacheKey)) && 
                !processingFlags.getOrDefault(cacheKey, new AtomicBoolean(false)).get()) {
            
            if (!processingFlags.computeIfAbsent(cacheKey, k -> new AtomicBoolean()).get()) {
                processingFlags.get(cacheKey).set(true);
                
                Object methodResult = null;
                try {
                    methodResult = joinPoint.proceed();
                } catch (Throwable e) {
                    Logger.error("Method execution failed", e);
                }
                
                storeInCache(cacheKey, methodResult, cachable);
                processingFlags.get(cacheKey).set(false);
            }
        }
        
        processingFlags.remove(cacheKey);
        return resultValue;
    }

    private void storeInCache(String cacheKey, Object value, Cachable cachable) {
        if (value == null) return;
        
        if (value instanceof String) {
            redisTemplate.opsForValue().set(cacheKey, value);
        } else {
            redisTemplate.opsForValue().set(cacheKey, 
                JSON.toJSONString(value));
        }
        
        if (cachable.expireTime() != -1) {
            redisTemplate.expire(cacheKey, cachable.expireTime(), TimeUnit.MINUTES);
        }
    }

    private String generateCacheKey(Cachable cachable, ProceedingJoinPoint joinPoint) 
            throws InstantiationException, IllegalAccessException {
        MethodSignature signature = (MethodSignature) joinPoint.getSignature();
        Method method = signature.getMethod();
        Object[] args = joinPoint.getArgs();
        
        String generatorName = cachable.generator().getName();
        KeyGenerator generator = generatorRegistry.computeIfAbsent(generatorName, 
            name -> cachable.generator().newInstance());
        
        return generator.generate(cachable.key(), method, args);
    }
}

This caching aspect demonstrates how we can intercept method calls, check for cached results, and cache new results. Now, let's explore how Spring discovers and processes these aspects.

The journey begins during bean creation. When Spring initializes beans, it looks for aspects that need to be proxied:

protected Object resolveBeforeInstantiation(String beanName, RootBeanDefinition mbd) {
    Object bean = null;
    if (!Boolean.FALSE.equals(mbd.beforeInstantiationResolved)) {
        if (!mbd.isSynthetic() && hasInstantiationAwareBeanPostProcessors()) {
            Class<?> targetType = determineTargetType(beanName, mbd);
            if (targetType != null) {
                bean = applyBeanPostProcessorsBeforeInstantiation(targetType, beanName);
                if (bean != null) {
                    bean = applyBeanPostProcessorsAfterInitialization(bean, beanName);
                }
            }
        }
        mbd.beforeInstantiationResolved = (bean != null);
    }
    return bean;
}

The key to AOP activation lies in the hasInstantiationAwareBeanPostProcessors() method. When you include Spring AOP in your project, the AopAutoConfiguration automatically registers AnnotationAwareAspectJAutoProxyCreator, which enables aspect processing.

Let's examine how annotations are discovered:

public Object postProcessBeforeInstantiation(Class<?> beanClass, String beanName) throws BeansException {
    Object cacheKey = getCacheKey(beanClass, beanName);

    if (!StringUtils.hasLength(beanName) || !this.targetSourcedBeans.contains(beanName)) {
        if (this.advisedBeans.containsKey(cacheKey)) {
            return null;
        }
        
        if (isInfrastructureClass(beanClass) || shouldSkip(beanClass, beanName)) {
            this.advisedBeans.put(cacheKey, Boolean.FALSE);
            return null;
        }
    }

    TargetSource targetSource = getCustomTargetSource(beanClass, beanName);
    if (targetSource != null) {
        if (StringUtils.hasLength(beanName)) {
            this.targetSourcedBeans.add(beanName);
        }
        Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(beanClass, beanName, targetSource);
        Object proxy = createProxy(beanClass, beanName, specificInterceptors, targetSource);
        this.proxyTypes.put(cacheKey, proxy.getClass());
        return proxy;
    }

    return null;
}

The criticla part is the shouldSkip() method, which determines whether a bean should be processed for AOP:

protected boolean shouldSkip(Class<?> beanClass, String beanName) {
    List<Advisor> candidateAdvisors = findCandidateAdvisors();
    for (Advisor advisor : candidateAdvisors) {
        if (advisor instanceof AspectJPointcutAdvisor &&
                ((AspectJPointcutAdvisor) advisor).getAspectName().equals(beanName)) {
            return true;
        }
    }
    return super.shouldSkip(beanClass, beanName);
}

Spring finds candidate advisors through two main steps:

protected List<Advisor> findCandidateAdvisors() {
    List<Advisor> advisors = super.findCandidateAdvisors();
    if (this.aspectJAdvisorsBuilder != null) {
        advisors.addAll(this.aspectJAdvisorsBuilder.buildAspectJAdvisors());
    }
    return advisors;
}

The second step is where Spring discovers classes annotated with @Aspect:

public List<Advisor> buildAspectJAdvisors() {
    List<String> aspectNames = this.aspectBeanNames;

    if (aspectNames == null) {
        synchronized (this) {
            aspectNames = this.aspectBeanNames;
            if (aspectNames == null) {
                List<Advisor> advisors = new ArrayList<>();
                aspectNames = new ArrayList<>();
                
                String[] beanNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
                    this.beanFactory, Object.class, true, false);
                
                for (String beanName : beanNames) {
                    if (!isEligibleBean(beanName)) continue;
                    
                    Class<?> beanType = this.beanFactory.getType(beanName);
                    if (beanType == null) continue;
                    
                    if (this.advisorFactory.isAspect(beanType)) {
                        aspectNames.add(beanName);
                        AspectMetadata amd = new AspectMetadata(beanType, beanName);
                        
                        if (amd.getAjType().getPerClause().getKind() == PerClauseKind.SINGLETON) {
                            MetadataAwareAspectInstanceFactory factory =
                                new BeanFactoryAspectInstanceFactory(this.beanFactory, beanName);
                            
                            List<Advisor> classAdvisors = this.advisorFactory.getAdvisors(factory);
                            if (this.beanFactory.isSingleton(beanName)) {
                                this.advisorsCache.put(beanName, classAdvisors);
                            } else {
                                this.aspectFactoryCache.put(beanName, factory);
                            }
                            advisors.addAll(classAdvisors);
                        }
                    }
                }
                this.aspectBeanNames = aspectNames;
            }
        }
    }
    
    return advisors;
}

Once an aspect class is identified, Spring processes its methods to find advice:

public List<Advisor> getAdvisors(MetadataAwareAspectInstanceFactory aspectInstanceFactory) {
    Class<?> aspectClass = aspectInstanceFactory.getAspectMetadata().getAspectClass();
    String aspectName = aspectInstanceFactory.getAspectMetadata().getAspectName();
    validate(aspectClass);

    MetadataAwareAspectInstanceFactory lazySingletonAspectInstanceFactory =
        new LazySingletonAspectInstanceFactoryDecorator(aspectInstanceFactory);

    List<Advisor> advisors = new ArrayList<>();
    for (Method method : getAdvisorMethods(aspectClass)) {
        Advisor advisor = getAdvisor(method, lazySingletonAspectInstanceFactory, 
            advisors.size(), aspectName);
        if (advisor != null) {
            advisors.add(advisor);
        }
    }
    return advisors;
}

After identifying aspects and their advice, Spring creates proxies during the postProcessAfterInitialization phase:

public Object applyBeanPostProcessorsAfterInitialization(Object existingBean, String beanName)
        throws BeansException {
    
    Object result = existingBean;
    for (BeanPostProcessor processor : getBeanPostProcessors()) {
        Object current = processor.postProcessAfterInitialization(result, beanName);
        if (current == null) {
            return result;
        }
        result = current;
    }
    return result;
}

The proxy creation logic determines whether to use JDK dynamic proxies or CGLIB:

public AopProxy createAopProxy(AdvisedSupport config) throws AopConfigException {
    if (config.isOptimize() || config.isProxyTargetClass() || hasNoUserSuppliedProxyInterfaces(config)) {
        Class<?> targetClass = config.getTargetClass();
        if (targetClass == null) {
            throw new AopConfigException("TargetSource cannot determine target class: " +
                "Either an interface or a target is required for proxy creation.");
        }
        if (targetClass.isInterface() || Proxy.isProxyClass(targetClass)) {
            return new JdkDynamicAopProxy(config);
        }
        return new ObjenesisCglibAopProxy(config);
    } else {
        return new JdkDynamicAopProxy(config);
    }
}

The default behavior is controlled by the @EnableAspectJAutoProxy annotation and the proxyTargetClass property. When not explicitly configured, Spring uses CGLIB proxies:

@Configuration
@EnableAspectJAutoProxy(proxyTargetClass = true)
@ConditionalOnProperty(prefix = "spring.aop", name = "proxy-target-class", 
    havingValue = "true", matchIfMissing = true)
public static class CglibAutoProxyConfiguration {
}

When a method is invoked on the proxy, the interceptor chain is executed:

public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) 
        throws Throwable {
    
    Object oldProxy = null;
    boolean setProxyContext = false;
    Object target = null;
    TargetSource targetSource = this.advised.getTargetSource();
    
    try {
        List<Object> chain = this.advised.getInterceptorsAndDynamicInterceptionAdvice(method, targetClass);
        Object retVal;
        
        if (chain.isEmpty() && Modifier.isPublic(method.getModifiers())) {
            Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
            retVal = methodProxy.invoke(target, argsToUse);
        } else {
            retVal = new CglibMethodInvocation(proxy, target, method, args, 
                targetClass, chain, methodProxy).proceed();
        }
        return retVal;
    }
}

The interceptor chain processes each advice in sequence:

public Object proceed() throws Throwable {
    if (this.currentInterceptorIndex == this.interceptorsAndDynamicMethodMatchers.size() - 1) {
        return invokeJoinpoint();
    }

    Object interceptorOrInterceptionAdvice =
        this.interceptorsAndDynamicMethodMatchers.get(++this.currentInterceptorIndex);
    
    if (interceptorOrInterceptionAdvice instanceof InterceptorAndDynamicMethodMatcher) {
        InterceptorAndDynamicMethodMatcher dm =
            (InterceptorAndDynamicMethodMatcher) interceptorOrInterceptionAdvice;
        
        if (dm.methodMatcher.matches(this.method, this.targetClass, this.arguments)) {
            return dm.interceptor.invoke(this);
        } else {
            return proceed();
        }
    } else {
        return ((MethodInterceptor) interceptorOrInterceptionAdvice).invoke(this);
    }
}

For @Around advice, the pointcut expression is parsed and evaluated to determine if the advice should be applied:

// Pointcut parsing happens during advisor creation
PointcutParser.parsePointcutExpression(pointcutExpression, 
    new ClassPathClassResolver(), new AnnotationTargetReflector());

This completes our exploration of how Spring AOP loads and activates aspects. The framework automatical discovers annotated aspects, creates apppropriate proxies, and executes advice at the appropriate join points.

Related Articles

Understanding Strong and Weak References in Java

Strong References Strong reference are the most prevalent type of object referencing in Java. When an object has a strong reference pointing to it, the garbage collector will not reclaim its memory. F...

Comprehensive Guide to SSTI Explained with Payload Bypass Techniques

Introduction Server-Side Template Injection (SSTI) is a vulnerability in web applications where user input is improper handled within the template engine and executed on the server. This exploit can r...

Implement Image Upload Functionality for Django Integrated TinyMCE Editor

Django’s Admin panel is highly user-friendly, and pairing it with TinyMCE, an effective rich text editor, simplifies content management significantly. Combining the two is particular useful for bloggi...

Leave a Comment

Anonymous

◎Feel free to join the discussion and share your thoughts.