Java: System.exit() 与安全策略

本文探讨了如何在Java应用中避免使用System.exit()带来的问题,提出使用异常替代System.exit()的建议,并提供了实现代码示例。通过定义自定义安全管理器和异常类,确保应用程序能够正常运行而不被意外退出。此外,文章还提供了测试代码以验证解决方案的有效性。

摘要生成于 C知道 ,由 DeepSeek-R1 满血版支持, 前往体验 >

说明

System.exit() 的本质是通知 JVM 关闭。

一般来说,有两种禁用 System.exit() 的办法:

  • 安全管理器
  • 安全策略

本质都是JRE 提供的本地实现,在执行之前进行权限判断。

因为System.exit() 是一种很暴力的手段,如果在 Client 模式下自己写个小程序无所谓,但是在 Server 上多个程序、或者多线程时就会有很大的麻烦。

底层源码

1.先来看看静态方法 System.exit() 的源码:

// System.exit()
public static void exit(int status) {
    Runtime.getRuntime().exit(status);
}

应该说很简单, 只是简单地调用运行时的 exit 方法.

2.然后我们看运行时的实例方法 exit:

// Runtime.exit()
public void exit(int status) {
    SecurityManager security = System.getSecurityManager();
    if (security != null) {
        security.checkExit(status);
    }
    Shutdown.exit(status);
}

如果有安全管理器,那么就让安全管理器执行 checkExit退出权限检查。

如果检查不通过,安全管理器就会抛出异常(这就是约定!)。

然后当前线程就会往外一路抛异常,如果不捕获,那么该线程就会退出。

此时如果没有其他的前台线程正在运行,那么JVM也会跟着退出。

3.Shutdownjava.lang 包下面的一个类。

访问权限是 default, 所以我们在API中是不能调用的。

// Shutdown.exit()
static void exit(int status) {
    boolean runMoreFinalizers = false;
    synchronized (lock) {
        if (status != 0) runFinalizersOnExit = false;
        switch (state) {
        case RUNNING:       /* Initiate shutdown */
            state = HOOKS;
            break;
        case HOOKS:         /* Stall and halt */
            break;
        case FINALIZERS:
            if (status != 0) {
                /* Halt immediately on nonzero status */
                halt(status);
            } else {
                /* Compatibility with old behavior:
                 * Run more finalizers and then halt
                 */
                runMoreFinalizers = runFinalizersOnExit;
            }
            break;
        }
    }
    if (runMoreFinalizers) {
        runAllFinalizers();
        halt(status);
    }
    synchronized (Shutdown.class) {
        /* Synchronize on the class object, causing any other thread
         * that attempts to initiate shutdown to stall indefinitely
         */
        sequence();
        halt(status);
    }
}

其中有一些同步方法进行锁定。 退出逻辑是调用了 halt 方法。

// Shutdown.halt()
static void halt(int status) {
    synchronized (haltLock) {
        halt0(status);
    }
}

static native void halt0(int status);

然后就是调用 native 的 halt0() 方法让 JVM “自杀“了。

示例

使用安全管理器的实现代码如下所示:

1.定义异常类, 继承自 SecurityException

ExitException.java

package com.cncounter.security;

public class ExitException extends SecurityException {
    private static final long serialVersionUID = 1L;
    public final int status;

    public ExitException(int status) {
        super("忽略 Exit方法调用!");
        this.status = status;
    }
}

2.定义安全管理器类, 继承自 SecurityManager

NoExitSecurityManager.java

package com.cncounter.security;

import java.security.Permission;

public class NoExitSecurityManager extends SecurityManager {
    @Override
    public void checkPermission(Permission perm) {
        // allow anything.
    }

    @Override
    public void checkPermission(Permission perm, Object context) {
        // allow anything.
    }

    @Override
    public void checkExit(int status) {
        super.checkExit(status);
        throw new ExitException(status);
    }
}

其中直接拒绝系统退出。

3.增加一个辅助和测试类,实际使用时你也可以自己进行控制。

NoExitHelper.java

package com.cncounter.security;

public class NoExitHelper {

    /**
     * 设置不允许调用 System.exit(status)
     * 
     * @throws Exception
     */
    public static void setNoExit() throws Exception {
        System.setSecurityManager(new NoExitSecurityManager());
    }

    public static void main(String[] args) throws Exception {
        setNoExit();
        testNoExit();
        testExit();
        testNoExit();
    }

    public static void testNoExit() throws Exception {
        System.out.println("Printing works");
    }

