What strategies have people developed for controlling deployment configurations with Spring? I've already extracted out the environmental details (e.g. jdbc connection parameters) into a properties file, but I'm looking for some way of managing deployment details that aren't simple strings. Specifically, I'm currently using a locally configured datasource while doing development, and JNDI on our application servers (DEV, QA).
I've got an applicationContext.xml with the following two lines,
<import resource="spring/datasource-local-oracle.xml"/>
<import resource="spring/datasource-jndi.xml"/>
and I comment out whichever datasource isn't being used in that instance. There's got to be a better way to do this though. Thoughts, ideas, suggestions?
Use #Bean to define your datasource in code, rather than in XML. That way you can apply conditional logic to how the bean is created. For example:
#Value("${url:jdbc:hsqldb:mem:memdb}")
String url;
// username, password, etc
#Value("${jndiName:}")
String jndiName;
#Bean
public DataSource dataSource() {
DataSource ds;
if (jndiName == "") {
BasicDataSource bds = new BasicDataSource();
bds.setDriverClassName(driverClassName);
bds.setUrl(url);
bds.setUsername(username);
bds.setPassword(password);
ds = bds;
} else {
JndiObjectFactoryBean = jndiFactory = new JndiObjectFactoryBean();
jndiFactory.setJndiName("java:/" + jndiName);
jndiFactory.afterPropertiesSet();
ds = (DataSource) jndiFactory.getObject();
}
return ds;
}
Related
So I want this to work
#Bean
#ConfigurationProperties("datasource.mybatis-factory")
public SqlSessionFactoryBean sqlSessionFactoryBean() {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource());
return sqlSessionFactoryBean;
}
with property (among others)
datasource.mybatis-factory.mapper-locations=classpath*:sqlmap/*.xml
However, it fails even though the files are there:
Caused by: java.io.FileNotFoundException: class path resource [classpath*:sqlmap/*.xml] cannot be opened because it does not exist
Looking at setMapperLocations() I didn't do anything wrong, they clearly want me to use classpath*:...:
/**
* Set locations of MyBatis mapper files that are going to be merged into the {#code SqlSessionFactory} configuration
* at runtime.
*
* This is an alternative to specifying "<sqlmapper>" entries in an MyBatis config file. This property being
* based on Spring's resource abstraction also allows for specifying resource patterns here: e.g.
* "classpath*:sqlmap/*-mapper.xml".
*
* #param mapperLocations
* location of MyBatis mapper files
*/
public void setMapperLocations(Resource... mapperLocations) {
this.mapperLocations = mapperLocations;
}
Looking further down the code there's just this:
for (Resource mapperLocation : this.mapperLocations) {
if (mapperLocation == null) {
continue;
}
try {
XMLMapperBuilder xmlMapperBuilder = new XMLMapperBuilder(mapperLocation.getInputStream(),
targetConfiguration, mapperLocation.toString(), targetConfiguration.getSqlFragments());
xmlMapperBuilder.parse();
There is no code that would convert the classpath*:sqlmap/*.xml into openable resources or at least I don't see it. Or what am I missing here?
Work around:
What I have now and is working (note that I don't use datasource.mybatis-factory.mapper-locations as that would again overwrite what I set):
#Bean
#ConfigurationProperties("datasource.mybatis-factory")
public SqlSessionFactoryBean sqlSessionFactoryBean(
#Value("${datasource.mybatis-factory.mapper-location-pattern}") String mapperLocations) {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource());
sqlSessionFactoryBean.setMapperLocations(findMapperLocations(mapperLocations));
return sqlSessionFactoryBean;
}
private Resource[] findMapperLocations(String resourcePaths) {
PathMatchingResourcePatternResolver patternResolver = new PathMatchingResourcePatternResolver();
return Stream.of(resourcePaths.split(","))
.map(LambdaExceptionUtilities.rethrowFunction(patternResolver::getResources))
.flatMap(Stream::of)
.toArray(Resource[]::new);
}
with property
datasource.mybatis-factory.mapper-location-pattern=classpath*:sqlmap/*.xml
So: what is missing here to make it work without the work around? How do XMLs on the classpath find the way into MyBatis? Maybe something Spring-Bootish missing?
I ran into the same issue recently. I believe this is what what you're looking for:
#Bean
#ConfigurationProperties("datasource.mybatis-factory")
public SqlSessionFactoryBean sqlSessionFactoryBean(
#Value("${datasource.mybatis-factory.mapper-location-pattern}") String mapperLocations) {
SqlSessionFactoryBean sqlSessionFactoryBean = new SqlSessionFactoryBean();
sqlSessionFactoryBean.setDataSource(dataSource());
sqlSessionFactoryBean.setMapperLocations(
new PathMatchingResourcePatternResolver().getResources("classpath*:sqlmap/*.xml")
);
return sqlSessionFactoryBean;
}
Basically what you need is this line of code in your #Bean definition above:
sqlSessionFactoryBean.setMapperLocations( new PathMatchingResourcePatternResolver().getResources("classpath*:sqlmap/*.xml")
);
Note: the method name is getResources (the plural) and not getResource
Feel free to replace the hard coded value of classpath*:sqlmap/*.xml with the #Value("datasource.mybatis-factory.mapper-location-pattern") injected value instead.
Because you're using MyBatis with Spring, the issue here is not so much a MyBatis issue, as much as it is a Spring issue. More specifically, the wildcard feature that you want to use to load multiple resources, namely, classpath*:sqlmap/*.xml is specific to Spring and not MyBatis.
I know, that the way it's documented in the MyBatis-Spring docs may lead you to believe that it's a MyBatis feature that let's you do this type of wildcard Resource loading, but it's not. Here's the relevant part of the MyBatis-Spring doc (source: https://mybatis.org/spring/factorybean.html#properties):
The mapperLocations property takes a list of resource locations. This property can be used to specify the location of MyBatis XML mapper files. The value can contain Ant-style patterns to load all files in a directory or to recursively search all paths from a base location.
However, sadly the docs only provide a Spring example based on XML and not Java configuration. If you read the Java Docs docs for SqlSessionFactoryBean, you'll find the following (source: https://mybatis.org/spring/apidocs/org/mybatis/spring/SqlSessionFactoryBean.html#setMapperLocations(org.springframework.core.io.Resource...)):
public void setMapperLocations(org.springframework.core.io.Resource... mapperLocations)
Set locations of MyBatis mapper files that are going to be merged into the
SqlSessionFactory configuration at runtime.
This is an alternative to specifying "<sqlmapper>" entries in an MyBatis config file.
This property being based on Spring's resource abstraction also allows for
specifying resource patterns here: e.g. "classpath*:sqlmap/*-mapper.xml".
Parameters:
mapperLocations - location of MyBatis mapper files
So, the setMapperLocations method needs one or more org.springframework.core.io.Resource object(s). So, using Spring ClassPathResource will not work here because ClassPathResource expects only a single resource. What you need to use instead is Spring's PathMatchingResourcePatternResolver class. See: https://docs.spring.io/spring-framework/docs/current/javadoc-api/org/springframework/core/io/support/PathMatchingResourcePatternResolver.html
You may also find this Stack Overflow post useful: How to use wildcards when searching for resources in Java-based Spring configuration?
I hope this helps!
your property should be like this.
if you use default configuration,
mybatis.mapper-locations: classpath*:sqlmap/**/*.xml
if you use your own as you mention above,
datasource.mybatis-factory.mapper-locations= classpath*:sqlmap/**/*.xml
this is a working example code, you can get an idea from this.
#Configuration
#MapperScans(
{
#MapperScan(
basePackages = {"com.example.seeker.repository"},
sqlSessionFactoryRef = "sqlSessionFactorySeeker",
sqlSessionTemplateRef = "sqlSessionTemplateSeeker"
)
}
)
public class SeekerDataSourceConfig {
#Autowired
Environment environment;
#Bean(value = "dsSeeker")
#ConfigurationProperties(prefix = "spring.datasource.seeker")
DataSource dsSeeker() {
return DataSourceBuilder.create().build();
}
#Qualifier("dsSeeker")
#Autowired
private DataSource dsSeeker;
#Bean(value = "sqlSessionFactorySeeker")
public SqlSessionFactory sqlSessionFactorySeeker() {
SqlSessionFactory sessionFactory = null;
try {
SqlSessionFactoryBean bean = new SqlSessionFactoryBean();
PathMatchingResourcePatternResolver pathM3R = new PathMatchingResourcePatternResolver();
bean.setMapperLocations(pathM3R.getResources("classpath*:sqlmap/**/*.xml"));
bean.setDataSource(dsSeeker);
sessionFactory = bean.getObject();
} catch (Exception e) {
e.printStackTrace();
}
return sessionFactory;
}
#Bean(value = "sqlSessionTemplateSeeker")
public SqlSessionTemplate sqlSessionTemplateSeeker() {
return new SqlSessionTemplate(sqlSessionFactorySeeker());
}
#Bean(name = StaticResource.TxManager.TX_MANAGER_SEEKER)
public PlatformTransactionManager txManager() {
return new DataSourceTransactionManager(dsSeeker);
}
}
property file
spring.datasource.seeker.jdbc-url=jdbc:mysql://*********
spring.datasource.seeker.username=seeker***
spring.datasource.seeker.password=ewfky4eyrmggxbw6**
spring.datasource.seeker.driver-class-name=com.mysql.cj.jdbc.Driver
if you want to using with yml file you can add in the following path your application.yml file
mybatis:
mapperLocations: classpath:sql/*.xml
config-location: classpath:config/mybatis.xml
Implemented Spring's AbstractRoutingDatasource by dynamically determining the actual DataSource based on the current context.
Refered this article : https://www.baeldung.com/spring-abstract-routing-data-source.
Here on spring boot application start up . Created a map of contexts to datasource objects to configure our AbstractRoutingDataSource. All these client context details are fetched from a database table.
#Bean
#DependsOn("dataSource")
#Primary
public DataSource routeDataSource() {
RoutingDataSource routeDataSource = new RoutingDataSource();
DataSource defaultDataSource = (DataSource) applicationContext.getBean("dataSource");
List<EstCredentials> credentials = LocalDataSourcesDetailsLoader.getAllCredentails(defaultDataSource); // fetching from database table
localDataSourceRegistrationBean.registerDataSourceBeans(estCredentials);
routeDataSource.setDefaultTargetDataSource(defaultDataSource);
Map<Object, Object> targetDataSources = new HashMap<>();
for (Credentials credential : credentials) {
targetDataSources.put(credential.getEstCode().toString(),
(DataSource) applicationContext.getBean(credential.getEstCode().toString()));
}
routeDataSource.setTargetDataSources(targetDataSources);
return routeDataSource;
}
The problem is if i add a new client details, I cannot get that in routeDataSource. Obvious reason is that these values are set on start up.
How can I achieve to add new client context and I had to re intialize the routeDataSource object.
Planning to write a service to get all the client context newly added and reset the routeDataSource object, no need to restart the server each time any changes in the client details.
A simple solution to this situation is adding #RefreshScope to the bean definition:
#Bean
#Primary
#RefreshScope
public DataSource routeDataSource() {
RoutingDataSource routeDataSource = new RoutingDataSource();
DataSource defaultDataSource = (DataSource) applicationContext.getBean("dataSource");
List<EstCredentials> credentials = LocalDataSourcesDetailsLoader.getAllCredentails(defaultDataSource); // fetching from database table
localDataSourceRegistrationBean.registerDataSourceBeans(estCredentials);
routeDataSource.setDefaultTargetDataSource(defaultDataSource);
Map<Object, Object> targetDataSources = new HashMap<>();
for (Credentials credential : credentials) {
targetDataSources.put(credential.getEstCode().toString(),
(DataSource) applicationContext.getBean(credential.getEstCode().toString()));
}
routeDataSource.setTargetDataSources(targetDataSources);
return routeDataSource;
}
Add Spring Boot Actuator as a dependency:
<dependency>
<groupId>org.springframework.boot</groupId>
<artifactId>spring-boot-starter-actuator</artifactId>
</dependency>
Then trigger the refresh endpoint POST to /actuator/refresh to update the DataSource (actually every refresh scoped bean).
So this will depend on how much you know about the datasources to be added, but you could set this up as a multi-tenant project. Another example of creating new datasources:
#Autowired private Map <String, Datasource> mars2DataSources;
public void addDataSourceAtRuntime() {
DataSourceBuilder dataSourcebuilder = DataSourcebuilder.create(
MultiTenantJPAConfiguration.class.getclassloader())
.driverclassName("org.postgresql.Driver")
.username("postgres")
.password("postgres")
.url("Jdbc: postgresql://localhost:5412/somedb");
mars2DataSources("tenantX", datasourcebuilder.build())
}
Given that you are using Oracle, you could also use its database change notification features.
Think of it as a listener in the JDBC driver that gets notified whenever something changes in your database table. So upon receiving a change, you could reinitialize/add datasources.
You can find a tutorial of how to do this here: https://docs.oracle.com/cd/E11882_01/java.112/e16548/dbchgnf.htm#JJDBC28820
Though, depending on your organization database notifications need some extra firewall settings for the communication to work.
Advantage: You do not need to manually call the REST Endpoint if something changes, (though Marcos Barberios answer is perfectly valid!)
Is there a solution that is configured driver properties in the application.properties file?
DB2 HADR using clientRerouteAlternateServerName, clientRerouteAlternatePortNumber, enableClientAffinitiesList to indicate alternate server info.
I tried use below code to config datasource.
```
#Bean
public DataSource initds(){
Properties p = new Properties();
p.setProperty("user", "user");
p.setProperty("password", "password");
p.setProperty("clientRerouteAlternateServerName", "172.30.1.60,172.30.1.61");
p.setProperty("clientRerouteAlternatePortNumber", "53000,53000");
p.setProperty("enableClientAffinitiesList", "1");
DriverManagerDataSource dmds = new DriverManagerDataSource();
dmds.setDriverClassName("com.ibm.db2.jcc.DB2Driver");
dmds.setConnectionProperties(p);
dmds.setUrl("jdbc:db2://172.30.1.60:53000/dbsam");
return dmds;
}
```
My Spring Boot 1.3.1 based application relies on an Oracle 11.2 database and I want to tune the fetching of SELECT statement results.
JdbcTemplate offers public void setFetchSize(int fetchSize) to tune the fetch size, which for Oracle is preset to 10 by the driver:
Set the fetch size for this JdbcTemplate. This is important for
processing large result sets: Setting this higher than the default
value will increase processing speed at the cost of memory
consumption; setting this lower can avoid transferring row data that
will never be read by the application. Default is -1, indicating to
use the JDBC driver's default (i.e. to not pass a specific fetch size
setting on the driver).
The Oracle JDBC driver (I use ojdbc7.jar because it is downwards compatible) offers a defaultRowPrefetch parameter to increase the fetch size for the complete database connection.
According to the docs this parameter could be set this way:
java.util.Properties info = new java.util.Properties();
info.put ("user", "scott");
info.put ("password","tiger");
info.put ("defaultRowPrefetch","15");
getConnection ("jdbc:oracle:oci8:#",info);
But my application is configured using application.yml:
datasource:
url: jdbc:oracle:thin:#xyz:1521:abc
username: ${name}
password: ${password}
driver-class-name: oracle.jdbc.driver.OracleDriver
...
And even if I wanted to change that configuration to use spring.datasource.url=jdbc:... instead there is no way to set the fetch size globally according to this post.
Is there a more "Spring Boot style" approach or do I need to configure each template manually ?
A BeanPostProcessor will process all the beans in the ApplicationContext and that way you can add additional configuration or replace it totally if you would like.
You could create a BeanPostProcessor that would add the properties to the configured DataSource. The sample below assumes the use of commons-dbcp 1 or 2 if you use a different DataSource modify accordingly.
public class DataSourceConfiguringBeanPostProcessor implements BeanPostProcessor {
private final Map<String,String> properties = new HashMap<>;
public Object postProcessBeforeInitialization(Object bean, String beanName) throws BeansException {
if (bean instance BasicDataSource ) {
for (Map.Entry<String, String> prop : properties.entrySet()) {
((BasicDataSource) bean).addConnectionProperty(prop.getKey(), prop.getValue());
}
}
return bean;
}
public Object postProcessAfterInitialization(Object bean, String beanName) throws BeansException {
return bean;
}
public void setProperties(Map<String, String> properties) {
this.properties.putAll(properties);
}
}
Now you can add this to your configuration and it will add the properties to DataSource beans.
#Bean
public BeanPostProcessor dataSourcePostProcessor() {
DataSourceConfiguringBeanPostProcessor processor = new DataSourceConfiguringBeanPostProcessor();
Map<String, String> properties = new HashMap<>();
properties.put("defaultRowPrefetch", "15");
properties.put("defaultBatchValue", "25");
processor.setProperties(properties);
return processor;
}
That should do the trick for configuring the datasource.
Using NetBeans IDE and Glassfish Server. For some reason I can't get a DataSource injected (have tried a million variations).
private DataSource iserver;
#Resource(name="jdbc/iserver", type=DataSource.class)
public void setIServer(DataSource dataSource) {
this.iserver = dataSource;
}
(I already tried adding #Resource annotation directly to the field). The connection pool and jdbc resource are configured on Glassfish, and for the time being, I have added the workaround code (in the same class):
ctx = new InitialContext();
iserver = (DataSource) ctx.lookup("jdbc/iserver");
The workaround code works perfectly. I don't see any obvious relevant errors in Glassfish log. I do see this, but not sure it's related:
*name cannot be null at javax.management.ObjectName.construct(ObjectName.java:405) * at javax.management.ObjectName.(ObjectName.java:1403) at org.glassfish.admingui.common.handlers.ProxyHandlers.getRuntimeProxyAttrs(ProxyHandlers.java:289) at org.glassfish.admingui.common.handlers.ProxyHandlers.getProxyAttrs(ProxyHandlers.java:273) at ...
Any suggestions?
Choose the option "name" by "lookup" -->
#Resource(lookup = "java:global/env/jdbc/__default")
DataSource dataSource;
Make sure you're in a session bean or injection won't work.
Here's an example of how I inject
#Resource(name="jdbc/my_db") private DataSource dataSource;