05 Spring Aop Common Errors Part 1

05 Spring AOP Common Errors Part 1 #

Hello, I’m Fu Jian. In this lesson, let’s talk about some common issues encountered when using Spring AOP.

Spring AOP is one of the core features in Spring, apart from Dependency Injection (DI). As the name suggests, AOP stands for Aspect Oriented Programming, which can be translated as “面向切面编程” in Chinese.

Spring AOP utilizes techniques such as CGlib and JDK dynamic proxies to achieve runtime method enhancement. Its purpose is to separate and extract code that is irrelevant to business logic, thereby reducing the coupling between code and improving the reusability and development efficiency of the program. Therefore, AOP has become a widely-used technology in various aspects such as logging, monitoring, performance tracking, exception handling, authorization management, and unified authentication.

If we trace back to the origin, the reason why we can seamlessly add arbitrary code snippets before or after methods in container objects is because Spring dynamically “weaves” the code logic from aspects into the methods of container objects at runtime. Therefore, it can be said that AOP is essentially a proxy pattern. However, when using this proxy pattern, it is easy to make mistakes. In this lesson, we will analyze the common problems and the underlying principles behind them.

Case 1: The current class method called by “this” cannot be intercepted #

Let’s assume we are developing a dormitory management system, and this module contains a class called ElectricService responsible for recharging electricity. It has a charging method charge():

@Service
public class ElectricService {

    public void charge() throws Exception {
        System.out.println("Electric charging ...");
        this.pay();
    }

    public void pay() throws Exception {
        System.out.println("Pay with alipay ...");
        Thread.sleep(1000);
    }

}

In the charge() method for recharging electricity, we use Alipay for payment. Therefore, I added the pay() method in this method. To simulate the time-consuming pay() method, the code sleeps for 1 second, and the pay() method is called using this.pay().

However, since Alipay payment is a third-party interface, we need to record the interface call time. This is where we introduced an @Around advice to record the time before and after the execution of the pay() method, and calculate the time consumption of executing the pay() method.

@Aspect
@Service
@Slf4j
public class AopConfig {
    @Around("execution(* com.spring.puzzle.class5.example1.ElectricService.pay()) ")
    public void recordPayPerformance(ProceedingJoinPoint joinPoint) throws Throwable {
        long start = System.currentTimeMillis();
        joinPoint.proceed();
        long end = System.currentTimeMillis();
        System.out.println("Pay method time cost(ms): " + (end - start));
    }
}

Finally, we define a Controller to provide an interface for recharging electricity:

@RestController
public class HelloWorldController {
    @Autowired
    ElectricService electricService;
    @RequestMapping(path = "charge", method = RequestMethod.GET)
    public void charge() throws Exception{
        electricService.charge();
    };
}

After completing the code, when we access the above interface, we find that this aspect of calculating time is not executed, and the output log is as follows:

Electric charging ...- Pay with alipay ...

By tracing back the code, we can see that in the @Around aspect class, we clearly defined the method corresponding to the aspect, but it was not executed. This indicates that the methods called by the this reference internally in the class are not enhanced by Spring AOP. Why is this? Let’s analyze it.

Case Analysis #

We can find the truth from the source code. Let’s set a breakpoint and debug to see what the object corresponding to this is:

As we can see, this corresponds to a normal ElectricService object, nothing special. Let’s also see what the ElectricService object autowired in the Controller layer looks like:

As we can see, this is an enhanced Bean by Spring AOP, so when executing the charge() method, the enhancement operation of recording the interface call time will be executed. However, the object corresponding to this is just a normal object without any additional enhancements.

Why is the object corresponding to this just a normal object? To understand this, let’s look at the process of enhancing objects by Spring AOP. But before that, there are some fundamentals that I need to emphasize here.

1. Implementation of Spring AOP

The underlying implementation of Spring AOP is dynamic proxy. There are two ways to create proxies: JDK proxies and CGLIB proxies. JDK dynamic proxies can only generate proxies for classes that implement interfaces, while CGLIB can generate proxies for regular classes. It mainly creates a subclass for the specified class, overrides its methods, and implements the proxy object. The specific differences can be seen in the following diagram:

