caoccao / Javet

Javet is Java + V8 (JAVa + V + EighT). It is an awesome way of embedding Node.js and V8 in Java.
https://www.caoccao.com/Javet/
Apache License 2.0
737 stars 69 forks source link

[Feature] Extend an Existing Java Class #365

Closed AsyncProxy closed 1 day ago

AsyncProxy commented 4 months ago

I hope to support JS to inherit classes from Java and implement rewrites, because many Java interfaces need to be rewritten, read-only, and there are still big limitations

caoccao commented 4 months ago

Please try Proxy.

AsyncProxy commented 4 months ago

Please try Proxy.

Didn't you say before that this feature would take 40 hours to implement?https://github.com/caoccao/Javet/issues/355

AsyncProxy commented 4 months ago

请尝试代理

The agent can only generate a new object, but I need the JS class to inherit from the Java class to directly override the method, such as the Java annotation Override

AsyncProxy commented 4 months ago

Please try Proxy.


class obj extends java.io.File {
    exists() {
        return 'exists';
    }
}

console.log(new obj('').exists())//exists```
caoccao commented 4 months ago

May I know how much you would like to donate for this feature, USD 5000? It is almost technically impossible.

AsyncProxy commented 4 months ago

May I know how much you would like to donate for this feature, USD 5000? It is almost technically impossible.

But how does the Rhino engine's JavaAdapter work?, I just want a feature like Rhino

AsyncProxy commented 4 months ago

我能知道你想为此功能捐款多少,5000美元吗?这在技术上几乎是不可能的。

Want this kind of defineClass feature. a

anonyein commented 4 months ago

我能知道你想为此功能捐款多少,5000美元吗?这在技术上几乎是不可能的。

Want this kind of defineClass feature. a

This is based on "rhinojs", maybe you can copy their code and realize it! And I think that it was possible! https://github.com/openautojs/openautojs/blob/f23289d25edbcb1b1b73248006a46462d437f826/autojs/src/main/java/com/stardust/autojs/rhino/AndroidClassLoader.java#L58

RevolvingMadness commented 3 months ago

@AsyncProxy what was the solution you came up with? I'm also questioning how this might be implemented. If you could reply with little code snippets or make a GitHub repo (or fork) that would be great! I've been stuck on this for a long time and finding a solution would make my day!

Losin6450 commented 3 months ago

You can use a library like Bytebuddy to do this:

const ByteBuddy = javet.package.net.bytebuddy.ByteBuddy;
const Handler = javet.package.Handler;
const HandlerAdapter = javet.package.net.bytebuddy.implementation.InvocationHandlerAdapter;
const ElementMatchers = javet.package.net.bytebuddy.matcher.ElementMatchers;
const defaultLoader = javet.package.java.lang.ClassLoader.getSystemClassLoader();
const FunctionJava = javet.package.java.util.function.Function;

function extend(cls, options){

    let loader = options.loader != null ? options.loader : defaultLoader;

    let buddy = new ByteBuddy();

    let proxy = buddy.subclass(cls);

    if (options.name != null) {
        proxy = proxy.name(options.name);
    }

    if (options.methods != null) {
        let keys =  Object.keys(options.methods);

        for (let key of keys){

            proxy = proxy.method(ElementMatchers.named(key)).intercept(HandlerAdapter.of(new Handler((args) => {return options.methods[key].apply(null, args)})));
        }
    }

    return proxy.make().load(loader).getLoaded();
}

const Func = extend(FunctionJava, {
    name: "FunctionProxy",
    methods: {
        apply: (test) => {
            return test[0]
        }
    }
});

let f = new Func();

console.log(f.apply("test"))
public class Handler implements InvocationHandler {

    private final Function<Object, Object> handler;
    public Handler(Function<Object, Object> handler){
        this.handler = handler;
    }

    @Override
    public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
        return handler.apply(new Object[] {args});
    }
}
Losin6450 commented 3 months ago

and by utilizing javascripts classes you can change the function a bit to make it easier to create proxies


function extendClass(cls, jsclass){
    let loader = defaultLoader;

    let buddy = new ByteBuddy();

    let proxy = buddy.subclass(cls).name(jsclass.name);

    let keys = Reflect.ownKeys(jsclass.prototype);

    for (let key of keys){
        proxy = proxy.method(ElementMatchers.named(key)).intercept(HandlerAdapter.of(new Handler((args) => {return Reflect.get(jsclass.prototype, key).apply(args[0], args[1])})));
    }

    return proxy.make().load(loader).getLoaded();
}

