Wie verstecke ich die Warnung "Illegaler reflektierender Zugriff" in Java 9 ohne JVM-Argument?

73

Ich habe gerade versucht, meinen Server mit Java 9 auszuführen, und habe die nächste Warnung erhalten:

WARNING: An illegal reflective access operation has occurred
WARNING: Illegal reflective access by io.netty.util.internal.ReflectionUtil (file:/home/azureuser/server-0.28.0-SNAPSHOT.jar) to constructor java.nio.DirectByteBuffer(long,int)
WARNING: Please consider reporting this to the maintainers of io.netty.util.internal.ReflectionUtil
WARNING: Use --illegal-access=warn to enable warnings of further illegal reflective access operations
WARNING: All illegal access operations will be denied in a future release

Ich möchte diese Warnung ausblenden, ohne sie --illegal-access=denybeim Start zu den JVM-Optionen hinzuzufügen . Etwas wie:

System.setProperty("illegal-access", "deny");

Gibt es eine Möglichkeit, das zu tun?

Alle zugehörigen Antworten, die die Verwendung von JVM-Optionen vorschlagen, möchte ich aus dem Code deaktivieren. Ist das möglich?

Zur Verdeutlichung: Bei meiner Frage geht es darum, diese Warnung aus dem Code zu entfernen und nicht über JVM-Argumente / Flags, wie in ähnlichen Fragen angegeben.

Dmitriy Dumanskiy
quelle
Ich weiß das und das Problem ist bereits gemeldet. Ich möchte diese Warnung jedoch jetzt deaktivieren. Da die Behebung des oben genannten Problems einige Zeit in Anspruch nehmen wird.
Dmitriy Dumanskiy
Noch einmal - ich weiß, dass ich JVM-Flags verwenden kann, aber ich muss das per Code tun. Bin ich hier nicht klar?
Dmitriy Dumanskiy
3
Das HotSpotDiagnosticMXBeanermöglicht es, einige JVM - Optionen zu ändern. Ich bin mir nicht sicher, ob Sie es für dieses verwenden können, und wenn Sie könnten, ist es etwas zweifelhaft, dies in der Produktion zu tun.
Mick Mnemonic
1
@nullpointer Nein. Mein Ziel ist es, die zusätzlichen Anweisungen für Endbenutzer zu vermeiden. Wir haben viele Benutzer mit unseren Servern installiert, und das wäre eine große Unannehmlichkeit für sie.
Dmitriy Dumanskiy

Antworten:

55

Es gibt Möglichkeiten, die Warnung vor illegalem Zugriff zu deaktivieren, obwohl ich dies nicht empfehle.

1. Einfacher Ansatz

Da die Warnung an den Standardfehlerstrom gedruckt wird, können Sie einfach schließen dieser Strom und Umleitung stderrzu stdout.

public static void disableWarning() {
    System.err.close();
    System.setErr(System.out);
}

Anmerkungen:

  • Dieser Ansatz führt Fehler- und Ausgabestreams zusammen. Dies ist in einigen Fällen möglicherweise nicht wünschenswert.
  • Sie können die Warnmeldung nicht einfach durch Aufrufen umleiten System.setErr, da der Verweis auf den Fehlerstrom IllegalAccessLogger.warningStreamfrühzeitig beim JVM-Bootstrap im Feld gespeichert wird .

2. Komplizierter Ansatz ohne Änderung von stderr

Eine gute Nachricht ist, dass sun.misc.Unsafein JDK 9 weiterhin ohne Warnungen zugegriffen werden kann. Die Lösung besteht darin, das interne IllegalAccessLoggermithilfe der unsicheren API zurückzusetzen.

