I have several microservices and I use openfeign to call the different micro services.
the entry point for the global application is named dispatcher-ws. His role is to dispatch calls depending on the payload.
As entry I do have the following payload:
{
"operation": "signature",
"clientId": "abcdef",
...
"pdfDocument": "JVBERi0xLjMNCiXi48/TDQoNCjEg..."
}
I have microservice named signature-ws that handles pdf signature. So far, so good. I implemented my client this way:
#FeignClient(name="signature-ws", decode404 = true, url = "http://localhost:8080/signature-ws/api")
public interface SignatureClient {
#PostMapping("/signature")
Map<String, Object> signDocument(RequestDto request) throws AppServiceException;
}
In my service layer, I try to make the call depending on operation value:
#Service
public class RequestServiceImpl implements DispatchService {
private final RequestRepository requestRepository;
private final SignatureClient signatureClient;
private final Resilience4JCircuitBreakerFactory circuitBreakerFactory;
#Autowired
public DispatchServiceImpl(RequestRepository requestRepository,
SignatureClient signatureClient,
Resilience4JCircuitBreakerFactory circuitBreakerFactory) {
this.requestRepository = requestRepository;
this.signatureClient = signatureClient;
this.circuitBreakerFactory = circuitBreakerFactory;
}
#Override
public RequestDto handleRequest(RequestDto request) {
RequestDto returnValue = new RequestDto();
// if not initialized, throw null pointer exception...
returnValue.setPayloads(new ArrayList<>());
if(request.getOperation().equals("signature") {
Resilience4JCircuitBreaker circuitBreaker = circuitBreakerFactory.create("signature");
Supplier<Map<String, Object>> signatureResponseSupplier =
() -> signatureClient.signDocument(request);
Map<String, Object> signatureResponse = circuitBreaker.run(
signatureResponseSupplier,
throwable -> handleException()
);
...
returnValue.getResponses().add(signatureResponse)
}
return retunValue;
}
...
private Map<String, Object> handleException() {
Map<String, Object> returnValue = new HashMap<>();
returnValue.put("Error", "Error rmessage ... ");
returnValue.put("status", "Failure");
return returnValue;
}
If i don't pass pdfDocument in signature webservice, I do retrieve an exception.
{
"errorId": "Qe99DwntFrMPCAfuZfDQW1ucwNh5BK",
"status": "ERROR",
"operations": "signature",
"profile": "client123456",
"errorMessage": "PDF is missing",
"createdAt": 1647354022127
}
I would like to retrieve the exception response and pass the key values to the map in the handleException method. At this stage, it doesn't return anything and worse of all, i do return a 200 status.
I implemented a controllerAdvice that manage the response to return. This class is identical in all my web services(i should think about creating a micro service for handling all exceptions...)
#ControllerAdvice(basePackages = { "com.company.app" })
public class AppExceptionsHandler {
private final RequestContext requestContext;
#Autowired
public AppExceptionsHandler(RequestContext requestContext) {
this.requestContext = requestContext;
}
#ExceptionHandler(value = {AppServiceException.class})
public ResponseEntity<Object> handleAppException(AppServiceException ex,
WebRequest request) {
// récupérer le body
DispatchDto response = requestContext.getResponse();
ErrorMessage errorMessage = ErrorMessage.builder()
.errorId(response.getId())
.status(RequestOperationStatus.ERROR.name())
.operations(response.getOperations())
.profile(response.getProfile())
.errorMessage(ex.getMessage())
.createdAt(new Date())
.build();
}
return new ResponseEntity<>(errorMessage, new HttpHeaders(), HttpStatus.INTERNAL_SERVER_ERROR);
}
}
What i expect is to return the same exception in my dispatcher microservice.
I found a trick to solve this issue.
First I surrounded with a try catch my feign request :
try {
...
Map<String, Object> facturxResponse =
facturXClient.createFacturX(dispatchDto);
...
} catch(FeignException e) {
System.out.println(e.getMessage());
throw new AppServiceException(e.getMessage());
}
I noted that e.getMessage returns a string which has this pattern:
[500 Internal Server Error] during [POST] to [http://localhost:8080/my-ws/api/ws]
[FacturXClient#createFacturX(DispatchDto)]: [{"errorId":"z3o1bE8SJrm8WGrxlpIIWe6TNf0NzR","status":"ERROR","operations":"facturx","profile":"client123456","errorMessage":"PDF is missing","createdAt":1647422337344}]
I throw this exception and intercept the response
#ExceptionHandler(value = {AppServiceException.class})
public ResponseEntity<Object> handleUserServiceException(AppServiceException ex,
WebRequest request) throws JsonProcessingException {
String input = ex.getMessage();
String[] splitResponse = input.split(":", 4);
ObjectMapper mapper = new ObjectMapper().enable(SerializationFeature.INDENT_OUTPUT);
String response = splitResponse[3].trim().substring(1, splitResponse[3].trim().length() -1);
ErrorMessage errorMessage = mapper.readValue(response, ErrorMessage.class);
System.out.println(errorMessage.toString());
return new ResponseEntity<>(errorMessage, new HttpHeaders(), HttpStatus.INTERNAL_SERVER_ERROR);
}
I finally get the expected response:
{
"errorId": "z3o1bE8SJrm8WGrxlpIIWe6TNf0NzR",
"status": "ERROR",
"operations": "facturx",
"profile": "client123456",
"errorMessage": "PDF is missing",
"createdAt": "2022-03-16T09:18:57.344+00:00"
}
Related
public void PayuMonney(){
**JAVA
this method i am using in my code but it's not done
here i am use payumonney code given to Documentation https://payumobile.gitbook.io/sdk-integration/android/payucheckoutpro. i trying too much but the toast appear in "invalid Hash"**
PayUPaymentParams.Builder builder = new PayUPaymentParams.Builder();
builder.setAmount(mAmount)
.setIsProduction(true)
.setProductInfo(mProductInfo)
.setKey(mMerchantKey)
.setPhone(mPhoneNumber)
.setTransactionId(mTXNId)
.setFirstName(mFirstName)
.setEmail(mEmailId)
.setSurl("https://www.payumoney.com/mobileapp/payumoney/success.php")
.setFurl("https://www.payumoney.com/mobileapp/payumoney/failure.php");
**Optional can contain any additional PG params**
PayUPaymentParams payUPaymentParams = builder.build();
***here i am calling Payuminney checkout process Sample code***
PayUCheckoutPro.open(
this,
payUPaymentParams,
new PayUCheckoutProListener() {
#Override
public void onPaymentSuccess(Object response) {
//Cast response object to HashMap
HashMap<String,Object> result = (HashMap<String, Object>) response;
String payuResponse = (String)result.get(PayUCheckoutProConstants.CP_PAYU_RESPONSE);
String merchantResponse = (String) result.get(PayUCheckoutProConstants.CP_MERCHANT_RESPONSE);
}
#Override
public void onPaymentFailure(Object response) {
//Cast response object to HashMap
HashMap<String,Object> result = (HashMap<String, Object>) response;
String payuResponse = (String)result.get(PayUCheckoutProConstants.CP_PAYU_RESPONSE);
String merchantResponse = (String) result.get(PayUCheckoutProConstants.CP_MERCHANT_RESPONSE);
}
#Override
public void onPaymentCancel(boolean isTxnInitiated) {
}
#Override
public void onError(ErrorResponse errorResponse) {
*//code give some error toast here i the onError fuction*
String errorMessage = errorResponse.getErrorMessage();
Toast.makeText(FinalPlaceOrderActivity.this, errorMessage, Toast.LENGTH_SHORT).show();
}
#Override
public void setWebViewProperties(#Nullable WebView webView, #Nullable Object o) {
*//For setting webview properties, if any. Check Customized Integration section for more details on this*
}
#Override
public void generateHash(HashMap<String, String> valueMap, PayUHashGenerationListener hashGenerationListener) {
String hashName = valueMap.get(PayUCheckoutProConstants.CP_HASH_NAME);
String hashData = valueMap.get(PayUCheckoutProConstants.CP_HASH_STRING);
if (!TextUtils.isEmpty(hashName) && !TextUtils.isEmpty(hashData)) {
*//Do not generate a hash from local, it needs to be calculated from server-side only. Here, hashString contains hash created from your server side.
//here i am call the server file where generate a hash*
StringRequest OrderPlace = new StringRequest(Request.Method.POST, "url link", new Response.Listener() {
#Override
public void onResponse(String response) {
//here i am get hash from the server
System.out.println(response);
String merchandHsh = response;
HashMap<String, String> dataMap = new HashMap<>();
dataMap.put(hashName, merchandHsh);
hashGenerationListener.onHashGenerated(dataMap);
}
}, new Response.ErrorListener() {
#Override
public void onErrorResponse(VolleyError error) {
Toast.makeText(getApplicationContext(), "We can't process this time..", Toast.LENGTH_SHORT).show();
}
}
) {
#Nullable
#Override
protected Map<String, String> getParams() throws AuthFailureError {
Map<String, String> map = new HashMap<String, String>();
map.put("key", mMerchantKey);
map.put("texID", mTXNId);
map.put("amount", mAmount);
map.put("productname", mProductInfo);
map.put("name", mFirstName);
map.put("email", mEmailId);
return map;
}
};
Volley.newRequestQueue(FinalPlaceOrderActivity.this).add(OrderPlace);
}
}
}
);
}
but the toast appears a "Hash invalid". this method I am using in my code but it's not done
here I am using the pay money code given to Documentation https://payumobile.gitbook.io/sdk-integration/android/payucheckoutpro. I trying too much but the toast appears in "invalid Hash"
How to implement PayUmonney in Android & how to create a Hash key in local because I don't know how to create in server
public class DgiQtyAction extends DispatchAction {
private final Logger mLog = Logger.getLogger(this.getClass());
public ActionForward fnDgiQty(ActionMapping mapping, ActionForm form,
HttpServletRequest request, HttpServletResponse response)
throws Exception {
mLog.debug(request.getParameter(EcoConstants.ecopidid));
ActionErrors errorMessage = new ActionErrors();
if(request.getSession().getAttribute(EcoConstants.userBean)==null)
{
request.setAttribute(EcoConstants.ERROR_MESSAGE,EcoConstants.SESSION_TIMEOUT);
errorMessage.add(Globals.MESSAGE_KEY, new ActionMessage(EcoConstants.error_message,EcoConstants.SESSION_TIMEOUT));
saveMessages(request, errorMessage);
request.setAttribute(EcoConstants.errorMessageType,EcoConstants.errorMessageType);
return mapping.findForward(EcoConstants.SESSION_FORWARD);
}
String ecoPidID = (String) request.getParameter(EcoConstants.ecopidid);
String pidId = ESAPI.encoder().encodeForHTML((String) request.getParameter(EcoConstants.pidid));
mLog.debug("pidid --------" + pidId);
mLog.debug("ecopidpid --------" + ecoPidID);
ArrayList dgiQty = new ArrayList();
NeedDgiForm niForm = new NeedDgiForm();
try {
NeedDgiBD niBD = new NeedDgiBD();
if (ecoPidID != null) {
dgiQty = niBD.getDgiQty(ecoPidID);
if (dgiQty.size() != 0) {
mLog.debug(dgiQty.get(0).toString());
if (dgiQty.get(0).toString().equals(EcoConstants.hundred)) {
niForm.setGlqtyList(new ArrayList());
request.setAttribute(EcoConstants.pidId, pidId);
return mapping.findForward(EcoConstants.SuccessInfo);
} else {
mLog.debug("fnBug 1----------------> " + dgiQty.get(0));
mLog.info("dgiQty sizeeeee: :" + dgiQty.size());
niForm.setGlqtyList(dgiQty);
}
}
}
} catch (Exception e) {
//log.error("General Exception in DgiQtyAction.fnDgiQty: "
// + e.getMessage(), e);
request.setAttribute(EcoConstants.ERROR_MESSAGE, e.getMessage());
return mapping.findForward(EcoConstants.ERROR_PAGE);
}
mLog.debug("pidid after wards--------" + pidId);
request.setAttribute(EcoConstants.pidId, pidId);
request.setAttribute(mapping.getAttribute(), niForm);
return mapping.findForward(EcoConstants.SuccessInfo);
}
}
public class DgiQtyActionTest {
ActionMapping am;
ActionForm af;
DgiQtyAction dat;
private MockHttpSession mocksession;
private MockHttpServletRequest mockrequest;
private MockHttpServletResponse mockresponse;
#Test
public void fnDgiQty() throws Exception
{
mocksession = new MockHttpSession();
mockrequest = new MockHttpServletRequest();
mockresponse = new MockHttpServletResponse();
((MockHttpServletRequest) mockrequest).setSession(mocksession);
mocksession.setAttribute("userBean","userBean");
RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(mockrequest));
mockrequest.addParameter("ecopid","something");
mockrequest.addParameter("pid","<script>");
Encoder instance = ESAPI.encoder();
assertEquals("something",mockrequest.getParameter("ecopid"));
assertEquals("<script>",instance.encodeForHTML(mockrequest.getParameter("pid")));
dat=mock(DgiQtyAction.class);
am=mock(ActionMapping.class);
af=mock(ActionForm.class);
dat.fnDgiQty(am,af,mockrequest, mockresponse);
}
}
I wrote the unit test case for above class. i ran this code through jenkins and used sonarqube as code coverage.I need to cover the ESAPi encoder for the parameter, it got build success but the coverage percentage doesn't increase. i couldn't found the mistake in it. pls help me guys. Thanks in Advance
There seem to be an issue when I use AggregatingReplyingKafkaTemplate with template.setReturnPartialOnTimeout(true) in that, it returns timeout exception even if partial results are available from consumers.
In example below, I have 3 consumers to reply to the request topic and i've set the reply timeout at 10 seconds. I've explicitly delayed the response of Consumer 3 to 11 seconds, however, I expect the response back from Consumer 1 and 2, so, I can return partial results. However, I am getting KafkaReplyTimeoutException. Appreciate your inputs. Thanks.
I follow the code based on the Unit Test below.
[ReplyingKafkaTemplateTests][1]
I've provided the actual code below:
#RestController
public class SumController {
#Value("${kafka.bootstrap-servers}")
private String bootstrapServers;
public static final String D_REPLY = "dReply";
public static final String D_REQUEST = "dRequest";
#ResponseBody
#PostMapping(value="/sum")
public String sum(#RequestParam("message") String message) throws InterruptedException, ExecutionException {
AggregatingReplyingKafkaTemplate<Integer, String, String> template = aggregatingTemplate(
new TopicPartitionOffset(D_REPLY, 0), 3, new AtomicInteger());
String resultValue ="";
String currentValue ="";
try {
template.setDefaultReplyTimeout(Duration.ofSeconds(10));
template.setReturnPartialOnTimeout(true);
ProducerRecord<Integer, String> record = new ProducerRecord<>(D_REQUEST, null, null, null, message);
RequestReplyFuture<Integer, String, Collection<ConsumerRecord<Integer, String>>> future =
template.sendAndReceive(record);
future.getSendFuture().get(5, TimeUnit.SECONDS); // send ok
System.out.println("Send Completed Successfully");
ConsumerRecord<Integer, Collection<ConsumerRecord<Integer, String>>> consumerRecord = future.get(10, TimeUnit.SECONDS);
System.out.println("Consumer record size "+consumerRecord.value().size());
Iterator<ConsumerRecord<Integer, String>> iterator = consumerRecord.value().iterator();
while (iterator.hasNext()) {
currentValue = iterator.next().value();
System.out.println("response " + currentValue);
System.out.println("Record header " + consumerRecord.headers().toString());
resultValue = resultValue + currentValue + "\r\n";
}
} catch (Exception e) {
System.out.println("Error Message is "+e.getMessage());
}
return resultValue;
}
public AggregatingReplyingKafkaTemplate<Integer, String, String> aggregatingTemplate(
TopicPartitionOffset topic, int releaseSize, AtomicInteger releaseCount) {
//Create Container Properties
ContainerProperties containerProperties = new ContainerProperties(topic);
containerProperties.setAckMode(ContainerProperties.AckMode.MANUAL_IMMEDIATE);
//Set the consumer Config
//Create Consumer Factory with Consumer Config
DefaultKafkaConsumerFactory<Integer, Collection<ConsumerRecord<Integer, String>>> cf =
new DefaultKafkaConsumerFactory<>(consumerConfigs());
//Create Listener Container with Consumer Factory and Container Property
KafkaMessageListenerContainer<Integer, Collection<ConsumerRecord<Integer, String>>> container =
new KafkaMessageListenerContainer<>(cf, containerProperties);
// container.setBeanName(this.testName);
AggregatingReplyingKafkaTemplate<Integer, String, String> template =
new AggregatingReplyingKafkaTemplate<>(new DefaultKafkaProducerFactory<>(producerConfigs()), container,
(list, timeout) -> {
releaseCount.incrementAndGet();
return list.size() == releaseSize;
});
template.setSharedReplyTopic(true);
template.start();
return template;
}
public Map<String, Object> consumerConfigs() {
Map<String, Object> props = new HashMap<>();
props.put(ConsumerConfig.BOOTSTRAP_SERVERS_CONFIG,bootstrapServers);
props.put(ConsumerConfig.GROUP_ID_CONFIG, "test_id");
props.put(ConsumerConfig.AUTO_OFFSET_RESET_CONFIG, "earliest");
props.put(ConsumerConfig.KEY_DESERIALIZER_CLASS_CONFIG, org.apache.kafka.common.serialization.StringDeserializer.class);
props.put(ConsumerConfig.VALUE_DESERIALIZER_CLASS_CONFIG, org.apache.kafka.common.serialization.StringDeserializer.class);
return props;
}
public Map<String, Object> producerConfigs() {
Map<String, Object> props = new HashMap<>();
// list of host:port pairs used for establishing the initial connections to the Kakfa cluster
props.put(ProducerConfig.BOOTSTRAP_SERVERS_CONFIG,
bootstrapServers);
props.put(ProducerConfig.KEY_SERIALIZER_CLASS_CONFIG,
org.apache.kafka.common.serialization.StringSerializer.class);
props.put(ProducerConfig.VALUE_SERIALIZER_CLASS_CONFIG, org.apache.kafka.common.serialization.StringSerializer.class);
return props;
}
public ProducerFactory<Integer,String> producerFactory() {
return new DefaultKafkaProducerFactory<>(producerConfigs());
}
#KafkaListener(id = "def1", topics = { D_REQUEST}, groupId = "D_REQUEST1")
#SendTo // default REPLY_TOPIC header
public String dListener1(String in) throws InterruptedException {
return "First Consumer : "+ in.toUpperCase();
}
#KafkaListener(id = "def2", topics = { D_REQUEST}, groupId = "D_REQUEST2")
#SendTo // default REPLY_TOPIC header
public String dListener2(String in) throws InterruptedException {
return "Second Consumer : "+ in.toLowerCase();
}
#KafkaListener(id = "def3", topics = { D_REQUEST}, groupId = "D_REQUEST3")
#SendTo // default REPLY_TOPIC header
public String dListener3(String in) throws InterruptedException {
Thread.sleep(11000);
return "Third Consumer : "+ in;
}
}
'''
[1]: https://github.com/spring-projects/spring-kafka/blob/master/spring-kafka/src/test/java/org/springframework/kafka/requestreply/ReplyingKafkaTemplateTests.java
template.setReturnPartialOnTimeout(true) simply means the template will consult the release strategy on timeout (with the timeout argument = true, to tell the strategy it's a timeout rather than a delivery call).
It must return true to release the partial result.
This is to allow you to look at (and possibly modify) the list to decide whether you want to release or discard.
Your strategy ignores the timeout parameter:
(list, timeout) -> {
releaseCount.incrementAndGet();
return list.size() == releaseSize;
});
You need return timeout ? true : { ... }.
I am trying to use postman to test one of the post requests I created for my spring boot application. My post requests through postman always return 404.
I have created a same mapping route for a get request and with the postman, the get request works as expected.
I have tested with aws cli and made sure that I have the correct access key and secret key for uploading files to S3.
Code for my services
#Service
public class AmazonClient {
private AmazonS3 s3client;
#Value("${amazonProperties.endpointUrl}")
private String endpointUrl;
#Value("${amazonProperties.bucketName}")
private String bucketName;
#Value("${amazonProperties.accessKey}")
private String accessKey;
#Value("${amazonProperties.secretKey}")
private String secretKey;
#PostConstruct
private void initializeAmazon() {
AWSCredentials credentials = new BasicAWSCredentials(this.accessKey, this.secretKey);
this.s3client = AmazonS3ClientBuilder.standard().withRegion(Regions.US_EAST_2).withCredentials(
new AWSStaticCredentialsProvider(credentials)).build();
}
#Async
public String uploadFile(MultipartFile multipartFile, boolean enablePublicReadAccess) {
String fileUrl = "";
System.out.println("Reach");
try {
File file = convertMultiPartToFile(multipartFile);
String fileName = generateFileName(multipartFile);
System.out.println("FileName: " + fileName);
fileUrl = endpointUrl + "/" + bucketName + "/" + fileName;
PutObjectRequest putObjectRequest = new PutObjectRequest(this.bucketName, fileName, file);
if (enablePublicReadAccess) {
putObjectRequest.withCannedAcl(CannedAccessControlList.PublicRead);
}
s3client.putObject(putObjectRequest);
file.delete();
} catch (Exception e) {
e.printStackTrace();
}
return fileUrl;
}
private File convertMultiPartToFile(MultipartFile file) throws IOException {
File convFile = new File(file.getOriginalFilename());
FileOutputStream fos = new FileOutputStream(convFile);
fos.write(file.getBytes());
fos.close();
return convFile;
}
private String generateFileName(MultipartFile multiPart) {
return new Date().getTime() + "-" + multiPart.getOriginalFilename().replace(" ", "_");
}
public String deleteFileFromS3Bucket(String fileUrl) {
String fileName = fileUrl.substring(fileUrl.lastIndexOf("/") + 1);
s3client.deleteObject(new DeleteObjectRequest(bucketName, fileName));
return "Successfully deleted";
}
}
Code for my controller:
#RestController
#RequestMapping("/storage/files")
public class BucketController {
private AmazonClient amazonClient;
#Autowired
BucketController(AmazonClient amazonClient) {
this.amazonClient = amazonClient;
}
#GetMapping
public String getFile(){
return "Files";
}
#PostMapping("/file")
public String file() {
return "Reach!";
}
#PostMapping
public String uploadFile(#RequestPart(value = "file") MultipartFile file) {
System.out.println("Reach!!");
return this.amazonClient.uploadFile(file, true);
}
#DeleteMapping
public String deleteFile(#RequestPart(value = "url") String fileUrl) {
return this.amazonClient.deleteFileFromS3Bucket(fileUrl);
}
}
My security config:
#Override
protected void configure(HttpSecurity http) throws Exception {
http.authorizeRequests().antMatchers("/css/**", "/js/**", "/fonts/**", "/index").permitAll()
.antMatchers("/storage*").permitAll();
Through postman, I have selected a POST request and put http://localhost:8080/storage/files/file, in the body, I have entered a key "file" and set the value to a file type and chose a file from my local.
Here is the response:
{
"timestamp": "2019-09-02T19:09:54.864+0000",
"status": 404,
"error": "Not Found",
"message": "No message available",
"path": "/storage/files/file"
}
Project Structure
Postman Results
This is almost certainly your security config interfering.
Have you tried: .antMatchers("/storage/**") instead?
This is my consumer request function:
#PostMapping(value = "/spuSkuInfo")
public DataResponseDto<Map<String, Object>> addSpuAndSkuInfo(#Valid SpuSkuInfoBo spuSkuInfoBo) {
String url = REST_URL_PREFIX + "addSpuAndSkuInfo";
DataResponseDto<Map<String, Object>> dataResponseDto;
MultiValueMap<String,SpuSkuInfoBo> reqMap = new LinkedMultiValueMap<>();
reqMap.add("spuSkuInfoBo",spuSkuInfoBo);
HttpEntity<MultiValueMap<String,SpuSkuInfoBo>> httpEntity = new HttpEntity<>(reqMap);
dataResponseDto = restTemplate.postForObject(url,httpEntity,DataResponseDto.class);
}
This is my provider:
#PostMapping(value = "/addSpuAndSkuInfo")
public DataResponseDto<Map<String, Object>> addSpuAndSkuInfo(SpuSkuInfoBo spuSkuInfoBo) {
DataResponseDto<Map<String, Object>> responseDto;
try {
responseDto = spuSkuService.addSpuAndSkuInfo(spuSkuInfoBo);
} catch (Exception e) {
logger.error("addSpuAndSkuInfo", e);
responseDto = new DataResponseDto<>();
responseDto.setRspCd(PrdErrorCode.PRD_FAIL_CD);
responseDto.setRspInf("");
return responseDto;
}
return responseDto;
}
In consumer I can got the Pojo 'SpuSkuInfoBo'
And When I debugger in provider, I just can not receive the request body 'SpuSkuInfoBo'
Finally I solved the problem by adding #RequstBody in provider function
public DataResponseDto<Map<String, Object>> addSpuAndSkuInfo(#RequestBody SpuSkuInfoBo spuSkuInfoBo)