프리뷰 실행 규칙
javac 진단 정보·프리뷰 클래스 파일 마이너 버전·실행 플래그 누락 실패를 실제 하위 프로세스로 관찰하고 StableValue 동시 초기화를 확인합니다.
프리뷰 기능을 사용하는 클래스 파일에는 프리뷰 사용 표식이 남습니다. --enable-preview만 켰다고 모든 클래스가 이 표식을 갖는 것은 아닙니다.
Java 25 프리뷰 클래스는 같은 릴리스의 JVM에서 --enable-preview를 켜야 로드할 수 있습니다. 다른 릴리스용 프리뷰 바이너리는 이 플래그만으로 호환되지 않습니다.
경고를 끄거나 빌드 산출물에서 버리지 말고 기능·소스·JDK 버전과 함께 실험 근거로 보존합니다.
프리뷰 클래스와 실행 플래그
다음 정식 Java 기능만 사용한 확인 프로그램은 임시 프리뷰 소스를 javac --enable-preview --release 25로 컴파일한 뒤 하위 java 프로세스에는 플래그를 주지 않습니다.
하위 JVM은 프리뷰 클래스 로딩을 거부합니다. 상위 프로그램은 그 종료 코드와 첫 진단 줄을 수집하므로, 자식의 실패와 상위 프로그램의 정상 종료를 구분합니다.
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import javax.tools.JavaCompiler;
import javax.tools.ToolProvider;
public final class PreviewClassfileWithoutRuntimeFlagProbe {
public static void main(String[] args) throws Exception {
Path directory = Files.createTempDirectory("preview-run-probe");
Path source = directory.resolve("PreviewPayload.java");
Files.writeString(
source,
"""
public class PreviewPayload {
public static void main(String[] args) {
long value = 25L;
System.out.println(switch (value) {
case long number -> number;
});
}
}
""",
StandardCharsets.UTF_8);
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
int compileExit =
compiler.run(
null,
System.out,
System.err,
"--enable-preview",
"--release",
"25",
"-d",
directory.toString(),
source.toString());
if (compileExit != 0) {
throw new IllegalStateException("preview compilation failed");
}
boolean windows = System.getProperty("os.name").toLowerCase().contains("win");
Path java = Path.of(System.getProperty("java.home"), "bin", windows ? "java.exe" : "java");
Process process =
new ProcessBuilder(java.toString(), "-cp", directory.toString(), "PreviewPayload")
.redirectErrorStream(true)
.start();
String output = new String(process.getInputStream().readAllBytes(), StandardCharsets.UTF_8);
int runExit = process.waitFor();
System.out.println(output.lines().findFirst().orElse("no-output"));
System.out.println("wrong-run-exit=" + runExit);
}
}원칙은 컴파일과 실행 명령을 한 빌드 구성으로 묶는 것입니다.
프리뷰 클래스가 플러그인이나 생성된 코드를 통해 안정 산출물에 섞이면 지역 컴파일은 성공해도 배포 실행기에서 실패할 수 있습니다.
JavaCompiler 프리뷰 진단
-Xlint:preview는 소스에서 프리뷰 기능을 사용한 위치를 경고로 알려 줍니다.
다음 확인 프로그램은 정식 기능만 사용해 기본형 패턴 소스를 컴파일하고 진단 종류와 행 번호를 출력합니다. preview-warnings는 MANDATORY_WARNING 개수이며 전체 진단 수는 아닙니다.
import java.nio.charset.StandardCharsets;
import java.nio.file.Files;
import java.nio.file.Path;
import javax.tools.DiagnosticCollector;
import javax.tools.JavaCompiler;
import javax.tools.JavaFileObject;
import javax.tools.StandardJavaFileManager;
import javax.tools.ToolProvider;
public final class PreviewDiagnosticCapture {
public static void main(String[] args) throws Exception {
Path directory = Files.createTempDirectory("preview-diagnostics");
Path source = directory.resolve("WarningPayload.java");
Files.writeString(
source,
"""
public class WarningPayload {
static String show(long value) {
return switch (value) {
case long number -> "value=" + number;
};
}
}
""",
StandardCharsets.UTF_8);
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
DiagnosticCollector<JavaFileObject> diagnostics = new DiagnosticCollector<>();
try (StandardJavaFileManager files =
compiler.getStandardFileManager(diagnostics, null, StandardCharsets.UTF_8)) {
Iterable<? extends JavaFileObject> units = files.getJavaFileObjects(source);
boolean success =
compiler.getTask(
null,
files,
diagnostics,
java.util.List.of(
"--enable-preview",
"--release",
"25",
"-Xlint:preview",
"-d",
directory.toString()),
null,
units)
.call();
long warnings =
diagnostics.getDiagnostics().stream()
.filter(
diagnostic ->
diagnostic.getKind()
== javax.tools.Diagnostic.Kind
.MANDATORY_WARNING)
.count();
System.out.println("success=" + success);
System.out.println("preview-warnings=" + warnings);
diagnostics
.getDiagnostics()
.forEach(
diagnostic ->
System.out.println(
diagnostic.getKind()
+ " line="
+ diagnostic.getLineNumber()));
}
}
}원문은 진단 메시지 본문을 출력하지 않습니다. 메시지도 보존하려면 공급자·로캘에 따른 차이를 구분해 기록합니다.
CI 로그는 컴파일러 버전과 명령도 함께 보존합니다.
클래스 파일 헤더의 프리뷰 식별
클래스 파일의 첫 8바이트에는 매직 값, 마이너, 메이저가 있습니다.
다음 안정 확인 프로그램이 직접 컴파일하고 헤더를 읽습니다.
import java.nio.ByteBuffer;
import java.nio.file.Files;
import java.nio.file.Path;
import javax.tools.JavaCompiler;
import javax.tools.ToolProvider;
public final class PreviewClassFileHeader {
public static void main(String[] args) throws Exception {
Path directory = Files.createTempDirectory("preview-header");
Path source = directory.resolve("HeaderPayload.java");
Files.writeString(
source,
"""
public class HeaderPayload {
static boolean flip(boolean value) {
return switch (value) {
case true -> false;
case false -> true;
};
}
}
""");
JavaCompiler compiler = ToolProvider.getSystemJavaCompiler();
int exit =
compiler.run(
null,
null,
null,
"--enable-preview",
"--release",
"25",
"-d",
directory.toString(),
source.toString());
if (exit != 0) {
throw new IllegalStateException("compile exit=" + exit);
}
byte[] header = Files.readAllBytes(directory.resolve("HeaderPayload.class"));
ByteBuffer buffer = ByteBuffer.wrap(header);
int magic = buffer.getInt();
int minor = Short.toUnsignedInt(buffer.getShort());
int major = Short.toUnsignedInt(buffer.getShort());
System.out.printf("magic=%08x, minor=%d, major=%d%n", magic, minor, major);
}
}아래는 Java 25 클래스 파일 형식에 따른 헤더입니다. 필드의 바이트 순서는 빅 엔디언입니다.
원문 코드와 Java 25 명세에 따른 조건과 결과를 비교합니다.
| 필드 | 바이트 · 16진수 | 의미 |
|---|---|---|
| magic | CA FE BA BE | 클래스 파일 식별 |
| minor | FF FF | 65535 · 프리뷰 표식 |
| major | 00 45 | 69 · Java 25 |
- magic
- 바이트 · 16진수: CA FE BA BE의미: 클래스 파일 식별
- minor
- 바이트 · 16진수: FF FF의미: 65535 · 프리뷰 표식
- major
- 바이트 · 16진수: 00 45의미: 69 · Java 25
헤더 확인은 산출물 유출 게이트에 유용하지만 어떤 프리뷰 기능을 사용했는지까지 알려 주지는 않습니다.
StableValue 동시 초기화
StableValue.orElseSet()은 공급자가 정상 반환해 값을 설정하면 이후 그 값을 재사용합니다. 공급자가 예외를 던지면 미설정 상태가 유지되어 나중 호출이 초기화를 재시도할 수 있습니다.
원문은 정상 반환하는 공급자를 두고 가상 스레드 작업 20개의 결과를 검사합니다. 정상 완료 경로에서는 set=true, initializer-calls=1이며, 작업 제출만으로 20개가 동시에 초기화에 진입했다고 볼 수는 없습니다.
import java.util.ArrayList;
import java.util.List;
import java.util.concurrent.Executors;
import java.util.concurrent.Future;
import java.util.concurrent.atomic.AtomicInteger;
public final class ConcurrentStableValueProbe {
public static void main(String[] args) throws Exception {
StableValue<String> value = StableValue.of();
AtomicInteger initializers = new AtomicInteger();
try (var executor = Executors.newVirtualThreadPerTaskExecutor()) {
List<Future<String>> results = new ArrayList<>();
for (int task = 0; task < 20; task++) {
results.add(
executor.submit(
() ->
value.orElseSet(
() -> {
initializers.incrementAndGet();
return "ready";
})));
}
for (Future<String> result : results) {
if (!result.get().equals("ready")) {
throw new AssertionError("unexpected value");
}
}
}
System.out.println("set=" + value.isSet());
System.out.println("initializer-calls=" + initializers.get());
}
}관찰 결과 기록 형식
실험 보고서에는 JEP 번호·프리뷰 차수, java -version, 정확한 컴파일·실행 명령, 경고 개수, 종료 코드, 표준 출력·표준 오류 핵심, 클래스 파일 헤더, 반복 횟수를 남깁니다.
“동작함” 한 줄은 다음 릴리스 마이그레이션에 쓸 수 없습니다.
프리뷰 경고를 -Werror로 막으면 안 되나요?
프리뷰 소스 세트에서 모든 프리뷰 경고를 오류로 바꾸면 의도한 실험 자체가 컴파일되지 않을 수 있습니다.
안정 소스에는 프리뷰 사용을 금지하고, 프리뷰 작업은 경고를 수집·검토하되 별도 정책을 적용합니다.
다른 린트 경고는 오류로 유지할 수 있습니다.
연습 문제
클래스 파일 헤더의 마이너 버전이 65535이면 정식 산출물에서 거부하는 함수를 작성하세요.
해설 보기
import java.nio.ByteBuffer;
public final class PreviewClassGateSolution {
static boolean isPreview(byte[] classFile) {
if (classFile.length < 8) {
throw new IllegalArgumentException("short class file");
}
ByteBuffer buffer = ByteBuffer.wrap(classFile);
if (buffer.getInt() != 0xcafebabe) {
throw new IllegalArgumentException("magic");
}
int minor = Short.toUnsignedInt(buffer.getShort());
return minor == 0xffff;
}
public static void main(String[] args) {
byte[] stable = {-54, -2, -70, -66, 0, 0, 0, 69};
byte[] preview = {-54, -2, -70, -66, -1, -1, 0, 69};
System.out.println("stable-preview=" + isPreview(stable));
System.out.println("preview-preview=" + isPreview(preview));
}
}두 8바이트 예제의 판정은 false와 true입니다. 이 함수는 매직 값과 마이너만 검사하며 메이저나 전체 클래스의 유효성은 검사하지 않습니다.
실제 게이트에는 JAR의 모든 .class 항목과 다중 릴리스 디렉터리를 순회하는 처리가 추가로 필요합니다.