class BiFunctionProxy {

    apply(arg, arg2) {
        console.log(arg)
        console.log(arg2)
        return arg + arg2;
    }
}

const Functioncls = extendClass(BiFunctionJava, BiFunctionProxy);

let f = new Functioncls();

console.log(f.apply("test", "hej"))

Output:

[String: 'test']
[String: 'hej']
[String: 'testhej']
BiFunctionJava.isAssignableFrom(Functioncls); returns true
Losin6450 commented 3 months ago

you can access the proxy object by using this in the function but you are not able to use super to get the super object.

Losin6450 commented 3 months ago

Added a bit onto it and have gotten it to this result

const ByteBuddy = javet.package.net.bytebuddy.ByteBuddy;
const Handler = javet.package.Handler;
const HandlerAdapter = javet.package.net.bytebuddy.implementation.InvocationHandlerAdapter;
const ElementMatchers = javet.package.net.bytebuddy.matcher.ElementMatchers;

function generate(jsClass){
    let loader = javet.package.java.lang.ClassLoader.getSystemClassLoader();

    let buddy = new ByteBuddy();

    let javaClass = Object.getPrototypeOf(jsClass);

    let proxy = buddy.subclass(javaClass).name(jsClass.name);

    let keys = Reflect.ownKeys(jsClass.prototype);

    for (let key of keys){
        proxy = proxy.method(ElementMatchers.named(key)).intercept(HandlerAdapter.of(new Handler((args) => {return Reflect.get(jsClass.prototype, key).apply(args[0], args[1])})));
    }

    return proxy.make().load(loader).getLoaded();
}

class BiFunctionProxy extends javet.package.java.lang.System {

    apply(arg) {
        console.log(arg)
        return arg;
    }
}

const Functioncls = generate(BiFunctionProxy);

let f = new Functioncls();

f.apply("hej")

but there is an issue with this since it doesn't work with interface classes because in the proxy converter interface classes do not get the prototype of classes check out V8ProxyMode::isClassMode

Losin6450 commented 3 months ago

but if this were to change you would be able to use extends

anonyein commented 3 months ago

Added a bit onto it and have gotten it to this result

const ByteBuddy = javet.package.net.bytebuddy.ByteBuddy;
const Handler = javet.package.Handler;
const HandlerAdapter = javet.package.net.bytebuddy.implementation.InvocationHandlerAdapter;
const ElementMatchers = javet.package.net.bytebuddy.matcher.ElementMatchers;

function generate(jsClass){
    let loader = javet.package.java.lang.ClassLoader.getSystemClassLoader();

    let buddy = new ByteBuddy();

    let javaClass = Object.getPrototypeOf(jsClass);

    let proxy = buddy.subclass(javaClass).name(jsClass.name);

    let keys = Reflect.ownKeys(jsClass.prototype);

    for (let key of keys){
        proxy = proxy.method(ElementMatchers.named(key)).intercept(HandlerAdapter.of(new Handler((args) => {return Reflect.get(jsClass.prototype, key).apply(args[0], args[1])})));
    }

    return proxy.make().load(loader).getLoaded();
}

class BiFunctionProxy extends javet.package.java.lang.System {

    apply(arg) {
        console.log(arg)
        return arg;
    }
}

const Functioncls = generate(BiFunctionProxy);

let f = new Functioncls();

f.apply("hej")

but there is an issue with this since it doesn't work with interface classes because in the proxy converter interface classes do not get the prototype of classes check out V8ProxyMode::isClassMode

May I post a question? How to extend final "System" class?

AsyncProxy commented 3 months ago

Added a bit onto it and have gotten it to this result

const ByteBuddy = javet.package.net.bytebuddy.ByteBuddy;
const Handler = javet.package.Handler;
const HandlerAdapter = javet.package.net.bytebuddy.implementation.InvocationHandlerAdapter;
const ElementMatchers = javet.package.net.bytebuddy.matcher.ElementMatchers;

