I want to inject property values into the Spring context when it is started.
I am trying to do this using the new Environment and PropertySource features of Spring 3.1.
In the class in which loads the Spring context, I define my own PropertySource class as follows:
private static class CustomPropertySource extends PropertySource<String> {
public CustomPropertySource() {super("custom");}
#Override
public String getProperty(String name) {
if (name.equals("region")) {
return "LONDON";
}
return null;
}
Then, I add this property source to the application context:
ClassPathXmlApplicationContext springIntegrationContext =
new ClassPathXmlApplicationContext("classpath:META-INF/spring/ds-spring-intg-context.xml");
context.getEnvironment().getPropertySources().addLast( new CustomPropertySource());
context.refresh();
context.start();
In one of my beans, I try to access the property value:
#Value("${region}")
public void setRegion(String v){
...
}
bur recieve the folowing error:
java.lang.IllegalArgumentException: Caused by:
java.lang.IllegalArgumentException: Could not resolve placeholder
'region' in string value [${region}]
Any help is greatly appreciated
when you pass an XML file location as a constructor argument to ClassPathXmlApplicationContext(..) it straight away does the context.refresh()/context.start() methods. so by passing in your XML locations, you're essentially doing it all in one pass and the context is already started/loaded by the time you get to call context.getEnvironment().getPropertySources....
try this;
ClassPathXmlApplicationContext springIntegrationContext =
new ClassPathXmlApplicationContext();
context.getEnvironment().getPropertySources().addLast( new CustomPropertySource());
context.setLocations("classpath:META-INF/spring/ds-spring-intg-context.xml");
context.refresh();
it will set your sources, then your xml, then start the app context.
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
I'm trying to use Picocli with Spring Boot 2.2 to pass command line parameters to a Spring Bean, but not sure how to structure this. For example, I have the following #Command to specify a connection username and password from the command line, however, want to use those params to define a Bean:
#Component
#CommandLine.Command
public class ClearJdoCommand extends HelpAwarePicocliCommand {
#CommandLine.Option(names={"-u", "--username"}, description = "Username to connect to MQ")
String username;
#CommandLine.Option(names={"-p", "--password"}, description = "Password to connect to MQ")
String password;
#Autowired
JMSMessagePublisherBean jmsMessagePublisher;
#Override
public void run() {
super.run();
jmsMessagePublisher.publishMessage( "Test Message");
}
}
#Configuration
public class Config {
#Bean
public InitialContext getJndiContext() throws NamingException {
// Set up the namingContext for the JNDI lookup
final Properties env = new Properties();
env.put(Context.INITIAL_CONTEXT_FACTORY, INITIAL_CONTEXT_FACTORY);
env.put(Context.PROVIDER_URL, "http-remoting://localhost:8080");
env.put(Context.SECURITY_PRINCIPAL, username);
env.put(Context.SECURITY_CREDENTIALS, password);
return new InitialContext(env);
}
#Bean
public JMSPublisherBean getJmsPublisher(InitialContext ctx){
return new JMSPublisherBean(ctx);
}
}
I'm stuck in a bit of a circular loop here. I need the command-line username/password to instantiate my JMSPublisherBean, but these are only available at runtime and not available at startup.
I have managed to get around the issue by using Lazy intialization, injecting the ClearJdoCommand bean into the Configuration bean and retrieving the JMSPublisherBean in my run() from the Spring context, but that seems like an ugly hack. Additionally, it forces all my beans to be Lazy, which is not my preference.
Is there another/better approach to accomplish this?
Second option might be to use pure PicoCli (not PicoCli spring boot starter) and let it run command; command will not be Spring bean and will only be used to validate parameters.
In its call method, Command would create SpringApplication, populate it with properties (via setDefaultProperties or using JVM System.setProperty - difference is that environment variables will overwrite default properties while system properties have higher priority).
#Override
public Integer call() {
var application = new SpringApplication(MySpringConfiguration.class);
application.setBannerMode(Mode.OFF);
System.setProperty("my.property.first", propertyFirst);
System.setProperty("my.property.second", propertySecond);
try (var context = application.run()) {
var myBean = context.getBean(MyBean.class);
myBean.run(propertyThird);
}
return 0;
}
This way, PicoCli will validate input, provide help etc. but you can control configuration of Spring Boot application. You can even use different Spring configurations for different commands. I believe this approach is more natural then passing all properties to CommandLineRunner in Spring container
One idea that may be useful is to parse the command line in 2 passes:
the first pass is just to pick up the information needed for configuration/initialization
in the second pass we pick up additional options and execute the application
To implement this, I would create a separate class that "duplicates" the options that are needed for configuration. This class would have an #Unmatched field for the remaining args, so they are ignored by picocli. For example:
class Security {
#Option(names={"-u", "--username"})
static String username;
#Option(names={"-p", "--password"}, interactive = true, arity = "0..1")
static String password;
#Unmatched List<String> ignored;
}
In the first pass, we just want to extract the username/password info, we don't want to execute the application just yet. We can use the CommandLine.parseArgs or CommandLine.populateCommand methods for that.
So, our main method can look something like this:
public static void main(String[] args) throws Exception {
// use either populateCommand or parseArgs
Security security = CommandLine.populateCommand(new Security(), args);
if (security.username == null || security.password == null) {
System.err.println("Missing required user name or password");
new CommandLine(new ClearJdoCommand()).usage(System.err);
System.exit(CommandLine.ExitCode.USAGE);
}
// remainder of your normal main method here, something like this?
System.exit(SpringApplication.exit(SpringApplication.run(MySpringApp.class, args)));
}
I would still keep (duplicate) the usage and password options in the ClearJdoCommand class, so the application can print a nice usage help message when needed.
Note that I made the fields in the Security class static.
This is a workaround (hack?) that allows us to pass information to the getJndiContext method.
#Bean
public InitialContext getJndiContext() throws NamingException {
// Set up the namingContext for the JNDI lookup
final Properties env = new Properties();
env.put(Context.INITIAL_CONTEXT_FACTORY, INITIAL_CONTEXT_FACTORY);
env.put(Context.PROVIDER_URL, "http-remoting://localhost:8080");
env.put(Context.SECURITY_PRINCIPAL, Security.username); // use info from 1st pass
env.put(Context.SECURITY_CREDENTIALS, Security.password);
return new InitialContext(env);
}
There is probably a better way to pass information to this method.
Any Spring experts willing to jump in and show us a nicer alternative?
I have some countries list in map in a class. Now I want to access this map in my application in some other classes.
Basically I want this map available throughout the application.
I am getting countries list in boot application just after startup. But able to set it to application scope. I am using spring boot and web.
Below is my code.
#SpringBootApplication
public class FinalApplication {
public static void main(String[] args) {
ApplicationContext context = new SpringApplicationBuilder(FinalApplication.class).run(args);
Country cfg = context.getBean(Country.class);
CountryDataLoader loader = new CountryDataLoader(cfg);
}
#ApplicationScope
#Component
#NoArgsConstructor
public class CountryDataLoader {
Map<String,List<String>> regionMap = new HashMap<>();
Map<String,List<String>> languageMap = new HashMap<>();
public List<String> countryListByAmericaRegion () {
//System.out.println(myConfig.getList());
List<CountryBean> list = myConfig.getList();
Iterator<CountryBean> it = list.iterator();
while(it.hasNext()) {
CountryBean bean = it.next();
if(bean.getRegion().equals(AMERICAS)) {
byAmericaRegion.add(bean.getCountryName());
}
}
regionMap.put(AMERICAS, byAmericaRegion);
//System.out.println(byAmericaRegion);
return byAmericaRegion;
}
}
In countrydataLoader class I am loading all country data. how to access those two maps in some other restcontrollers?
Thanks
CountryDaoLoader is a spring bean and you can get the instance of a spring bean from application context as shown below.
ApplicationContextProvider.getApplicationContext().getBean(CountryDataLoader.class);
I am upgrading Spring 2.5 to 4.2.
The issue is with one bean which has property type org.springframework.core.io.ClassPathResource. The resource value is defined in xml as p:location="classpath:/<the resource path>"
This worked perfect and bean property was populated with the resource. But in 4.2 the value is not getting set.
So I debugged the code and found that the class org.springframework.beans.BeanWrapperImpl was manipulating the value and removing classpath: string from actual value in Spring 2.5.
However the same is not true in 4.2 and class org.springframework.beans.BeanWrapperImpl isn't modifying the value which results in spring not finding the resource.
Anyone faced similar situation? What solution did you apply?
Thanks,
Hanumant
EDIT 1: code sample
spring config file
<bean class="com.test.sample.TestBean" id="testBean"
p:schemaLocation="classpath:/com/test/sample/Excalibur_combined.xsd" />
TestBean.java
public class TestBean {
private ClassPathResource schemaLocation;
public ClassPathResource getSchemaLocation() {
return schemaLocation;
}
public void setSchemaLocation(ClassPathResource schemaLocation) {
this.schemaLocation = schemaLocation;
}
}
App.java
public class App {
public static void main(String[] args) {
ApplicationContext ap = new ClassPathXmlApplicationContext("classpath:/com/test/sample/spring-config.xml");
TestBean tb = (TestBean) ap.getBean("testBean");
try {
URL url = tb.getSchemaLocation().getURL();
System.out.println(url);
} catch (IOException e) {
// TODO Auto-generated catch block
e.printStackTrace();
}
}
}
Error Message
INFO: Loading XML bean definitions from class path resource
[com/test/sample/spring-config.xml] java.io.FileNotFoundException:
class path resource
[classpath:/com/test/sample/Excalibur_combined.xsd] cannot be resolved
to URL because it does not exist at
org.springframework.core.io.ClassPathResource.getURL(ClassPathResource.java:187)> at com.test.sample.App.main(App.java:20)
However if I remove the classpath: from bean definition it works.
So is classpth: necessary in bean definition xml file?? And why it was working fine in Spring 2.5??
The main issue is that you aren't programming to interfaces. Instead of the concrete org.springframework.core.io.ClassPathResource use org.springframework.core.io.Resource. When doing to the org.springframework.core.io.ResourceEditor will kick in and convert the String into a Resource instance. The location you are providing classpath:/<the resource path> will be passed to the ResourceLoader which will get the resource or throw an error if it doesn't exist.
If, however, you are using the concrete type ClassPathResouce directly this mechanism doesn't kick in and the location is set to what you provide classpath:/<the resource path>. However this is actually not a valid location for the URL class and that will eventually fail with the message you see.
It worked in earlier versions due to a hack/workaround/patch in the BeanWrapperImpl to strip the prefix.
Basically it now fails because you where doing things you shouldn't have been doing in the first place.
I am moving a working project from using SpringBoot command line arguments to reading properties from a file. Here are the involved portions of the #Configuration class:
#Configuration
class RemoteCommunication {
#Inject
StandardServletEnvironment env
#Bean
static PropertySourcesPlaceholderConfigurer placeholderConfigurer () {
// VERIFIED this is executing...
PropertySourcesPlaceholderConfigurer target = new PropertySourcesPlaceholderConfigurer()
// VERIFIED this files exists, is readable, is a valid properties file
target.setLocation (new FileSystemResource ('/Users/me/Desktop/mess.properties'))
// A Debugger does NOT show this property source in the inject Environment
target
}
#Bean // There are many of these for different services, only one shown here.
MedicalSorIdService medicalSorIdService () {
serviceInstantiator (MedicalSorIdService_EpicSoap, 'uri.sor.id.lookup.internal')
}
// HELPER METHODS...
private <T> T serviceInstantiator (final Class<T> classToInstantiate, final String propertyKeyPrimary) {
def value = retrieveSpringPropertyFromConfigurationParameter (propertyKeyPrimary)
classToInstantiate.newInstance (value)
}
private def retrieveSpringPropertyFromConfigurationParameter (String propertyKeyPrimary) {
// PROBLEM: the property is not found in the Environment
def value = env.getProperty (propertyKeyPrimary, '')
if (value.isEmpty ()) throw new IllegalStateException ('Missing configuration parameter: ' + "\"$propertyKeyPrimary\"")
value
}
Using #Value to inject the properties does work, however I'd rather work with the Environment directly if at all possible. If the settings are not in the Environment then I am not exactly sure where #Value is pulling them from...
env.getProperty() continues to work well when I pass in command line arguments specifying the properties though.
Any suggestions are welcome!
The issue here is the distinction between PropertySourcesPlaceholderConfigurer and StandardServletEnvironment, or Environment for simplicity.
The Environment is an object that backs the whole ApplicationContext and can resolve a bunch of properties (the Environment interface extends PropertyResolver). A ConfigurableEnvironment has a MutablePropertySources object which you can retrieve through getPropertySources(). This MutablePropertySources holds a LinkedList of PropertySource objects which are checked in order to resolve a requested property.
PropertySourcesPlaceholderConfigurer is a separate object with its own state. It holds its own MutablePropertySources object for resolving property placeholders. PropertySourcesPlaceholderConfigurer implements EnvironmentAware so when the ApplicationContext gets hold of it, it gives it its Environment object. The PropertySourcesPlaceholderConfigurer adds this Environment's MutablePropertySources to its own. It then also adds the various Resource objects you specified with setLocation() as additional properties. These Resource objects are not added to the Environment's MutablePropertySources and therefore aren't available with env.getProperty(String).
So you cannot get the properties loaded by the PropertySourcesPlaceholderConfigurer into the Environment directly. What you can do instead is add directly to the Environment's MutablePropertySouces. One way is with
#PostConstruct
public void setup() throws IOException {
Resource resource = new FileSystemResource("spring.properties"); // your file
Properties result = new Properties();
PropertiesLoaderUtils.fillProperties(result, resource);
env.getPropertySources().addLast(new PropertiesPropertySource("custom", result));
}
or simply (thanks #M.Deinum)
#PostConstruct
public void setup() throws IOException {
env.getPropertySources().addLast(new ResourcePropertySource("custom", "file:spring.properties")); // the name 'custom' can come from anywhere
}
Note that adding a #PropertySource has the same effect, ie. adding directly to the Environment, but you're doing it statically rather than dynamically.
In SpringBoot it's enough to use #EnableConfigurationProperties annotation - you don't need to setup PropertySourcesPlaceholderConfigurer.
Then on POJO you add annotation #ConfigurationProperties and Spring automatically injects your properties defined in application.properties.
You can also use YAML files - you just need to add proper dependency (like SnakeYaml) to classpath
You can find detailed example here: http://spring.io/blog/2013/10/30/empowering-your-apps-with-spring-boot-s-property-support
I achieved this during PropertySourcesPlaceholderConfigurer instantiation.
#Bean
public static PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurerBean(Environment env) {
PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer = new PropertySourcesPlaceholderConfigurer();
YamlPropertiesFactoryBean yamlFactorybean = new YamlPropertiesFactoryBean();
yamlFactorybean.setResources(determineResources(env));
PropertiesPropertySource yampProperties = new PropertiesPropertySource("yml", yamlFactorybean.getObject());
((AbstractEnvironment)env).getPropertySources().addLast(yampProperties);
propertySourcesPlaceholderConfigurer.setProperties(yamlFactorybean.getObject());
return propertySourcesPlaceholderConfigurer;
}
private static Resource[] determineResources(Environment env){
int numberOfActiveProfiles = env.getActiveProfiles().length;
ArrayList<Resource> properties = new ArrayList(numberOfActiveProfiles);
properties.add( new ClassPathResource("application.yml") );
for (String profile : env.getActiveProfiles()){
String yamlFile = "application-"+profile+".yml";
ClassPathResource props = new ClassPathResource(yamlFile);
if (!props.exists()){
log.info("Configuration file {} for profile {} does not exist");
continue;
}
properties.add(props);
}
if (log.isDebugEnabled())
log.debug("Populating application context with properties files: {}", properties);
return properties.toArray(new Resource[properties.size()]);
}
Maybe all you need is to set -Dspring.config.location=... (alternatively SPRING_CONFIG_LOCATION as an env var)? That has the effect of adding an additional config file to the default path for the app at runtime which takes precedence over the normal application.properties? See howto docs for details.