2. How to use Spring AOP

In Spring Boot, we generally only need to add the following dependencies to directly use AOP functionality:

- org.springframework.boot: spring-boot-starter-aop

For non-Spring Boot programs, in addition to adding related AOP dependencies, we often use @EnableAspectJAutoProxy to enable AOP functionality. This annotation class imports (Import) AspectJAutoProxyRegistrar, which implements the ImportBeanDefinitionRegistrar interface method to prepare AOP-related beans.

After supplementing the basic knowledge of Spring’s underlying and usage, let’s specifically look at the process of creating proxy objects. Let’s first look at the call stack:

The timing of creating proxy objects is when creating a Bean, and the key work of creating it is actually done by AnnotationAwareAspectJAutoProxyCreator. It is essentially a BeanPostProcessor. So its execution is in the process of initializing the Bean after completing the construction of the original Bean. So what exactly does it do? Let’s take a look at its postProcessAfterInitialization method:

public Object postProcessAfterInitialization(@Nullable Object bean, String beanName) {
   if (bean != null) {
      Object cacheKey = getCacheKey(bean.getClass(), beanName);
      if (this.earlyProxyReferences.remove(cacheKey) != bean) {
         return wrapIfNecessary(bean, beanName, cacheKey);
      }
   }
   return bean;
}

The key method in the above code is wrapIfNecessary. As the name suggests, when AOP is needed, it wraps the created original Bean object as a proxy object and returns it as a Bean. Specific to this wrap process, refer to the following key line of code:

protected Object wrapIfNecessary(Object bean, String beanName, Object cacheKey) {
   // Omit non-key code
   Object[] specificInterceptors = getAdvicesAndAdvisorsForBean(bean.getClass(), beanName, null);
   if (specificInterceptors != DO_NOT_PROXY) {
      this.advisedBeans.put(cacheKey, Boolean.TRUE);
      Object proxy = createProxy(
            bean.getClass(), beanName, specificInterceptors, new SingletonTargetSource(bean));
      this.proxyTypes.put(cacheKey, proxy.getClass());
      return proxy;
   }
   // Omit non-key code
}

In the above code, the createProxy method call in line 6 is the key to creating the proxy object. In a nutshell, it first creates a proxy factory, then adds advisors and the target object to the proxy factory, and finally retrieves the proxy object using the proxy factory. The key steps are as follows:

protected Object createProxy(Class<?> beanClass, @Nullable String beanName,
      @Nullable Object[] specificInterceptors, TargetSource targetSource) {
  // Omit non-key code
  ProxyFactory proxyFactory = new ProxyFactory();
  if (!proxyFactory.isProxyTargetClass()) {
   if (shouldProxyTargetClass(beanClass, beanName)) {
      proxyFactory.setProxyTargetClass(true);
   }
   else {
      evaluateProxyInterfaces(beanClass, proxyFactory);
   }
  }
  Advisor[] advisors = buildAdvisors(beanName, specificInterceptors);
  proxyFactory.addAdvisors(advisors);
  proxyFactory.setTargetSource(targetSource);
  customizeProxyFactory(proxyFactory);
  // Omit non-key code
  return proxyFactory.getProxy(getProxyClassLoader());
}

After going through this process, a proxy object is created. When we retrieve objects from Spring, it is actually this proxy object that we get, which has the AOP functionality. The object referred to by this before is just a regular object and does not have AOP functionality.

Issue Fix #

From the above analysis, we know that only the object referred to by the dynamically created proxy will be enhanced by Spring and have the necessary AOP functionality. So what kind of object meets this condition?

There are two ways. One way is to use the @Autowired annotation, so we can modify the code as follows, internally referring to itself within the class using @Autowired:

@Service
public class ElectricService {
    @Autowired
    ElectricService electricService;
    public void charge() throws Exception {
        System.out.println("Electric charging ...");
        electricService.pay();
    }
    public void pay() throws Exception {
        System.out.println("Pay with alipay ...");
        Thread.sleep(1000);
    }
}

The other way is to directly access the current proxy from AopContext. You might ask, what is AopContext? In simple terms, its core is to bind the proxy and thread together using a ThreadLocal, so that the current proxy can be retrieved at any time.

