如何使用 Spring MVC 设计通用响应构建器/RESTful Web 服务?

Posted

技术标签:

【中文标题】如何使用 Spring MVC 设计通用响应构建器/RESTful Web 服务?【英文标题】:How do I design a generic Response builder / RESTful Web Service using Spring MVC? 【发布时间】:2013-01-07 05:22:39 【问题描述】:

尝试使用 Spring MVC 构建 RESTful Web 服务。

控制器应返回特定的 Java 类型,但响应主体必须是通用信封。如何才能做到这一点?

以下代码部分是我目前所拥有的:

控制器方法:

    @Controller
    @RequestMapping(value = "/mycontroller")
    public class MyController 

        public ServiceDetails getServiceDetails() 
             return new ServiceDetails("MyService");
        
    

响应信封:

    public class Response<T> 

        private String message;
        private T responseBody;

    

ServiceDetails代码:

    public class ServiceDetails 

        private String serviceName;

        public ServiceDetails(String serviceName) 
            this.serviceName = serviceName;
        
    

对客户的预期最终响应应显示为:

   

     "message" : "Operation OK"
     "responseBody" : 
                        "serviceName" : "MyService"
                      

     

【问题讨论】:

如果您使用的是 Jackson 或 FlexJson,为什么不能直接在方法中返回 Response 对象呢? JSON 序列化器应该能够序列化任何复杂的对象。 我正在使用杰克逊。但我仍然希望控制器返回请求特定的 java 类型。我认为它更干净、更直观。 另外还有一些样板代码来构造响应对象,所以我想从一个地方做 我推荐的方法与@ben75 所述的方法相同:创建您的包装对象,返回它,让 Jackson 处理您对 JSON 的序列化。这就是我一直看到它完成的方式。您可以直接返回对象,也可以使用 Jackson 将其转换为字符串并返回。当我并不真正关心生成的 JSON 是什么样子(即我正在控制接收器,所以我可以让它做任何需要的事情)时,我使用了前者,而当我不控制接收器时,我使用了后一种方法(即其他人已指定格式)。 关键是 (at)ResponseBody,它告诉 Spring 接受返回的任何内容并将其用作 HttpResponse 主体,以及来自 Spring 的一些漂亮的 PFM 使用 Jackson 将对象自动转换为 JSON。我什至使用 (at)ResponseBody 来返回二进制文件,比如图像。 【参考方案1】:

您可以做的是让MyRestController 将结果包装在Response 中,如下所示:

@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");
    

此解决方案使您的原始 MyController 独立于您的 REST 代码。看来您需要在类路径中包含 Jackson,以便 Spring 自动神奇地序列化为 JSON(有关详细信息,请参阅this)

编辑

看来你需要更通用的东西......所以这里有一个建议。

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

并保持原来的“myController”几乎原样:

@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");
    

主要问题:MyController 中的 @RequestMapping 可能需要被一些自定义注解替换(并调整 findMethodWithRequestMapping 以对此自定义注解执行自省)。

【讨论】:

问题是我想避免写 new Response() 很多次。我希望实现控制器方法的人不必知道响应信封,而只需返回特定类型并让拦截器采用该类型并插入响应信封。 非常感谢您的彻底重新部署。但我真的想避免使用某种包装控制器,而且我不太喜欢使用反射。看起来我最好的解决方案将来自 reidarok 在下面写的内容。你能分享你的见解吗?谢谢【参考方案2】:

默认情况下,Spring MVC 使用 org.springframework.http.converter.json.MappingJacksonHttpMessageConverter 通过 Jackson 序列化/反序列化 JSON。

我不确定这是否是个好主意,但解决您的问题的一种方法是扩展此类,并覆盖 writeInternal 方法:

public class CustomMappingJacksonHttpMessageConverter extends MappingJacksonHttpMessageConverter 

    @Override
    protected void writeInternal(Object object, HttpOutputMessage outputMessage) throws IOException, HttpMessageNotWritableException 
        super.writeInternal(new Response(object, "Operation OK"), outputMessage);
    

如果您使用 XML 配置,您可以像这样启用自定义转换器:

<mvc:annotation-driven>
    <mvc:message-converters>
        <bean class="path.to.CustomMappingJacksonHttpMessageConverter">
    </mvc:message-converters>
</mvc:annotation-driven>

【讨论】:

看起来不错。但这意味着这个转换器将用于我所有的控制器?我只需要一个特定的...有可能吗?基本上我想覆盖 MappingJacksonJsonView 并使用自定义视图作为该特定控制器的视图,但这似乎不起作用。由于某种原因,spring 总是使用默认的 jackson 视图 我真的不确定如何为单个控制器设置特定的转换器。乏味,但也许可以接受的解决方案是为这个特定的控制器创建一个单独的 spring 上下文(即 servlet)?

以上是关于如何使用 Spring MVC 设计通用响应构建器/RESTful Web 服务?的主要内容,如果未能解决你的问题,请参考以下文章

带有 MVC 的 Spring 3 JSON

Spring MVC:通用 DAO 和服务类

哪种设计更好:通用构建器或几种具体方法?

如何在 Spring MVC 中代理 HTTP 请求?

基于Expression Lambda表达式树的通用复杂动态查询构建器——《构思篇二》已开源

构建通用WebSocket推送网关的设计与实践