0

I have a simple FooBarRepository interface. I have a single implementation called DefaultFooBarRespository, which looks like this:

@Repository("fooBarRepository")
public class DefaultFooBarRepository implements FooBarRepository {
  …
}

I know that Spring is instantiating DefaultFooBarRepository and giving it a bean name of fooBarRepository, because in my Thymeleaf view I can do something like:

<li th:each="fooBar: ${@fooBarRepository.getFooBars()}" …

As we all know, it's best to program to interfaces. After all I might want to wire up a different implementation of FooBarRepository at some point. So in my MVC controller FooBarController I try to inject the interface:

@GetMapping
public String getFooBars(FooBarRepository repository) {
  …
}

That results in an error:

java.lang.IllegalStateException: No primary or single unique constructor found for interface ….FooBarRepository

Spring I suppose is trying to create an instance of FooBarRepository, not realizing that the DefaultFooBarRepository it already created is an instance of FooBarRepository.

I imagine I could create a separate class annotated with @Configuration and have a method that returns an FooBarRepository. I could even probably inject the DefaultFooBarRepository instance into this method and return it as the FooBarRepository.

But why should I have to do all that? Spring is already creating a DefaultFooBarRepository, adding it to the context, giving it a bean name, etc. How can I tell Spring simply to register the DefaultFooBarRepository with the FooBarRepository interface rather than the DefaultFooBarRepository implementation? Is there another annotation or an annotation parameter I can add to DefaultFooBarRepository or something?

Update: And this is even stranger: in FooBarController I tried to inject DefaultFooBarRepository:

@GetMapping
public String getFooBars(DefaultFooBarRepository repository) {
  …
}

Then I get another error saying that one of the parameters of the DefaultFooBarRepository constructor is null—specifically a @ConfigurationProperties annotated class AppProperties. It appears that in the controller GET handler it's trying to create an instance of DefaultFooBarRepository. But Spring should have already scanned the classes and instantiated both AppProperties and DefaultFooBarRepository. And we know it did, because as I mentioned the Thymeleaf view can access @fooBarRepository.getFooBars(). What am I missing here? Why can't the controller see any of the bean classes the the view can see?

Garret Wilson
  • 18,219
  • 30
  • 144
  • 272
  • Try removing @Repository from the interface. Or add @Primary to `DefaultFooBarRepository`. And see @Conditional when there are multiple instances to choose from. – Andrew S Apr 17 '23 at 22:24
  • Hi G. , Same reason/issue as with [other question](https://stackoverflow.com/q/75860160/592355): While it is *theoretically* possible to `@Autowire` "methods" (im- vs. ex-plicitly!) ..."I never saw this" except for -getter/setter, -`@Configuration#Bean`, -"constructor" "methods"… With "handler methods" you create an ambiguity (for the handling of method parameters) So "implicit autowiring" is no option (for "handler methods") at all! ..I will/can try/test "explicit auto wiring", but have few hope.. :) – xerx593 Apr 18 '23 at 06:26
  • Also when (trying to) doing so, you have to make (at least few) considerations about "scope".... While a "controller/repository/service/..." is "singleton" by default... A "handler method" is "request[-session]-scopable" ;) – xerx593 Apr 18 '23 at 06:29

2 Answers2

0

I think the problem here is the same one in Spring MVC inject `Environment` in constructor but not in request handler method : Spring doesn't do normal injection in the controller request handlers, but instead only supports a limited number of objects. Full dependency injection only works in the constructor.

So I moved the injection to the controller constructor, and it worked fine, even for the interface:

public FooBarController(FooBarRepository fooBarRepository) {
  …
}
Garret Wilson
  • 18,219
  • 30
  • 144
  • 272
0

Hmhmh, "explicit auto-wiring" (so using the @Autowired annotation) seems NOT to help!

  • https://start.spring.io

  • and very basic sample:

    package com.example.demo;
    
    /* import org.springframework...*/
    
    @SpringBootApplication
    public class DemoApplication {
    
      public static void main(String[] args) {
       SpringApplication.run(DemoApplication.class, args);
      }
    
      @Component
      static class SomeBean {
    
      }
    
      @Controller
      static class DemoController {
       @GetMapping("/hello")
       public @ResponseBody String hello(@Autowired SomeBean someBean, String q) {
        System.err.println(someBean);
        return "Hello, " + q;
       }
      }
    }
    

... works NOT as expected! (someBean reference changes with every request ;(;(

... "SomeBean" is treated as "Controller method argument", and corresponds with (last row/entry):

Any other argument

If a method argument is not matched to any of the earlier values in this table and it is a simple type (as determined by BeanUtils#isSimpleProperty), it is resolved as a @RequestParam. Otherwise, it is resolved as a @ModelAttribute.

(spring-web populates this "param/attrib" via existing default/resolvable constructor)


Doing (e.g.) so:

@Controller
static class DemoController {

  @Autowired SomeBean someBean;

  @GetMapping("/hello")
  public @ResponseBody String hello(String q) {
    System.err.println(someBean);
    return "Hello, " + q;
  }
}

(moving someBean to an "autowired field",) "fix-/changes everything"/works as expected.


Overall Scenario

Accessing a (rarely changing) "list of values" via thymeleaf:spring-mvc

@ModelAttribute (on controller(!)/method level) is documented/recommended by both, spring and thymeleaf.

// in your @[Rest]Controller (annotated) class (to be tested with: @ControllerAdvice!=;)
@ModelAttribute("fooBars")
public List<FooBarVO> fooBars(/* also with arguments like @PathVariable, @RequestParam, ...*/) {
  return fooRepository.findAll(); // or even fooService.findAllCached()... fooRepository/-Service comes through "normal" spring dependency injection (of your choice... manual/auto/exotic ((final/)field/accessor/(constuctor/)parameter...)
}

To access (in all templates rendered by this controller class) like:

<xx th:each="foo : ${fooBars}">
  ...
xerx593
  • 12,237
  • 5
  • 33
  • 64