Condition evaluation depends on a value provided in data base table
#Component
public class XYZCondition implements Condition{
#Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
//based on value defined in DB should return true/false
}
}
As Condition is executing very early, unable to fetch db value
is there any alternate way to achieve this ?
Database values can be changed during application work, while it doesn't seem a good idea to reload application context.
So I would recommended using configuration properties to choose which beans should be available in context.
Moreover, there's a Spring Cloud Config that allows you to store configuration in git or some other storages. Its consumers may restart context once configuration changes.
It seems worth talking a look at it as well.
Well, you're trying to do something that doesn't map well to spring boot in this form.
I suggest to slightly change the requirement:
Instead of trying to access the database in the custom condition, create a custom source of configuration and load the property from the database into the Environment so that when the conditionals get evaluated later on during the startup process, the property with an associated value (previously resolved from the database) is already available.
Examples of following such an approach are:
- Spring boot cloud config that reads the configuration properties from "remote" config service (via REST)
- Spring boot consul integration (that reads from consul obviously)
This approach is much more spring-friendly, and also has can save the application from calling the database multiple times (what if you have 100 beans with this custom conditional) - will it do 100 queries?
Now, this will mean probably that you won't need a custom conditional - probably it will be #Condition on property.
Another caveat is that you won't be able to use JPA/Spring Data to load this property, probably you'll have to go with a Plain JDBC here.
Hmm, maybe you can just create a Configuration Class and Inject your Repository then create a Bean. Then inside the bean method fetch the value from the database and return conditional instance. something like this
#Configuration
public class Config {
#Autowired
private Repository repository;
#Bean
public Interface interface(){
boolean val = reposiory.getDBValue();
if(val)
return new Impl1();
else
return new Impl2();
}
}
sadly you cannot inject a lot into condition: so try with plain url and make manual connection to server,
but in my case this didn't work as I had flyway migration that was adding the same configuration value into the database (chicken and egg problem)
class EnableXXXCondition implements Condition {
private Environment environment;
#SneakyThrows
#Override
public boolean matches(ConditionContext context, AnnotatedTypeMetadata metadata) {
environment = context.getBeanFactory().getBean(Environment.class);
final String url = environment.getProperty("spring.datasource.url");
final String user = environment.getProperty("spring.datasource.username");
final String password = environment.getProperty("spring.datasource.password");
String x;
//we have problems with flyway before
try (Connection connection = DriverManager.getConnection(url, user, password)) {
try (Statement statement = connection.createStatement()) {
try (final ResultSet resultSet = statement.executeQuery("select x from y")) {
resultSet.next();
x = resultSet.getString("x");
}
}
}
return Boolean.valueOf(x);
}
}
You can try defining your condition in your configuration file and use it like below :
#ConditionalOnProperty(name="filter.enabled", havingValue="true")
Related
I'm trying to do some tests to see if my transactional methods are working fine. However I do not fully understand whether or not I should mock the database or not and how JOOQ comes into this equation. Below is the Service class with the transaction of adding a role into the databse.
#Service
public class RoleService implements GenericRepository<Role>
{
#Autowired
private DSLContext roleDSLContext;
#Override
#Transactional
public int add(Role roleEntry)
{
return roleDSLContext.insertInto(Tables.ROLE,
Tables.ROLE.NAME,
Tables.ROLE.DESCRIPTION,
Tables.ROLE.START_DATE,
Tables.ROLE.END_DATE,
Tables.ROLE.ID_RISK,
Tables.ROLE.ID_TYPE,
Tables.ROLE.ID_CONTAINER)
.values(roleEntry.getName(),
roleEntry.getDescription(),
roleEntry.getStartDate(),
roleEntry.getEndDate(),
roleEntry.getIdRisk(),
roleEntry.getIdType(),
roleEntry.getIdContainer())
.execute();
}
}
I'm using MySQL and the connection to the database is made using the spring config file
spring.datasource.url=jdbc:mysql://localhost:3306/role_managementverifyServerCertificate=false&useSSL=true
spring.datasource.username=root
spring.datasource.password=123456
spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver
I'm assuming I don't have to reconnect to the database everytime I'm testing the transaction and closing the connection after it finishes. I know that there is
MockDataProvider provider = new MockDataProvider()
but I don't understand how it works.
What is the best way to test the before mentioned method?
Disclaimer
Have you read the big disclaimer in the jOOQ manual regarding mocking of your database?
Disclaimer: The general idea of mocking a JDBC connection with this jOOQ API is to provide quick workarounds, injection points, etc. using a very simple JDBC abstraction. It is NOT RECOMMENDED to emulate an entire database (including complex state transitions, transactions, locking, etc.) using this mock API. Once you have this requirement, please consider using an actual database product instead for integration testing, rather than implementing your test database inside of a MockDataProvider.
It is very much recommended you use something like testcontainers to integration test your application, instead of implementing your own "database product" via the mock SPI of jOOQ (or any other means of mocking).
If you must mock
To answer your actual question, you can configure your DSLContext programmatically, e.g. using:
#Bean
public DSLContext getDSLContext() {
if (testing)
return // the mocking context
else
return // the actual context
}
Now inject some Spring profile value, or whatever, to the above configuration class containing that DSLContext bean configuration, and you're all set.
Alternatively, use constructor injection instead of field injection (there are many benefits to that)
#Service
public class RoleService implements GenericRepository<Role> {
final DSLContext ctx;
public RoleService(DSLContext ctx) {
this.ctx = ctx;
}
// ...
}
So you can manually construct your service in the test that mocks the database:
RoleService testService = new RoleService(mockingContext);
testService.add(...);
But as you can see, the mocking is completely useless. Because what you want to test is that there's a side effect in your database (a record has been inserted), and to test that side effect, you'll want to query the database again, but unless you mock that as well, or re-implement an entire RDBMS, you won't see that record in the database. So, again, why not just integration test your code, instead?
I'm new to Spring and I'm building an application where some entities (JPA/Hibernate) need access to a property from application.properties. I do have a configuration class in which this is trivial:
#Configuration
public class FactoryBeanAppConfig {
#Value("${aws.accessKeyId}")
private String awsAccessKeyId;
#Value("${aws.secretKey}")
private String awsSecretKey;
}
but since entities do not have and I think they should not have the annotations such as #Configuration or #Component, what's the Spring way for them to access the property?
Now, I know I can create my own class, my own bean, and make it as a simple wrapper around the properties; but is that the Spring way to do it or is there another way?
specify Property file location using #PropertySource
Something like below
#PropertySource("classpath:/application.proerties")
You also need to add below bean in your config
#Bean
public static PropertySourcesPlaceholderConfigurer propertyConfigIn() {
return new PropertySourcesPlaceholderConfigurer();
}
There is no "Spring way", since JPA entities and Spring have nothing to do with each other. Most importantly, JPA entities are not Spring beans, so Spring doesn't know or care about them as they're not managed by Spring.
You can try to hack around, trying in vain to access Spring's configuration from code that should not be trying to access it, or you can accept the truth that your design is broken and you're trying to do something that's not meant to be done.
As was proposed several times, use a service class for this. It's managed by Spring, so it can access the Spring config, and it can handle entities, so there's no crossing boundaries.
First create a public static variable in some bean managed by Spring, then make the following use of the #Value annotation.
public static String variable;
#Value("${variable}")
private void setVariable(String value) {
variable = value;
}
It will be set at runtime on startup, now you can access it from entities and everywhere else because it is just a public static var.
You can use #PropertySource to load the properties file as follows
#Configuration
#PropertySource("classpath:/com/organization/config/application.proerties")
public class FactoryBeanAppConfig {
...
}
Entities should not acces environment properties. If you are using your entity through a service, then the service can access the properties to act on the entity.
The problem rears its ugly head when the JdbcJobInstanceDao attempts to call the FIND_JOBS_WITH_KEY query:
SELECT JOB_INSTANCE_ID, JOB_NAME from %PREFIX%JOB_INSTANCE where JOB_NAME = ? and JOB_KEY = ?
the %PREFIX% token is replaced with the value of application.properties key spring.batch.table-prefix which defaults to "BATCH_".
The application properties are definitely loading from the files as my small test demonstrates:
#ActiveProfiles("test") // to load `application-test.properties`
#RunWith(SpringRunner.class)
// we don't need a web context as we are playing with only server side classes
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.NONE, classes = {TestDatabaseConfig.class,
MyBatchProperties.class, SpringBatchTestConfig.class})
#ComponentScan(basePackageClasses = {MyBatchConfig.class})
// MyBatchConfig has #EnableBatchProcessing and all job configurations.
public class BatchTest {
#Autowired
private JobLauncherTestUtils jobLauncherTestUtils;
#Autowired
private ApplicationContext applicationContext;
#Before
public void setup() {
Environment environment = applicationContext.getEnvironment();
System.out.println(environment.getProperty("spring.batch.table-prefix"));
// above prints MY_SCEHMA_USER.BATCH_ as expected
}
#Test
public void checkJobRuns() {
try {
jobLauncherTestUtils.launchJob();
} catch (Exception e) {
e.printStackTrace(); // <-- fails here with the query returning "table not found" because the prefix was not configured correctly.
}
}
}
application-test.properties:
spring.batch.table-prefix=MY_SCHEMA_USER.BATCH_
I've been working with custom configuration for job runs for a long time but the JobLauncherTestUtils doesn't seem to honour these configuration properties.
I need the different table prefix as the batch database tables are owned by a different schema to the connected database user. (i.e. MY_APP_USER trying to access MY_SCHEMA_USER.BATCH_JOB_INSTANCE). unqualified references to tables try (and fail) to resolve the batch tables against MY_APP_USER instead of MY_SCHEMA_USER.
I've tried to create a JobRepositoryFactoryBean bean and annotate it with #ConfigurationProperties("spring.batch"). However - along with this not working anyway - I don't see why I should configure these this way rather than with properties.
How to I get the Batch related beans properly configured with application properties in junit tests using JobLauncherTestUtils?
Fixed
The problem was because i was creating my own BatchConfigurer bean inside of the MyBatchConfig class.
The BatchConfigurer registers the properties for configuration with each component of the Batch framework (jobRepository, daos, etc...)
This meant that the properties were not being populated through the #Component annotated DefaultBatchConfigurer class. As to why I decided to place this piece of code in here even I - the author - Am not sure. I guess i need to not code after minimal sleep.
Thanks for indulging my stupidity!
I have a question about Spring annotation configurations. I have a bean:
#Bean
public ObservationWebSocketClient observationWebSocketClient(){
log.info("creating web socket connection...");
return new ObservationWebSocketClient();
}
and I have a property file:
#Autowired
Environment env;
In the property file I want to have a special boolean property
createWebsocket=true/false
which signs whether a bean ObservationWebSocketClient should be created. If property value is false I don't want to establish web socket connection at all.
Is there any technical possibility to realize this?
Though I've not used this functionality, it appears that you can do this with spring 4's #Conditional annotation.
First, create a Condition class, in which the ConditionContext has access to the Environment:
public class MyCondition implements Condition {
#Override
public boolean matches(ConditionContext context,
AnnotatedTypeMetadata metadata) {
Environment env = context.getEnvironment();
return null != env
&& "true".equals(env.getProperty("createWebSocket"));
}
}
Then annotate your bean:
#Bean
#Conditional(MyCondition.class)
public ObservationWebSocketClient observationWebSocketClient(){
log.info("creating web socket connection...");
return new ObservationWebSocketClient();
}
edit The spring-boot annotation #ConditionalOnProperty has implemented this generically; the source code for the Condition used to evaluate it is available on github here for those interested. If you find yourself often needing this funcitonality, using a similar implementation would be advisable rather than making lots of custom Condition implementations.
Annotate your bean method with #ConditionalOnProperty("createWebSocket").
Note that Spring Boot offers a number of useful conditions prepackaged.
For Spring Boot 2+ you can simply use:
#Profile("prod")
or
#Profile({"prod","stg"})
That will allow you to filter the desired profile/profiles, for production or staging and for the underlying Bean using that annotation it only will be loaded by Springboot when you set the variable spring.profiles.active is equals to "prod" and ("prod" or "stg"). That variable can be set on O.S. environment variables or using command line, such as -Dspring.profiles.active=prod.
As for me, this problem can be solved by using Spring 3.1 #Profiles, because #Conditional annotation give you opportunity for define some strategy for conditional bean registration (user-defined strategies for conditional checking), when #Profiles can based logic only on Environment variables only.
Is it possible to pass a variable to the #Qualifier annotation in Spring?
For example,
#Autowried
#Qualifier("datasource_" + "#{jobParameters['datasource.number']}")
private DataSource ds;
I have 10 different databases where my Spring batch job runs everyday. The database number is passed as a job parameter. I want to define the datasource to connect to based on the job parameter.
Thanks!
You are only allowed constant expressions in annotations.
So you are creating 10 data sources in your spring configuration - does your job need to use all ten in one run?? If you only need one connection for the lifetime of your spring context, can you just have 10 different sets of property files?
One thing you could do is to create all of your data sources in a map (keyed by "database number", then inject this map AND the key into your bean, for example...
public class MyBean {
#Autowired #Qualifier("dataSourceMap")
private Map<String, DataSource> dataSourceMap;
#Value("#{jobParameters['datasource.number']}")
private String dbKey;
public void useTheDataSource() {
DataSource ds = dataSourceMap.get(dbKey);
...
}
}
Or have I misunderstood?
no, you can't pass variables to any annotations in java. it has nothing to do with spring.
use a workaround. create and pass a service that will pick correct database each time it's needed