Spring 4 #Conditional: Environment properties not available unless #PropertySource is used - java

Problem:
When using #Conditional bean definitions within #Configuration classes, properties from external sources, e. g. files, are not available in the Environment when Condition#matches is evaluated - unless #PropertySource is used. Using ConfigurationCondition instead of Condition doesn't make a difference, no matter which ConfigurationPhase is used.
Since I also have the requirement to use wildcards for the names of my properties files, using #PropertySource is not an option, since it requires the locations to be absolute. Spring Boot is completely off the table, so #ConditionalOnProperty is not an option either. This leaves me with defining a PropertySourcesPlaceholderConfigurer bean as the only remaining viable option.
Question:
Is there any way to define beans depending on the presence, absence or value of properties in the Environment using plain Spring mechanisms (not boot, and not some other hacky way) while also specifying property locations with wildcards and if so, what would need to be done?
Example:
In the following JUnit test, I expect a bean named bean to be available, iff the property key is available (i. e. not null) in the Environment during the evaluation of the matches method in the PropertyCondition. This is not the case, thus the test fails.
#RunWith(SpringJUnit4ClassRunner.class)
#ContextConfiguration(classes = { ConditionTest.ConditionTestConfiguration.class })
public class ConditionTest {
private static final String PROPERTIES_LOCATION = "test.properties";
private static final String BEAN_NAME = "bean";
#Autowired
private ApplicationContext applicationContext;
#Test
public void testBeanNotNull() {
assertNotNull(applicationContext.getBean(BEAN_NAME));
}
#Configuration
static class ConditionTestConfiguration {
#Bean
public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer = new PropertySourcesPlaceholderConfigurer();
propertySourcesPlaceholderConfigurer.setLocation(new ClassPathResource(PROPERTIES_LOCATION));
return propertySourcesPlaceholderConfigurer;
}
#Bean
#Conditional(PropertyCondition.class)
public Object bean() {
return BEAN_NAME;
}
}
static class PropertyCondition implements ConfigurationCondition {
private static final String PROPERTY_KEY = "key";
#Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
return context.getEnvironment().getProperty(PROPERTY_KEY) != null;
}
#Override
public ConfigurationPhase getConfigurationPhase() {
return ConfigurationPhase.REGISTER_BEAN;
}
}
}
Once I add an #PropertySource annotation to the ConditionTestConfiguration like this:
#Configuration
#PropertySource("classpath:test.properties")
static class ConditionTestConfiguration
the property key is available, thus the PropertyCondition#matches method evaluates to true, thus bean is available in the ApplicationContext and the test passes.
Additional Information:
A file called test.properties containing key=value exists in the classpath
I used a ConfigurationCondition with ConfigurationPhase.REGISTER_BEAN, just to show that it doesn't change the behaviour for the better
Package declaration and import statements are omitted for brevity

Related

Overriding spring beans with the same Qualifier Name

I have 2 configuration classes in my spring application.
Configuration and AnotherConfiguration. The AnotherConfiguration is conditioned to create beans only if a certain parameter is provided (this is handled by the ConditionalOnClass annotation).
Configuration.java
#Configuration
public class Configuration {
#Bean
public Stage testStage() {
return someStage1;
}
#Bean
public Stage testStage2() {
return someStage2;
}
}
AnotherConfiguration.java
#Configuration
#ConditionalOnClass()
public class AnotherConfiguration {
#Bean
public Stage testStage2() {
return newStage2;
}
}
The use case is that if I supply an argument that satisfies the Conditional argument for AnotherConfiguration, newStage2 should be returned to all the classes expecting a testStage2 bean. But currently, the testStage2 bean is being resolved from Configuration class instead of being overridden by AnotherConfiguration.
I have tried adding the #Primary annotation to the definition in AnotherConfiguration but that just resolves newStage2 to all the classes expecting bean of type Stage irrespective of the qualifier. Is there a way to instruct spring to override bean definitions only of the same QualifierName (here testStage2.
Due to the project constraints, I cannot make changes to Configuration.java but can make any change to AnotherConfiguration.java keeping the name (testStage2()) same.
I really don't recomend it but
use a conditional instead of an onClass because that will always be true without params
public class Cond implements Condition {
#Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
return false;
}
}
and then define the overridden bean to load into the context
#Component("testStage2")
#Conditional(value = Cond.class)
#Primary
public class AnotherStage extends Stage {
public AnotherStage(){
//do whatever
}
}
Sorry bean style
#Configuration
public class AnotherConfiguration {
#Bean("testBean2")
#Conditional(value = Cond.class)
#Primary
public Stage testStage2() {
return newStage2;
}
}

Spring Boot Cucumber tests could not resolve placeholder 'random.uuid'

