AlexZhur
AlexZhur

Reputation: 49

Beans are not autowired if to use caching

In my app beans are not autowired if I use @Cacheable or/and @CacheEvict annotations. I figured this out by elimination. If you comment out the lines with @Cacheable and @CacheEvict, then everything is ok. If you uncomment them, then all the beans inside the RestClientWorker are null. When call MyService's getOrders method, then throw Exception:

Cannot invoke "org.springframework.web.client.RestClient.post()" because "this.restClient" is null

But in fact(after debugging) all beans inside RestClientWorker are null. What could be the problem?

pom.xml

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <parent>
        <groupId>org.springframework.boot</groupId>
        <artifactId>spring-boot-starter-parent</artifactId>
        <version>3.3.3</version>
    </parent>

    <groupId>com.example</groupId>
    <artifactId>app</artifactId>
    <version>1.0.0</version>

    <properties>
        <maven.compiler.source>21</maven.compiler.source>
        <maven.compiler.target>21</maven.compiler.target>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-cache</artifactId>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-validation</artifactId>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <version>1.18.34</version>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-test</artifactId>
            <scope>test</scope>
            <exclusions>
                <exclusion>
                    <groupId>org.junit.vintage</groupId>
                    <artifactId>junit-vintage-engine</artifactId>
                </exclusion>
            </exclusions>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-configuration-processor</artifactId>
            <optional>true</optional>
        </dependency>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-actuator</artifactId>
        </dependency>
        <dependency>
            <groupId>io.micrometer</groupId>
            <artifactId>micrometer-registry-prometheus</artifactId>
            <version>1.13.3</version>
        </dependency>
        <dependency>
            <groupId>com.custom</groupId>
            <artifactId>pam-clients-lib</artifactId>
            <version>1.0.0</version>
        </dependency>
        <dependency>
            <groupId>com.custom</groupId>
            <artifactId>lib-logger</artifactId>
            <version>1.0.1</version>
        </dependency>
        <dependency>
            <groupId>org.yaml</groupId>
            <artifactId>snakeyaml</artifactId>
            <version>2.3</version>
        </dependency>
        <dependency>
            <groupId>org.wiremock</groupId>
            <artifactId>wiremock-jetty12</artifactId>
            <version>3.9.1</version>
        </dependency>
    </dependencies>

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
                <version>2.7.0</version>
                <dependencies>
                    <dependency>
                        <groupId>org.codehaus.plexus</groupId>
                        <artifactId>plexus-utils</artifactId>
                        <version>3.5.0</version>
                    </dependency>
                    <dependency>
                        <groupId>org.apache.maven.shared</groupId>
                        <artifactId>maven-shared-utils</artifactId>
                        <version>3.3.4</version>
                    </dependency>
                </dependencies>
                <configuration>
                    <excludes>
                        <exclude>
                            <groupId>org.projectlombok</groupId>
                            <artifactId>lombok</artifactId>
                        </exclude>
                    </excludes>
                    <layers>
                        <enabled>true</enabled>
                    </layers>
                </configuration>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-clean-plugin</artifactId>
                <version>3.2.0</version>
                <dependencies>
                    <dependency>
                        <groupId>org.codehaus.plexus</groupId>
                        <artifactId>plexus-utils</artifactId>
                        <version>3.5.0</version>
                    </dependency>
                </dependencies>
            </plugin>
            <plugin>
                <groupId>org.jacoco</groupId>
                <artifactId>jacoco-maven-plugin</artifactId>
                <version>0.8.8</version>
                <dependencies>
                    <dependency>
                        <groupId>org.codehaus.plexus</groupId>
                        <artifactId>plexus-utils</artifactId>
                        <version>3.5.0</version>
                    </dependency>
                </dependencies>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-compiler-plugin</artifactId>
                <version>3.10.1</version>
                <dependencies>
                    <dependency>
                        <groupId>org.codehaus.plexus</groupId>
                        <artifactId>plexus-utils</artifactId>
                        <version>3.5.0</version>
                    </dependency>
                </dependencies>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-install-plugin</artifactId>
                <version>2.5.2</version>
                <dependencies>
                    <dependency>
                        <groupId>org.codehaus.plexus</groupId>
                        <artifactId>plexus-utils</artifactId>
                        <version>3.5.0</version>
                    </dependency>
                    <dependency>
                        <groupId>org.apache.maven.shared</groupId>
                        <artifactId>maven-shared-utils</artifactId>
                        <version>3.3.4</version>
                    </dependency>

                </dependencies>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-surefire-plugin</artifactId>
                <version>3.0.0-M6</version>
            </plugin>
            <plugin>
                <groupId>org.apache.maven.plugins</groupId>
                <artifactId>maven-dependency-plugin</artifactId>
                <version>3.3.0</version>
                <dependencies>
                    <dependency>
                        <groupId>dom4j</groupId>
                        <artifactId>dom4j</artifactId>
                        <version>1.6.1</version>
                    </dependency>
                </dependencies>
            </plugin>
        </plugins>
    </build>
</project>

Application.java

@SpringBootApplication
@EnableAsync(proxyTargetClass = true) // used for other case
@EnableCaching
@EnableScheduling
public class Application {
    public static void main(String[] args) {
        SpringApplication.run(Application.class, args);
    }
}

BaseConfig.java

@Configuration
public class BaseConfig {
    @Value("${HTTP_CLIENT_MAX_CONN_TOTAL:2000}")
    private Integer maxPoolSize;
    @Value("${HTTP_CLIENT_MAX_PER_ROUTE:1000}")
    private Integer maxPerRoute;
    @Value("${HTTP_CLIENT_MAIN_TIMEOUT:2000}")
    private Integer timeout;

