How to replace implementation of javax.validation.Valid annotation in Spring? - spring

Suppose I don't wanna use JSR 303 to validate my beans. Is it possible to implement custom validator which will be used by Spring when bean is marked as #Valid?
It would be great if my custom validator will be a Spring Component

#fedor.belov
I detailed this problem in another question. In my needs I want to keep JSR-303 and make custom validator works, but you can change my example code to fit your needs.
This problem can be solved extending the LocalValidatorFactoryBean, you can override the validate method inside this class giving any behavior that you want.
In my case I need to use JSR-303 AND custom validators for same model in different methods in same Controller, normally is recommended to use #InitBinder, but it is not sufficient for my case because InitBinder make a bind between Model and Validator (if you use #RequestBody InitBinder is just for one model and one validator per Controller).
Controller
#RestController
public class LoginController {
#PostMapping("/test")
public Test test(#Validated(TestValidator.class) #RequestBody Test test) {
return test;
}
#PostMapping("/test2")
public Test test2(#Validated #RequestBody Test test) {
return test;
}
}
Custom Validator
public class TestValidator implements org.springframework.validation.Validator {
#Override
public boolean supports(Class<?> clazz) {
return Test.class.isAssignableFrom(clazz);
}
#Override
public void validate(Object target, Errors errors) {
Test test = (Test) target;
errors.rejectValue("field3", "weird");
System.out.println(test.getField1());
System.out.println(test.getField2());
System.out.println(test.getField3());
}
}
Class to be validate
public class Test {
#Size(min = 3)
private String field2;
#NotNull
#NotEmpty
private String field1;
#NotNull
#Past
private LocalDateTime field3;
//...
//getter/setter
//...
}
CustomLocalValidatorFactoryBean
public class CustomLocalValidatorFactoryBean extends LocalValidatorFactoryBean {
private Logger logger = LoggerFactory.getLogger(this.getClass());
#Override
public void validate(#Nullable Object target, Errors errors, #Nullable Object... validationHints) {
Set<Validator> concreteValidators = new LinkedHashSet<>();
Set<Class<?>> interfaceGroups = new LinkedHashSet<>();
extractConcreteValidatorsAndInterfaceGroups(concreteValidators, interfaceGroups, validationHints);
proccessConcreteValidators(target, errors, concreteValidators);
processConstraintViolations(super.validate(target, interfaceGroups.toArray(new Class<?>[interfaceGroups.size()])), errors);
}
private void proccessConcreteValidators(Object target, Errors errors, Set<Validator> concreteValidators) {
for (Validator validator : concreteValidators) {
validator.validate(target, errors);
}
}
private void extractConcreteValidatorsAndInterfaceGroups(Set<Validator> concreteValidators, Set<Class<?>> groups, Object... validationHints) {
if (validationHints != null) {
for (Object hint : validationHints) {
if (hint instanceof Class) {
if (((Class<?>) hint).isInterface()) {
groups.add((Class<?>) hint);
} else {
Optional<Validator> validatorOptional = getValidatorFromGenericClass(hint);
if (validatorOptional.isPresent()) {
concreteValidators.add(validatorOptional.get());
}
}
}
}
}
}
#SuppressWarnings("unchecked")
private Optional<Validator> getValidatorFromGenericClass(Object hint) {
try {
Class<Validator> clazz = (Class<Validator>) Class.forName(((Class<?>) hint).getName());
return Optional.of(clazz.newInstance());
} catch (ClassNotFoundException | InstantiationException | IllegalAccessException e) {
logger.info("There is a problem with the class that you passed to "
+ " #Validated annotation in the controller, we tried to "
+ " cast to org.springframework.validation.Validator and we cant do this");
}
return Optional.empty();
}
}
Configure application
#SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
#Bean
public javax.validation.Validator localValidatorFactoryBean() {
return new CustomLocalValidatorFactoryBean();
}
}
Input to /test endpoint:
{
"field1": "",
"field2": "aaaa",
"field3": "2018-04-15T15:10:24"
}
Output from /test endpoint:
{
"timestamp": "2018-04-16T17:34:28.532+0000",
"status": 400,
"error": "Bad Request",
"errors": [
{
"codes": [
"weird.test.field3",
"weird.field3",
"weird.java.time.LocalDateTime",
"weird"
],
"arguments": null,
"defaultMessage": null,
"objectName": "test",
"field": "field3",
"rejectedValue": "2018-04-15T15:10:24",
"bindingFailure": false,
"code": "weird"
},
{
"codes": [
"NotEmpty.test.field1",
"NotEmpty.field1",
"NotEmpty.java.lang.String",
"NotEmpty"
],
"arguments": [
{
"codes": [
"test.field1",
"field1"
],
"arguments": null,
"defaultMessage": "field1",
"code": "field1"
}
],
"defaultMessage": "Não pode estar vazio",
"objectName": "test",
"field": "field1",
"rejectedValue": "",
"bindingFailure": false,
"code": "NotEmpty"
}
],
"message": "Validation failed for object='test'. Error count: 2",
"path": "/user/test"
}
Input to /test2 endpoint:
{
"field1": "",
"field2": "aaaa",
"field3": "2018-04-15T15:10:24"
}
Output to /test2 endpoint:
{
"timestamp": "2018-04-16T17:37:30.889+0000",
"status": 400,
"error": "Bad Request",
"errors": [
{
"codes": [
"NotEmpty.test.field1",
"NotEmpty.field1",
"NotEmpty.java.lang.String",
"NotEmpty"
],
"arguments": [
{
"codes": [
"test.field1",
"field1"
],
"arguments": null,
"defaultMessage": "field1",
"code": "field1"
}
],
"defaultMessage": "Não pode estar vazio",
"objectName": "test",
"field": "field1",
"rejectedValue": "",
"bindingFailure": false,
"code": "NotEmpty"
}
],
"message": "Validation failed for object='test'. Error count: 1",
"path": "/user/test2"
}
Original answer with original question.
I hope this help.

