Rest Json Jackson Mapper Custom Object Mapper - spring

I am having an issue with the Jackson Json mapper which I can't figure out how to solve.
I am having a Spring MVC Rest application and the endpoints are converted to Json using Jackson.
Some of the result objects contain a type that I want to tamper with before it gets converted.
More specifically, a result object could look like this.
ResultObject
- getDoubleMap() : DoubleMap
- getDoubleEntries() : List<DoubleEntry>
- toMap() : Map<String, Double>
What I want to do is to not have Jackson convert the DoubleMap instance but much rather override it like this
Object someJacksonMapInterceptor(Object object) {
if(object instanceof DoubleMap) {
return ((DoubleMap) object).toMap();
}
return object;
}
I have tortured google quite a while now and not a simple solution. Hope someone can advise.
Many thanks in advance.

In one application, we are custom-deserealizing date, probably you can use it for your custom deserealization.
public class VitalSign {
public static final String DATE_FORMAT1 = "yyyy-MM-dd'T'HH:mm:ssZ";
public static final String DATE_FORMAT2 = "yyyy-MM-dd'T'HH:mm:ss";
//public static final String DATE_FORMAT3 = "yyyy-MM-dd'T'HH:mm:ssTDZ";
public static final String DATE_FORMAT4 = "MMM dd, yyyy h:mm:ss aa";
#NotNull
#Column(name = "observed")
#Temporal(TemporalType.TIMESTAMP)
#DateTimeFormat(style = "M-")
#JsonDeserialize(using = CustomJsonDateDeserializer.class)
private Date timestamp;
public static class CustomJsonDateDeserializer extends JsonDeserializer<Date> {
public CustomJsonDateDeserializer() {
super();
}
#Override
public Date deserialize(JsonParser jsonparser, DeserializationContext deserializationcontext) throws IOException, JsonProcessingException {
SimpleDateFormat[] formats = { new SimpleDateFormat(DATE_FORMAT1), new SimpleDateFormat(DATE_FORMAT2), new SimpleDateFormat(DATE_FORMAT4, Locale.US) };
String date = jsonparser.getText();
for (SimpleDateFormat format : formats) {
try {
return format.parse(date);
} catch (ParseException e) {
}
}
throw new RuntimeException("Unparseable date " + date);
}
}
}

For serializing, you can just annotate your toMap() method with #JsonValue. For deserializing, if you have a static factory to create a DoubleMap from a Map<String, Double>, you can just annotate that with #JsonCreator.
private final ObjectMapper mapper = new ObjectMapper();
#Test
public void serialize_doublemap() throws Exception {
DoubleMap map = new DoubleMap();
map.put("red", 0.5);
map.put("orange", 0.7);
assertThat(mapper.writeValueAsString(map), equivalentTo("{ red: 0.5, orange: 0.7 }"));
}
#Test
public void deserialize_doublemap() throws Exception {
assertThat(mapper.readValue("{ \"red\": 0.5, \"orange\": 0.7 }", DoubleMap.class).toMap(),
equalTo(ImmutableMap.of("red", 0.5, "orange", 0.7)));
}
public static class DoubleMap {
public List<DoubleEntry> entries = new ArrayList<>();
public void put(String label, double value) {
entries.add(new DoubleEntry(label, value));
}
#JsonCreator
public static DoubleMap fromJson(Map<String, Double> input) {
DoubleMap map = new DoubleMap();
input.forEach(map::put);
return map;
}
public List<DoubleEntry> getDoubleEntries() {
return entries;
}
#JsonValue
public Map<String, Double> toMap() {
return entries.stream().collect(Collectors.toMap(e -> e.label, e -> e.value));
}
}
public static final class DoubleEntry {
public final String label;
public final double value;
public DoubleEntry(String label, double value) {
this.label = label;
this.value = value;
}
}

Related

JPA criteria builder equal method is not working as expected

