Skip to content

Review documentation and migration guide about changes in @AutoConfigureCache #48522

@iparadiso

Description

@iparadiso

Problem

While Upgrading from Spring Boot 3.5.8 to 4.0.0, I'm seeing minimal @WebMvcTest tests and other test slices (e.g. @DataJdbcTest) fail with the following exception.

No qualifying bean of type 'org.springframework.cache.CacheManager' available: no CacheResolver specified - register a CacheManager bean or remove the @EnableCaching annotation from your configuration.
org.springframework.beans.factory.NoSuchBeanDefinitionException: No qualifying bean of type 'org.springframework.cache.CacheManager' available: no CacheResolver specified - register a CacheManager bean or remove the @EnableCaching annotation from your configuration.
	at app//org.springframework.cache.interceptor.CacheAspectSupport.afterSingletonsInstantiated(CacheAspectSupport.java:287)
	at app//org.springframework.beans.factory.support.DefaultListableBeanFactory.preInstantiateSingletons(DefaultListableBeanFactory.java:1147)
	at app//org.springframework.context.support.AbstractApplicationContext.finishBeanFactoryInitialization(AbstractApplicationContext.java:983)
	at app//org.springframework.context.support.AbstractApplicationContext.refresh(AbstractApplicationContext.java:620)
	at app//org.springframework.boot.SpringApplication.refresh(SpringApplication.java:765)
	at app//org.springframework.boot.SpringApplication.refreshContext(SpringApplication.java:454)
	at app//org.springframework.boot.SpringApplication.run(SpringApplication.java:321)

Example

I've included the abbreviated example below to demonstrate the problem.

@WebMvcTest(ProductController.class)
class ProductControllerTest {

    @Autowired
    private MockMvc mockMvc;

    @Autowired
    private ObjectMapper objectMapper;

    @MockitoBean
    private ProductService productService;

    @Test
    void getProduct_WhenProductExists_ReturnsProduct() throws Exception {
        Product product = new Product(1L, "Test Product", "Test Description", 99.99);
        when(productService.getProductById(1L)).thenReturn(Optional.of(product));

        mockMvc.perform(get("/api/products/1"))
                .andExpect(status().isOk())
                .andExpect(content().contentType(MediaType.APPLICATION_JSON))
                .andExpect(jsonPath("$.id").value(1))
                .andExpect(jsonPath("$.name").value("Test Product"))
                .andExpect(jsonPath("$.description").value("Test Description"))
                .andExpect(jsonPath("$.price").value(99.99));
    }

}

@RestController
@RequestMapping("/api/products")
public class ProductController {

    private final ProductService productService;

    public ProductController(ProductService productService) {
        this.productService = productService;
    }

    @GetMapping("/{id}")
    public ResponseEntity<Product> getProduct(@PathVariable Long id) {
        return productService.getProductById(id)
                .map(ResponseEntity::ok)
                .orElse(ResponseEntity.notFound().build());
    }

}

@Service
public class ProductService {

    private final Map<Long, Product> dataStore = new ConcurrentHashMap<>();
    private final AtomicLong idGenerator = new AtomicLong(1);

    public ProductService() {
        dataStore.put(1L, new Product(1L, "Sample Product", "A sample product", 99.99));
        dataStore.put(2L, new Product(2L, "Another Product", "Another sample", 149.99));
        idGenerator.set(3);
    }

    @Cacheable(value = "products", key = "#id")
    public Optional<Product> getProductById(Long id) {
        System.out.println("Fetching product from data store for id: " + id);
        return Optional.ofNullable(dataStore.get(id));
    }

}

@Configuration
public class CacheConfig {

    @Bean
    public CacheManager cacheManager() {
        return new ConcurrentMapCacheManager("products");
    }
}

@EnableCaching
@SpringBootApplication
public class DemoApplication {

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

Full Reproducer:

What appears to be happening is these test slices scan the @SpringBootApplication to pick up configurations and sees the @EnableCacheing and then aggressively tries to autoconfigure cacheing.

In Spring Boot 3.x, if cacheing was not configured, it would silently ignore @Cacheable annotation. But in Spring Boot 4.x it fails the test.

Expectation

OSS tests slices and minimal test slices should not fail when trying to configure AOP integrations like cacheing when the dependencies and configurations are provided not provided those annotations to work.

The workaround is move @EnableCacheing to another @Configuration so it's not picked up by OSS Test slice annotations. However, this is not ideal or intuitive for the user to be aware of delicate separations like this. This is one example, but there's probably many ways this fails if @EnableCacheing is seen in a minimal test, but we don't want to intentionally test Cacheing.

Metadata

Metadata

Assignees

Labels

Type

No type

Projects

No projects

Milestone

Relationships

None yet

Development

No branches or pull requests

Issue actions