Java 如何配置SpringREST服务来处理多个版本?

Java 如何配置SpringREST服务来处理多个版本?,java,spring,rest,spring-mvc,spring-ws,Java,Spring,Rest,Spring Mvc,Spring Ws,几乎所有的API都处理不同的发布版本。您经常会看到这种版本控制: 但是我还没有找到一个描述如何在Spring堆栈中组织它们的源代码。我想在每个控制器上都有/v1前缀,比如@RequestMapping(“/v1/questions”)并不是最好的方法 想象一下,只有当前版本(在我们的例子中是V2)有一个@Service层 我们的服务应该处理V1和V2的请求。唯一的变化是V2在问题实体上添加了一个新字段(这意味着V1问题可以轻松转换为V2问题) 现在的问题是: 如何从java包的角度组

几乎所有的API都处理不同的发布版本。您经常会看到这种版本控制:

但是我还没有找到一个描述如何在Spring堆栈中组织它们的源代码。我想在每个控制器上都有
/v1
前缀,比如
@RequestMapping(“/v1/questions”)
并不是最好的方法

想象一下,只有当前版本(在我们的例子中是V2)有一个
@Service

我们的服务应该处理V1和V2的请求。唯一的变化是V2在问题实体上添加了一个新字段(这意味着V1问题可以轻松转换为V2问题)

现在的问题是:

  • 如何从java包的角度组织不同的
    ~.web.*@Controller
  • 如何对不同的
    ~.web.*@Controller
    进行注释,说明它们知道自己的版本?以
    请求映射的方式
    ?或者可以在v1java包中使用context:component扫描配置它们吗
  • 如何组织转换器?放在哪里,如何命名?Smth。像问题1到2控制器
  • 是否需要DTO层?因为我们的域名必须同时处理多个版本
示例如下(我在所有地方都添加了包):


这就是我们在项目中所做的:

  • 包裹
    • {developer}.{customer}.{project}.core
      -
      @Service
      @Dao
      东西
    • {developer}.{customer}.{project}.core.model
      -模型实体,这就是整个
      core
      包的工作原理
    • {developer}.{customer}.{project}.api.rest.v1.resource
      -
      @Controller
      rest资源
    • {developer}.{customer}.{project}.api.rest.v1.model
      -v1api的DTOs
    • {developer}.{customer}.{project}.api.rest.v1.mapper
      -DTO和核心模型实体之间的映射器
如果核心不断发展(并且不断发展),那么它只涉及API映射器。资源和DTO保持不变

自从我们引入v1API以来,我们的核心及其模型发生了很大变化。当然,我们正在对v1做一些向后兼容的更改-引入新的搜索参数、资源或基于参数的响应修饰符

我必须说,我们还没有发展到v2API,但这已经是过去的事了,因为v1已经是史前的,并且在某些方面与核心模型不兼容(不同的模型属性、不同的模型关系、过时和不一致的命名…)

如果我们想在版本之间重用一些代码,我可以想象它将被放置在
{developer}.{customer}.{project}.api.rest.base

在每个REST资源中手动编写请求映射。因此,每个资源在其映射中都有
/v1/..


我不认为我们的方法是正确的解决方案,甚至不认为我们的方法接近正确的解决方案。但这很简单。我们将看到v2将如何适应。RESTAPI的版本控制是一个复杂的问题。首先,让我们确定一些高级版本控制方法:

  • URI版本控制-资源被认为是不可变的,我们使用版本指示符在资源表示中创建一个新的URI空间
  • 语言扩展/版本控制-考虑到正在更改的是资源的表示,此解决方案将在不影响URI空间的情况下对表示本身进行版本化

记住这一点,让我们考虑一些<强>目标< /强>:(直接)< /P>

  • 将兼容更改保留在名称之外
  • 避免新的主要版本
  • 使更改向后兼容
  • 考虑向前兼容性

接下来,让我们考虑到API:

的一些<强>可能的更改< /强> 1.添加到资源的表示中 语言的设计应该明确地考虑到向前兼容性,客户端应该忽略他们不理解的信息

因此,向资源的表示添加信息并不是不兼容的更改

2.删除或更改现有表达 这种对语言的扩展/更改可以利用Accept标头和内容协商—表示使用自定义供应商MIME媒体类型进行版本控制。这些文章更深入地讨论了这一点:

因此,这确实代表了客户机的不兼容更改—客户机必须请求新的表示并理解新的语义,但URI空间将保持稳定且不会受到影响

3.标准不兼容更改 这些都是资源的含义和它们之间关系的变化。 在本例中,我们可以考虑更改资源和URI结构之间的映射。但是,这并不一定意味着在URI中使用版本指示符

RESTAPI应该遵守HATEOAS约束——大多数URI应该由客户端发现,而不是硬编码。更改这样的URI不应被视为不兼容的更改-新的URI可以替换旧的URI,客户端将能够重新发现URI并仍然运行

4.不兼容的重大变化 对于这种全面的更改,URI中的版本指示器是最后的解决方案

在技术方面,我发现:
  • DAO和服务层不应根据版本进行更改
  • 在服务层之上使用Facade层(和DTO)意味着每个版本都有自己的Facade和DTO
  • 为了清晰地区分这两个版本,使用两个
    DispatcherServlets
    创建两个web上下文可能是有意义的
    // on V1 Question look like this:
    public class project.domain.Question
    {
        private String question;
    }
    
    // on v2 Question looks like this:
    public class project.domain.Question
    {
        private String question;
        private Date creationDate;
    }
    
    
    @Service
    public class project.service.QuestionService
    {
        public long create(Question q) {...};
        public Question read(long id) {...};
        public void remove(long id) {...};
        public void update(Question qd) {...};
    
    }
    
    
    @Controller
    @RequestMapping("/v2/question")
    public class project.web.v2.QuestionController
    {
        @Autowired
        project.service.QuestionService questionService;
    
        @RequestMapping(method = RequestMethod.POST)
        @ResponseBody
        public long create(Question q)
        {
            return questionService.create(q);
        }
    }
    
    @Controller
    @RequestMapping("/v1/question")
    public class project.web.v1.QuestionController
    {
        @Autowired
        project.service.QuestionService questionService;
    
        @RequestMapping(method = RequestMethod.POST)
        @ResponseBody
        public long create(Question q)
        {
            // this will not work, because the v1 haven't had the 'creationDate' field.
            return questionService.create(q);
        }
    }