i have a RESTful api application build on spring mvc.
recently i was doing something integration between spring mvc and reactive stream (like rxjava and project-reactor) and try to make the application more reactive.
i have just build some demo like this below:
1.for rxjava,i use PublishSubject
private SerializedSubject<StreamResult, StreamResult> subject = PublishSubject.<StreamResult>create().toSerialized();
public ReactiveStreamController() {
this.subject.subscribe(streamResult -> {
String id = streamResult.getRequest().getParameter("id");
System.out.println("[" + Thread.currentThread().getName() + "] request received. id = " + id);
String random = StringUtils.isBlank(id) ? StringUtils.EMPTY : id;
ResponseVO vo = new ResponseVO(200, "success = " + random);
streamResult.getFuture().complete(vo);
}, Throwable::printStackTrace);
}
#ResponseBody
#RequestMapping(value = "/rxJava", method = RequestMethod.GET)
public CompletableFuture<ResponseVO> rxJavaController(HttpServletRequest httpServletRequest) {
StreamResult sr = new StreamResult();
sr.setRequest(httpServletRequest);
subject.onNext(sr);
return sr.getFuture();
}
2.for project reactor
#ResponseBody
#RequestMapping(value = "/reactorCodeNew", method = RequestMethod.GET)
public CompletableFuture<ResponseVO> reactorCoreNewParadigm(HttpServletRequest servletRequest) {
Mono<ResponseVO> mono = Mono.just(servletRequest)
.subscribeOn(executorService)
.map(request -> {
String id = request.getParameter("id");
System.out.println("[" + Thread.currentThread().getName() + "] request received. id = " + id);
String random = StringUtils.isBlank(id) ? StringUtils.EMPTY : id;
ResponseVO vo = new ResponseVO(200, "success = " + random);
return vo;
})
.timeout(Duration.ofSeconds(2), Mono.just(new ResponseVO(500, "error")));
return mono.toCompletableFuture();
}
while running both the demos, i don't quite see too many difference between just using a java's CompletableFuture to supply among the controller method.
what i understand reactive stream and what i want is treating the servlet request as a stream and cosume it with some feature like backpressure.
i wanna know:
1. is there a better way to make the application more reactive?
2. is it correct or compatible to integrate spring mvc with reactive streams? if yes, how can i performce feature like backpressure?
i realize maybe i forgot to declare why/how i return a completablefuture in the controller, actually i inject a customized MethodReturnValueHandler to transform the CompletableFuture to DefferdResult.
public class CompletableFutureMethodReturnValueHandler extends DeferredResultMethodReturnValueHandler {
#Override
public boolean supportsReturnType(MethodParameter returnType) {
return CompletableFuture.class.isAssignableFrom(returnType.getParameterType());
}
#Override
public void handleReturnValue(Object returnValue, MethodParameter returnType, ModelAndViewContainer mavContainer, NativeWebRequest webRequest) throws Exception {
CompletableFuture<?> completableFuture = (CompletableFuture<?>) returnValue;
super.handleReturnValue(CompletableDeferredResult.newInstance(completableFuture), returnType, mavContainer, webRequest);
}
}
Spring MVC is based on the Servlet API and is mostly blocking internally, so it cannot leverage reactive streams behavior. Writing adapters for the Controller layer won't be enough.
The Spring team is working on a separate initiative for this purpose. Follow SPR-14161 and the Spring blog (including this and this) to know more about reactive Spring.
Related
There are 4 interaction models provided in RSocket.
fire and forget
request and response
request stream
request channel
(metadata push)
Spring(and Spring Boot) provides RSocket integration, it is easy to build a RSocket server with the existing messaging infrastructure to hide the original RSocket APIs.
#MessageMapping("hello")
public Mono<Void> hello(Greeting p) {
log.info("received: {} at {}", p, Instant.now());
return Mono.empty();
}
#MessageMapping("greet.{name}")
public Mono<String> greet(#DestinationVariable String name, #Payload Greeting p) {
log.info("received: {}, {} at {}", name, p, Instant.now());
return Mono.just("Hello " + name + ", " + p.getMessage() + " at " + Instant.now());
}
#MessageMapping("greet-stream")
public Flux<String> greetStream(#Payload Greeting p) {
log.info("received: {} at {}", p, Instant.now());
return Flux.interval(Duration.ofSeconds(1))
.map(i -> "Hello #" + i + "," + p.getMessage() + " at " + Instant.now());
}
And in the client side, there is a RescoketRequester provided to shake hands with the server.
#GetMapping("hello")
Mono<Void> hello() {
return this.requester.route("hello").data(new Greeting("Welcome to Rsocket")).send();
}
#GetMapping("name/{name}")
Mono<String> greet(#PathVariable String name) {
return this.requester.route("greet." + name).data(new Greeting("Welcome to Rsocket")).retrieveMono(String.class);
}
#GetMapping(value = "stream", produces = MediaType.TEXT_EVENT_STREAM_VALUE)
Flux<String> greetStream() {
return this.requester.route("greet-stream").data(new Greeting("Welcome to Rsocket"))
.retrieveFlux(String.class)
.doOnNext(msg -> log.info("received messages::" + msg));
}
But how to use requestChannel and metadataPush model in Spring way(using messaging infrastructure)?
The sample codes is on Github. Update: added requestChannel sample.
Update: SETUP and METADATA_PUSH can be handled by #ConnectMapping. And Spring Security RSocket can secure SETUP and REQUEST.
Reference example
For a reference example, let's refer to the client-to-server integration tests and, in particular, to the ServerController class: spring-framework/RSocketClientToServerIntegrationTests.java (line 200) at 6d7bf8050fe710c5253e6032233021d5e025e1d5 · spring-projects/spring-framework · GitHub.
This commit has been mentioned in the release notes:
<…>
RSocket support including response handling via annotated #MessageMapping methods and performing requests via RSocketRequester.
<…>
— Spring Framework 5.2.0.M1 available now.
Channel interaction model
The corresponding code part of the reference example:
#MessageMapping("echo-channel")
Flux<String> echoChannel(Flux<String> payloads) {
return payloads.delayElements(Duration.ofMillis(10)).map(payload -> payload + " async");
}
Metadata push
It seems that, currently, it is not supported by the #MessageMapping annotation.
According to this discussion - "RESTful API - Correct behavior when spurious/not requested parameters are passed in the request", we shouldn't ignore not requested parameters but how we can process this situation on all endpoint?
For example for this endpoint:
#RequestMapping(value = "/transactions/",
method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
public RestResultSupport getCommandsById(#PathVariable("id") String id) throws IOException {
validateId(id);
....
return result;
}
We'll get the same result for 2 different requests:
curl localhost:8080/?id=1200
and
curl localhost:8080/?id=1200&unknown=incorrect
If we imagine that we should process this situation on 20 endpoints, how can we simplify our code? Does Spring provide some tools for that?
I found only one way to do this - implement HandlerInterceptor.
Please have a look at an example:
public class RequestInterceptor implements HandlerInterceptor {
#Override
public boolean preHandle(HttpServletRequest request,
HttpServletResponse response, Object handler) throws Exception {
Set<String> innerParams = request.getParameterMap().keySet();
Set<String> describedParams = new HashSet<>();
for (MethodParameter methodParameter : ((HandlerMethod) handler).getMethodParameters()) {
if (methodParameter.hasParameterAnnotation(RequestParam.class)) {
RequestParam requestParam = methodParameter.getParameterAnnotation(RequestParam.class);
describedParams.add(requestParam.name());
}
}
for (String inputRequestParam : innerParams) {
if (!describedParams.contains(inputRequestParam)) {
throw new BadDataException("Please provide valid request paramaters. [ Valid request parameters - " + describedParams + " ]");
}
}
return true;
}
... empty other required methods ...
}
Code analyzes required parameters and if it gets something unknown it will throw RuntimeException
I'm starting with Spring and REST application. Currently, I'm developing one application on my own and I stuck.
The app is divided just like standard Spring Boot project. All of the controllers are contained in web package.
One of "standard" controller is responsible for handling HTTP request and returning an HTML website. I have added a REST controller which should respond to POST request from the first controller, but I receive a 404 error.
How it looks like in code?
#RestController
#RequestMapping("/users")
public class UserRestController {
#Autowired
private UserService userService;
#RequestMapping(value = "/user", method = RequestMethod.POST, consumes = "application/json", produces = "application/json")
public ResponseEntity<?> getUser(#RequestParam("userId") String userId, Errors errors) {
AjaxUser response = new AjaxUser();
if (errors.hasErrors()) {
response.message = errors.getAllErrors().stream().map(x -> x.getDefaultMessage()).collect(Collectors.joining(","));
return ResponseEntity.badRequest().body(response);
}
response.setUser(userService.getUserById(Integer.getInteger(userId).intValue()));
return ResponseEntity.ok(response);
}
private class AjaxUser {
private User user;
private String message;
public void setUser(User user) {
this.user = user;
}
public void setMessage(String message) {
this.message = message;
}
public String getMessage() {
return message;
}
#Override
public String toString() {
return "User { id:" + user.getId() + ", Name: " + user.getName() + ", surname: " + user.getSurname() + "}";
}
}
}
From .js file I send a ajax query which should trigger a rest controller, here is the code:
function sendUserId(id) {
var user = {};
user["userId"] = id;
console.log("USER: ", user);
$.ajax({
type: "POST",
contentType: "application/json",
url: "/users/user",
data: JSON.stringify(user),
dataType: 'json',
cache: false,
timeout: 100000,
success: function (user) {
var json = "<h4>Ajax Response</h4><pre>"
+ JSON.stringify(user, null, 4) + "</pre>";
console.log("SUCCESS : ", user);
},
error: function (e) {
var json = "<h4>Ajax Response</h4><pre>"
+ e.responseText + "</pre>";
console.log("ERROR : ", e);
}
});
}
userId is taken from a html by jQuery, console.log show existing and right value.
Note: There exist a standard user #Controller which is responsible for displaying a user list, it works, problem appear during sending a user id to REST controller. It behaves just like the REST controller doesn't exist and browser return 404 status response. Btw, page use a Spring Secure to login and so on.
Could someone help?
BR Konrad
The controller is looking to have a request parameter that you are missing in the js requesting url
/users/user?userId=1
You can get a user by id like below:
#RequestMapping(value = "{id}", method = RequestMethod.GET)
public ResponseEntity<User> get(#PathVariable("id") int id) {
User user = userService.findById(id);
if (user == null) {
return new ResponseEntity<User>(HttpStatus.NOT_FOUND);
}
return new ResponseEntity<User>(user, HttpStatus.OK);
}
So your rest entry point is /users/userid, eg: /users/1
Found this from the post Spring MVC RESTFul Web Service CRUD Example
the problem based on function arguments, REST controller should take String argument and next parse it to JSON object, the response should be String too. Topic can be closed, thanks all to be involved.
I am working on spring web socket. i just create a controller for calling apis through WS. but unfortunately its mapping is not done. so i cant call that apis from client.
This is my controller class
#Controller
public class FloorController {
#MessageMapping("/floormapupdate")
public Message greeting(#Header(value = "nativeHeaders") Map s, Principal principal, Message message) throws Exception {
String type = (String) ((List) (s.get("type"))).get(0);
Map headers = new HashMap();
headers.put("type", type);
System.out.println("===================" + principal.getName());
simpMessagingTemplate.convertAndSendToUser(principal.getName(), "/channel/me", message);
return message;
}
}
what is the issue with it ?
Is there any way integrate ajax in spring mvc and apache tile. For me, when sent the request to controller and after that send back to view, the data can not pass through tile config.
Help me please. Thanks
I do this all the time. What i do is, instead of sending back a "View" string or "ModelAndView" object, return an object of type ResponseEntity.
For instance, in your controller class, you can have a method like:
#RequestMapping(value="/cache/clear", method = RequestMethod.GET)
public ResponseEntity<String> clearAllCaches() {
Map<String, Object> results = new HashMap<String, Object>();
long startTimestamp = System.currentTimeMillis();
for (CachingService cachingService : cachingServices) {
LOGGER.info("Clearing caches in " + cachingService.getClass().getSimpleName());
cachingService.clearCache();
}
long finishTimestamp = System.currentTimeMillis();
long executionTime = finishTimestamp - startTimestamp;
LOGGER.warn("Finished clearing caches in " + cachingServices.size() + " services in " + executionTime + " ms.");
results.put("executionTime", executionTime);
results.put("serviceCount", cachingServices.size());
results.put(ServiceConstants.RETURN_KEY_SUCCESS, Boolean.TRUE);
return createResponse(results, HttpStatus.CREATED);
}
protected ResponseEntity<String> createResponse(Map <String, Object> results, HttpStatus successCode) {
JSONSerializer serializer = new JSONSerializer();
String responseBody = serializer.deepSerialize(results);
HttpStatus status = successCode;
boolean success = results.containsKey(ServiceConstants.RETURN_KEY_SUCCESS)
? (Boolean)results.get(ServiceConstants.RETURN_KEY_SUCCESS)
: false;
if (!success) {
status = HttpStatus.BAD_REQUEST;
}
HttpHeaders headers = new HttpHeaders();
headers.setContentType(MediaType.APPLICATION_JSON);
headers.setContentLength(responseBody.length());
headers.add("Access-Control-Allow-Origin", "*");
return new ResponseEntity<String>(responseBody, headers, status);
}
Note that I'm using FlexJSON as I'm using Spring Roo. You could also manually invoke Jackson.
i solve my problem by annotation #ResponseBody to send string back to ajax page.
public #ResponseBody
String test() {
List<User> users = userDetailsService.test();
for (User user : users) {
System.out.println(user);
}
return "1";
}