Skip to content
New issue

Have a question about this project? Sign up for a free GitHub account to open an issue and contact its maintainers and the community.

By clicking “Sign up for GitHub”, you agree to our terms of service and privacy statement. We’ll occasionally send you account related emails.

Already on GitHub? Sign in to your account

Support Optional with null-safe and Elvis operators in SpEL expressions #20433

Closed
spring-projects-issues opened this issue Aug 18, 2017 · 17 comments
Assignees
Labels
in: core Issues in core modules (aop, beans, core, context, expression) type: enhancement A general enhancement
Milestone

Comments

@spring-projects-issues
Copy link
Collaborator

Peter Luttrell opened SPR-15878 and commented

This is a feature request to add support for Java 8 Optionals to the Spring Expression Language.

One use case that I just ran into is wanting to use @PostAuthorize on a method that returns an Optional in conjunctions with custom expressions. For example the following fails:

@PostAuthorize("canAccessOrganization(returnObject.organiztionId)")
public Optional<Person> getPerson(String personId){
    ...
}

In this case, if the returned reference isn't present, that @PostAuthorize would allow the response, which should be Optional.empty(). If it is present, then it'd be dereferenced into the returnObject, so we'd have direct access to its fields for use in the expression.


4 votes, 6 watchers

@spring-projects-issues
Copy link
Collaborator Author

Juergen Hoeller commented

Rob Winch, I figure this might have to be handled in Spring Security's authorization interceptor, specifically detecting an Optional return value there and unwrapping it?

@spring-projects-issues
Copy link
Collaborator Author

Rob Winch commented

Thanks for the report Peter Luttrell

. For example the following fails:

This does fail because Optional does not have a method of getOrganizationId() on it.

In this case, if the returned reference isn't present, that @PostAuthorize would allow the response,

This is not true. In either case, the PostAuthorize will fail because Optional does not have a method getOrganizationId()

Juergen Hoeller I don't think it really makes sense to automatically unwrap Optional. If the method signature is Optional, then returnType should be Optional. If Optional is automatically unwrapped, then how would someone use Optional return types?

Users can always do something like @PostAuthorize("canAccessOrganization(returnObject.orElse(null)?.organiztionId)"). Finally, if someone really wants to automatically unwrap the returnValue when it is Optional, they can override DefaultMethodSecurityExpressionHandler.setReturnObject.

@spring-projects-issues
Copy link
Collaborator Author

Mohamed Amine Mrad commented

Hello,
I was not able to create an issue.
I have a suggestion to add here:
In fact SpEL should support writing an Optional properly like writing an Enum. There is an EnumToStringConverter
added to DefaultConversionService method addScalarConverters
There should be an OptionalToStringConverter also.
I'm using thymeleaf and I'm struggling with the ugly get() in HTML.

@spring-projects-issues
Copy link
Collaborator Author

Stéphane Toussaint commented

I have another use case for this feature request.

I use Spring Integration and some of my service layer bean methods are now returning Optional generic types.

This is a sample of two successive service-activator call, the payload is the Optional<Person> return from retrieveByUsername.

<int:service-activator expression="@personService.retrieveByUsername(headers.username)" />
<int:service-activator expression="@personService.doWithPerson(payload)" />

The doWithPerson method has not changed ; still waiting for a Person.

Class PersonService {
  public void doWithPerson(Person person) {
    ...
  }
}

The retrieveByUsername however now returns an Optional of Person

Class PersonService {
  public Optional<Person> retrieveByUsername(String username) {
    return Optional.of(...)
  }
}

Now Spring Integration (actually the Spring Expression Evaluator) complains it can't find the targeted method with a message like :

Expression evaluation failed: @personService.doWithPerson(payload); nested exception is org.springframework.expression.spel.SpelEvaluationException: EL1004E: Method call: Method doWithPerson(java.util.Optional) cannot be found on PersonService.

Will an OptionalToObjectConverter be feasible ? Maybe it can be possible to rely on Nullable annotation on the target method to handle the Optional.empty() case ?(return null or throw ?).

@spring-projects-issues spring-projects-issues added status: waiting-for-triage An issue we've not yet triaged or decided on type: enhancement A general enhancement in: core Issues in core modules (aop, beans, core, context, expression) and removed type: enhancement A general enhancement labels Jan 11, 2019
@wimdeblauwe
Copy link

wimdeblauwe commented Mar 9, 2020

Another use case is with @PreAuthorize. Assume a Task has a reference to a User and there is a TaskServiceImpl class with:

    Optional<Task> getTask(int taskId);

