防止API中的System.exit()调用

38

我正在使用一个第三方库,当它遇到异常时会执行System.exit()。我正在使用来自jar包的API。有没有办法阻止System.exit()调用,因为它会导致我的应用关闭?由于许多其他许可问题,我无法在删除System.exit()后反编译和重新编译jar文件。我曾经在stackoverflow上看到过一个答案(对于我不记得的其他问题),可以使用Java中的SecurityManager来做这样的事情。


4
按照定义,调用 System.exit() 的第三方库是有问题的。如果他们不修复这个问题,我会考虑寻找更高质量的替代品。 - Stephen C
请问您使用的是哪个库? - Dead Programmer
@Suresh...这是我所在公司拥有的一个库。透露名称违反了公司政策。 - Swaranga Sarma
1
我讨厌闭源库的另一个原因... - Daniel
4个回答

37

您可以安装一个安全管理器来禁用System.exit()

  private static class ExitTrappedException extends SecurityException { }

  private static void forbidSystemExitCall() {
    final SecurityManager securityManager = new SecurityManager() {
      public void checkPermission( Permission permission ) {
        if( "exitVM".equals( permission.getName() ) ) {
          throw new ExitTrappedException() ;
        }
      }
    } ;
    System.setSecurityManager( securityManager ) ;
  }

  private static void enableSystemExitCall() {
    System.setSecurityManager( null ) ;
  }

编辑Max在下面的评论中指出,从Java 6开始,权限名称实际上是“exitVM. ”+状态,例如“exitVM.0”。

然而,权限exitVM.*涵盖所有退出状态,而exitVM仍然保留为exitVM.*的简写形式,因此上述代码仍然有效(请参见RuntimePermission的文档)。


1
+1 - 这就是方法。它的意思是,如果第三方库调用了 System.exit(),它将触发一个 SecurityException...因此您的代码必须处理此情况。 - Stephen C
1
System.setSecurityManager(null); 对我来说似乎是一种非常hack的方法。开发者刚刚打开了JRE,使得任何代码都可以做任何事情。虽然我概述的扩展的 SecurityManager 也只是一个玩具版本,但至少它有潜力对第三方API提供更进一步的控制。此外,这种方法似乎可能会对启动自己线程的第三方API失败。 - Andrew Thompson
2
更新:从Java 6开始,权限名称实际上是“exitVM。”+状态,例如“exitVM.0”。 - Max
5
上述代码存在一些问题。1)尽管在“exitVM。”+状态和“exitVM.*”上有再次确认,并且在Java 6+中“exitVM”的等效性,但以上代码不会像原样工作,因为通配符权限应用于比被覆盖方法更低的级别。将传递“exitVM.0”(或.1、.2等)的permission.getName(),这将不符合“exitVM”。更好的方式是使用permission.getName().startsWith("exitVM")。2)如果您想要捕获的权限是“System.exit()”,请使用java.lang.SecurityManager.checkExit(int)并重写该方法。 - javabrett
1
http://jroller.com/ethdsy/entry/disabling_system_exit 似乎是一个失效的链接。在Wayback机器上也找不到它。如果有人知道某个缓存版本的存在,我很想读一下。 - childofsoong
显示剩余2条评论

6
请查看我对如何避免JFrame EXIT_ON_CLOSE操作退出整个应用程序?的回复。
编辑1:链接的源代码。演示了如何使用SecurityManager来防止System.exit(n)
import java.awt.*;
import java.awt.event.*;
import java.security.Permission;

/** NoExit demonstrates how to prevent 'child'
applications from ending the VM with a call
to System.exit(0).
@author Andrew Thompson */
public class NoExit extends Frame implements ActionListener {

  Button frameLaunch = new Button("Frame"),
     exitLaunch = new Button("Exit");

  /** Stores a reference to the original security manager. */
  ExitManager sm;

  public NoExit() {
     super("Launcher Application");

     sm = new ExitManager( System.getSecurityManager() );
     System.setSecurityManager(sm);

     setLayout(new GridLayout(0,1));

     frameLaunch.addActionListener(this);
     exitLaunch.addActionListener(this);

     add( frameLaunch );
     add( exitLaunch );

     pack();
     setSize( getPreferredSize() );
  }