function generate(jsClass){
    let loader = javet.package.java.lang.ClassLoader.getSystemClassLoader();

    let buddy = new ByteBuddy();

    let javaClass = Object.getPrototypeOf(jsClass);

    let proxy = buddy.subclass(javaClass).name(jsClass.name);

    let keys = Reflect.ownKeys(jsClass.prototype);

    for (let key of keys){
        proxy = proxy.method(ElementMatchers.named(key)).intercept(HandlerAdapter.of(new Handler((args) => {return Reflect.get(jsClass.prototype, key).apply(args[0], args[1])})));
    }

    return proxy.make().load(loader).getLoaded();
}

class BiFunctionProxy extends javet.package.java.lang.System {

    apply(arg) {
        console.log(arg)
        return arg;
    }
}

const Functioncls = generate(BiFunctionProxy);

let f = new Functioncls();

f.apply("hej")

but there is an issue with this since it doesn't work with interface classes because in the proxy converter interface classes do not get the prototype of classes check out V8ProxyMode::isClassMode

May I post a question? How to extend final "System" class?

I can't use the example you gave me at all, and there is no method in the generated class.

Losin6450 commented 3 months ago

Added a bit onto it and have gotten it to this result

const ByteBuddy = javet.package.net.bytebuddy.ByteBuddy;
const Handler = javet.package.Handler;
const HandlerAdapter = javet.package.net.bytebuddy.implementation.InvocationHandlerAdapter;
const ElementMatchers = javet.package.net.bytebuddy.matcher.ElementMatchers;

function generate(jsClass){
    let loader = javet.package.java.lang.ClassLoader.getSystemClassLoader();

    let buddy = new ByteBuddy();

    let javaClass = Object.getPrototypeOf(jsClass);

    let proxy = buddy.subclass(javaClass).name(jsClass.name);

    let keys = Reflect.ownKeys(jsClass.prototype);

    for (let key of keys){
        proxy = proxy.method(ElementMatchers.named(key)).intercept(HandlerAdapter.of(new Handler((args) => {return Reflect.get(jsClass.prototype, key).apply(args[0], args[1])})));
    }

    return proxy.make().load(loader).getLoaded();
}

class BiFunctionProxy extends javet.package.java.lang.System {

    apply(arg) {
        console.log(arg)
        return arg;
    }
}

const Functioncls = generate(BiFunctionProxy);

let f = new Functioncls();

f.apply("hej")

but there is an issue with this since it doesn't work with interface classes because in the proxy converter interface classes do not get the prototype of classes check out V8ProxyMode::isClassMode

May I post a question? How to extend final "System" class?

I can't use the example you gave me at all, and there is no method in the generated class.

That was to show to usage of it and the javascript engine allows the use of extends javet.package.java.lang.System because the System object gets the prototype of a class but interface classes does not. so that was only a concept for it. if you want something that works now you would have to use this

anonyein commented 3 months ago

Added a bit onto it and have gotten it to this result

const ByteBuddy = javet.package.net.bytebuddy.ByteBuddy;
const Handler = javet.package.Handler;
const HandlerAdapter = javet.package.net.bytebuddy.implementation.InvocationHandlerAdapter;
const ElementMatchers = javet.package.net.bytebuddy.matcher.ElementMatchers;

function generate(jsClass){
    let loader = javet.package.java.lang.ClassLoader.getSystemClassLoader();

    let buddy = new ByteBuddy();

    let javaClass = Object.getPrototypeOf(jsClass);

    let proxy = buddy.subclass(javaClass).name(jsClass.name);

    let keys = Reflect.ownKeys(jsClass.prototype);

    for (let key of keys){
        proxy = proxy.method(ElementMatchers.named(key)).intercept(HandlerAdapter.of(new Handler((args) => {return Reflect.get(jsClass.prototype, key).apply(args[0], args[1])})));
    }

    return proxy.make().load(loader).getLoaded();
}

class BiFunctionProxy extends javet.package.java.lang.System {

    apply(arg) {
        console.log(arg)
        return arg;
    }
}

const Functioncls = generate(BiFunctionProxy);

let f = new Functioncls();

f.apply("hej")

but there is an issue with this since it doesn't work with interface classes because in the proxy converter interface classes do not get the prototype of classes check out V8ProxyMode::isClassMode

May I post a question? How to extend final "System" class?

I can't use the example you gave me at all, and there is no method in the generated class.

That was to show to usage of it and the javascript engine allows the use of extends javet.package.java.lang.System because the System object gets the prototype of a class but interface classes does not. so that was only a concept for it. if you want something that works now you would have to use this

