so I've started playing with rsocket and spring boot 2.2 to see if I can use it in my projects, but I'm facing a bit of troubles.
Normally, with spring messaging I define a listener method like the following:
#MessageMapping("addGeolocation")
public Mono<Boolean> addGeolocation(#Header("metadata") MmeMetadata metadata, #Payload String geolocation) { ... }
My understanding is that with rsocket I should be able to use the same logic, but when I'm defining the client I couldn't find an easy way to set message headers.
Currently I'm stuck with this:
boolean outcome = rSocketRequester.route("addGeolocation").metadata(...?).data(geolocationWKT).block();
is the metadata a replacement for headers? that method signature seems a little too generic to be used like headers. If I put an Map in it will spring be able to decode headers out of it?
Thank you,
Fernando
Please see this question: RSocket Metadata - custom object.
I used it as a starting point for my solution.
The term 'header' actually means some custom metadata. So, in order to get the correct value you need to configure metadataExtractorRegistry. For Spring Boot do it this way (code in kotlin):
val CUSTOM_MIMETYPE = MimeType.valueOf("<some custom mime type>")
val CUSTOM_HEADER = "<the name of a header>"
...
#Bean
fun rSocketStrategiesCustomizer(): RSocketStrategiesCustomizer {
return RSocketStrategiesCustomizer { strategies: RSocketStrategies.Builder ->
strategies.metadataExtractorRegistry {
it.metadataToExtract(CUSTOM_MIMETYPE, String::class.java, CUSTOM_HEADER)
}
}
}
The type of the data object can be any, not necessarily a String. There is default String endcoder/decoder, so I didn't provide one in the code. For your own type you can provide one of existing encoders/decoders (Json for example) or create your own:
#Bean
fun rSocketStrategiesCustomizer(): RSocketStrategiesCustomizer {
return RSocketStrategiesCustomizer { strategies: RSocketStrategies.Builder ->
strategies.metadataExtractorRegistry {
it.metadataToExtract(CUSTOM_MIMETYPE, YourType::class.java, CUSTOM_HEADER)
}.decoder(Jackson2JsonDecoder())
.encoder(Jackson2JsonEncoder())
}
}
After you've configured registry as above, use the header name defined in the registry in your controller:
#MessageMapping("addGeolocation")
public Mono<Boolean> addGeolocation(#Header(CUSTOM_HEADER) String metadata, #Payload String geolocation) { ... }
And in order to send that header, use next code:
boolean outcome = rSocketRequester.route("addGeolocation")
.metadata("value", CUSTOM_MIMETYPE)
.data(geolocationWKT)
.block();
Hope this helps
Instead of a bag of name-value pairs (i.e. headers), RSocket uses metadata which can be in any format (i.e. MIME type) and it can be composite metadata with multiple types of metadata each formatted differently. So you can have one section with routing metadata, another with security, yet another with tracing, and so on.
To achieve something similar to headers, you can send name-value pairs as JSON-formatted metadata. Now on the server side, you'll need to provide a hint to Spring for how to extract a Map (of headers) from the metadata of incoming requests. To do that you can configure a MetadataExtractor and that's described in this section of the docs. Once that's configured, the extracted Map becomes the headers of the message and can be accessed from #MessageMapping methods as usual (via MessageHeaders, #Header, etc).
Related
I'm using the functional web framework for a non-reactive application. Consuming JSON seems simple enough as request.body(FormModel.class)
However, when the data is x-www-form-urlencoded, I'm trying
default RouterFunction<ServerResponse> submit() {
return RouterFunctions.route(POST(submitRoute()),request -> {
var request.body(new ParameterizedTypeReference<FormModel>() {});
return temporaryRedirect(allRoute()).build();
});
}
And I'm getting:
Content type 'application/x-www-form-urlencoded;charset=UTF-8' not supported
Using MVC, this would work just by declaring a FormModel parameter and annotating it with #ModelAttribute
Apparently, "binding" is not the same as "converting" for Spring, and the systems involved are different. There's no easy way of getting binding to work with router functions. There's an open issue about it targeting Spring 6. As per the workaround given there, my code would work with
default RouterFunction<ServerResponse> submit() {
return RouterFunctions.route(POST(submitRoute()).and(accept(MediaType.APPLICATION_FORM_URLENCODED)), request -> {
var form = formService().emptyForm();
var binder = new ExtendedServletRequestDataBinder(form, "editing");
binder.bind(request.servletRequest());
return temporaryRedirect(allRoute()).build();
});
}
Note that you have to get an empty FormModel instance from somewhere. It's not the best, but it works.
I have the following configuration for sending SOAP requests as part of a integration flow, where uriDefinedInApplicationProperties is a fixed uri, defined in 'application.properties' file :
#Bean
public MarshallingWebServiceOutboundGateway outboundSOAPGateway()
{
final MarshallingWebServiceOutboundGateway outboundGateway = new MarshallingWebServiceOutboundGateway(
uriDefinedInApplicationProperties,
requestMarshaller,
responseMarshaller);
outboundGateway.setAdviceChain(Collections.singletonList(retryOutboundGatewayAdvice));
if (soapActionCallback!= null) {
outboundGateway.setRequestCallback(soapActionCallback);
}
return outboundGateway;
}
Now i have the requirement that the URI of the remote SOAP server should be dynamically generated ( i'm planning on using message headers to store the URI).
I wanted to do something like the following, but MarshallingWebServiceOutboundGateway does not seem to support it, and i haven't been able to find how to do something similar using spring integration dsl:
#Bean
public MarshallingWebServiceOutboundGateway outboundSOAPGateway()
{
final MarshallingWebServiceOutboundGateway outboundGateway = new MarshallingWebServiceOutboundGateway(
message -> (message -> message.getHeaders().get("remote.uri.header"),
requestMarshaller,
responseMarshaller);
outboundGateway.setAdviceChain(Collections.singletonList(retryOutboundGatewayAdvice));
if (soapActionCallback!= null) {
outboundGateway.setRequestCallback(soapActionCallback);
}
return outboundGateway;
}
I have noted that MarshallingWebServiceOutboundGateway has a setUriVariableExpressions(Map<String, Expression> uriVariableExpressions) method, but i didn't find any clear documentation on what it is supposed to do and how it works.
Also i tried to do something like the following to create the outbound gateway, but it does not seem to support requestCallbacks nor advice chain.
Http.outboundGateway(message -> message.getHeaders().get("remote.uri.header"))
.messageConverters(new MarshallingHttpMessageConverter(
remoteRequestMarshaller,
remoteResponseMarshaller));
What is the best way to create a SOAP outbound gateway with retry advice and dynamically generated uri?
The advice config is not a MessageHandler responsibility. If you use Java DSL, see a second argument (a GenericEndpointSpec lambda) of the handle() you use for that MarshallingWebServiceOutboundGateway:
/**
* Configure a list of {#link Advice} objects to be applied, in nested order, to the
* endpoint's handler. The advice objects are applied only to the handler.
* #param advice the advice chain.
* #return the endpoint spec.
*/
public S advice(Advice... advice) {
Yes, I agree that MarshallingWebServiceOutboundGateway (and its super class) doesn't support URI resolution against message at the moment. Feel free to raise a GH issue to fix a gap with a SpEL configuration like we have it for the mentioned Http.outboundGateway.
Meanwhile as a workaround you can consider to implement a DestinationProvider which reads an URI from a TheadLocal store. Before calling this gateway you should consult your message and store built URI into that ThreadLocal variable.
I am trying to implement the Content-Security-Policy-Report-Only Header for my website.
In order to do that, i need a controller that will accept the browsers POST-Request - which will send data about the violation in form of JSON. This request, however, seems to specify the Content-Type as application/csp-report instead of application/json (side note: Why the hell??).
This apparently causes Spring to refuse the request - it seems like the usage of #RequestBody makes spring only accept requests that are of Content-Type "application/json".
Even when i specifically set the value consumes of the #RequestMapping annotation to application/csp-report it still does not accept the request.
I have gone as far as using a filter to wrap the request with HttpServletRequestWrapper - which seems to be the common way of modifying the behavior of a request.
I have overwritten all of these methods: getContentType(), getHeader(String), getHeaders(String) to return "application/json".
But the request still does not go through.
What am i missing here?
Are there any better solutions to this that
do not require me to do magic with the request?
I wouldn't even
mind parsing the JSON manually, can i somehow accept it as plain
String instead?
According documentation on #RequestBody annotation,
The body of the request is passed through an HttpMessageConverter to resolve the method argument depending on the content type of the request
What that means is Spring Framework defines a specialized API for defining how certain MediaTypes are converted into certain Java types when used as parameters to REST endpoints.
Here is a pretty good article showcasing these capabilities.
There are a great deal of builtin Spring converters that you may be able to just configure and use if your media format can be mapped to their respective media formats. Specifically for your case, you should look at one of the converters available in spring.converter.json package. In simplest case, making it work should be as simple as:
HttpMessageConverter converter = new <JsonConverterOfYourChoice>(JsonMapper);
converter.getSupportedMediaTypes().add(new MediaType("application", "csp-report"));
And then registering such converter into a spring's configuration as you do.
Other available converter types include:
StringHttpMessageConverter
ObjectToStringHttpMessageConverter (if you have already configured Spring's ConversionService to read your desired types from String)
ByteArrayHttpMessageConverter
FormHttpMessageConverter
Various XML-based converters living in spring.converter.xml package
AllEncompassingFormHttpMessageConverter (converter built on top of Form converter and capable of handling XML and JSON as well)
ProtobufHttpMessageConverter
Atom/RSS feed message converters.
Finally, if none of the above does not apply for you, you can make and register your own HttpMessageConverter implementation.
Building M. Prokhorov's answer into a complete snippet, this code worked for me to add application/csp-report as a JSON message converter (using Jackson) in Spring Boot:
#Bean
public WebMvcConfigurer webMvcConfigurer() {
return new WebMvcConfigurerAdapter() {
#Override
public void extendMessageConverters(List<HttpMessageConverter<?>> converters) {
super.extendMessageConverters(converters);
final List<MediaType> cspReportMediaTypes =
Collections.singletonList(new MediaType("application", "csp-report"));
final HttpMessageConverter<Object> cspReportConverter =
new MappingJackson2HttpMessageConverter() {
#Override
public List<MediaType> getSupportedMediaTypes() {
return cspReportMediaTypes;
}
};
converters.add(cspReportConverter);
}
};
}
The other answers helper me figuring out what was going on with #ResponseBody.
CSP reporting is still relatively new and not quite standardized among browsers:
The CSP2 spec states that CSP report requests need to use a
content-type header of application/csp-report. In my sample, I
observed four different content-type headers:
application/x-www-form-urlencoded
application/json
application/csp-report
application/json; charset=UTF-8
What to Expect When Expecting Content Security Policy Reports
So an alternative, to accept any kind of content type, is to read the input stream directly from the HttpServletRequest instead of using #ReponseBody with a message converter.
For example, in Kotlin:
#PostMapping(path = [CSP_REPORT_URL])
fun reportFrontendError(
request: HttpServletRequest
): ResponseEntity<Void> {
val body = request.inputStream.bufferedReader().use { it.readText() }
// ...
return ResponseEntity(HttpStatus.OK)
}
I have a Spring REST application that accepts JSON messages, written like
#RequestMapping(value = "/myhook", method = RequestMethod.POST,
produces = JSON, consumes = JSON)
public #ResponseBody MyResponse doIt
(#Valid #RequestBody(required = true) MyContractRequest request) {
MyResponse response;
...
return response;
}
This works really well with almost no code to support, but now I have a requirement to sign both response and request.
I started from simply computing the shared signature of all message fields at Java level and assigning it to the dedicated signature field. However this requires to have and maintain code for computing the signatures:
public void update(java.security.Signature sign) throws Exception {
sign.update(name);
sign.update(value);
sign.update(etc);
}
Some people around me expressed opinion that the need to write and maintain this signing code may not be the best design, and it may be better to sign the whole message as a single JSON string. I could fetch the request as a string manually, and then process JSON manually, but I really would like to preserve the Spring controller concepts.
Also, I cannot longer have the signature field in the message itself because the value of this field obviously also changes the signature of the JSON string.
Is there any way to compute the signature of the whole JSON message body on the message departure and arrival, and where to place the signature so it could be passed together with the message? One of the idea is to use the custom HTTP header for the signature. Anyway, how to compute it first?
You can use a servlet filter with Spring MVC and modified your content whatever you want in request and response as well
Example :
http://www.mkyong.com/spring-mvc/how-to-register-a-servlet-filter-in-spring-mvc/
or you can use Spring 3 MVC Interceptor
http://viralpatel.net/blogs/spring-mvc-interceptor-example/
All examples that I have found around internet were about to use content negotiation with json, xml etc. Is there any chance to have only one method producing all kind of contents, like:
#RequestMapping(produces={"text/html","application/json"})
public List<Product> list() {
return productDAO.list();
}
I tried to use the ContentNegotiationManager, but nothing worked for me.
One method can return responses of different content types. Some you can get with default settings, some you have to additionally configure. Take for example a following method, quite similar to yours,
#RequestMapping(value="/response", method=RequestMethod.GET, produces={MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE})
public #ResponseBody Foo multipleTypes() {
return new Foo();
}
this method is capable of returning both XML and JSON, even more Spring MVC will automatically configure the converters if you have JAXB2 and Jackson libs on the classpath.
When reasoning whether it will return an XML or JSON its where content negotiation comes to play. If the request is suffixed with a path e.g. /response.json or /response.xml the response will be set based on it. The resolution can be based on a parameter as well, so /response?format=xml. Finally, if the request has an Accept header set to XML or JSON a response will be converted to the respective type. This constitutes a PPA strategy (Path, Parameter, Accept).
In other words, if you provide a proper converter implementations and configure them properly (some are available out of the box), you can get a single method that returns different representations, that you can control based on the PPA strategy.
Content Negotiation Using Spring MVC is a great post on Spring's Blog site with working examples.