eclipse-archived/ceylon

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.

There you go, @bjansen, done! HTH.

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.

xkr47 commented

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 ;-)

xkr47 commented

So can you annotate i.e. whether Ceylon class annotations end up getting attached to the class, the constructor or both?

@xkr47 You can, but it's been designed so that that should never be necessary.