I want to implement a conditional Bean depending on a flag in my application.properties. Example:
// application.properties
service=foobar
The idea is to make different service implementations configurable, let assume I got a central configuration class for this service in Spring:
#Configuration
#Import({ServiceA.class, ServiceB.class, ...})
public class ServiceConfiguration {
...
}
And possible service implementations would look like
#Configuration
public class ServiceA implements Condition {
#Bean
#Conditional(ServiceA.class)
public Service service() {
Service a = ...
return a;
}
#Override
public boolean matches(
ConditionContext conditionContext,
AnnotatedTypeMetadata annotatedTypeMetadata) {
// getProperty will alsways return null for some reason
return conditionContext
.getEnvironment()
.getProperty("service")
.equals("ServiceA");
}
// This will be null anyways
#Value("${service}")
private String confService;
}
Since the class implementing Condition (here just the same class ServiceA) will be initialized via default constructor #Value-injections won't work. How ever, by what I understand getProperty()should return the correct value. What am I doing wrong? How can I access application properties at this point?
I found at "dirty workarround", I really don't like that solution, how ever, it solves the problem. As mentioned here a #PropertySource fixes the problem (I haven't tried this before posting here since it wasn't an accpeted answer).
#Configuration
#PropertySource(value="file:config/application.properties")
public class ServiceA implements Condition {
#Bean
#Conditional(ServiceA.class)
public Service service() {
Service a = ...
return a;
}
#Override
public boolean matches(
ConditionContext conditionContext,
AnnotatedTypeMetadata annotatedTypeMetadata) {
// Will work now
return conditionContext
.getEnvironment()
.getProperty("service")
.equals("ServiceA");
}
}
Although this works I don't like it for several reason:
With every implementation I have code redundancy (giving a path to a config file)
It's highly unmaintainable when having multiple configuration files
Example: Behavior like load default.properties <-then load and overwrite with -> customer.properties won't work anymore (altough this should be solvable using #PropertySources which would, on the other hand, increase code redundancy)
Related
I am thinking what would be best solution for following case. Suppose we have at start CRUD app - using Spring Boot. I would like to add read only state for this application - which allows only data read and blocks create, update, delete data operations for admin role. I think about adding aspect (#Aspect) which checks current app state (which is saved in db) and starts if create, update, update operations are invoked. If app is in read-only state - exception will be thrown (handled by #ControllerAdvice)
I wonder if adding aspect is the best option - I dont want modify existing code. Whats your take on that? Moreover - how would you write integration tests for #aspect - testing if aspect starts properly? How could be aspects tested for this case? What are good practises for testing #aspects (integration tests #springboottest)
This honestly seems like an inconvenient way of doing this. Why not just add an Interceptor that checks for this? I did something similar before
#Component
#RequiredArgsConstructor
public class ReadOnlyModeInterceptor implements HandlerInterceptor {
private final ServerProperties serverProperties;
#Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) {
if (serverProperties.isReadOnlyMode()) {
String method = request.getMethod();
boolean isReadOnlyMethod = "GET".equals(method) || "OPTIONS".equals(method);
String servletPath = request.getServletPath();
boolean isReadOnlyPath = isReadOnlyPath(servletPath);
if (!isReadOnlyMethod && isReadOnlyPath) {
throw new ServiceUnavailableException("Server is in read-only mode.");
}
}
return true;
}
private boolean isReadOnlyPath(String servletPath) {
if (serverProperties.isFullyReadOnly()) {
return true; // wildcard option, entire server is read-only
}
return serverProperties.getReadOnlyPaths().stream().anyMatch(servletPath::contains);
}
}
You also need to register it
#RequiredArgsConstructor
#Configuration
public class WebMvcConfig implements WebMvcConfigurer {
private final ReadOnlyModeInterceptor readOnlyModeInterceptor;
#Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(readOnlyModeInterceptor).order(0);
}
}
I have application.yml that looks like this:
feature:
toggles:
checksLoginAndRegistration: true
I am trying to get it in my class with #Value annotation, but it's not working.
public class UMLUserRepository implements UserRepository {
#Value("${feature.toggles.checksLoginAndRegistration}")
private boolean checksLoginAndRegistration;
private void validateLoginNow(LoginInfo info, User user) {
checkKnownBlock(info, user.username);
if(checksLoginAndRegistration){
try {
service.validateLogin(user.username);
} catch (ValidationException alidationException) {
throw new Exception(user.username);
}
}
}
When I debug the code my checksLoginAndRegistration variable is set to false.
According to the comments you have used #Value annotation within a simple POJO. Not inside a Spring Bean like #Component, #Service or #Configuration.
You cannot inject a value to a POJO class using #Value.
This annotation can be used for injecting values into fields in Spring-managed beans, and it can be applied at the field or constructor/method parameter level.
But still you get value false for checksLoginAndRegistration parameter because it is an primitive type which has a default value false. If you chaged it to boxed type Boolean you can see the value of checksLoginAndRegistration is null
Update
#ConfigurationProperties(prefix = "feature.toggles")
public class AppConfig {
private Boolean checksLoginAndRegistration;
}
Then update your UMLUserRepository class, (We make checksLoginAndRegistration is a dependency to UMLUserRepository class)
public class UMLUserRepository implements UserRepository {
private final Boolean checksLoginAndRegistration;
public UMLUserRepository(Boolean checksLoginAndRegistration) {
this.checksLoginAndRegistration = checksLoginAndRegistration;
}
}
This is the class where you crate instance of UMLUserRepository class. An it should be a Spring Bean.
#Component (or #Service)
public class ClassYouInitatingUMLUserRepository {
#Autowire
private AppConfig appConfig;
public void yourMethod() {
UMLUserRepository repo = new UMLUserRepository(appConfig.getChecksLoginAndRegistration());
}
I would encourage you to check the possibility to convert UMLUserRepository class to a Spring bean. Then this won't be needed.
Hmm, it seems like you do everything correctly. I can suggest what can go wrong
Is it all what file contains?
If not, check is there only one feature key or not. If there's another one, remove it.
Have you added #Configuration annotation to your configuration class?
If not, add it.
In out project we don't use setter or filed injection, we use only constructor injection, and I know that both options 1. and 2. may work.
Is it unsafe to work with beans in constructor in that case?
Or spring boot 2+ makes something, and I should better use option 1. instead of 2. I can't imagine case when option 1 will go wrong
#Component
#ConfigurationProperties("config")
public class ServiceConfigProperties {
// .... some code
}
Can be unsafe? - but it looks better
#Component
public class Service {
private boolean skipCheck;
public Service(ServiceConfigProperties configProps) {
this.skipCheck = configProps.isSkipCheck();
}
}
Can't be unsafe?
#Component
public class Service {
private boolean skipCheck;
private ServiceConfigProperties configProps;
public Service(ServiceConfigProperties configProps) {
this.configProps= configProps;
}
#PostConstruct
public void initConfig() {
this.skipCheck= configProps.isSkipCheck();
}
}
With a couple of caveats, interacting with constructor-injected beans inside the constructor is completely safe.
I'm trying to achieve something like this:
#Controller
public SomeController {
#CustomConfig("var.a")
private String varA;
#CustomConfig("var.b")
private String varB;
#RequestMapping(value = "/", method = RequestMethod.GET)
public String get() {
return varA;
}
}
CustomConfig would be an #Interface class that accepts one value parameter. The reason why we are not using #Value is because this will not come from config file but from API (such as https://getconfig.com/get?key=var.a). So we are going to make HTTP request to inject it.
So far I've only manage to make something work if the varA and varB is inside get() method as parameter, by using below in a class that extends WebMvcConfigurerAdapter:
#Override
public void addArgumentResolvers(List<HandlerMethodArgumentResolver> argumentResolvers) {
CustomConfigResolver resolver = new CustomConfigResolver();
argumentResolvers.add(resolver);
}
And inside CustomComfigResolver.resolveArgument() we would do the HTTP query, but that's not really what we wanted, we need it to be injected as class variable.
Does anyone have experience in resolving it at class variable level?
Thank you
This could work if you use #Value instead of your own custom annotation. This uses the built in environment:
#Order(Ordered.HIGHEST_PRECEDENCE)
#Configuration
public class TcpIpPropertySourceConfig implements InitializingBean {
#Autowired
private ConfigurableEnvironment env;
#Autowired
private RestTemplate rest;
public void afterPropertiesSet() {
// Call your api using Resttemplate
RemoteProperties props = //Rest Call here;
// Add your source to the environment.
MutablePropertySources sources = env.getPropertySources();
sources.addFirst(new PropertiesPropertySource("customSourceName", props)
}
}
What you are trying to achieve is difficult when you start to consider "unhappy" scenarios. Server down / not reachable. You need to account for all of that in the method above.
I would highly recommend to instead use Spring Cloud Config. Great guide on that is here: https://www.baeldung.com/spring-cloud-configuration
This provides:
- Reloading of your #Value() properties, so no custom annotation needed.
- A more stable server and great Spring integration out of the box.
Best of all, it is easy to apply Retries and Backoffs if the configuration server goes down (see https://stackoverflow.com/a/44203216/2082699). This will make sure your app doesn't just crash when the server is not available.
I am working on spring but not spring boot. I have a class marked as #Component (but not as #Configuration [it's not config class]). I would like to create it based on the serviceA.create property in app.properties. It the file it looks like this: serviceA.create=true. I would think, that in the code it narrows down to:
#Override
public boolean matches(final ConditionContext context, final AnnotatedTypeMetadata metadata) {
return Boolean.valueOf(context.getEnvironment().getProperty("serviceA.create"));
}
But based on different questions/answers (f.e. this) on the same topic, it sounds like adding #PropertySource(value="classpath:config.properties") is mandatory for this to work (to actually read the property value).
Question is, if I am able to get property from ConditionContext without defining #PropertySource ? because currently I am getting null without that annotation.
It should be done in the following way:
#Component
#Conditional(ServiceACondition.class)
#PropertySource(value="classpath:app.properties")
public class ServiceA {
(...)
}
and the cond. class
public class ServiceACondition implements Condition {
#Override
public boolean matches(final ConditionContext context, final AnnotatedTypeMetadata metadata) {
return Boolean.valueOf(context.getEnvironment().getProperty("serviceA.start"));
}
}