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=..., custom_name=...): 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:
Custom generated class name
App version
Work only in app, which version code >= 66690
You can control the middle part of the generated Java proxy class name with custom_name=....
That generates a class name in this shape:
If custom_name is not provided, the proxy system uses the Python class name automatically:
This becomes:
The same option is available on bind(...):
Names are sanitized before generation, so unsupported characters are replaced with _.
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.
Built-in Java Object Modifiers
The user-friendly behavior is now available directly in the Externagram Chaquopy fork, so you can use it for any Java object or Java class without additional wrappers or imports:
What is available
There are two groups of modifiers.
Persistent modifiers:
JUseGetterAndSetter/JGSJNotUseGetterAndSetter/JNGSJAccessAll/JAJNotAccessAll/JNA
These change the behavior of the current Java object or Java class itself.
Scoped chain modifiers:
JIgnoreResult/JIRJNotIgnoreResult/JNIRJSafe/JSJNotSafe/JNS
These only affect the current access chain and do not permanently mutate the object.
Default behavior
By default:
- getter/setter mode is enabled
- access-all mode is disabled
- safe mode is disabled
- ignore-result mode is disabled
That means regular access tries normal Chaquopy behavior first, but field-like names also prefer JavaBean-style accessors when available.
This getter/setter preference applies to:
- Java object instances
- Java classes when you work with static getters/setters
Getter/Setter mode
Use JNGS when you want raw field-style access instead of getX() / setX(...).
The same works on classes:
Notes:
- getter/setter mode is intended for field-like names such as
title,result,messageObject - names which already look like methods, such as
getClass()orisInterface(), stay method-like and are not remapped again
AccessAll mode
Use JA to allow access to non-public declared members.
This also works on Java classes for static members:
Internally, the Chaquopy fork caches declared methods, fields, and nested classes per Java class, so JA does not re-scan reflection metadata every time.
IgnoreResult mode
Use JIR when you want a method chain to keep returning the original receiver instead of the Java return value.
You can turn it off in the same chain with JNIR:
Safe mode
Use JS when you want safe optional-style chaining.
If some attribute or method is missing anywhere in the chain, the result becomes JNone instead of raising immediately.
Safe mode also propagates to Java objects returned during the chain, so you do not need to write .JS again after every step.
It also treats None as safe-empty and converts it to JNone while safe mode is active.
You can disable it mid-chain with JNS.
JNone
JNone is the sentinel returned by safe chains when access fails or a safe-chain step returns None.
Properties:
- falsy in boolean context
- keeps swallowing attribute access
- keeps swallowing calls
- can be compared by identity when needed
Object-level and class-level examples
Practical behavior summary
JGS/JNGSandJA/JNAare persistent for the current object or Java classJIR/JNIRandJS/JNSonly affect the current chaindir(obj)anddir(SomeJavaClass)expose the modifier namesJAworks with fields, methods, and nested classes/interfaces declared on the Java classJSafepropagates through returned Java objects automaticallyJSafeconvertsNonetoJNoneinside the safe chain
Limitations
This is still convenience access, not a full replacement for manual signature-driven reflection.
Keep in mind:
- if exact overload control matters, explicit Java reflection is still safer
- non-public lookup is based on declared members and class metadata from the Chaquopy fork
- getter/setter preference is only for field-like names, not for method-like names such as
getX
If you need precise overload selection, use regular reflection:
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