You will have to implement Validator interface and mark the implementation as #Component. Then you can bind your custom validator using #InitBinder annotation. Something like this:
#Component
public class MyCustomValidator implements Validator
{
#Override
public boolean supports(Class<?> clazz)
{
// your custom logic
}
#Override
public void validate(Object target, Errors errors)
{
// your custom logic
}
}
To bind it with annotation:
#Controller
public class MyController
{
#Autowired
private MyCustomValidator validator;
#InitBinder
protected void initBinder(final WebDataBinder binder)
{
binder.addValidators(validator);
}
}

Related

with #SpringBootTest (or #WebfluxTest), the errors attribute disappears

I made very simple controller like below.
#PostMapping("/books")
public void create(#Valid #RequestBody BookPayload bookPayload) {
}
#Getter
#Setter
public class BookPayload {
#NotBlank
private String name;
#NotBlank
private String author;
}
When I call this api without name. It responses like below.
{
"timestamp": "2022-03-26T14:06:43.564+00:00",
"path": "/books",
"status": 400,
"error": "Bad Request",
"requestId": "654248ee-5",
"errors": [
{
"codes": [
"NotBlank.bookPayload.name",
"NotBlank.name",
"NotBlank.java.lang.String",
"NotBlank"
],
"arguments": [
{
"codes": [
"bookPayload.name",
"name"
],
"arguments": null,
"defaultMessage": "name",
"code": "name"
}
],
... omit ...
}
]
}
You can see errors attribute in the response body.
But If I test this api with #SpringBootTest or #WebfluxTest, There is no errors attribute.
#Slf4j
#SpringBootTest(webEnvironment = SpringBootTest.WebEnvironment.RANDOM_PORT)
public class CommonErrorResponseTest {
private final WebClient web;
public CommonErrorResponseTest(#LocalServerPort Integer port) {
web = WebClient.create("http://localhost:" + port);
}
#Test
void _400_badRequest_violation() {
BookPayload bookPayload = new BookPayload();
bookPayload.setAuthor("John");
Mono<String> stringMono = web.post().uri("/books")
.header("Content-Type", MediaType.APPLICATION_JSON_VALUE)
.bodyValue(bookPayload)
.exchangeToMono(response -> response.bodyToMono(String.class));
String body = stringMono.block();
log.info("body: {}", body);
}
}
console
body: {"timestamp":"2022-03-26T14:19:21.981+00:00","path":"/books","status":400,"error":"Bad Request","requestId":"68df2a79-1"}
I'd like to know why I'm getting different results.
Spring Boot’s DevTools enables the inclusion of binding errors in the error response to ease problem solving during development. You can configure the same behaviour in your tests by setting server.error.include-binding-errors to always.
You can see a complete list of the properties that DevTools sets in the reference documentation.