In my case I'm trying to fetch some data by extracting a value from a json column in db. My code is as follows,
criteriaBuilder.equal(criteriaBuilder.function("JSON_EXTRACT", Boolean.class, root.get("result"), criteriaBuilder.literal("$.matched")), false);
Above code gives me an empty set of data. Also this is working fine in query console.
But,
criteriaBuilder.between(criteriaBuilder.function("JSON_EXTRACT", Double.class, root.get("result"), criteriaBuilder.literal("$.streaming_threshold")), 0.1, 0.9);
this between method is working fine. What could be the mistake here?
UPDATE
Boolean values are the values that I couldn't read. NOT INTEGERS. My JSON structure,
{
"status": "SUCCESS",
"request_id": "request_id",
"time_taken": 8454,
"matched": false,
"streaming_threshold": 0.5
}
I was not able to get it working with the raw boolean field. I converted the Boolean property on the object to String and vice-versa using #JsonSerialize and #JsonDeserialize and then persisted that as json and followed the same approach you did but now searching String.class as false instead of Boolean. My solution is as below:
Entity
#Entity
#Table(name = "json_container")
public class JsonContainer {
#Id
#GeneratedValue
#Type(type = "uuid-char")
private UUID id;
#Column(columnDefinition = "json", name = "json_data")
private String jsonData;
public UUID getId() {
return id;
}
public String getJsonData() {
return jsonData;
}
public void setJsonData(String jsonData) {
this.jsonData = jsonData;
}
public static class SampleDetails {
private String status;
private String requestId;
private Integer timeTaken;
#JsonSerialize(using = StringBooleanJsonSerializer.class)
#JsonDeserialize(using = StringBooleanJsonDeserializer.class)
private Boolean matched;
private Double streamingThreshold;
public SampleDetails() {
}
public SampleDetails(String status, String requestId, Integer timeTaken, Boolean matched, Double streamingThreshold) {
this.status = status;
this.requestId = requestId;
this.timeTaken = timeTaken;
this.matched = matched;
this.streamingThreshold = streamingThreshold;
}
public String getStatus() {
return status;
}
public String getRequestId() {
return requestId;
}
public Integer getTimeTaken() {
return timeTaken;
}
public Double getStreamingThreshold() {
return streamingThreshold;
}
public Boolean getMatched() {
return matched;
}
static class StringBooleanJsonSerializer extends JsonSerializer<Boolean> {
#Override
public void serialize(Boolean value, JsonGenerator gen, SerializerProvider serializers) throws IOException {
gen.writeString(value != null && value ? value.toString() : "false");
}
}
static class StringBooleanJsonDeserializer extends JsonDeserializer<Boolean> {
#Override
public Boolean deserialize(JsonParser p, DeserializationContext ctxt) throws IOException, JsonProcessingException {
try {
return Boolean.parseBoolean(p.getText());
} catch (RuntimeException e) {
return Boolean.FALSE;
}
}
}
}
Test Class
class MySQLJsonConverterTest {
#Autowired
private EntityManager entityManager;
#Rollback(false)
#Test
void testCustomJsonConverter() throws JsonProcessingException {
JsonContainer jsonContainer = new JsonContainer();
jsonContainer.setJsonData(
getAsJson(new JsonContainer.SampleDetails("success", "12344567", 8454, false, 0.1)));
entityManager.persist(jsonContainer);
Assertions.assertNotNull(jsonContainer.getId());
jsonContainer = new JsonContainer();
jsonContainer.setJsonData(
getAsJson(new JsonContainer.SampleDetails("success", "8989", 121, true, 0.5)));
entityManager.persist(jsonContainer);
Assertions.assertNotNull(jsonContainer.getId());
CriteriaBuilder criteriaBuilder = entityManager.getCriteriaBuilder();
CriteriaQuery<JsonContainer> criteriaQuery = criteriaBuilder.createQuery(JsonContainer.class);
Root<JsonContainer> from = criteriaQuery.from(JsonContainer.class);
criteriaQuery.where(criteriaBuilder.equal(criteriaBuilder.function("JSON_EXTRACT", String.class, from.get("jsonData"),
criteriaBuilder.literal("$.matched")), "false"));
TypedQuery<JsonContainer> typedQuery = entityManager.createQuery(criteriaQuery);
List<JsonContainer> resultList = typedQuery.getResultList();
Assertions.assertEquals(1, resultList.size());
}
private String getAsJson(JsonContainer.SampleDetails sampleDetails) throws JsonProcessingException {
//var created so debugging is ez
String json = new ObjectMapper().writeValueAsString(sampleDetails);
return json;
}
}
Default JPA convert boolean to 0/1, so if serialize boolean to 0/1 into database, the equal query will be ok.

Custom Object as a #RequestParam

I have a paginated endpoint that looks like this /api/clients?range=0-25.
I'd like the getClients() method in my ClientController to directly receive an instance of a custom Range object rather than having to validate a "0-25" String but I'm having trouble figuring this out.
#Getter
final class Range {
#Min(0)
private Integer offset = 0;
#Min(1)
private Integer limit = 25;
}
#ResponseBody
#GetMapping(params = { "range" })
public ResponseEntity<?> getAllClients(#RequestParam(value = "range", required = false) QueryRange queryRange, final HttpServletResponse response) {
...
}
I'm not sure how to instruct the Controller to correctly deserialize the "0-25" string into the Range...
You can use a Converter<S, T>, as shown below:
#Component
public class RangeConverter implements Converter<String, Range> {
#Override
public Range convert(String source) {
String[] values = source.split("-");
return new Range(Integer.valueOf(values[0]), Integer.valueOf(values[1]));
}
}
You also could handle invalid values according to your needs. If you use the above converter as is, the attempt to convert an invalid value such as 1-x will result in a ConversionFailedException.
You can also do the following it seems :
public class QueryRangeEditor extends PropertyEditorSupport {
private static final Pattern PATTERN = Pattern.compile("^([1-9]\\d*|0)-([1-9]\\d*|0)$");
#Override
public void setAsText(String text) throws IllegalArgumentException {
final QueryRange range = new QueryRange();
Matcher matcher = PATTERN.matcher(text);
if (matcher.find()) {
range.setOffset(Integer.valueOf(matcher.group(1)));
range.setLimit(Integer.valueOf(matcher.group(2)));
} else {
throw new IllegalArgumentException("OI"); // todo - replace
}
setValue(range);
}
}
#InitBinder
public void initBinder(WebDataBinder binder) {
binder.registerCustomEditor(QueryRange.class, new QueryRangeEditor());
}
But #cassiomolin's looks cleaner...

Spring Rest -> Hibernate entity to JSON

I am creating REST API using spring framework. My entity is based on one table and REST API is supposed to be invoked using POST operation with below JSON structure. Can someone explain me how to map the entity class so that it can consume below-shown json.
Since my entity is based on only one table, I am not able to understand how can it create nested json objects for same table properties.
{
"process_ar_receipt": {
"message_header": {
"source_system_guid": "DDED-DBCD-REV-E1F4343DB3434",
"source_system": "MeSo_TravelAds"
},
"receipt_header": {
"customer_number": "123",
"source_receipt_number": "TESTRCPT_1523",
}
}
}
you could use Gson to convert the json to a DTO
https://jarroba.com/gson-json-java-ejemplos/
pseudo code
assuming your Entity class as
#Entity(name="foo")
class Data{
#Id
private String source_system_guid;
#Column
private String source_system;
#Column
private String customer_number;
#Column
private String source_receipt_number;
public Data() {}
public Data(String ssId, String sourceSystm, String custNum, String srcRcptNum) {
this.source_system_guid = ssId;
this.source_system = sourceSystm;
this.customer_number = custNum;
this.source_receipt_number = srcRcptNum;
}
public String getSource_system_guid() {
return source_system_guid;
}
public void setSource_system_guid(String source_system_guid) {
this.source_system_guid = source_system_guid;
}
public String getSource_system() {
return source_system;
}
public void setSource_system(String source_system) {
this.source_system = source_system;
}
public String getCustomer_number() {
return customer_number;
}
public void setCustomer_number(String customer_number) {
this.customer_number = customer_number;
}
public String getSource_receipt_number() {
return source_receipt_number;
}
public void setSource_receipt_number(String source_receipt_number) {
this.source_receipt_number = source_receipt_number;
}
}
Now since your DTO/BO i.e. Data Transfer Object or Business Object is different from the actual entity we will create the required BO object as below
class DataTO{
#JsonProperty("process_ar_receipt")
private ReceiptTO receiptTO=new ReceiptTO();
public ReceiptTO getReceiptTO() {
return receiptTO;
}
public void setReceiptTO(ReceiptTO receiptTO) {
this.receiptTO = receiptTO;
}
}
class ReceiptTO{
#JsonProperty("message_header")
private MessageHeader messageHeder = new MessageHeader();
#JsonProperty("receipt_header")
private ReceiptHeader receiptHeder = new ReceiptHeader();
public MessageHeader getMessageHeder() {
return messageHeder;
}
public void setMessageHeder(MessageHeader messageHeder) {
this.messageHeder = messageHeder;
}
public ReceiptHeader getReceiptHeder() {
return receiptHeder;
}
public void setReceiptHeder(ReceiptHeader receiptHeder) {
this.receiptHeder = receiptHeder;
}
}
class MessageHeader{
#JsonProperty("source_System_Guid")
private String sourceSystemId;
#JsonProperty("system_Id")
private String systemId;
public String getSourceSystemId() {
return sourceSystemId;
}
public void setSourceSystemId(String sourceSystemId) {
this.sourceSystemId = sourceSystemId;
}
public String getSystemId() {
return systemId;
}
public void setSystemId(String systemId) {
this.systemId = systemId;
}
}
class ReceiptHeader{
#JsonProperty("customer_number")
private String customerNumber;
#JsonProperty("source_rcpt_number")
private String sourceReceiptNumber;
public String getCustomerNumber() {
return customerNumber;
}
public void setCustomerNumber(String customerNumber) {
this.customerNumber = customerNumber;
}
public String getSourceReceiptNumber() {
return sourceReceiptNumber;
}
public void setSourceReceiptNumber(String sourceReceiptNumber) {
this.sourceReceiptNumber = sourceReceiptNumber;
}
}
The #JsonProperty annotation is imported from org.codehaus.jackson.annotate.JsonProperty; i.e from jackson jar
Now a Simple Test class to demo DTO/BO back and forth Entity conversion
public class Test{
public static void main(String[] args) throws JsonGenerationException, JsonMappingException, IOException {
List<Data> datas = new ArrayList<Data>();
datas.add(new Data("DDED-DBCD-REV-E1F4343DB3434","MeSo_TravelAds","123","TESTRCPT_1523"));
datas.add(new Data("ADED-EWQD-REV-E1F4343YG3434","FooSo_MusicAds","132","TESTRCPT_1523"));
datas.add(new Data("YDED-YUTR-REV-E1F43UIDB3434","BarSo_HealthAds","143","TESTRCPT_1523"));
List<DataTO> dataTOs = new ArrayList<DataTO>();
for (Data data : datas) {
DataTO dataTO = new DataTO();
dataTO.getReceiptTO().getMessageHeder().setSourceSystemId(data.getSource_system_guid());
dataTO.getReceiptTO().getMessageHeder().setSystemId(data.getSource_system());
dataTO.getReceiptTO().getReceiptHeder().setCustomerNumber(data.getCustomer_number());
dataTO.getReceiptTO().getReceiptHeder().setSourceReceiptNumber(data.getSource_receipt_number());
dataTOs.add(dataTO);
}
ObjectMapper mapper = new ObjectMapper();
String str = mapper.writeValueAsString(dataTOs);
System.out.println(str);
}
}
This will give you below result
[
{
"process_ar_receipt":{
"message_header":{
"source_System_Guid":"DDED-DBCD-REV-E1F4343DB3434",
"system_Id":"MeSo_TravelAds"
},
"receipt_header":{
"customer_number":"123",
"source_rcpt_number":"TESTRCPT_1523"
}
}
},
{
"process_ar_receipt":{
"message_header":{
"source_System_Guid":"ADED-EWQD-REV-E1F4343YG3434",
"system_Id":"FooSo_MusicAds"
},
"receipt_header":{
"customer_number":"132",
"source_rcpt_number":"TESTRCPT_1523"
}
}
},
{
"process_ar_receipt":{
"message_header":{
"source_System_Guid":"YDED-YUTR-REV-E1F43UIDB3434",
"system_Id":"BarSo_HealthAds"
},
"receipt_header":{
"customer_number":"143",
"source_rcpt_number":"TESTRCPT_1523"
}
}
}
]
similarly the other conversion
String input = "{ \r\n" +
" \"process_ar_receipt\":{ \r\n" +
" \"message_header\":{ \r\n" +
" \"source_System_Guid\":\"ADED-EWQD-REV-E1F4343YG3434\",\r\n" +
" \"system_Id\":\"FooSo_MusicAds\"\r\n" +
" },\r\n" +
" \"receipt_header\":{ \r\n" +
" \"customer_number\":\"132\",\r\n" +
" \"source_rcpt_number\":\"TESTRCPT_1523\"\r\n" +
" }\r\n" +
" }\r\n" +
" }";
DataTO dataTO = mapper.readValue(input, DataTO.class);
System.out.println(dataTO.getReceiptTO().getMessageHeder().getSourceSystemId());
System.out.println(dataTO.getReceiptTO().getMessageHeder().getSystemId());
System.out.println(dataTO.getReceiptTO().getReceiptHeder().getCustomerNumber());
System.out.println(dataTO.getReceiptTO().getReceiptHeder().getSourceReceiptNumber());
this will print
ADED-EWQD-REV-E1F4343YG3434
FooSo_MusicAds
132
TESTRCPT_1523
You dont have to use the mapper code you can directly add the jackson converter as HttpMessageConverted which will convert the JSON to java object automatically
#Configuration
#EnableWebMvc
public class WebConfiguration extends WebMvcConfigurerAdapter {
... other configurations
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
Jackson2ObjectMapperBuilder builder = new Jackson2ObjectMapperBuilder();
builder.serializationInclusion(JsonInclude.Include.NON_NULL);
builder.propertyNamingStrategy(PropertyNamingStrategy.CAMEL_CASE_TO_LOWER_CASE_WITH_UNDERSCORES);
builder.serializationInclusion(Include.NON_EMPTY);
builder.indentOutput(true).dateFormat(new SimpleDateFormat("yyyy-MM-dd"));
converters.add(new MappingJackson2HttpMessageConverter(builder.build()));
converters.add(new MappingJackson2XmlHttpMessageConverter(builder.createXmlMapper(true).build()));
}
}

