eclipse / microprofile-config

MicroProfile Configuration Feature
Apache License 2.0
214 stars 115 forks source link

Null must be injectable #365

Closed rmannibucau closed 4 years ago

rmannibucau commented 6 years ago
@Inject
@ConfigProperty(name="...")
String value;

It must be allowed to inject that and get null, a workaround is to use an optional but it is not recommanded (even the opposite) to use optionals as fields so this is not a solution. Injecting null is fine from all point of views and compatible with mainstream config solution so it should probably make its path in mp-config asap.

dmlloyd commented 4 years ago

defaultValue+nullable are just as interoperable as defaultValue+Optional. This combination is already supported and meaningful in Quarkus via SmallRye Config. It is not a question of priority because there should be no conflict, particularly in light of #446: if a user explicitly empties a value, then null is injected even if there is a default value specified.

rmannibucau commented 4 years ago

@Emily-Jiang hmm, do you assume we keep NULL_VALUE? otherwise it means nullable is only used if unconfigured if the default value I guess so likely your B option. A is not really an option since the annotation has default so both will exists, we can only check the actual value.

However, if we are breaking the API, there is another option: extract defaultValue and nullable as annotation, there is no more ambiguity and startup (build for graalvm friends ;)) time validation will be back with a clean programming model:

@Inject
@ConfigProperty
@DefaultValue("....")
String myUrl;

@Inject
@Nullable
@ConfigProperty
String myUrl;

both are valid but

@Inject
@ConfigProperty
@Nullable
@DefaultValue("....")
String myUrl;

this last one is not valid

Note that this option also enables to type the default which would be a huge win for a spec on top of CDI:

@Inject
@ConfigProperty
@DefaultValue.String("....")
String myUrl;

side note: @dmlloyd you mix config injection and value extraction from the injection point and configuration value materialization in Config, this thread is 100% about injection, not about value materialization, maybe you should open another topic?

dmlloyd commented 4 years ago

The combination of nullable and Optional is not meaningful but does not have to be explicitly disallowed (it would not result in incorrect operation, just arguably useless behavior).

dmlloyd commented 4 years ago

@Emily-Jiang hmm, do you assume we keep NULL_VALUE? otherwise it means nullable is only used if unconfigured if the default value I guess so likely your B option. A is not really an option since the annotation has default so both will exists, we can only check the actual value.

No, if you read the thread we're talking about introducing a nullable parameter to @ConfigProperty.

However, if we are breaking the API, there is another option: extract defaultValue and nullable as annotation, there is no more ambiguity and startup (build for graalvm friends ;)) time validation will be back with a clean programming model:

We weren't talking about breaking the API, but if you want to propose some of these changes for 2.0 I think that would make sense, at least as a proposal. I think some of the CDI stuff does need a bit more examination.

side note: @dmlloyd you mix config injection and value extraction from the injection point and configuration value materialization in Config, this thread is 100% about injection, not about value materialization, maybe you should open another topic?

You're assuming an injection implementation that is different from what we do. This isn't a topic, it's a proposal specifically to add a nullable attribute, which was pretty well on track with a good degree of consensus to this point.

rmannibucau commented 4 years ago

@dmlloyd nullable is not about materializing null for empty strings

if a user explicitly empties a value, then null is injected even if there is a default value specified

This is typically a bug, it should be an empty string (and it is used in some apps from what I saw, typically to set a prefix/suffix).

So we still have a topic to enable to specify null in the config source, this will likely pass through returning optional instead of String in the source but this is unrelated to the enablement of null injections (default to the injection type default vs explicitly set null).

Wdyt about the proposal of splitting in multiple annotations? It drops all ambiguity and is quite successful in JSON-B for example.

dmlloyd commented 4 years ago

@dmlloyd nullable is not about materializing null for empty strings

if a user explicitly empties a value, then null is injected even if there is a default value specified

This is typically a bug, it should be an empty string (and it is used in some apps from what I saw, typically to set a prefix/suffix).

