Search code examples
spring-bootspring-mvcspring-dataspring-data-jdbc

Spring boot can't find Repository bean


I'm building a Todo application in Spring Boot with Spring Data JDBC. I've built out all the REST endpoints and they work fine in Unit Tests, but when I run the application to access the endpoints/pages in browser, I get the following error:

Parameter 0 of constructor in dev.iosenberg.todo.controllers.TodoController required a bean of type 'dev.iosenberg.todo.repositories.TodoRepository' that could not be found.

Here are some relevant files:

TodoApplication.java:

package dev.iosenberg.todo;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class TodoApplication {

    public static void main(String[] args) {
        SpringApplication.run(TodoApplication.class, args);
    }

}

models/Todo.java:

package dev.iosenberg.todo.models;

import org.springframework.data.annotation.Id;

public record Todo(@Id Long id, Long userId, String name, String description, boolean completed) {

}

repositories/TodoRepository.java:

package dev.iosenberg.todo.repositories;

import org.springframework.data.repository.CrudRepository;
import org.springframework.data.repository.PagingAndSortingRepository;
import org.springframework.stereotype.Repository;

import dev.iosenberg.todo.models.Todo;

@Repository
public interface TodoRepository extends CrudRepository<Todo,Long>, PagingAndSortingRepository<Todo, Long>{

}

controllers/TodoController.java:

package dev.iosenberg.todo.controllers;

import java.net.URI;
import java.util.Optional;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.autoconfigure.data.web.SpringDataWebProperties.Pageable;
import org.springframework.data.domain.Page;
import org.springframework.data.domain.PageRequest;
import org.springframework.http.ResponseEntity;
import org.springframework.web.bind.annotation.DeleteMapping;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.PutMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.util.UriComponentsBuilder;

import dev.iosenberg.todo.models.Todo;
import dev.iosenberg.todo.repositories.TodoRepository;

import java.util.List;

@RestController
@RequestMapping("/todos")
public class TodoController {
    @Autowired
    private TodoRepository todoRepository;
    
    public TodoController(TodoRepository todoRepository) {
        this.todoRepository = todoRepository;
    }

    @GetMapping
    public ResponseEntity<List<Todo>> findAll(Pageable pageable) {
        Page<Todo> page = todoRepository.findAll(
            PageRequest.of(1,1)
            );
        return ResponseEntity.ok(page.getContent());
    }

    @GetMapping("/{requestedId}")
    public ResponseEntity<Todo> findById(@PathVariable Long requestedId) {
        Optional<Todo> todoOptional = todoRepository.findById(requestedId);
        if(todoOptional.isPresent()) {
            return ResponseEntity.ok(todoOptional.get());
        }
        else {
            return ResponseEntity.notFound().build();
        }
    }

    @PostMapping
    public ResponseEntity<Void> createTodo(@RequestBody Todo newTodoRequest, UriComponentsBuilder ucb) {
        Todo savedTodo = todoRepository.save(newTodoRequest);
        URI locationOfNewTodo = ucb
            .path("todos/{id}")
            .buildAndExpand(savedTodo.id())
            .toUri();
        return ResponseEntity.created(locationOfNewTodo).build();
    }

    @PutMapping("/{id}")
    private ResponseEntity<Void> putTodo(@PathVariable Long id, @RequestBody Todo todoUpdate) {
        Optional<Todo> todoOptional = todoRepository.findById(id);
        if(todoOptional.isPresent()) {
            Todo updatedTodo = todoUpdate;
            todoRepository.save(updatedTodo);
            return ResponseEntity.noContent().build();
        }
        return ResponseEntity.notFound().build();
    }

    @DeleteMapping("/{id}")
    private ResponseEntity<Void> deleteTodo(@PathVariable Long id) {
        if(!todoRepository.existsById(id)) {
            return ResponseEntity.notFound().build();
        }
        todoRepository.deleteById(id);
        return ResponseEntity.noContent().build();
    }
}

