Learn how to use Java Spring Beans

Intro

Why write this, if there are too many resources already? -Simple, Many answers are too complex and others too simple. My purpose is to show practically how to use it, in other words, a real-case scenario.

What is a spring bean?

The answer can be found in the Spring Framework website:

In Spring, the objects that form the backbone of your application and that are managed by the Spring IoC container are called beans. A bean is an object that is instantiated, assembled, and managed by a Spring IoC container. Otherwise, a bean is simply one of many objects in your application. Beans, and the dependencies among them, are reflected in the configuration metadata used by a container.

Reading this we can say that a Bean is nothing more than an object, managed by a Spring IoC container. All right but what does this mean?

Let's put the following example:

You want an instance of an object you just created or from another library (let's say a client of some service), but, it needs configuration (providing credentials). So each time you use it you have to give it to the client so that you can use it. And that is not proper. now here comes the Bean, your best friend. You can make that plain object a Bean, so it's managed by Spring and that includes providing the credentials, so every time you use it no configuration is needed, just use it as you would.

Let's say that the client is an Elasticsearch client instance, that instance needs configuration (so it can connect to the server and provide the certificates if that happens to be the case)

// URL and API key
String serverUrl = "https://localhost:9200";
String apiKey = "VnVhQ2ZHY0JDZGJrU...";

// Create the low-level client
RestClient restClient = RestClient
    .builder(HttpHost.create(serverUrl))
    .setDefaultHeaders(new Header[]{
        new BasicHeader("Authorization", "ApiKey " + apiKey)
    })
    .build();

// Create the transport with a Jackson mapper
ElasticsearchTransport transport = new RestClientTransport(
    restClient, new JacksonJsonpMapper());

// And create the API client
ElasticsearchClient esClient = new ElasticsearchClient(transport);

Elasticsearch docs here!

This is how you initialize an Elasticsearch client, but wouldn't you prefer to inject the already configured instance to it instead of adding that code in your class constructor thus, leaving the configuration aside?

class SearchRepositoryElastic implements SearchRepository {
    private final ElasticsearchClient client;

    @Autowired
    public SearchRepositoryElastic(ElasticsearchClient client) {
        this.client = client;
    }

    // ...
}

It is way much cleaner, you just need to implement what your business logic requires and forget about configuration.

Let's code

Environment setup

  • Elasticsearch

  •   # 1
      docker network create elastic
      # 2
      docker pull docker.elastic.co/elasticsearch/elasticsearch:8.14.2
      # 3
      docker run --name es01 --net elastic \
          -p 9200:9200 \
          -it -m 1GB \
          docker.elastic.co/elasticsearch/elasticsearch:8.14.2
    
  • More info here

code

First of all, create a Spring boot project, to do so we are going to use Spring Initializr. I am going to use the following configuration and packages:

Note*: No need to include the GraalVM Native Support for this example.*

Now unzip the downloaded file and open it with your favorite IDE.

Now create a package named config and create a Java file (ElasticConnectionConfig) where we are going to create the configuration of our elastic connection.

@Configuration
@ConfigurationProperties(prefix = "elastic")
public class ElasticConnectionConfig {

    private String baseUrl;

    private String user;

    private String password;

    private String fingerprint;

    public String getBaseUrl() {
        return baseUrl;
    }

    public void setBaseUrl(String baseUrl) {
        this.baseUrl = baseUrl;
    }

    public String getUser() {
        return user;
    }

    public void setUser(String user) {
        this.user = user;
    }

    public String getPassword() {
        return password;
    }

    public void setPassword(String password) {
        this.password = password;
    }

    public String getFingerprint() {
        return fingerprint;
    }

    public void setFingerprint(String fingerprint) {
        this.fingerprint = fingerprint;
    }

    @Bean
    @ConditionalOnMissingBean
    public ElasticsearchClient elasticClient() {
        SSLContext sslContext = TransportUtils.sslContextFromCaFingerprint(this.fingerprint);

        BasicCredentialsProvider credsProv = new BasicCredentialsProvider();
        credsProv.setCredentials(AuthScope.ANY, new UsernamePasswordCredentials(this.user, this.password));

        RestClient restClient = RestClient.builder(HttpHost.create(this.baseUrl))
            .setHttpClientConfigCallback(hc -> hc.setSSLContext(sslContext).setDefaultCredentialsProvider(credsProv))
            .build();

        ElasticsearchTransport transport = new RestClientTransport(restClient, new JacksonJsonpMapper());
        return new ElasticsearchClient(transport);
    }

}

In this configuration class, we've added two decorators: @Configuration and @ConfigurationProperties(prefix = "elastic").

  • @Configuration: This annotation indicates that the class can be used by the Spring IoC container as a source of bean definitions. Essentially, it tells Spring that this class contains configuration settings.

  • @ConfigurationProperties(prefix = "elastic"): This annotation binds the properties defined in the configuration file (such as application.yml or application.properties) with the prefix "elastic" to the fields of this class. This allows us to externalize the configuration and makes it easier to manage.

The class contains fields for the Elasticsearch connection properties like baseUrl, user, password, and fingerprint. These fields have their respective getters and setters.

We can also use Lombok to reduce boilerplate code for getters and setters.

The real magic happens with the @Bean and @ConditionalOnMissingBean decorators:

  • @Bean: This annotation indicates that a method produces a bean to be managed by the Spring container. Here, it defines a bean of type ElasticsearchClient.

  • @ConditionalOnMissingBean: This annotation ensures that the bean is only created if no other bean of the same type has been defined. It prevents the creation of multiple beans of the same type, which could lead to conflicts.

Inside the elasticClient() method, we configure and instantiate the ElasticsearchClient:

  1. SSLContext: We create an SSL context using the certificate fingerprint to ensure secure communication with Elasticsearch.

  2. BasicCredentialsProvider: This is used to set up the credentials (username and password) for authentication.

  3. RestClient: We configure the Elasticsearch REST client with the base URL, SSL context, and credentials provider.

  4. ElasticsearchTransport: This wraps the REST client and uses the JacksonJsonpMapper for JSON processing.

  5. ElasticsearchClient: Finally, we return an instance of ElasticsearchClient using the configured transport.

And now we can inject it into our class constructor :)

