How to read a variable number of #RequestMapping paths from properties - spring

In Spring (Boot) I can externalize annotation values to application / environment properties using the ${...} syntax:
#RequestMapping("${some.path.property}")
I can also map a controller to more than one path:
#RequestMapping("/one", "/two")
How do I combine the two? I would like to define a list of paths in my properties, either as comma-separated /one, /two or (preferably) as a list in my application.yaml:
some.path.property:
- /one
- /two
But how can I interpolate either kind of list into the annotation?
#RequestMapping(???)
Edit: I couldn't figure out how to read the entire list from YAML (maybe because it's turned into separate properties some.path.property[0], some.path.property[1]... at YAML parse time?)
For the simpler case of a single CSV property, say:
some.csv.property: /one, /two
I can use a property substitution: "${some.csv.property}" or an explicit SpEL split: "#{'${some.csv.property}'.split('[, ]+')}" to convert it into an array, but in both cases it only work for #Value annotations. If I try it on #RequestMapping, I always end up with a single path.
Edit2: I can do this, where -- is just a random string that is not a valid path, but it's super ugly:
#RequestMapping(
"${some.path.property[0]:--}",
"${some.path.property[1]:--}",
"${some.path.property[2]:--}",
"${some.path.property[3]:--}",
"${some.path.property[4]:--}",
"${some.path.property[5]:--}",
"${some.path.property[6]:--}",
"${some.path.property[7]:--}",
"${some.path.property[8]:--}",
"${some.path.property[9]:--}"
)

Have You tried this?:
some:
path:
property: /one, /two
And then
#RequestMapping("${some.path.property}")
Based on this answer https://stackoverflow.com/a/41462567/7425783 it should work fine

