Copy of the request/response body on a Spring reactive app? - java

I'm looking into optimal ways of accessing the HTTP request and response bodies for tracing in a Spring reactive application.
For previous versions, we've leveraged Servlet filters and Servlet request wrappers to consume the incoming request's input stream and hold a copy of it for asynchronous processing of the traces (we send them to Elasticsearch).
But for a Spring reactive app (using webflux), I'm wondering what'd be the most appropriate way to access the requests before they're decoded. Any thoughts?

Turns out this can be achieved using the provided decorators: ServerWebExchangeDecorator, ServerHttpRequestDecorator and ServerHttpResponseDecorator, respectively.
Here's a sample request decorator that accumulates the DataBuffer contents as its read by the request's default subscriber:
#Slf4j
public class CachingServerHttpRequestDecorator extends ServerHttpRequestDecorator {
#Getter
private final OffsetDateTime timestamp = OffsetDateTime.now();
private final StringBuilder cachedBody = new StringBuilder();
CachingServerHttpRequestDecorator(ServerHttpRequest delegate) {
super(delegate);
}
#Override
public Flux<DataBuffer> getBody() {
return super.getBody().doOnNext(this::cache);
}
#SneakyThrows
private void cache(DataBuffer buffer) {
cachedBody.append(UTF_8.decode(buffer.asByteBuffer())
.toString());
}
public String getCachedBody() {
return cachedBody.toString();
}
Just make sure that, when you decorate the ServerWebExchange passed by the WebFilter, you also override getRequest() to return the request decorator as well:
public final class PartnerServerWebExchangeDecorator extends ServerWebExchangeDecorator {
private final ServerHttpRequestDecorator requestDecorator;
private final ServerHttpResponseDecorator responseDecorator;
public PartnerServerWebExchangeDecorator(ServerWebExchange delegate) {
super(delegate);
this.requestDecorator = new PartnerServerHttpRequestDecorator(delegate.getRequest());
this.responseDecorator = new PartnerServerHttpResponseDecorator(delegate.getResponse());
}
#Override
public ServerHttpRequest getRequest() {
return requestDecorator;
}
#Override
public ServerHttpResponse getResponse() {
return responseDecorator;
}
}
On the filter:
#Component
public class TracingFilter implements WebFilter {
#Override
public Mono<Void> filter(ServerWebExchange exchange, WebFilterChain chain) {
return chain.filter(new PartnerServerWebExchangeDecorator(exchange));
}
}
Which can be used as such (beware the statically imported functions):
#Bean
public HttpHandler myRoute(MyHandler handler) {
final RouterFunction<ServerResponse> routerFunction =
route(POST("/myResource"), handler::persistNotification);
return webHandler(toWebHandler(routerFunction))
.filter(new TracingFilter())
.build();
}

Related

Thread Local remove() in Spring Boot webflux

I have a Web Filter that sets an object in a ThreadLocal attribute and I'm trying to understand how/when this Thread local should be cleaned-up (ThreadLocal.remove()) to avoid the exception "User context already initiated." that happens because it is being retrieved from the Spring Boot Thread Pool with the previous values set.
I'm using Spring Webflux.
Where can I hook this SecurityAuthorizationContext.clean() call?
public class SecurityAuthorizationContext
{
private static final ThreadLocal<PrivilegeHolder> userContext = new ThreadLocal<>();
private final List<String> roles;
private SecurityAuthorizationContext(List<String> roles)
{
this.roles = roles;
}
public static void create(List<String> roles)
{
if (nonNull(userContext.get()))
{
log.error("User context already initiated.");
throw new AuthorizationException("User context already initiated.");
}
PrivilegeHolder privilegeHolder = new PrivilegeHolder();
userContext.set(privilegeHolder);
// example of privileges retrieved from database by the user roles
privilegeHolder.add(INSERT);
privilegeHolder.add(DELETE);
}
public static void clean()
{
userContext.remove();
}
public static boolean hasInsertPrivilege()
{
return userContext.get().hasPrivilege(INSERT);
}
public static boolean hasDeletePrivilege()
{
return userContext.get().hasPrivilege(DELETE);
}
}
public class AuthorizationFilter implements OrderedWebFilter
{
private static final String USER_ROLES = "user-roles";
#Override
public int getOrder()
{
return SecurityWebFiltersOrder.AUTHORIZATION.getOrder();
}
#Override
public Mono<Void> filter(ServerWebExchange serverWebExchange, WebFilterChain webFilterChain)
{
ServerHttpRequest request = serverWebExchange.getRequest();
HttpHeaders headers = request.getHeaders();
List<String> roles = headers.get(USER_ROLES);
SecurityAuthorizationContext.create(roles);
return webFilterChain.filter(serverWebExchange);
}
}
#Configuration
#EnableWebFluxSecurity
#EnableTransactionManagement
public class ApplicationConfiguration
{
#Autowired
private AuthorizationFilter authorizationFilter;
#Bean
public SecurityWebFilterChain springSecurityFilterChain(ServerHttpSecurity http)
{
return http
.csrf().disable()
.authorizeExchange()
.pathMatchers("/**").permitAll()
.and()
.addFilterAt(authorizationFilter, AUTHORIZATION)
.build();
}
}
UPDATE: Long story short ... I just want to extract something from request headers and make it available to all the stack without passing it as parameter.
So, better to use reactor context instead of ThreadLocal, here you can read about: https://projectreactor.io/docs/core/release/reference/#context

WebClient ExchangeFilterFunction caches value from ThreadLocal

I faced with the following issue while using Spring MVC with ThreadLocal and WebClient from Webflux.
My task is to:
Intercept the user's request to my application and get all the headers from it and save it in ThreadLocal.
After that, when my application makes a call to another service through the WebClient, intercept this request in ExchangeFilterFunction and supplement it with the Authorization header from p.1.
When I finish processing the user's request, I clear the context.
I use my custom class "RequestContext" to store headers in ThreadLocal:
public class RequestContext {
private HttpHeaders requestHeaders;
private String jwt;
private static final String BEARER_PREFIX = "Bearer ";
public RequestContext(HttpHeaders httpHeaders) {
this.requestHeaders = httpHeaders;
if (Objects.nonNull(httpHeaders)) {
init();
}
}
private void init() {
if (Objects.nonNull(requestHeaders)) {
extractJwt();
}
}
private void extractJwt() {
var jwtHeader = requestHeaders.getFirst(HttpHeaders.AUTHORIZATION);
if (StringUtils.isNotBlank(jwtHeader) && jwtHeader.startsWith(BEARER_PREFIX)) {
jwt = jwtHeader.substring(7);
}
}
}
I use my custom clas "RequestContextService" to deal with ThreadLocal:
public class RequestContextService {
private static final ThreadLocal<RequestContext> CONTEXT = new InheritableThreadLocal<>();
public void init(RequestContext requestContext) {
if (Objects.isNull(CONTEXT.get())) {
CONTEXT.set(requestContext);
} else {
log.error("#init: Context init error");
}
}
public RequestContext get() {
return CONTEXT.get();
}
public void clear() {
CONTEXT.remove();
}
}
My app is a WebMvc app. To complete step 1, I intercept the request with an HandlerInterceptor and set all headers to Threadlocal.
public class HeaderInterceptor implements HandlerInterceptor {
private final RequestContextService requestContextService;
#Override
public boolean preHandle(#NonNull HttpServletRequest request,
#NonNull HttpServletResponse response,
#NonNull Object handler) {
if (Objects.equals(request.getDispatcherType(), DispatcherType.REQUEST)) {
var headers = new ServletServerHttpRequest(request).getHeaders();
requestContextService.init(new RequestContext(headers));
}
return true;
}
#Override
public void afterCompletion(#NonNull HttpServletRequest request, #NonNull HttpServletResponse response,
#NonNull Object handler, Exception ex) {
requestContextService.clear();
}
}
As you can see, after every request I call "requestContextService.clear()" method to clear ThreadLocal.
To perform step two, I use the ExchangeFilterFunction, where I turn to the threadlocal and get the title from there.
public class SamlExchangeFilterFunction implements ExchangeFilterFunction {
private final RequestContextService requestContextService;
private static final ClientResponse UNAUTHORIZED_CLIENT_RESPONSE =
ClientResponse.create(HttpStatus.UNAUTHORIZED).build();
#Override
public #NotNull Mono<ClientResponse> filter(#NotNull ClientRequest request, #NotNull ExchangeFunction next) {
var jwt = requestContextService.get().getJwt();
if (StringUtils.isNoneBlank(jwt)) {
var clientRequest = ClientRequest.from(request)
.headers(httpHeaders -> httpHeaders.set(SAML_HEADER_NAME, jwt))
.build();
return next.exchange(clientRequest);
}
return Mono.just(UNAUTHORIZED_CLIENT_RESPONSE);
}
}
The problem is that the SamlExchangeFilterFunction works correctly only once.
On the first request to the application, everything works as it should. But with further requests with different authorization headers, the ExchangeFilterFunction seems to cache the value from the first request and substitutes it despite the fact that the threadlocal itself contains a completely different meaning of Authorization header.

