Can't use enumration on #ConditionalOnProperty havingValue - java

Following code won't compile, I get "Attribute value must be constant".
Is there a workaround to get it working?
#Service
#ConditionalOnProperty(name = "my.property", havingValue = MyEnum.A.name())
public class MyService {
}
public enum MyEnum {
A,
B
}

Unfortunately you cannot do this with #ConditionalOnProperty or with a custom Conditional class / annotation like in the example in this link.
You can't send MyEnum.A or MyEnum.A.name() with havingValue, because compile constants can only be primitives and Strings.
You can do the comparison with #ConditionalOnProperty:
#Service
#ConditionalOnProperty(
name = "my.property", havingValue = "A"
)
public class MyService {}
Alternatively you can do the comparison with #ConditionalOnExpression:
#Service
#ConditionalOnExpression(
value = "#{T(com._75471475.MyEnum).valueOf(\'${my.property}\') == T(com._75471475.MyEnum).A}"
)
public class MyService {}

Related

Testing conditional Bean creation with #ConditionalOnProperty and #FeignClient

I have a Rest Controller, a Service and a Feign Client being used inside the service.
Now, I need to conditionally create the controller bean based on an environment variable. I have been able to set up the configuration and it looks like it should work. So far so good. But I am having a hard time testing the conditional bean creation.
The REST Controller:
#RestController
#RequestMapping("/api/path")
public class AbcController {
private final AbcService abcService;
#Autowired
public AbcController(AbcService abcService) {
this.abcService = abcService;
}
...
}
The Service:
#Service
public class AbcService {
private final AbcClient abcClient;
#Autowired
public AbcService(AbcClient abcClient) {
this.abcClient = abcClient;
}
Feign Client:
#Component
#FeignClient(name = "abc", url = "${abc.url}", decode404 = true)
public interface AbcClient {
#RequestMapping(
value = "/match",
method = RequestMethod.POST,
produces = MediaType.APPLICATION_JSON_VALUE
)
MatchResponse getSsoIds(RequestDto requestDto);
}
Configuration for conditionally loading the Controller class:
#Configuration
public class AbcConfiguration {
#Bean
#ConditionalOnProperty(
value="feature.enabled",
havingValue = "true",
matchIfMissing = true
)
public AbcController abcController(AbcService abcService) {
return new AbcController(abcService);
}
}
The Test class for testing the conditional bean creation:
public class AbcControllerTest {
private final ApplicationContextRunner contextRunner = new ApplicationContextRunner()
.withConfiguration(AutoConfigurations.of(AbcConfiguration.class));
#Test
public void loadsControllerBeanWhenPropertyIsEnabled() {
this.contextRunner
.withPropertyValues("feature.enabled=true")
.run(context -> context.assertThat().hasSingleBean(AbcController.class));
}
#Test
public void loadsControllerBeanWithoutDefinedProperty() {
this.contextRunner
.run(context -> context.assertThat().hasSingleBean(AbcController.class));
}
#Test
public void doesNotLoadControllerBeanWhenPropertyIsDisabled() {
this.contextRunner
.withPropertyValues("feature.enabled=false")
.run(context -> context.assertThat().doesNotHaveBean(AbcController.class));
}
}
I have tried to run the test but the first 2 tests always ends up complaining about missing or non-configurable beans. The last test runs as expected.
With the above test setup it complains about missing AbcService bean, which we can solve by adding
#ExtendWith(SpringExtension.class)
#ContextConfiguration(classes = AbcService.class)
to the top of the test class, but then it complains No qualifying bean of type 'de.xxx.xxx.AbcClient' and we cannot create a bean of the feignClient AbcClient because it is an interface.
Help needed! How to make these tests pass?

Passing decisional param to #TestConfiguration class

I would like to send a parameter to #Bean in #TestConfiguration class to return diffrent AnimalType depends of the string which comes in the parameter. Is it possible to do it from a test class or I need a diffrent Config for each class?
#TestConfiguration
public class TestConfig {
#Bean
public AnimalType componentType(String type) {
return new AnimalType(type);
}
}
It could be achieved also by constructo injection but it's also not possible to call #TestConfig constructor with parameter which is not a bean;
You could read the type from a property like this:
#Component
public class AnimalType {
#Value("${animal.type}")
private String value;
}
and then in your test specify it with #SpringBootTest(properties = { "animal.type=dog" }).