If you have a yaml property file (don't repeat your self principle :) ), you can do it like so:
some:
path:
property:
one: /path1
two: /path2
If you're using #GetMapping ( or #RequestMapping ) you can do it like this in your controller :
#GetMapping(value={"${some.path.property.one}", "${some.path.property.two}"})
And here is the log
Mapped "{[/path1 || /path2],methods=[GET]}" onto public java.util.List<com.zero.SimpleController> com.zero.SimpleController.hello()

You can try create HandlerMapping to add urls, here is just an example to use SimpleUrlHandlerMapping
#RestController
public class WelcomeController {
public String ping() {
return "pong";
}
}
#SpringBootApplication
#Slf4j
#RestController
public class StackOverflowApplication {
#Autowired
WelcomeController welcomeController;
#Value("${paths}")
List<String> paths;
public static void main(String[] args) {
SpringApplication.run(StackOverflowApplication.class, args);
}
#Bean
public SimpleUrlHandlerMapping simpleUrlHandlerMapping() {
SimpleUrlHandlerMapping simpleUrlHandlerMapping = new SimpleUrlHandlerMapping();
Map<String, Object> map = new HashMap<>();
final Method getUser = ReflectionUtils.findMethod(WelcomeController.class, "ping");
final HandlerMethod handlerMethod = new HandlerMethod(welcomeController, getUser);
for (String path : paths) {
map.put(path, handlerMethod);
}
simpleUrlHandlerMapping.setUrlMap(map);
simpleUrlHandlerMapping.setOrder(Ordered.HIGHEST_PRECEDENCE);
return simpleUrlHandlerMapping;
}
}
yml file
paths: ping, ping1, ping2, ping3
Here is the code in github

Related

Retrieve YML / YAML properties

I have a external yaml properties files that I have loaded and I want to retrieve. But the suggested way to get YAML is like so:
#Value("${some.var}");
and this isn't working.
I am loading the files in like so:
#Bean
public static PropertySourcesPlaceholderConfigurer properties() {
String userHome = System.getProperty('user.home');
ArrayList<String> locations = new ArrayList<String>(
Arrays.asList(
"${userHome}/.boot/beapi_server.yml",
"${userHome}/.boot/beapi.yml",
"${userHome}/.boot/beapi_db.yml",
"${userHome}/.boot/beapi_api.yml"
)
);
Collections.reverse(locations);
PropertySourcesPlaceholderConfigurer propertySourcesPlaceholderConfigurer = new PropertySourcesPlaceholderConfigurer();
for (String location : locations) {
String finalLocation = location.toString();
YamlPropertiesFactoryBean yaml = new YamlPropertiesFactoryBean();
yaml.setResources(new FileSystemResource(finalLocation));
propertySourcesPlaceholderConfigurer.setProperties(Objects.requireNonNull(yaml.getObject()));
}
return propertySourcesPlaceholderConfigurer;
}
The actuator/configprops also don't show the properties.
What am I doing wrong? A bit frustrated.
Ok so I figured it out and wanted to post the answer for others to see.
After loading the external files into your PropertySources, you can now access them through #ConfigurationProperties annotation by referencing the YML structure prefix head and then just access the properties directly through assignment
// 'tomcat' if the top level prefix in my yml file
#ConfigurationProperties(prefix="tomcat")
class something{
ArrayList jvmArgs
}
Now lets look at my yml:
tomcat:
jvmArgs:
-'-Xms1536m'
-'-Xmx2048m'
-'-XX:PermSize=256m'
-'-XX:MaxPermSize=512m'
-'-XX:MaxNewSize=256m'
-'-XX:NewSize=256m',
-'-XX:+CMSClassUnloadingEnabled'
-'-XX:+UseConcMarkSweepGC'
-'-XX:+CMSIncrementalMode'
-'-XX:+CMSIncrementalPacing'
-'-XX:CMSIncrementalDutyCycle=10'
-'-XX:+UseParNewGC'
-'-XX:MaxGCPauseMillis=200'
-'-XX:MaxGCMinorPauseMillis=50'
-'-XX:SurvivorRatio=128'
-'-XX:MaxTenuringThreshold=0'
-'-server'
-'-noverify'
-'-Xshare:off'
-'-Djava.net.preferIPv4Stack=true'
-'-XX:+EliminateLocks'
-'-XX:+ExplicitGCInvokesConcurrent'
-'-XX:+UseBiasedLocking'
-'-XX:+UseTLAB'
And we see this will directly assign the value jvmArgs to the ArrayList JvmArgs in the class above. Simple.
So, rather simple and elegant solution once you know how... but knowing how is the trick isn't it :)
i'm using yaml configuration too. in my case, you have to create a configuration class then you can autowired it.
this is my code:
PdfConfig.java
#Component
#ConfigurationProperties(prefix = "pdf")
#EnableConfigurationProperties
public class PdfConfig {
private String licensePath;
private Watermark watermark;
// setter getter goes here
}
PdfServiceImpl.java
#Service
public class PdfServiceImpl implements PdfService {
#Autowired
private PdfConfig pdfConfig;
}
application-local.yml
#### Pdf Config ####
pdf:
license-path: classpath:license/development/itextkey.xml
watermark:
image-path: classpath:static/img/logo.png
opacity: 0.2f
enabled: true
If you want to learn about setting spring yaml, you can go to this link spring-yaml
Hope this helped!

How to disable spring boot parameter split

We have many #RestController receiving phrases in common language written by users. Phrases can be very long and contains punctuation, like periods and, of course, commas.
Simplified controller example:
#RequestMapping(value = "/countphrases", method = RequestMethod.PUT)
public String countPhrases(
#RequestParam(value = "phrase", required = false) String[] phrase) {
return "" + phrase.length;
}
Spring boot default behaviour is to split parameters values at comma, so the previous controller called with this url:
[...]/countphrases?phrase=john%20and%20me,%20you%and%her
Will return "2" istead of "1" like we want. In fact with the comma split the previous call is equivalent to:
[...]/countphrases?phrase=john%20and%20me&phrase=you%and%her
We work with natural language and we need to analyze phrases exactly how the users wrote them and to know exactly how many they wrote.
We tried this solution: https://stackoverflow.com/a/42134833/1085716 after adapting it to our spring boot version (2.0.5):
#Configuration
public class MvcConfig implements WebMvcConfigurer {
#Override
public void addFormatters(FormatterRegistry registry) {
// we hoped this code could remove the "split strings at comma"
registry.removeConvertible(String.class, Collection.class);
}
}
But it doesn't work.
Anyone know how to globally remove the "spring boot split string parameters at comma" behaviour in spring boot 2.0.5?
I find the solution.
To override a default conversion we must add a new one. If we remove the old one only it doesn't work.
The correct (example) code should be:
#Configuration
public class MvcConfig implements WebMvcConfigurer {
#Override
public void addFormatters(FormatterRegistry registry) {
registry.removeConvertible(String.class, String[].class);
registry.addConverter(String.class, String[].class, noCommaSplitStringToArrayConverter());
}
#Bean
public Converter<String, String[]> noCommaSplitStringToArrayConverter() {
return new Converter<String, String[]>() {
#Override
public String[] convert(String source) {
String[] arrayWithOneElement = {source};
return arrayWithOneElement;
}
};
}
}
This way any controller like the one in the main question will not split parameters values:
[...]/countphrases?phrase=a,b will return 1 (and fq=["a,b"])
[...]/countphrases?phrase=a,b&phrase=c,d will return 2 (and fq=["a,b", "c,d"])
Replacing your formatter registry with a completely new list could make you loose some needed default formatters that would come with the framework. This will also disable all String-To-Collections parsing for the entire application, on every endpoint, such that if you want to a request filter such as the following at another endpoint, it won't work:
identifiers = 12,34,45,56,67
Solution:
Just change your delimiter into something else... # or ; or $
identifiers = 12;23;34;45;56
This is what I have been doing, so I don't mess with all the goodies in the formatter registry.

Preventing Spring Boot from creating nested map from dot-separated key in application.yml?

I have a problem with Spring Boot creating a nested map from a dot-separated key. It's essentially the same problem that is described here, but the solution suggested there doesn't work for me. I'm using Spring Boot 1.5.3.RELEASE in case it matters. My applications.yml file contains this:
props:
webdriver.chrome.driver: chromedriver
My config class:
#Configuration
#EnableConfigurationProperties
public class SpringConfig {
private Map<String, String> props = new HashMap<>();
#ConfigurationProperties(prefix = "props")
public void setProps(Map<String, String> props) {
this.props = props;
}
#ConfigurationProperties(prefix = "props")
#Bean(destroyMethod="", name = "props")
public Map<String, String> getProps() {
return props;
}
}
Unfortunately, after Spring Boot processes the YAML file, the dot separated key gets split up into sub-maps. The result from callig getProps() and printing the result to System.out looks like this:
{webdriver={chrome={driver=chromedriver}}}
I've tried changing the type of the props field to Properties, Map<String, Object> etc, but nothing seems to make any difference.
I haven't found any way of manipulating the parsing behavior to accomplish what I want. Any help is much appreciated. I've spent so much time on this, that I'll go blind if I look at the code any further.
Try using YamlMapFactoryBean this will load YAML as MAP.
#Bean
public YamlMapFactoryBean yamlFactory() {
YamlMapFactoryBean factory = new YamlMapFactoryBean();
factory.setResources(resource());
return factory;
}
public Resource resource() {
return new ClassPathResource("application.yml");
}
public Map<String, String> getProps() {
props = yamlFactory().getObject();
return props;
}
The output looks
props{webdriver.chrome.driver=chromedriver}
After much experimenting, this seemed to work:
#Configuration
#EnableAutoConfiguration
#EnableConfigurationProperties
#ConfigurationProperties
public class SpringConfig {
private Properties info = new Properties();
public Properties getProps() {
return info;
}
}
}
But I had to put single quotes around the YAML entry, otherwise Spring Boot would make the property nested:
props:
'webdriver.chrome.driver': chromedriver
'foo.bar.baz': foobarbaz
A couple of things I noticed. The getter for the Properties (getProps() in this case) must be declared public, and it has to match the property key that you're trying to bind in the YAML. I.e. since the key is 'props', the getter has to be called getProps(). I guess it's logical, and documented somewhere, but that had slipped me by somehow. I thought by using the prefix="foobar" on the #ConfigurationProperties annotation, that wasn't the case, but it seems to be. I guess I should RTFM ;-)