Firebase multi-tenancy with play framework depends on header value of HTTP request

I have a play framework project that provides APIs that shared between multiple front-ends, currently, I'm working on single front-end but I want to create a multi-tenant backend, each front-end got its own Firebase account.
My problem that I have to consider which firebase project to access depends on the request header value, that came with different values depends on the front end.
What I have now:
FirebaseAppProvider.java:
public class FirebaseAppProvider implements Provider<FirebaseApp> {
private final Logger.ALogger logger;
private final Environment environment;
private final Configuration configuration;
#Inject
public FirebaseAppProvider(Environment environment, Configuration configuration) {
this.logger = Logger.of(this.getClass());
this.environment = environment;
this.configuration = configuration;
}
#Singleton
#Override
public FirebaseApp get() {
HashMap<String, String> firebaseProjects = (HashMap<String, String>) configuration.getObject("firebase");
firebaseProjects.forEach((websiteId, projectId) -> {
FileInputStream serviceAccount = null;
try {
serviceAccount = new FileInputStream(environment.classLoader().getResource(String.format("firebase/%s.json", projectId)).getPath());
} catch (FileNotFoundException e) {
e.printStackTrace();
return;
}
FirebaseOptions options = new FirebaseOptions.Builder().setCredential(FirebaseCredentials.fromCertificate(serviceAccount))
.setDatabaseUrl(String.format("https://%s.firebaseio.com/", projectId))
.build();
FirebaseApp firebaseApp = FirebaseApp.initializeApp(options, projectId);
logger.info("FirebaseApp initialized");
});
return FirebaseApp.getInstance();
}
}
Also for Database:
FirebaseDatabaseProvider.java
public class FirebaseDatabaseProvider implements Provider<FirebaseDatabase> {
private final FirebaseApp firebaseApp;
public static List<TaxItem> TAXES = new ArrayList<>();
#Inject
public FirebaseDatabaseProvider(FirebaseApp firebaseApp) {
this.firebaseApp = firebaseApp;
fetchTaxes();
}
#Singleton
#Override
public FirebaseDatabase get() {
return FirebaseDatabase.getInstance(firebaseApp);
}
#Singleton
public DatabaseReference getUserDataReference() {
return this.get().getReference("/usersData");
}
#Singleton
public DatabaseReference getTaxesConfigurationReference() {
return this.get().getReference("/appData/taxConfiguration");
}
private void fetchTaxes() {
DatabaseReference bundlesRef = getTaxesConfigurationReference().child("taxes");
bundlesRef.addValueEventListener(new ValueEventListener() {
#Override
public void onDataChange(DataSnapshot dataSnapshot) {
TAXES.clear();
dataSnapshot.getChildren().forEach(tax -> TAXES.add(tax.getValue(TaxItem.class)));
Logger.info(String.format("==> %d taxes records loaded", TAXES.size()));
}
#Override
public void onCancelled(DatabaseError databaseError) {
Logger.warn("The read failed: " + databaseError.getCode());
}
});
}
}
So I bind them as well from Module.java:
public class Module extends AbstractModule {
#Override
public void configure() { bind(FirebaseApp.class).toProvider(FirebaseAppProvider.class).asEagerSingleton();
bind(FirebaseAuth.class).toProvider(FirebaseAuthProvider.class).asEagerSingleton();
bind(FirebaseDatabase.class).toProvider(FirebaseDatabaseProvider.class).asEagerSingleton();
}
}
my ActionCreator:
public class ActionCreator implements play.http.ActionCreator {
#Inject
public ActionCreator() {
}
#Override
public Action createAction(Http.Request request, Method actionMethod) {
switchTenancyId(request);
return new Action.Simple() {
#Override
public CompletionStage<Result> call(Http.Context ctx) {
return delegate.call(ctx);
}
};
}
private void switchTenancyId(Http.RequestHeader request) {
// DO something here
}
private Optional<String> getTenancyId(Http.RequestHeader request) {
String websiteId = request.getHeader("Website-ID");
System.out.println(websiteId);
return null;
}
}
What I want is when I use Database service, or auth service, I read the website id and decide which firebase project to access, I really tried the solution like this answer here:
Multi tenancy with Guice Custom Scopes and Jersey
Please note I'm willing to use differents projects, not the same firebase project for each front-end.
But kinda lost, especially the request can be only accessed from controller or ActionCreator, so what I got from the question above is load providers by key into ThreadLocal and switch them for each request depends on the annotation, but I was unable to do this because of the lack of knowledge.
The minimized version of my project can be found here: https://github.com/almothafar/play-with-multi-tenant-firebase
Also, I uploaded taxes-data-export.json file to import inside firebase project for a test.
Right, so I know Play a lot better than FireBase, but it seems to me you want to extract a tenancy ID from the request prior to feeding this into your FrieBase backend? Context when writing Java in play is Thread local, but even when doing things async you can make sure the Http.context info goes along for the ride by injecting the execution context. I would not do this via the action creator, unless you want to intercept which action is called. (Though I have a hackish solution for that as well.)
So, after a comment I'll try to elucidate here, your incoming request will be routed to a controller, like below (let me know if you need clearing up on routing etc):
Below is a solution for caching a retrieved FireBaseApp based on a "Website-ID" retrieved from the request, though I would likely put the tenancyId in the session.
import javax.inject.Inject;
import java.util.concurrent.CompletionStage;
public class MyController extends Controller {
private HttpExecutionContext ec; //This is the execution-context.
private FirebaseAppProvider appProvider;
private CacheApi cache;
#Inject
public MyController(HttpExecutionContext ec, FireBaseAppProvider provider,CacheApi cache) {
this.ec = ec;
this.appProvider = provider;
this.cache = cache;
}
/**
*Retrieves a website-id from request and attempts to retrieve
*FireBaseApp object from Cache.
*If not found a new FireBaseApp will be constructed by
*FireBaseAppProvider and cached.
**/
private FireBaseApp getFireBaseApp(){
String tenancyId = request.getHeader("Website-ID);
FireBaseApp app = (FireBaseApp)cache.get(tenancyId);
if(app==null){
app=appProvider.get();
cache.put(tenancyId,app);
}
return app;
}
public CompletionStage<Result> index() {
return CompletableFuture.supplyAsync(() -> {
FireBaseApp app = getFireBaseApp();
//Do things with app.
}, ec.current()); //Used here.
}
}
Now in FireBaseAppProvider you can access the header via play.mvc.Controller, the only thing you need is to provide the HttpExecutionContext via ec.current. So (once again, I'm avoiding anything FireBase specific), in FireBaseProvider:
import play.mvc.Controller;
public class FireBaseAppProvider {
public String getWebsiteKey(){
String website = Controller.request().getHeader("Website-ID");
//If you need to handle a missing header etc, do it here.
return website;
}
public FireBaseApp get(){
String tenancyId = getWebsiteKey();
//Code to do actual construction here.
}
}
Let me know if this is close to what you're asking and I'll clean it up for you.
Also, if you want to store token validations etc, it's best to put them in the "session" of the return request, this is signed by Play Framework and allows storing data over requests. For larger data you can cache this using the session-id as part of the key.
I believe Custom Scopes for this is overkill. I would recommend doing the Request-Scoped seeding from Guice's own wiki. In your case that would be something like
public class TenancyFilter implements Filter {
#Override
public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
HttpServletRequest httpRequest = (HttpServletRequest) request;
String tenancyId = httpRequest.getHeader("YOUR-TENANCY-ID-HEADER-NAME");
httpRequest.setAttribute(
Key.get(String.class, Names.named("tenancyId")).toString(),
userId
);
chain.doFilter(request, response);
}
#Override
public void init(FilterConfig filterConfig) throws ServletException { }
#Override
public void destroy() { }
};
It has to be bound in a ServletModule
public class YourModule extends ServletModule {
#Override
protected void configureServlets() {
filter("/*").through(TenancyFilter.class);
}
#Provides
#RequestScoped
#Named("tenancyId")
String provideTenancyId() {
throw new IllegalStateException("user id must be manually seeded");
}
}
Then anywhere you need to get the Tenancy ID you just inject
public class SomeClass {
private final Provider<String> tenancyIdProvider;
#Inject
SomeClass(#Named("tenancyId") Provider<String> tenancyIdProvider) {
this.tenancyIdProvider = tenancyIdProvider;
}
// Methods in request call tenancyIdProvider.get() to get and take action based on Tenancy ID.
}

Unable to intercept and manipulate HttpServletResponse in Spring Boot

I have a requirement to Base64 decode every JSON request payload that my Spring Boot service receives. The JSON payload would have been Base64 encoded at the client before posting using the HTTP POST method. Further, I also need to Base64 encode the JSON response before presenting to the calling client application.
I am required to reduce boilerplate code by using handler interceptors.
I have already achieved the request/incoming leg of the operation by the use of interceptors but is yet to achieve this for the response leg.
I have posted the code snippets below. The code to intercept the response and base64 encode it is in the postHandle method of the interceptor class.
What am I doing wrong here?
Interceptor Class:
public class Base64ResponseEncodingInterceptor implements HandlerInterceptor {
private static final String DECODED_REQUEST_ATTRIB = "decodedRequest";
private static final String POST = "POST";
#Override
public void postHandle(HttpServletRequest request, HttpServletResponse response, Object handler, ModelAndView arg3) throws Exception {
try {
if (POST.equalsIgnoreCase(request.getMethod())) {
CharResponseWrapper res = new CharResponseWrapper(response);
res.getWriter();
byte[] encoded = Base64.encodeBase64(res.toString().getBytes());
byte[] encoded = Base64.encodeBase64(response.getHeader(ENCODED_RESPONSE_ATTRIB).getBytes());
response.getWriter().write(new String(encoded));
}
} catch (Exception e) {
throw new Exception(e.getMessage());
}
}
// preHandle and afterCompletion methods
// Omitted
}
The CharResponseWrapper Class used above:
public class CharResponseWrapper extends HttpServletResponseWrapper {
protected CharArrayWriter charWriter;
protected PrintWriter writer;
protected boolean getOutputStreamCalled;
protected boolean getWriterCalled;
public CharResponseWrapper(HttpServletResponse response) {
super(response);
charWriter = new CharArrayWriter();
}
#Override
public ServletOutputStream getOutputStream() throws IOException {
if (getWriterCalled) {
throw new IllegalStateException("getWriter already called");
}
getOutputStreamCalled = true;
return super.getOutputStream();
}
#Override
public PrintWriter getWriter() throws IOException {
if (writer != null) {
return writer;
}
if (getOutputStreamCalled) {
throw new IllegalStateException("getOutputStream already called");
}
getWriterCalled = true;
writer = new PrintWriter(charWriter);
return writer;
}
#Override
public String toString() {
String s = null;
if (writer != null) {
s = charWriter.toString();
}
return s;
}
}
JavaConfig Class where Interceptor is registered:
#Configuration
#EnableJpaRepositories(repositoryBaseClass = BaseRepositoryBean.class, basePackages = "")
#EntityScan(basePackages = { "com.companyname", "com.companyname.productname"})
public class RestConfig extends WebMvcConfigurerAdapter {
#Override
public void addInterceptors(InterceptorRegistry registry) {
registry.addInterceptor(new Base64ResponseEncodingInterceptor());
}
}
The Controller Class, where the Interceptor is used (Only the working request leg is shown here):
#Autowired
HttpServletRequest request;
String decodedRequest = null;
#ModelAttribute("decodedRequest")
public void getDecodedParam(){
decodedRequest = (String) request.getAttribute("decodedRequest");
}
The code in the postHandle method does not work. It is either the HttpServletResponse is null or I get an exception message:
getOutputStream already called
Update: Work around solution to reading the response directly in the ResponseBodyAdvice
In the Controller Class, I added the following:
#RestController
#RequestMapping("/api/ipmanager")
public class IPProfileRestController extends AbstractRestController {
#Autowired
HttpServletResponse response;
String encodedResponse = null;
#ModelAttribute("encodedResponse")
public void getEncodedResponse(){
response.setHeader("encodedResponse", StringUtils.EMPTY);
}
#RequestMapping(value = "/time", method = { RequestMethod.POST }, produces = { MediaType.APPLICATION_JSON_VALUE, MediaType.TEXT_PLAIN_VALUE }, consumes = {
MediaType.APPLICATION_JSON_VALUE })
public #ResponseBody String saveAccessClientTime(#RequestBody String ecodedRequest) {
// Some code here
String controllerResponse = prettyJson(iPProfileResponse);
response.setHeader("encodedResponse", controllerResponse);
return controllerResponse;
}
}
I have the following in the ResponseBodyAdvice
#ControllerAdvice
public class Base64EncodedResponseBodyAdvice implements ResponseBodyAdvice<Object> {
#Override
public boolean supports(MethodParameter returnType,
Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}
#Override
public Object beforeBodyWrite(Object body,
MethodParameter returnType,
MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> converterType,
ServerHttpRequest request,
ServerHttpResponse response) {
String body1 = StringUtils.EMPTY;
// Encode the response and return
List<String> listOfHeaderValues = response.getHeaders().get("encodedResponse");
body1=new String(Base64.encodeBase64(listOfHeaderValues.get(0).getBytes()));
return body1;
}
}
As the Spring MVC documentation states:
the postHandle method of HandlerInterceptor is not always ideally
suited for use with #ResponseBody and ResponseEntity methods. In such
cases an HttpMessageConverter writes to and commits the response
before postHandle is called which makes it impossible to change the
response, for example to add a header. Instead an application can
implement ResponseBodyAdvice and either declare it as an
#ControllerAdvice bean or configure it directly on
RequestMappingHandlerAdapter.
With that being said:
What am I doing wrong here?
Since the response has been already committed, you can't change it. In order to change the response you should register a ResponseBodyAdvice<T> and put your response encoding logic there:
#ControllerAdvice
public class Base64EncodedResponseBodyAdvice implements ResponseBodyAdvice<Object> {
#Override
public boolean supports(MethodParameter returnType,
Class<? extends HttpMessageConverter<?>> converterType) {
return true;
}
#Override
public Object beforeBodyWrite(Object body,
MethodParameter returnType,
MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> converterType,
ServerHttpRequest request,
ServerHttpResponse response) {
// Encode the response and return
}
}
If your method is returning ResponseEntity<T> then write this code in postHandle() method of your HandlerInterceptor:
eg. response.addHeader("jwt_token", "kajdlakjd");
it will work!!

