Why use spring.factories for Spring Boot auto-configuration instead of annotations? - spring-boot

The documentation states:
Developing auto-configuration and using conditions
If you work in a company that develops shared libraries, or if you work on an open-source or commercial library, you might want to develop your own auto-configuration. Auto-configuration classes can be bundled in external jars and still be picked-up by Spring Boot.
If I have annotations for everything else (even #AutoConfigureAfter or #AutoConfigureBefore annotations),
Why maintain a properties file to point to a class with an annotation?

Because we are not going to scan the world to figure out what auto-configuration classes exist in your project. For one, an auto-configuration is just a regular #Configuration class.
The way a Spring component is found is via explicit declaration or component scan but we need to know the list of auto-configuration classes way before we actually start the context.

When SpringBoot app is starting, it will not scan all the classes in jars, So SpringBoot starter should specify which classes are auto-configured. For example, in spring-boot-2.0.4.RELEASE, it initializes like this:
#SpringBootApplication
public class MyApplication {
public static void main(String[] args) {
//1. method run will call the construtor below
SpringApplication.run(MyApplication.class, args);
}
}
public SpringApplication(ResourceLoader resourceLoader, Class<?>... primarySources) {
this.resourceLoader = resourceLoader;
Assert.notNull(primarySources, "PrimarySources must not be null");
this.primarySources = new LinkedHashSet<>(Arrays.asList(primarySources));
this.webApplicationType = deduceWebApplicationType();
//2. find all the classes whose key is ApplicationContextInitializer in spring.factories and initialize them
setInitializers((Collection) getSpringFactoriesInstances(
ApplicationContextInitializer.class));
setListeners((Collection) getSpringFactoriesInstances(ApplicationListener.class));
this.mainApplicationClass = deduceMainApplicationClass();
}
...
private <T> Collection<T> getSpringFactoriesInstances(Class<T> type,
Class<?>[] parameterTypes, Object... args) {
ClassLoader classLoader = Thread.currentThread().getContextClassLoader();
// Use names and ensure unique to protect against duplicates
Set<String> names = new LinkedHashSet<>(
//3. use current thread classcloader to load resources in the classpath
SpringFactoriesLoader.loadFactoryNames(type, classLoader));
List<T> instances = createSpringFactoriesInstances(type, parameterTypes,
classLoader, args, names);
AnnotationAwareOrderComparator.sort(instances);
return instances;
}
//SpringFactoriesLoader.java
public static List<String> loadFactoryNames(Class<?> factoryClass, #Nullable ClassLoader classLoader) {
String factoryClassName = factoryClass.getName();
// 3.1 first find the configuration file
return loadSpringFactories(classLoader).getOrDefault(factoryClassName, Collections.emptyList());
}
private static Map<String, List<String>> loadSpringFactories(#Nullable ClassLoader classLoader) {
...
try {
Enumeration<URL> urls = (classLoader != null ?
// public static final String FACTORIES_RESOURCE_LOCATION = "META-INF/spring.factories";
//4. spring.factories file is defined here
classLoader.getResources(FACTORIES_RESOURCE_LOCATION) :
ClassLoader.getSystemResources(FACTORIES_RESOURCE_LOCATION));
result = new LinkedMultiValueMap<>();
...
}

all the entries in spring.factories are loaded by below method -
org.springframework.boot.autoconfigure.ImportAutoConfigurationImportSelector#loadFactoryNames
protected Collection<String> loadFactoryNames(Class<?> source) {
return SpringFactoriesLoader.loadFactoryNames(source, getClass().getClassLoader());
}
SpringFactoriesLoader belongs to the spring-core library see below screen shot

Improve the startup performance by creating a static list of candidates at compilation time.Just scan a static 'spring.factories' file, avoiding scanning a large number of class files in jars. Similar to #indexed in springframework 5.0. --META-INF/spring.components.
Centralized configuration management, concise and compatible.

Related

Quarkus extension using a repository based on PanacheMongoRepository

I'm currently working on a Quarkus extension which is basically a filter that is using a PanacheMongoRepository. Here is a code snippet (this is in the runtime part of the extension) :
#Provider
#Priority(Priorities.AUTHORIZATION)
#AuthorizationSecured
public class AuthorizationFilter implements ContainerRequestFilter {
// Some injection here
#Inject
UserRepository userRepository;
#Override
public void filter(ContainerRequestContext requestContext) throws IOException {
// Some business logic here...
UserEntity userEntity = userRepository.findByName(name);
// Some business logic here...
}
}
The repository :
#ApplicationScoped
public class UserRepository implements PanacheMongoRepository<UserEntity> {
public UserEntity findByName(String name) {
return find("some query...", name).firstResult();
}
}
When the repository is called, I get the following exception:
org.jboss.resteasy.spi.UnhandledException: java.lang.IllegalStateException: This method is normally automatically overridden in subclasses...
java.lang.IllegalStateException: This method is normally automatically overridden in subclasses\n\tat io.quarkus.mongodb.panache.common.runtime.MongoOperations.implementationInjectionMissing(MongoOperations.java:765)\n\tat io.quarkus.mongodb.panache.PanacheMongoRepositoryBase.find(PanacheMongoRepositoryBase.java:119)
The processor
class AuthorizeProcessor {
private static final String FEATURE = "authorize";
#BuildStep
FeatureBuildItem feature() {
return new FeatureBuildItem(FEATURE);
}
#BuildStep(onlyIf = IsAuthorizeEnabled.class)
void registerAuthorizeFilter(
BuildProducer<AdditionalBeanBuildItem> additionalBeanProducer,
BuildProducer<ResteasyJaxrsProviderBuildItem> resteasyJaxrsProviderProducer
) {
additionalBeanProducer.produce(new AdditionalBeanBuildItem(UserRepository.class));
additionalBeanProducer.produce(new AdditionalBeanBuildItem(AuthorizationFilter.class));
resteasyJaxrsProviderProducer.produce(new ResteasyJaxrsProviderBuildItem(AuthorizationFilter.class.getName()));
}
}
Any idea ?
Thanks for your help :)
MongoDB with Panache (and the same for Hibernate with Panache) uses bytecode enhancement at build time. When this enhancement didn't occurs it leads to the exception you mentionned at runtime: java.lang.IllegalStateException: This method is normally automatically overridden in subclasses
It can occurs only when the repository or entity is not in the Jandex index. Jandex is used to index all the code of your application to avoid using reflection and classpath scanning to discover classes. If your entity / repository is not in the index this means it's not part of your application as we automatically index the classes of your application, so it must be inside an external JAR.
Usually, this is solved by adding the Jandex plugin to index the code of the external JAR (in fact there is multiple way to do this, see How to Generate a Jandex Index).
An extension suffer from the same issue as extensions are not indexed by default. But from an extension you can index the needed classes via a build step wich is more easy and avoid polluting the index with classes that are not needed.
This can be done by generating a new AdditionalIndexedClassesBuildItem(UserRepository.class.getName()) inside a build step.

How to initiate a global method that can be accessed by all controllers in Spring Boot

I would like to initiate a simple POJO that generates an array of random Strings when initiated (or when the Spring Boot application starts). This array of Strings has to be shared across every controller in the Spring Boot application, and it cannot be different for different controllers. The class and methods (of this shared POJO) are internal to a Spring Boot application and are accessed by calling the getters in the controller methods (only).
Furthermore, I would like to avoid using the application.properties. The best solution would be java-only (no database such as H2 or offloading the POJO onto a file). Also, using the sessions will not help.
Something like this would help:
http://www.masterspringboot.com/security/authentication/securing-spring-boot-with-in-memory-basic-authentication
How can I accomplish such a design ?
My idea is to simply use a micro-service and launch it separately, but I would like to confirm if there is something else I can do within the single Spring Boot application.
You can create a singleton class that holds your values:
public class Main {
public static void main(String[] args) {
DataHolder dataHolder = DataHolder.getInstance();
String[] arr = dataHolder.getArr();
}
}
class DataHolder {
private static DataHolder instance = null;
private String[] arr = new String[10];
public static DataHolder getInstance() {
if (instance == null)
instance = new DataHolder();
return instance;
}
private DataHolder() {
fillArray();
}
private void fillArray() {
// use this method to fill your array
}
public String[] getArr() {
return arr;
}
}
To answer my own question, the solution was to use the in-memory provided by the mapDB, and generate all the data during the spring-boot initiating itself...
https://mvnrepository.com/artifact/org.mapdb/mapdb
The source code example can be found using the search engines...

How to test #ConfigurationProperties with ApplicationContextRunner from spring-boot-test?

I need to test my autoconfiguration classes that make use of #ConfigurationProperties beans. I'm making use of ApplicationContextRunner as documented in https://docs.spring.io/spring-boot/docs/current/reference/html/spring-boot-features.html#boot-features-test-autoconfig to make tests faster and avoid starting the servlet container between each variations. However, beans annotated with #AutoconfigurationProperties are not populated with values injected into ApplicationContextRunner.
I suspect that I'm hitting problem similar to https://stackoverflow.com/a/56023100/1484823
#ConfigurationProperties are not managed by the application context you build in tests, although they will be load when the application launches, because you have #EnableConfigurationProperties on your app main class.
How can I enable support for #ConfigurationProperties with ApplicationContextRunner ?
Here is the corresponding code
#Test
void ServiceDefinitionMapperPropertiesAreProperlyLoaded() {
ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(
SingleServiceDefinitionAnswerAutoConfig.class,
DynamicCatalogServiceAutoConfiguration.class
))
// .withPropertyValues(DynamicCatalogProperties.OPT_IN_PROPERTY + "=true") //Not sure why this seems ignored
.withSystemProperties(DynamicCatalogConstants.OPT_IN_PROPERTY + "=true",
ServiceDefinitionMapperProperties.PROPERTY_PREFIX
+ServiceDefinitionMapperProperties.SUFFIX_PROPERTY_KEY+ "=suffix")
;
contextRunner.run(context -> {
assertThat(context).hasSingleBean(ServiceDefinitionMapperProperties.class);
ServiceDefinitionMapperProperties serviceDefinitionMapperProperties
= context.getBean(ServiceDefinitionMapperProperties.class);
assertThat(serviceDefinitionMapperProperties.getSuffix()).isEqualTo("suffix");
});
}
which fails with:
org.opentest4j.AssertionFailedError:
Expecting:
<"">
to be equal to:
<"suffix">
but was not.
Expected :suffix
Actual :
<Click to see difference>
at org.springframework.cloud.appbroker.autoconfigure.DynamicCatalogServiceAutoConfigurationTest
public class DynamicCatalogServiceAutoConfiguration {
[...]
#Bean
#ConfigurationProperties(prefix=ServiceDefinitionMapperProperties.PROPERTY_PREFIX, ignoreUnknownFields = false)
public ServiceDefinitionMapperProperties serviceDefinitionMapperProperties() {
return new ServiceDefinitionMapperProperties();
}
[...]
}
Full sources available at https://github.com/orange-cloudfoundry/osb-cmdb-spike/blob/0da641e5f2f811f48b0676a25c8cbe97895168d1/spring-cloud-app-broker-autoconfigure/src/test/java/org/springframework/cloud/appbroker/autoconfigure/DynamicCatalogServiceAutoConfigurationTest.java#L89-L107
ps: I was about to submit an issue to https://github.com/spring-projects/spring-boot/issues to suggest documentation enhancement to warn of such limitation in ApplicationContext, and to ask for ways to turn on support for #ConfigurationProperties. Following guidance at https://raw.githubusercontent.com/spring-projects/spring-boot/master/.github/ISSUE_TEMPLATE.md, I'm first making sure here I'm not misunderstanding the problem.
If you want to populate a bean annotated with #ConfigurationProperties class as part of your test, and you normally depend on a configuration class annotated with #EnableConfigurationProperties to populate that bean, then you can create a trivial configuration class just for the test:
#ConfigurationProperties("app")
public class ConfigProps {
private int meaningOfLife;
public int getMeaningOfLife() { return meaningOfLife; }
public void setMeaningOfLife(int meaning) { this.meaningOfLife = meaning; }
}
class ConfigPropsTest {
private final ApplicationContextRunner runner = new ApplicationContextRunner();
#EnableConfigurationProperties(ConfigProps.class)
static class TrivialConfiguration {
}
#Test
void test() {
runner.withUserConfiguration(TrivialConfiguration.class)
.withPropertyValues("app.meaning-of-life=42")
.run(context -> {
assertEquals(42, context.getBean(ConfigProps.class).getMeaningOfLife());
});
}
}
Passing TrivialConfiguration to the ApplicationContextRunner is sufficient to make it create ConfigProps and populate it using the available properties.
As far as I can tell, none of the classes involved in your test enable configuration property binding. As a result, no properties are bound to ServiceDefinitionMapperProperties. You can enable configuration property binding using #EnableConfigurationProperties. A typical place to add it would be on DynamicCatalogServiceAutoConfiguration as its serviceDefinitionMapperProperties bean relies on configuration properties being enabled.

