Что не так с моим интерфейсом MySupplier<T>?

Рейтинг: 1Ответов: 2Опубликовано: 19.01.2023

В чём разница между стандартным типом java.util.function.Supplier<T> и написанным мной:

@FunctionalInterface
interface MySupplier<T> {
    T get();
}

В этой программе функции works и fails отличаются только типом, к которому приводится Main::prompt, но первая работает, а вторая говорит TypeError: prompt is not a function: tio.run

import java.util.function.*;
import javax.script.*;
import java.io.*;
    
@FunctionalInterface
interface MySupplier<T> {
    T get();
}
    
public class Main {
    public static void main(String[] args) throws Exception {
        System.out.println("=== works ===");
        works();
    
        System.out.println("=== fails ===");
        fails();
    }
    
    static void works() throws Exception {
        var engine = new ScriptEngineManager().getEngineByName("js");
        engine.put("prompt", (Supplier<String>)Main::prompt);
        System.out.println(engine.eval("prompt()"));
    }
    
    static void fails() throws Exception {
        var engine = new ScriptEngineManager().getEngineByName("js");
        engine.put("prompt", (MySupplier<String>)Main::prompt);
        System.out.println(engine.eval("prompt()"));
    }
    
    static String prompt() {
        return "Hello World!";
    }
}

Ответы

▲ 3Принят

Любой класс/интерфейс, который используется внутри js движка должен быть публичным для того, чтоб движок мог его использовать.

Для вызова методов движок использует рефлексию. Схематически и упрощенно то, как это происходит можно показать на примере:

// Main.java 
package main;
import java.util.function.Supplier;

import otherpackage.EngineFromOtherPackage;

@FunctionalInterface
interface MySupplier<T> {
  T get();
}

public class Main {
 
  public static void main(String[] args) throws Exception {
    System.out.println("=== works ===");
    works();

    System.out.println("=== fails ===");
    fails();
  }

  static void works() throws Exception {
    var engine = new EngineFromOtherPackage();
    engine.use((Supplier<String>)Main::prompt);
  }

  static void fails() throws Exception {
    var engine = new EngineFromOtherPackage();
    engine.use((MySupplier<String>)Main::prompt);
  }

  static String prompt() {
    return "Hello World!";
  }
}

// EngineFromOtherPackage.java
package otherpackage;
public class EngineFromOtherPackage {

  public void use(Object f) {
    Method method;
    try {
      method = f.getClass().getInterfaces()[0].getDeclaredMethod("get");
      method.invoke(f, null);
    } catch (NoSuchMethodException | SecurityException | IllegalAccessException | IllegalArgumentException | InvocationTargetException e) {
      e.printStackTrace();
    }
  }
    
}

Если запустить этот код, то получим исключение IllegalAccessException при попытке вызвать метод из класса MySupplier, так как он package-private и значит не доступен из класса js движка, который естественно находися в своем пакете.

На этапе связывания методов (т.е. когда для функции скрипта ищется ее реализация) в движке делается проверка прав доступа и сообщение об ошибке (по задумке более дружелюбное пользователю) говорит, что функция не найдена, так как она по сути недоступна для вызова (хотя и существует если обойти права доступа).

Может возникнуть вопрос: почему права доступа не обходятся, как это часто делается в фреймворках типа spring?

Можно поспекулировать на эту тему. Ключевой момент тут это то, что исполняемый код тут на скриптовом языке. Вообще в такого рода (скриптовых) движках есть большая проблема с обеспечением безопасности. Вот что пишет об этом документация по Nashorn, который используется в данном случае:

Applications that embed Nashorn, in particular, server-side JavaScript frameworks, often have to run scripts from untrusted sources and therefore must limit access to Java APIs.

Тут речь идет об ограничении доступа даже к открытому API из JDK типа reflection API и т.д. То есть если, скажем, в spring вам нужно, чтоб spring вызвал методы или устанавливал значения даже приватных полей ваших же бинов, которые точно так же могут быть недоступны, то тут для скриптовых движков ситуация отличается, так как скрипт может быть получен от пользователя и нужно его ограничить.

▲ 2

Нужно сделать интерфейс публичным: вынести его в файл MySupplier.java или хотя бы определить его внутри класса Main:

public class Main {
    @FunctionalInterface
    public interface MySupplier<T> {
        T get();
    }
    // ...
}

Тогда его сможет обнаружить скриптовый движок:

=== works ===
Hello World!
=== fails ===
Hello World!