Java(FX) - 只允许 1 class 从单例中调用方法 class

Java(FX) - Only allow 1 class to call a method from a singleton class

我目前正在开发一个项目,我在其中使用单例 class 来更改用户的视图。这个 DisplayManager(单例)有像 addView(View view)replaceView(View view) 这样的方法。但它也有一个名为 displayRootView() 的方法,它只能被调用一次(在初始化期间)并且只能被 1 class 调用,即扩展应用程序 class 的 StartUp class。

知道如何防止其他 class 使用单例的人调用 displayRootView() 方法吗?

我已经考虑过 StackTrace,但这似乎并不理想。我也许可以通过在 StartUp class 上使用标记界面将其与其余部分分开?

如有任何建议,我们将不胜感激。

当 displayRootView() 被多次调用时,我会抛出 IllegalStateException。

呃,这很难阻止某些 classes 调用你的方法,因为它违反了一些核心 OOP 原则。该方法不应该关心 调用它。这是基本的关注点分离——你的方法应该有一个关于它做什么的明确契约,而不是关于当时 JVM 的状态。

思考这些问题:

  • 如果你subclass StartUp会怎么样?例如,将桌面、移动和 Web 平台分开?
  • 如何在不涉及 StartUp 的情况下对该方法进行单元测试?
  • 如果你需要另一个抽象层会怎样?
  • 如果以及何时添加代理(通过 AOP 或 Spring 代理)怎么办?
  • 如果您需要从 Timer 调用方法会怎样?它仍然会从 StartUp class 源调用(并且是正确的),但它不会出现在堆栈跟踪中。

以及其他此类注意事项。

抛出某种异常(如 IllegalStateException 或自定义异常),以防第二次调用该方法是绝对有效的恕我直言。

这看起来您可能需要对您的代码进行静态检查,而不是代码内或运行时检查。我认为向 Findbugz 或 PMD 添加自定义规则以查找方法的所有直接调用并检查调用 class(如果从其他地方调用则构建失败)并不是非常困难。但我认为这样的检查实际上没有用。

最后,您需要在上述 class 之外合法使用该方法的可能性要大得多,而不是有人在收到警告后意外错误地使用它并且已创建适当的 Javadoc。

您可以考虑使用给定 here 的 "Romeo and Juliet" 技巧,最初是为了模拟 Java 中的 "friend" 机制(来自 C++)。

JavaFX Application source 通过解析堆栈跟踪来确定和检查调用者(例如,确保它仅从扩展应用程序的 class 调用)来做一些时髦的事情。

/**
 * Launch a standalone application. This method is typically called
 * from the main method(). It must not be called more than once or an
 * exception will be thrown.
 * This is equivalent to launch(TheClass.class, args) where TheClass is the
 * immediately enclosing class of the method that called launch. It must
 * be a subclass of Application or a RuntimeException will be thrown.
 *
 * <p>
 * The launch method does not return until the application has exited,
 * either via a call to Platform.exit or all of the application windows
 * have been closed.
 *
 * <p>
 * Typical usage is:
 * <ul>
 * <pre>
 * public static void main(String[] args) {
 *     Application.launch(args);
 * }
 * </pre>
 * </ul>
 *
 * @param args the command line arguments passed to the application.
 *             An application may get these parameters using the
 *             {@link #getParameters()} method.
 *
 * @throws IllegalStateException if this method is called more than once.
 */
public static void launch(String... args) {
    // Figure out the right class to call
    StackTraceElement[] cause = Thread.currentThread().getStackTrace();

    boolean foundThisMethod = false;
    String callingClassName = null;
    for (StackTraceElement se : cause) {
        // Skip entries until we get to the entry for this class
        String className = se.getClassName();
        String methodName = se.getMethodName();
        if (foundThisMethod) {
            callingClassName = className;
            break;
        } else if (Application.class.getName().equals(className)
                && "launch".equals(methodName)) {

            foundThisMethod = true;
        }
    }

    if (callingClassName == null) {
        throw new RuntimeException("Error: unable to determine Application class");
    }

    try {
        Class theClass = Class.forName(callingClassName, true,
                           Thread.currentThread().getContextClassLoader());
        if (Application.class.isAssignableFrom(theClass)) {
            Class<? extends Application> appClass = theClass;
            LauncherImpl.launchApplication(appClass, args);
        } else {
            throw new RuntimeException("Error: " + theClass
                    + " is not a subclass of javafx.application.Application");
        }
    } catch (RuntimeException ex) {
        throw ex;
    } catch (Exception ex) {
        throw new RuntimeException(ex);
    }
}

LauncherImpl 代码利用私有静态 AtomicBoolean 上的 getAndSet 操作来确保应用程序不会启动多次。

// Ensure that launchApplication method is only called once
private static AtomicBoolean launchCalled = new AtomicBoolean(false);

/**
 * This method is called by the standalone launcher.
 * It must not be called more than once or an exception will be thrown.
 *
 * Note that it is always called on a thread other than the FX application
 * thread, since that thread is only created at startup.
 *
 * @param appClass application class
 * @param preloaderClass preloader class, may be null
 * @param args command line arguments
 */
public static void launchApplication(final Class<? extends Application> appClass,
        final Class<? extends Preloader> preloaderClass,
        final String[] args) {

    if (launchCalled.getAndSet(true)) {
        throw new IllegalStateException("Application launch must not be called more than once");
    }

    if (! Application.class.isAssignableFrom(appClass)) {
        throw new IllegalArgumentException("Error: " + appClass.getName()
                + " is not a subclass of javafx.application.Application");
    }

    if (preloaderClass != null && ! Preloader.class.isAssignableFrom(preloaderClass)) {
        throw new IllegalArgumentException("Error: " + preloaderClass.getName()
                + " is not a subclass of javafx.application.Preloader");
    }

    // Create a new Launcher thread and then wait for that thread to finish
    final CountDownLatch launchLatch = new CountDownLatch(1);
    Thread launcherThread = new Thread(new Runnable() {
        @Override public void run() {
            try {
                launchApplication1(appClass, preloaderClass, args);
            } catch (RuntimeException rte) {
                launchException = rte;
            } catch (Exception ex) {
                launchException =
                    new RuntimeException("Application launch exception", ex);
            } catch (Error err) {
                launchException =
                    new RuntimeException("Application launch error", err);
            } finally {
                launchLatch.countDown();
            }
        }
    });
    launcherThread.setName("JavaFX-Launcher");
    launcherThread.start();

    // Wait for FX launcher thread to finish before returning to user
    try {
        launchLatch.await();
    } catch (InterruptedException ex) {
        throw new RuntimeException("Unexpected exception: ", ex);
    }

    if (launchException != null) {
        throw launchException;
    }
}

所以它有点复杂和奇怪,但如果您想要一个适用于 JavaFX 代码库的经过验证的解决方案,您可以尝试对其进行解析以了解正在发生的事情并根据您的情况进行调整。

我想说的是,只有在对您的应用程序来说至关重要的情况下,才将这种额外的复杂性引入您的应用程序。

Orodous 就此类逻辑对单元测试的阻碍程度提出了一些极好的观点。例如,看看这个关于 JavaFX application 的单元测试部分的建议。由于启动器中的奇怪检查,为了独立测试其应用程序的功能,开发人员需要通过奇怪的循环来绕过调用任何启动器代码(例如,使用基于 Swing 的 JFXPanel 而不是应用程序来初始化 JavaFX,因为应用程序只能启动一次)。