creating custom PropertySourcesPlaceholderConfigurer using property loaded dynamically

I am facing some challenge creating PropertySourcesPlaceholderConfigurer based on some value that is available in another property file.
I have a property file, custom-{environment}.property, which contains a value, that is needed to set location of PropertySourcesPlaceholderConfigurer.
My CustomConfiguration looks something like:
#Bean
public static PropertySourcesPlaceholderConfigurer placeholderConfigurer() {
PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer = new PropertySourcesPlaceholderConfigurer();
propertySourcesPlaceholderConfigurer.setLocation(customLocation);
//Custom url location based on a String available in the properties file. This is the problem area
return propertySourcesPlaceholderConfigurer;
}
I want to populate this customLocation from the properties file. Tried autowiring Environment, but it's failing as environment is null when placeholderConfigurer() is getting called. Tried using #PropertySource("custom-${environment}.property") and then #Value("**customLocation**"), but that's also not working.
Please let me know how this can be done. Thanks in advance.
I would suggest adding an ApplicationContextInitializer to load your property files instead of a plain #PropertySource. First load your custom-{environment}.properties next your configurable properties file.
public class PropertySourceInitializer implements ApplicationContextInitializer {
private static final String DEFAULT_CONFIG_FILE = "classpath:custom-${environment}.properties";
#Override
public void initialize(ConfigurableApplicationContext applicationContext) {
final ConfigurableEnvironment env = applicationContext.getEnvironment();
final MutablePropertySources mps = env.getPropertySources();
//
Resource resource = applicationContext.getResource(env.resolvePlaceholders(DEFAULT_CONFIG_FILE));
mps.addLast(new ResourcePropertySource(resource.getDescription(), resource));
String additional = env.getProperty("name.of.property");
if (StringUtils.hasText(additional) {
Resource additionalResource = applicationContext.getResource(env.resolvePlaceholders(additional));
if (additionalResource.isReadable() ) {
mps.addLast(new ResourcePropertySource(resource.getDescription(), resource));
}
}
}
}
Trying to get it to work with a #PropertySource will be much harder as the phases in which the PropertySourcesPlaceHolderConfigurer is created is different then the one in which the #PropertySource annotations are scanned. Staged loading of #PropertySource (which is basically what you want) is quite difficult. Spring Boot also has its own loading mechanism (which actually is also a ApplicationContextInitializer.
Can you try setting your location as a system property?
#Value("#{ systemProperties['myapp.location'] }")
private String location;
You need to set "myapp.location" as system property.

Customize #PropertyResource handling behavior for java annotation based configuration

I wish to customize the handling of the property source, while using java annotation based intializing a spring web application.
#Configuration
#PropertySource("ldap.properties")
#Repository
public class LdapDao {
...
#Autowired
public void setEnv(Environment env) throws NamingException {
this.url = env.getProperty("url").trim();
this.user = env.getProperty("user").trim();
this.password = env.getProperty("password).trim();
this.initializeLdapContext();
}
...
}
In this case, spring will look for the property source on classpath. If the property source is declared as:
#PropertySource("file:/${conf.dir}/ldap.properties")
ldap.properties is searched under the directory specified by the system property "conf.dir".
I need the behavior where the property resource is first searched under the directory specified by the system property "conf.dir". If it is not found there, its location defaults to classpath.
Any suggestion on how to achieve this behavior?
Use this
#PropertySource({"ldap.properties", "file:/${conf.dir}/ldap.properties"})
and you will get last one, just add in yours propConfig code
PropertySourcesPlaceholderConfigurer propConfig = new PropertySourcesPlaceholderConfigurer();
//.....
propConfig.setIgnoreResourceNotFound(true);
propConfig.setIgnoreUnresolvablePlaceholders(false);
propConfig.setLocalOverride(true);
//....

Resources