How to bind a string array of properties in Spring?

I have the following in my application.properties file
some.server.url[0]=http://url
some.server.url[1]=http://otherUrl
How do I refer to the array of properties using the #Value anotation inside a #Bean method?
I am using Java 6 with Tomcat 7 and Spring boot 1.4
I was also having the same problem as you mentioned and it seems using index form on application.properties was not working for me either.
To solve the problem I did something like below
some.server.url = url1, url2
Then to get the those properties I simply use #Value
#Value("${some.server.url}")
private String[] urls ;
Spring automatically splits the String with comma and return you an Array. AFAIK this was introduced in Spring 4+
If you don't want comma (,) as seperator you have to use SpEL like below.
#Value("#{'${some.server.url}'.split(',')}")
private List<String> urls;
where split() accepts the seperator
You can use a collection.
#Value("${some.server.url}")
private List<String> urls;
You can also use a configuration class and inject the bean into your other class:
#Component
#ConfigurationProperties("some.server")
public class SomeConfiguration {
private List<String> url;
public List<String> getUrl() {
return url;
}
public void setUrl(List<String> url) {
this.url = url;
}
}
Follow these steps
1)
#Value("${some.server.url}")
private List urls;
2)
#ConfigurationProperties("some.server")
public class SomeConfiguration {
3)
You should have getter and setter for instance variable 'urls'

Spring-boot ConditionalOnProperty with map-based properties

My spring-boot yaml properties look like this:
service:
mycomponent:
foo:
url: http://foo
bar:
url: http://bar
This results in the following properties being set in the Spring environment:
service.mycomponent.foo.url: http://foo
service.mycomponent.bar.url: http://bar
I'd like to define a 'mycomponent' bean if there are any properties that match service.mycomponent.[a-z]*.url. Is this possible using #ConditionalOnExpression or some other type of #Conditional?
I realize I can work around this by either adding a property such as service.mycomponent.enabled: true that could be used with #ConditionalOnProperty but I'd rather avoid that if possible.
Here's the solution I ended up taking:
Create a custom Condition which searches for any properties with a certain prefix. The RelaxedPropertyResolver has the convenient getSubProperties() method. Alternative options I found were cumbersome to iterate through the PropertySource instances.
public class MyComponentCondition extends SpringBootCondition {
#Override
public ConditionOutcome getMatchOutcome(final ConditionContext context,
final AnnotatedTypeMetadata metadata) {
final RelaxedPropertyResolver resolver = new RelaxedPropertyResolver(context.getEnvironment());
final Map<String, Object> properties = resolver.getSubProperties("service.mycomponent.");
return new ConditionOutcome(!properties.isEmpty(), "My Component");
}
}
Use that condition when setting up the bean:
#Conditional(MyComponentCondition.class)
#Bean
public MyComponent myComponent() {
return new MyComponent();
}
I'm still curious if the same thing could be done with #ConditionalOnExpression directly.

Resources