Avro class cannot be cast to class org.springframework.messaging.Message

I am consuming Avro data coming from Debezium
I made the kafka consumer as follows:
The Java POJO
import lombok.Data;
#Data
public class Shop {
Long shopId;
Double latitude, longitude;
String name;
String phoneNumber;
String placeId;
double rating;
String website;
int addressId;
String closingHours;
String email;
int maxAttendance;
String opening_hours;
String businessHours;
String closeDay;
String description;
boolean open;
String setWeekendBusinessHours;
Long userShopId;
}
Avro Message Format
{
"type": "record",
"name": "ShopMessage",
"namespace": "com.example.kafka.avro",
"fields": [
{
"name": "SHOP_ID",
"type": [
"null",
"long"
],
"default": null
},
{
"name": "LATITUDE",
"type": [
"null",
"double"
],
"default": null
},
{
"name": "LONGITUDE",
"type": [
"null",
"double"
],
"default": null
},
{
"name": "NAME",
"type": [
"null",
"string"
],
"default": null
},
{
"name": "PHONENUMBER",
"type": [
"null",
"string"
],
"default": null
},
{
"name": "PLACEID",
"type": [
"null",
"string"
],
"default": null
},
{
"name": "RATING",
"type": [
"null",
"double"
],
"default": null
},
{
"name": "WEBSITE",
"type": [
"null",
"string"
],
"default": null
},
{
"name": "ADDRESSID",
"type": [
"null",
"int"
],
"default": null
},
{
"name": "CLOSINGHOUR",
"type": [
"null",
"string"
],
"default": null
},
{
"name": "EMAIL",
"type": [
"null",
"string"
],
"default": null
},
{
"name": "MAXATTENDANCE",
"type": [
"null",
"int"
],
"default": null
},
{
"name": "OPENINGHOURS",
"type": [
"null",
"string"
],
"default": null
},
{
"name": "BUSINESSHOURS",
"type": [
"null",
"string"
],
"default": null
},
{
"name": "CLOSEDAY",
"type": [
"null",
"string"
],
"default": null
},
{
"name": "DESCRIPTION",
"type": [
"null",
"string"
],
"default": null
},
{
"name": "ISOPEN",
"type": [
"null",
"boolean"
],
"default": null
},
{
"name": "WEEKENDBUSINESSHOURS",
"type": [
"null",
"string"
],
"default": null
},
{
"name": "USERSHOPID",
"type": [
"null",
"long"
],
"default": null
}
]
}
ShopConsumer
#Component
public class ShopConsumer {
private final ShopMapper shopMapper;
private final Logger log = LogManager.getLogger(ShopConsumer.class);
public ShopConsumer(ShopMapper shopMapper) {
this.shopMapper = shopMapper;
}
#KafkaListener(
groupId = "${spring.kafka.consumer.group-id}",
topics = "${spring.kafka.consumer.topic}"
)
public void listen(List<Message<ShopMessage>> messages, Acknowledgment ack){
log.info("Received batch of messages with size: {}", messages.size());
List<Shop> shops = messages.stream()
.peek(this::logMessageReceived)
.map(message -> shopMapper.toChange(message.getPayload()))
.collect(Collectors.toList());
//do remove redis cache
ack.acknowledge();
}
private void logMessageReceived(Message<ShopMessage> message) {
log.info("Received shopId {} with a name of '{} and place id {}', partition={}, offset={}",
message.getPayload().getSHOPID(),
message.getPayload().getNAME(),
message.getPayload().getPLACEID(),
message.getHeaders().get(KafkaHeaders.RECEIVED_PARTITION_ID),
message.getHeaders().get(KafkaHeaders.OFFSET));
}
Consumer Config - ShopConsumerConfig.java
#EnableKafka
#Configuration
public class ShopsConsumerConfig {
private final KafkaProperties kafkaProperties;
public ShopsConsumerConfig(KafkaProperties kafkaProperties) {
this.kafkaProperties = kafkaProperties;
}
#Bean
public ConcurrentKafkaListenerContainerFactory<String, ShopMessage> kafkaListenerContainerFactory() {
ConcurrentKafkaListenerContainerFactory<String, ShopMessage> factory = new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(consumerFactory());
factory.setBatchListener(false);
factory.setConcurrency(kafkaProperties.getListener().getConcurrency());
factory.getContainerProperties().setAckMode(kafkaProperties.getListener().getAckMode());
return factory;
}
#Bean
public ConsumerFactory<String, ShopMessage> consumerFactory() {
return new DefaultKafkaConsumerFactory<>(consumerConfigs());
}
#Bean
public Map<String, Object> consumerConfigs() {
Map<String, Object> props = kafkaProperties.buildConsumerProperties();
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, StringDeserializer.class);
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, SpecificAvroWithSchemaDeserializer.class);
props.put(AbstractKafkaSchemaSerDeConfig.SCHEMA_REGISTRY_URL_CONFIG, kafkaProperties.getProperties().get("schema-registry-url"));
props.put(KafkaAvroDeserializerConfig.SPECIFIC_AVRO_READER_CONFIG, true);
props.put(SpecificAvroWithSchemaDeserializer.AVRO_VALUE_RECORD_TYPE, ShopMessage.class);
return props;
}
}
Schema Deserializer
public class SpecificAvroWithSchemaDeserializer extends AbstractKafkaAvroDeserializer implements Deserializer<Object> {
public static final String AVRO_KEY_RECORD_TYPE = "avro.key.record.type";
public static final String AVRO_VALUE_RECORD_TYPE = "avro.value.record.type";
private Schema readerSchema;
public SpecificAvroWithSchemaDeserializer() { }
#Override
public void configure(Map<String, ?> configs, boolean isKey) {
this.configure(new KafkaAvroDeserializerConfig(configs));
readerSchema = getSchema(getRecordClass(configs, isKey));
}
private Class<?> getRecordClass(Map<String, ?> configs, boolean isKey) {
String configsKey = isKey ? AVRO_KEY_RECORD_TYPE : AVRO_VALUE_RECORD_TYPE;
Object configsValue = configs.get(configsKey);
if (configsValue instanceof Class) {
return (Class<?>) configsValue;
} else if (configsValue instanceof String) {
String recordClassName = (String) configsValue;
try {
return Class.forName(recordClassName);
} catch (ClassNotFoundException e) {
throw new IllegalArgumentException(String.format("Unable to find the class '%s'", recordClassName));
}
} else {
throw new IllegalArgumentException(
String.format("A class or a string must be informed into ConsumerConfig properties: '%s' and/or '%s'",
AVRO_KEY_RECORD_TYPE, AVRO_VALUE_RECORD_TYPE));
}
}
private Schema getSchema(Class<?> targetType) {
try {
Field field = targetType.getDeclaredField("SCHEMA$");
return (Schema) field.get(null);
} catch (NoSuchFieldException | IllegalAccessException e) {
throw new IllegalArgumentException(
String.format("Unable to get Avro Schema from the class '%s'", targetType.getName()), e);
}
}
#Override
public Object deserialize(String topic, byte[] bytes) {
return super.deserialize(bytes, readerSchema);
}
#Override
public void close() {
}
}
Mapper Class
#Mapper(componentModel = "spring")
public interface ShopMapper {
default Shop toChange(ShopMessage shopMessage){
if(shopMessage == null){
return null;
}
Shop shop = new Shop();
shop.setDescription(shopMessage.getDESCRIPTION().toString());
shop.setMaxAttendance(shopMessage.getMAXATTENDANCE());
shop.setSetWeekendBusinessHours(shopMessage.getWEEKENDBUSINESSHOURS().toString());
shop.setOpen(shopMessage.getISOPEN());
shop.setWebsite(shopMessage.getWEBSITE().toString());
shop.setRating(shopMessage.getRATING());
shop.setLatitude(shopMessage.getLATITUDE());
shop.setLongitude(shopMessage.getLONGITUDE());
shop.setCloseDay(shopMessage.getCLOSEDAY().toString());
shop.setBusinessHours(shopMessage.getBUSINESSHOURS().toString());
shop.setPhoneNumber(shopMessage.getPHONENUMBER().toString());
shop.setEmail(shopMessage.getEMAIL().toString());
shop.setPlaceId(shopMessage.getPLACEID().toString());
return shop;
}
}
Configuration is present on the application.properties file but during message consumption, Spring throws me an error of
Caused by: java.lang.ClassCastException: class com.example.kafka.avro.ShopMessage cannot be cast to class org.springframework.messaging.Message (com.example.kafka.avro.ShopMessage and org.springframework.messaging.Message are in unnamed module of loader 'app')
Could someone give me a correct direction to fix this issue, please? Looks like casting from POJO from Avro is having the issue but I am not able to find the root.
Thanks in advance.
Update
After few attempt, it looks that the issue on the above error is due to casting from a single message to list of messages. I changed the listener function as below.
public void listen(ConsumerRecord<Integer ,?> messages, Acknowledgment ack){
//log.info("Received batch of messages with size: {}", messages.size());
log.info(messages.key());
log.info(messages.value());
ack.acknowledge();
}
and getting a value from Kafka topic.
{"before": {"id": 6, "latitude": 2.921318, "longitude": 101.655938, "name": "XYZ", "phone_number": "+12345678", "place_id": "P007", "rating": 5.0, "type": "Food", "website": "https://xyz.me", "address_id": 5, "closing_hours": null, "email": "info#xyz.me", "max_attendance": 11, "opening_hours": null, "business_hours": "09-18", "close_day": "Saturday", "description": "Some Dummy", "is_open": true, "weekend_business_hours": "08-12", "user_shop_id": 0}, "after": {"id": 6, "latitude": 2.921318, "longitude": 101.655938, "name": "XYZ - edited", "phone_number": "+12345678", "place_id": "P007", "rating": 5.0, "type": "Food 2", "website": "https://xyz.me", "address_id": 5, "closing_hours": null, "email": "info#xyz.me", "max_attendance": 11, "opening_hours": null, "business_hours": "09-18", "close_day": "Saturday", "description": "Some dummy", "is_open": true, "weekend_business_hours": "08-12", "user_shop_id": 0}, "source": {"version": "1.6.0.Final", "connector": "mysql", "name": "bookingdev_sqip_local", "ts_ms": 1629267837000, "snapshot": "false", "db": "booking", "sequence": null, "table": "shop", "server_id": 1, "gtid": null, "file": "mysql-bin.000044", "pos": 26432, "row": 0, "thread": null, "query": null}, "op": "u", "ts_ms": 1629267836453, "transaction": null}
apart of that, I also removed the custom deserializer and custom POJO as schema is already installed on the schema registry.
Now the question remains, how do I obtain debezium's schema generated from schema-registry and convert the message to the correct Java POJO to be executed further?
Update 19.08.2021
After discussion with #OneCricketeer, I made adjustment on the logic for Consumer as below
public void listen(ConsumerRecord<Integer, GenericRecord> messages, Acknowledgment ack) throws JsonProcessingException {
log.info(messages.key());
log.info(messages.value());
Shop shop = new ObjectMapper().readValue(messages.value().get("after").toString(), Shop.class);
log.info("NEW VALUE #####-> " + shop.getName());
//other logic here.
ack.acknowledge();
}
But I got another error:
java.lang.IllegalStateException: This error handler cannot process 'SerializationException's directly; please consider configuring an 'ErrorHandlingDeserializer' in the value and/or key deserializer
at org.springframework.kafka.listener.SeekUtils.seekOrRecover(SeekUtils.java:194) ~[spring-kafka-2.7.0.jar:2.7.0]
at org.springframework.kafka.listener.SeekToCurrentErrorHandler.handle(SeekToCurrentErrorHandler.java:112) ~[spring-kafka-2.7.0.jar:2.7.0]
at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.handleConsumerException(KafkaMessageListenerContainer.java:1598) ~[spring-kafka-2.7.0.jar:2.7.0]
at org.springframework.kafka.listener.KafkaMessageListenerContainer$ListenerConsumer.run(KafkaMessageListenerContainer.java:1210) ~[spring-kafka-2.7.0.jar:2.7.0]
at java.base/java.util.concurrent.Executors$RunnableAdapter.call(Executors.java:515) ~[na:na]
at java.base/java.util.concurrent.FutureTask.run(FutureTask.java:264) ~[na:na]
at java.base/java.lang.Thread.run(Thread.java:834) ~[na:na]
Caused by: org.apache.kafka.common.errors.SerializationException: Error deserializing key/value for partition bookingdev_sqip_local.booking.shop-0 at offset 0. If needed, please seek past the record to continue consumption.
Caused by: org.apache.kafka.common.errors.SerializationException: Could not find class bookingdev_sqip_local.booking.shop.Key specified in writer's schema whilst finding reader's schema for a SpecificRecord.
Checked the Schema-Registry Debezium created two subjects - one for key and one for value.
["bookingdev_sqip_local.booking.shop-value","bookingdev_sqip_local.booking.shop-key"]
Looks like the error due to unable to map the schema for the key.
The exception is because your Kafka listener method should receive List<ShopMessage> instead of List<Message<ShopMessage>>.
Try to change this line:
public void listen(List<Message<ShopMessage>> messages, Acknowledgment ack){
To:
public void listen(List<ShopMessage> messages, Acknowledgment ack){
Also shopMapper.toChange(message.getPayload()) to shopMapper.toChange(message)
Ok after struggling with this Spring Boot <-> Kafka Connector <-> Debezium CDC MySQL. I got a working application.
The architecture:
MySQL(Producer) <-> Debezium CDC Kafka Connect <-> Kafka <-> SpringBoot (Consumer)
I am using Schema-Registry to store the schema configuration.
ShopConsumerConfig.java
#EnableKafka
#Configuration
public class ShopsConsumerConfig {
private final KafkaProperties kafkaProperties;
public ShopsConsumerConfig(KafkaProperties kafkaProperties) {
this.kafkaProperties = kafkaProperties;
}
#Bean
public ConcurrentKafkaListenerContainerFactory<String, Shop> kafkaListenerContainerFactory() {
ConcurrentKafkaListenerContainerFactory<String, Shop> factory = new ConcurrentKafkaListenerContainerFactory<>();
factory.setConsumerFactory(consumerFactory());
factory.setBatchListener(false);
factory.setConcurrency(kafkaProperties.getListener().getConcurrency());
factory.getContainerProperties().setAckMode(kafkaProperties.getListener().getAckMode());
return factory;
}
#Bean
public ConsumerFactory<String, Shop> consumerFactory() {
return new DefaultKafkaConsumerFactory<>(consumerConfigs());
}
#Bean
public Map<String, Object> consumerConfigs() {
Map<String, Object> props = kafkaProperties.buildConsumerProperties();
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, io.confluent.kafka.serializers.KafkaAvroDeserializer.class);
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, io.confluent.kafka.serializers.KafkaAvroDeserializer.class);
props.put(AbstractKafkaSchemaSerDeConfig.SCHEMA_REGISTRY_URL_CONFIG, kafkaProperties.getProperties().get("schema-registry-url"));
props.put(KafkaAvroDeserializerConfig.SPECIFIC_AVRO_READER_CONFIG, false); //this thing is nasty do not turn to true for this!
props.put(KafkaAvroDeserializerConfig.USE_LATEST_VERSION, true);
return props;
}
}
Shop.java or the Java POJO
import lombok.Data;
#Data
public class Shop {
Long id;
Double latitude, longitude;
String name;
String phone_number;
String place_id;
String type;
double rating;
String website;
int address_id;
String closing_hours;
String email;
int max_attendance;
String opening_hours;
String business_hours;
String close_day;
String description;
String is_open;
String weekend_business_hours;
Long user_shop_id;
}
and finally the consumer ShopConsumer.java
#Component
public class ShopConsumer {
private final Logger log = LogManager.getLogger(ShopConsumer.class);
#KafkaListener(
groupId = "${spring.kafka.consumer.group-id}",
topics = "${spring.kafka.consumer.topic}"
)
public void listen(ConsumerRecord<?, GenericRecord> messages, Acknowledgment ack) throws JsonProcessingException {
//debugging purposes only TODO remove me
log.info(messages.key());
log.info(messages.value());
log.info(messages.value().getSchema().getField("after"));
//convert the message, obtain the "after" section to get the newly updated value and parse it to Java POJO (in this case Shop.java)
Shop shop = new ObjectMapper().readValue(messages.value().get("after").toString(), Shop.class);
//debugging purposes only.
log.info("NEW VALUE #####-> " + shop.getName());
//other logic goes here...
ack.acknowledge();
}
}
I hope this helps anyone out there who is struggling to understand how to consume the Debezium message.

