Using spring-boot-actuator version 2.1.7.RELEASE the following problem is observed:
The application uses a yaml configuration file that contains something like this:
datasource:
transaction:
managers:
- transactionManagerA
- transactionManagerB
- transactionManagerC
- transactionManagerD
This is injected into a class like:
@Configuration
public class MultipleDatasourceTransactionManager {
private static final Logger LOGGER = LoggerFactory.getLogger(MultipleDatasourceTransactionManager.class);
public static final String BEAN_ID_LIST_OF_TRANSACTION_MANAGERS = "listOfTransactionManagers";
public static final String BEAN_ID_TRANSACTION_MANAGER = "transactionManager";
@Autowired
private ApplicationContext context;
@Bean (name = BEAN_ID_LIST_OF_TRANSACTION_MANAGERS)
@ConfigurationProperties (prefix = "datasource.transaction.managers")
public List<String> listOfTransactionManagers() {
return new ArrayList<>();
}
...
... but when checking the rest API associated with the actuator endpoint /actuator/configprops
it contains a snippet:
"listOfTransactionManagers": {
"prefix": "datasource.transaction.managers",
"properties": {
"error": "Cannot serialize 'datasource.transaction.managers'"
}
},
... and this can be tracked to the class ConfigurationPropertiesReportEndpoint
which shows:
private Map<String, Object> safeSerialize(ObjectMapper mapper, Object bean, String prefix) {
try {
return new HashMap<>(mapper.convertValue(bean, Map.class));
}
catch (Exception ex) {
return new HashMap<>(Collections.singletonMap("error", "Cannot serialize '" + prefix + "'"));
}
}
Comment From: snicoll
Thanks for the report. binding on a list is quite unusual, I wouldn't expect this to work at all. The javadoc of @ConfigurationProperties
states that:
Binding is either performed by calling setters on the annotated class or, if @ConstructorBinding is in use, by binding to the constructor parameters. Note that contrary to
@Value
, SpEL expressions are not evaluated since property values are externalized.
That listOfTransactionManagers
should be a POJO with a List<String>
property. I don't know how easy it would be to support this use case or if we want to. Flagging for team attention.
Comment From: wilkinsona
This is (probably accidentally) already supported by the binder. Here's a standalone example:
package com.example;
import java.util.ArrayList;
import java.util.List;
import org.springframework.boot.autoconfigure.SpringBootApplication;
import org.springframework.boot.builder.SpringApplicationBuilder;
import org.springframework.boot.context.properties.ConfigurationProperties;
import org.springframework.context.annotation.Bean;
@SpringBootApplication
public class Gh20319Application {
public static void main(String[] args) {
System.out.println(new SpringApplicationBuilder(Gh20319Application.class)
.properties("datasource.transaction.managers[0]=transactionManagerA",
"datasource.transaction.managers[1]=transactionManagerB",
"datasource.transaction.managers[2]=transactionManagerC",
"datasource.transaction.managers[3]=transactionManagerD")
.run().getBean("listOfTransactionManagers"));
}
@Bean
@ConfigurationProperties(prefix = "datasource.transaction.managers")
public List<String> listOfTransactionManagers() {
return new ArrayList<>();
}
}
Running the above will output the following:
[transactionManagerA, transactionManagerB, transactionManagerC, transactionManagerD]
It's unfortunate that there's an asymmetry between what the binder is capable of binding and what the config props endpoint is capable of reporting. I think we either need to tighten up the binder to remove the undocumented behaviour or we need to improve the configprops endpoint. The former may break existing applications, so the latter's my preferred option at the moment.
Comment From: wilkinsona
@Technolords The documented way to achieve what you want is, as @snicoll said above, to use a POJO for your properties. The above example modified to do that would look like this:
@SpringBootApplication
@EnableConfigurationProperties(DatasourceTransactionProperties.class)
public class Gh20319Application {
public static void main(String[] args) {
System.out.println(new SpringApplicationBuilder(Gh20319Application.class)
.properties("datasource.transaction.managers[0]=transactionManagerA",
"datasource.transaction.managers[1]=transactionManagerB",
"datasource.transaction.managers[2]=transactionManagerC",
"datasource.transaction.managers[3]=transactionManagerD")
.run().getBean(DatasourceTransactionProperties.class).getManagers());
}
@ConfigurationProperties(prefix = "datasource.transaction")
static class DatasourceTransactionProperties {
private final List<String> managers = new ArrayList<>();
public List<String> getManagers() {
return managers;
}
}
}
The response for the configprops endpoint will then contain the following:
"datasource.transaction-com.example.Gh20319Application$DatasourceTransactionProperties": {
"prefix": "datasource.transaction",
"properties": {
"managers": [
"transactionManagerA",
"transactionManagerB",
"transactionManagerC",
"transactionManagerD"
]
}
}
An additional advantage of this approach is that you no longer have to retrieve the properties bean by name as it has a specific type.
Comment From: Technolords
Thanks! That is useful, and I will wrap my list in a bean so I don't have this problem anymore.
Comment From: wilkinsona
We discussed this one today and concluded that, ideally, the binder would fail fast in such a scenario as it's is binding something as configuration properties that doesn't meet the documented contract of being a POJO with properties. However, we also decided that we don't want to change the binder at this time as it would be a breaking change for anyone relying on the current behaviour with the only benefit being the serialisation errors would not be reported in the configprops endpoint. For users that do not use the configprops endpoint that benefit does not justify the cost of the change.
In short, we're going to leave things as they are and advise anyone else in the same situation to follow the recommendation of using POJOs for all types to which configuration properties are bound.