can I send Criteria in a spring feign client GetMapping request? - spring-boot

I'm trying to get userConfig from Organisation microservice, the app is getting quite large to I had too fetch UserConfig with userConfigCriteria for further use,
I wrote my feign service like this:
#AuthorizedFeignClient(name = "organization")
#Service
public interface UserConfigFeignService {
#RequestMapping(
value = "/api/feign-user-configs",
consumes = "application/json",
produces = "application/json",
method = RequestMethod.GET
)
ResponseEntity<List<UserConfigFein>> getUserConfig(UserConfigFeignCriteria criteria);
}
implemented like this:
public List<UserConfigFein> UserConfigByDepartment(UserConfigFeignCriteria criteria) {
try {
return userConfigFeignService.getUserConfig(criteria).getBody();
} catch (Exception e) {
e.printStackTrace();
throw new RuntimeException("feign error: couldn't get all userConfigs with criteria " + criteria);
}
}
in the Organisation microservice I created this end pt
#GetMapping("/feign-user-configs")
public ResponseEntity<List<UserConfigDTO>> getUserConfig(UserConfigCriteria criteria) {
log.debug("REST request to get a page of UserConfigs by criteria: {}", criteria);
//bla bla code that is not reached
}
an error is thrown at this point
throw new RuntimeException("feign error: couldn't get all userConfigs with criteria " + criteria);
[405 Method Not Allowed] during [GET] to [http://organization/api/feign-user-configs] [UserConfigFeignService#getUserConfigByDepartmentId(UserConfigFeignCriteria)]: [{
"title" : "Method Not Allowed",
"status" : 405,
"detail" : "Request method 'POST' not supported",
"path" : "/api/feign-user-configs",
"message" : "error.http.405"
}]
the end point in Organization is not being reached.
any ideas ?
I'm expecting
UserConfigFeignCriteria
to be sent to Organisation microservice
edit: here's UserConfigFeignCriteria
#ParameterObject
public class UserConfigFeignCriteria implements Serializable, Criteria {
private LongFilter id;
private StringFilter userId;
private StringFilter nickname;
private Boolean focusMode;
private LongFilter departmentId;
private Boolean distinct;
public UserConfigFeignCriteria(UserConfigFeignCriteria other) {
this.id = other.id == null ? null : other.id.copy();
this.userId = other.userId == null ? null : other.userId.copy();
this.nickname = other.nickname == null ? null : other.nickname.copy();
this.focusMode = other.focusMode;
this.distinct = other.distinct;
}
public UserConfigFeignCriteria() {}
#Override
public UserConfigFeignCriteria copy() {
return new UserConfigFeignCriteria(this);
}
//getters and setters
#Override
public boolean equals(Object o)
#Override
public int hashCode()
#Override
public String toString()
}

I solved the issue by changing the request method in interface UserConfigFeignService to POST like so
#AuthorizedFeignClient(name = "organization")
#Service
public interface UserConfigFeignService {
#RequestMapping(
value = "/api/feign-user-configs",
consumes = "application/json",
produces = "application/json",
method = RequestMethod.POST
)
ResponseEntity<List<UserConfigFein>> getUserConfig(UserConfigFeignCriteria criteria);
}
I have to make more research for why criteria can not be sent with a get method when using feign client, cause fetching data with POST methos is not a good solution.

Related

FeignException com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of `org.springframework.http.ResponseEntity`

