How to create multiple beans (same type) in one Spring Boot java config class (#Configuration)? - spring

In my yaml file, I have config values as below:
myapp:
rest-clients:
rest-templates:
- id: myService1
username: chris
password: li
base-url: http://localhost:3000/service1
read-timeout: 2s
connect-timeout: 1s
- id: myService2
username: chris
password: li
base-url: http://localhost:3000/service1
read-timeout: 2s
connect-timeout: 1s
I want to Spring Boot 2 app register a RestTemplate for each config items.
My configuration is bean is below:
#Configuration
#AllArgsConstructor
public class MyAppRestClientsConfiguration {
private MyAppRestClientsProperties properties;
private GenericApplicationContext applicationContext;
private RestTemplateBuilder restTemplateBuilder;
#PostConstruct
public void init() {
properties.getRestTemplates().forEach(this::registerRestTemplate);
}
private void registerRestTemplate(MyAppRestTemplateConfig config) {
// do some work
applicationContext.registerBean(config.getId(), RestTemplate.class, () -> restTemplate)
}
}
The problem is that when I inject my registered RestTemplate via #Autowire, this config bean has not finished init yet. So there is no RestTemplate bean could be injected.
#Autowired
#Qualifier("myService1")
private RestTemplate client1;
The injection point has the following annotations:
- #org.springframework.beans.factory.annotation.Autowired(required=true)
- #org.springframework.beans.factory.annotation.Qualifier(value=myService1)
Is there any correct way to implement this requirement?

The problem with registering new beans in a #PostConstruct annotated method is that Spring is already past that particular point in the Spring life cycle (more info on the Spring life cycle). Sometimes an annotation such as #DependsOn (already mentioned), #Order, or #Lazy might help. However, as you mentioned you'd rather not force (spring) implementation details upon projects that make use of your library, I've written a BeanFactoryPostProcessor that registers a RestTemplate bean:
#Component
public class DemoBeanFactoryPostProcessor implements BeanFactoryPostProcessor {
#Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) {
GenericBeanDefinition genericBeanDefinition = new GenericBeanDefinition();
genericBeanDefinition.setBeanClass(RestTemplate.class);
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
factory.setReadTimeout(Integer.valueOf(configurableListableBeanFactory.resolveEmbeddedValue("${rest-templates[0].read-timeout}")));
factory.setConnectTimeout(Integer.valueOf(configurableListableBeanFactory.resolveEmbeddedValue("${rest-templates[0].connect-timeout}")));
// etc
ConstructorArgumentValues constructorArgumentValues = new ConstructorArgumentValues();
constructorArgumentValues.addGenericArgumentValue(factory);
genericBeanDefinition.setConstructorArgumentValues(constructorArgumentValues);
String beanId = configurableListableBeanFactory.resolveEmbeddedValue("${rest-templates[0].id}");
((DefaultListableBeanFactory) configurableListableBeanFactory).registerBeanDefinition(beanId, genericBeanDefinition);
}
}
application.yml:
rest-templates:
- id: myService1
username: chris
password: li
base-url: http://localhost:3000/service1
read-timeout: 2000
connect-timeout: 1000
- id: myService2
username: chris
password: li
base-url: http://localhost:3000/service1
read-timeout: 2000
connect-timeout: 1000
Accompanying test:
#RunWith(SpringRunner.class)
#SpringBootTest
public class DemoApplicationTests {
#Autowired
#Qualifier("myService1")
private RestTemplate restTemplate;
#Test
public void demoBeanFactoryPostProcessor_shouldRegisterBean() {
String stackOverflow =
restTemplate.getForObject("https://stackoverflow.com/questions/57122343/how-to-create-multiple-beans-same-type-in-one-spring-boot-java-config-class", String.class);
Assertions.assertThat(stackOverflow).contains("How to create multiple beans (same type) in one Spring Boot java config class (#Configuration)?");
}
}
As the BeanFactoryPostProcessor is invoked before the application context is fully set up, I had to find a different way to retrieve the application properties. I used the method ConfigurableListableBeanFactory#resolveEmbeddedValue to retrieve placeholder values instead of having them injected by an #Value annotation or environment#getProperty. Furthermore, I rewrote the property value 2s to 2000 as the HttpComponentsClientHttpRequestFactory required an int value.

