Kotlin instance variable is null when accessed by

2020-06-12 03:54发布

问题:

I have a service class that is being proxied by Spring, like so:

@Service
@Transactional
open class MyService { ... }

If I remove the open modifier, Spring complains that it needs to proxy the class to apply the @Transactional annotation tweaks.

However, this is causing issues when calling a function on the proxied service, which attempts to access a variable:

@Service
@Transactional
open class MyService { 
    protected val internalVariable = ...

    fun doWork() {
        internalVariable.execute() // NullPointerException
    }
}

The internalVariable is assigned as part of its declaration, does not have any annotations (like @Autowired, etc.), and works fine when I remove the @Transactional annotation and the requirement for Spring to proxy the class.

Why is this variable null when Spring is proxying/subclassing my service class?

回答1:

I hit a similar issue and the above comments by Rafal G & Craig Otis helped me-- so I'd like to propose that the following write up be accepted as an answer (or the comments above be changed to an answer and they be accepted).

The solution: open the method/field.

(I hit a similar case where it was a closed method that caused the problem. But whether it is a field/method the solution is the same, and I think the general cause is the same...)

Explanation:

Why this is the solution is more complicated and definitely has to do with Spring AOP, final fields/methods, CGLIB proxies, and how Spring+CGLIB attempts to deal with final methods (or fields).

Spring uses proxies to represent certain objects to handle certain concerns dealt with by Aspect Oriented Programming. This happens with services & controllers (especially when @Transactional or other advice is given that requires AOP solutions).

So a Proxy/Wrapper is needed with these beans, and Spring has 2 choices-- but only CGLIB is available when the parent class is not an interface.

When using CGLIB to proxy classes Spring will create a subclass called something like myService$EnhancerByCGLIB. This enhanced class will override some if not all of your business methods to apply cross-cutting concerns around your actual code.

Here comes the real surprise. This extra subclass does not call super methods of the base class. Instead it creates second instance of myService and delegates to it. This means you have two objects now: your real object and CGLIB enhanced object pointing to (wrapping) it.

From: spring singleton bean fields are not populated

Referenced By: Spring AOP CGLIB proxy's field is null

In Kotlin, classes & methods are final unless explicitly opened.

The magic of how Spring/CGLib when & how chooses to wrap a Bean in an EnhancerByCGLIB with a target delegate (so that it can use finalized methods/fields) I don't know. For my case, however the debugger showed me the 2 different structures. When the parent methods are open, it does not create a delegate (using subclassing instead) and works without NPE. However, when a particular methods is closed then for that closed method Spring/CGLIB uses a wrapped object with delegation to a properly initialized target delegate. For some reason, the actual invocation of the method is done with the context being the wrapper with its uninitialized field values (NULLs), causing NPE. (Had the method on the actual target/delegate been called, there should not have been a problem).

Craig was able to solve the problem by opening the property (not the method)-- which I suspect had a similar effect of allowing Spring/CGLib to either not use a delegate, or to somehow use the delegate correctly.