dropwizard-guicey icon indicating copy to clipboard operation
dropwizard-guicey copied to clipboard

[BUG] Configuration bindings prints value but at runtime throws NullPointerException Version: 7.1.3

Open thiagohora opened this issue 1 year ago • 10 comments

NullPointerException when @Inject a particular configuration property

ru.vyarus.dropwizard.guice.debug.YamlBindingsDiagnostic: Available configuration bindings = 
...

Unique sub configuration objects bindings:

  MyConfig.batchOperations
            @Config BatchOperationsConfig = BatchOperationsConfig(datasets=DatasetsConfig[test=2])
    @Inject
    public MyService(@Config @NonNull BatchOperationsConfig batchOperationsConfig) {
       ...

thiagohora avatar Nov 07 '24 17:11 thiagohora

Could you please prepare an example project?

I tried to reproduce this, but it works. There must be some important specific in your project, not mentioned here. I can't imagine what could it be (and so I need some sample project to reproduce and investigate this problem).

The same configuration introspection data is used for configuration report and for actual bindings, so if report shows something - it should be bound in context. Also, as it shows value in report, the same value must be bound - have no idea how null could be bound instead (and we could be sure that binding exists because otherwise guice would complain).

You could also try to enable guice report:

.printAllGuiceBindings()

which shows all actual guice bindings (whreas configurtation report shows bindings that should be registered). It does not show the binding value (probably good idea for a new report), but you'll see if @Config BatchOperationsConfig binding is available at all. Not sure if it will show anything new because we already know the binding exists (maybe it will push you into right direction)

xvik avatar Nov 08 '24 09:11 xvik

@Singleton
@RequiredArgsConstructor(onConstructor_ = @Inject)
@Slf4j
class DatasetServiceImpl implements DatasetService {

    private static final String DATASET_ALREADY_EXISTS = "Dataset already exists";

    private final @NonNull IdGenerator idGenerator;
    private final @NonNull TransactionTemplate template;
    private final @NonNull Provider<RequestContext> requestContext;
    private final @NonNull EntitytDAO entityDAO;
    private final @NonNull @Config BatchOperationsConfig batchOperationsConfig;

    @Override
    public DatasetPage find(int page, int size, @NonNull DatasetCriteria criteria) {
        String workspaceId = requestContext.get().getWorkspaceId();
        String userName = requestContext.get().getUserName();

        int maxExperimentInClauseSize = batchOperationsConfig.getDatasets().getMaxExperimentInClauseSize();
        //...
     }   

Config class:


@Getter
public class MyAPPConfiguration extends Configuration {

    @Valid
    @NotNull @JsonProperty
    private DataSourceFactory database = new DataSourceFactory();

    @Valid
    @NotNull @JsonProperty
    private CorsConfig cors = new CorsConfig();

    @Valid
    @NotNull @JsonProperty
    private BatchOperationsConfig batchOperations = new BatchOperationsConfig();
}

Lombok Config

lombok.copyableAnnotations += jakarta.inject.Named
lombok.copyableAnnotations += ru.vyarus.dropwizard.guice.module.yaml.bind.Config

Error

ERROR [2024-11-08 14:34:35,225] io.dropwizard.jersey.errors.LoggingExceptionMapper: Error handling a request: a3c206a253a770ab
2024-11-08T14:34:35.241199214Z ! java.lang.NullPointerException: Cannot invoke "com.my.project.infrastructure.BatchOperationsConfig$DatasetsConfig.getMaxExperimentInClauseSize()" because the return value of "com.my.project.infrastructure.BatchOperationsConfig.getDatasets()" is null
2024-11-08T14:34:35.241203214Z ! at com.my.project.domain.DatasetServiceImpl.find(DatasetService.java:261)
2024-11-08T14:34:35.241205464Z ! at com.my.project.api.resources.v1.priv.DatasetsResource.findDatasets(DatasetsResource.java:127)
2024-11-08T14:34:35.241206339Z ! at java.base/jdk.internal.reflect.DirectMethodHandleAccessor.invoke(DirectMethodHandleAccessor.java:103)
2024-11-08T14:34:35.241207131Z ! at java.base/java.lang.reflect.Method.invoke(Method.java:580)
2024-11-08T14:34:35.241208423Z ! at org.glassfish.jersey.server.model.internal.ResourceMethodInvocationHandlerFactory.lambda$static$0(ResourceMethodInvocationHandlerFactory.java:52)
2024-11-08T14:34:35.241209298Z 

Log from print:

─ ConfigBindingModule          (r.v.d.g.m.yaml.bind)      
2024-11-08T14:07:13.800219219Z     │       ├── instance             [@Singleton]     ConfigurationTree                               at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.configure(ConfigBindingModule.java:52)
2024-11-08T14:07:13.800220135Z     │       ├── instance             [@Singleton]     Configuration                                   at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindRootTypes(ConfigBindingModule.java:102)
2024-11-08T14:07:13.800221094Z     │       ├── instance             [@Singleton]     MyAPPConfiguration                               at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindRootTypes(ConfigBindingModule.java:102)
2024-11-08T14:07:13.800222010Z     │       ├── instance             [@Singleton]     @Config Configuration                           at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindRootTypes(ConfigBindingModule.java:104)
2024-11-08T14:07:13.800224010Z     │       ├── instance             [@Singleton]     @ConfigMyAPPConfiguration                       at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindRootTypes(ConfigBindingModule.java:104)
2024-11-08T14:07:13.800224927Z     │       ├── instance             [@Singleton]     @Config AdminFactory                            at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindUniqueSubConfigurations(ConfigBindingModule.java:117)
2024-11-08T14:07:13.800225844Z     │       ├── instance             [@Singleton]     @Config AuthenticationConfig                    at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindUniqueSubConfigurations(ConfigBindingModule.java:117)
2024-11-08T14:07:13.800226760Z     │       ├── instance             [@Singleton]     @Config BatchOperationsConfig                   at ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindUniqueSubConfigurations(ConfigBindingModule.java:117)
2024-11-08T14:07:13.800227635Z     │       ├── instance             [@Singleton]     @Config CorsConfig                  

thiagohora avatar Nov 08 '24 14:11 thiagohora

Sorry, I can't reproduce this. There must be some side effect: either DatasetServiceImpl.batchOperationsConfig instance is different from MyAPPConfiguration.batchOperations or somewhere BatchOperationsConfig.setDatasets(null) is called.

One way to verify is by injecting MyAPPConfiguration in constructor to be able to compare instances:

@Singleton
@RequiredArgsConstructor(onConstructor_ = @Inject)
@Slf4j
class DatasetServiceImpl implements DatasetService {

    private static final String DATASET_ALREADY_EXISTS = "Dataset already exists";

    private final @NonNull IdGenerator idGenerator;
    private final @NonNull TransactionTemplate template;
    private final @NonNull Provider<RequestContext> requestContext;
    private final @NonNull EntitytDAO entityDAO;
    private final @NonNull @Config BatchOperationsConfig batchOperationsConfig;
    private final @NonNull MyAPPConfiguration configuration;

    @Override
    public DatasetPage find(int page, int size, @NonNull DatasetCriteria criteria) {
        String workspaceId = requestContext.get().getWorkspaceId();
        String userName = requestContext.get().getUserName();

        // must be the same instance
         System.out.println(System.idenityHashCode(configuration.getBatchOperationsConfig()))
         System.out.println(System.idenityHashCode(batchOperationsConfig))

        // must both be null
        System.out.println(configuration.getBatchOperationsConfig().getDatasets())
        System.out.println(batchOperationsConfig().getDatasets())

        int maxExperimentInClauseSize = batchOperationsConfig.getDatasets().getMaxExperimentInClauseSize();
        //...
     } 

If you'll make sure that BatchOperationsConfig is the same object then something sets null into datasets: try to debug setter (or println inside setter to track interactions)

Or, to avoid sharing project in public, you can send it directly to my email vyarus[at]gmail.com and I'll investigate the probelm.

xvik avatar Nov 10 '24 10:11 xvik

I just tested yesterday, it seems somehow related to docker. Out of the container it works, or the JVM, in docker I'm using correto 21 2033

On Sun, 10 Nov 2024, 11:01 Vyacheslav Rusakov, @.***> wrote:

Sorry, I can't reproduce this. There must be some side effect: either DatasetServiceImpl.batchOperationsConfig instance is different from MyAPPConfiguration.batchOperations or somewhere BatchOperationsConfig.setDatasets(null) is called.

One way to verify is by injecting MyAPPConfiguration in constructor to be able to compare instances:

@@.(onConstructor_ = @@. DatasetServiceImpl implements DatasetService {

private static final String DATASET_ALREADY_EXISTS = "Dataset already exists";

private final @NonNull IdGenerator idGenerator;
private final @NonNull TransactionTemplate template;
private final @NonNull Provider<RequestContext> requestContext;
private final @NonNull EntitytDAO entityDAO;
private final @NonNull @Config BatchOperationsConfig batchOperationsConfig;
private final @NonNull MyAPPConfiguration configuration;

@Override
public DatasetPage find(int page, int size, @NonNull DatasetCriteria criteria) {
    String workspaceId = requestContext.get().getWorkspaceId();
    String userName = requestContext.get().getUserName();

    // must be the same instance
     System.out.println(System.idenityHashCode(configuration.getBatchOperationsConfig()))
     System.out.println(System.idenityHashCode(batchOperationsConfig))

    // must both be null
    System.out.println(configuration.getBatchOperationsConfig().getDatasets())
    System.out.println(batchOperationsConfig().getDatasets())

    int maxExperimentInClauseSize = batchOperationsConfig.getDatasets().getMaxExperimentInClauseSize();
    //...
 }

If you'll make sure that BatchOperationsConfig is the same object then something sets null into datasets: try to debug getter (or println inside getter to track interactions)

Or, to avoid sharing project in public, you can send it directly to my email vyarus[at]gmail.com and I'll investigate the probelm.

— Reply to this email directly, view it on GitHub https://github.com/xvik/dropwizard-guicey/issues/409#issuecomment-2466668531, or unsubscribe https://github.com/notifications/unsubscribe-auth/ABSTONXY7PWHSG5BUS4ITM3Z74VHJAVCNFSM6AAAAABRLYTJYSVHI2DSMVQWIX3LMV43OSLTON2WKQ3PNVWWK3TUHMZDINRWGY3DQNJTGE . You are receiving this because you authored the thread.Message ID: @.***>

thiagohora avatar Nov 10 '24 10:11 thiagohora

I run all tests with correto 21 and everything works (it would be very strange if it didn't).

I assume that the simplest explanation could be server configuration file (yaml). If BatchOperationsConfig did not initialize DatasetsConfig and there is no configuration for it in yaml file, then it would remain null. But, in this case, configuration report should also show null values.

No other ideas. Only debug on real case could help.

xvik avatar Nov 13 '24 11:11 xvik

I will try to create a public repo with this issue reproduced

thiagohora avatar Nov 18 '24 09:11 thiagohora

The issue to be the mix between Lombok and the @Config, if add the annotation in the constructor param, it works, if I used the @RequiredArgsConstructor asking lombok to copy the annotation from the field to the param, guice seems no to see the annotation.

thiagohora avatar Mar 21 '25 17:03 thiagohora

guice seems no to see the annotation

I doubt. It's more likely annotation is not passed.

I tried to play with lombok and I have no problems (even don't need to configure lombok.copyableAnnotations, which is starnge). My lombok version is 1.18.32.

You can check if annotation is actually copied with ProvisionListener:

 // GuiceBundle.builder()

       .modules(binder -> binder.bindListener(
               // apply only for BatchOperationsConfig provision
               binding -> binding.getKey().getTypeLiteral().getRawType().isAssignableFrom(BatchOperationsConfig.class),

                new ProvisionListener() {
                       @Override
                       public <T> void onProvision(ProvisionInvocation<T> provision) {
                             System.out.println(">>>> " + provision.getBinding().getKey().toString());
                       }
        }))

This listener will react on BatchOperationsConfig injection, and, if its requested without annotation, you'll see something like this (I was testing on DataSourceFactory):

>>>> Key[type=io.dropwizard.db.DataSourceFactory, annotation=[none]]

Normally (when annotation is copied), you should see:

>>>> Key[type=io.dropwizard.db.DataSourceFactory, [email protected](value=)]

If you'll see that binding without annotation is requested then lombok didn't actually copied the annotation. In this case, gucie consider this as jit-in-time binding and constructs new BatchOperationsConfig instance.

Should be a lombok issue.

xvik avatar Mar 22 '25 09:03 xvik

Not sure if it would help, but in just released 7.2.0 there is a provision report (.printGuiceProvisionTime()) which also detects accidental JIT injections (unqualified injection when qualified bindings declared):

    Possible mistakes (unqualified JIT bindings):

         @Inject Sub:
              instance             [@Singleton]     @Config("val2") Sub                                                                   : 0.0005 ms        ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindValuePaths(ConfigBindingModule.java:129)
              instance             [@Singleton]     @Marker Sub                                                                           : 0.0007 ms        ru.vyarus.dropwizard.guice.module.yaml.bind.ConfigBindingModule.bindCustomQualifiers(ConfigBindingModule.java:87)
            > JIT                  [@Prototype]     Sub                

If @Config annotation is not copied, it should show it as a JIT injection.

xvik avatar May 12 '25 04:05 xvik

I will try it out

thiagohora avatar May 12 '25 07:05 thiagohora