Return Value from async rest template spring - java

I am creating a async rest call using spring
#GetMapping(path = "/testingAsync")
public String value() throws ExecutionException, InterruptedException, TimeoutException {
AsyncRestTemplate restTemplate = new AsyncRestTemplate();
String baseUrl = "https://api.github.com/users/XXX";
HttpHeaders requestHeaders = new HttpHeaders();
requestHeaders.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));
String value = "";
HttpEntity entity = new HttpEntity("parameters", requestHeaders);
ListenableFuture<ResponseEntity<User>> futureEntity = restTemplate.getForEntity(baseUrl, User.class);
futureEntity.addCallback(new ListenableFutureCallback<ResponseEntity<User>>() {
#Override
public void onSuccess(ResponseEntity<User> result) {
System.out.println(result.getBody().getName());
// instead of this how can i return the value to the user ?
}
#Override
public void onFailure(Throwable ex) {
}
});
return "DONE"; // instead of done i want to return value to the user comming from the rest call
}
And is there any way i can convert ListenableFuture to use CompletableFuture that is used in java 8 ?

There are basically 2 things you can do.
Remove the ListenableFutureCallback and simply return the ListenableFuture
Create a DeferredResult and set the value of that in a ListenableFutureCallback.
Returning a ListenableFuture
#GetMapping(path = "/testingAsync")
public ListenableFuture<ResponseEntity<User>> value() throws ExecutionException, InterruptedException, TimeoutException {
AsyncRestTemplate restTemplate = new AsyncRestTemplate();
String baseUrl = "https://api.github.com/users/XXX";
HttpHeaders requestHeaders = new HttpHeaders();
requestHeaders.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));
String value = "";
HttpEntity entity = new HttpEntity("parameters", requestHeaders);
return restTemplate.getForEntity(baseUrl, User.class);
}
Spring MVC will add a ListenableFutureCallback itself to fill a DeferredResult and you will get a User eventually.
Using a DeferredResult
If you want more control on what to return you can use a DeferredResult and set the value yourself.
#GetMapping(path = "/testingAsync")
public DeferredResult<String> value() throws ExecutionException, InterruptedException, TimeoutException {
AsyncRestTemplate restTemplate = new AsyncRestTemplate();
String baseUrl = "https://api.github.com/users/XXX";
HttpHeaders requestHeaders = new HttpHeaders();
requestHeaders.setAccept(Arrays.asList(MediaType.APPLICATION_JSON));
String value = "";
HttpEntity entity = new HttpEntity("parameters", requestHeaders);
final DeferredResult<String> result = new DeferredResult<>();
ListenableFuture<ResponseEntity<User>> futureEntity = restTemplate.getForEntity(baseUrl, User.class);
futureEntity.addCallback(new ListenableFutureCallback<ResponseEntity<User>>() {
#Override
public void onSuccess(ResponseEntity<User> result) {
System.out.println(result.getBody().getName());
result.setResult(result.getBody().getName());
}
#Override
public void onFailure(Throwable ex) {
result.setErrorResult(ex.getMessage());
}
});
return result;
}

I don't know too much about async calls in Spring but I would imagine that you could return the text that you want through the ResponseBody
It would look like this:
#GetMapping(path = "/testingAsync")
#ResponseBody
public String value() throws ExecutionException, InterruptedException, TimeoutException {
...
...
#Override
public void onSuccess(ResponseEntity<User> result) {
return result.getBody().getName();
}
...
}
Sorry if this isn't what you are asking about.

Related

Rest Template dependency not mocked in case of static mock block due to which exchange method returning null

