Search code examples
javaspringspring-aopcglib

Lazy object builders with Spring bean


I am playing with an idea to use a similar approach that @Configuration classes are able to do, that they can lazily create beans with calls to @Bean methods and return the existing objects if already called. This is done through some magic with CGLib proxies.

One particular interesting thing is that it works even when calling the method on itself:

@Configuration
class Config {
    @Bean ClassA beanA() {
        return new ClassA(beanB());
    }

    @Bean ClassB beanB() {
        return new ClassB();
    }
}

Now, in my use case, not concerning Spring configuration, I want to use this ability to lazily create arbitrary object graphs (which should not be Spring Beans) by calling a method of a Builder bean that would create the objects if not yet called, and returning existing objects if already called. And as well I want to leverage the ability to self-invoke methods on the same instance. So far, I wasn't able to do this.

How can I create and enhance Spring Beans (as CGLib proxies) so that they are able to self-invoke methods, similarly the @Configuration classes do, but with my own custom advice handling the laziness and caching?


EDIT : more detail

The result, in the end, should look similar to the configuration example above, but it would be a normal Spring singleton bean:

@Component
@MyBuilder // or some other custom annotation
class MyObjectGraphBuilder {
    @Builder ClassA objectA() {
        return new ClassA(objectB());
    }

    @Builder ClassB objectB() {
        return new ClassB();
    }
}

With the added capability to only call the original method once, and caching the result for any subsequent call (including especially the self-invocation). The above is just an example, there may be many such builder beans, and they can be complex with cross-dependencies between them.

The method call result caching is simple (could be done by AOP), but what I want is the self-invocation capability, which is normally not supported by Spring unless it's a @Configuration class.

I figured that Spring is doing this by enhancing the @Configuration bean classes with their own CGlib proxies. However, it involves a lot of copying and customizing (e.g. ConfigurationClassEnhancer, ConfigurationClassPostProcessor, etc), and so far I had no luck of actually making it work with my custom Post Processor and Enhancer (the code is too long, but it's basically a copy of the mentioned classes and writing my custom method interceptors). So I'm trying to find if there exists any other way.


Solution

  • The simple answer concerning AOP and self-invocation is: You cannot use Spring AOP, you have to use full AspectJ. The good news is that you don't require any proxies for that solution. The Spring manual describes how to use AspectJ from Spring via LTW (load-time weaving). Don't worry, if configured correctly you can use AspectJ alongside other aspects implemented via Spring AOP. Besides, if you don't like LTW, you can also use compile-time weaving via AspectJ Maven plugin.

    Now here is a little caching example in pure Java + AspectJ (no Spring involved) for demonstration:

    Builder annotation:

    package de.scrum_master.app;
    
    import static java.lang.annotation.ElementType.METHOD;
    import static java.lang.annotation.RetentionPolicy.RUNTIME;
    
    import java.lang.annotation.Retention;
    import java.lang.annotation.Target;
    
    @Retention(RUNTIME)
    @Target(METHOD)
    public @interface Builder {}
    

    Sample classes:

    package de.scrum_master.app;
    
    public class ClassB {
      @Override
      public String toString() {
        return "ClassB@" + hashCode();
      }
    }
    
    package de.scrum_master.app;
    
    public class ClassA {
      private ClassB objectB;
    
      public ClassA(ClassB objectB) {
        this.objectB = objectB;
      }
    
      @Override
      public String toString() {
        return "ClassA@" +hashCode() + "(" + objectB + ")";
      }
    }
    

    Driver application with annotated factory methods:

    package de.scrum_master.app;
    
    public class MyObjectGraphBuilder {
      @Builder
      ClassA objectA() {
        return new ClassA(objectB());
      }
    
      @Builder
      ClassB objectB() {
        return new ClassB();
      }
    
      public static void main(String[] args) {
        MyObjectGraphBuilder builder = new MyObjectGraphBuilder();
        System.out.println(builder.objectB());
        System.out.println(builder.objectA());
        System.out.println(builder.objectB());
        System.out.println(builder.objectA());
        System.out.println(builder.objectB());
        System.out.println(builder.objectA());
      }
    }
    

    Console log without caching aspect:

    ClassB@1829164700
    ClassA@2018699554(ClassB@1311053135)
    ClassB@118352462
    ClassA@1550089733(ClassB@865113938)
    ClassB@1442407170
    ClassA@1028566121(ClassB@1118140819)
    

    So far, so predictable. This is the normal behaviour, no caching at all.

    Caching aspect:

    Now this aspect is really simple. There is no thread-safety, no way to create multiple named beans of the same class or anything similar, but I guess you can take it from here, the principle stays the same.

    package de.scrum_master.app;
    
    import java.util.HashMap;
    import java.util.Map;
    
    import org.aspectj.lang.ProceedingJoinPoint;
    import org.aspectj.lang.annotation.Around;
    import org.aspectj.lang.annotation.Aspect;
    import org.aspectj.lang.reflect.MethodSignature;
    
    @Aspect
    public class BuilderCacheAspect {
      private Map<Class<?>, Object> cachedObjects = new HashMap<>();
    
      @Around("@annotation(de.scrum_master.app.Builder) && execution(* *(..))")
      public Object findOrCreateObject(ProceedingJoinPoint thisJoinPoint) throws Throwable {
        //System.out.println(thisJoinPoint);
        Class<?> returnType = ((MethodSignature) thisJoinPoint.getSignature()).getReturnType();
        Object cachedObject = cachedObjects.get(returnType);
        if (cachedObject == null) {
          cachedObject = thisJoinPoint.proceed();
          cachedObjects.put(returnType, cachedObject);
        }
        return cachedObject;
      }
    }
    

    Console log with caching aspect:

    ClassB@1392838282
    ClassA@664740647(ClassB@1392838282)
    ClassB@1392838282
    ClassA@664740647(ClassB@1392838282)
    ClassB@1392838282
    ClassA@664740647(ClassB@1392838282)
    

    Tadaa! There is our simple object cache. Enjoy.