@Repository
public class ElasticRepository implements ISearchRepository {
    private final ElasticsearchClient client;

    @Autowired
    public ElasticRepository(ElasticsearchClient client) {
        this.client = client;
    }

    // ...
}

Do not forget about the application.yml or application.properties, In my case, it looks like this.

spring:
  application:
    name: bean

elastic:
  base-url: ${ELASTIC_BASE_URL}
  user: ${ELASTIC_USER}
  password: ${ELASTIC_PASSWORD}
  fingerprint: ${ELASTIC_CERT_FINGERPRINT}

Note*: In Elasticsearch if you have an SSL/TLS certificate it is highly recommended to use a fingerprint, this fingerprint is generated from the certificate.*

Conclusion

We explored configuring an Elasticsearch client in Spring Boot. Using @Configuration, @ConfigurationProperties, @Bean, and @ConditionalOnMissingBean, we streamlined the setup and ensured efficient bean management. This approach simplifies integration, enhances security, and maintains a clean codebase. Stay tuned for more insights in our next issue!

Happy coding!

0
Subscribe to my newsletter

Read articles from Pablo Sanchidrian directly inside your inbox. Subscribe to the newsletter, and don't miss out.

Written by

Pablo Sanchidrian
Pablo Sanchidrian

I'm a Software Engineer who loves diving into system design and the creative world of software development. My experience spans working with Java, and Typescript, and I get genuinely excited about contributing to open-source projects. I've also got experience with cloud platforms, always to make strong, efficient systems that can handle whatever is thrown at them. I'm just someone who loves making software that makes a difference, using my skills to solve problems