Any Help please !!
I receive this error when I'm calling my endpoint which call Feign in the background :
com.fasterxml.jackson.databind.exc.InvalidDefinitionException: Cannot construct instance of
`org.springframework.http.ResponseEntity` (no Creators, like default constructor, exist): cannot deserialize
from Object value (no delegate- or property-based Creator)
at [Source: (BufferedReader); line: 1, column: 2]
This is my endpoint inside Controller :
#RestController
#RequestMapping(Routes.URI_PREFIX)
public class CartoController {
#Autowired
private ReadCartographyApiDelegate readCartographyApiDelegate;
#GetMapping(value = "/cartographies/{uid}", produces = {MediaType.APPLICATION_JSON_VALUE})
public ResponseWrapper<ReadCartographyResponse> readCarto(HttpServletRequest request,
#PathVariable(name = "uid") String uid) {
ResponseEntity<ReadCartographyResponse> result ;
try {
result = readCartographyApiDelegate.readCartography(uid);
}catch (Exception e){
throw new TechnicalException("Error during read Carto");
}
return responseWrapperWithIdBuilder.of(result.getBody());
}
}
Interface ReadCartographyApiDelegate generated automatically by openApi from yaml file :
#javax.annotation.Generated(value = "org.openapitools.codegen.languages.SpringCodegen", date = "...")
public interface ReadCartographyApiDelegate {
default Optional<NativeWebRequest> getRequest() {
return Optional.empty();
}
default ResponseEntity<ReadCartographyResponse> readCartography(String uid) {
getRequest().ifPresent(request -> {
for (MediaType mediaType: MediaType.parseMediaTypes(request.getHeader("Accept"))) {
if (mediaType.isCompatibleWith(MediaType.valueOf("application/json"))) {
String exampleString = "null";
ApiUtil.setExampleResponse(request, "application/json", exampleString);
break;
}
}
});
return new ResponseEntity<>(HttpStatus.NOT_IMPLEMENTED);
}
}
This my ReadCartoApiDelegateImpl which implements ReadCartographyApiDelegate interface :
#Service
public class ReadCartographyApiDelegateImpl implements ReadCartographyApiDelegate {
private EcomGtmClient ecomGtmClient;
public ReadCartographyApiDelegateImpl(EcomGtmClient ecomGtmClient) {
this.ecomGtmClient = ecomGtmClient;
}
#Override
public ResponseEntity<ReadCartographyResponse> readCartography(String uid) {
ResponseEntity<ReadCartographyResponse> response = ecomGtmClient.readCartography(uid);
return response;
}
}
This is the feign client :
#FeignClient(name = "ecomGtmSvc", url = "http://localhost/")
public interface EcomGtmClient {
#GetMapping(value = "/read-carto/{uid}")
ResponseEntity<ReadCartographyResponse> readCartography(#PathVariable("uid") String uid);
}
The problem is that ResponseEntity (spring class) class doesn't contain default constructor which is needed during creating of instance. is there Any config to resolve this issue ?
If you want access to the body or headers on feign responses, you should use the feign.Response class. ResponseEntity does not work with feign because it is not meant to. I think it is best if you just return Response from your feign client method. You should then be able to pass the body to the ResponseEntity instance in the Controller.
What is your reason to even use the response-wrapper, i can't really figure that out from your code?
Sadly I couldn't find any documentation on the Response class, but here's the link to the source on GitHub.
https://github.com/OpenFeign/feign/blob/master/core/src/main/java/feign/Response.java
My Suggestion would be
#FeignClient(name = "ecomGtmSvc", url = "http://localhost/")
public interface EcomGtmClient {
#GetMapping(value = "/read-carto/{uid}")
ReadCartographyResponse readCartography(#PathVariable("uid") String uid);
}
#RestController
#RequestMapping(Routes.URI_PREFIX)
public class CartoController {
#Autowired
private ReadCartographyApiDelegate readCartographyApiDelegate;
#GetMapping(value = "/cartographies/{uid}", produces = {MediaType.APPLICATION_JSON_VALUE})
public ResponseWrapper<ReadCartographyResponse> readCarto(HttpServletRequest request,
#PathVariable(name = "uid") String uid) {
ReadCartographyResponse result ;
try {
result = readCartographyApiDelegate.readCartography(uid);
}catch (Exception e){
throw new TechnicalException("Error during read Carto");
}
// I don't know where you get the builder from, so I assume it does something import and is needed
return responseWrapperWithIdBuilder.of(result);
}
}
Of course you'd also have to change all intermediate classes.
The Response Output was the correct Object that I have to put, cause every time I need to check the status from my feign client endpoint to do différent logic
#FeignClient(name = "ecomGtmSvc", url = "http://localhost/")
public interface EcomGtmClient {
#GetMapping(value = "/read-carto/{uid}")
ReadCartographyResponse readCartography(#PathVariable("uid") String uid);
}

unable to get the response entity from an api with mockmvc

I have an issue when I try to run my controller's unit test class. I get always a empty body in the response and I don't manage to find why.
I put here the code. Maybe someone with an external vision will be able to see the reason.
the controller:
#ResponseBody
#PostMapping(path = "/upload", consumes = { MediaType.MULTIPART_FORM_DATA_VALUE }, produces = { MediaType.APPLICATION_JSON_VALUE })
public ResponseEntity<Object> uploadFile(#RequestParam("file") MultipartFile multipartFileData, #RequestParam(name="jobId", required = false) String jobId) {
JobStatus result;
try {
result = this.fileService.uploadFileChunk(multipartFileData, 1, 1, jobId);
}catch (ExecutionException|InterruptedException|IOException ex){
Thread.currentThread().interrupt();
return new ResponseEntity<>(ex,HttpStatus.INTERNAL_SERVER_ERROR);
}
return new ResponseEntity<>(result,HttpStatus.OK);
}
the unit test class:
#RunWith(SpringRunner.class)
#SpringBootTest(classes= FileUploadServiceRestController.class)
public class FileUploadServiceControllerTest {
private MockMvc mockMvc;
#Autowired
private WebApplicationContext context;
#MockBean
private FileUploadServiceImpl fileService;
#Before
public void setup() {
mockMvc = MockMvcBuilders.webAppContextSetup(context).build();
}
#Test
public void testUploadFile()
throws Exception {
MockMultipartFile file
= new MockMultipartFile(
"file",
"hello.txt",
MediaType.TEXT_PLAIN_VALUE,
"Hello, World!".getBytes()
);
JobStatus job = new JobStatus("uuid", ConstantUtil.JOB_STARTED);
when(fileService.uploadFileChunk(Mockito.any(MultipartFile.class),Mockito.eq(1),Mockito.eq(1),Mockito.isNull())).thenReturn(job);
mockMvc.perform(MockMvcRequestBuilders.multipart("/file/upload").file(file))
.andDo(MockMvcResultHandlers.print())
.andExpect(status().isOk());
}
}
and the object which will be transfered:
public class JobStatus implements Serializable {
private static final long serialVersionUID = -4405865740177389860L;
private String jobId;
private String status;
public JobStatus() {
}
public JobStatus(String jobId, String status) {
this.jobId = jobId;
this.status = status;
}
public String getJobId() {
return jobId;
}
public void setJobId(String jobId) {
this.jobId = jobId;
}
public String getStatus() {
return status;
}
public void setStatus(String status) {
this.status = status;
}
}
For information, this controller works well when I call it from the client. I can see that the mock is well returned when I put a breakpoint at the end of the controller, but the response body stay empty.
I add here the result of print if it could help:
MockHttpServletRequest:
HTTP Method = POST
Request URI = /file/upload
Parameters = {}
Headers = [Content-Type:"multipart/form-data"]
Body = <no character encoding set>
Session Attrs = {}
Handler:
Type = org.iso.fileservice.controller.FileUploadServiceRestController
Method = org.iso.fileservice.controller.FileUploadServiceRestController#uploadFile(MultipartFile, String)
Async:
Async started = false
Async result = null
Resolved Exception:
Type = org.springframework.http.converter.HttpMessageNotWritableException
ModelAndView:
View name = null
View = null
Model = null
FlashMap:
Attributes = null
MockHttpServletResponse:
Status = 500
Error message = null
Headers = []
Content type = null
Body =
Forwarded URL = null
Redirected URL = null
Cookies = []
thanks, Mathieu
Just ran into this today.
I found the answer on Why MockMvc request retrieve empty responseBody while test succeed?
I just added My restController with #Autowired instead of #InjectMocks, after that I started to retrieve the Response Entity instead of a 500 status response

Netflix Feign custom error decoder doesn't work correctly

I have a problem with custom error decoder in netflix Feign.
I have a service A and a service B , service A is sending something to service B :
#FeignClient(value = "A", url = "${B.url}", configuration = MyConfig.class)
public interface NotificationClient {
#DeleteMapping(value = "/somepath", consumes = MediaType.APPLICATION_JSON_VALUE)
ResponseEntity<Void> sendSomething(MyObject myObject);
}
And MyConfig looks like:
#Configuration
public class MyConfig {
#Bean
ErrorDecoder errorDecoder() {
return new MyErrorDecoder();
}
}
And MyErrorDecoder :
public class MyErrorDecoder implements ErrorDecoder {
#Override
public Exception decode(String methodKey, Response response) {
if (response.status() >= 400 && response.status() <= 499) {
return new MyClientException(response.status(), response.reason(), response.request().url());
}
if (response.status() >= 500 && response.status() <= 599) {
return new MyServerException(response.status(), response.reason(), response.request().url());
}
return errorStatus(methodKey, response);
}
}
And for example MyClientException :
#Getter
public class MyClientException extends RuntimeException {
private static final long serialVersionUID = 8021218722733395779L;
private final int httpStatusCode;
public MyClientException(int status, String reason, String url){
super(String.format("Error when sending request to: ' %s ' : status: %d %s", url, status, reason != null ? reason : ""));
this.httpStatusCode = status;
}
}
My problem is that I have a validation in service 'B', and it returns http status code 400, but in my service 'A' i got status 500 and message like "Error when sending request to: ' url_from_properties/somepath ' : status: 400 ".. why? I want to have the same status code in service 'A' what i got in service 'B'.
I looking for answer but nothing works exactly like i expect. What is wrong here?

Java: GroupSequenceProvider for Validation, object is null in getValidationGroups method

This is what I am trying to achieve:
I have an update request object and user is allowed to do Partial Updates. But I want to validate the field only if it is in the request body. Otherwise, it is OK to be null. To achieve this, I am using GroupSequenceProvider to let the Validator know what groups to validate. What am I doing wrong here? If there is a blunder, how do I fix it?
Documentation: https://docs.jboss.org/hibernate/validator/5.1/reference/en-US/html/chapter-groups.html#example-implementing-using-default-group-sequence-provider
#GroupSequenceProvider(UpdateUserRegistrationGroupSequenceProvider.class)
public class UpdateUserRegistrationRequestV1 {
#NotBlank(groups = {EmailExistsInRequest.class})
#Email(groups = {EmailExistsInRequest.class})
#SafeHtml(whitelistType = SafeHtml.WhiteListType.NONE, groups = {EmailExistsInRequest.class})
private String email;
#NotNull(groups = {PasswordExistsInRequest.class})
#Size(min = 8, max = 255, groups = {PasswordExistsInRequest.class})
private String password;
#NotNull(groups = {FirstNameExistsInRequest.class})
#Size(max = 255, groups = {FirstNameExistsInRequest.class})
#SafeHtml(whitelistType = SafeHtml.WhiteListType.NONE, groups = {FirstNameExistsInRequest.class})
private String firstName;
// THERE ARE GETTERS AND SETTERS BELOW
}
Group Sequence Provider Code:
public class UpdateUserRegistrationGroupSequenceProvider implements DefaultGroupSequenceProvider<UpdateUserRegistrationRequestV1> {
public interface EmailExistsInRequest {}
public interface PasswordExistsInRequest {}
public interface FirstNameExistsInRequest {}
#Override
public List<Class<?>> getValidationGroups(UpdateUserRegistrationRequestV1 updateUserRegistrationRequestV1) {
List<Class<?>> defaultGroupSequence = new ArrayList<Class<?>>();
defaultGroupSequence.add(Default.class);
defaultGroupSequence.add(UpdateUserRegistrationRequestV1.class);
if(StringUtils.hasText(updateUserRegistrationRequestV1.getEmail())) {
defaultGroupSequence.add(EmailExistsInRequest.class);
}
if(StringUtils.hasText(updateUserRegistrationRequestV1.getPassword())) {
defaultGroupSequence.add(PasswordExistsInRequest.class);
}
if(StringUtils.hasText(updateUserRegistrationRequestV1.getFirstName())) {
defaultGroupSequence.add(FirstNameExistsInRequest.class);
}
return defaultGroupSequence;
}
}
I am using Spring MVC, so this is how my controller method looks,
#RequestMapping(value = "/{userId}", method = RequestMethod.PUT, consumes = MediaType.APPLICATION_JSON_VALUE)
#ResponseStatus(HttpStatus.NO_CONTENT)
public void updateUser(#PathVariable("userId") Long userId,
#RequestBody #Valid UpdateUserRegistrationRequestV1 request) {
logger.info("Received update request = " + request + " for userId = " + userId);
registrationService.updateUser(userId, conversionService.convert(request, User.class));
}
Now the problem is, the parameter "updateUserRegistrationRequestV1" in the UpdateUserRegistrationGroupSequenceProvider.getValidationGroups method is null. This is the request object that I am sending in the request body and I am sending email field with it.
What am I doing wrong?
I too went through the same issue ,and hopefully solved it
You just have to check the object is null and put all your conditions inside it.
public List<Class<?>> getValidationGroups(Employee object) {
List<Class<?>> sequence = new ArrayList<>();
//first check if the object is null
if(object != null ){
if (!object.isDraft()) {
sequence.add(Second.class);
}
}
// Apply all validation rules from default group
sequence.add(Employee.class);
return sequence;
}

receiving json and deserializing as List of object at spring mvc controller

My code is as below:
controller
#RequestMapping(value="/setTest", method=RequestMethod.POST, consumes="application/json")
public #ResponseBody ModelMap setTest(#RequestBody List<TestS> refunds, ModelMap map) {
for(TestS r : refunds) {
System.out.println(r.getName());
}
// other codes
}
TestS pojo
public class TestS implements Serializable {
private String name;
private String age;
//getter setter
}
Json request
var z = '[{"name":"1","age":"2"},{"name":"1","age":"3"}]';
$.ajax({
url: "/setTest",
data: z,
type: "POST",
dataType:"json",
contentType:'application/json'
});
It's giving this error
java.lang.ClassCastException: java.util.LinkedHashMap cannot be cast to com.air.cidb.entities.TestS
I am using spring 3.1.2 and jackson 2.0.4
Edit: I want to receive list of TestS objects at my controller method, and process them. I am not able to find if I am sending wrong json or my method signature is wrong.
Here is the code that works for me. The key is that you need a wrapper class.
public class Person {
private String name;
private Integer age;
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Integer getAge() {
return age;
}
public void setAge(Integer age) {
this.age = age;
}
#Override
public String toString() {
return "Person [name=" + name + ", age=" + age + "]";
}
A PersonWrapper class
public class PersonWrapper {
private List<Person> persons;
/**
* #return the persons
*/
public List<Person> getPersons() {
return persons;
}
/**
* #param persons the persons to set
*/
public void setPersons(List<Person> persons) {
this.persons = persons;
}
}
My Controller methods
#RequestMapping(value="person", method=RequestMethod.POST,consumes="application/json",produces="application/json")
#ResponseBody
public List<String> savePerson(#RequestBody PersonWrapper wrapper) {
List<String> response = new ArrayList<String>();
for (Person person: wrapper.getPersons()){
personService.save(person);
response.add("Saved person: " + person.toString());
}
return response;
}
The request sent is json in POST
{"persons":[{"name":"shail1","age":"2"},{"name":"shail2","age":"3"}]}
And the response is
["Saved person: Person [name=shail1, age=2]","Saved person: Person [name=shail2, age=3]"]
This is not possible the way you are trying it. The Jackson unmarshalling works on the compiled java code after type erasure. So your
public #ResponseBody ModelMap setTest(#RequestBody List<TestS> refunds, ModelMap map)
is really only
public #ResponseBody ModelMap setTest(#RequestBody List refunds, ModelMap map)
(no generics in the list arg).
The default type Jackson creates when unmarshalling a List is a LinkedHashMap.
As mentioned by #Saint you can circumvent this by creating your own type for the list like so:
class TestSList extends ArrayList<TestS> { }
and then modifying your controller signature to
public #ResponseBody ModelMap setTest(#RequestBody TestSList refunds, ModelMap map) {
#RequestMapping(
value="person",
method=RequestMethod.POST,
consumes="application/json",
produces="application/json")
#ResponseBody
public List<String> savePerson(#RequestBody Person[] personArray) {
List<String> response = new ArrayList<String>();
for (Person person: personArray) {
personService.save(person);
response.add("Saved person: " + person.toString());
}
return response;
}
We can use Array as shown above.
Solution works very well,
public List<String> savePerson(#RequestBody Person[] personArray)
For this signature you can pass Person array from postman like
[
{
"empId": "10001",
"tier": "Single",
"someting": 6,
"anything": 0,
"frequency": "Quaterly"
}, {
"empId": "10001",
"tier": "Single",
"someting": 6,
"anything": 0,
"frequency": "Quaterly"
}
]
Don't forget to add consumes tag:
#RequestMapping(value = "/getEmployeeList", method = RequestMethod.POST, consumes="application/json", produces = "application/json")
public List<Employee> getEmployeeDataList(#RequestBody Employee[] employeearray) { ... }
I believe this will solve the issue
var z = '[{"name":"1","age":"2"},{"name":"1","age":"3"}]';
z = JSON.stringify(JSON.parse(z));
$.ajax({
url: "/setTest",
data: z,
type: "POST",
dataType:"json",
contentType:'application/json'
});
For me below code worked, first sending json string with proper headers
$.ajax({
type: "POST",
url : 'save',
data : JSON.stringify(valObject),
contentType:"application/json; charset=utf-8",
dataType:"json",
success : function(resp){
console.log(resp);
},
error : function(resp){
console.log(resp);
}
});
And then on Spring side -
#RequestMapping(value = "/save",
method = RequestMethod.POST,
consumes="application/json")
public #ResponseBody String save(#RequestBody ArrayList<KeyValue> keyValList) {
//Saving call goes here
return "";
}
Here KeyValue is simple pojo that corresponds to your JSON structure also you can add produces as you wish, I am simply returning string.
My json object is like this -
[{"storedKey":"vc","storedValue":"1","clientId":"1","locationId":"1"},
{"storedKey":"vr","storedValue":"","clientId":"1","locationId":"1"}]

Resources