However, there is a small prerequisite for using this method, which is to add a exposeProxy = true configuration item in @EnableAspectJAutoProxy, indicating that the proxy object should be put into the ThreadLocal so that it can be directly retrieved using AopContext.currentProxy(). Otherwise, an error will occur as shown below:

org.springframework.aop.framework.AopConfigException: Could not generate CGLIB subclass of class ...

Following this train of thought, we can modify the relevant code as follows:

import org.springframework.aop.framework.AopContext;
import org.springframework.stereotype.Service;
@Service
public class ElectricService {
    public void charge() throws Exception {
        System.out.println("Electric charging ...");
        ElectricService electric = ((ElectricService) AopContext.currentProxy());
        electric.pay();
    }
    public void pay() throws Exception {
        System.out.println("Pay with alipay ...");
        Thread.sleep(1000);
    }
}

At the same time, don’t forget to modify the exposeProxy attribute of the EnableAspectJAutoProxy annotation, as shown in the example below:

@SpringBootApplication
@EnableAspectJAutoProxy(exposeProxy = true)
public class Application {
    // Omit non-key code
}

These two methods are essentially the same in effect, and in the end, we have printed the expected log, and thus the issue has been successfully resolved.

Electric charging ...
Pay with alipay ...
Pay method time cost(ms): 1005

Case 2: NullPointerException when accessing properties of intercepted class directly #

Continuing from the previous case, in the dormitory management system, we use the charge() method for payment. When settling the payment, we need to use an administrator user’s payment number. For this purpose, we introduce a few new classes.

User class, which contains information about the user’s payment number:

public class User {
    private String payNum;
    
    public User(String payNum) {
        this.payNum = payNum;
    }
    
    public String getPayNum() {
        return payNum;
    }
    
    public void setPayNum(String payNum) {
        this.payNum = payNum;
    }
}

AdminUserService class, which contains an administrator user (of the User class) and its payment number is 202101166. Additionally, this service class has a login() method used to login to the system:

@Service
public class AdminUserService {
    public final User adminUser = new User("202101166");
    
    public void login() {
        System.out.println("admin user login...");
    }
}

We need to modify the ElectricService class to fulfill this requirement: when recharging the electricity bill, the administrator needs to login and use their payment number for settlement. The complete code is as follows:

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;

@Service
public class ElectricService {
    @Autowired
    private AdminUserService adminUserService;

    public void charge() throws Exception {
        System.out.println("Electric charging ...");
        this.pay();
    }

    public void pay() throws Exception {
        adminUserService.login();
        String payNum = adminUserService.adminUser.getPayNum();
        System.out.println("User pay num: " + payNum);
        System.out.println("Pay with alipay ...");
        Thread.sleep(1000);
    }
}

When executing the charge() operation, everything works fine:

Electric charging ...
admin user login...
User pay num: 202101166
Pay with alipay ...

Now, for security reasons, we need to log the administrator’s login in order to audit their operations later. So we add a AOP-related configuration class, as follows:

@Aspect
@Service
@Slf4j
public class AopConfig {
    @Before("execution(* com.spring.puzzle.class5.example2.AdminUserService.login(..)) ")
    public void logAdminLogin(JoinPoint pjp) throws Throwable {
        System.out.println("! admin login ...");
    }
}

After adding this code, when we execute the charge() operation, we find that there is no related log message, and a NullPointerException is thrown when executing the following line of code:

String payNum = dminUserService.user.getPayNum();

The code, which was originally working fine, now throws a NullPointerException due to the introduction of the AOP aspect. What could be the reason for this? Let’s debug and see what the called object looks like after introducing AOP.

As we can see, after introducing AOP, our object has become a proxy object. If you have a keen eye, you will notice that in the above image, the adminUser property is indeed null. Why is it so? To answer this mysterious question, we need to further understand how Spring uses CGLIB to generate proxies.

Case Analysis #

