Tuesday, January 26, 2016

Find circular dependencies in Spring Autowired services before Spring does

We use Spring annotation based configuration, and @Autowire the dependencies. Using Autowire injection is a more convenient way than using constructor based injection, with less code to write. However, if your code grows, you may end up with circular dependencies in your code. While spring does a great job of still instantiating beans that have a circular reference, after too many circular references, you will end up getting an exception like this:

org.springframework.beans.factory.BeanCurrentlyInCreationException: Error creating bean with name 'agencyBean': Requested bean is currently in creation: Is there an unresolvable circular reference?
    at org.springframework.beans.factory.support.AbstractBeanFactory.doGetBean(AbstractBeanFactory.java:252)
    at org.springframework.beans.factory.support.AbstractBeanFactory.getBean(AbstractBeanFactory.java:190)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.findAutowireCandidates(DefaultListableBeanFactory.java:844)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.doResolveDependency(DefaultListableBeanFactory.java:786)
    at org.springframework.beans.factory.support.DefaultListableBeanFactory.resolveDependency(DefaultListableBeanFactory.java:703)
    at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor$AutowiredFieldElement.inject(AutowiredAnnotationBeanPostProcessor.java:474)
    at org.springframework.beans.factory.annotation.InjectionMetadata.inject(InjectionMetadata.java:84)
    at org.springframework.beans.factory.annotation.AutowiredAnnotationBeanPostProcessor.postProcessPropertyValues(AutowiredAnnotationBeanPostProcessor.java:282)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.populateBean(AbstractAutowireCapableBeanFactory.java:1074)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.doCreateBean(AbstractAutowireCapableBeanFactory.java:517)
    at org.springframework.beans.factory.support.AbstractAutowireCapableBeanFactory.createBean(AbstractAutowireCapableBeanFactory.java:456)
    at org.springframework.beans.factory.support.AbstractBeanFactory$2.getObject(AbstractBeanFactory.java:329)
    at com.myapp.utils.ViewScope.get(ViewScope.java:20)
    public class CircularDependencyUtil {
    private static final Logger logger = LoggerFactory.getLogger(CircularDependencyUtil.class);

When you get this exception, you might have reached a point where you have 50 services that have been autowired in such a way that you have ended up with multiple spring bean circular references. At this moment it is best to find out all the beans that have a circular reference.

Here is what you need to do:

First, add maven dependencies to jgrapht to your pom.xml
        
            org.jgrapht
            jgrapht-core
            0.9.1
        
        
            org.jgrapht
            jgrapht-ext
            0.9.0
        
Second, add the following code to a class of your choice.
    public static DirectedGraph generateDependencyGraphForServices(String packageName) {
        DirectedGraph dgraph = new DefaultDirectedGraph(DefaultEdge.class);
        ClassPathScanningCandidateComponentProvider scanner =
                new ClassPathScanningCandidateComponentProvider(true);

        scanner.addIncludeFilter(new AnnotationTypeFilter(Service.class));

        for (BeanDefinition bd : scanner.findCandidateComponents(packageName)) {
            try {
                Class beanClass = Class.forName(((ScannedGenericBeanDefinition) bd).getMetadata().getClassName());

                if (beanClass.getInterfaces().length == 0)
                    continue;
                String beanName = beanClass.getInterfaces()[0].getSimpleName();
                if (!beanClass.getInterfaces()[0].getName().startsWith(packageName)) { // only common services
                    continue;
                }
                if (!dgraph.containsVertex(beanName)) {
                    dgraph.addVertex(beanName);
                }
                Field[] fields = beanClass.getDeclaredFields();
                if (fields.length == 0)
                    continue;
                for (Field field : fields) {
                    if (!field.isAnnotationPresent(Autowired.class)) {
                        continue;
                    }
                    if (!field.getType().getName().startsWith(packageName)) { // only common services
                        continue;
                    }
                    String fieldName = field.getType().getSimpleName();
                    if (!dgraph.containsVertex(fieldName)) {
                        dgraph.addVertex(fieldName);
                    }
                    dgraph.addEdge(beanName, fieldName);
                    logger.info("Field={}", fieldName);
                }
            } catch (ClassNotFoundException e) {
                continue;
            }
        }
        return dgraph;
    }

    public static Set getCycleInDependencyGraph(DirectedGraph dgraph){
        CycleDetector cycleDetector = new CycleDetector<>(dgraph);
        Set cycles = cycleDetector.findCycles();
        return cycles;
    }

    public static OutputStream exportGraphInDOTFormat(DirectedGraph dgraph){
        VertexNameProvider vertexNameProvider = new VertexNameProvider() {
            @Override
            public String getVertexName(String vertex) {
                return vertex;
            }
        };
        DOTExporter exporter = new DOTExporter(vertexNameProvider, null, null, null, null);
        OutputStream outputStream = new ByteArrayOutputStream();
        OutputStreamWriter outputStreamWriter = new OutputStreamWriter(outputStream);
        try {
            exporter.export(outputStreamWriter, dgraph);
            //exporter.export(new FileWriter(targetDirectory + "initial-graph.dot"), dgraph);
        } catch (Exception e) {
            e.printStackTrace();
        }
        return outputStream;
    }
}

The function generateDependencyGraphForServices(String packageName) will create a graph of all the Beans in the package that have the Service annotation and their Autowired dependencies. After the graph is created, pass it to the getCycleInDependencyGraph(DirectedGraph dgraph) function, which will find all the beans that have a circular dependency.

Circular dependencies are never good, so even if Spring does work around them most of the times, it is best to not have them in your code.