#RefreshScope annotated Bean registered through BeanDefinitionRegistryPostProcessor not getting refreshed on Cloud Config changes

I've a BeanDefinitionRegistryPostProcessor class that registers beans dynamically. Sometimes, the beans being registered have the Spring Cloud annotation #RefreshScope.
However, when the cloud configuration Environment is changed, such beans are not being refreshed. Upon debugging, the appropriate application events are triggered, however, the dynamic beans don't get reinstantiated. Need some help around this. Below is my code:
TestDynaProps:
public class TestDynaProps {
private String prop;
private String value;
public String getProp() {
return prop;
}
public void setProp(String prop) {
this.prop = prop;
}
public String getValue() {
return value;
}
public void setValue(String value) {
this.value = value;
}
#Override
public String toString() {
StringBuilder builder = new StringBuilder();
builder.append("TestDynaProps [prop=").append(prop).append(", value=").append(value).append("]");
return builder.toString();
}
}
TestDynaPropConsumer:
#RefreshScope
public class TestDynaPropConsumer {
private TestDynaProps props;
public void setProps(TestDynaProps props) {
this.props = props;
}
#PostConstruct
public void init() {
System.out.println("Init props : " + props);
}
public String getVal() {
return props.getValue();
}
}
BeanDefinitionRegistryPostProcessor:
public class PropertyBasedDynamicBeanDefinitionRegistrar implements BeanDefinitionRegistryPostProcessor, EnvironmentAware {
private ConfigurableEnvironment environment;
private final Class<?> propertyConfigurationClass;
private final String propertyBeanNamePrefix;
private final String propertyKeysPropertyName;
private Class<?> propertyConsumerBean;
private String consumerBeanNamePrefix;
private List<String> dynaBeans;
public PropertyBasedDynamicBeanDefinitionRegistrar(Class<?> propertyConfigurationClass,
String propertyBeanNamePrefix, String propertyKeysPropertyName) {
this.propertyConfigurationClass = propertyConfigurationClass;
this.propertyBeanNamePrefix = propertyBeanNamePrefix;
this.propertyKeysPropertyName = propertyKeysPropertyName;
dynaBeans = new ArrayList<>();
}
public void setPropertyConsumerBean(Class<?> propertyConsumerBean, String consumerBeanNamePrefix) {
this.propertyConsumerBean = propertyConsumerBean;
this.consumerBeanNamePrefix = consumerBeanNamePrefix;
}
#Override
public void setEnvironment(Environment environment) {
this.environment = (ConfigurableEnvironment) environment;
}
#Override
public void postProcessBeanFactory(ConfigurableListableBeanFactory arg0) throws BeansException {
}
#Override
public void postProcessBeanDefinitionRegistry(BeanDefinitionRegistry beanDefRegistry) throws BeansException {
if (environment == null) {
throw new BeanCreationException("Environment must be set to initialize dyna bean");
}
String[] keys = getPropertyKeys();
Map<String, String> propertyKeyBeanNameMapping = new HashMap<>();
for (String k : keys) {
String trimmedKey = k.trim();
String propBeanName = getPropertyBeanName(trimmedKey);
registerPropertyBean(beanDefRegistry, trimmedKey, propBeanName);
propertyKeyBeanNameMapping.put(trimmedKey, propBeanName);
}
if (propertyConsumerBean != null) {
String beanPropertyFieldName = getConsumerBeanPropertyVariable();
for (Map.Entry<String, String> prop : propertyKeyBeanNameMapping.entrySet()) {
registerConsumerBean(beanDefRegistry, prop.getKey(), prop.getValue(), beanPropertyFieldName);
}
}
}
private void registerConsumerBean(BeanDefinitionRegistry beanDefRegistry, String trimmedKey, String propBeanName, String beanPropertyFieldName) {
String consumerBeanName = getConsumerBeanName(trimmedKey);
AbstractBeanDefinition consumerDefinition = preparePropertyConsumerBeanDefinition(propBeanName, beanPropertyFieldName);
beanDefRegistry.registerBeanDefinition(consumerBeanName, consumerDefinition);
dynaBeans.add(consumerBeanName);
}
private void registerPropertyBean(BeanDefinitionRegistry beanDefRegistry, String trimmedKey, String propBeanName) {
AbstractBeanDefinition propertyBeanDefinition = preparePropertyBeanDefinition(trimmedKey);
beanDefRegistry.registerBeanDefinition(propBeanName, propertyBeanDefinition);
dynaBeans.add(propBeanName);
}
private String getConsumerBeanPropertyVariable() throws IllegalArgumentException {
Field[] beanFields = propertyConsumerBean.getDeclaredFields();
for (Field bField : beanFields) {
if (bField.getType().equals(propertyConfigurationClass)) {
return bField.getName();
}
}
throw new BeanCreationException(String.format("Could not find property of type %s in bean class %s",
propertyConfigurationClass.getName(), propertyConsumerBean.getName()));
}
private AbstractBeanDefinition preparePropertyBeanDefinition(String trimmedKey) {
BeanDefinitionBuilder bdb = BeanDefinitionBuilder.genericBeanDefinition(PropertiesConfigurationFactory.class);
bdb.addConstructorArgValue(propertyConfigurationClass);
bdb.addPropertyValue("propertySources", environment.getPropertySources());
bdb.addPropertyValue("conversionService", environment.getConversionService());
bdb.addPropertyValue("targetName", trimmedKey);
return bdb.getBeanDefinition();
}
private AbstractBeanDefinition preparePropertyConsumerBeanDefinition(String propBeanName, String beanPropertyFieldName) {
BeanDefinitionBuilder bdb = BeanDefinitionBuilder.genericBeanDefinition(propertyConsumerBean);
bdb.addPropertyReference(beanPropertyFieldName, propBeanName);
return bdb.getBeanDefinition();
}
private String getPropertyBeanName(String trimmedKey) {
return propertyBeanNamePrefix + trimmedKey.substring(0, 1).toUpperCase() + trimmedKey.substring(1);
}
private String getConsumerBeanName(String trimmedKey) {
return consumerBeanNamePrefix + trimmedKey.substring(0, 1).toUpperCase() + trimmedKey.substring(1);
}
private String[] getPropertyKeys() {
String keysProp = environment.getProperty(propertyKeysPropertyName);
return keysProp.split(",");
}
The Config class:
#Configuration
public class DynaPropsConfig {
#Bean
public PropertyBasedDynamicBeanDefinitionRegistrar dynaRegistrar() {
PropertyBasedDynamicBeanDefinitionRegistrar registrar = new PropertyBasedDynamicBeanDefinitionRegistrar(TestDynaProps.class, "testDynaProp", "dyna.props");
registrar.setPropertyConsumerBean(TestDynaPropConsumer.class, "testDynaPropsConsumer");
return registrar;
}
}
Application.java
#SpringBootApplication
#EnableDiscoveryClient
#EnableScheduling
public class Application extends SpringBootServletInitializer {
private static Class<Application> applicationClass = Application.class;
public static void main(String[] args) {
SpringApplication sa = new SpringApplication(applicationClass);
sa.run(args);
}
}
And, my bootstrap.properties:
spring.cloud.consul.enabled=true
spring.cloud.consul.config.enabled=true
spring.cloud.consul.config.format=PROPERTIES
spring.cloud.consul.config.watch.delay=15000
spring.cloud.discovery.client.health-indicator.enabled=false
spring.cloud.discovery.client.composite-indicator.enabled=false
application.properties
dyna.props=d1,d2
d1.prop=d1prop
d1.value=d1value
d2.prop=d2prop
d2.value=d2value
Here are some guesses:
1) Perhaps the #RefreshScope metadata is not being passed to your metadata for the bean definition. Call setScope()?
2) The RefreshScope is actually implemented by https://github.com/spring-cloud/spring-cloud-commons/blob/master/spring-cloud-context/src/main/java/org/springframework/cloud/context/scope/refresh/RefreshScope.java, which itself implements BeanDefinitionRegistryPostProcessor. Perhaps the ordering of these two post processors is issue.
Just guesses.
We finally resolved this by appending the #RefreshScope annotation on the proposed dynamic bean classes using ByteBuddy and then, adding them to Spring Context using Bean Definition Post Processor.
The Post Processor is added to spring.factories so that it loads before any other dynamic bean dependent beans.