In the previous case, we analyzed the general process of creating a Spring Proxy. Here, we need to further study what kind of object is created through the Proxy. In normal cases, AdminUserService is just a regular object, while the enhanced version is an `AdminUserService $\(EnhancerBySpringCGLIB\)$xxxx.

This class is actually a subclass of AdminUserService. It will overwrite all public and protected methods, and internally delegate the calls to the original AdminUserService instance.

From the implementation perspective, the implementation of AOP in CGLIB is based on the Enhancer and MethodInterceptor interfaces in the org.springframework.cglib.proxy package.

The entire process can be summarized into three steps:

  • Define a custom MethodInterceptor responsible for delegating method execution.
  • Create an Enhancer and set the callback to the above MethodInterceptor.
  • Use enhancer.create() to create the proxy.

Next, let’s analyze the relevant implementation source code of Spring.

In the previous case analysis, we briefly mentioned the initialization mechanism of Spring’s dynamic proxy objects. After obtaining the Advisors, the proxy object is obtained through ProxyFactory.getProxy:

public Object getProxy(ClassLoader classLoader) {
    return createAopProxy().getProxy(classLoader);
}

Here, we take the CGLIB Proxy implementation class CglibAopProxy as an example to see the specific process:

public Object getProxy(@Nullable ClassLoader classLoader) {
    // Omit non-essential code
    // Create and configure Enhancer
    Enhancer enhancer = createEnhancer();
    // Omit non-essential code
    // Get Callback: DynamicAdvisedInterceptor, also a MethodInterceptor
    Callback[] callbacks = getCallbacks(rootClass);
    // Omit non-essential code
    // Generate proxy class and create the proxy (set the callbacks value of enhancer)
    return createProxyClassAndInstance(enhancer, callbacks);
    // Omit non-essential code
}

The key steps in the above code generally follow the three steps mentioned earlier, and the last step is usually executed in the createProxyClassAndInstance() method of the CglibAopProxy subclass ObjenesisCglibAopProxy:

```java
protected Object createProxyClassAndInstance(Enhancer enhancer, Callback[] callbacks) {
    // Create proxy class
    Class<?> proxyClass = enhancer.createClass();
    Object proxyInstance = null;
    // The default value of spring.objenesis.ignore is false,
    // so objenesis.isWorthTrying() is generally true.
    if (objenesis.isWorthTrying()) {
        try {
            // Create instance
            proxyInstance = objenesis.newInstance(proxyClass, enhancer.getUseCache());
        }
        catch (Throwable ex) {
            // Omitted non-core code
        }
    }

    if (proxyInstance == null) {
        // Try to create instance using normal reflection
        try {
            Constructor<?> ctor = (this.constructorArgs != null ?
                    proxyClass.getDeclaredConstructor(this.constructorArgTypes) :
                    proxyClass.getDeclaredConstructor());
            ReflectionUtils.makeAccessible(ctor);
            proxyInstance = (this.constructorArgs != null ?
                    ctor.newInstance(this.constructorArgs) : ctor.newInstance());
            // Omitted non-core code
        }
    }
    // Omitted non-core code
    ((Factory) proxyInstance).setCallbacks(callbacks);
    return proxyInstance;
}

From this code, we can understand that Spring will try to use Objenesis to instantiate objects by default, and if it fails, it will try to instantiate them using the normal method. Now, let’s take a further look at the process of instantiating objects with Objenesis.

Referring to the call stack shown in the screenshot above, the Objenesis method ultimately uses the JDK’s ReflectionFactory.newConstructorForSerialization() to instantiate the proxy object. If you study this method a little, you will be surprised to find that objects created in this way will not initialize class member variables.

So at this point, astute readers may have already realized that the truth has been exposed. The key issue here is that the default construction method of the proxy class is very special. Here, we can summarize and compare the ways to instantiate objects through reflection, including:

  • java.lang.Class.newInstance()
  • java.lang.reflect.Constructor.newInstance()
  • sun.reflect.ReflectionFactory.newConstructorForSerialization().newInstance()

The first two initialization methods will initialize class member variables, but the last one, ReflectionFactory.newConstructorForSerialization().newInstance(), will not initialize class member variables. This is the final answer to the current problem.

Problem Correction #

After understanding the root cause of the problem, the correction is not difficult. Since direct access to the member variables of the intercepted class is not possible, we can use a different approach and write a getUser() method in UserService to access the variables internally.

