Spring boot can't find Repository bean

600 views Asked by At

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.

3

There are 3 answers

0
Jens Schauder On BEST ANSWER

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.

1
Andrei Lisa On

In your case the problem are in using record as Entity what is really wrong and it is the root case of the problem.

Hibernate, entities are created and managed using proxies. Proxies are classes that are generated at runtime and extend the entity class. These proxies rely on the entity class to have a no-args constructor and setters. Since records don't have these, they can't be used as entities.

To fix it:

Instead of

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

Write an Java class

    @Entity  
    @Table(name= "todo")   
    public class Todo {    
      
    @Id   
    private Long id;

    //others entity fields


    … 
   }

A good answer and some explanation about this topic see this Q/A

1
Knight Rider On

There are a couple of issues with your code.

  • First of all, don't use java record as entity https://howtodoinjava.com/spring/java-records-with-spring-data-jpa/#:~:text=Java%20records%20cannot%20be%20used,that%20map%20to%20database%20tables.

  • You need to annotate your entities appropriately with @Entity and @Id from import jakarta.persistence.Id

  • Rename User entity to users

     @Entity
     @Table(name = "users")
     public class User {
    
      @Id
      @GeneratedValue(strategy = GenerationType.IDENTITY)
      private Long id;
      private String username;
      private String password;
    

    }

  • Using implementation 'org.springframework.boot:spring-boot-starter-data-jpa' is much convenient compared to jdbc and in your case, you were missing access to use the spring data jpa magic

  • 'com.h2database:h2' should be implementation. It was restricted to test only with testImplementation

  • In case you want to customize the datasource for h2, here is an example but it's optional

    spring.datasource.url=jdbc:h2:mem:test;DB_CLOSE_ON_EXIT=FALSE
    spring.datasource.username=sa
    spring.datasource.password=
    spring.datasource.driverClassName=org.h2.Driver

  • You can also customize or provide access to your repositores and entities if they are outside the scope of the root directory but in this case it's not so this configuration is also optional

@EnableJpaRepositories(basePackages = "dev.iosenberg.todo.repositories") @EntityScan(basePackages = "dev.iosenberg.todo.models")

  • Configurations I used in application.properties

    server.error.include-message=always server.error.include-exception=true server.error.include-stacktrace=always server.error.include-binding-errors=always spring.thymeleaf.prefix= classpath:/templates/ spring.thymeleaf.suffix= .html

    ===============================

    = DATA SOURCE

    ===============================

    spring.datasource.url=jdbc:h2:mem:test;DB_CLOSE_ON_EXIT=FALSE spring.datasource.username=sa spring.datasource.password= spring.datasource.driverClassName=org.h2.Driver

    ===============================

    = JPA / HIBERNATE

    ===============================

    spring.jpa.show-sql=true spring.jpa.open-in-view=false spring.jpa.hibernate.ddl-auto=update spring.jpa.properties.hibernate.format_sql=true #spring.jpa.properties.hibernate.generate_statistics=true

    ===============================

    = THYMELEAF

    ===============================

    Whether to enable template caching.

    spring.thymeleaf.cache=false

After this, all should work great.

