3

I've got small question. It's probably trivial, but I've never faced it before.

I have generic interface and a generic implementation of it. I wanted to autowire it, but an error occured. Here are the details:

Interface

@Service
public interface Serializing<T extends Serializable> {
    String serialize(T toBeSerialized);

    T deserialize(String toBeDeserialized, Class<T> resultType);
}

Implementation

@Service
public class JsonSerializer<T extends Serializable> implements Serializing<T> {
   /** code **/
}

Autowire attempt

private NoteDAO noteDAO;

@Qualifier("jsonSerializer")
private Serializing<UserComment> serializer;

@Autowired
public NoteController(NoteDAO noteDAO, Serializing<UserComment> serializer) {
    this.noteDAO = noteDAO;
    this.serializer = serializer;
}

Error

Parameter 1 of constructor in somepackagepath.NoteController required a bean of type 'anotherpackagepath.Serializing' that could not be found.

I want to keep it as simple as possible. I've check the web, but I've found only about defining my exact beans in configuration. I prefer to avoid it if possible.

Peteef
  • 377
  • 1
  • 2
  • 10

3 Answers3

10

In your specific case, Spring doesn't allow to use a generic type to be wired as dependency such as :

@Autowired
public NoteController(NoteDAO noteDAO, Serializing<UserComment> serializer) {
    this.noteDAO = noteDAO;
    this.serializer = serializer;
}

for a very simple reason : consistency.
This dependency that you made a Spring bean with @Service :

@Service
public class JsonSerializer<T extends Serializable> implements Serializing<T> {
   /** code **/
}

can be wired into other beans.

Imagine that the beans that depend on the Serializing instance don't use the same generic : Serializing<UserComment> in Foo and Serializing<UserQuestion> in Bar such as :

public class Foo{

    @Autowired
    public Foo(NoteDAO noteDAO, Serializing<UserComment> serializer) {
        this.noteDAO = noteDAO;
        this.serializer = serializer;
    }

}
public class Bar{

    @Autowired
    public Bar(NoteDAO noteDAO, Serializing<UserQuestion> serializer) {
        this.noteDAO = noteDAO;
        this.serializer = serializer;
    }

}

Here the Serializing object is the same but each bean declares a distinct generic on.
So it would break the type safety of the generic type.


In fact, erasure of the generics is not the real problem there as Spring (from Spring 4) owns a Resolver able to resolve the type :

behind the scenes, the new ResolvableType class provides the logic of actually working with generic types. You can use it yourself to easily navigate and resolve type information. Most methods on ResolvableType will themselves return a ResolvableType

And before Spring 4 you also had other workaround to accept generic types in the bean dependencies.
The real problem is that you annotated a generic class with @Service to make it a bean while it is an instance of this generic class that has to be configured as a bean.

So to achieve what you want to do, declare the JsonSerializer beans that you want to instantiate in a @Configuration class :

@Configuration
public class SerializingBeans {

    @Bean
    public JsonSerializer<UserComment> userCommentSerializer() {
        return new JsonSerializer<UserComment>();
    }

    @Bean
    public JsonSerializer<UserAnswer> userAnswerSerializer() {
        return new JsonSerializer<UserAnswer>();
    }
}

You can now wire the dependency as a generic type :

@Service
public class NoteController {

    private Serializing<UserComment> userCommentSerializer;

    @Autowired
    public NoteController(Serializing<UserComment> serializer) {
        this.userCommentSerializer = serializer;

    }
}
davidxxx
  • 125,838
  • 23
  • 214
  • 215
4

It's related to java's type erasure : https://docs.oracle.com/javase/tutorial/java/generics/erasure.html

In java, you can'nt know the generic type at runtime. The only workarround is to use anonymous classes or inherited classes with non-generic type : Inject anonymous classes with spring

Oreste Viron
  • 3,592
  • 3
  • 22
  • 34
2

You have to tell Spring about all the beans you want it to autowire. No exceptions.

If you cannot spell it out beforehand, then I'd say it's not a candidate for injection.

Somethings you need to have full control and call new. Maybe your case is one of those. There's nothing wrong with that; it just means that Spring DI isn't appropriate for that use case.

duffymo
  • 305,152
  • 44
  • 369
  • 561
  • only work around is to create a subclass right? `class UserCommentSerializer extends JsonSerializer { ... }` and then inject that into the constructor – Lino Jun 22 '18 at 11:31
  • Maybe. I'm not sure based on what has been posted. – duffymo Jun 22 '18 at 11:33
  • As I said, I've never faced this kind of situation. I thought that Spring is able to recognize param type by specified interface. To be clear, I've got enabled component scanning. – Peteef Jun 22 '18 at 11:34
  • 1
    Like they said above: type erasure is your problem. I wonder why you're going to all this trouble when the Jackson JSON library will handle all this for you. That's the standard for JSON serialization/deserialization in Spring Boot. – duffymo Jun 22 '18 at 11:35
  • To be honest I'm using Jackson library inside. It's about flexibility to change serializing strategy easily. – Peteef Jun 22 '18 at 11:37
  • 1
    You know your problem better than I do. I can't help but think you're designing for a situation that is highly unlikely. Once you annotate your classes properly Jackson will sort them out and you won't be changing them. Do the simple thing. YAGNI. – duffymo Jun 22 '18 at 11:38
  • I'm glad. Good luck. – duffymo Jun 22 '18 at 11:42
  • 1
    @Lino It is an acceptable workaround before Spring 4. Declaring the beans with the generic types in a configuration classes is a more flexible way. – davidxxx Jun 22 '18 at 12:09