0

everything started because Spring Cloud AWS does not configure the SimpleStorageProtocolResolver correctly. This class is responsible for handling s3:// protocol while using a ResourceLoader. See issue for further details: cannot be cast to org.springframework.core.io.WritableResource on Spring AWS example.

So, I had to create it manually. But I'm also using the LocalStack solution (https://github.com/localstack/localstack), and as Spring Cloud AWS does not have an option to configure the Endpoint manually, I had (guess what?) to create the AmazonS3Client by hand.

Here comes the problem, when I create both beans in my class S3Configuration class (see below), Spring Framework just skips the bean creation. And, when I try to wire it on the S3Handler class (see below), this error happens: Error creating a bean with name 'amazonS3Client': Requested bean is currently in creation: Is there an unresolvable circular reference?

But again, here comes the whole thing, there is no circular reference between those classes.

I have simplified the project so I would be able to post it here:

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>2.1.7.RELEASE</version>
        <relativePath/>
    </parent>
    <groupId>io.mobi7.ms</groupId>
    <artifactId>mobi7-temp-ms-ibutton-worker</artifactId>
    <version>1.0.0-SNAPSHOT</version>

    <properties>
        <spring-cloud-version>2.1.4.RELEASE</spring-cloud-version>
    </properties>

    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-logging</artifactId>
        </dependency>

        <dependency>
            <groupId>org.springframework.cloud</groupId>
            <artifactId>spring-cloud-starter-aws</artifactId>
            <version>${spring-cloud-version}</version>
        </dependency>
    </dependencies>

</project>

S3Configuration.java

package io.mobi7.ms.ibutton.worker;

import com.amazonaws.auth.AWSCredentialsProvider;
import com.amazonaws.client.builder.AwsClientBuilder;
import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.AmazonS3ClientBuilder;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.cloud.aws.core.io.s3.SimpleStorageProtocolResolver;
import org.springframework.cloud.aws.core.region.RegionProvider;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.core.io.DefaultResourceLoader;

@Configuration
public class S3Configuration {

    @Bean("amazonS3Client") // THIS METHOD IS NOT BEING CALLED!!!
    public AmazonS3 amazonS3Client(AWSCredentialsProvider credentialsProvider,
                                   RegionProvider regionProvider,
                                   @Value("${cloud.aws.s3.default-endpoint}") String endpoint) {
        return AmazonS3ClientBuilder.standard()
            .withCredentials(credentialsProvider)
            .withEndpointConfiguration(
                new AwsClientBuilder.EndpointConfiguration(endpoint, regionProvider.getRegion().getName()))
            .build();
    }

    @Autowired  // THIS METHOD IS NOT BEING CALLED!!!
    public void configureResourceLoader(@Qualifier("amazonS3Client") AmazonS3 amazonS3Client,
                                        DefaultResourceLoader resourceLoader) {
        SimpleStorageProtocolResolver simpleStorageProtocolResolver = new SimpleStorageProtocolResolver(amazonS3Client);
        // As we are calling it by hand, we must initialize it properly.
        simpleStorageProtocolResolver.afterPropertiesSet();
        resourceLoader.addProtocolResolver(simpleStorageProtocolResolver);
    }
}

S3Handle.java

package io.mobi7.ms.ibutton.worker;

import com.amazonaws.services.s3.AmazonS3;
import com.amazonaws.services.s3.model.S3ObjectSummary;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.stereotype.Component;

import java.util.List;
import java.util.stream.Collectors;

@Component
public class S3Handler {

    private AmazonS3 amazonS3Client;

    @Autowired
    public S3Handler(@Qualifier("amazonS3Client") AmazonS3 amazonS3Client) {
        this.amazonS3Client = amazonS3Client;
    }

    public List<String> listFiles(String bucketName) {
        return amazonS3Client.listObjects(bucketName).getObjectSummaries().stream().map(S3ObjectSummary::getKey)
            .collect(Collectors.toList());
    }
}

Application.java

package io.mobi7.ms.ibutton.worker;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.boot.WebApplicationType;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;

@SpringBootApplication
public class Application {

    public static void main(String[] args) {
        new SpringApplicationBuilder(Application.class).web(WebApplicationType.NONE).run(args);
    }

    @Autowired
    public void listFiles(S3Handler s3Handler) {
        s3Handler.listFiles("default-s3-bucket").forEach(System.out::println);
    }
}

application.properties

cloud.aws.region.static=us-east-1
cloud.aws.stack.auto=false
cloud.aws.s3.default-endpoint=http://localhost:4572

Any ideas, why is this happening?

Felipe Desiderati
  • 2,414
  • 3
  • 24
  • 42
  • The problem is partially because you are doing everything configuration time. I suggest instead of your `Application.listFiles` method, to create an `@Bean` method that returns an `ApplicationRunner` to do the same. This should delay the creation and give proper time to construct the objects. – M. Deinum Nov 29 '19 at 10:04
  • It makes senes but the listFiles method was created just to simplify the question here, even if I insert the S3Handler in a @RestController, the same error occurs. – Felipe Desiderati Nov 29 '19 at 19:10

1 Answers1

1

@Autowired annotation marks a method that constructs object state (e.g. setter or method injection). See "Autowired Methods" section here.

Both methods that you annotated with Autowired are not used for this need.

This question is about Spring. All other tags can be removed.


Update: Similar case is covered here.
Regardless of what is suggested there, I would not keep a single GOD configuration class, instead I would split your configuration class into 2 classes - factory for S3 client and configuration of resource loader.

nickolay.laptev
  • 2,253
  • 1
  • 21
  • 31
  • I disagree, the method configureResourceLoader in class S3Configuration is a config method. I need to reconfigure the DefaultResourceLoader. And in the documentation that you have passed, says that I can do it in a config method: "Config methods may have an arbitrary name and any number of arguments; each of those arguments will be autowired with a matching bean in the Spring container". – Felipe Desiderati Nov 29 '19 at 19:06
  • The second @autowired method it was created just to simplify the solution. Spring should have created/autowired in this order: S3Configuration.amazonS3Client, S3Configuration.configureResourceLoader, S3Handler.S3handler, Application.listFiles. I don't know why but if remove the Qualifier from configureResourceLoader method it works perfectly. Which not make senes at all. – Felipe Desiderati Nov 29 '19 at 19:08
  • 1
    @FelipeDesiderati This is covered here https://stackoverflow.com/a/28748366/1262265 I updated the answer – nickolay.laptev Nov 29 '19 at 20:16
  • Thaaaaaaaaanks, it makes more sense now. – Felipe Desiderati Nov 29 '19 at 23:50