Connected to the target VM, address: '127.0.0.1:52923', transport: 'socket'

  .   ____          _            __ _ _
 /\\ / ___'_ __ _ _(_)_ __  __ _ \ \ \ \
( ( )\___ | '_ | '_| | '_ \/ _` | \ \ \ \
 \\/  ___)| |_)| | | | | || (_| |  ) ) ) )
  '  |____| .__|_| |_|_| |_\__, | / / / /
 =========|_|==============|___/=/_/_/_/
 :: Spring Boot ::                (v3.1.5)

2023-11-09T13:33:33.955-05:00  INFO 49861 --- [           main] dev.iosenberg.todo.TodoApplication       : Starting TodoApplication using Java 17.0.9 with PID 49861 (/Users/ericus20/StackOverflow/todo/build/classes/java/main started by ericus20 in /Users/ericus20/StackOverflow/todo)
2023-11-09T13:33:33.957-05:00  INFO 49861 --- [           main] dev.iosenberg.todo.TodoApplication       : No active profile set, falling back to 1 default profile: "default"
2023-11-09T13:33:34.244-05:00  INFO 49861 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Bootstrapping Spring Data JPA repositories in DEFAULT mode.
2023-11-09T13:33:34.270-05:00  INFO 49861 --- [           main] .s.d.r.c.RepositoryConfigurationDelegate : Finished Spring Data repository scanning in 21 ms. Found 2 JPA repository interfaces.
2023-11-09T13:33:34.521-05:00  INFO 49861 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat initialized with port(s): 8080 (http)
2023-11-09T13:33:34.526-05:00  INFO 49861 --- [           main] o.apache.catalina.core.StandardService   : Starting service [Tomcat]
2023-11-09T13:33:34.526-05:00  INFO 49861 --- [           main] o.apache.catalina.core.StandardEngine    : Starting Servlet engine: [Apache Tomcat/10.1.15]
2023-11-09T13:33:34.584-05:00  INFO 49861 --- [           main] o.a.c.c.C.[Tomcat].[localhost].[/]       : Initializing Spring embedded WebApplicationContext
2023-11-09T13:33:34.585-05:00  INFO 49861 --- [           main] w.s.c.ServletWebServerApplicationContext : Root WebApplicationContext: initialization completed in 607 ms
2023-11-09T13:33:34.661-05:00  INFO 49861 --- [           main] o.hibernate.jpa.internal.util.LogHelper  : HHH000204: Processing PersistenceUnitInfo [name: default]
2023-11-09T13:33:34.696-05:00  INFO 49861 --- [           main] org.hibernate.Version                    : HHH000412: Hibernate ORM core version 6.2.13.Final
2023-11-09T13:33:34.699-05:00  INFO 49861 --- [           main] org.hibernate.cfg.Environment            : HHH000406: Using bytecode reflection optimizer
2023-11-09T13:33:34.848-05:00  INFO 49861 --- [           main] o.s.o.j.p.SpringPersistenceUnitInfo      : No LoadTimeWeaver setup: ignoring JPA class transformer
2023-11-09T13:33:34.862-05:00  INFO 49861 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Starting...
2023-11-09T13:33:34.953-05:00  INFO 49861 --- [           main] com.zaxxer.hikari.pool.HikariPool        : HikariPool-1 - Added connection conn0: url=jdbc:h2:mem:test user=SA
2023-11-09T13:33:34.954-05:00  INFO 49861 --- [           main] com.zaxxer.hikari.HikariDataSource       : HikariPool-1 - Start completed.
2023-11-09T13:33:35.400-05:00  INFO 49861 --- [           main] o.h.e.t.j.p.i.JtaPlatformInitiator       : HHH000489: No JTA platform available (set 'hibernate.transaction.jta.platform' to enable JTA platform integration)
Hibernate: 
    create table todo (
        id bigint generated by default as identity,
        completed boolean not null,
        description varchar(255),
        name varchar(255),
        user_id bigint,
        primary key (id)
    )
Hibernate: 
    create table users (
        id bigint generated by default as identity,
        password varchar(255),
        username varchar(255),
        primary key (id)
    )
2023-11-09T13:33:35.434-05:00  INFO 49861 --- [           main] j.LocalContainerEntityManagerFactoryBean : Initialized JPA EntityManagerFactory for persistence unit 'default'
2023-11-09T13:33:35.750-05:00  INFO 49861 --- [           main] o.s.b.w.embedded.tomcat.TomcatWebServer  : Tomcat started on port(s): 8080 (http) with context path ''
2023-11-09T13:33:35.756-05:00  INFO 49861 --- [           main] dev.iosenberg.todo.TodoApplication       : Started TodoApplication in 1.972 seconds (process running for 2.268)

For more information about the configurations and setup, you can reference https://github.com/ericus20/spring-boot-starter