    public static void testExit() throws Exception {
        try {
            System.exit(42);
        } catch (ExitException e) {
            //
            System.out.println("退出的状态码为: " + e.status);
        }
    }
}

在其中,使用了一个 main 方法来做简单的测试。 控制台输出结果如下:

Printing works
退出的状态码为: 42
Printing works

原问题

原来的问题如下:

I’ve got a few methods that should call System.exit() on certain inputs. Unfortunately, testing these cases causes JUnit to terminate! Putting the method calls in a new Thread doesn’t seem to help, since System.exit() terminates the JVM, not just the current thread. Are there any common patterns for dealing with this? For example, can I subsitute a stub for System.exit()?

大意是:

有一些方法需要测试, 但是在某些特定的输入时就会调用 System.exit()。这就杯具了,这时候 JUnit 测试也跟着退出了! 用一个新线程来调用这种方法也没什么用, 因为 System.exit() 会停止JVM , 而不是退出当前线程。有什么通用的模式来处理这种情况吗? 例如,我能替换掉 System.exit() 方法吗?

建议如下:

Instead of terminating with System.exit(whateverValue), why not throw an unchecked exception? In normal use it will drift all the way out to the JVM’s last-ditch catcher and shut your script down (unless you decide to catch it somewhere along the way, which might be useful someday).

In the JUnit scenario it will be caught by the JUnit framework, which will report that such-and-such test failed and move smoothly along to the next.

翻译如下:

在程序中调用 System.exit(whateverValue) 是一种很不好的编程习惯, 这种情况为什么不抛出一个未检测的异常(unchecked exception)呢? 如果程序中不进行捕获(catch), 抛出的异常会一路漂移到 JVM , 然后就会退出程序(只有主线程的话)。

在 JUnit 的测试场景中异常会被 JUnit 框架捕获, 然后就会报告说某某某测试执行失败,然后就继续下一个单元测试了。

当然,给出的解决方案就是前面的那段代码. 你还可以阅读下面的参考文章,查找其他的解决方案。

参考文章:

日期: 2015年08月25日

人员: 铁锚 http://blog.youkuaiyun.com/renfufei

