micronaut-projects / micronaut-core

Micronaut Application Framework
http://micronaut.io
Apache License 2.0
6.03k stars 1.05k forks source link

Bean classes cannot be instantiated when constructor calls method #10833

Open nbrugger-tgm opened 3 months ago

nbrugger-tgm commented 3 months ago

Expected Behavior

The constructor of a class that is used for a bean should be able to call method within the constructor

example:

public class ClassCallingInConstructor {
    private int age;
    public ClassCallingInConstructor() {
        this("10");
    }
    public ClassCallingInConstructor(String age) {
        setAge(age);
    }

    public void setAge(@NotNull String age) {
        this.age = Integer.parseInt(age);
    }
}

Actual Behaviour

The app compiles but at runntime the bean cannot be injected/instantiated: Message: Cannot load from object array because "this.$interceptors" is null since the proxy tries to intercept the call from the constructor which is not possible since the interceptors are only instantiated afterwards, eg:

com.example.$ClassCallingInConstructor$Definition$Intercepted calls ClassCallingInConstructor.<init>() -> calls $ClassCallingInConstructor$Definition$Intercepted.setAge() which uses $interceptors which is null since the object is not constructed yet and the interceptor classes constructor is only called after the original constructor!

Steps To Reproduce

  1. git clone https://github.com/nbrugger-tgm/micronaut-reproductions.git
  2. cd micronaut-reproductions/micronaut-4-factory-regression
  3. ./gradlew test

The result is 2 failing tests, one using @Singelton for bean instantiation and one @Factory

Environment Information

OS: Kubuntu 23.10 x86_64 Micronaut: (referenced in the demo) 4.4.6

Example Application

https://github.com/nbrugger-tgm/micronaut-reproductions

Version

4.4.2

dstepanov commented 3 months ago

And what are you suggesting?

dstepanov commented 3 months ago

It looks like @NotNull is causing the interceptor to be made

nbrugger-tgm commented 3 months ago

That @NotNullis creating a interceptor imo is a bug but i ~will create~ created another ticket for that (#10836) (since ONLY the notnull on the factory creates the interceptor but NOT the not null on setAge)

nbrugger-tgm commented 3 months ago

My suggestion? A null check in intercepted methods to bypass interception when called from constructors:

 public void setAge(String var1) {
        (new MethodInterceptorChain(this.$interceptors[0], this.$target, this.$proxyMethods[0], new Object[]{var1})).proceed();
    }

to

 public void setAge(String var1) {
        if(this.$interceptors != null) (new MethodInterceptorChain(this.$interceptors[0], this.$target, this.$proxyMethods[0], new Object[]{var1})).proceed();
       else super.setAge(var1);
    }
nbrugger-tgm commented 3 months ago

Also just in case you want to know. ClassCallingInConstructor is calling its constructor is not my class but a 3rd party one (Jooq) DaoImpland setConfiguration so i sadly cannot just "improve the design of the constructor" also this will most likely cause issues once java 22/23 is out and methods can be called BEFORE the super() or this() call (see jetbrains post