0

I'm trying to define the service bean name using a property placeholder value. But getting error saying no bean found for the specific name. I got to know that the issue is with reading the property value, because while hardcoding the value it's working. Please help as I need to read the value from property file. Code snippet below:

application.properties

event.testRequest=TEST_REQUEST

Service Class

@Service("${event.testRequest}") // This is not working, getting "No bean named 'TEST_REQUEST' available" error
// @Service("TEST_REQUEST")     // This is working
public class TestRequestExecutor extends DefaultExecutionService {
...
}

Also, to confirm the property value is reading correctly I tried using @Value("${event.testRequest}") private String value where I'm getting the value "TEST_REQUEST" as expected. Not sure how to use that with @Service annotation.

Edit: To elaborate the need of externalising the service bean name, I'm using factory pattern in getting the implementation based on the event name (event names such as Event1, Event2..). If there's a change in the event name, the change will be just on property file rather than the Service bean name which is using the property placeholder.

    @RestController
    public class RequestProcessController {
    
    @Autowired
    private ExecutorFactory executorFactory;
    ..
    ExecutionService executionService = executorFactory.getExecutionService(request.getEventType());
    executionService.executeRequest(request);
..
}


@Component
public class ExecutorFactory {

private BeanFactory beanFactory;

public ExecutionService getExecutionService(String eventType) {
  return beanFactory.getBean(eventType, DefaultExecutionService.class);
}

Here DefaultExecutionService has different implementations like below..

@Service("${event.first}")
public class Event1Executor extends DefaultExecutionService {..}
..
@Service("${event.second}")
public class Event2Executor extends DefaultExecutionService {..}

event.first = Event1
event.second = Event2

So basically in future if Event1 name is updated to EventOne, I just need to update the property file, not the service class.

Any help much appreciated! Thanks!

Shihad Salam
  • 79
  • 2
  • 11
  • You cannot use placeholders in `@Component` annotations (`@Service` is one of those)`. also dynamically assigning a name doesn't really make sense in my book. – M. Deinum Jan 20 '21 at 07:59
  • Can you elaborate on why do you really need to do that? names of spring beans are required to be referenced by spring for proper dependency injection resolution rules. I've never seen the need to make them dynamic. I believe there are other ways to solve the issue if you'll formulate the problem... – Mark Bramnik Jan 20 '21 at 08:44
  • @MarkBramnik We have different implementations. We need to externalize this to read from property file since the name of each of the placeholder (these are event names) might differ in future, and it will be updated in the property file. So instead of making the code changes, we can still go with the existing code since the services are referring the property value. Hope it's clear. – Shihad Salam Jan 20 '21 at 09:17
  • Not really clear, so you have different implementations of DefaultExecutionService, right? Assuming it can work, where do you use the resolved bean names, I mean, if spring was able to resolve @Service("someService") or @Service("anotherService") from the configuration file, where do you use string "someService" or "anotherService" in the application? If you need to load only one implementation out of many, you can use \@ConditionalOnProperty instead, in some other cases using \@Profile can be handy (under the hood its the same though), but its different technique from what you're asking... – Mark Bramnik Jan 20 '21 at 09:20
  • @MarkBramnik Please see the edit section in my post. I have explained it in detail. Please let me know if it's still unclear. – Shihad Salam Jan 20 '21 at 09:34

2 Answers2

1

Ok, Now its clear.

I think you can achieve such a behavior by changing the implementation:

You don't need to work with bean factory inside ExecutorFactory, instead consider creating the following implementation:

@AllArgsConstructor // note, its not a component - I'll use @Configuration
public class ExecutorFactory {
   private final Map<String, DefaultExecutionService> executorByEventName;

   public DefaultExecutorService  getExecutionService(String eventType) {
        return executorByEventName.get(eventType);
   }

Now the creation of such a map is something that is tricky and requires a different approach:

Don't use property resolution in your implementations of the executor service, instead go with some way of "static identification", it can be another annotation or maybe qualifier or even static bean name. In this example I'll go with a qualifier based approach since it seems to me the easiest to show/implement.

@Service
@Qualifier("evt1")
public class TestRequestExecutor1 extends DefaultExecutionService {
...
}

@Service
@Qualifier("evt2")
public class TestRequestExecutor2 extends DefaultExecutionService {
...
}

Then you can create an ExecutorFactory from Java Configuration class, that's why I haven't put the @Component/@Service annotation on the ExecutorFactory class at the beginning of the answer.

@Configuration
public class MyConfiguration {
    @Bean
    public ExecutorFactory executorFactory(Map<String, DefaultExecutorService> 
        allServicesByQualifierName, MyConfigurationProperties config) {
        Map<String, DefaultExecutorService> map = new HashMap<>();
        allServicesByQualifierName.forEach((qualifierName, serviceBean) -> {
             String actualEventName = config.getMappedEventName(qualifierName);
             map.put(actualEventName, serviceBean);
        });
        return new ExecutorFactory(map);    
    }
} 

First of all, I use here a feature of spring that allows to inject map of string to your interface (in this case DefaultExecutorService). Spring will inject a map of:

evt1 --> bean of type TestRequestExecutor1 
evt2 --> bean of type TestRequestExecutor2

Then I access the configuration that is supposed to support a method for getting all the events. This can be implemented in different ways, probably the most natural way of doing this is using @ConfigurationProperties annotation and mapping the event map of application.yaml to the Map in Java. You can read this article for technical details.

As a side note, although I've used @Configuration approach because It looks more clear to me, it's possible to use @Service on the ExecutorFactory, but then the similar logic that I've shown will be a part of the executor factory (constructor or post-construct method), you still can inject map of bean names to actual beans and configuration properties to the constructor, its up to you to decide

Mark Bramnik
  • 39,963
  • 4
  • 57
  • 97
0

There is something you can try. Although, I won't recommend doing this.

Try creating a BeanNameGenerator and supply it to SpringApplicationBuilder using the method beanNameGenerator(BeanNameGenerator beanNameGenerator). If you are curious, here is a link to the default implementation.

If I understand correctly, you have multiple implementations for this service, and you have to choose one based on the name that is provided in the properties file. If that is the case, take a look at this. And if those implementations depend on different profiles, take a look at this.

Edit after detailed explanation:

I think the simplest way to achieve this is to register your own beans. So remove @Service annotations from your executors. Then, use DefaultListableBeanFactory to register your own BeanDefinition for the executors.

The code would look something like this:

@Value("${event.first}")
String event1;

DefaultListableBeanFactory context = .. //Get BeanFactory

GenericBeanDefinition gbd = new GenericBeanDefinition();
gbd.setBeanClass(Event1Executor.class);
gbd.getPropertyValues().addPropertyValue("someProperty", "someValue");

context.registerBeanDefinition(event1, gbd);
Event1Executor bean = (Event1Executor) context.getBean(event1);

You can probably use BeanFactoryAware to get the bean factory, and BeanDefinitionBuilder if you want to set additional parameters before registering the bean.

Pranjal Gore
  • 532
  • 5
  • 14