For people in the Java ecosystem Spring is, and has been for some time, a popular framework. It's used across much of the landscape, from web apps to batch processing. While there are newer languages, and indeed newer Java frameworks, Spring remains a strong contender in many new projects, particularly in the enterprise space.
At $DAYJOB recently I came across a project where there was a desire to use Spring Boot as an application platform, while allowing the application to make use of third-party integrations for external dependencies. The application was being built in one environment but there was an expectation that this same application would be deployed in many other environments, aka at clients. Those other environments will have vastly different arrangements, but will be able to provide integrations through adaptors.
The ridiculously simplified pattern thus:
interface CRM {
Collection<Customer> search(String query);
}
class BankCRM implements CRM {
public Collection<Customer> search(String query) {
// go play with CORBA, SOAP or COBOL and find me the customers
}
}
class FintechCRM implements CRM {
public Collection<Customer> search(String query) {
// go query the cloud native grpc graph mesh thingy and find me the customers
}
}
The environments may be very different but the business case is largely the same.
The team building the application were told that the third-party implementations of these adaptors should not be required to use Spring, they would just be implemented against the POJO Java interfaces. An SPI in other words. As a result of this requirement the team set about building a dynamic resolution mechanism, looking up implementation class details in conventionally located .properties files. The result looked quite similar to java.util.ServiceLoader.
I thought I'd sketch out some ways that it's possible to leverage Spring to achieve the same thing in a more idiomatic way.
The Outline
@SpringBootApplication
class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class);
}
@Bean
public CommandLineRunner clr(CRM crm) {
return (String[] args) -> {
System.out.println(crm.search(args[0]).stream()
.map(Objects::toString)
.collect(Collectors.joining(", ")));
};
}
}
The CRM instance needs to be injected at runtime, without the application knowing anything about the implementation at compile time.
Solutions
Option #1
@Bean
@ConditionalOnProperty(name = "crm.implementationClass")
public FactoryBean<CRM> crmByConfiguration(@Value("${crm.implementationClass}" Class<? extends CRM> implementationClass) {
return new AbstractFactoryBean<>() {
@Override
public Class<?> getObjectType() { return CRM.class; }
@Override
protected CRM createInstance() throws Exception {
return implementationClass.getDeclaredConstructor().newInstance();
}
};
}
Declare a FactoryBean which determines the implementation class based on some Environment configuration, eg:
crm:
implementationClass: example.FintechCRM
The example uses @ConditionalOnProperty to ensure the @Bean is only defined if there's configuration for it.
Option #2
@Bean
@ConditionalOnMissingBean(CRM.class)
public FactoryBean<CRM> crmByScanning() {
return new AbstractFactoryBean<>() {
@Override
public Class<?> getObjectType() { return CRM.class; }
@Override
protected CRM createInstance() throws Exception {
var scanner = new ClassPathScanningCandidateComponentProvider(false);
scanner.addIncludeFilter(new AssignableTypeFilter(CRM.class));
// exceptions ignored for brevity
return scanner.findCandidateComponents("")
.stream()
.map(BeanDefinition::getBeanClassName)
.map(Class::forName)
.map(clazz -> clazz.getDeclaredConstructor().newInstance())
.map(CRM.class::cast)
.findFirst()
.orElseThrow()
}
};
}
Detect the implementation through class-path scanning. This one uses @ConditionalOnMissingBean to ensure the configuration doesn't try to replace an existing bean.
Enterprise Approved Option #3
There's something that old school enterprises love...
@SpringBootApplication
@ImportResource("file:/etc/acme-banking/crm.xml")
class Application {
...
}
along with an /etc/acme-banking/crm.xml file
<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE beans PUBLIC "-//SPRING//DTD BEAN 2.0//EN"
"http://www.springframework.org/dtd/spring-beans-2.0.dtd">
<beans>
<bean class="example.FintechCRM"/>
</beans>
Leveraging an old, and yet still supported, mechanism for defining beans from plain ol' POJOs.
Conclusion
This just explores a couple of ways of pulling plain POJOs into Spring at runtime. There will be many others, but these seemed like low-overhead options to try out.