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

3

10

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"
                      }

   }  
Inappreciative answered 23/1, 2013 at 16:35 Comment(5)
If you are using Jackson or FlexJson, why can't you just return the Response object in your method instead? The JSON serializer should be able to serialize any complex object.Caudex
i am using jackson. but i still would like the controller to return request specific java types. i think its cleaner and more intuitive.Inappreciative
plus there is some boiler plate code into constructing the response object, so i would like to do it just from one placeInappreciative
What I was recommending is the same approach that @ben75 stated: Create your wrapper object, return it, let Jackson handle your serialization to JSON. That's how I have ALWAYS seen it done. You can either return the object out-right, or use Jackson to convert to a String and return that. I have used the former when I didn't really care what the resulting JSON looked like (i.e. I am controlling the reciever, so I can make it do whatever is needed), and the latter approach when I don't control the reciever (i.e. someone else has dictated the formatting).Caudex
The key is the (at)ResponseBody which tells Spring to take whatever is being returned and use that as the HttpResponse body, and some nifty PFM from Spring to auto-convert objects to JSON using Jackson. I have even used (at)ResponseBody to return binary files, like images.Caudex
G
0

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).

Galway answered 23/1, 2013 at 16:57 Comment(2)
the thing is that i want to avoid writing new Response() a lot of times. i want for the person implementing the controller methods to not have to be aware of the Response envelope, but just return a specific type and for interceptor to take that type and insert into the response envelope.Inappreciative
thanks so much for the thorough reploy. but i really want to avoid a somewhat wrapper controller, plus i dont really like using reflection all that much. it looks like my best solution will come from something like what reidarok wrote below. can you share your insight on that maybe? thanksInappreciative
D
0

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>
Distant answered 24/1, 2013 at 13:5 Comment(2)
that looks nice. but that means this converter will be used for all of my controllers? i need it just for a specific one...is it possible? basically what i though was to override the MappingJacksonJsonView and using the custom view as the view for that specific controller, but that doesnt seem to work. spring always uses the default jackson view for some reasonInappreciative
I'm really not sure how to set a specific converter for just a single controller. Tedious, but maybe an acceptable solution would be to create a separate spring context (i.e. servlet) for this specific controller?Distant
P
0

Try the below solution.

  1. Create a separate class such ResponseEnvelop. It must implement ResponseBodyAdvice interface.
  2. Annotate the above class with @ControllerAdvice
  3. Autowire HttpServletRequest
  4. 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;
  }
Pontus answered 27/10, 2022 at 22:47 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.