How to display a link on a particular rel as an array even if there is only one link

for (Person person : company.getPersons()) {
resource.add(linkTo(methodOn(PersonController.class).view(person.getId()))
.withRel("persons"));
}
I want to return an array of links by "persons" rel. It's all ok if I have multiple persons, but if I have only a single person it returns a single element and my client code that expects array fails.
not possible in spring hateoas 18. We overloaded the built in serializer to account for this. It was very nasty.
Technically a client should interpret rel : {} as rel : [{}] to be HAL compliant..but they rarely do..
you have to remove and override the built in HATEOAS one, we did it like this, but this effectively removes all other converters:
#Configuration
public class WebMVCConfig extends WebMvcConfigurerAdapter {
private static final String DELEGATING_REL_PROVIDER_BEAN_NAME = "_relProvider";
private static final String LINK_DISCOVERER_REGISTRY_BEAN_NAME = "_linkDiscovererRegistry";
private static final String HAL_OBJECT_MAPPER_BEAN_NAME = "_halObjectMapper";
public WebMVCConfig(){
}
#Autowired
private ListableBeanFactory beanFactory;
#Override
public void configureMessageConverters(List<HttpMessageConverter<?>> converters) {
//Need to override some behaviour in the HAL Serializer...so let's do that
CurieProvider curieProvider = getCurieProvider(beanFactory);
RelProvider relProvider = beanFactory.getBean(DELEGATING_REL_PROVIDER_BEAN_NAME, RelProvider.class);
ObjectMapper halObjectMapper = beanFactory.getBean(HAL_OBJECT_MAPPER_BEAN_NAME, ObjectMapper.class);
halObjectMapper.registerModule(new MultiLinkAwareJackson2HalModule());
halObjectMapper.setHandlerInstantiator(new MultiLinkAwareJackson2HalModule.MultiLinkAwareHalHandlerInstantiator(relProvider, curieProvider));
MappingJackson2HttpMessageConverter halConverter = new TypeConstrainedMappingJackson2HttpMessageConverter(ResourceSupport.class);
halConverter.setSupportedMediaTypes(Arrays.asList(HAL_JSON));
halConverter.setObjectMapper(halObjectMapper);
converters.add(halConverter);
}
private static CurieProvider getCurieProvider(BeanFactory factory) {
try {
return factory.getBean(CurieProvider.class);
} catch (NoSuchBeanDefinitionException e) {
return null;
}
}
overriding the serializer is really ugly business..maybe we should have just built a new one from scratch
/*
* Copyright 2012-2014 the original author or authors.
*
* Licensed under the Apache License, Version 2.0 (the "License");
* you may not use this file except in compliance with the License.
* You may obtain a copy of the License at
*
* http://www.apache.org/licenses/LICENSE-2.0
*
* Unless required by applicable law or agreed to in writing, software
* distributed under the License is distributed on an "AS IS" BASIS,
* WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
* See the License for the specific language governing permissions and
* limitations under the License.
*/
import com.fasterxml.jackson.annotation.JsonProperty;
import com.fasterxml.jackson.databind.annotation.JsonDeserialize;
import com.fasterxml.jackson.databind.annotation.JsonSerialize;
import com.fasterxml.jackson.databind.introspect.Annotated;
import com.fasterxml.jackson.databind.ser.std.MapSerializer;
import com.fasterxml.jackson.databind.type.TypeFactory;
import com.google.common.collect.ImmutableSet;
import org.springframework.hateoas.hal.*;
import java.io.IOException;
import java.util.*;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.Iterator;
import java.util.LinkedHashMap;
import java.util.List;
import java.util.Map;
import org.springframework.hateoas.Link;
import org.springframework.hateoas.Links;
import org.springframework.hateoas.RelProvider;
import org.springframework.hateoas.ResourceSupport;
import com.fasterxml.jackson.core.JsonGenerationException;
import com.fasterxml.jackson.core.JsonGenerator;
import com.fasterxml.jackson.databind.BeanProperty;
import com.fasterxml.jackson.databind.JavaType;
import com.fasterxml.jackson.databind.JsonMappingException;
import com.fasterxml.jackson.databind.JsonSerializer;
import com.fasterxml.jackson.databind.SerializationConfig;
import com.fasterxml.jackson.databind.SerializerProvider;
import javax.xml.bind.annotation.XmlElement;
/**
* Jackson 2 module implementation to render {#link org.springframework.hateoas.Link} and {#link org.springframework.hateoas.ResourceSupport} instances in HAL compatible JSON.
*
* Extends this class to make it possible for a relationship to be serialized as an array even if there is only 1 link
* This is done is in OptionalListJackson2Serializer::serialize method.
*
* Relationships to force as arrays are defined in relsToForceAsAnArray
*/
public class MultiLinkAwareJackson2HalModule extends Jackson2HalModule {
private static final long serialVersionUID = 7806951456457932384L;
private static final ImmutableSet<String> relsToForceAsAnArray = ImmutableSet.copyOf(Arrays.asList(
ContractConstants.REL_PROMOTION_TARGET,
ContractConstants.REL_PROFILE,
ContractConstants.REL_IMAGE_FLAG,
ContractConstants.REL_IMAGE,
ContractConstants.REL_IMAGE_PRIMARY,
ContractConstants.REL_IMAGE_SECONDARY,
ContractConstants.REL_IMAGE_MENU,
ContractConstants.REL_ITEM
));
private static abstract class MultiLinkAwareResourceSupportMixin extends ResourceSupport {
#Override
#XmlElement(name = "link")
#JsonProperty("_links")
//here's the only diff from org.springframework.hateoas.hal.ResourceSupportMixin
//we use a different HalLinkListSerializer
#JsonSerialize(include = JsonSerialize.Inclusion.NON_EMPTY, using = MultiLinkAwareHalLinkListSerializer.class)
#JsonDeserialize(using = MultiLinkAwareJackson2HalModule.HalLinkListDeserializer.class)
public abstract List<Link> getLinks();
}
public MultiLinkAwareJackson2HalModule() {
super();
//NOTE: super calls setMixInAnnotation(Link.class, LinkMixin.class);
//you must not override this as this is how Spring-HATEOAS determines if a
//Hal converter has been registered for not.
//If it determines a Hal converter has not been registered, it will register it's own
//that will override this one
//Use customized ResourceSupportMixin to use our LinkListSerializer
setMixInAnnotation(ResourceSupport.class, MultiLinkAwareResourceSupportMixin.class);
}
public static class MultiLinkAwareHalLinkListSerializer extends Jackson2HalModule.HalLinkListSerializer {
private final BeanProperty property;
private final CurieProvider curieProvider;
private final Set<String> relsAsMultilink;
public MultiLinkAwareHalLinkListSerializer(BeanProperty property, CurieProvider curieProvider, Set<String> relsAsMultilink) {
super(property, curieProvider);
this.property = property;
this.curieProvider = curieProvider;
this.relsAsMultilink = relsAsMultilink;
}
#Override
public void serialize(List<Link> value, JsonGenerator jgen, SerializerProvider provider) throws IOException,
JsonGenerationException {
// sort links according to their relation
Map<String, List<Object>> sortedLinks = new LinkedHashMap<String, List<Object>>();
List<Link> links = new ArrayList<Link>();
boolean prefixingRequired = curieProvider != null;
boolean curiedLinkPresent = false;
for (Link link : value) {
String rel = prefixingRequired ? curieProvider.getNamespacedRelFrom(link) : link.getRel();
if (!link.getRel().equals(rel)) {
curiedLinkPresent = true;
}
if (sortedLinks.get(rel) == null) {
sortedLinks.put(rel, new ArrayList<Object>());
}
links.add(link);
sortedLinks.get(rel).add(link);
}
if (prefixingRequired && curiedLinkPresent) {
ArrayList<Object> curies = new ArrayList<Object>();
curies.add(curieProvider.getCurieInformation(new Links(links)));
sortedLinks.put("curies", curies);
}
TypeFactory typeFactory = provider.getConfig().getTypeFactory();
JavaType keyType = typeFactory.uncheckedSimpleType(String.class);
JavaType valueType = typeFactory.constructCollectionType(ArrayList.class, Object.class);
JavaType mapType = typeFactory.constructMapType(HashMap.class, keyType, valueType);
//CHANGE HERE: only thing we are changing ins the List Serializer
//shame there's not a better way to override this very specific behaviour
//without copy pasta the whole class
MapSerializer serializer = MapSerializer.construct(new String[] {}, mapType, true, null,
provider.findKeySerializer(keyType, null), new MultiLinkAwareOptionalListJackson2Serializer(property, relsAsMultilink), null);
serializer.serialize(sortedLinks, jgen, provider);
}
public MultiLinkAwareHalLinkListSerializer withForcedRels(String[] relationships) {
ImmutableSet<String> relsToForce = ImmutableSet.<String>builder().addAll(this.relsAsMultilink).add(relationships).build();
return new MultiLinkAwareHalLinkListSerializer(this.property, this.curieProvider, relsToForce);
}
#Override
public JsonSerializer<?> createContextual(SerializerProvider provider, BeanProperty property)
throws JsonMappingException {
return new MultiLinkAwareHalLinkListSerializer(property, curieProvider, this.relsAsMultilink);
}
}
public static class MultiLinkAwareOptionalListJackson2Serializer extends Jackson2HalModule.OptionalListJackson2Serializer {
private final BeanProperty property;
private final Map<Class<?>, JsonSerializer<Object>> serializers;
private final Set<String> relsAsMultilink;
public MultiLinkAwareOptionalListJackson2Serializer(BeanProperty property, Set<String> relsAsMultilink) {
super(property);
this.property = property;
this.serializers = new HashMap<Class<?>, JsonSerializer<Object>>();
this.relsAsMultilink = relsAsMultilink;
}
#Override
public void serialize(Object value, JsonGenerator jgen, SerializerProvider provider) throws IOException,
JsonGenerationException {
List<?> list = (List<?>) value;
if (list.isEmpty()) {
return;
}
if(list.get(0) instanceof Link) {
Link link = (Link) list.get(0);
String rel = link.getRel();
if (list.size() > 1 || relsAsMultilink.contains(rel)) {
jgen.writeStartArray();
serializeContents(list.iterator(), jgen, provider);
jgen.writeEndArray();
} else {
serializeContents(list.iterator(), jgen, provider);
}
}
}
private void serializeContents(Iterator<?> value, JsonGenerator jgen, SerializerProvider provider)
throws IOException, JsonGenerationException {
while (value.hasNext()) {
Object elem = value.next();
if (elem == null) {
provider.defaultSerializeNull(jgen);
} else {
getOrLookupSerializerFor(elem.getClass(), provider).serialize(elem, jgen, provider);
}
}
}
private JsonSerializer<Object> getOrLookupSerializerFor(Class<?> type, SerializerProvider provider)
throws JsonMappingException {
JsonSerializer<Object> serializer = serializers.get(type);
if (serializer == null) {
serializer = provider.findValueSerializer(type, property);
serializers.put(type, serializer);
}
return serializer;
}
#Override
public JsonSerializer<?> createContextual(SerializerProvider provider, BeanProperty property)
throws JsonMappingException {
return new MultiLinkAwareOptionalListJackson2Serializer(property, relsAsMultilink);
}
}
public static class MultiLinkAwareHalHandlerInstantiator extends Jackson2HalModule.HalHandlerInstantiator {
private final MultiLinkAwareHalLinkListSerializer linkListSerializer;
public MultiLinkAwareHalHandlerInstantiator(RelProvider resolver, CurieProvider curieProvider) {
super(resolver, curieProvider, true);
this.linkListSerializer = new MultiLinkAwareHalLinkListSerializer(null, curieProvider, relsToForceAsAnArray);
}
#Override
public JsonSerializer<?> serializerInstance(SerializationConfig config, Annotated annotated, Class<?> serClass) {
if(serClass.equals(MultiLinkAwareHalLinkListSerializer.class)){
if (annotated.hasAnnotation(ForceMultiLink.class)) {
return this.linkListSerializer.withForcedRels(annotated.getAnnotation(ForceMultiLink.class).value());
} else {
return this.linkListSerializer;
}
} else {
return super.serializerInstance(config, annotated, serClass);
}
}
}
}
that ForceMultiLink stuff was an additional thing we ended up needing where on some resource classes a rel needed to be multi and on others it did not...so it looks like this:
#Target(ElementType.METHOD)
#Retention(RetentionPolicy.RUNTIME)
public #interface ForceMultiLink {
String[] value();
}
you use it to annotate the getLinks() method in your resource class
I have a workaround for this issue that is along similar lines to Chris' answer. The main difference is that I did not extend Jackson2HalModule, but created a new handler-instantiator and set it as the handler-instantiator for a new instance of Jackson2HalModule that I create myself. I hope Spring HATEOAS will eventually support this functionality natively; I have a pull request that attempts to do this. Here's how I implemented my workaround:
Step 1: Create the mixin class:
public abstract class HalLinkListMixin {
#JsonProperty("_links") #JsonSerialize(using = HalLinkListSerializer.class)
public abstract List<Link> getLinks();
}
This mixin class will associate the HalLinkListSerializer (shown later) serializer with the links property.
Step 2: Create a container class that holds the rels whose link representations should always be an array of link objects:
public class HalMultipleLinkRels {
private final Set<String> rels;
public HalMultipleLinkRels(String... rels) {
this.rels = new HashSet<String>(Arrays.asList(rels));
}
public Set<String> getRels() {
return Collections.unmodifiableSet(rels);
}
}
Step 3: Create our new serializer that will override Spring HATEOAS's link-list serializer:
public class HalLinkListSerializer extends ContainerSerializer<List<Link>> implements ContextualSerializer {
private final BeanProperty property;
private CurieProvider curieProvider;
private HalMultipleLinkRels halMultipleLinkRels;
public HalLinkListSerializer() {
this(null, null, new HalMultipleLinkRels());
}
public HalLinkListSerializer(CurieProvider curieProvider, HalMultipleLinkRels halMultipleLinkRels) {
this(null, curieProvider, halMultipleLinkRels);
}
public HalLinkListSerializer(BeanProperty property, CurieProvider curieProvider, HalMultipleLinkRels halMultipleLinkRels) {
super(List.class, false);
this.property = property;
this.curieProvider = curieProvider;
this.halMultipleLinkRels = halMultipleLinkRels;
}
#Override
public void serialize(List<Link> value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonGenerationException {
// sort links according to their relation
Map<String, List<Object>> sortedLinks = new LinkedHashMap<>();
List<Link> links = new ArrayList<>();
boolean prefixingRequired = curieProvider != null;
boolean curiedLinkPresent = false;
for (Link link : value) {
String rel = prefixingRequired ? curieProvider.getNamespacedRelFrom(link) : link.getRel();
if (!link.getRel().equals(rel)) {
curiedLinkPresent = true;
}
if (sortedLinks.get(rel) == null) {
sortedLinks.put(rel, new ArrayList<>());
}
links.add(link);
sortedLinks.get(rel).add(link);
}
if (prefixingRequired && curiedLinkPresent) {
ArrayList<Object> curies = new ArrayList<>();
curies.add(curieProvider.getCurieInformation(new Links(links)));
sortedLinks.put("curies", curies);
}
TypeFactory typeFactory = provider.getConfig().getTypeFactory();
JavaType keyType = typeFactory.uncheckedSimpleType(String.class);
JavaType valueType = typeFactory.constructCollectionType(ArrayList.class, Object.class);
JavaType mapType = typeFactory.constructMapType(HashMap.class, keyType, valueType);
MapSerializer serializer = MapSerializer.construct(new String[]{}, mapType, true, null,
provider.findKeySerializer(keyType, null), new ListJackson2Serializer(property, halMultipleLinkRels), null);
serializer.serialize(sortedLinks, jgen, provider);
}
#Override
public JavaType getContentType() {
return null;
}
#Override
public JsonSerializer<?> getContentSerializer() {
return null;
}
#Override
public boolean hasSingleElement(List<Link> value) {
return value.size() == 1;
}
#Override
protected ContainerSerializer<?> _withValueTypeSerializer(TypeSerializer vts) {
return null;
}
#Override
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException {
return new HalLinkListSerializer(property, curieProvider, halMultipleLinkRels);
}
private static class ListJackson2Serializer extends ContainerSerializer<Object> implements ContextualSerializer {
private final BeanProperty property;
private final Map<Class<?>, JsonSerializer<Object>> serializers = new HashMap<>();
private final HalMultipleLinkRels halMultipleLinkRels;
public ListJackson2Serializer() {
this(null, null);
}
public ListJackson2Serializer(BeanProperty property, HalMultipleLinkRels halMultipleLinkRels) {
super(List.class, false);
this.property = property;
this.halMultipleLinkRels = halMultipleLinkRels;
}
#Override
public void serialize(Object value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonGenerationException {
List<?> list = (List<?>) value;
if (list.isEmpty()) {
return;
}
if (list.size() == 1) {
Object element = list.get(0);
if (element instanceof Link) {
Link link = (Link) element;
if (halMultipleLinkRels.getRels().contains(link.getRel())) {
jgen.writeStartArray();
serializeContents(list.iterator(), jgen, provider);
jgen.writeEndArray();
return;
}
}
serializeContents(list.iterator(), jgen, provider);
return;
}
jgen.writeStartArray();
serializeContents(list.iterator(), jgen, provider);
jgen.writeEndArray();
}
#Override
public JavaType getContentType() {
return null;
}
#Override
public JsonSerializer<?> getContentSerializer() {
return null;
}
#Override
public boolean hasSingleElement(Object value) {
return false;
}
#Override
protected ContainerSerializer<?> _withValueTypeSerializer(TypeSerializer vts) {
throw new UnsupportedOperationException("not implemented");
}
#Override
public JsonSerializer<?> createContextual(SerializerProvider prov, BeanProperty property) throws JsonMappingException {
return new ListJackson2Serializer(property, halMultipleLinkRels);
}
private void serializeContents(Iterator<?> value, JsonGenerator jgen, SerializerProvider provider) throws IOException, JsonGenerationException {
while (value.hasNext()) {
Object elem = value.next();
if (elem == null) {
provider.defaultSerializeNull(jgen);
} else {
getOrLookupSerializerFor(elem.getClass(), provider).serialize(elem, jgen, provider);
}
}
}
private JsonSerializer<Object> getOrLookupSerializerFor(Class<?> type, SerializerProvider provider) throws JsonMappingException {
JsonSerializer<Object> serializer = serializers.get(type);
if (serializer == null) {
serializer = provider.findValueSerializer(type, property);
serializers.put(type, serializer);
}
return serializer;
}
}
}
This class unfortunately duplicates logic, but it's not too bad. The key difference is that instead of using OptionalListJackson2Serializer, I'm using ListJackson2Serializer, which will force a rel's link representation as an array, if that rel exists in the container of rel overrides (HalMultipleLinkRels):
Step 4: Create a custom handler-instantiator:
public class HalHandlerInstantiator extends HandlerInstantiator {
private final Jackson2HalModule.HalHandlerInstantiator halHandlerInstantiator;
private final Map<Class<?>, JsonSerializer<?>> serializerMap = new HashMap<>();
public HalHandlerInstantiator(RelProvider relProvider, CurieProvider curieProvider, HalMultipleLinkRels halMultipleLinkRels) {
this(relProvider, curieProvider, halMultipleLinkRels, true);
}
public HalHandlerInstantiator(RelProvider relProvider, CurieProvider curieProvider, HalMultipleLinkRels halMultipleLinkRels, boolean enforceEmbeddedCollections) {
halHandlerInstantiator = new Jackson2HalModule.HalHandlerInstantiator(relProvider, curieProvider, enforceEmbeddedCollections);
serializerMap.put(HalLinkListSerializer.class, new HalLinkListSerializer(curieProvider, halMultipleLinkRels));
}
#Override
public JsonDeserializer<?> deserializerInstance(DeserializationConfig config, Annotated annotated, Class<?> deserClass) {
return halHandlerInstantiator.deserializerInstance(config, annotated, deserClass);
}
#Override
public KeyDeserializer keyDeserializerInstance(DeserializationConfig config, Annotated annotated, Class<?> keyDeserClass) {
return halHandlerInstantiator.keyDeserializerInstance(config, annotated, keyDeserClass);
}
#Override
public JsonSerializer<?> serializerInstance(SerializationConfig config, Annotated annotated, Class<?> serClass) {
if(serializerMap.containsKey(serClass)) {
return serializerMap.get(serClass);
} else {
return halHandlerInstantiator.serializerInstance(config, annotated, serClass);
}
}
#Override
public TypeResolverBuilder<?> typeResolverBuilderInstance(MapperConfig<?> config, Annotated annotated, Class<?> builderClass) {
return halHandlerInstantiator.typeResolverBuilderInstance(config, annotated, builderClass);
}
#Override
public TypeIdResolver typeIdResolverInstance(MapperConfig<?> config, Annotated annotated, Class<?> resolverClass) {
return halHandlerInstantiator.typeIdResolverInstance(config, annotated, resolverClass);
}
}
This instantiator will control the lifecycle for our custom serializer. It maintains an internal instance of Jackson2HalModule.HalHandlerInstantiator, and delegates to that instance for all other serializers.
Step 5: Put it all together:
#Configuration
public class ApplicationConfiguration {
private static final String HAL_OBJECT_MAPPER_BEAN_NAME = "_halObjectMapper";
private static final String DELEGATING_REL_PROVIDER_BEAN_NAME = "_relProvider";
#Autowired
private BeanFactory beanFactory;
private static CurieProvider getCurieProvider(BeanFactory factory) {
try {
return factory.getBean(CurieProvider.class);
} catch (NoSuchBeanDefinitionException e) {
return null;
}
}
#Bean
public ObjectMapper objectMapper() {
CurieProvider curieProvider = getCurieProvider(beanFactory);
RelProvider relProvider = beanFactory.getBean(DELEGATING_REL_PROVIDER_BEAN_NAME, RelProvider.class);
ObjectMapper halObjectMapper = beanFactory.getBean(HAL_OBJECT_MAPPER_BEAN_NAME, ObjectMapper.class);
//Create a new instance of Jackson2HalModule
SimpleModule module = new Jackson2HalModule();
//Provide the mix-in class so that we can override the serializer for links with our custom serializer
module.setMixInAnnotation(ResourceSupport.class, HalLinkListMixin.class);
//Register the module in the object mapper
halObjectMapper.registerModule(module);
//Set the handler instantiator on the mapper to our custom handler-instantiator
halObjectMapper.setHandlerInstantiator(new HalHandlerInstantiator(relProvider, curieProvider, halMultipleLinkRels()));
return halObjectMapper;
}
...
}
Don't forget the "self" resource link required by HAL.
In that case, that's no so common to have only one link.

Resources