I am trying to write junit for existing class which i can't modify but there are two method for which i need to write junit both are using restTemplate but for some reason rest template is not mocking if i mock it within a mock static context in junit .
But if i Mock globally then getRestTemplateInstance().exchange returning null. Could you please someone correct or point out problem so my test case can achieve 100% coverage
Class:
#Service
public class LocationRestTemplate {
private static final int READ_TIME_OUT = 5000;
private RestTemplate restTemplate;
#Value("${external-services.api}")
private String fallBackApiUrl;
RestTemplate getRestTemplateInstance() {
if (this.restTemplate == null) {
SimpleClientHttpRequestFactory factory = new SimpleClientHttpRequestFactory();
this.restTemplate = new RestTemplate(clientHttpRequestFactory());
restTemplate.setRequestFactory(factory);
List<HttpMessageConverter<?>> messageConverterList = restTemplate.getMessageConverters();
// Set HTTP Message converter using a JSON implementation.
MappingJackson2HttpMessageConverter jsonMessageConverter = new MappingJackson2HttpMessageConverter();
// Add supported media type
List<MediaType> supportedMediaTypes = new ArrayList<>();
supportedMediaTypes.add(new MediaType("text", "plain"));
supportedMediaTypes.add(new MediaType("application", "json"));
jsonMessageConverter.setSupportedMediaTypes(supportedMediaTypes);
messageConverterList.add(jsonMessageConverter);
restTemplate.setMessageConverters(messageConverterList);
}
return restTemplate;
}
public ExternalSkuInfo getSkuInfoFromExternalService(String store, List<String> skuList) {
java.net.URI builtUrl = null;
try {
UriComponentsBuilder builder = UriComponentsBuilder.fromHttpUrl(fallBackApiUrl + "/bays/store/")
.pathSegment(store).path("/sku").pathSegment(String.join(",", skuList));
builtUrl = builder.build().encode().toUri();
ResponseEntity<ExternalSkuInfo> response = getRestTemplateInstance().exchange(builtUrl, HttpMethod.GET,
getRequestEntity(), ExternalSkuInfo.class);
if (response.getStatusCode() == HttpStatus.OK) {
return response.getBody();
}
} catch (RestClientException e) {
} catch (Exception exception) {
}
return null;
}
private ClientHttpRequestFactory clientHttpRequestFactory() {
HttpComponentsClientHttpRequestFactory factory = new HttpComponentsClientHttpRequestFactory();
factory.setReadTimeout(READ_TIME_OUT);
factory.setConnectTimeout(READ_TIME_OUT);
return factory;
}
HttpEntity<Object> getRequestEntity() {
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Lists.newArrayList(MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN));
return new HttpEntity<>(headers);
}
}
Test Class
public class LocationRestTemplateTest {
#InjectMocks
private LocationRestTemplate cut;
#Before
public void setUp() {
MockitoAnnotations.openMocks(this);
}
#Test
public void testGetRestTemplateInstance() {
RestTemplate restTemplate = cut.getRestTemplateInstance();
Assert.assertNotNull(restTemplate);
}
#Test(expected = Exception.class)
public void testGetSkuInfoFromExternalService() {
RestTemplate restTemplate1 = Mockito.mock(RestTemplate.class);
try (MockedStatic<UriComponentsBuilder> uriComponentsBuilderMockedStatic = Mockito
.mockStatic(UriComponentsBuilder.class)) {
List<String> sku = new ArrayList<>();
sku.add("01234");
ReflectionTestUtils.setField(cut, "fallBackApiUrl", "test");
UriComponentsBuilder uriComponentsBuilder = Mockito.mock(UriComponentsBuilder.class);
UriComponents uriComponents = Mockito.mock(UriComponents.class);
URI uri = Mockito.mock(URI.class);
RestTemplate restTemplate = Mockito.mock(RestTemplate.class);
ResponseEntity responseEntity = Mockito.mock(ResponseEntity.class);
uriComponentsBuilderMockedStatic.when(() -> UriComponentsBuilder.fromHttpUrl("test/bays/store/"))
.thenReturn(uriComponentsBuilder);
Mockito.doReturn(uriComponentsBuilder).when(uriComponentsBuilder).pathSegment("0123");
Mockito.doReturn(uriComponentsBuilder).when(uriComponentsBuilder).path("/sku");
Mockito.doReturn(uriComponentsBuilder).when(uriComponentsBuilder).pathSegment(String.join(",", sku));
Mockito.doReturn(uriComponents).when(uriComponentsBuilder).build();
Mockito.doReturn(uriComponents).when(uriComponents).encode();
Mockito.doReturn(uri).when(uriComponents).toUri();
Mockito.doReturn(responseEntity).when(restTemplate)
.exchange(uri, HttpMethod.GET, getRequestEntity(), ExternalSkuInfo.class);
cut.getSkuInfoFromExternalService("0123", sku);
}
}
HttpEntity<Object> getRequestEntity() {
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Lists.newArrayList(MediaType.APPLICATION_JSON, MediaType.TEXT_PLAIN));
return new HttpEntity<>(headers);
}
}