public static void disableWarning() {
    try {
        Field theUnsafe = Unsafe.class.getDeclaredField("theUnsafe");
        theUnsafe.setAccessible(true);
        Unsafe u = (Unsafe) theUnsafe.get(null);

        Class cls = Class.forName("jdk.internal.module.IllegalAccessLogger");
        Field logger = cls.getDeclaredField("logger");
        u.putObjectVolatile(cls, u.staticFieldOffset(logger), null);
    } catch (Exception e) {
        // ignore
    }
}
Apangin
quelle
2
@nullpointer Es wird nicht exportiert, muss aber nicht sein, damit der Trick funktioniert.
Apangin
13
Es ist ein Hack, der wahrscheinlich in zukünftigen Updates brechen wird. Das Richtige bei solchen Problemen ist, einen Fehler an die betreffende Bibliothek zu senden (möglicherweise haben Sie dies bereits getan). Sie können dies vorübergehend umgehen, indem Sie das Paket mit den gehackten Klassen genau öffnen. Es gibt verschiedene Möglichkeiten, die --add-opensOption zu übergeben (CLI, JAR-Manifest von ausführbaren JARs, env-Variablen).
Alan Bateman
7
WTF?! Harmless Reflection, z. B. für dokumentierte protectedMitglieder der offiziellen API, erhalten eine Warnung, der Zugriff sun.misc.Unsafejedoch nicht? Ok, habe die Nachricht erhalten, benutze Reflection nicht mehr (außer zum Erhalten theUnsafe) und benutze immer Unsafe. Dann müssen wir die Warnung nicht einmal ausschalten ...
Holger
2
@Holger Ja, die Ironie der Situation ist, dass all diese Aufregung um "Kapselung interner APIs" zu einer noch umfassenderen Verwendung von Unsafe führen könnte.
Apangin
4
Ich habe einen anderen Weg gefunden, Sie können das Äquivalent zu --add-opensprogrammgesteuert ausführen , z. B. müssen Target.class.getModule().addOpens(Target.class.getPackage(), MyClass.class.getModule());Sie dies für jedes Paket wiederholen, auf das Sie zugreifen möchten.
Holger
14

Es gibt eine weitere Option, für die keine Stream-Unterdrückung erforderlich ist und die nicht auf undokumentierten oder nicht unterstützten APIs basiert. Mit einem Java-Agenten können Module neu definiert werden, um die erforderlichen Pakete zu exportieren / zu öffnen. Der Code dafür würde ungefähr so ​​aussehen:

void exportAndOpen(Instrumentation instrumentation) {
  Set<Module> unnamed = 
    Collections.singleton(ClassLoader.getSystemClassLoader().getUnnamedModule());
  ModuleLayer.boot().modules().forEach(module -> instrumentation.redefineModule(
        module,
        unnamed,
        module.getPackages().stream().collect(Collectors.toMap(
          Function.identity(),
          pkg -> unnamed
        )),
        module.getPackages().stream().collect(Collectors.toMap(
           Function.identity(),
           pkg -> unnamed
         )),
         Collections.emptySet(),
         Collections.emptyMap()
  ));
}

Sie können jetzt jeden illegalen Zugriff ohne Warnung ausführen, da Ihre Anwendung im unbenannten Modul enthalten ist, wie zum Beispiel:

Method method = ClassLoader.class.getDeclaredMethod("defineClass", 
    byte[].class, int.class, int.class);
method.setAccessible(true);

Um an die InstrumentationInstanz zu gelangen, können Sie entweder einen recht einfachen Java-Agenten schreiben und ihn in der Befehlszeile (anstelle des Klassenpfads) angeben -javaagent:myjar.jar. Der Agent würde nur eine premainMethode wie folgt enthalten:

public class MyAgent {
  public static void main(String arg, Instrumentation inst) {
    exportAndOpen(inst);
  }
}

Alternativ können Sie befestigen dynamisch die anhängen API , die bequem durch zugänglich gemacht wird die Byte-Buddy-Agent - Projekt (die ich verfasst):

exportAndOpen(ByteBuddyAgent.install());

die Sie vor dem illegalen Zugriff anrufen müssten. Beachten Sie, dass dies nur auf JDKs und auf Linux-VMs verfügbar ist, während Sie den Byte Buddy-Agenten in der Befehlszeile als Java-Agenten angeben müssten, wenn Sie ihn auf anderen VMs benötigen würden. Dies kann praktisch sein, wenn Sie die Selbstanbringung auf Test- und Entwicklungsmaschinen wünschen, auf denen normalerweise JDKs installiert sind.

