[BUG] Configuration bindings prints value but at runtime throws NullPointerException Version: 7.1.3
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) {
...
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)
@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
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.
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: @.***>
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.
I will try to create a public repo with this issue reproduced
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.
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.
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.
I will try it out