#Valid | response very verbose, how to customize?

I have a Spring Boot microservice and I want to valide the incoming requestBody of an endpoint.
By using #Valid with #NotBlank I have noticed that the answer is very verbose and my customized error message is deep into the object; here is an example:
{
"timestamp": "2020-12-17T09:28:26.529+0000",
"status": 400,
"error": "Bad Request",
"errors": [
{
"codes": [
"NotBlank.createUserRequest.username",
"NotBlank.username",
"NotBlank.java.lang.String",
"NotBlank"
],
"arguments": [
{
"codes": [
"createUserRequest.username",
"username"
],
"arguments": null,
"defaultMessage": "username",
"code": "username"
}
],
"defaultMessage": "USERNAME IS REQUIRED",
"objectName": "createUserRequest",
"field": "username",
"rejectedValue": "",
"bindingFailure": false,
"code": "NotBlank"
}
],
"message": "Validation failed for object='createUserRequest'. Error count: 1",
"path": "/api/user/create"
}
How can I customize this object returned? I would like the response to simply be something like this:
{
"timestamp": "2020-12-17T09:28:26.529+0000",
"status": 400,
"error": "Bad Request",
"message": "USERNAME IS REQUIRED"
}
Here is my code:
Request
#Data
public class CreateUserRequest {
#NotBlank(message = "username is required")
private String username;
#Size(min = 3, max = 64)
#NotBlank(message = "password is required")
private String password;
#NotBlank(message = "confirmPassword is required")
#Size(min = 3, max = 64)
private String confirmPassword;
}
Controller
#PostMapping("/create")
public ResponseEntity<User> createUser(#Valid #RequestBody CreateUserRequest request) {
User user = appService.createUserAndCart(request);
return ResponseEntity.ok(user);
}
Thank you for your experience
You can use #ControllerAdvice/#RestControllerAdvice
it allows you to handle exceptions across the whole application. You can think of it as an interceptor of exceptions thrown by methods annotated with #RequestMapping and similar.
And add a method like this,
#ExceptionHandler(Exception.class)
public ResponseEntity<Object> handleException(Exception ex)
//your custom body
return new ResponseEntity<>(body, HttpStatus.XXXXX);
}
You can specify a specific Exception type (I think it's InvalidArgumentException in your case)
Define a return class
import org.springframework.http.HttpStatus;
import java.util.HashMap;
/**
* #description:
* #author: 582895699#qq.com
* #time: 2020/12/20 下午 01:50
*/
public class Resp extends HashMap {
private static final long serialVersionUID = 1L;
public static final String TIMESTAMP = "timestamp";
public static final String STATUS = "status";
public static final String ERROR = "error";
public static final String MESSAGE = "message";
public static Resp fail(String message) {
Resp resp = new Resp();
resp.put(TIMESTAMP, System.currentTimeMillis());
resp.put(STATUS, HttpStatus.BAD_REQUEST.value());
resp.put(ERROR, HttpStatus.BAD_REQUEST.getReasonPhrase());
resp.put(MESSAGE, message);
return resp;
}
#Override
public Object put(Object key, Object value) {
return super.put(key, value);
}
}
Define global exception handling class and obtain exception information
import org.springframework.validation.BindingResult;
import org.springframework.validation.ObjectError;
import org.springframework.web.bind.MethodArgumentNotValidException;
import org.springframework.web.bind.annotation.ExceptionHandler;
import org.springframework.web.bind.annotation.RestControllerAdvice;
/**
* #description:
* #author: 582895699#qq.com
* #time: 2020/12/20 下午 01:55
*/
#RestControllerAdvice
public class GlobalExceptionHandler {
#ExceptionHandler(value = MethodArgumentNotValidException.class)
public Resp methodArgumentNotValidExceptionHandler(MethodArgumentNotValidException e) {
BindingResult bindingResult = e.getBindingResult();
ObjectError objectError = bindingResult.getAllErrors().get(0);
String message = objectError.getDefaultMessage();
return Resp.fail(message);
}
}

Handling of MethodArgumentNotValidException overridden by DefaultErrorAttributes

My goal is to have a custom response body for validation errors. This is a very common case and I've read lots of posts/blogs/articles and I've even implemented this myself in the past. For some reason, I cannot figure this out.
I have this #RestControllerAdvice
#Slf4j
#RequiredArgsConstructor
#RestControllerAdvice
public class ErrorHandler extends ResponseEntityExceptionHandler {
private final MessageSource messageSource;
#Override
protected ResponseEntity<Object> handleMethodArgumentNotValid(MethodArgumentNotValidException ex,
HttpHeaders headers, HttpStatus status, WebRequest request) {
ValidationErrorDTO validationError = ValidationErrorDTO.builder()
.fieldErrors(ex.getBindingResult().getFieldErrors().stream()
.map(fieldError -> FieldErrorDTO.builder()
.field(fieldError.getField())
.message(messageSource.getMessage(fieldError, LocaleContextHolder.getLocale()))
.build())
.collect(Collectors.toList()))
.build();
LOGGER.debug("W3MnsZ validation error: {}", validationError);
return new ResponseEntity<Object>(validationError, HttpStatus.BAD_REQUEST);
}
}
====
#ToString
#Builder
public class ValidationErrorDTO {
private final List<FieldErrorDTO> fieldErrors;
}
====
#ToString
#Builder
#Getter
#JsonInclude(Include.NON_NULL)
public class FieldErrorDTO {
private final String field;
private final String message;
}
My ErrorHandler.handleMethodArgumentNotValid() gets hit, but the actual response body returned to the client is not from my ValidationErrorDTO.
{
"timestamp": 1523115887261,
"status": 400,
"error": "Bad Request",
"exception": "org.springframework.web.bind.MethodArgumentNotValidException",
"errors": [
{
"codes": [
"NotEmpty.myDTO.field.another.lastly",
"NotEmpty.field.another.lastly",
"NotEmpty.lastly",
"NotEmpty.java.lang.String",
"NotEmpty"
],
"arguments": [
{
"codes": [
"myDTO.field.another.lastly",
"field.another.lastly"
],
"arguments": null,
"defaultMessage": "field.another.lastly",
"code": "field.another.lastly"
}
],
"defaultMessage": "may not be empty",
"objectName": "myDTO",
"field": "field.another.lastly",
"rejectedValue": null,
"bindingFailure": false,
"code": "NotEmpty"
}
],
"message": "Validation failed for object='myDTO'. Error count: 1",
"path": "/myPath"
}
I've figured out that what's happening is org.springframework.boot.autoconfigure.web.DefaultErrorAttributes is getting hit and somehow overriding my custom response body.
What do I do to allow my custom response body to be returned to the client?
Just figured it out.
ValidationErrorDTO.fieldErrors had no getter.
#ToString
#Builder
#Getter
public class ValidationErrorDTO {
#Singular
private final List<FieldErrorDTO> fieldErrors;
}
Now it works and we get response:
{
"fieldErrors": [
{
"field": "field.another.lastly",
"message": "may not be empty"
}
]
}

How to consume _embedded resources with Spring HATEOAS

I am trying to consume the following REST HAL response from a 3rd party service:
{
"id": 51780,
"name": "Lambeth",
"description": "",
"address_id": 54225,
"website": "",
"numeric_widget_id": 3602008,
"currency_code": "GBP",
"timezone": "Europe/London",
"country_code": "gb",
"live": true,
"_embedded": {
"settings": {
"has_services": true,
"has_classes": true,
"payment_tax": 0,
"currency": "GBP",
"requires_login": false,
"has_wallets": false,
"ask_address": true,
"_links": {
"self": {
"href": "https://myhost.com/api/v1/51780/settings"
}
}
}
},
"_links": {
"self": {
"href": "https://myhost.com/api/v1/company/51780"
},
"settings": {
"href": "https://myhost.com/api/v1/51780/settings"
}
}
}
Which I would like to map to a class like this:
public class Company extends ResourceSupport {
private String name;
private CompanySettings settings;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public CompanySettings getSettings() {
return settings;
}
public void setSettings(CompanySettings settings) {
this.settings = settings;
}
}
And a class for the embedded item like this:
public class CompanySettings extends ResourceSupport {
private String currency;
public String getCurrency() {
return currency;
}
public void setCurrency(String currency) {
this.currency = currency;
}
}
However I am having no luck getting the embedded item to map to the nested settings object. My code is below.
RestTemplate restTemplate = new RestTemplate();
HttpEntity<String> entity = new HttpEntity<String>("parameters", headers);
ResponseEntity<Resource<Company>> responseEntity = restTemplate.exchange("https://uk.bookingbug.com/api/v1/company/51780",
HttpMethod.GET, null, new ParameterizedTypeReference<Resource<Company>>() {
}, Collections.emptyMap());
if (responseEntity.getStatusCode() == HttpStatus.OK) {
Resource<Company> userResource = responseEntity.getBody();
Company company = userResource.getContent();
}
Any help would be greatly appreciated.

Resources