Wie andere betonten, sollte dies nur als Zwischenlösung dienen, aber ich verstehe voll und ganz, dass das aktuelle Verhalten häufig die Protokollierung von Crawlern und Konsolen-Apps beeinträchtigt, weshalb ich dies selbst in Produktionsumgebungen als kurzfristige Lösung für die Verwendung von Java 9 und verwendet habe so lange hatte ich keine probleme.

Das Gute ist jedoch, dass diese Lösung gegenüber zukünftigen Updates robust ist, da jeder Vorgang, selbst der dynamische Anhang, legal ist. Mit einem Hilfsprozess umgeht Byte Buddy sogar die normalerweise verbotene Selbstanhaftung.

Rafael Winterhalter
quelle
-1 Lösungen wie diese sind nicht nur kompliziert in die Praxis umzusetzen, sondern stehen auch den Zielen der "starken Verkapselung" des neuen Modulsystems von Java 9 gegenüber. Anstatt solche schmutzigen Hacks zu fördern, sollten wir entweder mit den JDK-Entwicklern von Oracle zusammenarbeiten, um reale Abwärtskompatibilitätsprobleme zu lösen, oder mit den Benutzern unserer Tools zusammenarbeiten, damit diese die strengeren Anforderungen von JDK 9+ akzeptieren. Zum Beispiel könnten wir Oracle bitten, eine (sehr vernünftige) java.lang.instrument.InstrumentationFactoryKlasse hinzuzufügen, damit kein dynamischer Anhang mehr benötigt wird.
Rogério
9
Diese Diskussion fand viele Monate nach der Mailingliste statt und wurde abgelehnt. Wenn Sie ein System mit Java 9 benötigen und ein begrenztes Budget haben, müssen Sie sich manchmal für eine Zwischenlösung entscheiden. Da keine inoffiziellen APIs verwendet werden, gilt dies auch nicht als Hack.
Rafael Winterhalter
@ Rogério Du liegst ziemlich falsch. Die Probleme müssen eines Tages gelöst werden, aber es gibt nichts, was ein gewöhnlicher Entwickler tun könnte oder sollte. Der illegale Zugriff findet normalerweise in Bibliotheken statt, die jetzt gut funktionieren und sich wahrscheinlich bereits darum gekümmert haben: Sie führen den illegalen Zugriff aus Effizienzgründen durch und greifen auf legale Wege zurück, wenn er fehlschlägt. Sie werden in dieser Hinsicht nicht aktualisiert, solange die Wahrscheinlichkeit, dass der illegale Zugang funktioniert, dh für einige Jahrzehnte, nicht zu vernachlässigen ist. Es sei denn, sie bekommen einen rechtlich effizienten Weg ....
maaartinus
10

Ich kenne keine Möglichkeit, das zu erreichen, wonach Sie fragen. Wie Sie bereits erwähnt haben, müssten Sie dem JVM-Start Befehlszeilenoptionen hinzufügen ( --add-opensjedoch nicht --illegal-access=deny).

Sie schrieben:

Mein Ziel ist es, die zusätzlichen Anweisungen für Endbenutzer zu vermeiden. Wir haben viele Benutzer mit unseren Servern installiert, und das wäre eine große Unannehmlichkeit für sie.

Wie es aussieht, lassen Ihre Anforderungen nur den Schluss zu, dass das Projekt nicht für Java 9 bereit ist. Es sollte seinen Benutzern ehrlich mitteilen, dass es etwas länger dauert, bis es vollständig Java 9-kompatibel ist. Das ist so früh nach der Veröffentlichung völlig in Ordnung.

Nicolai Parlog
quelle
10
import java.lang.reflect.Field;
import java.lang.reflect.Method;