How do I design a generic Response builder / RESTful Web Service using Spring MVC?

Trying to build a RESTful web service using Spring MVC.
The controller should return specific Java types, but the response body must be a generic envelope. How can this be done?
The following sections of code are what I have so far:
Controller method:
#Controller
#RequestMapping(value = "/mycontroller")
public class MyController {
public ServiceDetails getServiceDetails() {
return new ServiceDetails("MyService");
}
}
Response envelope:
public class Response<T> {
private String message;
private T responseBody;
}
ServiceDetails code:
public class ServiceDetails {
private String serviceName;
public ServiceDetails(String serviceName) {
this.serviceName = serviceName;
}
}
Intended final response to clients should appear as:
{
"message" : "Operation OK"
"responseBody" : {
"serviceName" : "MyService"
}
}
What you can do is having a MyRestController just wrapping the result in a Response like this:
#Controller
#RequestMapping(value = "/mycontroller")
public class MyRestController {
#Autowired
private MyController myController;
#RequestMapping(value = "/details")
public #ResponseBody Response<ServiceDetails> getServiceDetails() {
return new Response(myController.getServiceDetails(),"Operation OK");
}
}
This solution keep your original MyController independant from your REST code. It seems you need to include Jackson in your classpath so that Spring will auto-magically serialize to JSON (see this for details)
EDIT
It seems you need something more generic... so here is a suggestion.
#Controller
#RequestMapping(value = "/mycontroller")
public class MyGenericRestController {
#Autowired
private MyController myController;
//this will match all "/myController/*"
#RequestMapping(value = "/{operation}")
public #ResponseBody Response getGenericOperation(String #PathVariable operation) {
Method operationToInvoke = findMethodWithRequestMapping(operation);
Object responseBody = null;
try{
responseBody = operationToInvoke.invoke(myController);
}catch(Exception e){
e.printStackTrace();
return new Response(null,"operation failed");
}
return new Response(responseBody ,"Operation OK");
}
private Method findMethodWithRequestMapping(String operation){
//TODO
//This method will use reflection to find a method annotated
//#RequestMapping(value=<operation>)
//in myController
return ...
}
}
And keep your original "myController" almost as it was:
#Controller
public class MyController {
//this method is not expected to be called directly by spring MVC
#RequestMapping(value = "/details")
public ServiceDetails getServiceDetails() {
return new ServiceDetails("MyService");
}
}
Major issue with this : the #RequestMapping in MyController need probably to be replaced by some custom annotation (and adapt findMethodWithRequestMapping to perform introspection on this custom annotation).
By default, Spring MVC uses org.springframework.http.converter.json.MappingJacksonHttpMessageConverter to serialize/deserialize JSON through Jackson.
I'm not sure if it's a great idea, but one way of solving your problem is to extend this class, and override the writeInternal method:
public class CustomMappingJacksonHttpMessageConverter extends MappingJacksonHttpMessageConverter {
#Override
protected void writeInternal(Object object, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException {
super.writeInternal(new Response(object, "Operation OK"), outputMessage);
}
}
If you're using XML configuration, you could enable the custom converter like this:
<mvc:annotation-driven>
<mvc:message-converters>
<bean class="path.to.CustomMappingJacksonHttpMessageConverter">
</mvc:message-converters>
</mvc:annotation-driven>
Try the below solution.
Create a separate class such ResponseEnvelop. It must implement ResponseBodyAdvice interface.
Annotate the above class with #ControllerAdvice
Autowire HttpServletRequest
Override methods according to your requirement. Take reference from below.
#Override
public boolean supports(
MethodParameter returnType, Class<? extends HttpMessageConverter<?>> converterType) {
if (httpServletRequest.getRequestURI().startsWith("/api")) {
return true;
}
return false;
}
#Override
public Object beforeBodyWrite(
Object body,
MethodParameter returnType,
MediaType selectedContentType,
Class<? extends HttpMessageConverter<?>> converterType,
ServerHttpRequest request,
ServerHttpResponse response) {
if (((ServletServerHttpResponse) response).getServletResponse().getStatus()
== HttpStatus.OK.value()
|| ((ServletServerHttpResponse) response).getServletResponse().getStatus()
== HttpStatus.CREATED.value()) {
return new EntityResponse(Constants.SUCCESS, body);
}
return body;
}

Categories

Resources