Spring Circular Dependencies

Spring Circular Dependencies

I know you’ve been there, like me:

  • I Have a circular dependency in Spring and my application fails to launch. How to solve this?

The typical exception thrown by Spring is the following:

‘org.springframework.security.authenticationManager’: Requested bean is currently in creation: Is there an unresolvable circular reference?

And, It does not give much details. So, let’s see how we can break the circular dependency cycle and save some sanity!

Understand the Issue

Let’s take a very simple example to illustrate the issue. Here is part of our code handling the application permissions.

public interface PermissionByObject {

  boolean accept(Object targetObject, String permission);

  boolean test(User user, Object targetObject, String permission);
}

The SpringPermissionEvaluator then requires all the PermissionByObject implementations to be injected via collection autowiring.


final class SpringPermissionEvaluator implements SystemPermissionEvaluator {
  List<PermissionByObject> byObject;

  SpringPermissionEvaluator(final List<PermissionByObject> byObjects) {
    super();
    this.byObject = requireNonNull(byObjects);
  }

  @Override
  public boolean hasPermission(
    final Authentication authentication,
    final Object targetObject,
    final Object key) {
    final User user = (User) authentication.getPrincipal();

    final String permission = valueOf(key);
    return byObject
      .stream()
      .filter(p -> p.accept(targetObject, permission))
      .anyMatch(p -> p.test(user, targetObject, permission));
  }
}

Suppose now that we have the following implementation named WorkspaceByObject:

@Component
final class WorkspaceByObject implements PermissionByObject {
  
  private final WorkspacePermission delegate;

  WorkspaceByObject(WorkspacePermission delegate) {
    this.delegate = checkNotNull(delegate);
  }

  @Override
  public boolean accept(final Object targetObject, final String permission) {
    return targetObject instanceof Workspace;
  }

  @Override
  public boolean test(final User user, final Object targetObject, final String permission) {
    // do something with the evaluator like delegating the call
  }

}

We have a circular dependency here:

  • SystemPermissionEvaluator requires all PermissionByObject on construction,
  • WorkspaceByObject implements PermissionByObject and requires SystemPermissionEvaluator on construction.

We’re going to explore the ways we can solve this issue.

Field Autowiring

The first and most obvious solution is to prevent the WorkspaceByObject from requiring the SystemPermissionEvaluator upon construction by using Field injection.

@Component
final class WorkspaceByObject implements PermissionByObject {
  @Autowired
  SystemPermissionEvaluator evaluator;

  @Override
  public boolean accept(final Object targetObject, final String permission) {
    return targetObject instanceof Workspace;
  }

  @Override
  public boolean test(final User user, final Object targetObject, final String permission) {
    // do something with the evaluator like delegating the call
  }

}

SystemPermissionEvaluator injected via Field Injection

Setter Autowiring

Or Setter Injection:

@Component
final class WorkspaceByObject implements PermissionByObject {
  
  SystemPermissionEvaluator evaluator;

  @Override
  public boolean accept(final Object targetObject, final String permission) {
    return targetObject instanceof Workspace;
  }

  @Override
  public boolean test(final User user, final Object targetObject, final String permission) {
    // do something with the evaluator like delegating the call
  }

  public void setEvaluator(final SystemPermissionEvaluator evaluator) {
    this.evaluator = evaluator;
  }
}

SystemPermissionEvaluator injected via Setter Injection

Another solution would be to apply the same trick to the SystemPermissionEvaluator. Why does it work? Because Spring can inject the dependencies after construction.

Using @Lazy

The @Lazy annotation allows Spring to inject a Java Proxy of your bean, instead of the bean itself. It can resolves the cycle by instantiating the beans when a method on it is called.

@Component
final class WorkspaceByObject implements PermissionByObject {
  
  private final SystemPermissionEvaluator evaluator;

  WorkspaceByObject(@Lazy final SystemPermissionEvaluator evaluator) {
    super();
    this.evaluator = checkNotNull(evaluator);
  }

  @Override
  public boolean accept(final Object targetObject, final String permission) {
    return targetObject instanceof Workspace;
  }

  @Override
  public boolean test(final User user, final Object targetObject, final String permission) {
    // do something with the evaluator like delegating the call
  }
}

Because @Lazy proxyfies your bean, calls to your service are done through reflection, thus being 2x slower than direct calls. Therefore, I would recommend using @Lazy only when nothing else is possible.

Using @PostConstruct

PostConstruct can be used to annotate a method which is then called once the bean has been properly autowired. It can be effectively used to fix a circular dependency, although you should be aware it’s pretty ugly to do so.

We can do so by altering the permission interface:

public interface PermissionByObject {

  void setPermissionEvaluator(SystemPermissionEvaluator evaluator);

  boolean accept(Object targetObject, String permission);

  boolean test(User user, Object targetObject, String permission);
}

As you can see, this will pretty much affect every single PermissionByObject implementation even if it’s not using the SystemPermissionEvaluator

Then, we’ll do this in the SystemPermissionEvaluator:


final class SpringPermissionEvaluator implements SystemPermissionEvaluator {
  List<PermissionByObject> byObject;

  SpringPermissionEvaluator(final List<PermissionByObject> byObjects) {
    super();
    this.byObject = requireNonNull(byObjects);
  }

  @PostConstructor 
  void postConstruct() {
    for (final PermissionByObject p : byObjects) {
      p.setPermissionEvaluator(this);
    }
  }
}

Needless to say, this solution is exposing the internals of a specific permission to everyone. It’s also affecting permissions which don’t need the SystemPermissionEvaluator being injected.

So, what can be do to solve this issue properly?

Using @Configuration

Another solution is to register the PermissionByObject instances into the SystemPermissionEvaluator using an external Spring Configuration within the same package. The key is to keep the registering logic invisible to external callers.


@Service
final class SpringPermissionEvaluator implements SystemPermissionEvaluator {
  private final List<PermissionByObject> byObject = new ArrayList<>();

  void register(final PermissionByObject p) {
    byObject.add(p);
  }
}

@Configuration
class PermissionConfig {

  PermissionConfig(final SystemPermissionEvaluator evaluator, final List<PermissionByObject> permissions) {
    super();
    permissions.forEach(evaluator::register);
  }
}

By doing this, the cycle is broken down. Both permission evaluator and permissions can be instantiated independently.

Changing the Code

A circular dependency issue should be an alarm screaming: Hey, there is something wrong with your code architecture! Of course, it’s not always possible to fix the issue by yourself. It may happen due to a badly designed library you are depending on.

After figuring it out, in fact the WorkspaceByObject has been rewritten (as well as other parts of the permission system) to completely avoid injecting the SystemPermissionEvaluator at all!

Final Words

Always consider redesigning your code before deep diving into fancy solutions to fix spring circular dependencies issues. Use the methods described here as last resort when nothing else is possible.

Remember Spring isn’t difficult! There is a solution to every problem related to Spring, and chances are that you are not the single one experiencing those issues. Always look on Spring StackOverflow to see if someone else has had the same issue.

By - CTO.
Tags: Java Spring

Comments

 

Thank you

Your comment has been submitted and will be published once it has been approved.

OK

OOPS!

Your post has failed. Please return to the page and try again. Thank You!

OK