This is simply not the case universally. If you maintain this behavior, you create a large number of future problems - inconsistent array and list handling, potential problems relating to the future property expansion proposal, etc. In Quarkus we've successfully adopted the behavior specified by #446, which allows (for example) patterns like list.prop=a,${some.prop:},c to return [ "a", "c" ] when some.prop is not set. Prefixes/suffixes are trivially achievable by way of Optional and prefix.orElse("") as appropriate.

Emily-Jiang commented 4 years ago

It is not a question of priority because there should be no conflict, particularly in light of #446: if a user explicitly empties a value, then null is injected even if there is a default value specified.

If a user empties the property, the null comes from the configsource. null will be used. However, if the config property does not exist in any config sources and the defaultValue and nullable specified, which value should be assigned to the variable?

Emily-Jiang commented 4 years ago

@rmannibucau the nullable is similar to your originally suggestion of nullAllowed as shown in my code snippet. No additional annotation is introduced. I think the consensus is that we will remove NULL_VALUE and replace with an additional method boolean nullable() default false; @Inject @ConfigProperty(name="some", nullable=true) String value;

dmlloyd commented 4 years ago

It is not a question of priority because there should be no conflict, particularly in light of #446: if a user explicitly empties a value, then null is injected even if there is a default value specified.

If a user empties the property, the null comes from the configsource. null will be used. However, if the config property does not exist in any config sources and the defaultValue and nullable specified, which value should be assigned to the variable?

If the property does not exist in any config sources, then the default value would be assigned to the variable.

Emily-Jiang commented 4 years ago

To clarify:

emattheis commented 4 years ago

@emilyjiang Assuming @Config means @Inject above...

As far as which value to use for injection, the following should be equivalent:

@Inject
@ConfigProperty(name="some", defaultValue="blah", nullable=true)
String figure;
@Inject
@ConfigProperty(name="some", defaultValue="blah")
Optional<String> figure;

