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
Comment From: spring-projects-issues
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?
Comment From: spring-projects-issues
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
.
Comment From: spring-projects-issues
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.
Comment From: spring-projects-issues
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\
<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 ?).
Comment From: wimdeblauwe
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.
Comment From: sanatik
My use case would be with @Cacheable
@Cacheable(unless = "#result.isEmpty()")
public Optional<User> getUserById(final String userId);
Comment From: dkfellows
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.
Comment From: OrangeDog
This also applies to Thymeleaf templating, which uses SpEL in the default Boot configuration.
Comment From: cheatmenot
+1
Comment From: lauroschuck
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!
Comment From: drekbour
Indeed. Lack of this caused us to drop SpEL long ago as the basis for something.
Comment From: sbrannen
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.
- transparent unwrapping of
Optional
for property and field access - 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
.
Comment From: sbrannen
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.
https://github.com/spring-projects/spring-framework/compare/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";
}
Comment From: OrangeDog
returnObject.organizationId
would be passed to thecanAccessOrganization()
invocation.
Do you mean returnObject.get().organizationId
?
@taskServiceImpl.getTask(#taskId)?.user.id
Do you mean @taskServiceImpl.getTask(#taskId)?.user?.id
?
Comment From: sbrannen
returnObject.organizationId
would be passed to thecanAccessOrganization()
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.
Comment From: sbrannen
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! 😎