The controller could check if the task is linked to the authorized user like this:

    @DeleteMapping("/{taskId}")
    @PreAuthorize("@taskServiceImpl.getTask(#taskId).orElse(null)?.user.id == #userDetails.id")
    public String destroy(@AuthenticationPrincipal ApplicationUserDetails userDetails,
                          @PathVariable("taskId") Integer taskId) {
        return "redirect:/tasks";
    }

The orElse(null) is perfect for this, not sure if anything else is needed, but just wanted to let you know about this use case.

@sanatik
Copy link

sanatik commented Jul 16, 2020

My use case would be with @Cacheable

@Cacheable(unless = "#result.isEmpty()")
public Optional<User> getUserById(final String userId);

@dkfellows
Copy link

At the very least, @PreFilter and @PostFilter ought to work with Optional as if they were collections containing at most one object. If the filter sees a non-empty optional but doesn't want to let the contained object through, that becomes an empty optional; the obvious semantics.

@OrangeDog
Copy link

This also applies to Thymeleaf templating, which uses SpEL in the default Boot configuration.

@cheatmenot
Copy link

+1

@jhoeller jhoeller added status: declined A suggestion or change that we don't feel we should currently apply and removed status: waiting-for-triage An issue we've not yet triaged or decided on labels Dec 29, 2023
@jhoeller jhoeller closed this as not planned Won't fix, can't repro, duplicate, stale Dec 29, 2023
@lauroschuck
Copy link

This is causing me so much trouble now! I created a custom company lib that would benefit a lot from SpEL, and this is essentially making it useless because a lot of our codebase has a bias towards using Optional.

@jhoeller Why declined? Why not planned? This sounds like such a resonable request, so many people here voicing their support, and probably very natural do implement!

@drekbour
Copy link

Indeed. Lack of this caused us to drop SpEL long ago as the basis for something.

@sbrannen sbrannen self-assigned this Feb 27, 2025
@sbrannen sbrannen added type: enhancement A general enhancement and removed status: declined A suggestion or change that we don't feel we should currently apply labels Feb 27, 2025
@sbrannen sbrannen added this to the 7.0.0-M3 milestone Mar 3, 2025
@sbrannen sbrannen reopened this Mar 3, 2025
@sbrannen sbrannen changed the title Add Support for Java 8 Optional to the Spring Expression Language [SPR-15878] Support null-safe operator for Optional in SpEL expressions Mar 3, 2025
@sbrannen sbrannen changed the title Support null-safe operator for Optional in SpEL expressions Support Optional with null-safe and Elvis operators in SpEL expressions Mar 5, 2025
@sbrannen sbrannen modified the milestones: 7.0.0-M3, 7.0.x Mar 5, 2025
sbrannen added a commit to sbrannen/spring-framework that referenced this issue Mar 5, 2025
@sbrannen
Copy link
Member

sbrannen commented Mar 5, 2025

After further consideration, we have decided to introduce first-class support for Optional in SpEL expressions in Spring Framework 7.0. This aligns nicely with our overall null-safety efforts with JSpecify annotations and Kotlin improvements.

The discussions in this issue have presented two separate use cases.

  1. transparent unwrapping of Optional for property and field access
  2. transparent unwrapping on Optional when invoking a constructor, method, or function

After experimentation, I have determined that supporting Optional with the null-safe and Elvis operators would address use case #1.

To address use case #2, I have created #34544 to introduce an OptionalToObjectConverter.

@sbrannen
Copy link
Member

sbrannen commented Mar 5, 2025

After experimentation, I have determined that supporting Optional with the null-safe and Elvis operators would address use case #1.

A proof of concept can be viewed in the following feature branch.

main...sbrannen:spring-framework:issues/gh-20433-spel-null-safe-elvis-optional

That allows the original use case:

@PostAuthorize("canAccessOrganization(returnObject.organizationId)")
public Optional<Person> getPerson(String personId){
    // ...
}

... to be replaced with:

@PostAuthorize("canAccessOrganization(returnObject?.organizationId)")
public Optional<Person> getPerson(String personId){
    // ...
}

If returnObject is either null or an empty Optional, null would be passed to the canAccessOrganization() invocation.

If returnObject is not null and not an Optional, returnObject.organizationId would be passed to the canAccessOrganization() invocation.

If returnObject is a non-null Optional, returnObject.get().organizationId would be passed to the canAccessOrganization() invocation, transparently unwrapping the Optional.