~You will never get a null value or an empty Optional injected.~ (depends on #446)

radcortez commented 4 years ago

In cases where we are injecting into primitive values like:

@ConfigProperty(name="bla", nullable=true)
int bla;

This of course, doesn't make much sense. Should we just let the container ignore this or throw a DeploymentException saying that nullable is not allowed for primitives?

emattheis commented 4 years ago

@radcortez I was just about to comment on that. CDI specifies that null injection to primitives results in the default value of the respective type. Not sure if that is a good idea here. I think I'm leaning towards disallowing nullable=true when the injection target is a primitive. 🤷‍♂

rmannibucau commented 4 years ago

Nullable means the user value is null and it is ok, injected value is the default of the type, no issue.

dmlloyd commented 4 years ago

To clarify:

  • @Config @ConfigProperty(name="some", defaultValue="blah", nullable=true) String figure; if the property some does not exist in any config source, the value of figure will be blah not null
  • @Config @ConfigProperty(name="some", nullable=true) String figure; if the property some does not exist in any config source, the value of figure will be null In the spec, since nullable is another way to set defaultValue, we should specify the behaviour as above.

nullable must not be considered to be another way to set defaultValue, because that eliminates the possibility to support the explicit erasure of property values described in #446.

Under this model, given a property with a default value and nullable=true, there are three possibilities:

So as you see, we must consider nullable (which is really just a variation on Optional) to be orthogonal to default values.

rmannibucau commented 4 years ago

No, your second point is a case we dont want null but "". Therefore you just demonstrated it is equivalent to defaultValue I think.

Resetting a value requires a specific value handled in configsources by Config, still nothing linked to this thread.

emattheis commented 4 years ago

It's linked to this thread because, ideally, handling of the defatultValue string should be the same as a string provided by any other config source. Thus, the motivation for avoiding the concept of a NULL_VALUE sentinel until that is straightened out.

radcortez commented 4 years ago
  • The property is explicitly emptied in a configuration source, in which case null is used

Well, at this point, this doest not apply yet, since there is no way in the API to empty a configuration value. Only in the discussion here https://github.com/eclipse/microprofile-config/issues/446, right?

dmlloyd commented 4 years ago

No, your second point is a case we dont want null but "". Therefore you just demonstrated it is equivalent to defaultValue I think.

First, I would request that you please not change my words. We want null explicitly in this case; I meant what I said and I said what I meant.

But if you really believe that "" should be returned then you should be busy refuting #446, and also spend some time thinking about what should be returned if the property is an Integer or a List for example.

Resetting a value requires a specific value handled in configsources by Config, still nothing linked to this thread.

This concept of resetting a value is something that you have invented. What I've suggested is completely internally consistent within the specification, and is also implemented consistently and correctly in SmallRye.

Once again, this thread is about nullable. If you want to suggest a different possibility, then please suggest it. I consider any attempt to "gaslight" or derail the conversation by stating altered facts as hostile behavior and a violation of the code of conduct and I will lock the thread if necessary to ensure that the discussion stays on topic.

dmlloyd commented 4 years ago
  • The property is explicitly emptied in a configuration source, in which case null is used

Well, at this point, this doest not apply yet, since there is no way in the API to empty a configuration value. Only here #446, right?

Correct, and in the SmallRye implementation. Once we have a few more cleanup items accomplished, I will be updating the implementation options for #446 to target 2.0.

radcortez commented 4 years ago

Ok, I think we are getting to a conclusion. I've already implemented the changes, but before I've submit, we just need to decide on what to do with nullable=true for primitives.

I think it is ok to allow it and use initialise the primitive with their default value.

emattheis commented 4 years ago

@dmlloyd I think I glossed over the point @radcortez raised earlier - are you saying we need the ability to distinguish between 'property is not configured' and 'property is explicitly emptied by a config source' when considering whether to use defaultValue?

dmlloyd commented 4 years ago

@dmlloyd I think I glossed over the point @radcortez raised earlier - are you saying we need the ability to distinguish between 'property is not configured' and 'property is explicitly emptied by a config source' when considering whether to use defaultValue?

Yes, I do, even if only for the purpose of giving space to solve #446. If we do not do so, then solving #446 will require that this solution be revisited and probably changed.

dmlloyd commented 4 years ago

Ok, I think we are getting to a conclusion. I've already implemented the changes, but before I've submit, we just need to decide on what to do with nullable=true for primitives.

I think it is ok to allow it and use initialise the primitive with their default value.

I think this might muddy the waters further with respect to distinguishing an empty value (which would be forbidden for primitives) from a missing value (which would result in a default value of 0/false to be used for primitives). I'd rather see nullable be forbidden completely for primitives for this reason, and for the simple reason that primitives cannot be null, so why should they be allowed to be nullable?

rmannibucau commented 4 years ago

Let's keep it simple. What we want is:

  1. Be able to default an injection to a value
  2. Be able to inject the default value of the type and not fail the boot cause there is no configured value
  3. (446) be able to reset a set value

As explained, 3 is about config-configsource contract so no link to this issue since this one is not about overriding a value to null but lztting null pass without a failure before, 1 and 2 are solved by NULL_VALUE already (or any of the - at least - 3 other concurrent API: nullable, placeholder, split annotation)

emattheis commented 4 years ago

Yes, I do, even if only for the purpose of giving space to solve #446.

Then I think we should align defaultValue behavior in @ConfigProperty with a corresponding Config method.

e.g.

<T> getValue(String propertyName, Class<T> propertyType, String defaultValue);

I think the CDI producer should be able to delegate directly to the Config API without special access to the underlying implementation.

That being said, I can't think of a use case where I would want to ever get null returned from that method.

rmannibucau commented 4 years ago

It goes further, as explained you must propagate it to the source with optional return types. Otherwise +1 for a getValue(ValueRequest) supporting all features of the api - we will add converters at some point, just wait ;).

emattheis commented 4 years ago

Yes, but keeping it encapsulated behind Config is a good idea, IMHO, otherwise the same rules need to be applied in the CDI layer.

radcortez commented 4 years ago

Ok, I think we are getting to a conclusion. I've already implemented the changes, but before I've submit, we just need to decide on what to do with nullable=true for primitives. I think it is ok to allow it and use initialise the primitive with their default value.