Spring Boot + React CORS issue without spring security

I'm using Spring Boot 2.2.2.RELEASE as a REST service and React for front-end.
Just a simple GET method implemented, But getting CORS issue when communicating to the server via REACT.
https://spring.io/guides/gs/rest-service-cors/ -> followed this link, but no luck.
my Spring Boot Controller:
#RestController
public class FeedController {
#Autowired
private IFeedService IFeedService;
#CrossOrigin(origins = "http://localhost:3000")
#GetMapping(path="/v1/getdashboard")
public ResponseEntity<String> feedDashBoardController(){
String result = null;
HttpStatus httpStatus = HttpStatus.BAD_REQUEST;
try {
List<FeedData> dashBoardFeedInfo = IFeedService.getDashBoardFeedService();
// Create ObjectMapper
ObjectMapper mapper = new ObjectMapper();
JsonNode dataNode = mapper.valueToTree(dashBoardFeedInfo);
result = FeedResponseData.generateFeedResponse(dataNode);
httpStatus = HttpStatus.OK;
}catch(TBServiceException e) {
result = AppExceptions.handleException("Something Went Wrong");
httpStatus = HttpStatus.BAD_REQUEST;
}
return new ResponseEntity<String>(result,httpStatus);
}
}
my Spring Boot Application:
#SpringBootApplication
public class TechnicalBlogApplication {
public static void main(String[] args) {
SpringApplication.run(TechnicalBlogApplication.class, args);
System.out.println("Application Main - Update -1");
}
#Bean
public WebMvcConfigurer corsConfigurer() {
return new WebMvcConfigurer() {
#Override
public void addCorsMappings(CorsRegistry registry) {
registry.addMapping("/v1/getdashboard").allowedOrigins("http://localhost:3000");
}
};
}
}
my Spring application properties:
spring.profiles.active=dev
server.port=6001
server.servlet.context-path=/technical-blog
my React Code Snippet:
async componentDidMount() {
const dashboardData= await fetch("http://localhost:6001/technical-blog/v1/getdashboard");
console.log("dash ",dashboardData)
}
I have also tried setting the headers, below is the re-modified controller. i got multi CORS definition error.
#RestController
public class FeedController {
#Autowired
private IFeedService IFeedService;
#CrossOrigin(origins = "http://localhost:3000")
#GetMapping(path="/v1/getdashboard")
public ResponseEntity<String> feedDashBoardController(){
String result = null;
HttpStatus httpStatus = HttpStatus.BAD_REQUEST;
try {
List<FeedData> dashBoardFeedInfo = IFeedService.getDashBoardFeedService();
// Create ObjectMapper
ObjectMapper mapper = new ObjectMapper();
JsonNode dataNode = mapper.valueToTree(dashBoardFeedInfo);
result = FeedResponseData.generateFeedResponse(dataNode);
httpStatus = HttpStatus.OK;
}catch(TBServiceException e) {
result = AppExceptions.handleException("Something Went Wrong");
httpStatus = HttpStatus.BAD_REQUEST;
}
return new ResponseEntity<String>(result,setHeaders(),httpStatus);
}
}
private HttpHeaders setHeaders() {
List<HttpMethod> allowedMethods = new ArrayList<>();
allowedMethods.add(HttpMethod.GET);
allowedMethods.add(HttpMethod.POST);
HttpHeaders httpHeaders = new HttpHeaders();
httpHeaders.setContentType(MediaType.APPLICATION_JSON);
//httpHeaders.setAccessControlAllowOrigin("*");
httpHeaders.setAccessControlAllowCredentials(true);
httpHeaders.setAccessControlAllowMethods(allowedMethods);
httpHeaders.setAccessControlMaxAge(3600);
return httpHeaders;
}
i think you should put #CrossOrigin(origins = "http://localhost:3000") on the controller it self because the first thing that request goes to is the controller not the function
so it will be like that
#RestController
#CrossOrigin(origins = "http://localhost:3000")
public class FeedController {
#Autowired
private IFeedService IFeedService;
#GetMapping(path="/v1/getdashboard")
public ResponseEntity<String> feedDashBoardController(){
String result = null;
HttpStatus httpStatus = HttpStatus.BAD_REQUEST;
try {
List<FeedData> dashBoardFeedInfo = IFeedService.getDashBoardFeedService();
// Create ObjectMapper
ObjectMapper mapper = new ObjectMapper();
JsonNode dataNode = mapper.valueToTree(dashBoardFeedInfo);
result = FeedResponseData.generateFeedResponse(dataNode);
httpStatus = HttpStatus.OK;
}catch(TBServiceException e) {
result = AppExceptions.handleException("Something Went Wrong");
httpStatus = HttpStatus.BAD_REQUEST;
}
return new ResponseEntity<String>(result,setHeaders(),httpStatus);
}
}

