Why does adding a super-class break my Spring beans?

dave :

I have a Spring Boot web application that is working correctly. I noticed that two @Repository beans had a lot in common, so I refactored them using an abstract super class and now my application is broken. I've double-checked and this is the only change that I've made between the working and non-working states. Can anyone see what I've done wrong?

Here's my working code:

public class One { ... }
public class Two { ... }

@Repository
public class RepoOne {
    private final ISource<One> sourceOne;
    private ICache<One> cache;
    @Value("${com.example.lifetime.one}")
    private int lifetime;
    public RepoOne(ISource<One> sourceOne) {
       this.sourceOne = sourceOne;
    }
    @PostConstruct
    public void createCache() {
       Duration lifetime = Duration.ofMinutes(this.lifetime);
       this.cache = new Cache<>(lifetime, sourceOne);
    }
    public One get(String key) {
      return cache.get(key);
    }
}

@Repository
public class RepoTwo {
    private final ISource<Two> sourceTwo;
    private ICache<Two> cache;
    @Value("${com.example.lifetime.two}")
    private int lifetime;
    public RepoOne(ISource<Two> sourceTwo) {
        this.sourceTwo = sourceTwo;
    }
    @PostConstruct
    public void createCache() {
        Duration lifetime = Duration.ofMinutes(this.lifetime);
        this.cache = new Cache<>(lifetime, sourceTwo);
    }
    public Two get(String key) {
        return cache.get(key);
    }
}

@Service
public class RepoService {
    private final RepoOne repoOne;
    private final RepoTwo repoTwo;
    public RepoService(RepoOne repoOne, RepoTwo repoTwo) {
        this.repoOne = repoOne;
        this.repoTwo = repoTwo;
    }
    public void doSomething(String key) {
        One one = repoOne.get(key);
        ...
    }
}

Here's my re-factored code where I introduced an abstract, generic super-class.

abstract class AbstractRepo<T> {
    private final ISource<T> source;
    private ICache<T> cache;
    AbstractRepo (ISource<T> source) {
       this.source = source;
    }
    @PostConstruct
    private void createCache() {
       Duration lifetime = Duration.ofMinutes(lifetime());
       this.cache = new Cache<>(lifetime, source);
    }
    protected abstract int lifetime();
    public final T get(String key) {
        return cache.get(key);
    }
}

@Repository
public class RepoOne extends AbstractRepo<One> {
    @Value("${com.example.lifetime.one}")
    private int lifetime;
    public RepoOne(ISource<One> sourceOne) {
       super(source);
    }
    protected int lifetime() { return lifetime; }
}

@Repository
public class RepoTwo extends AbstractRepo<Two> {
    @Value("${com.example.lifetime.two}")
    private int lifetime;
    public RepoTwo(ISource<Two> sourceTwo) {
       super(source);
    }
    protected int lifetime() { return lifetime; }
}

When using the re-factored code I get a NullPointerException in AbstractRepo::get(). I've confirmed via the debugger that cache is null (along with source). However, I also confirmed via the debugger that instances of RepoOne and RepoTwo are created and their createCache() method called. It's as if two instances of each are being created and only the one is initialised. Any thoughts?

M. Deinum :

It isn't the fact that you introduced a parent class but the fact that you turned the get method into a final method.

A class annotated with @Repository will get automatic exception translation. This automatic exception translation is added through the use of AOP. The default mechanism to apply AOP in Spring is to use proxies and in this case a class based proxy.

What happens is that CgLib creates a proxy for your classes by subclassing it, so that when a method is called an advice can be added. However a final method cannot be overridden in a subclass. Which will lead to the get method being called on the proxy instead of the actual instance.

There are 2 ways of fixing this

  1. Remove the final keyword
  2. Introduce an interface defining the contract for your repositories. This will lead to a JDK Dynamic proxy being created. JDK Dynamic Proxies are interface based and don't need to subclass your actual class (that is only for class based proxies).

Guess you like

Origin http://43.154.161.224:23101/article/api/json?id=80762&siteId=1