You can levarage the binding mechanism that Spring Boot uses for the #ConfigurationPropeties. The corresponding beans can then be generated dynamically from a map of configurations
#Configuration
public class MyConfiguration implements BeanFactoryPostProcessor, EnvironmentAware {
private Environment environment;
#Override
public void setEnvironment(Environment environment) {
this.environment = environment;
}
#Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory configurableListableBeanFactory) {
Binder.get(environment)
.bind("com.example", FooMapProps.class)
.get()
.getMap()
.forEach((name, props) -> configurableListableBeanFactory.registerSingleton(name + "FooService", new FooService(props.getGreeting())));
}
}
In this way you can create multiple FooServices with different configurations based on your YAML config
com:
example:
map:
hello:
greeting: 'hello there!'
hey:
greeting: 'hey ho :)'
The corresponding properties class looks like this
#ConfigurationProperties(prefix = "com.example")
public class FooMapProps {
private Map<String, FooProps> map;
public Map<String, FooProps> getMap() {
return map;
}
public void setMap(Map<String, FooProps> map) {
this.map = map;
}
public static class FooProps {
private String greeting;
public String getGreeting() {
return greeting;
}
public void setGreeting(String greeting) {
this.greeting= greeting;
}
}
}
In this example two FooService beans has been created with different greetings as constructor paramters. The beans can be accessed by the qualifiers 'helloFooService' and 'heyFooService'.

Related

How to add a Property Source to a Spring Cloud Stream Binder's Environment

