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

标签 java design-patterns dependency-injection singleton global-state

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

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

让我向您介绍我所写内容的更多背景信息。该架构由以下组件组成:

  • 向 REST API 发送请求的客户端上传或检索“保存对象”,或者简单地说,JSON 或 XML 格式的 PO(文件 + 元数据)。

  • 高级 REST API,用于接收来自客户端的请求并将数据存储在存储层中。

  • 存储层,可能包含 OpenStack Swift 容器、磁带库和文件系统的组合。每个“存储容器”(为简单起见,我将其称为文件系统容器)在我的架构中称为端点。存储层显然并不与 REST API 位于同一服务器上。

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

我的问题是:如何以可测试、可重用且优雅的方式存储和加载配置?我什至不会考虑将配置对象传递给实现 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 的评论 **

在下面找到我使用建议答案的实现。工厂创建实现较低级别存储操作的具体策略对象。上下文对象(由中间件来回传递)包含抽象类型的对象(在本例中为接口(interface))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 类中构建一个返回适当“配置”对象的工厂是一种非常干净的方法。但是如果您知道正在讨论的论文的“上下文”(是的,重载用法),它主要用于中间件。存在多层上下文变化的地方。请注意,在“实现”部分下,建议使用 Strategy Pattern了解如何将每个图层的“上下文信息”添加到“上下文对象”。

我会推荐类似的方法。每个“存储容器”都有不同的与之相关的策略。因此,每个“驱动程序”都有自己的策略实现。类(class)。该策略将从工厂获得,然后根据需要使用。 (如何设计你的 Strats...最好的方法(我猜)是让你的“驱动程序 strat”对于每个驱动程序类型都是通用的,然后在新资源出现/分配 strat 对象时对其进行适当配置)

但据我现在所知(除非我错误地阅读了你的问题),这只会有 2 个“上下文对象”能够感知的“层”,即“其余服务器”和“存储端点”。如果我错了,那就这样吧......但只有 2 层,你可以像你思考“上下文模式”一样使用“策略模式”,并避免单例/上下文“反模式”的问题'。 (您“可以”有一个上下文对象,其中包含要使用哪个驱动程序的策略,然后是该驱动程序的“配置”......这不会很疯狂,并且可能非常适合您的动态 HTTP 配置。)

策略工厂类也不必是单例/具有静态工厂方法。我之前已经制作过作为对象的工厂,即使使用 D.I.供测试用。不同的方法总是需要权衡,但我发现在我遇到的几乎所有情况下,更好的测试都是值得的。

关于java - 存储和加载 REST 服务器的配置,避免全局状态(即单例、上下文、依赖注入(inject)),我们在Stack Overflow上找到一个类似的问题: https://stackoverflow.com/questions/34437812/

相关文章:

java - 如何维护自动连线 Web 服务的状态?

Angularjs 4 - 注入(inject)服务而不在构造函数上传递参数

c# - 使用依赖注入(inject)处理对象

c# - 使用缓存依赖项对 MVC 操作方法进行单元测试?

Java/Mockito - 线程在重试之前以指数方式 hibernate 的单元测试

java - 证明 SimpleDateFormat 不是线程安全的

java - 抽象方法和构造函数

java - 如何将 SQL 数据映射到 Java JSON 对象和 JSON 数组?

java - 使多态性打败那些 switch/case 语句的麻烦

python - 在 Python 中管理连接创建?