简体   繁体   中英

SpringBoot: Configurable @Qualifier to select bean

Small question for SpringBoot, and how to configure the bean using @Qualifier please.

I have a very straightforward piece of code:

<?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.0.1</version>
        <relativePath/>
    </parent>

    <groupId>com.question</groupId>
    <artifactId>language</artifactId>
    <version>1.1</version>

    <name>language</name>
    <description>Spring Boot</description>

    <properties>
        <java.version>17</java.version>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <project.build.sourceEncoding>UTF-8</project.build.sourceEncoding>
    </properties>

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

    <build>
        <plugins>
            <plugin>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-maven-plugin</artifactId>
            </plugin>
        </plugins>
    </build>

</project>

package com.question;

import org.springframework.boot.SpringApplication;
import org.springframework.boot.autoconfigure.SpringBootApplication;

@SpringBootApplication
public class LanguageApplication {

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

}
package com.question.service;

public interface LanguageService {

    String process(String name);

}

package com.question.service;

import org.springframework.stereotype.Service;

@Service("french")
public class FrenchLanguageServiceImpl implements LanguageService {

    @Override
    public String process(String name) {
        return "Bonjour " + name;
    }

}

package com.question.service;

import org.springframework.stereotype.Service;

@Service("english")
public class EnglishLanguageServiceImpl implements LanguageService {

    @Override
    public String process(String name) {
        return "Welcome " + name;
    }

}

package com.question.controller;

import com.question.service.LanguageService;
import org.springframework.beans.factory.annotation.Qualifier;
import org.springframework.beans.factory.annotation.Value;
import org.springframework.web.bind.annotation.*;

@RestController
public class LanguageController {

    private final LanguageService languageService;

    @Value("${configuration}")
    public String configuration;

    public LanguageController(@Qualifier(configuration) LanguageService languageService) {
        this.languageService = languageService;
    }

    @GetMapping("/test")
    public String test(@RequestParam String name) {
        return languageService.process(name);
    }

}

Expected:

What I hope to achieve is equally straightforward. I would like to pass some sort of configuration to application.properties , something like configuration=french or configuration=english .

At the controller layer, to use (@Qualifier(configuration) LanguageService languageService) And the correct concrete service will be used.

Actual:

Unfortunately,

@Qualifier(configuration) + @Value("${configuration}") public String configuration;

will yield Attribute Value must be constant .

Is there a way we can configure the concrete Bean via a configurable @Qualifier please?

I understand there is a way to workaround this by using ApplicationContext getBean.

But having this construct: @Qualifier(configuration) makes the code clean and easily understandable. How to achieve this please?

Thank you

You can't make this dynamic, you'll need to use a workaround. A factory pattern would do well, here. Instead of one bean, you would have two, and based on the value of that property, you would let your factory choose which one to use.

@Component
public class LanguageFactory {

  @Autowired
  @Qualifier("english")
  private LanguageService englishLanguage;

  @Autowired
  @Qualifier("french")
  private LanguageService frenchLanguage;

  @Value("${configuration}")
  private String chosenLanguage;

  public LanguageService getLanguageService() {
    switch(chosenLanguage) {
      case "english": return englishLanguage;
      case "french": return frenchLanguage;
      // add any other language you need
      default: throw new Exception("Invalid language: " + chosenLanguage);
    }
   }
}

So, in your LanguageController, you would get something like this:

  @Autowired
  private LanguageFactory languageFactory;

  @GetMapping("/test")
  public String test(@RequestParam String name) {
      return languageFactory.getLangaugeService().process(name);
  }

EDIT:

Another option would be to use a Map. You would avoid all the if-elses, but you would still have to add them to a Map.

@Component
public class LanguageFactory {

  private Map<String, LanguageService> languages = new HashMap<>();

  @Value("${configuration}")
  private String chosenLanguage;

  @Autowired
  public LanguageFactory(@Qualifier("english") LanguageService englishLanguage, @Qualifer("french") LanguageService frenchService) {
    this.languages.put("english", englishLanguage);
    this.languages.put("french", frenchLanguage);
  }

  public LanguageService getLanguageService() {
    LanguageService ls = this.languages.get(chosenLanguage);
    if ( ls == null ) {
      // handle the issue as you see fit
    }
    return ls;
  }
}

If you only need 1 of the LanguageService beans active at a time, then you can use @ConditionalOnProperty on each of them, each using a unique havingValue . Like this (warning, untested code written from memory):

interface ConfigKeys {
    public static final String LANGUAGE = "my.app.prefix.language";
}


@Service
@ConditionalOnProperty(ConfigKeys.LANGUAGE, havingValue = "english")
public class EnglishLanguageServiceImpl implements LanguageService {

    @Override
    public String process(String name) {
        return "Welcome " + name;
    }

}

@Service
@ConditionalOnProperty(ConfigKeys.LANGUAGE, havingValue = "french")
public class FrenchLanguageServiceImpl implements LanguageService {

    @Override
    public String process(String name) {
        return "Bonjour " + name;
    }

}

With that, you don't need any qualifiers, just set the my.app.prefix.language property in your config (application.properties or application.yaml) to the value you want, and there will just one LanguageService bean in the context. You can inject that bean wherever you need it without needing a qualifier.

A much simpler option would be to group the related beans in @Configuration classes, and then use @Conditional to enable (or not) the configuration, and in turn, a set of beans. That way, you're not dealing with individual beans, as you say you've many.

The technical post webpages of this site follow the CC BY-SA 4.0 protocol. If you need to reprint, please indicate the site URL or the original address.Any question please contact:yoyou2525@163.com.

 
粤ICP备18138465号  © 2020-2024 STACKOOM.COM