public class Main {
    @SuppressWarnings("unchecked")
    public static void disableAccessWarnings() {
        try {
            Class unsafeClass = Class.forName("sun.misc.Unsafe");
            Field field = unsafeClass.getDeclaredField("theUnsafe");
            field.setAccessible(true);
            Object unsafe = field.get(null);

            Method putObjectVolatile = unsafeClass.getDeclaredMethod("putObjectVolatile", Object.class, long.class, Object.class);
            Method staticFieldOffset = unsafeClass.getDeclaredMethod("staticFieldOffset", Field.class);

            Class loggerClass = Class.forName("jdk.internal.module.IllegalAccessLogger");
            Field loggerField = loggerClass.getDeclaredField("logger");
            Long offset = (Long) staticFieldOffset.invoke(unsafe, loggerField);
            putObjectVolatile.invoke(unsafe, loggerClass, offset, null);
        } catch (Exception ignored) {
        }
    }

    public static void main(String[] args) {
        disableAccessWarnings();
    }
}

Es funktioniert für mich in JAVA 11.

Gan
quelle
1
@AlexByrth Finde eine Möglichkeit, das loggerFeld von jdk.internal.module.IllegalAccessLoggerauf null zu setzen. Schauen Sie sich den Quellcode von IllegalAccessLogger an.
Gan
Dies funktioniert auch für mich mit OpenJDK 14 unter ArchLinux, vielen Dank !!
Hartmut P.
3

Es gibt eine andere Methode, die nicht auf Hacks basiert und in keiner der obigen Antworten erwähnt wurde. Es funktioniert jedoch nur für Code, der im Klassenpfad ausgeführt wird. Daher kann jede Bibliothek, die die Ausführung unter Java 9+ unterstützen muss, diese Technik verwenden, sofern sie über den Klassenpfad ausgeführt wird.

Es basiert auf der Tatsache, dass Code, der auf Klassenpfad (dh vom unbenannten Modul) ausgeführt wird, Pakete eines Moduls frei dynamisch öffnen kann (dies kann nur vom Zielmodul selbst oder vom unbenannten Modul aus erfolgen).

Wenn Sie beispielsweise diesen Code verwenden, greifen Sie auf ein privates java.io.ConsoleKlassenfeld zu:

Field field = Console.class.getDeclaredField("formatter");
field.setAccessible(true);

Um die Warnung nicht auszulösen, müssen wir das Paket des Zielmoduls für unser Modul öffnen:

if (!ThisClass.class.getModule().isNamed()) {
    Console.class.getModule().addOpens(Console.class.getPackageName(), ThisClass.class.getModule());
}

Wir haben auch eine Überprüfung hinzugefügt, dass wir tatsächlich auf Klassenpfad laufen.

Ivan
quelle
2

Falls jemand die Protokollnachrichten umleiten möchte, anstatt sie zu verwerfen, funktioniert dies für mich in Java 11. Es ersetzt den Stream, in den der Logger für illegalen Zugriff schreibt.

public class AccessWarnings {

  public static void redirectToStdOut() {
    try {

      // get Unsafe
      Class<?> unsafeClass = Class.forName("sun.misc.Unsafe");
      Field field = unsafeClass.getDeclaredField("theUnsafe");
      field.setAccessible(true);
      Object unsafe = field.get(null);

      // get Unsafe's methods
      Method getObjectVolatile = unsafeClass.getDeclaredMethod("getObjectVolatile", Object.class, long.class);
      Method putObject = unsafeClass.getDeclaredMethod("putObject", Object.class, long.class, Object.class);
      Method staticFieldOffset = unsafeClass.getDeclaredMethod("staticFieldOffset", Field.class);
      Method objectFieldOffset = unsafeClass.getDeclaredMethod("objectFieldOffset", Field.class);

      // get information about the global logger instance and warningStream fields 
      Class<?> loggerClass = Class.forName("jdk.internal.module.IllegalAccessLogger");
      Field loggerField = loggerClass.getDeclaredField("logger");
      Field warningStreamField = loggerClass.getDeclaredField("warningStream");

      Long loggerOffset = (Long) staticFieldOffset.invoke(unsafe, loggerField);
      Long warningStreamOffset = (Long) objectFieldOffset.invoke(unsafe, warningStreamField);

      // get the global logger instance
      Object theLogger = getObjectVolatile.invoke(unsafe, loggerClass, loggerOffset);
      // replace the warningStream with System.out
      putObject.invoke(unsafe, theLogger, warningStreamOffset, System.out);
    } catch (Throwable ignored) {
    }
  }
}