I have written a PropertySource that enables classpath: prefix for spring.kafka.properties.ssl.truststore.location (which out-of-the-box is not supported).
Essentially, this lets me place a truststore.jks inside my Spring Boot application's src/main/resources folder and reference it from inside the .jar file.
This works nicely for plain Spring Kafka configurations, like these:
spring:
kafka:
properties:
ssl.truststore.location: classpath:myTruststore.jks
It currently fails when the same configurations are given in the context of a Spring Cloud Stream Binder:
spring:
cloud:
stream:
binders:
my-binder:
type: kafka
environment:
spring:
kafka:
properties:
ssl.truststore.location: classpath:myTruststore.jks
My PropertySource is not even called back, when I would have expected it to be called with a poperty name of spring.cloud.stream.binders.my-binder.environment.spring.kafka.properties.ssl.truststore.location.
I think my PropertySource that would do the classpath: resolution is not part of the Environment of the given Spring Cloud Stream binder.
Question: how can one add PropertySources to a specific Binder's environment (or to all of them)?
Thanks!
EDIT
I add my PropertySource in a Spring Boot auto-configuration like this:
#Configuration
#AutoConfigureBefore(KafkaAutoConfiguration.class)
#ConditionalOnProperty(name = "com.acme.kafka.enabled", matchIfMissing = true)
#EnableConfigurationPropertiesAcmeKafkaConfigurations.class)
public class AcmeKafkaAutoConfiguration {
#Bean
ClasspathResourceSupportEnablingPropertySource acmeKafkaClasspathResourceEnablingPropertySource(ConfigurableEnvironment environment) throws IOException {
ClasspathResourcesSupport classpathResourcesSupport = new ClasspathResourcesSupport(Files.createTempDirectory(ACME_KAFKA_PREFIX));
ClasspathResourceSupportEnablingPropertySource propertySource
= new ClasspathResourceSupportEnablingPropertySource(ClasspathResourceSupportEnablingPropertySource.NAME, environment, classpathResourcesSupport);
environment.getPropertySources().addFirst(propertySource);
return propertySource;
}
}
EDIT NO.2: I tried out what Gary Russel suggested below (using a Bean Post Processor declared as a static bean method).
It works but in my case I get a lot of additional warning logs at startup of the form:
Bean '...' of type [...] is not eligible for getting processed by all BeanPostProcessors (for example: not eligible for auto-proxying)
According to this post this can cause some really nasty side effects.
Here is the code I was using (which caused the warnings above):
#Configuration
#AutoConfigureBefore(KafkaAutoConfiguration.class)
#ConditionalOnProperty(name = "com.acme.kafka.enabled", matchIfMissing = true)
#EnableConfigurationProperties(AcmeKafkaConfigurations.class)
public class AcmeKafkaAutoConfiguration {
private static final String ACME_KAFKA_PREFIX = "acme.kafka.";
#Bean
#ConditionalOnMissingBean
public static List<ConnectivityConfigurationsProvider> acmeKafkaTokenProviders(OAuth2TokenClient oAuthClient, AcmeKafkaConfigurations configuration) {
List<ConnectivityConfigurationsProvider> connectivityConfigurationsProviders = new ArrayList<>();
configuration.getInstances().forEach(serviceInstanceConfiguration -> {
TokenProvider tokenProvider = new DefaultOAuth2TokenProvider(oAuthClient, serviceInstanceConfiguration);
ConnectivityConfigurationsProvider connectivityConfigurationsProvider = new ConnectivityConfigurationsProvider(serviceInstanceConfiguration, tokenProvider);
connectivityConfigurationsProviders.add(connectivityConfigurationsProvider);
});
return connectivityConfigurationsProviders;
}
#Bean
#ConditionalOnMissingBean
#Scope(ConfigurableBeanFactory.SCOPE_PROTOTYPE)
public static OAuth2TokenClient acmeKafkaOAuth2TokenClient() {
return new DefaultOAuth2TokenClient(new DefaultClientCredentialsTokenResponseClient());
}
#Bean
public static ConnectivityConfigurationsProviders acmeKafkaConnectivityConfigurationsProviders(AcmeKafkaConfigurations configuration, List<ConnectivityConfigurationsProvider> connectivityConfigurationsProviders) {
return new ConnectivityConfigurationsProviders(connectivityConfigurationsProviders);
}
#Bean
static NoOpBeanPostProcessor springKafkaConfigurationsPropertySource(ConfigurableEnvironment environment, ConnectivityConfigurationsProviders connectivityConfigurationsProviders) {
SpringKafkaConfigurationsPropertySource propertySource = new SpringKafkaConfigurationsPropertySource(SpringKafkaConfigurationsPropertySource.NAME, connectivityConfigurationsProviders);
environment.getPropertySources().addLast(propertySource);
return new NoOpBeanPostProcessor();
}
#Bean
#ConditionalOnClass(name = "org.springframework.cloud.stream.binder.BinderConfiguration")
static NoOpBeanPostProcessor springCloudStreamKafkaConfigurationsPropertySource(ConfigurableEnvironment environment, ConnectivityConfigurationsProviders connectivityConfigurationsProviders) {
SpringCloudStreamKafkaConfigurationsPropertySource propertySource = new SpringCloudStreamKafkaConfigurationsPropertySource(SpringCloudStreamKafkaConfigurationsPropertySource.NAME, connectivityConfigurationsProviders);
environment.getPropertySources().addLast(propertySource);
return new NoOpBeanPostProcessor();
}
#Bean
static NoOpBeanPostProcessor acmeKafkaConnectivityConfigurationsPropertySource(ConfigurableEnvironment environment, ConnectivityConfigurationsProviders connectivityConfigurationsProviders) {
AcmeKafkaConnectivityConfigurationsPropertySource propertySource = new AcmeKafkaConnectivityConfigurationsPropertySource(AcmeKafkaConnectivityConfigurationsPropertySource.NAME, connectivityConfigurationsProviders);
environment.getPropertySources().addLast(propertySource);
return new NoOpBeanPostProcessor();
}
#Bean
static NoOpBeanPostProcessor acmeKafkaClasspathResourceEnablingPropertySource(ConfigurableEnvironment environment) throws IOException {
ClasspathResourcesSupport classpathResourcesSupport = new ClasspathResourcesSupport(Files.createTempDirectory(ACME_KAFKA_PREFIX));
ClasspathResourceSupportEnablingPropertySource propertySource
= new ClasspathResourceSupportEnablingPropertySource(ClasspathResourceSupportEnablingPropertySource.NAME, environment, classpathResourcesSupport);
environment.getPropertySources().addFirst(propertySource);
return new NoOpBeanPostProcessor();
}
/**
* This BeanPostProcessor does not really post-process any beans.
* It is a way of getting the bean methods that add the property sources
* above to be called early enough in the lifecycle of Spring ApplicationContext
* creation.
*
* BeanPostProcessors are instantiated by Spring extremely early.
* #Bean methods providing them should be declared as static methods.
* See: https://stackoverflow.com/questions/30874244/bean-annotation-on-a-static-method
*/
static class NoOpBeanPostProcessor implements BeanPostProcessor, Ordered {
#Override
public int getOrder() {
return Ordered.LOWEST_PRECEDENCE;
}
}
}
EDIT
This seems to work:
#SpringBootApplication
public class So61826877Application {
private static final String KEY = "spring.cloud.stream.binders.my-binder.environment."
+ "spring.kafka.properties.ssl.truststore.location";
public static void main(String[] args) {
SpringApplication.run(So61826877Application.class, args);
}
#Bean
public static BeanPostProcessor configureSource(ConfigurableEnvironment env) {
Properties source = new Properties();
System.out.println(env.getProperty(KEY));
source.setProperty(KEY, "/path/to/myTruststore.jks");
env.getPropertySources().addFirst(new PropertiesPropertySource("cp", source));
return new MyBpp();
}
#Bean
Consumer<String> input() {
return System.out::println;
}
}
class MyBpp implements BeanPostProcessor, Ordered {
#Override
public int getOrder() {
return Integer.MAX_VALUE;
}
}
classpath:myTruststore.jks
...
ConsumerConfig values:
allow.auto.create.topics = true
auto.commit.interval.ms = 100
...
ssl.truststore.location = /path/to/myTruststore.jks
...

Spring Data Rest: #Autowire in Custom JsonDeserializer