I also have this to serve some webpages MvcConfig.java:

package dev.iosenberg.todo;

import org.springframework.context.annotation.Configuration;
import org.springframework.web.servlet.config.annotation.ViewControllerRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;

@Configuration
public class MvcConfig implements WebMvcConfigurer {
    
    public void addViewControllers(ViewControllerRegistry registry) {
        registry.addViewController("/home").setViewName("home");
        registry.addViewController("/").setViewName("home");
        registry.addViewController("/test").setViewName("test");
        registry.addViewController("/hello").setViewName("hello");
        registry.addViewController("/login").setViewName("login");
    }
}

build.gradle:

plugins {
    id 'java'
    id 'org.springframework.boot' version '3.1.5'
    id 'io.spring.dependency-management' version '1.1.3'
}

group = 'dev.iosenberg'
version = '0.0.1-SNAPSHOT'

java {
    sourceCompatibility = '17'
}

repositories {
    mavenCentral()
}

dependencies {
    // implementation 'org.springframework.boot:spring-boot-starter-security'
    implementation 'org.springframework.boot:spring-boot-starter-thymeleaf'
    implementation 'org.springframework.boot:spring-boot-starter-web'
    // implementation 'org.thymeleaf.extras:thymeleaf-extras-springsecurity6'
    implementation 'org.springframework.data:spring-data-jdbc'

    testImplementation 'com.h2database:h2'
    testImplementation 'org.springframework.boot:spring-boot-starter-test'
    // testImplementation 'org.springframework.security:spring-security-test'
}

tasks.named('test') {
    useJUnitPlatform()
}

test {
    testLogging {
        events "passed", "skipped", "failed", "standardOut", "standardError"

        showExceptions true
        exceptionFormat "full"
        showCauses true
        showStackTraces true

        // Change to `true` for more verbose test output
        showStandardStreams = true
    }
}

And here's the whole console output:

2023-11-09T12:09:23.449-05:00  INFO 27748 --- [           main] dev.iosenberg.todo.TodoApplication       : Starting TodoApplication using Java 17.0.9 with PID 27748 (C:\Users\ikeos\Documents\git\todo\bin\main started by ikeos in C:\Users\ikeos\Documents\git\todo)
2023-11-09T12:09:23.455-05:00  INFO 27748 --- [           main] dev.iosenberg.todo.TodoApplication       : No active profile set, falling back to 1 default profile: "default"
2023-11-09T12:09:25.106-05:00  INFO 27748 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2023-11-09T12:09:25.119-05:00  INFO 27748 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2023-11-09T12:09:25.120-05:00  INFO 27748 --- [           main] o.apache.catalina.core.StandardEngine    : Starting Servlet engine: [Apache Tomcat/10.1.15]     
2023-11-09T12:09:25.274-05:00  INFO 27748 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2023-11-09T12:09:25.277-05:00  INFO 27748 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 1740 ms
2023-11-09T12:09:25.366-05:00  WARN 27748 --- [           main] ConfigServletWebServerApplicationContext : Exception encountered during context initialization - cancelling refresh attempt: org.springframework.beans.factory.UnsatisfiedDependencyException: Error creating bean with name 'todoController' defined in file [C:\Users\ikeos\Documents\git\todo\bin\main\dev\iosenberg\todo\controllers\TodoController.class]: Unsatisfied dependency expressed through constructor parameter 0: No qualifying bean of type 'dev.iosenberg.todo.repositories.TodoRepository' available: expected at least 1 bean which qualifies as autowire candidate. Dependency annotations: {}
2023-11-09T12:09:25.372-05:00  INFO 27748 --- [           main] o.apache.catalina.core.StandardService   : Stopping service [Tomcat]
2023-11-09T12:09:25.395-05:00  INFO 27748 --- [           main] .s.b.a.l.ConditionEvaluationReportLogger : 