Slawomir Chodnicki
quelle
1

Sie können openPakete einbinden module-info.javaoder ein erstellen open module.

Zum Beispiel: Kasse Schritt 5 und 6 der schrittweisen Migration Ihres Projekts zu Jigsaw

module shedlock.example {
    requires spring.context;
    requires spring.jdbc;
    requires slf4j.api;
    requires shedlock.core;
    requires shedlock.spring;
    requires HikariCP;
    requires shedlock.provider.jdbc.template;
    requires java.sql;
    opens net.javacrumbs.shedlockexample to spring.core, spring.beans, spring.context;
}

open module shedlock.example {
    requires spring.context;
    requires spring.jdbc;
    requires slf4j.api;
    requires shedlock.core;
    requires shedlock.spring;
    requires HikariCP;
    requires shedlock.provider.jdbc.template;
    requires java.sql;
}
TheKojuEffect
quelle
Der Link erwähnt erneut die Verwendung der Befehlszeile, die das OP ausdrücklich verweigert hat.
Naman
1

Hier ist, was für mich funktioniert hat

-Djdk.module.illegalAccess=deny
Ich
quelle
Wie ist das kein JVM-Argument?
Crusy
0

Ich habe eine Möglichkeit gefunden, diese Warnung zu deaktivieren, ohne Unsafe zu verwenden oder auf undokumentierte APIs zuzugreifen. Es funktioniert mit Reflection, um das FilterOutputStream::outFeld von System.errauf null zu setzen.

Wenn Sie versuchen, Reflection zu verwenden, wird natürlich die Warnung ausgegeben, die wir unterdrücken möchten, aber wir können die Parallelität ausnutzen, um dies zu umgehen:

  1. Sperren, System.errdamit kein anderer Thread darauf schreiben kann.
  2. Spawne 2 Threads, die setAccessibledas outFeld aufrufen . Einer von ihnen bleibt hängen, wenn versucht wird, die Warnung anzuzeigen, der andere wird abgeschlossen.
  3. Setzen Sie das outFeld von System.errauf null und lassen Sie die Sperre auf System.err. Der zweite Thread wird nun abgeschlossen, es wird jedoch keine Warnung angezeigt.
  4. Warten Sie, bis der zweite Thread beendet ist, und stellen Sie das outFeld von wieder her System.err.

Der folgende Code demonstriert dies:

public void suppressWarning() throws Exception
{
    Field f = FilterOutputStream.class.getDeclaredField("out");
    Runnable r = () -> { f.setAccessible(true); synchronized(this) { this.notify(); }};
    Object errorOutput;
    synchronized (this)
    {
        synchronized (System.err) //lock System.err to delay the warning
        {
            new Thread(r).start(); //One of these 2 threads will 
            new Thread(r).start(); //hang, the other will succeed.
            this.wait(); //Wait 1st thread to end.
            errorOutput = f.get(System.err); //Field is now accessible, set
            f.set(System.err, null); // it to null to suppress the warning

        } //release System.err to allow 2nd thread to complete.
        this.wait(); //Wait 2nd thread to end.
        f.set(System.err, errorOutput); //Restore System.err
    }
}

Dieser Code funktioniert auch dann, wenn --illegal-access"warn" oder "debug" eingestellt ist, da diese Modi die Warnung für denselben Anrufer nur einmal anzeigen.

Anstatt den ursprünglichen Status von wiederherzustellen System.err, können Sie das outFeld auch auf einen benutzerdefinierten OutputStream festlegen, um zukünftige Warnungen zu filtern.

Leandro
quelle