I am trying to autowire a component into a custom JsonDeserializer but cannot get it right even with the following suggestions I found:
Autowiring in JsonDeserializer: SpringBeanAutowiringSupport vs HandlerInstantiator
Right way to write JSON deserializer in Spring or extend it
How to customise the Jackson JSON mapper implicitly used by Spring Boot?
Spring Boot Autowiring of JsonDeserializer in Integration test
My final goal is to accept URLs to resources in different microservices and store only the ID of the resource locally. But I don't want to just extract the ID from the URL but also verify that the rest of the URL is correct.
I have tried many things and lost track a bit of what I tried but I believe I tried everything mentioned in the links above. I created tons of beans for SpringHandlerInstantiator, Jackson2ObjectMapperBuilder, MappingJackson2HttpMessageConverter, RestTemplate and others and also tried with setting the SpringHandlerInstantiator in RepositoryRestConfigurer#configureJacksonObjectMapper.
I am using Spring Boot 2.1.6.RELEASE which makes me think something might have changed since some of the linked threads are quite old.
Here's my last attempt:
#Configuration
public class JacksonConfig {
#Bean
public HandlerInstantiator handlerInstantiator(ApplicationContext applicationContext) {
return new SpringHandlerInstantiator(applicationContext.getAutowireCapableBeanFactory());
}
}
#Configuration
public class RestConfiguration implements RepositoryRestConfigurer {
#Autowired
private Validator validator;
#Autowired
private HandlerInstantiator handlerInstantiator;
#Override
public void configureValidatingRepositoryEventListener(ValidatingRepositoryEventListener validatingListener) {
validatingListener.addValidator("beforeCreate", validator);
validatingListener.addValidator("beforeSave", validator);
}
#Override
public void configureJacksonObjectMapper(ObjectMapper objectMapper) {
objectMapper.setHandlerInstantiator(handlerInstantiator);
}
}
#Component
public class RestResourceURLSerializer extends JsonDeserializer<Long> {
#Autowired
private MyConfig config;
#Override
public Long deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
ServiceConfig serviceConfig = config.getServices().get("identity");
URI serviceUri = serviceConfig.getExternalUrl();
String servicePath = serviceUri.getPath();
URL givenUrl = p.readValueAs(URL.class);
String givenPath = givenUrl.getPath();
if (servicePath.equals(givenPath)) {
return Long.parseLong(givenPath.substring(givenPath.lastIndexOf('/') + 1));
}
return null;
}
}
I keep getting a NullPointerException POSTing something to the API endpoint that is deserialized with the JsonDeserializer above.
I was able to solve a similar problem by marking my deserializer constructor accept a parameter (and therefore removing the empty constructor) and marking constructor as #Autowired.
public class MyDeserializer extends JsonDeserializer<MyEntity> {
private final MyBean bean;
// no default constructor
#Autowired
public MyDeserializer(MyBean bean){
this.bean = bean
}
...
}
#JsonDeserialize(using = MyDeserializer.class)
public class MyEntity{...}
My entity is marked with annotation #JsonDeserialize so I don't have to explicitly register it with ObjectMapper.

Spring Boot Camel Testing