06-10 14:27:52.220 16648 16648 W System.err: java.io.FileNotFoundException: /sys/devices/platform/fe8a0000.usb2-phy/otg_mode: open failed: EACCES (Permission denied) 06-10 14:27:52.221 16648 16648 W System.err: at libcore.io.IoBridge.open(IoBridge.java:492) 06-10 14:27:52.221 16648 16648 W System.err: at java.io.FileOutputStream.<init>(FileOutputStream.java:236) 06-10 14:27:52.222 16648 16648 W System.err: at java.io.FileOutputStream.<init>(FileOutputStream.java:125) 06-10 14:27:52.222 16648 16648 W System.err: at com.kgzn.developer.dialog.CommonToolHolder.switchOTG(CommonToolHolder.java:36) 06-10 14:27:52.222 16648 16648 W System.err: at com.kgzn.developer.dialog.SwitchDialog$16.onClick(SwitchDialog.java:292) 06-10 14:27:52.222 16648 16648 W System.err: at android.view.View.performClick(View.java:7448) 06-10 14:27:52.223 16648 16648 W System.err: at android.widget.CompoundButton.performClick(CompoundButton.java:144) 06-10 14:27:52.223 16648 16648 W System.err: at android.view.View.performClickInternal(View.java:7425) 06-10 14:27:52.223 16648 16648 W System.err: at android.view.View.access$3600(View.java:810) 06-10 14:27:52.223 16648 16648 W System.err: at android.view.View$PerformClick.run(View.java:28305) 06-10 14:27:52.223 16648 16648 W System.err: at android.os.Handler.handleCallback(Handler.java:938) 06-10 14:27:52.224 16648 16648 W System.err: at android.os.Handler.dispatchMessage(Handler.java:99) 06-10 14:27:52.224 16648 16648 W System.err: at android.os.Looper.loop(Looper.java:223) 06-10 14:27:52.224 16648 16648 W System.err: at android.app.ActivityThread.main(ActivityThread.java:7660) 06-10 14:27:52.224 16648 16648 W System.err: at java.lang.reflect.Method.invoke(Native Method) 06-10 14:27:52.225 16648 16648 W System.err: at com.android.internal.os.RuntimeInit$MethodAndArgsCaller.run(RuntimeInit.java:592) 06-10 14:27:52.226 16648 16648 W System.err: at com.android.internal.os.ZygoteInit.main(ZygoteInit.java:947) 06-10 14:27:52.227 16648 16648 W System.err: Caused by: android.system.ErrnoException: open failed: EACCES (Permission denied) 06-10 14:27:52.227 16648 16648 W System.err: at libcore.io.Linux.open(Native Method) 06-10 14:27:52.227 16648 16648 W System.err: at libcore.io.ForwardingOs.open(ForwardingOs.java:166) 06-10 14:27:52.227 16648 16648 W System.err: at libcore.io.BlockGuardOs.open(BlockGuardOs.java:254) 06-10 14:27:52.228 16648 16648 W System.err: at libcore.io.ForwardingOs.open(ForwardingOs.java:166) 06-10 14:27:52.228 16648 16648 W System.err: at android.app.ActivityThread$AndroidOs.open(ActivityThread.java:7546) 06-10 14:27:52.228 16648 16648 W System.err: at libcore.io.IoBridge.open(IoBridge.java:478)
06-11
import org.apache.ibatis.io.Resources; import org.apache.ibatis.session.SqlSession; import org.apache.ibatis.session.SqlSessionFactory; import org.apache.ibatis.session.SqlSessionFactoryBuilder; import java.io.IOException; import java.io.InputStream; import java.util.List; public class mbdemo { public static void main(String[] args) throws IOException { String resource = "mybatis-config.xml"; InputStream inputStream = Resources.getResourceAsStream(resource); SqlSessionFactory sqlSessionFactory = new SqlSessionFactoryBuilder().build(inputStream); SqlSession sqlSession = sqlSessionFactory.openSession(); sqlSession.selectOne("test.selectall"); List<Object> objects = sqlSession.selectList("test.selectall"); System.out.println(objects); sqlSession.close(); } } public class user { private Integer id; private String name_; private Integer age; private Integer birth; private Integer tall; public user(Integer id, String name_, Integer age, Integer birth, Integer tall) { this.id = id; this.name_ = name_; this.age = age; this.birth = birth; this.tall = tall; } @Override public String toString() { return "user{" + "id=" + id + ", name_='" + name_ + '\'' + ", age=" + age + ", birth=" + birth + ", tall=" + tall + '}'; } } <?xml version="1.0" encoding="UTF-8"?> <configuration> <!-- CONSOLE :表示当前的日志信息是可以输出到控制台的。 --> <appender name="Console" class="ch.qos.logback.core.ConsoleAppender"> <encoder> <pattern>[%level] %blue(%d{HH:mm:ss.SSS}) %cyan([%thread]) %boldGreen(%logger{15}) - %msg %n</pattern> </encoder> </appender> <logger name="com.itheima" level="DEBUG" additivity="false"> <appender-ref ref="Console"/> </logger> <!-- level:用来设置打印级别,大小写无关:TRACE, DEBUG, INFO, WARN, ERROR, ALL 和 OFF , 默认debug <root>可以包含零个或多个<appender-ref>元素,标识这个输出位置将会被本日志级别控制。 --> <root level="DEBUG"> <appender-ref ref="Console"/> </root> </configuration><?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE configuration PUBLIC "-//mybatis.org//DTD Config 3.0//EN" "http://mybatis.org/dtd/mybatis-3-config.dtd"> <configuration> <environments default="development"> <environment id="development"> <transactionManager type="JDBC"/> <dataSource type="POOLED"> <!--数据库连接信息--> <property name="driver" value="com.mysql.jdbc.Driver"/> <property name="url" value="jdbc:mysql:///mybatis?useSSL=false"/> <property name="username" value="root"/> <property name="password" value="root"/> </dataSource> </environment> </environments> <mappers> <!--加载sql的映射文件--> <mapper resource="tablemapper.xml"/> </mappers> </configuration><?xml version="1.0" encoding="UTF-8" ?> <!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd"> <!-- namespace 名称空间 --> <mapper namespace="test"> <select id="selectall" resultType="user"> select * from student; </select> </mapper><?xml version="1.0" encoding="UTF-8"?> <project xmlns="http://maven.apache.org/POM/4.0.0" xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd"> <modelVersion>4.0.0</modelVersion> <groupId>org.example</groupId> <artifactId>mybatis-demo</artifactId> <version>1.0-SNAPSHOT</version> <dependencies> <dependency> <groupId>org.mybatis</groupId> <artifactId>mybatis</artifactId> <version>3.5.5</version> </dependency> <dependency> <groupId>mysql</groupId> <artifactId>mysql-connector-java</artifactId> <version>8.0.33</version> </dependency> <dependency> <groupId>junit</groupId> <artifactId>junit</artifactId> <version>4.13.1</version> <scope>test</scope> </dependency> <!-- 添加slf4j日志api --> <dependency> <groupId>org.slf4j</groupId> <artifactId>slf4j-api</artifactId> <version>1.7.20</version> </dependency> <!-- 添加logback-classic依赖 --> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-classic</artifactId> <version>1.2.3</version> </dependency> <!-- 添加logback-core依赖 --> <dependency> <groupId>ch.qos.logback</groupId> <artifactId>logback-core</artifactId> <version>1.2.3</version> </dependency> </dependencies> </project>"C:\Program Files\Java\jdk-1.8\bin\java.exe" "-javaagent:D:\idea\IntelliJ IDEA 2020.1.2\lib\idea_rt.jar=51781:D:\idea\IntelliJ IDEA 2020.1.2\bin" -Dfile.encoding=UTF-8 -classpath "C:\Program Files\Java\jdk-1.8\jre\lib\charsets.jar;C:\Program Files\Java\jdk-1.8\jre\lib\deploy.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\access-bridge-64.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\cldrdata.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\dnsns.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\jaccess.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\jfxrt.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\localedata.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\nashorn.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\sunec.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\sunjce_provider.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\sunmscapi.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\sunpkcs11.jar;C:\Program Files\Java\jdk-1.8\jre\lib\ext\zipfs.jar;C:\Program Files\Java\jdk-1.8\jre\lib\javaws.jar;C:\Program Files\Java\jdk-1.8\jre\lib\jce.jar;C:\Program Files\Java\jdk-1.8\jre\lib\jfr.jar;C:\Program Files\Java\jdk-1.8\jre\lib\jfxswt.jar;C:\Program Files\Java\jdk-1.8\jre\lib\jsse.jar;C:\Program Files\Java\jdk-1.8\jre\lib\management-agent.jar;C:\Program Files\Java\jdk-1.8\jre\lib\plugin.jar;C:\Program Files\Java\jdk-1.8\jre\lib\resources.jar;C:\Program Files\Java\jdk-1.8\jre\lib\rt.jar;C:\Users\22844\IdeaProjects\mybatis-demo\target\classes;C:\Users\22844\.m2\repository\org\mybatis\mybatis\3.5.5\mybatis-3.5.5.jar;C:\Users\22844\.m2\repository\com\mysql\mysql-connector-j\8.0.33\mysql-connector-j-8.0.33.jar;C:\Users\22844\.m2\repository\com\google\protobuf\protobuf-java\3.21.9\protobuf-java-3.21.9.jar;C:\Users\22844\.m2\repository\org\slf4j\slf4j-api\1.7.20\slf4j-api-1.7.20.jar;C:\Users\22844\.m2\repository\ch\qos\logback\logback-classic\1.2.3\logback-classic-1.2.3.jar;C:\Users\22844\.m2\repository\ch\qos\logback\logback-core\1.2.3\logback-core-1.2.3.jar" mbdemo [DEBUG] 12:16:35.713 [main] o.a.i.l.LogFactory - Logging initialized using 'class org.apache.ibatis.logging.slf4j.Slf4jImpl' adapter. [DEBUG] 12:16:35.728 [main] o.a.i.d.p.PooledDataSource - PooledDataSource forcefully closed/removed all connections. [DEBUG] 12:16:35.728 [main] o.a.i.d.p.PooledDataSource - PooledDataSource forcefully closed/removed all connections. [DEBUG] 12:16:35.728 [main] o.a.i.d.p.PooledDataSource - PooledDataSource forcefully closed/removed all connections. [DEBUG] 12:16:35.728 [main] o.a.i.d.p.PooledDataSource - PooledDataSource forcefully closed/removed all connections. [DEBUG] 12:16:35.768 [main] o.a.i.t.j.JdbcTransaction - Opening JDBC Connection Loading class `com.mysql.jdbc.Driver'. This is deprecated. The new driver class is `com.mysql.cj.jdbc.Driver'. The driver is automatically registered via the SPI and manual loading of the driver class is generally unnecessary. Exception in thread "main" org.apache.ibatis.exceptions.PersistenceException: ### Error querying database. Cause: java.sql.SQLNonTransientConnectionException: Public Key Retrieval is not allowed ### The error may exist in tablemapper.xml ### The error may involve test.selectall ### The error occurred while executing a query ### Cause: java.sql.SQLNonTransientConnectionException: Public Key Retrieval is not allowed at org.apache.ibatis.exceptions.ExceptionFactory.wrapException(ExceptionFactory.java:30) at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:149) at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:140) at org.apache.ibatis.session.defaults.DefaultSqlSession.selectOne(DefaultSqlSession.java:76) at org.apache.ibatis.session.defaults.DefaultSqlSession.selectOne(DefaultSqlSession.java:70) at mbdemo.main(mbdemo.java:16) Caused by: java.sql.SQLNonTransientConnectionException: Public Key Retrieval is not allowed at com.mysql.cj.jdbc.exceptions.SQLError.createSQLException(SQLError.java:111) at com.mysql.cj.jdbc.exceptions.SQLExceptionsMapping.translateException(SQLExceptionsMapping.java:122) at com.mysql.cj.jdbc.ConnectionImpl.createNewIO(ConnectionImpl.java:825) at com.mysql.cj.jdbc.ConnectionImpl.<init>(ConnectionImpl.java:446) at com.mysql.cj.jdbc.ConnectionImpl.getInstance(ConnectionImpl.java:239) at com.mysql.cj.jdbc.NonRegisteringDriver.connect(NonRegisteringDriver.java:188) at java.sql.DriverManager.getConnection(DriverManager.java:664) at java.sql.DriverManager.getConnection(DriverManager.java:208) at org.apache.ibatis.datasource.unpooled.UnpooledDataSource.doGetConnection(UnpooledDataSource.java:224) at org.apache.ibatis.datasource.unpooled.UnpooledDataSource.doGetConnection(UnpooledDataSource.java:219) at org.apache.ibatis.datasource.unpooled.UnpooledDataSource.getConnection(UnpooledDataSource.java:95) at org.apache.ibatis.datasource.pooled.PooledDataSource.popConnection(PooledDataSource.java:432) at org.apache.ibatis.datasource.pooled.PooledDataSource.getConnection(PooledDataSource.java:89) at org.apache.ibatis.transaction.jdbc.JdbcTransaction.openConnection(JdbcTransaction.java:139) at org.apache.ibatis.transaction.jdbc.JdbcTransaction.getConnection(JdbcTransaction.java:61) at org.apache.ibatis.executor.BaseExecutor.getConnection(BaseExecutor.java:337) at org.apache.ibatis.executor.SimpleExecutor.prepareStatement(SimpleExecutor.java:86) at org.apache.ibatis.executor.SimpleExecutor.doQuery(SimpleExecutor.java:62) at org.apache.ibatis.executor.BaseExecutor.queryFromDatabase(BaseExecutor.java:325) at org.apache.ibatis.executor.BaseExecutor.query(BaseExecutor.java:156) at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:109) at org.apache.ibatis.executor.CachingExecutor.query(CachingExecutor.java:89) at org.apache.ibatis.session.defaults.DefaultSqlSession.selectList(DefaultSqlSession.java:147) ... 4 more Caused by: com.mysql.cj.exceptions.UnableToConnectException: Public Key Retrieval is not allowed at sun.reflect.NativeConstructorAccessorImpl.newInstance0(Native Method) at sun.reflect.NativeConstructorAccessorImpl.newInstance(NativeConstructorAccessorImpl.java:62) at sun.reflect.DelegatingConstructorAccessorImpl.newInstance(DelegatingConstructorAccessorImpl.java:45) at java.lang.reflect.Constructor.newInstance(Constructor.java:423) at com.mysql.cj.exceptions.ExceptionFactory.createException(ExceptionFactory.java:62) at com.mysql.cj.exceptions.ExceptionFactory.createException(ExceptionFactory.java:86) at com.mysql.cj.protocol.a.authentication.CachingSha2PasswordPlugin.nextAuthenticationStep(CachingSha2PasswordPlugin.java:130) at com.mysql.cj.protocol.a.authentication.CachingSha2PasswordPlugin.nextAuthenticationStep(CachingSha2PasswordPlugin.java:49) at com.mysql.cj.protocol.a.NativeAuthenticationProvider.proceedHandshakeWithPluggableAuthentication(NativeAuthenticationProvider.java:443) at com.mysql.cj.protocol.a.NativeAuthenticationProvider.connect(NativeAuthenticationProvider.java:213) at com.mysql.cj.protocol.a.NativeProtocol.connect(NativeProtocol.java:1430) at com.mysql.cj.NativeSession.connect(NativeSession.java:134) at com.mysql.cj.jdbc.ConnectionImpl.connectOneTryOnly(ConnectionImpl.java:945) at com.mysql.cj.jdbc.ConnectionImpl.createNewIO(ConnectionImpl.java:815) ... 24 more Process finished with exit code 1
05-12
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值