I would like to read POST data from a Spring Boot controller.
I have tried all the solutions given here: HttpServletRequest get JSON POST data, but I still am unable to read post data in a Spring Boot servlet.
My code is here:
package com.testmockmvc.testrequest.controller;
import org.apache.commons.io.IOUtils;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.ResponseBody;
import javax.servlet.http.HttpServletRequest;
import java.io.IOException;
import java.nio.charset.StandardCharsets;
#Controller
public class TestRequestController {
#RequestMapping(path = "/testrequest")
#ResponseBody
public String testGetRequest(HttpServletRequest request) throws IOException {
final byte[] requestContent;
requestContent = IOUtils.toByteArray(request.getReader());
return new String(requestContent, StandardCharsets.UTF_8);
}
}
I have tried using the Collectors as an alternative, and that does not work either. What am I doing wrong?
First, you need to define the RequestMethod as POST.
Second, you can define a #RequestBody annotation in the String parameter
#Controller
public class TestRequestController {
#RequestMapping(path = "/testrequest", method = RequestMethod.POST)
public String testGetRequest(#RequestBody String request) throws IOException {
final byte[] requestContent;
requestContent = IOUtils.toByteArray(request.getReader());
return new String(requestContent, StandardCharsets.UTF_8);
}
}
Related
I'm working with two Spring Boot applications, let's call them ServiceA and ServiceB, both exposing a REST API.
ServiceA is called by end users from the browser via a frontend app (we use #RestController classes). On some calls, ServiceA has to call ServiceB (using RestTemplate). We've got authentication and authorization sorted out for our target environment, but for testing locally we are relying on Basic Auth instead, and that's where we're hitting a snag: we would like ServiceA to re-use the Basic Auth credentials the user provided when calling Service B.
Is there an easy way to pass the Basic Auth credentials used on the call to our REST controller to the RestTemplate call?
Quick and dirty solution
The easiest way to do this would be:
import org.springframework.http.HttpEntity;
import org.springframework.http.HttpHeaders;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
#RestController
class SomeController {
private final RestTemplate restTemplate = new RestTemplate();
#PostMapping("/delegate/call")
public void callOtherService(#RequestHeader(HttpHeaders.AUTHORIZATION) String authorization) {
HttpHeaders headers = new HttpHeaders();
headers.add(HttpHeaders.AUTHORIZATION, authorization);
restTemplate.postForEntity("other-service.com/actual/call", new HttpEntity<Void>(null, headers), Void.class);
// handling the response etc...
}
}
Using interceptors and RestTemplateCustomizer
I didn't want to change to add an extra parameter on each controller method, and I wanted a way to enable or disable this behavior depending on the environment, so here is a slightly more complicated solution that can be enabled using Spring profiles, and doesn't touch the controllers:
import org.springframework.http.HttpHeaders;
import org.springframework.http.HttpRequest;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.web.servlet.HandlerInterceptor;
import org.springframework.web.servlet.ModelAndView;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import java.io.IOException;
public class BasicAuthPropagationInterceptor implements HandlerInterceptor, ClientHttpRequestInterceptor {
private final ThreadLocal<String> cachedHeader = new ThreadLocal<>();
#Override
public boolean preHandle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
final String header = request.getHeader(HttpHeaders.AUTHORIZATION);
cachedHeader.set(header);
return true;
}
#Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView modelAndView) throws Exception {
cachedHeader.remove();
}
#Override
public ClientHttpResponse intercept(HttpRequest request, byte[] body, ClientHttpRequestExecution execution) throws IOException {
String ch = cachedHeader.get();
if (!request.getHeaders().containsKey(HttpHeaders.AUTHORIZATION) && ch != null) {
request.getHeaders().add(HttpHeaders.AUTHORIZATION, ch);
}
return execution.execute(request, body);
}
}
This stores the received header in a ThreadLocal and adds it with an interceptor for RestTemplate.
This can then be configured as such:
import org.springframework.boot.web.client.RestTemplateCustomizer;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.Profile;
import org.springframework.web.servlet.config.annotation.InterceptorRegistry;
import org.springframework.web.servlet.config.annotation.WebMvcConfigurer;
#Configuration
#Profile("LOCAL")
class LocalConfiguration implements WebMvcConfigurer {
private final BasicAuthPropagationInterceptor basicAuthPropagationInterceptor
= new BasicAuthPropagationInterceptor();
#Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(basicAuthPropagationInterceptor);
}
#Bean
RestTemplateCustomizer restTemplateCustomizer() {
return restTemplate -> restTemplate.getInterceptors().add(basicAuthPropagationInterceptor);
}
}
RestTemplate obtained by using the default RestTemplateBuilder bean will then automatically set the Authorization HTTP header if it's available in the current thread.
I have this request and response:
#Data
public class TestRequestDto {
#Min(7)
private String name;
}
#Data
public class TestResponseDto {
private String response;
}
And I have a controller:
package com.example.validation.demo;
import lombok.extern.slf4j.Slf4j;
import org.springframework.web.bind.annotation.PostMapping;
import org.springframework.web.bind.annotation.RequestBody;
import org.springframework.web.bind.annotation.RestController;
import javax.validation.Valid;
#Slf4j
#RestController
public class TetController {
#PostMapping("/test")
public TestResponseDto getTestResponseDto(#Valid #RequestBody TestRequestDto request){
log.info(request.getName());
TestResponseDto response = new TestResponseDto();
response.setResponse("response");
return response;
}
}
I send a post request({"name":"test"}) with an invalid name but it works. What am I doing wrong?
Starting with Boot 2.3, we also need to explicitly add the spring-boot-starter-validation dependency
Hello I am having some trouble in my Controller class. All requests work fine except for this one which for some reason refuses to redirect me to another page even though I am doing everything fine as far as I know.
Here is my Controller class.
The problem is with the beginReservation endpoint
package movie_manager.web.presentation;
import movie_manager.model.dto.MovieDto;
import movie_manager.model.dto.ReservationDto;
import movie_manager.model.dto.UserDto;
import movie_manager.model.pojo.FormPojo;
import movie_manager.model.pojo.ReservationPojo;
import movie_manager.service.MovieService;
import movie_manager.service.ReservationService;
import movie_manager.service.UserService;
import movie_manager.web.session.SessionInfo;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.*;
import org.springframework.web.servlet.ModelAndView;
import javax.validation.Valid;
import java.io.IOException;
import java.util.ArrayList;
import java.util.Collection;
import java.util.List;
#RestController
#RequestMapping("/user")
public class UserController {
#Autowired
private SessionInfo sessionInfo;
#Autowired
private UserService userService;
#Autowired
private MovieService movieService;
#Autowired
private ReservationService reservationService;
#GetMapping
public ModelAndView userPage(){
ModelAndView modelAndView = new ModelAndView("user");
Collection<MovieDto> movies = movieService.getAll();
modelAndView.addObject("movies", movies);
return modelAndView;
}
#GetMapping("view_movie/{movieId}")
public ModelAndView viewMovie(#Valid #PathVariable long movieId){
ModelAndView modelAndView = new ModelAndView("view_movie");
modelAndView.addObject("movie", movieService.get(movieId));
return modelAndView;
}
#PostMapping("/make_reservation")
public void makeReservation(#Valid #RequestBody FormPojo formPojo){
MovieDto movieDto = movieService.get(formPojo.movieId);
UserDto userDto = sessionInfo.getUser();
ReservationDto reservationDto = new ReservationDto();
reservationDto.movieDto = movieDto;
reservationDto.userDto = userDto;
reservationDto.userName = formPojo.userName;
reservationService.add(reservationDto);
}
#GetMapping("/begin_reservation")
public ModelAndView beginReservation(#Valid #RequestBody ReservationPojo reservationPojo) {
ModelAndView modelAndView = new ModelAndView("reservation");
MovieDto movieDto = movieService.get(reservationPojo.movieId);
if(movieDto.availableSeats < reservationPojo.numberOfSeats)
return new ModelAndView("notEnoughSeats");
modelAndView.addObject("movie", movieDto);
List<Integer> list = new ArrayList<>();
for (int i = 0; i< reservationPojo.numberOfSeats;i++){
list.add(i);
}
modelAndView.addObject("numberOfForms", list);
return modelAndView;
}
}
All other endpoints work just fine. And the problematic endpoint is reached successfully. The issue seems to be with the ModelAndView object. The page just stays static no errors no nothing. I would also note that no path seems to work(even those used above in other endpoints).
Any help would be appreciated.
#RestController is not meant to be used to return views to be resolved. It is supposed to return data which will be written to the body of the response, hence the inclusion of #ResponseBody. You can not selectively disable the #ResponseBody on individual handler methods when #ResponseBody is already annotation on class level.
Original answer here Returning view from Spring MVC #RestController.
We are doing an architectural refactoring to convert a monolithic J2EE EJB application to Spring services. In order to do that I'm creating services by breaking the application against the joints of its domain. Currently, I have three of them and each calls another service via Rest.
In this project our ultimate purpose is transforming the application to microservices, but since cloud infrastructure isn't clear and probably won't be possible, we decided to make it this way and thought that since services using Rest, it will be easy to make the transform in future.
Does our approach makes sense? My question stems from this.
I send a request to UserService with a header parameter, userName from Postman.
GET http://localhost:8087/users/userId?userName=12345
UserService calls another service which calls another. Rest call order between services is this:
UserService ---REST--> CustomerService ---REST--> AlarmService
Since I'm doing the work of carrying the common request parameters like this right now, I need to set common header parameters in every method that making Rest requests by taking them from incoming request to outgoing request:
#RequestMapping(value="/users/userId", method = RequestMethod.GET)
public ResponseEntity<Long> getUserId(#RequestHeader("userName") String userName) {
...
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Collections.singletonList
(MediaType.APPLICATION_JSON));
headers.set("userName", userName);
HttpEntity<String> entity = new HttpEntity<>("parameters", headers);
HttpEntity<Long> response =
restTemplate.exchange(CUSTOMER_REST_SERVICE_URI,
HttpMethod.GET, entity, Long.class);
...
}
UserService:
package com.xxx.userservice.impl;
import org.apache.log4j.Logger;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.http.*;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.RestController;
import org.springframework.web.client.RestTemplate;
import java.util.Collections;
import java.util.Map;
#RestController
public class UserController extends AbstractService{
Logger logger = Logger.getLogger(UserController.class.getName());
#Autowired
private RestTemplate restTemplate;
private final String CUSTOMER_REST_SERVICE_HOST = "http://localhost:8085";
private final String CUSTOMER_REST_SERVICE_URI = CUSTOMER_REST_SERVICE_HOST + "/customers/userId";
#RequestMapping(value="/users/userId", method = RequestMethod.GET)
public ResponseEntity<Long> getUserId(#RequestHeader("userName") String userName) {
logger.info(""user service is calling customer service..."");
try {
//do the internal customer service logic
//call other service.
HttpHeaders headers = new HttpHeaders();
headers.setAccept(Collections.singletonList
(MediaType.APPLICATION_JSON));
headers.set("userName", userName);
HttpEntity<String> entity = new HttpEntity<>("parameters", headers);
HttpEntity<Long> response =
restTemplate.exchange(CUSTOMER_REST_SERVICE_URI,
HttpMethod.GET, entity, Long.class);
return ResponseEntity.ok(response.getBody());
} catch (Exception e) {
logger.error("user service could not call customer service: ", e);
throw new RuntimeException(e);
}
finally {
logger.info("customer service called...");
}
}
}
CustomerService:
package com.xxxx.customerservice.impl;
import java.rmi.RemoteException;
import java.util.ArrayList;
import java.util.Collections;
import java.util.List;
import com.xxx.interf.CustomerService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestHeader;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
#RestController
public class CustomerController extends AbstractService{
private final String ALARM_REST_SERVICE_HOST = "http://localhost:8086";
private final String ALARM_REST_SERVICE_URI = ALARM_REST_SERVICE_HOST + "/alarms/maxAlarmCount";
#Autowired
private CustomerService customerService;
#Autowired
private RestTemplate restTemplate;
...
#GetMapping(path="/customers/userId", produces = "application/json")
public long getUserId(#RequestHeader(value="Accept") String acceptType) throws RemoteException {
//customer service internal logic.
customerService.getUserId();
//customer service calling alarm service.
return restTemplate.getForObject(ALARM_REST_SERVICE_URI, Long.class);
}
}
AlarmService:
package com.xxx.alarmservice.impl;
import com.xxx.interf.AlarmService;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;
#RestController
public class PriceAlarmController extends AbstractService{
#Autowired
private AlarmService priceAlarmService;
#RequestMapping("/alarms/maxAlarmCount")
public long getMaxAlarmsPerUser() {
// alarm service internal logic.
return priceAlarmService.getMaxAlarmsPerUser();
}
}
I have tried these config and interceptor files but i can use them just for logging and can't transfer header parameters by using them. Probably because each service has them. And also, this interceptor only works in UserService which first uses RestTemplate to send request. Called service and first request which is coming from Postman doesn't work with it because they doesn't print any log message like UserService does.
CommonModule:
package com.xxx.common.config;
import com.xxx.common.util.HeaderRequestInterceptor;
import org.apache.cxf.common.util.CollectionUtils;
import org.springframework.context.annotation.Bean;
import org.springframework.context.annotation.Configuration;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.web.client.RestTemplate;
import java.util.ArrayList;
import java.util.List;
#Configuration
public class RestTemplateConfig {
#Bean
public RestTemplate restTemplate() {
RestTemplate restTemplate = new RestTemplate();
List<ClientHttpRequestInterceptor> interceptors
= restTemplate.getInterceptors();
if (CollectionUtils.isEmpty(interceptors)) {
interceptors = new ArrayList<>();
}
interceptors.add(new HeaderRequestInterceptor());
restTemplate.setInterceptors(interceptors);
return restTemplate;
}
}
ClientHttpRequestInterceptor:
package com.xxx.common.util;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.http.HttpRequest;
import org.springframework.http.MediaType;
import org.springframework.http.client.ClientHttpRequestExecution;
import org.springframework.http.client.ClientHttpRequestInterceptor;
import org.springframework.http.client.ClientHttpResponse;
import org.springframework.util.StreamUtils;
import java.io.IOException;
import java.nio.charset.Charset;
public class HeaderRequestInterceptor implements ClientHttpRequestInterceptor {
private final Logger log = LoggerFactory.getLogger(this.getClass());
#Override
public ClientHttpResponse intercept(
HttpRequest request,
byte[] body,
ClientHttpRequestExecution execution) throws IOException
{
log.info("HeaderRequestInterceptor....");
logRequest(request, body);
request.getHeaders().set("Accept", MediaType.APPLICATION_JSON_VALUE);
ClientHttpResponse response = execution.execute(request, body);
logResponse(response);
return response;
}
private void logRequest(HttpRequest request, byte[] body) throws IOException
{
log.info("==========request begin=======================");
}
private void logResponse(ClientHttpResponse response) throws IOException
{
log.info("==========response begin=============");
}
}
How can I manage the passing of common header information like userName by using some kind of interceptors or other mechanism in single place?
In your HeaderRequestInterceptor's intercept method, you can access the current http request and its headers (userId in your case) in the following way:
#Override
public ClientHttpResponse intercept(HttpRequest request..
...
HttpServletRequest httpServletRequest = ((ServletRequestAttributes) RequestContextHolder.currentRequestAttributes()).getRequest();
String userId = httpServletRequest.getHeader("userId");
request.getHeaders().set("userId", userId);
I am studying Spring OAuth by decomposing this set of three interconnected apps at GitHub, while also carefully studying the Spring OAuth 2 Developer Guide at this link. The Developer Guide says that the /oauth/error endpoint needs to be customized, but what specific code should be use to accomplish a successful override of /oauth/error?
FIRST ATTEMPT:
My first attempt at doing the override, is throwing errors, which you can see in the debug log, which I have uploaded to a file sharing site at this link.
I started by trying to get code elements provided by Spring to work in the sample app.
First, I added a new controller class to the authserver app in the sample app link above, and I added some of the sample code from Spring's WhiteLabelErrorEndpoint.java, which you can read at this link. My attempt is as follows:
package demo;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import javax.servlet.http.HttpSession;
import org.springframework.security.oauth2.common.exceptions.OAuth2Exception;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.bind.annotation.ResponseBody;
import org.springframework.web.servlet.ModelAndView;
import org.springframework.web.util.HtmlUtils;
#Controller
public class CustomViewsController {
private static final String ERROR = "<html><body><h1>OAuth Error</h1><p>${errorSummary}</p></body></html>";
#RequestMapping("/oauth/error")
public ModelAndView handleError(HttpServletRequest request) {
Map<String, Object> model = new HashMap<String, Object>();
Object error = request.getAttribute("error");
// The error summary may contain malicious user input,
// it needs to be escaped to prevent XSS
String errorSummary;
if (error instanceof OAuth2Exception) {
OAuth2Exception oauthError = (OAuth2Exception) error;
errorSummary = HtmlUtils.htmlEscape(oauthError.getSummary());
}
else {
errorSummary = "Unknown error";
}
model.put("errorSummary", errorSummary);
return new ModelAndView(new SpelView(ERROR), model);
}
}
And I added the following SpelView.java String handler class used by the link above by copying the code from this link:
package demo;
import java.util.HashMap;
import java.util.Map;
import javax.servlet.http.HttpServletRequest;
import javax.servlet.http.HttpServletResponse;
import org.springframework.context.expression.MapAccessor;
import org.springframework.expression.Expression;
import org.springframework.expression.spel.standard.SpelExpressionParser;
import org.springframework.expression.spel.support.StandardEvaluationContext;
import org.springframework.security.oauth2.common.util.RandomValueStringGenerator;
import org.springframework.util.PropertyPlaceholderHelper;
import org.springframework.util.PropertyPlaceholderHelper.PlaceholderResolver;
import org.springframework.web.servlet.View;
import org.springframework.web.servlet.support.ServletUriComponentsBuilder;
/**
* Simple String template renderer.
*
*/
class SpelView implements View {
private final String template;
private final String prefix;
private final SpelExpressionParser parser = new SpelExpressionParser();
private final StandardEvaluationContext context = new StandardEvaluationContext();
private PlaceholderResolver resolver;
public SpelView(String template) {
this.template = template;
this.prefix = new RandomValueStringGenerator().generate() + "{";
this.context.addPropertyAccessor(new MapAccessor());
this.resolver = new PlaceholderResolver() {
public String resolvePlaceholder(String name) {
Expression expression = parser.parseExpression(name);
Object value = expression.getValue(context);
return value == null ? null : value.toString();
}
};
}
public String getContentType() {
return "text/html";
}
public void render(Map<String, ?> model, HttpServletRequest request, HttpServletResponse response)
throws Exception {
Map<String, Object> map = new HashMap<String, Object>(model);
String path = ServletUriComponentsBuilder.fromContextPath(request).build()
.getPath();
map.put("path", (Object) path==null ? "" : path);
context.setRootObject(map);
String maskedTemplate = template.replace("${", prefix);
PropertyPlaceholderHelper helper = new PropertyPlaceholderHelper(prefix, "}");
String result = helper.replacePlaceholders(maskedTemplate, resolver);
result = result.replace(prefix, "${");
response.setContentType(getContentType());
response.getWriter().append(result);
}
}
To override the error view define a controller, e.g.
#Controller
public class ErrorController {
#RequestMapping("/oauth/error")
public String error(Map<String,Object> model) {
// .. do stuff to the model
return "error";
}
}
and then implement the "error" view. E.g. using Freemarker, in a Spring Boot app you create a file called "error.ftl" in the "templates" directory at the top of the classpath:
<html><body>Wah, there was an error!</body></html>
This is all just vanilla Spring MVC so please refer to the Spring user guide for more details.