I need to test Camel routes in a Spring Boot Application.
I've the Spring boot main class with all the necessary beans declared in it.
I am using the CamelSpringJUnit4ClassRunner.class.
Added my Spring boot main class in #ContextConfiguration as it contains all the configurations. I don't have a separate configuration class.
I 've autowired CamelContext in my Test class:
#Autowired
CamelContext camelContext;
But the test fails with the error:
Caused by: org.springframework.beans.factory.NoSuchBeanDefinitionException:
No qualifying bean of type 'org.apache.camel.CamelContext' available: expected at least 1 bean which qualifies as autowire candidate.
Dependency annotations: {#org.springframework.beans.factory.annotation.Autowired(required=true)}
Try to use the CamelSpringBootRunner.class as the runner and add the #SpringBootTest annotation to the test class.
Example from the Camel repository
UPDATE (based on your comment)
If you change your bootstrapper class to SpringBootTestContextBootstrapper then it should work:
#BootstrapWith(SpringBootTestContextBootstrapper.class)
The equivalent configuration as you have but in this case you don't need to add the ContextConfiguration and the BootstrapWith annotation:
#RunWith(CamelSpringBootRunner.class)
#DirtiesContext(classMode = DirtiesContext.ClassMode.AFTER_EACH_TEST_METHOD)
#MockEndpoints("log:*")
#DisableJmx(false)
#SpringBootTest(classes = MyClass.class)
just enable #EnableAutoConfiguration it will work
With Camel 3.1 Spring Boot 2.2.5 and JUnit5, while also setting test application properties:
#CamelSpringBootTest
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
#TestPropertySource(properties = "spring.cloud.consul.enabled=false")
public class CamelRouteTest {
#Autowired
private TestRestTemplate restTemplate;
#Autowired
private CamelContext camelContext;
#EndpointInject("mock:bean:userService")
private MockEndpoint mockUserService;
private User user;
#BeforeEach
public void setUp() throws Exception {
AdviceWithRouteBuilder.adviceWith(camelContext, "getUsersRoute", a -> {
a.mockEndpointsAndSkip("bean:userService*");
});
user = new User();
user.setId(1);
user.setName("Jane");
mockUserService.returnReplyBody(constant(new User[] {user}));
}
#Test
public void callsRestWithMock() {
ResponseEntity<User[]> response = restTemplate.getForEntity("/rest/users", User[].class);
assertThat(response.getStatusCode()).isEqualTo(HttpStatus.OK);
User[] s = response.getBody();
assertThat(s).contains(user);
}
#Test
public void callsDirectRouteWithMock() throws Exception {
User[] users = DefaultFluentProducerTemplate.on(camelContext)
.to("direct:getusers")
.request(User[].class);
assertThat(users).contains(user);
}
#Test
public void camelStarts() {
assertEquals(ServiceStatus.Started, camelContext.getStatus());
assertThat(camelContext.getRoutes()).hasSizeGreaterThan(0);
}
}
Assuming a RouteBuilder:
#Component
public class CamelRouter extends RouteBuilder {
#Value("${server.port}")
private int serverPort;
#Override
public void configure() throws Exception {
restConfiguration()
.contextPath("/rest")
.component("servlet")
.apiContextPath("/api-doc")
.port(serverPort)
.bindingMode(RestBindingMode.json)
.dataFormatProperty("prettyPrint", "true");
rest("/users")
.consumes("application/json")
.produces("application/json")
.get()
.outType(User[].class).to("direct:getusers");
from("direct:getusers").routeId("getUsersRoute")
.log("Get users")
.to("bean:userService?method=listUsers");
}
}
and application.yml:
camel:
component:
servlet:
mapping:
context-path: /rest/*
springboot:
name: MyCamel

Spring #PropertySource using YAML

Spring Boot allows us to replace our application.properties files with YAML equivalents. However, I seem to hit a snag with my tests. If I annotate my TestConfiguration (a simple Java config), it is expecting a properties file.
For example this doesn't work:
#PropertySource(value = "classpath:application-test.yml")
If I have this in my YAML file:
db:
url: jdbc:oracle:thin:#pathToMyDb
username: someUser
password: fakePassword
And I'd be leveraging those values with something like this:
#Value("${db.username}") String username
However, I end up with an error like so:
Could not resolve placeholder 'db.username' in string value "${db.username}"
How can I leverage the YAML goodness in my tests as well?
Spring-boot has a helper for this, just add
#ContextConfiguration(initializers = ConfigFileApplicationContextInitializer.class)
at the top of your test classes or an abstract test superclass.
Edit: I wrote this answer five years ago. It doesn't work with recent versions of Spring Boot. This is what I do now (please translate the Kotlin to Java if necessary):
#TestPropertySource(locations=["classpath:application.yml"])
#ContextConfiguration(
initializers=[ConfigFileApplicationContextInitializer::class]
)
is added to the top, then
#Configuration
open class TestConfig {
#Bean
open fun propertiesResolver(): PropertySourcesPlaceholderConfigurer {
return PropertySourcesPlaceholderConfigurer()
}
}
to the context.
As it was mentioned #PropertySource doesn't load yaml file. As a workaround load the file on your own and add loaded properties to Environment.
Implemement ApplicationContextInitializer:
public class YamlFileApplicationContextInitializer implements ApplicationContextInitializer<ConfigurableApplicationContext> {
#Override
public void initialize(ConfigurableApplicationContext applicationContext) {
try {
Resource resource = applicationContext.getResource("classpath:file.yml");
YamlPropertySourceLoader sourceLoader = new YamlPropertySourceLoader();
PropertySource<?> yamlTestProperties = sourceLoader.load("yamlTestProperties", resource, null);
applicationContext.getEnvironment().getPropertySources().addFirst(yamlTestProperties);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
Add your initializer to your test:
#RunWith(SpringJUnit4ClassRunner.class)
#SpringApplicationConfiguration(classes = Application.class, initializers = YamlFileApplicationContextInitializer.class)
public class SimpleTest {
#Test
public test(){
// test your properties
}
}
#PropertySource can be configured by factory argument. So you can do something like:
#PropertySource(value = "classpath:application-test.yml", factory = YamlPropertyLoaderFactory.class)
Where YamlPropertyLoaderFactory is your custom property loader:
public class YamlPropertyLoaderFactory extends DefaultPropertySourceFactory {
#Override
public PropertySource<?> createPropertySource(String name, EncodedResource resource) throws IOException {
if (resource == null){
return super.createPropertySource(name, resource);
}
return new YamlPropertySourceLoader().load(resource.getResource().getFilename(), resource.getResource(), null);
}
}
Inspired by https://stackoverflow.com/a/45882447/4527110
Another option is to set the spring.config.location through #TestPropertySource:
#TestPropertySource(properties = { "spring.config.location = classpath:<path-to-your-yml-file>" }
#PropertySource only supports properties files (it's a limitation from Spring, not Boot itself). Feel free to open a feature request ticket in JIRA.
From Spring Boot 1.4, you can use the new #SpringBootTest annotation to achieve this more easily (and to simplify your integration test setup in general) by bootstrapping your integration tests using Spring Boot support.
Details on the Spring Blog.
As far as I can tell, this means you get all the benefits of Spring Boot's externalized config goodness just like in your production code, including automatically picking up YAML config from the classpath.
By default, this annotation will
... first attempt to load #Configuration from any inner-classes, and if that fails, it will search for your primary #SpringBootApplication class.
but you can specify other configuration classes if required.
For this particular case, you can combine #SpringBootTest with #ActiveProfiles( "test" ) and Spring will pick up your YAML config, provided it follows the normal Boot naming standards (i.e. application-test.yml).
#RunWith( SpringRunner.class )
#SpringBootTest
#ActiveProfiles( "test" )
public class SpringBootITest {
#Value("${db.username}")
private String username;
#Autowired
private MyBean myBean;
...
}
Note: SpringRunner.class is the new name for SpringJUnit4ClassRunner.class
The approach to loading the yaml properties, IMHO can be done in two ways:
a. You can put the configuration in a standard location - application.yml in the classpath root - typically src/main/resources and this yaml property should automatically get loaded by Spring boot with the flattened path name that you have mentioned.
b. The second approach is a little more extensive, basically define a class to hold your properties this way:
#ConfigurationProperties(path="classpath:/appprops.yml", name="db")
public class DbProperties {
private String url;
private String username;
private String password;
...
}
So essentially this is saying that load the yaml file and populate the DbProperties class based on the root element of "db".
Now to use it in any class you will have to do this:
#EnableConfigurationProperties(DbProperties.class)
public class PropertiesUsingService {
#Autowired private DbProperties dbProperties;
}
Either of these approaches should work for you cleanly using Spring-boot.
Since Spring Boot 2.4.0 you can use ConfigDataApplicationContextInitializer as follows:
#SpringJUnitConfig(
classes = { UserAccountPropertiesTest.TestConfig.class },
initializers = { ConfigDataApplicationContextInitializer.class }
)
class UserAccountPropertiesTest {
#Configuration
#EnableConfigurationProperties(UserAccountProperties.class)
static class TestConfig { }
#Autowired
UserAccountProperties userAccountProperties;
#Test
void getAccessTokenExpireIn() {
assertThat(userAccountProperties.getAccessTokenExpireIn()).isEqualTo(120);
}
#Test
void getRefreshTokenExpireIn() {
assertThat(userAccountProperties.getRefreshTokenExpireIn()).isEqualTo(604800);
}
}
See also: https://www.baeldung.com/spring-boot-testing-configurationproperties#YAML-binding
I found a workaround by using #ActiveProfiles("test") and adding an application-test.yml file to src/test/resources.
It ended up looking like this:
#SpringApplicationConfiguration(classes = Application.class, initializers = ConfigFileApplicationContextInitializer.class)
#ActiveProfiles("test")
public abstract class AbstractIntegrationTest extends AbstractTransactionalJUnit4SpringContextTests {
}
The file application-test.yml just contains the properties that I want to override from application.yml (which can be found in src/main/resources).
I have tried all of the listed questions, but all of them not work for my task: using specific yaml file for some unit test.
In my case, it works like this:
#RunWith(SpringJUnit4ClassRunner.class)
#ContextConfiguration(initializers = {ConfigFileApplicationContextInitializer.class})
#TestPropertySource(properties = {"spring.config.location=file:../path/to/specific/config/application.yml"})
public class SomeTest {
#Value("${my.property.value:#{null}}")
private String value;
#Test
public void test() {
System.out.println("value = " + value);
}
}
it's because you have not configure snakeyml.
spring boot come with #EnableAutoConfiguration feature.
there is snakeyml config too when u call this annotation..
this is my way:
#Configuration
#EnableAutoConfiguration
public class AppContextTest {
}
here is my test:
#RunWith(SpringJUnit4ClassRunner.class)
#SpringApplicationConfiguration(
classes = {
AppContextTest.class,
JaxbConfiguration.class,
}
)
public class JaxbTest {
//tests are ommited
}
I needed to read some properties into my code and this works with spring-boot 1.3.0.RELEASE
#Autowired
private ConfigurableListableBeanFactory beanFactory;
// access a properties.yml file like properties
#Bean
public PropertySource properties() {
PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer = new PropertySourcesPlaceholderConfigurer();
YamlPropertiesFactoryBean yaml = new YamlPropertiesFactoryBean();
yaml.setResources(new ClassPathResource("properties.yml"));
propertySourcesPlaceholderConfigurer.setProperties(yaml.getObject());
// properties need to be processed by beanfactory to be accessible after
propertySourcesPlaceholderConfigurer.postProcessBeanFactory(beanFactory);
return propertySourcesPlaceholderConfigurer.getAppliedPropertySources().get(PropertySourcesPlaceholderConfigurer.LOCAL_PROPERTIES_PROPERTY_SOURCE_NAME);
}
Loading custom yml file with multiple profile config in Spring Boot.
1) Add the property bean with SpringBootApplication start up as follows
#SpringBootApplication
#ComponentScan({"com.example.as.*"})
public class TestApplication {
public static void main(String[] args) {
SpringApplication.run(TestApplication.class, args);
}
#Bean
#Profile("dev")
public PropertySourcesPlaceholderConfigurer propertiesStage() {
return properties("dev");
}
#Bean
#Profile("stage")
public PropertySourcesPlaceholderConfigurer propertiesDev() {
return properties("stage");
}
#Bean
#Profile("default")
public PropertySourcesPlaceholderConfigurer propertiesDefault() {
return properties("default");
}
/**
* Update custom specific yml file with profile configuration.
* #param profile
* #return
*/
public static PropertySourcesPlaceholderConfigurer properties(String profile) {
PropertySourcesPlaceholderConfigurer propertyConfig = null;
YamlPropertiesFactoryBean yaml = null;
propertyConfig = new PropertySourcesPlaceholderConfigurer();
yaml = new YamlPropertiesFactoryBean();
yaml.setDocumentMatchers(new SpringProfileDocumentMatcher(profile));// load profile filter.
yaml.setResources(new ClassPathResource("env_config/test-service-config.yml"));
propertyConfig.setProperties(yaml.getObject());
return propertyConfig;
}
}
2) Config the Java pojo object as follows
#Component
#JsonIgnoreProperties(ignoreUnknown = true)
#JsonInclude(Include.NON_NULL)
#ConfigurationProperties(prefix = "test-service")
public class TestConfig {
#JsonProperty("id")
private String id;
#JsonProperty("name")
private String name;
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
}
3) Create the custom yml (and place it under resource path as follows,
YML File name : test-service-config.yml
Eg Config in the yml file.
test-service:
id: default_id
name: Default application config
---
spring:
profiles: dev
test-service:
id: dev_id
name: dev application config
---
spring:
profiles: stage
test-service:
id: stage_id
name: stage application config
<dependency>
<groupId>com.github.yingzhuo</groupId>
<artifactId>spring-boot-stater-env</artifactId>
<version>0.0.3</version>
</dependency>
Welcome to use my library. Now yaml, toml, hocon is supported.
Source: github.com
This is not an answer to the original question, but an alternative solution for a need to have a different configuration in a test...
Instead of #PropertySource you can use -Dspring.config.additional-location=classpath:application-tests.yml.
Be aware, that suffix tests does not mean profile...
In that one YAML file one can specify multiple profiles, that can kind of inherit from each other, read more here - Property resolving for multiple Spring profiles (yaml configuration)
Then, you can specify in your test, that active profiles (using #ActiveProfiles("profile1,profile2")) are profile1,profile2 where profile2 will simply override (some, one does not need to override all) properties from profile1.
project demo url: https://github.com/Forest10/spring-boot-family/tree/spring-boot-with-yml
I run this answer in my prod env!!! so if you against this ans. please test first!!!
There is no need to add like YamlPropertyLoaderFactory or YamlFileApplicationContextInitializer. You should convert your idea
Follow these steps:
Just add applicationContext.xml like
#ImportResource({"classpath:applicationContext.xml"})
to your ApplicationMainClass.
and your applicationContext.xml should write like this
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns:context="http://www.springframework.org/schema/context"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
default-autowire="byName"
xmlns="http://www.springframework.org/schema/beans"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-2.5.xsd
http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-2.5.xsd">
<context:property-placeholder location="classpath*:*.yml"/>
</beans>
This can help scan your application-test.yml
db:
url: jdbc:oracle:thin:#pathToMyDb
username: someUser
password: fakePassword
Enhancing Mateusz Balbus answer.
Modified YamlFileApplicationContextInitializer class where YAML location is defined per test class. It does not work per test, unfortunately.
public abstract class YamlFileApplicationContextInitializer
implements ApplicationContextInitializer<ConfigurableApplicationContext> {
/***
* Return location of a YAML file, e.g.: classpath:file.yml
*
* #return YAML file location
*/
protected abstract String getResourceLocation();
#Override
public void initialize(ConfigurableApplicationContext applicationContext) {
try {
Resource resource = applicationContext.getResource(getResourceLocation());
YamlPropertySourceLoader sourceLoader = new YamlPropertySourceLoader();
PropertySource<?> yamlTestProperties = sourceLoader.load("yamlTestProperties", resource, null);
applicationContext.getEnvironment().getPropertySources().addFirst(yamlTestProperties);
} catch (IOException e) {
throw new RuntimeException(e);
}
}
}
Usage:
Create subclass of YamlFileApplicationContextInitializer with defined getResourceLocation() method and add this subclass into #SpringApplicationConfiguration annotation.
This way it is easiest to make the test class itself.
#RunWith(SpringRunner.class)
#SpringApplicationConfiguration(classes = Application.class, initializers = SimpleTest.class)
public class SimpleTest extends YamlFileApplicationContextInitializer {
#Override
protected String getResourceLocation() {
return "classpath:test_specific.yml";
}
#Test
public test(){
// test your properties
}
}
Here's an improved version of YamlPropertyLoaderFactory which supports the new PropertySource.ignoreResourceNotFound, based on this answer:
Java:
public final class YamlPropertyLoaderFactory extends DefaultPropertySourceFactory {
private final YamlPropertySourceLoader yamlPropertySourceLoader = new YamlPropertySourceLoader();
#NotNull
public PropertySource createPropertySource(
#Nullable String name,
#NotNull EncodedResource resource
) {
try {
String parsedName;
if (name != null && !name.equals(""))
parsedName = name;
else parsedName = resource.getResource().getFilename();
return yamlPropertySourceLoader.load(parsedName, resource.getResource()).get(0);
} catch (Exception e) {
Exception possibleFileNotFoundException = ExceptionUtils.throwableOfType(e, FileNotFoundException.class);
throw possibleFileNotFoundException != null ? possibleFileNotFoundException : e;
}
}
}
// Usage
#PropertySource(
value = {"file:./my-optional-config.yml"},
factory = YamlPropertyLoaderFactory.class,
ignoreResourceNotFound = true
)
Kotlin:
class YamlPropertyLoaderFactory : DefaultPropertySourceFactory() {
private val yamlPropertySourceLoader = YamlPropertySourceLoader()
override fun createPropertySource(
name: String?,
resource: EncodedResource
): PropertySource<*> = try {
(
yamlPropertySourceLoader.load(
if (name != null && name.isNotBlank()) name else resource.resource.filename,
resource.resource
)
)[0]
} catch (e: Exception) {
throw ExceptionUtils.throwableOfType(e, FileNotFoundException::class.java) ?: e
}
}
// Usage
#PropertySource(
value = ["file:/my-optional-config.yml"],
factory = YamlPropertyLoaderFactory::class,
ignoreResourceNotFound = true
)