    @Bean
    public ObjectMapper mapper(Jackson2ObjectMapperBuilder builder) {
        return builder.modules(new JavaTimeModule())
                .featuresToDisable(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS)
                .featuresToEnable(MapperFeature.ACCEPT_CASE_INSENSITIVE_PROPERTIES, SerializationFeature.INDENT_OUTPUT)
                .serializationInclusion(JsonInclude.Include.NON_NULL)
                .build();
    }

    @Bean
    public HttpClient httpClient(MeterRegistry meterRegistry){

        PoolingHttpClientConnectionManager connectionManager = new PoolingHttpClientConnectionManager();

        connectionManager.setMaxTotal(maxPoolSize);
        connectionManager.setDefaultMaxPerRoute(maxPerRoute);
        new PoolingHttpClientConnectionManagerMetricsBinder(connectionManager, "http-client- 
        pool").bindTo(meterRegistry);
        RequestConfig requestConfig = RequestConfig.custom()
                .setConnectionRequestTimeout(timeout, TimeUnit.MILLISECONDS)
                .setResponseTimeout(timeout, TimeUnit.MILLISECONDS)
                .build();

        return HttpClientBuilder.create().setDefaultRequestConfig(requestConfig)
                .setConnectionManager(connectionManager)
                .evictExpiredConnections()
                .build();
    }

    @Bean
    public ClientHttpRequestFactory httpRequestFactory(HttpClient httpClient){
        return new HttpComponentsClientHttpRequestFactory(httpClient);
    }

    @Bean
    @Primary
    public RestClient restClient(RestClient.Builder restClientBuilder, @Qualifier("mapper") 
                                 ObjectMapper mapper,
                                 UrlsConfig urlsConfig, HttpClient httpClient) {
        List<HttpMessageConverter<?>> messageConverters = new ArrayList<>();
        messageConverters.add(0, new StringHttpMessageConverter(StandardCharsets.UTF_8));
        var converter = new MappingJackson2HttpMessageConverter(mapper);
        converter.setSupportedMediaTypes(Collections.singletonList(MediaType.ALL));
        messageConverters.add(1, converter);
        return restClientBuilder
                .defaultStatusHandler(new RestErrorHandler(urlsConfig))
                .requestFactory(httpRequestFactory(httpClient))
                .messageConverters(converters -> converters.addAll(messageConverters)).build();
    }

    @Bean
    public CacheManager cacheManager() {
        return new ConcurrentMapCacheManager("a", "b");
    }
}

CacheCustomizer.java

@Component
public class CacheCustomizer implements CacheManagerCustomizer<ConcurrentMapCacheManager> {

    @Override
    public void customize(ConcurrentMapCacheManager cacheManager) {
        cacheManager.setCacheNames(asList("a", "b"));
    }
}

RestClientWorker.java

@Component
@Slf4j
public class RestClientWorker {
    private final UrlsConfig urlsConfig;
    private final RestClient restClient;
    private final StaticUserCredentials creds;

    public RestClientWorker(UrlsConfig urlsConfig, RestClient restClient,
                            StaticUserCredentials creds) {
        this.urlsConfig = urlsConfig;
        this.restClient = restClient;
        this.creds = creds;        
    }

    public final ResponseEntity<CatalogOrdersResp> catalogOrders(OrdersReq req) {
        return restClient.post()
                .uri(urlsConfig.getOrdersPath())
                .body(req)
                .retrieve()
                .toEntity(CatalogOrdersResp.class);
    }
   
    @Cacheable("a")
    public final ResponseEntity<CatalogClientContextResp> catalogA() {
        return restClient.get()
                .uri(urlsConfig.getCatalogAPath())                
                .retrieve()
                .toEntity(CatalogClientContextResp.class);
    }

    @Cacheable("b")
    public final ResponseEntity<CatalogClientContextResp> catalogB() {
        return restClient.get()
                .uri(urlsConfig.getCatalogBPath())                
                .retrieve()
                .toEntity(CatalogClientContextResp.class);
    }   

    @CacheEvict(value = {"a", "b"}, allEntries = true)
    @Scheduled(cron = "${cron.scheduling}")
    public void evictCaches() {
        log.info("A and B caches evicted");
    }
}

MyService.java

@Service
@Slf4j
public class MyService {
    private final RestClientWorker restClientWorker;

    public MyService(RestClientWorker restClientWorker) {
        this.restClientWorker = restClientWorker;       
    }

    public ResponseEntity<CatalogOrdersResp> getOrders(OrdersReq req) {
        return restClientWorker.catalogOrders(req);
    }    
}

application.properties

server.port = 8099
server.servlet.context-path=/api/@project.artifactId@
spring.application.name = @project.artifactId@

[email protected]@

management.endpoint.metrics.enabled=true
management.endpoints.web.exposure.include=*
management.endpoint.prometheus.enabled=true
management.prometheus.metrics.export.enabled=true
management.endpoints.web.base-path=/manage
management.server.port=8099
management.health.db.enabled=false

spring.main.allow-bean-definition-overriding=true

cron.scheduling=${ORDERS_CRON:0 0 0,12 * * *}

Upvotes: 0

Views: 95

Answers (1)

M. Deinum
M. Deinum

Reputation: 125202

Spring AOP uses proxies, Spring Boot by default enables class based proxies. Which means to apply behavior the proxy will extend the original class and override methods. As your methods are final this isn't happening. Which means the methods are invoked on the proxy not on the wrapped object.

For more information see https://deinum.biz/2020-07-03-Autowired-Field-Null/ (which is a blog post I've written quite some time ago).

Basically an autowired field cannot be null as that would prevent the application to start. So if your autowired field is null one of the causes mentioned in my blog is the cause.

In your case to fix, remove the final keyword from the methods.

Upvotes: 3

Related Questions