Why is this test junit test returning a 400?

I have a controller that looks like:
#PostMapping(path = "/email", consumes = "application/json", produces = "application/json")
public String notification(#RequestBody EmailNotificationRequest emailNotificationRequest) throws IOException {
String jobId = emailNotificationRequest.getJobId();
try {
service.jobId(jobId);
return jobId;
} catch (ApplicationException e) {
return "failed to send email to for jobId: " + jobId;
}
}
And I am trying to test the controller but getting a 400 back:
#Before
public void setUp() {
this.mvc = MockMvcBuilders.standaloneSetup(emailNotificationController).build();
}
#Test
public void successfulServiceCallShouldReturn200() throws Exception {
String request = "{\"jobId\" : \"testId\"}";
MvcResult result = mvc.perform(post("/email")
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andExpect(content().json(request))
.andReturn();
String content = result.getResponse().getContentAsString();
assertThat(content, isNotNull());
}
Now I realize that 400 means the request is bad. So I tried making my own request and then turning it to a JSON string like so:
#Test
public void successfulServiceCallShouldReturn200() throws Exception {
EmailNotificationRequest emailNotificationRequest = new emailNotificationRequest();
emailNotificationRequest.setJobId("testJobId");
MvcResult result = mvc.perform(post("/notification/email")
.content(asJsonString(emailNotificationRequest))
.contentType(MediaType.APPLICATION_JSON))
.andExpect(status().isOk())
.andReturn();
assertThat(result, isNotNull());
}
public static String asJsonString(final Object obj) {
try {
final ObjectMapper mapper = new ObjectMapper();
mapper.setVisibility(PropertyAccessor.FIELD, JsonAutoDetect.Visibility.ANY);
final String jsonContent = mapper.writeValueAsString(obj);
return jsonContent;
} catch (Exception e) {
throw new RuntimeException(e);
}
}
I think it has something to do with the content() since i am getting 400 and that has to do with the actual request. Can someone please tell me why the request is still bad here? Or a better way to test this particular POST method? Thanks in advance.
You have to add accept("application/json")).
If not the mock does not accept this content type.