Error starting ApplicationContext. To display the condition evaluation report re-run your application with 'debug' enabled.
2023-11-09T12:09:25.431-05:00 ERROR 27748 --- [           main] o.s.b.d.LoggingFailureAnalysisReporter   : 

***************************
APPLICATION FAILED TO START
***************************

Description:

Parameter 0 of constructor in dev.iosenberg.todo.controllers.TodoController required a bean of type 'dev.iosenberg.todo.repositories.TodoRepository' that could not be found.


Action:

Consider defining a bean of type 'dev.iosenberg.todo.repositories.TodoRepository' in your configuration. 

There's more code that's less relevant, but the whole repo is here if it's helpful: https://github.com/iosenberg/todo

I tried two solutions.

First was to add @ComponentScan and (basePackages = "dev.iosenberg.todo")/(basePackages = "dev.iosenberg.todo.repositories","dev.iosenberg.todo.controllers",etc.), and the best outcome was the removal of the Repository Bean error, but when I tried to access any pages served through the application, I get a Whitelabel 404 error page.

The second solution was to move my test repository (data.sql and schema.sql) from src/test/resources to src/main/resources, but I simply got SQL errors saying it didn't recognize the table.

I've scoured pretty much every Stack Overflow page mentioning the Repository error and have come up completely blank on what to do next.


Solution

  • The problem is you don't have a database.

    You have declared com.h2database:h2 as a testImplementation dependency only, so it is available in the tests but not when you actually run the application.

    The easiest fix is to change that to an implementation dependency and your application will start up fine. Of course, for production you probably want to connect to a proper persistent database. Also you'll want to use Testcontainers instead of an in-memory database for integration tests. But these are beyond the scope of this question.

    How to debug problems with Spring Boots autoconfiguration

    1. Enable Spring Boot's debugging by adding debug=true to your application.properties file

    2. Look especially in the section Negative matches of the output for auto-configurations that should happen but don't. Searching for relevant technologies is a good idea. Searching for JDBC yields the following results that seem relevant:

       DataSourceAutoConfiguration matched:
          - @ConditionalOnClass found required classes 'javax.sql.DataSource', 'org.springframework.jdbc.datasource.embedded.EmbeddedDatabaseType' (OnClassCondition)
          - @ConditionalOnMissingBean (types: io.r2dbc.spi.ConnectionFactory; SearchStrategy: all) did not find any beans (OnBeanCondition)
    
       DataSourceTransactionManagerAutoConfiguration matched:
          - @ConditionalOnClass found required classes 'org.springframework.jdbc.core.JdbcTemplate', 'org.springframework.transaction.TransactionManager' (OnClassCondition)
    

    This looks like you have a DataSource, which just demonstrates that you shouldn't stop at the first match.

    The next match is:

       DataSourceInitializationConfiguration:
          Did not match:
             - @ConditionalOnSingleCandidate (types: javax.sql.DataSource; SearchStrategy: all) did not find any beans (OnBeanCondition)
          Matched:
             - @ConditionalOnClass found required class 'org.springframework.jdbc.datasource.init.DatabasePopulator' (OnClassCondition)
    

    So, DataSourceAutoConfiguration did run, but we still don't seem to have a DataSource bean. From this we learn that there is an important difference between a class being available and a bean of that type being available. Kind of obvious when you say it out loud, yet easy to miss when you look at some log file.

    The rest of the search turns up just more stuff that doesn't work, which is either irrelevant, or not surprising if there is no DataSource bean. Therefore I switched to searching for datasource in the log.

    That returns tons of hits. But the first one in the Negative matches was really helpful.

       DataSourceAutoConfiguration.EmbeddedDatabaseConfiguration:
          Did not match:
             - EmbeddedDataSource did not find embedded database (DataSourceAutoConfiguration.EmbeddedDatabaseCondition)
    

    So no embedded database found! What database is it trying to use? I checked the application.properties for a jdbc url (there is none) and your dependencies for databases and only found the above mentioned test dependency.