I've got a Spring Boot app. Its a standard http rest api. I'm adding a grpc host inside of it to provide a parallel grpc experience. That part is all working fine. One of my requirements is request / response logging.
I've got a ServerInterceptor, a SimpleForwardingServerCall wrapper and a SimpleForwardingServerCallListener wrapper to trap all the places I need to log (success & failure calls).
I need to get the request body from onMessage and the status code from 2 places (one for fail, one for success) and then the elapsed time.
So long question short, I'm not sure how the threading model works in grpc, where can I store this information on a per request basis so by the time I get to onComplete() which has no params, I can access it?
I'm doing something like:
return new xxxServerCallListener<>(next.startCall(new xxxServerCall<>(call), headers), call);
So I assume a new xxxServerCallListener and xxxServerCall get created for each request and I can store stuff in there as it moves through the pipeline? Would that be the best place to store?
Yes, construct your own ServerCall.Listener per-RPC and store the information there.
#Override
public <ReqT, RespT> ServerCall.Listener<ReqT> interceptCall(
ServerCall<ReqT, RespT> call,
final Metadata headers,
ServerCallHandler<ReqT, RespT> next) {
// resp could be stored in the call class, but then it would need to be
// a named class which is more boilerplate. AtomicReference is
// convenient.
final AtomicReference<RespT> resp = new AtomicReference<>();
call = new SimpleForwardingServerCall<ReqT, RespT>(call) {
#Override public void sendMessage(RespT message) {
// We assume we're getting immutable protobufs or something similar
// that doesn't mutate. Otherwise we'd need to copy it here.
resp.set(message);
super.sendMessage(message);
}
};
return new SimpleForwardingServerCallListener<ReqT>(next.startCall(call, headers)) {
private ReqT req;
#Override public void onMessage(ReqT message) {
this.req = message;
super.onMessage(message);
}
#Override public void onCancel() {
// No point in using 'resp' as the client probably didn't use it.
// Note that sendMessage() has likely not been called so 'resp' is
// frequently null. If referencing 'resp' here, synchronizing (like
// that provided by AtomicReference) is necessary.
log(req);
super.onCancel();
}
#Override public void onComplete() {
// For properly-behaving callers (those that do not invoke
// call.sendMessage() after call.close()), this actually doesn't
// need to be synchronized as call.close() (and thus any
// sendMessage()) is guaranteed to have been called by this point.
log(req, resp.get());
super.onComplete();
}
};
}
Related
I have been working with Glassfish/Jackson for over a year and I always have this problem when introducing a new endpoint implementation: when the endpoint is not reached and I want to understand why, the only hints I have to go on are the returned request, since the execution doesn't reach the desired endpoint or resource (routing/mapping error).
I want to intercept the Jersey mapping/routing execution before reaching endpoints/resources, with the "raw" request, so that I can better understand resource/endpoint mapping and routing problems.
This answer to a different question, by #xeye, solved this problem for me:
Create a filter that implements ContainerRequestFilter, and override its filter method. This will be where we can intercept all requests for debugging.
// Executed whenever a request is sent to the API server.
// Useful for debugging errors that don't reach the desired endpoint implementation.
#Provider
#Priority(value = 0)
public class MyFilter implements ContainerRequestFilter {
#Context // request scoped proxy
private ResourceInfo resourceInfo;
#Override
public void filter(ContainerRequestContext requestContext) throws IOException {
try {
// put a breakpoint or log desired attributes of requestContext here.
} catch (Exception e) {
// ignore
}
}
}
Then register this new class in your ConfigResource implementation.
public class MyResourceConfig extends ResourceConfig {
public MyResourceConfig(){
register(MyFilter.class);
// ...
}
(It's OK to Ask and Answer Your Own Questions)
I have this Jersey2-based application, with a custom ContainerRequestFilter.
When the filter(ContainerRequestContext) method is called I want to do a check and, if needed, I want to be able to stop the request before entering the main logic of the application.
At the moment I'm using the ContainerRequestContext#abortWith method to block the call and return an "error" response to the client.
My application returns JSONP to the client, and if I block with abortWith the response is always a JSON.
Looking at the jersey sources I found
org.glassfish.jersey.server.internal.JsonWithPaddingInterceptor that is responsible of the JSONP serialization.
In the abortWith flow I see it fails to find the JSONP annotation, but I don't know where it search for it.
My method has it, in fact in the "normal" scenario (without the abortWith) I see correctly the JSONP format.
I found the solution.
The ContainerRequestFilter#filter method was something like
public void filter(final ContainerRequestContext crc) throws IOException {
if (/* logic */) {
CustomObject ret = new CustomObject();
ret.error = "error message";
crc.abortWith(Response.ok(ret)).build());
}
}
JsonWithPaddingInterceptor expected a response with a JSONP annotation so I retrieve them from the ResourceInfo#resourceMethod, with something like
public void filter(final ContainerRequestContext crc) throws IOException {
if (/* logic */) {
Annotation[] as = this.resourceInfo.getResourceMethod().getAnnotations();
CustomObject ret = new CustomObject();
ret.error = "error message";
crc.abortWith(Response.ok().entity(ret, as).build());
}
}
this way the annotation is correctly found
I have a problem using Spring WebFlux. Actually my project is composed by
Api wrapper ( basically code that uses WebClient to call a remote service)
private final BinanceServerTimeApi binanceServerTimeApi;
private final WebClient webClient;
#Value("${binance.api.secret}")
private String secret;
#Autowired
public BinanceAccountApi(#Value("${binance.api.baseurl}") String baseUrl,
#Value("${binance.api.key}") String key,
BinanceServerTimeApi binanceServerTimeApi) {
this.binanceServerTimeApi = binanceServerTimeApi;
this.webClient = WebClient.builder()
.baseUrl(baseUrl)
.defaultHeader("X-MBX-APIKEY",key)
.build();
}
public Mono<AccountInformation> getAccountInformation() {
Mono<ResponseServerTime> responseServerTime = binanceServerTimeApi.getServerTime();
String apiEndpoint = "api/v3/account?";
String queryParams = "recvWindow=50000×tamp=" + responseServerTime.block().getServerTime();
String signature = HmacSHA256Signer.sign(queryParams, secret);
String payload = apiEndpoint+queryParams+"&signature="+signature;
log.info("final url for getAccountInformation is {}", payload);
return this.webClient.get().uri(payload).accept(MediaType.APPLICATION_JSON)
.retrieve().bodyToMono(AccountInformation.class).log();
}
the endpoint used by my javascript client
#Autowired
private BinanceAccountApi binanceAccountApi;
public Mono<ServerResponse> getAccountPortfolio(ServerRequest request) {
return binanceAccountApi.getAccountInformation()
.flatMap(accountInformation -> ServerResponse.ok()
.contentType(MediaType.APPLICATION_JSON)
.body(fromObject(accountInformation))).log();
}
Here my main class
#Bean
public RouterFunction<ServerResponse> route(AccountHandler handler) {
return RouterFunctions .route(GET("/route/accountInformation").and(accept(MediaType.APPLICATION_JSON)),handler::getAccountPortfolio);
}
When I hit a get to this route /route/accountInformation, the first call works fine but the others call are pending (the server never sends the response).
Note that the first call to the endpoint lasts for 2000 ms.
This is my first approach to the WebFlux project and I am trying to figure out how it works.
Without more information it's hard to tell what's happening (the output of your log operator should help here). But using the block operator right in the middle of your handler is suspicious; by doing that, you might be blocking one of the few server threads.
Try something like:
return binanceServerTimeApi.getServerTime().flatMap(responseServerTime -> {
// ...
return this.webClient.get().uri(payload).accept(MediaType.APPLICATION_JSON)
.retrieve().bodyToMono(AccountInformation.class).log();
});
This will chain operations in a non-blocking way. If the situation doesn't improve after that, try adding a few log operators to understand where time is spent.
I'm building a small application to serve as a client for some third party library here at work. The API states that a Webhookis needed to respond some asynchronous events, but all their methods have the very same signature, apart from a changing _method field between the calls. For example, I have a _method = ping, media, etc.
I'd like to have separate methods on my controller to respond for each one of these methods. If the app allowed me to specify different URLs for each method it would be easy to use Spring MVC's #RequestMapping for each one of them. But I have to specify a single endpoint to receive all calls.
Is there a way (for example using Spring's HttpMessageConverter or something like that) to map different controller methods based on what the Request Body is? I've already tried with #RequestBody, #RequestParam but didn't seem to find anything.
I really, really didn't want to use a bunch of case, switch methods on a front controller to dispatch actions based on my _method field that comes with my POST data, so I happen to believe someone had this problem before and solved it intelligently.
Thanks a lot!
Edit 1: Providing source code
#Controller
#RequestMapping("/webhooks")
public class WebhookController {
#RequestMapping(method = RequestMethod.POST, params = {"_method=ping"})
#ResponseBody
public String ping(){
return "pong";
}
#RequestMapping(method = RequestMethod.POST, params = {"_method=media"})
#ResponseBody
public String media(){
return "media";
}
}
This is the answer:
{
"timestamp": 1440875190389,
"status": 400,
"error": "Bad Request",
"exception": "org.springframework.web.bind.UnsatisfiedServletRequestParameterException",
"message": "Parameter conditions \"_method=ping\" not met for actual request parameters: ",
"path": "/webhooks"
}
Right, I got it working. The answer is a bit tricky so I wanted to register it here should anyone have such problem.
#Neil McGuigan pointed me on the right direction on his comment but I didn't pay attention at first. The main culprit here is a very, very, very bad API design on our remote application's side.
_method is a field used to specify non-standard HTTP verbs such as PUT, PATCH, DELETE, TRACE and so on. This field is filtered by HiddenHttpMethodFilter and the HttpServletRequest is wrapped with this 'new' method. You can see at the file's source how it works.
As I wanted this _method field to get thru the filter without modifying the whole request (and causing the errors because there's no such verb as pingor message on `RequestMethod) I firstly had to deactivate the filter. This could be done by two ways:
I could stop Spring Boot from automagically configuring Spring MVC, skipping WebMvcAutoConfiguration from being loaded when the ApplicationContext was loaded. As you can imagine this is a BIG, BIG, BIIIIG NO because, well, things could happen.
I could use a FilterRegistrationBean to disable the bad filter. Pretty simple and straightforward, this was the method I chose to use:
#Bean
public FilterRegistrationBean registration(HiddenHttpMethodFilter filter) {
FilterRegistrationBean registration = new FilterRegistrationBean(filter);
registration.setEnabled(false);
return registration;
}
Last but not least, I decided to give HiddenHttpMethodFilter a little extension to somehow improve how the requests were getting thru. The Java EE Spec is pretty clear on the Servlet Spec Commandments where it states:
Thou should not alter your request on your side. You must respect the sender (something like that)
Though I agree with this, for the sake of my mental stability I decided to alter it anyway. To achieve this, we can use a simple HttpServletRequestWrapper, override the chosen methods and filter the original request with the wrapped part. I ended up doing something like this:
public class WhatoolsHiddenHttpMethodFilter extends OrderedHiddenHttpMethodFilter {
#Override
protected void doFilterInternal(HttpServletRequest request, HttpServletResponse response, FilterChain filterChain) throws ServletException, IOException {
String paramValue = request.getParameter(OrderedHiddenHttpMethodFilter.DEFAULT_METHOD_PARAM);
if("POST".equals(request.getMethod()) && StringUtils.hasLength(paramValue)) {
String method = paramValue.toUpperCase(Locale.ENGLISH);
List<String> whatoolsMethods = Arrays.asList("ping", "message", "carbon", "media", "media_carbon", "ack");
if(whatoolsMethods.contains(paramValue)){
WhatoolsHiddenHttpMethodFilter.HttpMethodRequestWrapper wrapper = new WhatoolsHiddenHttpMethodFilter
.HttpMethodRequestWrapper(request, "POST", paramValue);
filterChain.doFilter(wrapper, response);
} else {
WhatoolsHiddenHttpMethodFilter.HttpMethodRequestWrapper wrapper = new WhatoolsHiddenHttpMethodFilter
.HttpMethodRequestWrapper(request, method, null);
filterChain.doFilter(wrapper, response);
}
} else {
filterChain.doFilter(request, response);
}
}
private static class HttpMethodRequestWrapper extends HttpServletRequestWrapper {
private final String method;
private final String whatoolsMethod;
public HttpMethodRequestWrapper(HttpServletRequest request, String method, String whatoolsMethod) {
super(request);
this.method = method;
this.whatoolsMethod = whatoolsMethod;
}
#Override
public String getMethod() {
return this.method;
}
#Override
public String getHeader(String name) {
if("x-whatools-method".equals(name)){
return this.whatoolsMethod;
}
return super.getHeader(name);
}
#Override
public Enumeration<String> getHeaderNames() {
List<String> names = Collections.list(super.getHeaderNames());
if(this.whatoolsMethod != null){
names.add("x-whatools-method");
}
return Collections.enumeration(names);
}
}
}
So, what this does is to wrap the request with a new x-whatools-method header when the header is in my whatoolsMethods list. With this, I can easily use #RequestMapping's headers property and map the requests to the correct controller methdods.
Back to the initial question, I'm almost sure (well, 99,95% should be completely sure but let's not risk it) the params property on #RequestMapping works only for request parameters on GET URIs, e.g http://foo.bar/?baz=42. It won't work filtering parameters sent on the request's body.
Thanks Neil for your guidance, even if small! I hope this helps someone.
You can use params in a request mapping:
#RequestMapping(value="/foo", params={"_method=ping"})
Assuming these are post parameters that is
params DOES work for POST, I promise you
Here's my controller:
#Controller
#RequestMapping("/test1")
public class ParamTestController {
#RequestMapping(method = RequestMethod.POST)
#ResponseBody String getA(){
return "A";
}
#RequestMapping(method = RequestMethod.POST, params = {"b"})
#ResponseBody String getB(){
return "B";
}
}
Here's my test:
For the rest interface the Spring MVC + RxJava + DeferredResult returned from controllers is used.
I am thinking about adding Hateoas support to the endpoints. The natural choice would be the Spring Hateoas. The problem is that Spring Hateoas would not work in the asynchronous/multi-threading environment since it uses ThreadLocal.
Is there any way to workaround that constraint? I do not think so but maybe someone has any suggestions.
Has anyone used other APIs to add Hateoas support to the rest endpoints?
Thank you.
So the solution I've used is to closure in the request attributes and then apply them as part of a lift operator
public class RequestContextStashOperator<T> implements Observable.Operator<T, T> {
private final RequestAttributes attributes;
/**
* Spring hateoas requires the request context to be set but observables may happen on other threads
* This operator will reapply the context of the constructing thread on the execution thread of the subscriber
*/
public RequestContextStashOperator() {
attributes = RequestContextHolder.currentRequestAttributes();
}
#Override
public Subscriber<? super T> call(Subscriber<? super T> subscriber) {
return new Subscriber<T>() {
#Override
public void onCompleted() {
subscriber.onCompleted();
}
#Override
public void onError(Throwable e) {
subscriber.onError(e);
}
#Override
public void onNext(T t) {
RequestContextHolder.setRequestAttributes(attributes);
subscriber.onNext(t);
}
};
}
}
which you can then use on an observable like
lift(new RequestContextStashOperator<>())
as long as the object is created in the same thread as the request. You can then use a map after in the observable chain to map your object up to being a resource and add your hateoas links in.
So answer is a bit late, but probably someone will find it useful.
You are right about ThreadLocal - if you generate hateoas links in different thread, then it fails with exception. I found some kind of workaround for this:
#RequestMapping(path = "/{id}", method = RequestMethod.GET, produces = MediaType.APPLICATION_JSON_VALUE)
DeferredResult<ResponseEntity<ProductResource>> example(#PathVariable("id") final String productId, final HttpServletRequest request) {
final DeferredResult<ResponseEntity<ProductResource>> deferredResult = new DeferredResult<>();
request.setAttribute(WebUtils.INCLUDE_CONTEXT_PATH_ATTRIBUTE, request.getContextPath());
final RequestAttributes requestAttributes = new ServletRequestAttributes(request);
productByIdQuery.byId(UUID.fromString(productId)).subscribe(productEntity -> {
RequestContextHolder.setRequestAttributes(requestAttributes);
deferredResult.setResult(result, HttpStatus.OK))
}, deferredResult::setErrorResult);
return deferredResult;
}
So as you see, I save RequestAttributes so I can set them later in the callback. This solves just part of the problem - you'll get another exception because you'll loose contextPath attribute. To avoid this save it explicitly:
request.setAttribute(WebUtils.INCLUDE_CONTEXT_PATH_ATTRIBUTE, request.getContextPath());
After those changes everything seems to work, but looks messy of course. I hope that somebody can provide more elegant solution.