How to add a bean in SpringBootTest

The question seems extremely simple, but strangely enough I didn't find a solution.
My question is about adding/declaring a bean in a SpringBootTest, not overriding one, nor mocking one using mockito.
Here is what I got when trying the simplest implementation of my real need (but it doesn't work):
Some service, bean, and config:
#Value // lombok
public class MyService {
private String name;
}
#Value // lombok
public class MyClass {
private MyService monitoring;
}
#Configuration
public class SomeSpringConfig {
#Bean
public MyClass makeMyClass(MyService monitoring){
return new MyClass(monitoring);
}
}
The test:
#RunWith(SpringRunner.class)
#SpringBootTest(classes = { SomeSpringConfig.class })
public class SomeSpringConfigTest {
private String testValue = "testServiceName";
// this bean is not used
#Bean
public MyService monitoringService(){ return new MyService(testValue); }
// thus this bean cannot be constructed using SomeSpringConfig
#Autowired
public MyClass myClass;
#Test
public void theTest(){
assert(myClass.getMonitoring().getName() == testValue);
}
}
Now, if I replace the #Bean public MyService monitoring(){ ... } by #MockBean public MyService monitoring;, it works. I find it strange that I can easily mock a bean, but not simply provide it.
=> So how should I add a bean of my own for one test?
Edit:
I think ThreeDots's answer (create a config test class) is the general recommendation.
However, Danylo's answer (use #ContextConfiguration) fit better to what I asked, i.e. add #Bean directly in the test class.
Spring Test needs to know what configuration you are using (and hence where to scan for beans that it loads). To achieve what you want you have more options, the most basic ones are these two:
Create configuration class outside the test class that includes your bean
#Configuration
public class TestConfig {
#Bean
public MyService monitoringService() {
return new MyService();
}
}
and then add it to to test as configuration class #SpringBootTest(classes = { SomeSpringConfig.class, TestConfig.class })
or
If you only need to use this configuration in this particular test, you can define it in static inner class
public class SomeSpringConfigTest {
#Configuration
static class ContextConfiguration {
#Bean
public MyService monitoringService() {
return new MyService();
}
}
}
this will be automatically recognized and loaded by spring boot test
Simply add the config as
#ContextHierarchy({
#ContextConfiguration(classes = SomeSpringConfig.class)
})
What i am using in this cases is #Import:
#DataJpaTest(showSql = false)
//tests against the real data source defined in properties
#AutoConfigureTestDatabase(replace = AutoConfigureTestDatabase.Replace.NONE)
#Import(value = {PersistenceConfig.class, CustomDateTimeProvider.class})
class MessageRepositoryTest extends PostgresBaseTest {
....
Here i am using a pre configured "test slice".
In this case a need to add my JpaAuditingConfig.
But why not just adding the other beans as you did with your SomeSpringConfig.class ?:
#RunWith(SpringRunner.class)
#SpringBootTest(classes = { SomeSpringConfig.class, OtherBean.class })
public class SomeSpringConfigTest {
...
Everything listed in test will be injectable directly, all not declared must be added as mocks.

Cant load property in java match-

Trying to implement conditional bean loading in spring. This is the code, problem is, I am not able to load the property inside match method,
#Configuration
public class Class implements Condition {
#Value("${test.property}")
private boolean testProperty;
#Override
public boolean matches(ConditionContext conditionContext,
AnnotatedTypeMetadata annotatedTypeMetadata) {
sout(testProperty);
return true;
}
}
I can however print the property if I inject it into a constructor,
but that does not solve my issue, any thoughts?
For conditional instantiation based on environment variables you could use the #ConditionalOnProperty annotation on top of your bean definitions.
#Configuration
public class Config {
#Bean
#ConditionalOnProperty(name = "your.property", havingValue = "true")
public YourBean instantiateIfTrue() {
// instantiate and return YourBean in case the property is true
}
#Bean
#ConditionalOnProperty(name = "your.property", havingValue = "false")
public YourBean instantiateIfFalse() {
// instantiate and return YourBean in case the property is false
}
}
In your case, you could add the #ConditionalOnProperty on top of your #Configuration (and no longer extend Condition).
#Configuration
#ConditionalOnProperty(name = "your.property", havingValue = "true")
public class Config {
// ...
}
Rely on extending Condition only if #ConditionalOnProperty is not flexible enough for your use case.

Spring boot: Consider defining a bean of type 'com.repository.services.interfacename' in your configuration

I've deployed an spring boot application, where i created an interface and implement it with two classes.
public interface interfacename {
LinkedHashSet<String> names(String path);
}
And implemented classes are
#Component
public class class1 implements interfacename {
......
}
#Component
public class class2 implements interfacename {
......
}
Now i try to create an instance for both the classes using interface name,
#Autowired
#Qualifier("class1")
interfacename imp1;
#Autowired
#Qualifier("class2")
interfacename imp2;
It is the configuration class,
#Configuration
public class interfacenameConfig {
#Bean
#ConditionalOnProperty(name = "class1", matchIfMissing = true)
public interfacename class1Service() {
return new class1();
}
#Bean
#ConditionalOnProperty(name = "stanfordname")
public interfacename class2Service() {
return new class2();
}
}
My Project structure is,
com.repository
application.java(#SpringApplcation)
com.repository.controller
applicationcontroller.java(#RestController)
com.repository.services
interfacename.java
interfacenameconfig.java(#configuration)
class1.java(#component)
class2.java(#component)
It throws the following error
Action:
Consider defining a bean of type 'com.repository.services.interfacename' in your configuration.
please someone guide me to solve this.
Thanks in advance
In you're usage you're saying that you want beans with the ids / names class1 and class2respectively:
#Autowired
#Qualifier("class1")
interfacename imp1;
#Autowired
#Qualifier("class2")
interfacename imp2;
But in the configuration you gave them different names:
#Configuration
public class interfacenameConfig {
#Bean
#ConditionalOnProperty(name = "class1", matchIfMissing = true)
public interfacename class1Service() {
return new class1();
}
#Bean
#ConditionalOnProperty(name = "stanfordname")
public interfacename class2Service() {
return new class2();
}
}
Namely: class1Service and class2Service. Those Ids are derived from the name of the function instantiating the beans
Two possible fixes:
Give them the names you want with #Bean("class1") and #Bean("class2").
OR
Use the names they have in the qualifier, that is: #Qualifier("class1Service") and #Qualifier("class2Service")
In your configuration class you should have an annotation to prompt for component scanning to the package that your interface interfacename belongs.
E.g.:
#ComponentScan({"com.repository.services"})
In Spring-boot you usually have this annotation in the Spring boot application class
e.g.
#SpringBootApplication
#ComponentScan({"com.repository.services"})
public class MyApplication {
}
UPDATE
If you have multiple classes implementing an interface you can use the value attribute when annotating them as #Component
#Component(value="class1")
public class class1 implements interfacename
#Component(value="class2")
public class class2 implements interfacename
and then #Autowire them with #Qualifier as you already do.
Based on your last update, since the #SpringBootApplication is in the parent directory of your spring-managed beans I think you can omit the #ComponentScan annotation. Spring will scan by default all the sub-packages below com.repository.
However I still believe that the interfacenameconfig class is redundant. Why are you declaring the same beans as the ones you have annotated as #Component? Either #Component or #Bean, there is no reason having both for the same beans as far as I know and it could probably be the source of your problem.
You need to add #Service annotation above interface implementation.
e.g. #Component
public interface interfacename {
LinkedHashSet<String> names(String path);
}
#Service
public class interfaceDefinition implements interfacename{
LinkedHashSet<String> names(String path){
// write code here
}
}
I have add the qualifier annotation along with #Component annotation. Then i ensure the application it is working fine.
#Component
#Qualifier("class1")
public class class1 implements interfacename {
......
}
Thanks for the reply

Categories

Resources