  public void actionPerformed(ActionEvent ae) {
     if ( ae.getSource()==frameLaunch ) {
        TargetFrame tf = new TargetFrame();
     } else {
        // change back to the standard SM that allows exit.
        System.setSecurityManager(
           sm.getOriginalSecurityManager() );
        // exit the VM when *we* want
        System.exit(0);
     }
  }

  public static void main(String[] args) {
     NoExit ne = new NoExit();
     ne.setVisible(true);
  }
}

/** This example frame attempts to System.exit(0)
on closing, we must prevent it from doing so. */
class TargetFrame extends Frame {
  static int x=0, y=0;

  TargetFrame() {
     super("Close Me!");
     add(new Label("Hi!"));

     addWindowListener( new WindowAdapter() {
        public void windowClosing(WindowEvent we) {
           System.out.println("Bye!");
           System.exit(0);
        }
     });

     pack();
     setSize( getPreferredSize() );
     setLocation(++x*10,++y*10);
     setVisible(true);
  }
}

/** Our custom ExitManager does not allow the VM
to exit, but does allow itself to be replaced by
the original security manager.
@author Andrew Thompson */
class ExitManager extends SecurityManager {

  SecurityManager original;

  ExitManager(SecurityManager original) {
     this.original = original;
  }

  /** Deny permission to exit the VM. */
   public void checkExit(int status) {
     throw( new SecurityException() );
  }

  /** Allow this security manager to be replaced,
  if fact, allow pretty much everything. */
  public void checkPermission(Permission perm) {
  }

  public SecurityManager getOriginalSecurityManager() {
     return original;
  }
}

4
我认为这个问题与Java Swing无关。 - Andy
1
虽然我的回复是基于GUI的,但使用SecurityManager技术同样适用于无头应用程序(也许需要对帖子的源代码进行一些微调),就像GUI应用程序一样。我将编辑我的帖子,包括一些在这个主题上显然被忽略的源代码。O_o - Andrew Thompson
checkPermission 是否应该委托 original 来执行,以防止 TargetFrame 获得比其他代码更多的特权? - Mike Samuel
@MikeSamuel 很好的问题(也许你应该提出一个专门的问题来问)。 - Andrew Thompson

5

之前的代码示例部分正确,但我发现它会阻塞我代码对文件的访问。为了解决这个问题,我稍微改写了我的 SecurityManager:

public class MySecurityManager extends SecurityManager {

private SecurityManager baseSecurityManager;

public MySecurityManager(SecurityManager baseSecurityManager) {
    this.baseSecurityManager = baseSecurityManager;
}

@Override
public void checkPermission(Permission permission) {
    if (permission.getName().startsWith("exitVM")) {
        throw new SecurityException("System exit not allowed");
    }
    if (baseSecurityManager != null) {
        baseSecurityManager.checkPermission(permission);
    } else {
        return;
    }
}

}

在我的情况下,我需要防止第三方库终止VM。但是还有一些Grails测试正在调用System.exit。因此,我编写了代码,使其仅在调用第三方库之前立即激活自定义安全管理器(这不是常见的事件),然后立即恢复原始安全管理器(如果有任何)。
这有点丑陋。理想情况下,我希望只需删除System.exit代码,但我无法访问第三方库的源代码。

3
使用SecurityManager禁止System.exit()调用并不完美,原因至少有两个:
  1. 启用和禁用SecurityManager的Java应用程序非常不同。这就是为什么它最终需要关闭,但不能通过System.setSecurityManager(null)来实现。该调用将导致另一次安全权限检查,这将不可避免地失败,因为应用程序代码(SecurityManager子类)位于调用堆栈的顶部。
  2. 所有Java应用程序都是多线程的,其他线程可以在forbidSystemExitCall()和enableSystemExitCall()之间执行各种操作。其中一些操作可能受到安全权限检查的保护,由于上述原因,这些检查将失败。如果重写checkPermission()而不是[更为通用的]checkExit(),则将覆盖大多数情况。
我所知道的唯一解决方法是授予SecurityManager子类所有特权。这很可能要求它由一个单独的类加载器加载,例如引导(null)类加载器。

网页内容由stack overflow 提供, 点击上面的
可以查看英文原文,
原文链接