I want a Spring Boot property to have an impossible to guess random value by default (for security reasons), so I am trying to use a random UUID as the default value, using code like this:
#Service
public class UserServiceImpl implements UserService {
...
#Autowired
public UserServiceImpl(#NonNull final PasswordEncoder passwordEncoder,
#NonNull final UserRepository userRepository,
#NonNull #Value("${administrator.password:${random.uuid}}") final String administratorPassword) {
...
}
But my Cucumber Spring Boot tests are complaining about the ${random.uuid} thus:
org.springframework.beans.factory.BeanCreationException: Error creating bean with name 'userServiceImpl' defined in file [.../UserServiceImpl.class]: Unexpected exception during bean creation; nested exception is java.lang.IllegalArgumentException: Could not resolve placeholder 'random.uuid' in value "administrator.password:${random.uuid}"
What do I have to do to get my application to use a random property value?
The problem might be related to test slicing. If I run a clean Spring boot project's test with:
#SpringBootTest
class DemoApplicationTests {
#Value("${nonexistingValue:${random.uuid}}")
private String someVal;
#Test
public void someTest() {
assertThat(someVal).contains("-");
}
}
The test passes. However, if I change #SpringBootTest to #ExtendWith({SpringExtension.class}) or #RunWith(SpringRunner.class), the test fails. ${random.uuid} and similar expressions should be available in a normal runtime environment.
Because of this slicing, it seems that the RandomValuePropertySource is not available. A rather inelegant work-around is to explicitly add it to the context, using a PropertySourcesPlaceholderConfigurer bean created in your test context:
#Configuration
public class CucumberBeansConfiguration {
#Bean
public PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer() {
final var configurer = new PropertySourcesPlaceholderConfigurer();
final var sources = new MutablePropertySources();
sources.addFirst(new RandomValuePropertySource());
configurer.setPropertySources(sources);
return configurer;
}
}
${random.uuid} can only be used if RandomValuePropertySource is available.
As workaround, you can define a "dummy default value" and detect this in your code:
#Autowired
public UserServiceImpl(#NonNull final PasswordEncoder passwordEncoder,
#NonNull final UserRepository userRepository,
#NonNull #Value("${administrator.password:no-default-vale-provided}") final String administratorPassword) {
this.adminPassword = "no-default-value-provided".equals(administratorPassword)
? UUID.randomUUID().toString()
: administratorPassword;
}
If you have [`RandomValuePropertySource`](https://docs.spring.io/spring-boot/docs/current/api/org/springframework/boot/env/RandomValuePropertySource.html) available, then the SpEL expression `${random.uuid}` should indeed resolve to a random UUID. `${…}` is a property placeholder and `#{$…}` is SpEL syntax.
You need to change your annotation:
#NonNull #Value("#{${administrator.password} ?: ${random.uuid}}") final String administratorPassword
Cucumber uses Springs TestContextManager. By annotating your context configuration with #CucumberContextConfiguration Cucumber knows which class use to start the test context.
import com.example.app;
import org.springframework.boot.test.context.SpringBootTest;
import io.cucumber.spring.CucumberContextConfiguration;
#CucumberContextConfiguration
#SpringBootTest
public class CucumberSpringConfiguration {
}
Do make sure the CucumberSpringConfiguration is on the cucumber.glue path.

How to use #ConstructorBinding and #PropertySource together with #ConfigurationProperties with in Spring Boot 2.2.4?

I am new to Spring Boot. Currently, I am trying to create a POJO class (SystemProperties.class) to read the value in a properties file (parameter.properties separate from application.properties but still under the same directory /src/main/resources. The issue happens when I am using the #ConstructorBinding in the class in order for it to be immutable.
#ConstructorBinding needs to be used with #EnableConfigurationProperties or #ConfigurationPropertiesScan.
#ConfigurationPropertiesScan will ignore #Configuration annotation which is needed when using #PropertySource to specify external
*.properties file.
A) SystemProperties.class
#Configuration
#PropertySource("classpath:parameter.properties")
#ConstructorBinding
#ConfigurationProperties(prefix = "abc")
public class SystemProperties {
private final String test;
public SystemProperties (
String test) {
this.test = test;
}
public String getTest() {
return test;
}
B) parameter.properties
abc.test=text1
I have tried to remove the #PropertySource annotation but the value cannot be retrieved unless it is from the application.properties. Any help is greatly appreciated!
The way to solve this is to split the class into two classes with two different concerns. With such solution, you retain the SystemProperties class you created and additionally add another class simply for loading the properties file parameters to make them available to your application.
The solution would be as follows:
#ConstructorBinding
#ConfigurationProperties(prefix = "abc")
public class SystemProperties {
private final String test;
public SystemProperties(
String test) {
this.test = test;
}
public String getTest() {
return test;
}
}
Notice that I have omitted the #Configuration and #PropertySource annotations.
#Configuration
#PropertySource("classpath:parameter.properties")
public class PropertySourceLoader {
}
Notice I have simply added this annotations on a new class, solely created to load the properties file.
Finally, you can add #ConfigurationPropertiesScan on your main application class to enable the property mapping mechanism.

Access properties in BeanFactoryPostProcessor

I am trying to create something which will auto-create beans based on configurable properties (from application.yml and the like).
Since I can't just access the properties component like I normally would in the BeanFactoryPostProcessor, I'm kind of stumped how I can access them.
How can I access application properties in BeanFactoryPostProcessor?
If you want to access properties in a type-safe manner in a BeanFactoryPostProcessor you'll need to bind them from the Environment yourself using the Binder API. This is essentially what Boot itself does to support #ConfigurationProperties beans.
Your BeanFactoryPostProcessor would look something like this:
#Bean
public static BeanFactoryPostProcessor beanFactoryPostProcessor(
Environment environment) {
return new BeanFactoryPostProcessor() {
#Override
public void postProcessBeanFactory(
ConfigurableListableBeanFactory beanFactory) throws BeansException {
BindResult<ExampleProperties> result = Binder.get(environment)
.bind("com.example.prefix", ExampleProperties.class);
ExampleProperties properties = result.get();
// Use the properties to post-process the bean factory as needed
}
};
}
I didn't want to use the solution above that used an #Bean producer method since it's contrary to the recommended approach of annotating a class with #Component and picking up via component scanning. Fortunately it's straightforward to do that by implementing EnvironmentAware:
#Component
#Order(Ordered.HIGHEST_PRECEDENCE)
public class ConditionalDependencyPostProcessor implements BeanFactoryPostProcessor, EnvironmentAware {
/** Logger instance. */
private final Logger logger = LoggerFactory.getLogger(ConditionalDependencyPostProcessor.class);
/** Spring environment. */
private Environment environment;
#Override
public void setEnvironment(final Environment env) {
environment = env;
}
...
private boolean hasRequiredProfiles(final DependencyInfo info) {
final Set<String> activeProfiles = new HashSet<>(Arrays.asList(environment.getActiveProfiles()));
for (String profile : info.requiredProfiles) {
if (!activeProfiles.contains(profile)) {
return false;
}
}
return true;
}
I should note what did NOT work: trying to autowire an Environment constructor argument. BeanFactoryPostProcessors require a no-argument constructor and don't support autowiring, which is itself a feature implemented by another post processor, AutowiredAnnotationBeanPostProcessor.

How to read properties file in Controller using annotations only? Spring MVC

How to read properties file in Controller using annotations only?
Properties file contains (env.properties):
document.portal.path=http://flana.gost.com/service
Spring Controller:
#Controller
#RequestMapping("/kap/*")
#SessionAttributes({"user", "KapForm", "activity"})
public class KapController {
#Value("${document.portal.path}")
private String URL;
}
Nothing else is done. In XML, we use to use placeholder, which i am not getting how to introduce in it. So I am getting exception.
Injection of autowired dependencies failed;
You can achieve it in two ways
Option 1
In config class put #PropertySource and define a bean for PropertySourcesPlaceholderConfigurer as below -
#Configuration
#PropertySource("classpath:someFile.properties")
public class SampleConfig {
// other configs...
#Bean
public static PropertySourcesPlaceholderConfigurer placeHolderConfigurer() {
return new PropertySourcesPlaceholderConfigurer();
}
}
Option 2
In config class directly specify the bean for PropertyPlaceholderConfigurer and supply the name of property file as ClassPathResource
#Configuration
public class SampleConfig {
// other configs...
#Bean
public static PropertyPlaceholderConfigurer placeHolderConfigurer(){
PropertyPlaceholderConfigurer placeHolderConfigurer = new PropertyPlaceholderConfigurer();
ClassPathResource[] cpResources = new ClassPathResource[]
{ new ClassPathResource( "someFile.properties" ) };
placeHolderConfigurer.setLocations(cpResources);
placeHolderConfigurer.setIgnoreUnresolvablePlaceholders(true);
return placeHolderConfigurer;
}
}
Do note that the bean definition for place holder need to be static as per java docs (excerpts below)
Special consideration must be taken for #Bean methods that return Spring BeanFactoryPostProcessor (BFPP) types. Because BFPP objects must be instantiated very early in the container lifecycle, they can interfere with processing of annotations such as #Autowired, #Value, and #PostConstruct within #Configuration classes. To avoid these lifecycle issues, mark BFPP-returning #Bean methods as static.
Another way out found is
import org.springframework.context.MessageSource;
#Autowired
private MessageSource messageSource;
cutiePie = messageSource.getMessage("cutie.pie.property", new Object[] {},"cutie.pie.property", LocaleContextHolder.getLocale());

Categories

Resources