We added a getUser() method in AdminUserService:

public User getUser() {
    return user;
}

In ElectricService, we use getUser() to get the User object:

// Original way with error:
// String payNum = adminUserService.adminUser.getPayNum();
// Modified way:
String payNum = adminUserService.getAdminUser().getPayNum();

When running it, everything is normal, and we can see the admin login logs:

Electric charging ...
! admin login ...
admin user login...
User pay num : 202101166
Pay with alipay ...

But may I confuse you again? Since the class attributes of the proxy class will not be initialized, why can we get the attributes of the proxy class instance by writing a getUser() method in AdminUserService?

Let’s review the logic of the createProxyClassAndInstance method again. After creating the proxy class, we call setCallbacks to set the injected code after interception:

protected Object createProxyClassAndInstance(Enhancer enhancer, Callback[] callbacks) {
    Class<?> proxyClass = enhancer.createClass();
    Object proxyInstance = null;
    if (objenesis.isWorthTrying()) {
        try {
            proxyInstance = objenesis.newInstance(proxyClass, enhancer.getUseCache());
        }
        // Omitted non-core code
    }
    ((Factory) proxyInstance).setCallbacks(callbacks);
    return proxyInstance;
}

Through code debugging and analysis, we can find out that the callbacks above will include a DynamicAdvisedInterceptor that serves AOP. It implements the intercept() method of the MethodInterceptor interface (a sub-interface of Callback). Let’s see how it implements this method:

public Object intercept(Object proxy, Method method, Object[] args, MethodProxy methodProxy) throws Throwable {
    // Omitted non-core code
    TargetSource targetSource = this.advised.getTargetSource();
    // Omitted non-core code
    if (chain.isEmpty() && Modifier.isPublic(method.getModifiers())) {
        Object[] argsToUse = AopProxyUtils.adaptArgumentsIfNecessary(method, args);
        retVal = methodProxy.invoke(target, argsToUse);
    }
    else {
        // We need to create a method invocation...
        retVal = new CglibMethodInvocation(proxy, target, method, args, targetClass, chain, methodProxy).proceed();
    }
    retVal = processReturnType(proxy, target, method, retVal);
    return retVal;
}

When a method of the proxy class is called, it will be intercepted by Spring, and then enter this intercept() method, where the original object being proxied is obtained. In the original object, the class attributes are instantiated and exist. Therefore, the proxy class can access the attributes of the instance of the proxied class through method interception.

Now we have solved the problem. But if you look closely, you will find that you can also make the attributes of the generated proxy objects not null by modifying a property. For example, modify the startup parameter spring.objenesis.ignore as follows:

When debugging the program again, you will find that adminUser is no longer null:

So this is another way to solve the problem. I believe that astute readers can already find out how it works from the code posted earlier.

## Key Points Review

Through the introduction of the two cases above, I believe you have gained a further understanding of the initialization mechanism of Spring AOP dynamic proxy. Here are the key points summarized as follows:

1. Using AOP means letting Spring automatically create a proxy for us, so that the caller can invoke a specified method without awareness. Spring helps us dynamically weave in other logic at runtime, so AOP is essentially a dynamic proxy.

2. To obtain the functionality implemented by AOP, we can only access the methods of these proxy objects. Therefore, it is not possible to correctly use AOP functionality through the `this` reference. Without changing the code result, we can use methods such as `@Autowired` and `AopContext.currentProxy()` to obtain the corresponding proxy object to achieve the desired functionality.

3. Generally, we cannot directly access the attributes of the proxied class from the proxy class. This is because unless we explicitly set `spring.objenesis.ignore` to true, the attributes of the proxy class will not be initialized by Spring. We can indirectly obtain the attributes by adding a method in the proxied class.
## Thought-provoking question

In the second case, we mentioned three ways to instantiate a class using reflection:

  * `java.lang.Class.newInstance()`
  * `java.lang.reflect.Constructor.newInstance()`
  * `sun.reflect.ReflectionFactory.newConstructorForSerialization().newInstance()`

Among them, the third way does not initialize the class attributes. Can you provide an example to demonstrate this?

Looking forward to your thoughts in the comments section!