存储和加载 REST 服务器避免全局状态的配置(即单例、上下文和依赖注入)

Storing and loading configuration for REST server avoding global state (i.e. singleton vs. context vs. dependency injection)

我正在使用 tomcat 在 Java 中开发一个架构,我遇到了一个我认为非常普遍的情况,但是在阅读了 Whosebug 中的几个 questions/answers 之后,我找不到确定的答案。我的架构有一个 REST API(运行 on tomcat)接收一个或多个文件及其关联的元数据并将它们写入存储。存储层的配置与 REST API 服务器具有 1-1 的关系,因此直观的方法是编写一个 Singleton 来保存该配置。

显然我知道由于全局状态和模拟单例的困难,单例带来了可测试性问题。我也考虑过使用 Context 模式,但我不相信 Context 模式适用于这种情况,我担心我最终会使用 "Context anti-pattern" 来编码。

让我为您提供更多有关我所写内容的背景知识。该架构由以下组件组成:

端点的配置是通过 REST API(例如 POST /configEndpoint)完成的,因此管理用户可以通过 HTTP 调用注册新端点、编辑或删除现有端点。虽然我只使用 OpenStack Swift 端点实现了架构,但我预计每个端点的信息至少包含一个 IP 地址、某种形式的身份验证信息和一个驱动程序名称,例如"the Swift driver"、"the LTFS driver"等(这样当新的存储技术到来时,只要有人为它编写驱动程序,它们就可以很容易地集成到我的架构中)。

我的问题是:如何以可测试、可重用和优雅的方式存储和加载配置?我什至不会考虑将配置对象传递给实现 REST API 调用的所有各种方法。

几个 REST API 调用示例以及配置发挥作用的地方:

// Retrieve a preservation object metadata (PO)
@GET
@Path("container/{containername}/{po}")
@Produces({ MediaType.APPLICATION_JSON, MediaType.APPLICATION_XML })
public PreservationObjectInformation getPOMetadata(@PathParam("containername") String containerName, @PathParam("po") String poUUID) {

    // STEP 1 - LOAD THE CONFIGURATION
    // One of the following options:
    // StorageContext.loadContext(containerName);
    // Configuration.getInstance(containerName);
    // Pass a configuration object as an argument of the getPOMetadata() method?
    // Some sort of dependency injection

    // STEP 2 - RETRIEVE THE METADATA FROM THE STORAGE
    // Call the driver depending on the endpoint (JClouds if Swift, Java IO stream if file system, etc.)
    // Pass poUUID as parameter

    // STEP 3 - CONVERT JSON/XML TO OBJECT
    // Unmarshall the file in JSON format
    PreservationObjectInformation poi = unmarshall(data);

    return poi;
}


// Delete a PO
@DELETE
@Path("container/{containername}/{po}")
public Response deletePO(@PathParam("containername") String containerName, @PathParam("po") String poName) throws IOException, URISyntaxException {

    // STEP 1 - LOAD THE CONFIGURATION
    // One of the following options:
    // StorageContext.loadContext(containerName); // Context
    // Configuration.getInstance(containerName); // Singleton
    // Pass a configuration object as an argument of the getPOMetadata() method?
    // Some sort of dependency injection

    // STEP 2 - CONNECT TO THE STORAGE ENDPOINT
    // Call the driver depending on the endpoint (JClouds if Swift, Java IO stream if file system, etc.)

    // STEP 3 - DELETE THE FILE

    return Response.ok().build();
}


// Submit a PO and its metadata
@POST
@Consumes(MediaType.MULTIPART_FORM_DATA)
@Path("container/{containername}/{po}")
public Response submitPO(@PathParam("containername") String container, @PathParam("po") String poName, @FormDataParam("objectName") String objectName,
        @FormDataParam("inputstream") InputStream inputStream) throws IOException, URISyntaxException {

    // STEP 1 - LOAD THE CONFIGURATION
    // One of the following options:
    // StorageContext.loadContext(containerName);
    // Configuration.getInstance(containerName);
    // Pass a configuration object as an argument of the getPOMetadata() method?
    // Some sort of dependency injection

    // STEP 2 - WRITE THE DATA AND METADATA TO STORAGE
    // Call the driver depending on the endpoint (JClouds if Swift, Java IO stream if file system, etc.)

    return Response.created(new URI("container/" + container + "/" + poName))
            .build();
}

** 更新#1 - 我的实现基于@mawalker 的评论**

使用建议的答案在下面找到我的实现。工厂创建具体的策略对象来实现较低级别的存储操作。上下文对象(由中间件来回传递)包含抽象类型的对象(在本例中为接口)StorageContainerStrategy(其实现将取决于运行时每个特定情况下的存储类型)。

public interface StorageContainerStrategy {
    public void write();
    public void read();

    // other methods here
}

public class Context {
    public StorageContainerStrategy strategy;

    // other context information here...
}

public class StrategyFactory {
    public static StorageContainerStrategy createStorageContainerStrategy(Container c) {
        if(c.getEndpoint().isSwift())
            return new SwiftStrategy();
        else if(c.getEndpoint().isLtfs())
            return new LtfsStrategy();
        // etc.
        return null;
    }
}

public class SwiftStrategy implements StorageContainerStrategy {
    @Override
    public void write() {
        // OpenStack Swift specific code
    }

    @Override
    public void read() {
        // OpenStack Swift specific code
    }
}

public class LtfsStrategy implements StorageContainerStrategy {
    @Override
    public void write() {
        // LTFS specific code
    }

    @Override
    public void read() {
        // LTFS specific code
    }
}

这是 Doug Schmidt(完全公开了我目前的博士生导师)写的关于上下文对象模式的论文。

https://www.dre.vanderbilt.edu/~schmidt/PDF/Context-Object-Pattern.pdf

正如 dbugger 所述,在您的 api class 中构建一个工厂 returns 适当的 'configuration' 对象是一种非常简洁的方法。但是如果你知道所讨论的论文的'context'(是的,重载用法),它主要用于中间件。有多层上下文变化的地方。请注意,在 'implementation' 部分下,它建议使用 Strategy Pattern 来了解如何将每个图层的 'context information' 添加到 'context object'。

我会推荐类似的方法。每个 'storage container' 都有与之相关的不同策略。因此,每个 "driver" 都有自己的策略含义。 class。该策略将从工厂获得,然后根据需要使用。 (如何设计你的 Strats ......最好的方法(我猜)是让你的 'driver strat' 对每种驱动程序类型都是通用的,然后适当地配置它作为新资源 arise/the strat 对象是已分配)

但据我现在所知(除非我读错了你的问题),这只有 2 'layers' 'context object' 会知道,'rest server(s)' 和 'storage endpoints'。如果我弄错了,那就这样吧......但是只有 2 层,你可以像你想的那样使用 'strategy pattern' 'context pattern',并避免 singletons/Context 的问题'anti-pattern'。 (您 'could' 有一个上下文对象,其中包含要使用哪个驱动程序的策略,然后是该驱动程序的 'configuration' ... HTTP 配置。)

策略工厂 Class 也不是 'have to' 静态工厂方法。我之前已经创建了作为对象的工厂,即使使用 D.I 也是如此。用于检测。不同的方法总是需要权衡取舍,但我发现在我 运行 遇到的几乎所有情况下,更好的测试都是值得的。