Class Proxy
Create Java subclasses and dynamic proxy classes from Python.
Class Proxy
The class proxy API allows a plugin to create a real Java class at runtime and implement it in Python.
This is useful when a Java API expects:
- a subclass of an existing Java class
- an implementation which overrides parent methods
- extra methods declared directly on the generated Java class
- Java fields stored on the generated object
The proxy system is built on top of DexMaker, so the result is an actual Java class which can be passed anywhere the app expects that type.
new_instance() returns a Python peer object, not the raw Java instance. Use .java when an API explicitly requires the generated Java object itself.
Recommended API
The recommended API is the managed DSL from extera_utils.classes.
Main pieces:
Base: base Python class for managed Java subclasses@java_subclass(JavaClass, Interface1, Interface2, ..., methods=..., constructors=...): binds your Python class to a Java base class and optional Java interfaces@joverride(...): overrides an existing Java method@joverload(name, arg_types): overload-friendly alias for@joverride@jmethod(...): adds a new method directly to the generated Java classjMVELmethod(...): adds a Java method backed by MVEL instead of PythonjMVELoverride(...): overrides a parent Java method with MVEL code@jclassbuilder(): lets you modifyDexMakerbefore the class is finalized and loadedjfield(...): adds a Java field to the generated classjgetmethod(...)/jsetmethod(...): generate Java-only getter/setter methods for a field@jconstructor(...): runs Python code after the Java constructor has finished@jpreconstructor(...): modifies constructor arguments before calling the Java parent constructor
Useful aliases also exist:
jmvelmethod/jMVELmethodjmveloverride/jMVELoverridejoverride,joverload,jmethod,jfield,jconstructor,jpreconstructorjgetmethod,jsetmethod,jclassbuilder
Quick Start
What happens here:
CountingListbecomes a generated Java subclass ofjava.util.ArrayListadded_countbecomes a real Java field on that classadd(Object)is overridden in Java and routed into Pythonsuper().add_item(...)calls the original parent Java implementation
Because ArrayList.add is overloaded, the example uses @joverload(...). For overloaded Java methods, prefer explicit signatures instead of bare @joverride().
Creating a Java Subclass
Use @java_subclass for the most readable syntax:
If the generated Java class must also implement interfaces, pass them after the base class:
This produces a generated Java class equivalent to:
Generated proxy classes are also loaded through a shared generated-class loader chain, so a proxy created later can reference earlier generated proxy classes when needed.
This is still not a substitute for a stable API type. If possible, prefer referencing another proxy through an interface or a common base class instead of depending on the exact generated class.
You can also bind after the class definition:
And bind(...) supports the same interface list:
To get the generated Java class:
To create an instance:
instance is a Python peer of MySubclass. If you need the raw Java object:
If you want to pass separate arguments into the Python __init__ method, use init_args:
In this case:
"java ctor value"is used for Java constructor resolution["python init value"]is passed to Python__init__
If you already have a raw Java instance of the generated class and want to recover its Python peer, use from_java(...):
This is useful when:
- some Java API created or stored the proxy instance for you
- you received the generated object back through another callback
- you want to continue working with the Python peer instead of the raw Java object
Overriding Methods
Simple Override
If the Java method name is the same as the Python method name:
Override With Another Python Name
If the Java method name is not convenient in Python, pass it explicitly:
This is useful for names such as equals, hashCode, or when you want several Python handlers for overloaded methods.
Overloaded Methods
When a Java class has several methods with the same name, use the overload-aware decorators.
You can write the same thing with joverride:
Supported Type Formats
Argument and return types may be specified as:
- primitive names like
"int","boolean","long" - fully-qualified class names like
"java.lang.String" - Java classes returned by
jclass(...) - Java primitive type objects
Example:
Calling Parent Methods With super()
For overridden methods, super() works like a normal Python method call:
For overloaded methods, call the Python method name you defined:
Adding New Java Methods
Use @jmethod to declare a method which does not exist on the parent Java class.
This creates a real Java method named debugLabel(int) on the generated class.
jmethod Signatures
These forms are supported:
If the return type and argument types are not passed explicitly, jmethod can infer them from Python annotations:
Explicit types are still recommended when the Java signature matters a lot.
Adding MVEL Methods
If a method is called very often and the logic is simple enough to keep on the Java side, you can generate a method whose body is executed by MVEL instead of calling Python.
This creates a real Java method named debug_label(String, int) on the generated class, but its implementation runs through compiled MVEL.
Overriding With MVEL
For parent methods, use jMVELoverride(...):
If the parent method is overloaded, pass explicit argument types just like @joverload(...):
MVEL Context
Inside MVEL:
- the root object is the generated Java instance, so Java fields and methods can be accessed directly
java: the same generated Java instancepython/py/self: the attached Python peer objectargs:Object[]with all method argumentsargc: number of argumentsarg0,arg1, ...: positional arguments- named arguments from
arguments=[("name", "type"), ...]
That means all of these are valid:
For override methods, the generated SUPER_<methodName>(...) bridge is also available:
That pattern is useful for small hot-path UI overrides where the method body is simple but called very often.
When To Use MVEL
- Use
jfield(..., methods=[jgetmethod(...), jsetmethod(...)])for pure field getter/setter hot paths. That is still the fastest option. - Use
jMVELmethod(...)orjMVELoverride(...)when the method needs a bit of logic, but you still want to avoid calling into Python every time. - Keep MVEL bodies small and predictable. If the logic needs Python objects, complex state, or heavy control flow, regular Python overrides are usually easier to maintain.
Adding Java Fields
Use jfield to declare fields on the generated Java class.
You can read and write them like normal Python attributes:
Notes:
- the field is generated on the Java class
- the value is available through the Python peer object as a normal attribute
- assigning to the field updates the attached Java instance when possible
Java-only Field Accessors
If a method is called very often and it only needs to return or update a field value, you can generate a Java-only getter or setter directly from jfield(...).
This generates:
- a Java field named
shadow_value - a Java method
isShadow()which directly returns that field - a Java method
setShadow(boolean)which directly writes that field
These accessor methods do not call into Python at all, so they are useful for very hot paths such as repeated UI queries.
This is especially useful when you want to override a parent getter with a static value:
If SomeFactoryClass already has isShadow(), the generated Java method overrides it in the proxy class and returns the field immediately.
Because generated proxy classes now load through a shared generated-class loader chain, a proxy created later can also reference a proxy class created earlier as a field or method type if you truly need that.
Even so, a stable shared type such as an interface or common base class is still the safer design for public APIs.
Constructors
Constructor hooks are split into two stages:
@jpreconstructor(...): before the parent Java constructor call@jconstructor(...): after the Java object has already been created
Pre-constructor
Use this when you need to change constructor arguments:
The function should return:
Noneto keep original arguments- a list or tuple with replacement arguments
- a single value for a one-argument constructor
Post-constructor
Use this when the Java object already exists and you want to initialize Python state:
__init__ and on_post_init
The managed DSL also supports generic initialization:
Initialization order is:
@jpreconstructor(...)if matched- Java parent constructor
- Python
__init__(...) - matched
@jconstructor(...) on_post_init(...)
If you only need a single generic hook, __init__ is usually enough.
If you need different logic for overloaded constructors, use @jconstructor(...).
Separate Python init arguments
By default, Python __init__ receives the same arguments as the Java constructor.
If you need different Python-only initialization data, pass it explicitly through new_instance(..., init_args=[...]):
This is useful when the Java constructor signature and the Python peer state do not match exactly.
Accessing the Attached Java Instance
Inside a managed subclass:
self.javais the attached Java instanceself.thisis an alias toself.java
Example:
If Python cannot find an attribute on the peer object, it falls back to the Java instance:
This means most Java calls work directly on the peer object:
But if a Java API checks the exact runtime type and expects a real Java object parameter, pass instance.java.
Wrapping Existing Java Instances
Sometimes an API hands your generated object back to you as a plain Java object. In that case, use from_java(...) to recover the Python peer instead of manually copying state around.
If the peer already exists, from_java(...) returns the same Python object. If it does not, the proxy system creates and binds it.
Passing Python Objects Through Java
The low-level helper PyObj is available when you need to carry an arbitrary Python object through Java-facing APIs.
PyObj is mainly useful when:
- a Java API needs to carry opaque plugin-owned state
- you are building low-level adapters or factory layers
- you want to place Python data into a Java field or something like
UItem.object
For normal subclassing and method overrides, you usually do not need PyObj directly.
Java Helper (J / JavaHelper / ClassHelper)
At the bottom of extera_utils.classes there is also a small reflection helper:
All three names point to the same class. In normal code, J(...) is the shortest and most convenient form.
What it is for
J(obj) wraps a Java object and gives you a more Python-friendly way to:
- call reflected Java methods
- read or write reflected Java fields
- optionally prefer Java getters and setters over direct field access
- optionally access non-public members
- optionally ignore method return values so calls can be chained
This helper is meant for convenience when you already have a Java object and want a compact reflection-style API.
It is not a replacement for:
hook_utils.find_class(...)when you first need to locate a Java class- the managed proxy DSL when you need real Java subclasses
- overload-aware reflection when exact method signatures matter
Basic usage
From there:
helper.someFieldreads a reflected field, orgetSomeField()if getter/setter mode is enabled and such a getter existshelper.someField = valuewrites a reflected field, or callssetSomeField(value)if setter/getter mode is enabled and such a setter existshelper.someMethod(...)reflects a Java method and invokes it
Getter/setter mode
By default, J(...) prefers JavaBean-like getters and setters when they exist.
So this:
roughly means:
if getTitle() / setTitle(...) are declared on the class.
If no matching getter/setter exists, the helper falls back to direct field access.
Example
Accessing non-public members
By default, non-public reflected members are blocked.
To allow access to private, protected, or package-private declared members, switch to JAccessAll:
This causes the helper to call setAccessible(True) on the reflected member before using it.
To explicitly switch that back off, use JNotAccessAll.
Disabling getter/setter mode
If you want raw field access instead of getX() / setX(...) resolution, switch to JNotUseGetterAndSetter:
To explicitly switch it back on, use JUseGetterAndSetter.
Ignoring return values for chaining
Normally, a reflected method call returns the Java result:
If you want fluent chaining instead, use JIgnoreResult:
In that mode, each reflected method call returns the helper itself instead of the Java return value.
To switch back to normal behavior, use JNotIgnoreResult.
Flags are immutable wrappers
The special toggles:
JAccessAll/JNotAccessAllJUseGetterAndSetter/JNotUseGetterAndSetterJIgnoreResult/JNotIgnoreResult
do not mutate the existing helper in place.
Instead, each one returns a new helper wrapper with the updated behavior:
Static members
If the reflected field or method is static, the helper automatically uses None as the receiver when invoking it.
That means static members declared on the wrapped object's runtime class can still be reached through the same wrapper.
Important limitations
J(...) is intentionally lightweight, so there are a few constraints:
- it indexes methods by name only, so overloaded Java methods are not disambiguated safely
- it uses
getDeclaredMethods()andgetDeclaredFields(), so the helper focuses on members declared on the concrete runtime class - if it does not find a declared reflected member, it falls back to normal Chaquopy attribute access on the original object
Because of that, J(...) is best for simple convenience access, not for overload-sensitive or heavily dynamic reflection code.
If you need exact overload control, use normal Java reflection directly:
Practical example
Full Example
Full Smoke Test Example
This example is designed to verify almost the entire class proxy API in one place:
- subclass generation
- Java field creation
- constructor preprocessing
- post-constructor hooks
- overloaded method overrides
super()calls- custom Java methods
- access to the attached Java instance
It uses java.util.ArrayList, because it is a normal subclassable Java class with overloaded methods and constructors.
What to expect
If everything works, this example should confirm that:
DebugList.new_instance(4)selects theArrayList(int)constructorDebugList.new_instance(4)returns the Python peer object, anddebug_list.javais the raw Java instance@jpreconstructor(["int"])runs before Java construction@jconstructor(["int"])runs after Java constructionadd_calls,remove_calls, andlast_actionbehave like Java-backed fields- the correct overloaded
addandremovehandlers are called super()reaches the originalArrayListimplementationdebugSummary()andhistoryDump()exist as real Java methods on the generated class
Custom Settings Example
The settings SDK uses this system internally for custom setting factories.
Here is a simplified pattern:
If you instantiate this proxy with FactoryProxy.new_instance(), the result is the Python peer. Pass factory.java into APIs which require CustomSetting.Factory.
For a higher-level settings guide, see Plugin Settings.
Advanced Notes
Method Resolution Order
For managed overrides, the proxy system installs a Python bridge so that super() calls the parent Java implementation.
Because of this, it is best to use super() only for methods marked with @joverride(...) or @joverload(...).
Exact Signature Dispatch
When Java calls Python, the proxy system uses a full signature key internally:
This is what makes overloaded methods and constructors work correctly.
Primitive Types
Supported primitive type names:
voidbooleanbytecharshortintlongfloatdouble
When To Use It
Use class proxy when:
- a Java API requires subclassing instead of interface proxying
- you need parent-method calls with
super() - you need overload-specific behavior
- you want Java fields or extra Java methods on the generated object
Do not use it when a simple dynamic_proxy(...) is enough for an interface.
Also make sure the base Java class is actually subclassable. Final classes cannot be used as a proxy base class.
Troubleshooting
super() does not work
Check that:
- the method is declared with
@joverride(...)or@joverload(...) - the method name or signature matches the Java parent method
My overload is not called
Check that:
- the Java name is correct
- the argument types match the real Java signature exactly
- primitive types use primitive names such as
"int"instead of wrapper classes
Constructor hook is not called
Check that:
@jpreconstructor(...)or@jconstructor(...)uses the correct constructor signature- for wildcard constructor hooks, your annotations are either complete or you intentionally omitted the signature
Field value is always None
Check that:
- the field was declared with
jfield(...) - the field type is valid
- the default value is compatible with the declared Java type
Class generation fails immediately
Check that:
- the base class is not
final - constructor signatures match a real constructor
- overload signatures match real Java method argument types