@wimdeblauwe, you should then be able to rewrite your example as follows.

    @DeleteMapping("/{taskId}")
    @PreAuthorize("@taskServiceImpl.getTask(#taskId)?.user?.id == #userDetails.id")
    public String destroy(@AuthenticationPrincipal ApplicationUserDetails userDetails,
                          @PathVariable("taskId") Integer taskId) {
        return "redirect:/tasks";
    }

@OrangeDog
Copy link

OrangeDog commented Mar 5, 2025

returnObject.organizationId would be passed to the canAccessOrganization() invocation.

Do you mean returnObject.get().organizationId?

@taskServiceImpl.getTask(#taskId)?.user.id

Do you mean @taskServiceImpl.getTask(#taskId)?.user?.id ?

@sbrannen
Copy link
Member

sbrannen commented Mar 5, 2025

returnObject.organizationId would be passed to the canAccessOrganization() invocation.

Do you mean returnObject.get().organizationId?

No, in that particular example returnObject is "not an Optional".

@taskServiceImpl.getTask(#taskId)?.user.id

Do you mean @taskServiceImpl.getTask(#taskId)?.user?.id ?

Yes, that would indeed be better. I was merely modifying the original example to make use of the proposed new feature. But I'll update that to use null-safe navigation throughout the compound expression.

sbrannen added a commit to sbrannen/spring-framework that referenced this issue Mar 7, 2025
This commit introduces null-safe support for java.util.Optional in the
following SpEL operators:

- Elvis
- Indexer
- PropertyOrFieldReference

Operators not yet supported:

- MethodReference
- Projection
- Selection

See spring-projectsgh-20433
@sbrannen sbrannen modified the milestones: 7.0.x, 7.0.0-M3 Mar 7, 2025
sbrannen added a commit to sbrannen/spring-framework that referenced this issue Mar 7, 2025
This commit introduces null-safe support for java.util.Optional in the
following SpEL operators:

- Elvis
- Indexer
- PropertyOrFieldReference

TODO: support the following operators:

- MethodReference
- Projection
- Selection

TODO: update reference manual

Closes spring-projectsgh-20433
sbrannen added a commit to sbrannen/spring-framework that referenced this issue Mar 7, 2025
This commit introduces null-safe support for java.util.Optional in the
following SpEL operators:

- Elvis
- Indexer
- MethodReference
- PropertyOrFieldReference

TODO: support the following operators:

- Projection
- Selection

TODO: update reference manual

Closes spring-projectsgh-20433
sbrannen added a commit to sbrannen/spring-framework that referenced this issue Mar 7, 2025
This commit introduces null-safe support for java.util.Optional in the
following SpEL operators:

- Elvis
- Indexer
- MethodReference
- PropertyOrFieldReference
- Projection

TODOs:

- Support the Selection operator.
- Update the SpEL chapter of the reference manual.

Closes spring-projectsgh-20433
sbrannen added a commit to sbrannen/spring-framework that referenced this issue Mar 7, 2025
This commit introduces null-safe support for java.util.Optional in the
following SpEL operators:

- Elvis
- Indexer
- MethodReference
- PropertyOrFieldReference
- Projection
- Selection

TODO: Update the SpEL chapter of the reference manual.

Closes spring-projectsgh-20433
sbrannen added a commit to sbrannen/spring-framework that referenced this issue Mar 10, 2025
This commit introduces null-safe support for java.util.Optional in the
following SpEL operators:

- Elvis
- Indexer
- MethodReference
- PropertyOrFieldReference
- Projection
- Selection

TODO: Update the SpEL chapter of the reference manual.

Closes spring-projectsgh-20433
sbrannen added a commit to sbrannen/spring-framework that referenced this issue Mar 11, 2025
This commit introduces null-safe support for java.util.Optional in the
following SpEL operators:

- Elvis
- Indexer
- MethodReference
- PropertyOrFieldReference
- Projection
- Selection

TODO: Update the SpEL chapter of the reference manual.

Closes spring-projectsgh-20433
sbrannen added a commit to sbrannen/spring-framework that referenced this issue Mar 12, 2025
This commit introduces null-safe support for java.util.Optional in the
following SpEL operators:

- Elvis
- Indexer
- MethodReference
- PropertyOrFieldReference
- Projection
- Selection

TODO: Update the SpEL chapter of the reference manual.

Closes spring-projectsgh-20433
sbrannen added a commit to sbrannen/spring-framework that referenced this issue Mar 12, 2025
This commit introduces null-safe support for java.util.Optional in the
following SpEL operators:

- PropertyOrFieldReference
- MethodReference
- Indexer
- Projection
- Selection
- Elvis

Specifically, when a null-safe operator is applied to an empty
`Optional`, it will be treated as if the `Optional` were `null`, and
the subsequent operation will evaluate to `null`. However, if a
null-safe operator is applied to a non-empty `Optional`, the subsequent
operation will be applied to the object contained in the `Optional`,
thereby effectively unwrapping the `Optional`.

For example, if `user` is of type `Optional<User>`, the expression
`user?.name` will evaluate to `null` if `user` is either `null` or an
empty `Optional` and will otherwise evaluate to the `name` of the
`user`, effectively `user.get().getName()` for property access.

Note, however, that invocations of methods defined in `Optional` are
still supported on an empty `Optional`. For example, if `name` is of
type `Optional<String>`, the expression `name?.orElse('Unknown')` will
evaluate to `Unknown` if `name` is an empty `Optional` and will
otherwise evaluate to the `String` contained in the `Optional` if
`name` is a non-empty `Optional`, effectively `name.get()`.

Closes spring-projectsgh-20433
sbrannen added a commit that referenced this issue Mar 12, 2025
This is a prerequisite for null-safe Optional support.

See gh-20433
sbrannen added a commit that referenced this issue Mar 12, 2025
This is a prerequisite for null-safe Optional support.

See gh-20433
sbrannen added a commit to sbrannen/spring-framework that referenced this issue Mar 12, 2025
This commit introduces null-safe support for java.util.Optional in the
following SpEL operators:

- PropertyOrFieldReference
- MethodReference
- Indexer
- Projection
- Selection
- Elvis

Specifically, when a null-safe operator is applied to an empty
`Optional`, it will be treated as if the `Optional` were `null`, and
the subsequent operation will evaluate to `null`. However, if a
null-safe operator is applied to a non-empty `Optional`, the subsequent
operation will be applied to the object contained in the `Optional`,
thereby effectively unwrapping the `Optional`.

For example, if `user` is of type `Optional<User>`, the expression
`user?.name` will evaluate to `null` if `user` is either `null` or an
empty `Optional` and will otherwise evaluate to the `name` of the
`user`, effectively `user.get().getName()` for property access.

Note, however, that invocations of methods defined in the `Optional`
API are still supported on an empty `Optional`. For example, if `name`
is of type `Optional<String>`, the expression `name?.orElse('Unknown')`
will evaluate to "Unknown" if `name` is an empty `Optional` and will
otherwise evaluate to the `String` contained in the `Optional` if
`name` is a non-empty `Optional`, effectively `name.get()`.

Closes spring-projectsgh-20433
@sbrannen
Copy link
Member

This support will be available in the upcoming Spring Framework 7.0 M3 release.

For details, check out the updated sections of the reference manual.

Feedback is welcome! 😎

sbrannen added a commit that referenced this issue Mar 29, 2025
We have had an ObjectToOptionalConverter since Spring Framework 4.1;
however, prior to this commit we did not have a standard Converter for
the inverse (Optional to Object).

To address that, this commit introduces an OptionalToObjectConverter
that unwraps an Optional, using the ConversionService to convert the
object contained in the Optional (potentially null) to the target type.

This allows for conversions such as the following.

- Optional.empty()                             -> null
- Optional.of(42) with Integer target          -> 42
- Optional.of(42) with String target           -> "42"
- Optional.of(42) with Optional<String> target -> Optional.of("42")

The OptionalToObjectConverter is also registered by default in
DefaultConversionService, alongside the existing
ObjectToOptionalConverter.

See gh-20433
Closes gh-34544
@sbrannen
Copy link
Member

The discussions in this issue have presented two separate use cases.

1. transparent unwrapping of `Optional` for property and field access

2. transparent unwrapping on `Optional` when invoking a constructor, method, or function

To address use case #2, I have created #34544 to introduce an OptionalToObjectConverter.

FYI: I introduced an OptionalToObjectConverter in 8379ac7, which is registered by default with the DefaultConversionService.

@stoussaint, it would be great if you could give it a try to see if it addresses your needs. You can experiment with in 7.0 snapshots.

Sign up for free to join this conversation on GitHub. Already have an account? Sign in to comment
Labels
in: core Issues in core modules (aop, beans, core, context, expression) type: enhancement A general enhancement
Projects
None yet
Development

No branches or pull requests

10 participants