I think this might muddy the waters further with respect to distinguishing an empty value (which would be forbidden for primitives) from a missing value (which would result in a default value of 0/false to be used for primitives). I'd rather see nullable be forbidden completely for primitives for this reason, and for the simple reason that primitives cannot be null, so why should they be allowed to be nullable?

Nevermind. I got the idea that CDI would inject the default value on a native if the value was null via producer or something else. It does not: https://docs.jboss.org/cdi/spec/1.1.EDR1/html_single/#null

It will just fail. So I guess we can specify that the implementation needs to validate and throw a DeploymentException stating that that combination is not supported. Or, we can just let the CDI implementation handle it and fail anyway.

emattheis commented 4 years ago

@radcortez https://docs.jboss.org/cdi/spec/1.2/cdi-spec.html#primitive_types_and_null_values

Must have changed from 1.1. to 1.2 😛

radcortez commented 4 years ago

Ops... that what happens when you search on Google for the spec and don't take the version into account. Silly me!

radcortez commented 4 years ago

So, I believe @dmlloyd and @emattheis would prefer to disable nullable=true for primitives. I think we should allow it to make it consistent with CDI. Any other thoughts?

emattheis commented 4 years ago

Seems like maybe we're back to square one on this. The original request is really about avoiding the deployment time validation for non-optional types. Today, that is equivalent to checking for null from the underlying config and whether or not a default is supplied at the injection point. That may not be the case down the road when #446 is resolved.

So, optional=true is confusing in light of Optional<T> support, and nullable=true lead us down this rabbit hole.

How about required=false and we sidestep #446 concerns?

By default, required=true so this fails at deployment if the property is missing (same as today):

@Inject @ConfigProperty(name="foo") String foo;

