Java 并行流中I/O代码的SecurityException

Java 并行流中I/O代码的SecurityException,java,parallel-processing,java-8,java-stream,securitymanager,Java,Parallel Processing,Java 8,Java Stream,Securitymanager,我无法解释这一点,但我在其他人的代码中发现了这种现象: import java.io.IOException; import java.io.UncheckedIOException; import java.nio.file.Files; import java.util.stream.Stream; import org.junit.Test; public class TestDidWeBreakJavaAgain { @Test public void testIoI

我无法解释这一点,但我在其他人的代码中发现了这种现象:

import java.io.IOException;
import java.io.UncheckedIOException;
import java.nio.file.Files;
import java.util.stream.Stream;

import org.junit.Test;

public class TestDidWeBreakJavaAgain
{
    @Test
    public void testIoInSerialStream()
    {
        doTest(false);
    }

    @Test
    public void testIoInParallelStream()
    {
        doTest(true);
    }

    private void doTest(boolean parallel)
    {
        Stream<String> stream = Stream.of("1", "2", "3");
        if (parallel)
        {
            stream = stream.parallel();
        }
        stream.forEach(name -> {
            try
            {
                Files.createTempFile(name, ".dat");
            }
            catch (IOException e)
            {
                throw new UncheckedIOException("Failed to create temp file", e);
            }
        });
    }
}
import java.io.IOException;
导入java.io.UncheckedIOException;
导入java.nio.file.Files;
导入java.util.stream.stream;
导入org.junit.Test;
公共类TestDidWebreakJavaServer
{
@试验
public void testIoInSerialStream()
{
doTest(假);
}
@试验
公共void testIoInParallelStream()
{
多斯特(真);
}
私有void doTest(布尔并行)
{
溪流=溪流,共(“1”、“2”、“3”);
if(并行)
{
stream=stream.parallel();
}
stream.forEach(名称->{
尝试
{
createTempFile(名称为“.dat”);
}
捕获(IOE异常)
{
抛出新的未选中异常(“未能创建临时文件”,e);
}
});
}
}
在启用安全管理器的情况下运行时,仅对流调用
parallel()
,或从集合获取流时调用
parallelStream()
,似乎可以保证所有执行I/O的尝试都将抛出
SecurityException
。(最有可能的是,调用任何可以抛出
SecurityException
的方法都会抛出。)

我知道
parallel()
意味着它将在另一个线程中运行,而这个线程可能与我们开始使用的线程没有相同的权限,但我想框架会为我们解决这个问题


在整个代码库中删除对
parallel()
parallelStream()
的调用可以避免风险。插入一个
AccessController.doPrivileged
也可以修复它,但对我来说并不安全,至少在所有情况下都不安全。还有其他选择吗?

并行流执行将使用Fork/Join框架,更具体地说,它将使用Fork/Join公共池。这是一个实现细节,但正如在本例中所观察到的,这些细节可能会以意外的方式泄露出去

请注意,当使用
CompletableFuture
异步执行任务时,也可能发生相同的行为

当存在安全管理器时,Fork/Join公共池的线程工厂被设置为创建无害线程的工厂。这样一个无害线程没有被授予任何权限,不是任何已定义线程组的成员,并且在顶级Fork/Join任务完成其执行后,所有线程局部变量(如果已创建)都将被清除。这种行为确保了在共享公共池时,Fork/Join任务彼此隔离

这就是为什么在示例中会抛出
SecurityException
,可能是:

java.lang.SecurityException:无法创建临时文件或目录

有两种可能的解决办法。根据安全经理使用的原因,每种变通方法都可能增加不安全的风险

第一个更一般的解决方法是通过系统属性注册Fork/Join线程工厂,告诉Fork/Join框架公共池的默认线程工厂应该是什么。例如,这里有一个非常简单的线程工厂:

public class MyForkJoinWorkerThreadFactory
        implements ForkJoinPool.ForkJoinWorkerThreadFactory {
    public final ForkJoinWorkerThread newThread(ForkJoinPool pool) {
        return new ForkJoinWorkerThread(pool) {};
    }
}
可以使用以下系统属性注册:

-Djava.util.concurrent.ForkJoinPool.common.threadFactory=MyForkJoinWorkerThreadFactory

MyForkJoinWorkerThreadFactory
的行为目前与


第二个更具体的解决方法是创建一个新的Fork/Join池。在这种情况下,
ForkJoinPool.defaultForkJoinWorkerThreadFactory
将用于不接受
ForkJoinWorkerThreadFactory
参数的构造函数。任何并行流执行都需要在该池中执行的任务中执行。请注意,这是一个实现细节,可能在将来的版本中起作用,也可能不起作用。

您不必担心
AccessController.doPrivileged
。如果操作正确,不会降低安全性。采用单个action参数的版本将在您的上下文中执行该操作,忽略您的调用者,但存在重载方法,具有一个附加参数,一个以前记录的上下文:

private void doTest(boolean parallel)
{
    Consumer<String> createFile=name -> {
        try {
            Files.createTempFile(name, ".dat");
        }
        catch (IOException e) {
            throw new UncheckedIOException("Failed to create temp file", e);
        }
    }, actualAction;
    Stream<String> stream = Stream.of("1", "2", "3");

    if(parallel)
    {
        stream = stream.parallel();
        AccessControlContext ctx=AccessController.getContext();
        actualAction=name -> AccessController.doPrivileged(
          (PrivilegedAction<?>)()->{ createFile.accept(name); return null; }, ctx);
    }
    else actualAction = createFile;

    stream.forEach(actualAction);
}
private void doTest(布尔并行)
{
消费者创建文件=名称->{
试一试{
createTempFile(名称为“.dat”);
}
捕获(IOE异常){
抛出新的未选中异常(“未能创建临时文件”,e);
}
},实际情况;
溪流=溪流,共(“1”、“2”、“3”);
if(并行)
{
stream=stream.parallel();
AccessControlContext ctx=AccessController.getContext();
actualAction=name->AccessController.doPrivileged(
(PrivilegedAction)(->{createFile.accept(name);返回null;},ctx);
}
else actualAction=createFile;
forEach流(实际流量);
}

第一行是
AccessControlContext ctx=AccessController.getContext()语句它记录当前安全上下文,其中包括代码和当前调用方。(请记住,有效权限是所有调用方集合的交集)。通过将结果上下文对象
ctx
提供给
Consumer
中的
doPrivileged
方法,换句话说,您正在重新建立上下文,
PrivilegedAction
将具有与单线程场景中相同的权限。

请提供您获得的异常的堆栈跟踪。还请添加您的
SecurityManager
代码,以便我们可以准确地重现您的问题。这可能与使用公共fork-join池的并行流有关。它是如何工作的?此问题还影响存储在
ThreadLocal
中的上下文,例如Spring的
SecurityContext
,请参见例如“好答案”。也是