Use the @Nullable annotation on fields too
Closed this issue · 18 comments
Spring 5 (currently in M4) is going to support Kotlin's nullable types when injecting beans. Under the hood, it will look for any @Nullable
annotation on autowired fields. That's great, except it doesn't work on Ceylon declarations, because we put @Nullable
on $priv$
getters but not on the actual field for code like this:
class MyClass() {
autowired late IService? myService;
}
In such classes, Spring will use the field because there's no public getter (only a getMyService$priv()$
method).
It might be interesting to add another @Nullable
annotation on the field to leverage that new feature in Spring core.
Unfortunately it's not enough to make Spring read the annotation, because it's currently @Retention(CLASS)
instead of @Retention(RUNTIME)
, so it won't be readable via field.getAnnotations()
.
I'm not sure where else this @Nullable
annotation is actually used so IDK if it's safe to change its retention :/
Here's a sample in case someone wants to reproduce: https://github.com/bjansen/ceylon-spring5
And now I realize that autowired late String? foo
will likely throw a Accessing uninitialized 'late' attribute 'foo'
when there's no bean candidate for Spring to inject :(
OK, it can be solved using constructor injection, which the preferred way in Spring anyway.
I changed the retention policy to RUNTIME
, otherwise Nullable
and NonNull
are basically useless because they can't be accessed using reflection (they would be accessible by reading the bytecode directly though).
My Spring 5 demo is now working correctly: using constructor injection, Spring 5 will now detect nullable types and inject null
if no matching bean is found. Neat :)
Thanks @gavinking !
@bjansen But do we even need the annotations on the fields if you're using constructor injection??
I'd have to take a look at how Spring decides which dependencies are optional and which are mandatory, but I believe it's gathering metadata from fields (or getters/setters if present), even when injection is done using constructors.
OK, fine, so it's useful, great. Then I won't roll back my work :-)
I did a little investigation, and it turns out that even when I use constructor injection, Spring will in fact do field-based injection. That's because both of the following Ceylon variants:
component class MyController(autowired MyRepository repository,
autowired NotABean? noBeanCandidate) {
shared void check() {
print(repository);
print(noBeanCandidate);
}
}
component class MyController(repository, noBeanCandidate) {
autowired MyRepository repository;
autowired NotABean? noBeanCandidate;
shared void check() {
print(repository);
print(noBeanCandidate);
}
}
will result in Java fields annotated @Autowired
in the bytecode. The constructors will have @Nullable / @NonNull
parameters, but in the end Spring will look for the @Autowired
annotation so it will do field-based injection.
No need to revert your change then :)
@bjansen I think that's just because you're supposed to annotate the class or constructor, not the parameter.
@Autowired
is declared @Target({CONSTRUCTOR,FIELD,METHOD})
. So if you stick it on the parameter, the annotation will be added to the field. But if you stick it on the class or constructor it will be added to the class or constructor, which is what you probably wanted.
Coincidentally both work?
Coincidentally both work?
I guess.
Huh, it never occurred to me that it was possible to use @Autowired
on methods...
And I also learned that if I write autowired component class MyController(...)
, it will generate a @Component
class with an @Autowired
constructor, which is neat!
This is much more readable than multiple autowired fields imo.
And I also learned that if I write
autowired component class MyController(...)
, it will generate a@Component
class with an@Autowired
constructor, which is neat!
Yep, we thought this through ;-)
So can you annotate i.e. whether Ceylon class annotations end up getting attached to the class, the constructor or both?