Thanks and I roughly inject into "JavetJVMInterceptor.JavetV8.proxyGetStringGetterMap" with

stringGetterMap.put("extend", (propertyName) ->
                        v8Runtime.createV8ValueFunction(
                                new JavetCallbackContext(
                                        propertyName,
                                        this, JavetCallbackType.DirectCallNoThisAndResult,
                                        (NoThisAndResult<Exception>) (v8Values) -> {
                                            if (v8Values.length > 1) {
                                                try (V8ValueProxy v8ValueProxy = (V8ValueProxy) v8Values[0];
                                                     V8ValueObject v8ValueObject = (V8ValueObject) v8Values[1]) {
                                                    Class<?> cls = v8ValueProxy.getV8Runtime().getConverter().toObject(v8ValueProxy);
                                                    if (v8ValueObject.getType() == V8ValueReferenceType.Function) {
                                                        V8ValueFunction jclass = (V8ValueFunction) v8ValueObject;
                                                        ByteBuddy buddy = new ByteBuddy();
                                                        Builder<?> builders = buddy.subclass(cls);
                                                        final AtomicReference<Builder<?>> proxy = new AtomicReference<>(builders);
                                                        V8ValueObject prototype = jclass.getPrototype();
                                                        try (V8ValueFunction getOwnKeys = prototype.getV8Runtime()
                                                                .createV8ValueFunction("Reflect.ownKeys");
                                                             V8ValueArray keys = getOwnKeys.call(null, prototype)) {
                                                            keys.forEach((key -> {
                                                                proxy.set(proxy.get().method(ElementMatchers.named(key.asString()))
                                                                        .intercept(InvocationHandlerAdapter.of(new Handler((args) -> {
                                                                            try (V8ValueFunction v8ValueFunction = prototype.get(key.asString());
                                                                                 V8Value result = v8ValueFunction.call(null, args)) {
                                                                                return v8ValueFunction.getV8Runtime().toObject(result);
                                                                            } catch (JavetException e) {
                                                                                throw new RuntimeException(e);
                                                                            }
                                                                        }))));
                                                            }));
                                                        }
                                                        ClassLoader loader = ClassLoader.getSystemClassLoader();
                                                        try (Unloaded<?> make = proxy.get().make()) {
                                                            return v8ValueProxy.getV8Runtime()
                                                                    .getConverter()
                                                                    .toV8Value(v8ValueProxy.getV8Runtime(),
                                                                            make.load(loader, strategy).getLoaded()
                                                                    );
                                                        }
                                                    } else {
                                                        try (V8Value _methods = v8ValueObject.get("methods");
                                                             V8Value _loader = v8ValueObject.get("loader")) {
                                                            ClassLoader loader = ClassLoader.getSystemClassLoader();
                                                            if (!_loader.isNullOrUndefined()) {
                                                                loader = _loader.getV8Runtime().toObject(_loader);
                                                            }
                                                            ByteBuddy buddy = new ByteBuddy();
                                                            Builder<?> builders = buddy.subclass(cls);
                                                            final AtomicReference<Builder<?>> proxy = new AtomicReference<>(builders);
                                                            if (!_methods.isNullOrUndefined()) {
                                                                V8ValueObject methods = v8ValueObject.get("methods");
                                                                try (V8ValueArray keys = ((V8ValueArray) methods.getPropertyNames())) {
                                                                    keys.forEach((key -> proxy.set(proxy.get().method(ElementMatchers.named(key.asString()))
                                                                            .intercept(InvocationHandlerAdapter.of(new Handler((args) -> {
                                                                                try (V8ValueFunction v8ValueFunction = methods.get(key.asString());
                                                                                     V8Value result = v8ValueFunction.call(null, args)) {
                                                                                    return v8ValueFunction.getV8Runtime().toObject(result);
                                                                                } catch (JavetException e) {
                                                                                    throw new RuntimeException(e);
                                                                                }
                                                                            }))))));
                                                                }
                                                            }
                                                            try (Unloaded<?> make = proxy.get().make()) {
                                                                return v8ValueProxy.getV8Runtime()
                                                                        .getConverter()
                                                                        .toV8Value(v8ValueProxy.getV8Runtime(),
                                                                                make.load(loader, strategy).getLoaded()
                                                                        );
                                                            }
                                                        }
                                                    }
                                                }
                                            } else {
                                                try (V8ValueFunction jclass = (V8ValueFunction) v8Values[0];
                                                     V8ValueFunction getPrototypeOf = (V8ValueFunction) jclass.getV8Runtime()
                                                             .createV8ValueFunction("Object.getPrototypeOf")) {
                                                    Class<?> cls = getPrototypeOf.getV8Runtime().toObject(getPrototypeOf.call(null, jclass));
                                                    ByteBuddy buddy = new ByteBuddy();
                                                    Builder<?> builders = buddy.subclass(cls);
                                                    final AtomicReference<Builder<?>> proxy = new AtomicReference<>(builders);
                                                    V8ValueObject prototype = jclass.getPrototype();
                                                    try (V8ValueFunction getOwnKeys = prototype.getV8Runtime()
                                                            .createV8ValueFunction("Reflect.ownKeys");
                                                         V8ValueArray keys = getOwnKeys.call(null, prototype)) {
                                                        keys.forEach((key -> {
                                                            proxy.set(proxy.get().method(ElementMatchers.named(key.asString()))
                                                                    .intercept(InvocationHandlerAdapter.of(new Handler((args) -> {
                                                                        try (V8ValueFunction v8ValueFunction = prototype.get(key.asString());
                                                                             V8Value result = v8ValueFunction.call(null, args)) {
                                                                            return v8ValueFunction.getV8Runtime().toObject(result);
                                                                        } catch (JavetException e) {
                                                                            throw new RuntimeException(e);
                                                                        }
                                                                    }))));
                                                        }));
                                                    }
                                                    ClassLoader loader = ClassLoader.getSystemClassLoader();
                                                    try (Unloaded<?> make = proxy.get().make()) {
                                                        return jclass.getV8Runtime()
                                                                .getConverter()
                                                                .toV8Value(jclass.getV8Runtime(),
                                                                        make.load(loader, strategy).getLoaded()
                                                                );
                                                    }
                                                }
                                            }
                                        })
                        ));