Produce Different type of response from spring boot rest controller

I have a scenario to create a rest method which return json response if there is any validation fail. and if everything is correct then download a pdf method.
Is it possible to be done via same method?
PFB the method signature what i need to achieve.
#RequestMapping(value = "/getPdf", method = RequestMethod.POST, consumes = MediaType.ALL_VALUE, produces = {"application/pdf","application/json"})
public #ResponseBody LinkedHashMap<String, Object> getPdf(#Valid #RequestBody DownloadPdfDTO downloadPdfDTO,BindingResult result,HttpServletRequest request,HttpServletResponse response) throws IOException {
}
This Method works fine when everything correct and PDF is getting download.
But when there is any validation fail then no response is getting render and i am getting Status : 406 Not Acceptable.
Thanks
Yes u can dot it using global exception handler.
#Order(Ordered.HIGHEST_PRECEDENCE)
#ControllerAdvice
public class GlobalExceptionHandler {
ObjectMapper objectMapper = new ObjectMapper();
#ExceptionHandler(AccessDeniedException.class)
public void handleAccessDenied(HttpServletResponse response, AccessDeniedException ex) {
response.setStatus(HttpStatus.UNAUTHORIZED);
try (PrintWriter writer = response.getWriter()) {
objectMapper.writeValue(writer, YOUR_OBJECT_TO_RETURN);
} catch (IOException ex) {
}
}
#ExceptionHandler({MethodArgumentNotValidException.class, ConstraintViolationException.class})
public void handleException(HttpServletResponse response, Exception ex) {
response.setStatus(HttpStatus.UNPROCESSABLE_ENTITY);
try (PrintWriter writer = response.getWriter()) {
objectMapper.writeValue(writer, YOUR_OBJECT_TO_RETURN);
} catch (IOException ex) {
}
}
#ExceptionHandler(NullPointerException.class)
public void handleNullPointerException(HttpServletResponse response, Exception ex) {
//return your required response
}
}
You can give a return type as ResponseEntity. With ResponseEntity you can bind different response as per your business logic.
An example will be like this
#ApiOperation(value = "Get PDF API", consumes = "application/json", httpMethod = "GET")
#ApiResponses(value = {#ApiResponse(code = 404, message = "Bad request")})
#RequestMapping(value = "/pdf/{filename}", headers = "Accept=application/json", method = RequestMethod.GET)
public
#ResponseBody
ResponseEntity getPDF(#ApiParam(value = "filename", required = true) #PathVariable String filename) {
try {
Boolean validationPass = checkValidation();
if (validationPass) {
resp = getPDF();
responseHeaders.setContentType(MediaType.APPLICATION_PDF);
responseHeaders.set("charset", "utf-8");
responseHeaders.setContentLength(resp.length);
responseHeaders.set("Content-disposition", "attachment; filename=response.pdf");
return new ResponseEntity<>(resp,responseHeaders, HttpStatus.OK);
}
else {
// create your json validation eneity here
return new ResponseEntity<>(validation entity, HttpStatus.BAD_REQUEST);
}
} catch (Exception e) {
return new ResponseEntity<>( createExceptionEntity(e) ,HttpStatus.BAD_REQUEST);
}
}

Reading httprequest content from spring exception handler

