Search code examples
javaspringspring-bootgenericsspring-ioc

Spring Inject Generic Type


Say we have an interface like this one:

public interface Validator<T> {
  boolean isValid(T data);
}

And this is part of a core module. Multiple apps can use that same core module, with a different value for the generic T. An example implementation is (from a specific module of the application):

@Component
public class AppValidator implements Validator<String> {
  @Override
  public boolean isValid(String data) {
    return false;
  }
}

And then in the controller (which is part of the core module):

@RestController
public class ValidateController {
  @Autowired
  private Validator validator;

  @RequestMapping("/")
  public void index() {
    validator.validate("");
  }
}

IntelliJ is complaining that I'm using raw types; as you can see, I'm actually doing that in the controller.

My question: is there a way to inject the dependency in a bounded way (instead of injecting Validator, injecting Validator<String>)? But of course, the bound type could change depending on the application using the core module?

If not possible (probably due to type erasure), what's the best practice for this? Is it just to use Object? Is there no nicer alternative that still provides type-safety?

I've seen somewhere people saying it's possible to do some magic at compile-time to change the types, but I'm not sure how, or even if I read it correctly?

I am using Spring, so I'm hoping Spring can provide something to help me here! Some magic is welcome!


Solution

  • Here the relationships between your modules (applications and core) :

    Application 1       Application 2      Application 3
         |                   |                   |
    Validator<Foo>     Validator<Bar>     Validator<FooBar>
         |                   |                   |  
         |                   |                   |  
         |__ __ __ __ __ __ _| __ __ __ __ __ __ | 
                             |
                             | <<uses>>
                             |
                            \ /
                         Core Module    
                             |
                     ValidateController  (not generic rest controller)                   
    

    Something is wrong here since you want that a shared component ValidateController relies on a specific application generic Validator class but ValidateController is not a generic class, so you could only stick with Object as generic type where you will use the Validator field.
    To make things consistent, you should create this missing link. In fact you need distinct subclasses of controller because each controller needs to use a specific instance of validator.
    You could for example define an abstract class/interface ValidateController in the shared/code module and leave each subclass extends it and defines itself the generic Validator class to use.

    Here the target relationships between your modules :

    Application 1        Application 2        Application 3
         |                   |                      |
    Validator<Foo>       Validator<Bar>       Validator<FooBar>
    FooController(bean)  BarController(bean)  FooBarController(bean)
         |                   |                      |  
         |                   |                      |  
         |__ __ __ __ __ ___ | __ ___ __ __ __ __ __| 
                             |
                             | <<uses>>
                             |
                            \ /
                         Core Module    
                             |
                     ValidateController<T>  (abstract class and not a bean)                   
    

    For example in the core/shared module :

    public abstract class ValidateController<T> {
    
      private Validator<T> validator;
    
      ValidateController(Validator<T> validator){
         this.validator = validator;
      }
    
      @RequestMapping("/")
      public void index(T t) {
        boolean isValid = validator.validate(t);
      }
    
    }
    

    In the application, define your validator implementation :

    @Component
    public class AppValidator implements Validator<String> {
      @Override
      public boolean validate(String data) {
        return ...;
      }
    }
    

    And define also the StringController subclass (or @Bean as alternative) to set the right Validator :

    @RestController
    public class StringController extends ValidateController<String>{
    
       public ValidateControllerApp(Validator<String> validator){
           this.validator = validator;
       }
    
    }