But this will not (even after #446):

@Inject @ConfigProperty(name="foo", required=false) String foo;

And is equivalent from a value perspective to this (today, and post #446):

@Inject @ConfigProperty(name="foo") Optional<String> foo;
Emily-Jiang commented 4 years ago

@Inject @ConfigProperty(name="someint", nullable=true) int/Integer some; if the property does not exist, maybe we should throw DeploymentException as nullable=true is not applicable.

Emily-Jiang commented 4 years ago

`java.lang.OptionalDouble java.lang.OptionalInt java.lang.OptionalLong

These are the specialised Optional for primitive types. I don't think others exist.`

@radcortez nullable is different from Optional.

radcortez commented 4 years ago

`java.lang.OptionalDouble java.lang.OptionalInt java.lang.OptionalLong

These are the specialised Optional for primitive types. I don't think others exist.` @radcortez nullable is different from Optional.

Sorry wrong issue, this was meant to #513

Emily-Jiang commented 4 years ago

@emattheis @Inject @ConfigProperty(name="foo") String foo; means that the property is mandatory. If you add defaultValue, it will make it optional.

emattheis commented 4 years ago

Sure, but we're back to arguing the semantics of optional. Now that I understand the motivations of #446 better, nullable=true seems like the worst option 😛

If optionality dictates whether we validate at deployment time, then optional=true makes perfect sense to me despite earlier objection in the thread above.

We can just specify that the default is optional=false but it will be implicitly set to true if the injection target is an optional type.

rmannibucau commented 4 years ago

Think @emattheis has a point. Once core API is fixed, CDI layer would be simpler to do cleanly instead of doing the opposite. Let's try to fix our core then come back on this one.

radcortez commented 4 years ago

I'm starting to think that we should retract this from 1.4 and actually do a breaking change for 2.0 and allow injection of null by default without any use of additional annotation configuration.

rmannibucau commented 4 years ago

I agree Roberto, was an original error IMO, in particular when config can be reloaded these days (cloud etc).

dmlloyd commented 4 years ago

That's a fundamental question of "avoid null in the API and CDI usages" i.e. "null is an anti-pattern" versus "avoid Optional in the API and CDI usages in favor of null" i.e. "Optional is an anti-pattern".

This is dangerously close to being a self-defeating, unending discussion in the vein of the "C vs C++" argument that has been raging on usenet for the past 20+ years. I've been around for long enough to know that neither side is "right" or "wrong". Traditionally this specification has favored Optional for end-user APIs, and I think it makes sense for this reason to stay with this approach (considering also that Optional fits in well with the JDK's streaming APIs and other usages, in addition to the historical usage in this specification).

If this specification had historically used null instead, then I would have the opposite opinion. I don't think that hatred for null or hatred for Optional is sufficient to justify one or the other. I think that supporting both only adds a layer of confusion. Nevertheless as long as the proposal remained amicable I for one was willing to entertain it fairly. But so far the discussion has been quite contentious. Therefore I think that we should just proceed with the revert and try restarting the discussion later.

The revert is justified because the original change was made without appropriate process, so consensus is (IMO) no more needed for the revert than it was for the original change.

dmlloyd commented 4 years ago

Sure, but we're back to arguing the semantics of optional. Now that I understand the motivations of #446 better, nullable=true seems like the worst option 😛

If optionality dictates whether we validate at deployment time, then optional=true makes perfect sense to me despite earlier objection in the thread above.

We can just specify that the default is optional=false but it will be implicitly set to true if the injection target is an optional type.

Can we at least agree that the semantics of nullable (or whatever we call it) would be exactly the same as if the type of the property were Optional? Which is to say, any case where an Optional value would be empty, a nullable property would be null?

emattheis commented 4 years ago

Can we at least agree that the semantics of nullable (or whatever we call it) would be exactly the same as if the type of the property were Optional? Which is to say, any case where an Optional value would be empty, a nullable property would be null?

And if so, that goes to your point about whether or not we support both null and Optional in MicroProfile Config in general. That's certainly the crux of the issue since the original ask is to have the injection equivalent of Config#getValue produce null instead of throwing an exception at deploy time.

Also, for the record, I don't view this thread as overly contentious and I understand the problem space much better having participated. If my comments seem contentious to other participants, I do apologize, and thanks for everyone for sticking with it!

emattheis commented 4 years ago

@rmannibucau I'm curious to know if your motivation for raising the issue was because an injected field was actually going to end up as null, or it was just null at deploy time and you need to bypass the validation?

The latter case is one I'm particularly interested in and it seems that maybe what we really need in the injection layer is not a null-handling solution, but a way to indicate whether deployment time validation is to be performed.

If we don't validate at deploy time, you have until the bean is instantiated to provide the config. Of course, we can't guarantee that NoSuchElementException won't occur during injection, but we can't really guarantee that today anyway in the face of mutable sources.

Personally, I advocate for deploy time validation going away entirely.

dmlloyd commented 4 years ago

Can we at least agree that the semantics of nullable (or whatever we call it) would be exactly the same as if the type of the property were Optional? Which is to say, any case where an Optional value would be empty, a nullable property would be null?

And if so, that goes to your point about whether or not we support both null and Optional in MicroProfile Config in general. That's certainly the crux of the issue since the original ask is to have the injection equivalent of Config#getValue produce null instead of throwing an exception at deploy time.

Precisely. I contend that we do not need two distinct mechanisms for optionality. If the problem is deployment time validation, then let's solve that problem. If the problem is that we really hate Optional and regret its usage in MP Config, let's tackle that directly instead.

In other words, let's make absolutely certain that this feature request is not a proxy for some other problem. It certainly seems like an XY problem the more we discuss it.

Again, either way we must revert this commit because it was merged without consensus.

rmannibucau commented 4 years ago

I raised this issue for the validation blocker requiring a serious number of config entries to have a defaultValue=fakeDefault instead of just letting null being handled. Optional is not an option cause it is not serializable + half of the time didnt match the expected injection type - libraries signatures and i didnt want a postconstruct just to unwrap all my conf.

dmlloyd commented 4 years ago

Have you tried using javax.inject.Provider for your configuration properties?

rmannibucau commented 4 years ago

It is not an option since by spec it requires too much work (resolution and reinstantiation, it is as redoing the injection) - no cache - so it is too slow for any runtime having some serious config. But it would have been for an one time startup configuration usage.

Emily-Jiang commented 4 years ago

ok. Let's revert the change and do a white board design after Config 1.4 is out! I will move this out of the milestone release and tidy up the release note.