【问题标题】:Java(FX) - Only allow 1 class to call a method from a singleton classJava(FX) - 只允许 1 个类从单例类调用方法
【发布时间】:2015-01-23 15:53:51
【问题描述】:

我目前正在做一个项目,我正在使用单例类来更改用户的视图。此 DisplayManager(单例)具有 addView(View view)replaceView(View view) 之类的方法。但它也有一个名为 displayRootView() 的方法,它只能被调用一次(在初始化期间)并且只能由 1 个类调用,即扩展 Application 类的 StartUp 类。

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

我已经考虑过 StackTrace,但这似乎并不理想。我虽然可能通过在 StartUp 类上使用标记接口将其与其余部分分开?

任何建议将不胜感激。

【问题讨论】:

    标签: java methods javafx singleton


    【解决方案1】:

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

    考虑以下问题:

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

    以及其他类似的考虑。

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

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

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

    【讨论】:

    • 感谢您的快速回复;)
    【解决方案2】:

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

    /**
     * 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,因为应用程序只能启动一次)。

    【讨论】:

      【解决方案3】:

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

      【讨论】:

        【解决方案4】:

        您可以考虑使用here 给出的“罗密欧与朱丽叶”技巧,最初是为了模拟 Java 中的“朋友”机制(来自 C++)。

        【讨论】:

          猜你喜欢
          • 2011-08-11
          • 1970-01-01
          • 1970-01-01
          • 2017-04-06
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          • 1970-01-01
          相关资源
          最近更新 更多