I Am using Spring's #ExceptionHandler annotation to catch exceptions in my controllers.
Some requests hold POST data as plain XML string written to the request body, I want to read that data in order to log the exception.
The problem is that when i request the inputstream in the exception handler and try to read from it the stream returns -1 (empty).
The exception handler signature is:
#ExceptionHandler(Throwable.class)
public ModelAndView exception(HttpServletRequest request, HttpServletResponse response, HttpSession session, Throwable arff)
Any thoughts? Is there a way to access the request body?
My controller:
#Controller
#RequestMapping("/user/**")
public class UserController {
static final Logger LOG = LoggerFactory.getLogger(UserController.class);
#Autowired
IUserService userService;
#RequestMapping("/user")
public ModelAndView getCurrent() {
return new ModelAndView("user","response", userService.getCurrent());
}
#RequestMapping("/user/firstLogin")
public ModelAndView firstLogin(HttpSession session) {
userService.logUser(session.getId());
userService.setOriginalAuthority();
return new ModelAndView("user","response", userService.getCurrent());
}
#RequestMapping("/user/login/failure")
public ModelAndView loginFailed() {
LOG.debug("loginFailed()");
Status status = new Status(-1,"Bad login");
return new ModelAndView("/user/login/failure", "response",status);
}
#RequestMapping("/user/login/unauthorized")
public ModelAndView unauthorized() {
LOG.debug("unauthorized()");
Status status = new Status(-1,"Unauthorized.Please login first.");
return new ModelAndView("/user/login/unauthorized","response",status);
}
#RequestMapping("/user/logout/success")
public ModelAndView logoutSuccess() {
LOG.debug("logout()");
Status status = new Status(0,"Successful logout");
return new ModelAndView("/user/logout/success", "response",status);
}
#RequestMapping(value = "/user/{id}", method = RequestMethod.POST)
public ModelAndView create(#RequestBody UserDTO userDTO, #PathVariable("id") Long id) {
return new ModelAndView("user", "response", userService.create(userDTO, id));
}
#RequestMapping(value = "/user/{id}", method = RequestMethod.GET)
public ModelAndView getUserById(#PathVariable("id") Long id) {
return new ModelAndView("user", "response", userService.getUserById(id));
}
#RequestMapping(value = "/user/update/{id}", method = RequestMethod.POST)
public ModelAndView update(#RequestBody UserDTO userDTO, #PathVariable("id") Long id) {
return new ModelAndView("user", "response", userService.update(userDTO, id));
}
#RequestMapping(value = "/user/all", method = RequestMethod.GET)
public ModelAndView list() {
return new ModelAndView("user", "response", userService.list());
}
#RequestMapping(value = "/user/allowedAccounts", method = RequestMethod.GET)
public ModelAndView getAllowedAccounts() {
return new ModelAndView("user", "response", userService.getAllowedAccounts());
}
#RequestMapping(value = "/user/changeAccount/{accountId}", method = RequestMethod.GET)
public ModelAndView changeAccount(#PathVariable("accountId") Long accountId) {
Status st = userService.changeAccount(accountId);
if (st.code != -1) {
return getCurrent();
}
else {
return new ModelAndView("user", "response", st);
}
}
/*
#RequestMapping(value = "/user/logout", method = RequestMethod.GET)
public void perLogout(HttpServletRequest request, HttpServletResponse response) throws ServletException, IOException {
userService.setOriginalAuthority();
response.sendRedirect("/marketplace/user/logout/spring");
}
*/
#ExceptionHandler(Throwable.class)
public ModelAndView exception(HttpServletRequest request, HttpServletResponse response, HttpSession session, Throwable arff) {
Status st = new Status();
try {
Writer writer = new StringWriter();
byte[] buffer = new byte[1024];
//Reader reader2 = new BufferedReader(new InputStreamReader(request.getInputStream()));
InputStream reader = request.getInputStream();
int n;
while ((n = reader.read(buffer)) != -1) {
writer.toString();
}
String retval = writer.toString();
retval = "";
} catch (IOException e) {
e.printStackTrace();
}
return new ModelAndView("profile", "response", st);
}
}
Thank you
I've tried your code and I've found some mistakes in the exception handler, when you read from the InputStream:
Writer writer = new StringWriter();
byte[] buffer = new byte[1024];
//Reader reader2 = new BufferedReader(new InputStreamReader(request.getInputStream()));
InputStream reader = request.getInputStream();
int n;
while ((n = reader.read(buffer)) != -1) {
writer.toString();
}
String retval = writer.toString();
retval = "";
I've replaced your code with this one:
BufferedReader reader = new BufferedReader(new InputStreamReader(request.getInputStream()));
String line = "";
StringBuilder stringBuilder = new StringBuilder();
while ( (line=reader.readLine()) != null ) {
stringBuilder.append(line).append("\n");
}
String retval = stringBuilder.toString();
Then I'm able to read from InputStream in the exception handler, it works!
If you can't still read from InputStream, I suggest you to check how you POST xml data to the request body.
You should consider that you can consume the Inputstream only one time per request, so I suggest you to check that there isn't any other call to getInputStream(). If you have to call it two or more times you should write a custom HttpServletRequestWrapper like this to make a copy of the request body, so you can read it more times.
UPDATE
Your comments has helped me to reproduce the issue. You use the annotation #RequestBody, so it's true that you don't call getInputStream(), but Spring invokes it to retrieve the request's body. Have a look at the class org.springframework.web.bind.annotation.support.HandlerMethodInvoker: if you use #RequestBody this class invokes resolveRequestBody method, and so on... finally you can't read anymore the InputStream from your ServletRequest. If you still want to use both #RequestBody and getInputStream() in your own method, you have to wrap the request to a custom HttpServletRequestWrapper to make a copy of the request body, so you can manually read it more times.
This is my wrapper:
public class CustomHttpServletRequestWrapper extends HttpServletRequestWrapper {
private static final Logger logger = Logger.getLogger(CustomHttpServletRequestWrapper.class);
private final String body;
public CustomHttpServletRequestWrapper(HttpServletRequest request) {
super(request);
StringBuilder stringBuilder = new StringBuilder();
BufferedReader bufferedReader = null;
try {
InputStream inputStream = request.getInputStream();
if (inputStream != null) {
bufferedReader = new BufferedReader(new InputStreamReader(inputStream));
String line = "";
while ((line = bufferedReader.readLine()) != null) {
stringBuilder.append(line).append("\n");
}
} else {
stringBuilder.append("");
}
} catch (IOException ex) {
logger.error("Error reading the request body...");
} finally {
if (bufferedReader != null) {
try {
bufferedReader.close();
} catch (IOException ex) {
logger.error("Error closing bufferedReader...");
}
}
}
body = stringBuilder.toString();
}
#Override
public ServletInputStream getInputStream() throws IOException {
final StringReader reader = new StringReader(body);
ServletInputStream inputStream = new ServletInputStream() {
public int read() throws IOException {
return reader.read();
}
};
return inputStream;
}
}
Then you should write a simple Filter to wrap the request:
public class MyFilter implements Filter {
public void init(FilterConfig fc) throws ServletException {
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
chain.doFilter(new CustomHttpServletRequestWrapper((HttpServletRequest)request), response);
}
public void destroy() {
}
}
Finally, you have to configure your filter in your web.xml:
<filter>
<filter-name>MyFilter</filter-name>
<filter-class>test.MyFilter</filter-class>
</filter>
<filter-mapping>
<filter-name>MyFilter</filter-name>
<url-pattern>/*</url-pattern>
</filter-mapping>
You can fire your filter only for controllers that really needs it, so you should change the url-pattern according to your needs.
If you need this feature in only one controller, you can also make a copy of the request body in that controller when you receive it through the #RequestBody annotation.
Recently I faced this issue and solved it slightly differently. With spring boot 1.3.5.RELEASE
The filter was implemented using the Spring class ContentCachingRequestWrapper. This wrapper has a method getContentAsByteArray() which can be invoked multiple times.
import org.springframework.web.util.ContentCachingRequestWrapper;
public class RequestBodyCachingFilter implements Filter {
public void init(FilterConfig fc) throws ServletException {
}
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
chain.doFilter(new ContentCachingRequestWrapper((HttpServletRequest)request), response);
}
public void destroy() {
}
}
Added the filter to the chain
#Bean
public RequestBodyCachingFilter requestBodyCachingFilter() {
log.debug("Registering Request Body Caching filter");
return new RequestBodyCachingFilter();
}
In the Exception Handler.
#ControllerAdvice(annotations = RestController.class)
public class GlobalExceptionHandlingControllerAdvice {
private ContentCachingRequestWrapper getUnderlyingCachingRequest(ServletRequest request) {
if (ContentCachingRequestWrapper.class.isAssignableFrom(request.getClass())) {
return (ContentCachingRequestWrapper) request;
}
if (request instanceof ServletRequestWrapper) {
return getUnderlyingCachingRequest(((ServletRequestWrapper)request).getRequest());
}
return null;
}
#ResponseStatus(value = HttpStatus.INTERNAL_SERVER_ERROR)
#ExceptionHandler(Throwable.class)
public #ResponseBody Map<String, String> conflict(Throwable exception, HttpServletRequest request) {
ContentCachingRequestWrapper underlyingCachingRequest = getUnderlyingCachingRequest(request);
String body = new String(underlyingCachingRequest.getContentAsByteArray(),Charsets.UTF_8);
....
}
}
I had the same problem and solved it with HttpServletRequestWrapper as described above and it worked great. But then, I found another solution with extending HttpMessageConverter, in my case that was MappingJackson2HttpMessageConverter.
public class CustomJsonHttpMessageConverter extends MappingJackson2HttpMessageConverter{
public static final String REQUEST_BODY_ATTRIBUTE_NAME = "key.to.requestBody";
#Override
public Object read(Type type, Class<?> contextClass, final HttpInputMessage inputMessage) throws IOException, HttpMessageNotReadableException {
final ByteArrayOutputStream writerStream = new ByteArrayOutputStream();
HttpInputMessage message = new HttpInputMessage() {
#Override
public HttpHeaders getHeaders() {
return inputMessage.getHeaders();
}
#Override
public InputStream getBody() throws IOException {
return new TeeInputStream(inputMessage.getBody(), writerStream);
}
};
RequestContextHolder.getRequestAttributes().setAttribute(REQUEST_BODY_ATTRIBUTE_NAME, writerStream, RequestAttributes.SCOPE_REQUEST);
return super.read(type, contextClass, message);
}
}
com.sun.xml.internal.messaging.saaj.util.TeeInputStream is used.
In spring mvc config
<mvc:annotation-driven >
<mvc:message-converters>
<bean class="com.company.remote.rest.util.CustomJsonHttpMessageConverter" />
</mvc:message-converters>
</mvc:annotation-driven>
In #ExceptionHandler method
#ExceptionHandler(Exception.class)
public ResponseEntity<RestError> handleException(Exception e, HttpServletRequest httpRequest) {
RestError error = new RestError();
error.setErrorCode(ErrorCodes.UNKNOWN_ERROR.getErrorCode());
error.setDescription(ErrorCodes.UNKNOWN_ERROR.getDescription());
error.setDescription(e.getMessage());
logRestException(httpRequest, e);
ResponseEntity<RestError> responseEntity = new ResponseEntity<RestError>(error,HttpStatus.INTERNAL_SERVER_ERROR);
return responseEntity;
}
private void logRestException(HttpServletRequest request, Exception ex) {
StringWriter sb = new StringWriter();
sb.append("Rest Error \n");
sb.append("\nRequest Path");
sb.append("\n----------------------------------------------------------------\n");
sb.append(request.getRequestURL());
sb.append("\n----------------------------------------------------------------\n");
Object requestBody = request.getAttribute(CustomJsonHttpMessageConverter.REQUEST_BODY_ATTRIBUTE_NAME);
if(requestBody != null) {
sb.append("\nRequest Body\n");
sb.append("----------------------------------------------------------------\n");
sb.append(requestBody.toString());
sb.append("\n----------------------------------------------------------------\n");
}
LOG.error(sb.toString());
}
I hope it helps :)

Categories

Resources