and could be used by "javet.v8.extend" through your ideal of javascript

Losin6450 commented 3 months ago

well if you are gonna change the source code you might as well use this.

anonyein commented 3 months ago

well if you are gonna change the source code you might as well use this.

copy JavetJVMInterceptor and design by yourself, also you can add "await" at this place, such that you can use by "javet.v8.await()" from js side, many other methods have to be added into for convenient.

caoccao commented 1 month ago

This feature is on the way. The following PoC works.

v8Runtime.getGlobalObject().set("File", File.class);
String codeString = "let ChildFile = extend(File, {\n" +
        "  exists: () => !$super.exists(),\n" +
        "  isFile: () => true,\n" +
        "  isDirectory: () => true,\n" +
        "});\n" +
        "let file = new ChildFile('/tmp/not-exist-file');\n" +
        "JSON.stringify([file.exists(), file.isFile(), file.isDirectory()]);";
assertEquals("[true,true,true]", v8Runtime.getExecutor(codeString).executeString());
AsyncProxy commented 1 month ago

此功能即将推出。以下 PoC 的工作原理:

v8Runtime.getGlobalObject().set("File", File.class);
String codeString = "let ChildFile = extend(File, {\n" +
        "  exists: () => !$super.exists(),\n" +
        "  isFile: () => true,\n" +
        "  isDirectory: () => true,\n" +
        "});\n" +
        "let file = new ChildFile('/tmp/not-exist-file');\n" +
        "JSON.stringify([file.exists(), file.isFile(), file.isDirectory()]);";
assertEquals("[true,true,true]", v8Runtime.getExecutor(codeString).executeString());

Really, also for android, if it's true, it's the equivalent of JavaApater for rhino js, awesome

AsyncProxy commented 1 month ago

此功能即将推出。以下 PoC 的工作原理:

v8Runtime.getGlobalObject().set("File", File.class);
String codeString = "let ChildFile = extend(File, {\n" +
        "  exists: () => !$super.exists(),\n" +
        "  isFile: () => true,\n" +
        "  isDirectory: () => true,\n" +
        "});\n" +
        "let file = new ChildFile('/tmp/not-exist-file');\n" +
        "JSON.stringify([file.exists(), file.isFile(), file.isDirectory()]);";
assertEquals("[true,true,true]", v8Runtime.getExecutor(codeString).executeString());

I hope to refer to Rhino JS to achieve better compatibility with JavaApater and support for class-to-interface interfaces.