Why does Springs #Required not work when configured by annotations

I wonder if there is a way to get #Required working when doing the configuration by annotations. I turned my configuration up-and-down and back again but nothing seems to work for me. I'm using Spring 3.1
My basic configuration looks like this:
#Configuration
public class SpringConfig {
#Bean
public MailSender mailSender() {
MailSender MailSender = new MailSender();
// mailSender.setBean(dlMailSender);
return mailSender;
}
#Bean
public MyBean myBean() {
MyBean myBean = new MyBean();
// setting som props
return myBean;
}
}
MailSender is here:
#Configurable
public class MailSender {
private MyBean myBean;
#Required
public void setMyBean(MyBean myBean) {
this.myBean = myBean;
}
}
I'm testing it with this junit:
#RunWith(SpringJUnit4ClassRunner.class)
#ContextConfiguration(classes = { SpringConfig.class }, loader = AnnotationConfigContextLoader.class)
public class MailSenderTest {
#Test
public void test_main_beans_exists() {
// when then given
}
}
Thanks for any help
Short answer - this is not even theoretically possible.
When using XML-based, bean definitions with their dependencies are completely managed by application context. Spring is able to check, what is being set and what is not being set.
When using annotation-based configuration, you are setting the dependencies yourself. There is no way how Spring can even know what you are doing with the bean before returning it from the factory method.
If you want to check whether the bean is correctly initialized, use InitializingBean or #PostConstruct and implement self-checking method. Spring is doing this regularly in its own beans.

Resources