ProxyBuilder contains runtime proxy builders and annotation processors for generated static proxies. It is intended both as a ready-to-use proxy utility and as a base for custom annotation processors, for example security wrappers.
<dependency>
<groupId>com.svenruppert</groupId>
<artifactId>proxybuilder</artifactId>
<version>00.11.01</version>
</dependency>For annotation processing in Maven:
<plugin>
<artifactId>maven-compiler-plugin</artifactId>
<configuration>
<annotationProcessorPaths>
<path>
<groupId>com.svenruppert</groupId>
<artifactId>proxybuilder</artifactId>
<version>00.11.01</version>
</path>
</annotationProcessorPaths>
</configuration>
</plugin>- JDK 26
- Maven 4 via the repository Maven Wrapper
- EUPL-1.2
Build:
./mvnw clean installRelease artifact checks:
./mvnw -P release -Dgpg.skip verifyproxybuilder-parent: reactor parent.proxybuilder-annotations: tiny annotations-only JAR consumed and emitted by the processor. Put this on the consumer compile classpath; seeproxybuilder-annotations/README.mdfor the annotation catalogue.proxybuilder: implementation artifact with runtime proxy utilities, annotation processors, and generated-proxy base APIs. Add via<annotationProcessorPaths>.proxybuilder-testusage: integration and usage examples.
DynamicProxyBuilder creates JDK dynamic proxies around an interface and implementation instance.
Service proxy = DynamicProxyBuilder
.createBuilder(Service.class, new ServiceImpl())
.addIPreAction((original, method, args) -> {
// executed before every invocation
})
.addIPostAction((original, method, args) -> {
// executed after every invocation
})
.build();Supported runtime features:
- Pre-actions before method invocation.
- Post-actions after method invocation.
- Security rules via
SecurityRule. - Logging proxy behavior.
- Metrics proxy behavior using Dropwizard Metrics.
- Virtual proxy creation strategies.
Service proxy = DynamicProxyBuilder
.createBuilder(Service.class, new ServiceImpl())
.addSecurityRule(() -> currentUserCanCallService())
.build();If the rule returns false, the invocation is blocked by the proxy.
Service proxy = DynamicProxyBuilder
.createBuilder(Service.class, new ServiceImpl())
.addMetrics()
.build();Metrics are collected through RapidPMMetricsRegistry.
metrics-jmx intentionally remains a dependency because JMX/console reporting is part of the metrics support.
Static proxies are generated during Java compilation through annotation processing.
Generates a wrapper that logs method calls through the project logger infrastructure.
@StaticLoggingProxy
public interface Service {
String work(String input);
}Generated class:
ServiceStaticLoggingProxy proxy = new ServiceStaticLoggingProxy()
.withDelegator(new ServiceImpl());Generates a wrapper that records method timings with Dropwizard Metrics.
@StaticMetricsProxy
public interface Service {
String work(String input);
}Generated class:
ServiceStaticMetricsProxy proxy = new ServiceStaticMetricsProxy()
.withDelegator(new ServiceImpl());Generates a static virtual proxy backed by the virtual proxy strategy infrastructure.
@StaticVirtualProxy
public interface Service {
String work(String input);
}Generates a static object adapter. For each adapted method, a functional interface is generated and can be installed on the adapter.
@StaticObjectAdapter
public interface Service {
String work(String input);
}Typical generated API shape:
ServiceStaticObjectAdapter adapter = new ServiceStaticObjectAdapter()
.withService(new ServiceImpl())
.withServiceMethodWorkString(input -> "adapted " + input);Generates a builder and invocation-handler pair for dynamic object adapter scenarios.
@DynamicObjectAdapterBuilder
public interface Service {
String work(String input);
}Typical generated API shape:
ServiceAdapterBuilder builder = ServiceAdapterBuilder.newBuilder()
.setOriginal(new ServiceImpl())
.withWork(input -> "adapted " + input);The static proxy processors fail compilation for method shapes that cannot be safely proxied:
finalclasses annotated for static proxy generation.finalmethods.privatemethods.staticmethods, unless-Aproxybuilder.failOnStaticMethods=falseis used.
Methods declared by java.lang.Object are not generated as proxy methods:
equalshashCodetoStringfinalize- other direct
Objectmethods such aswait,notify, andgetClass
Use BasicStaticProxyAnnotationProcessor<T> as the extension base.
Implement responsibleFor(), class-level additions, and method-generation logic.
public final class SecuredAnnotationProcessor
extends BasicStaticProxyAnnotationProcessor<Secured> {
@Override
public Class<Secured> responsibleFor() {
return Secured.class;
}
@Override
protected void addClassLevelSpecs(TypeElement typeElement, RoundEnvironment roundEnv) {
// Add fields, marker annotations, helper methods, or constructor details.
}
@Override
protected CodeBlock defineMethodImplementation(ExecutableElement methodElement,
String methodName2Delegate,
TypeElement targetType) {
return CodeBlock.builder()
.addStatement(delegatorStatementWithReturn(methodElement, methodName2Delegate))
.build();
}
}Override the class suffix programmatically:
@Override
protected String generatedClassSuffix(TypeElement typeElement) {
return "Secured";
}Or use a processor option:
-Aproxybuilder.suffix=SecuredAdjust generated constructor visibility:
@Override
protected Set<Modifier> filterConstructorModifiers(Set<Modifier> modifiers) {
modifiers.remove(Modifier.PUBLIC);
modifiers.add(Modifier.PROTECTED);
return modifiers;
}addStaticImports(JavaFile.Builder) is a no-op by default.
Override it only when generated sources need static imports.
@Override
protected void addStaticImports(JavaFile.Builder builder) {
builder.addStaticImport(MySecurityApi.class, "requireRole");
}Use beforeDelegation, aroundDelegation, and afterDelegation to combine several method-level annotations on the same method.
@Override
protected List<CodeBlock> beforeDelegation(ExecutableElement methodElement,
String methodName2Delegate,
TypeElement targetType) {
if (hasAnnotation(methodElement, RequiresRole.class.getCanonicalName())) {
return List.of(CodeBlock.of("securityEnforcer.requireRole();\n"));
}
return List.of();
}
@Override
protected Optional<CodeBlock> aroundDelegation(ExecutableElement methodElement,
String methodName2Delegate,
TypeElement targetType) {
return Optional.of(CodeBlock.of("$L;\n", delegatorStatementWithReturn(methodElement, methodName2Delegate)));
}
@Override
protected List<CodeBlock> afterDelegation(ExecutableElement methodElement,
String methodName2Delegate,
TypeElement targetType) {
return List.of(CodeBlock.of("securityAudit.recordSuccess();\n"));
}Security-style target:
public interface AccountService {
@RequiresPolicy("account")
@RequiresPermission("account:read")
@RequiresRole("admin")
Account loadAccount(String accountId);
}Annotation lookup helpers:
annotationsOn(ExecutableElement)annotationsOn(ExecutableElement, Set<String>)hasAnnotation(ExecutableElement, String)
BasicAnnotationProcessor exposes helpers used by custom processors:
pkgName(TypeElement): package name of the target type.className(Element): simple class name.targetClassNameSimpleForGeneratedClass(TypeElement): generated class simple name.targetClassNameSimpleForSourceClass(TypeElement): source class simple name.defineDelegatorField(TypeElement): standard delegator field.defineSimpleClassNameField(TypeElement): static class-name field.defineParamsForMethod(ExecutableElement): JavaPoet parameter specs.extractArgumentTypeListFromMethod(ExecutableElement): raw parameter type mirrors.delegatorMethodCall(ExecutableElement, String): method-call text.delegatorStatementWithReturn(ExecutableElement, String):return delegator.method(...).delegatorStatementWithOutReturn(ExecutableElement, String):delegator.method(...).delegatorStatementWithLocalVariableResult(ExecutableElement, String): localresultassignment.error,warning,note: compiler diagnostics throughMessager.
The base processor declares:
proxybuilder.verbose: emitsDiagnostic.Kind.NOTEmessages for@SkipProxyandexcludeMethodNamesskips.proxybuilder.suffix: overrides the generated class suffix.proxybuilder.failOnStaticMethods: defaults totrue; set tofalseto downgrade static-method findings to warnings.proxybuilder.suppressDelegatesTo: defaults tofalse; set totrueto strip the auto-emitted@DelegatesToannotation from generated wrapper methods.
For per-type overrides see @ProxyBuilderOptions and @ProxyName in proxybuilder-annotations.
Maven example:
<compilerArgs>
<arg>-Aproxybuilder.suffix=Secured</arg>
<arg>-Aproxybuilder.failOnStaticMethods=true</arg>
</compilerArgs>The implementation artifact provides module-info.java with module name:
module com.svenruppert.proxybuilderjdeps --check com.svenruppert.proxybuilder is used to validate the descriptor.
API-visible dependencies such as JavaPoet, java.compiler, core, and Metrics are marked as transitive where needed.
Trigger annotations are evaluated on the elements returned by the annotation-processing round. Put trigger annotations directly on the type that should produce generated sources.
The processors walk methods declared on superclasses and implemented interfaces for generation and validation. Trigger annotations on a superclass do not automatically trigger generation for subclasses.
The processors generate sources through the standard Filer API and tolerate duplicate generation attempts across rounds.
The project is verified with Maven's normal incremental compile behavior through ./mvnw clean install.
Gradle incremental annotation-processing metadata is not published yet.
proxybuilder-testusage intentionally keeps a small maven-antrun-plugin step that compiles annotation-generated sources before test compilation.
Removing that step currently breaks testCompile, because some tests reference generated classes directly.
Run the focused processor tests:
./mvnw -pl impl testRun the full reactor:
./mvnw clean installValidate the release profile without signing:
./mvnw -P release -Dgpg.skip verifyValidate JPMS after a build:
./mvnw -pl impl dependency:build-classpath -Dmdep.outputFile=target/classpath.txt
jdeps --multi-release 26 \
--module-path "impl/target/proxybuilder-00.11.00-SNAPSHOT.jar:$(cat impl/target/classpath.txt)" \
--check com.svenruppert.proxybuildercentral-publishing-maven-plugin 0.10.0 (the current latest) is not Maven-4-aware.
It emits the Maven 4 consumer POM as a consumer-classified artefact (*-consumer.pom) and uploads the build POM as the primary .pom, which the Central Portal validator rejects with Failed to associate file with coordinates ….
Plugin v0.10.0 also offers no "stage but don't upload" mode — skipPublishing=true skips staging altogether ("No files to stage for artifact" in the log).
The workaround bypasses the plugin: ./mvnw install already produces all signed artefacts in ~/.m2/repository/com/svenruppert/<module>/<version>/ with Maven 4's correct local-repo naming (the consumer POM as <artifact>-<version>.pom, the build POM as <artifact>-<version>-build.pom).
The script in scripts/clean-bundle-for-central.sh copies the publishable files (drops *-build.pom, *-tests*, etc.), generates the four checksum types Central expects, and zips a bundle that is uploaded by hand through the Central Portal web UI.
Central's validator inspects each artifact's POM directly and does not resolve <parent> for the metadata it requires. Every published module's pom.xml must therefore declare these fields inline:
<name><description><url><licenses>(at least one)<scm><developers>(at least one)
impl/pom.xml already carries <name>, <description>, and <url>. The remaining fields are currently inherited from proxybuilder-parent/com.svenruppert:dependencies; if a future Central validation pass rejects the bundle with a missing-field error (Error: Project <field> is missing), copy that field into impl/pom.xml and re-bundle.
./mvnw clean verify
./mvnw versions:set -DnewVersion=<RELEASE> -DprocessAllModules=true -DgenerateBackupPoms=false
./mvnw clean verify
git commit -am "release <RELEASE>"
git tag <RELEASE>
./mvnw clean install -P release,_release_prepare
./scripts/clean-bundle-for-central.sh
# Upload target/central-publishing/central-bundle.zip via
# https://central.sonatype.com/publishing → Publish Component
./mvnw versions:set -DnewVersion=<NEXT>-SNAPSHOT -DprocessAllModules=true -DgenerateBackupPoms=false
git commit -am "prepare <NEXT>-SNAPSHOT"
git push origin <branch> <RELEASE>Verify the bundle layout with:
unzip -l target/central-publishing/central-bundle.zip | grep -c build.pom # expected: 0
unzip -l target/central-publishing/central-bundle.zip | grep -c consumer # expected: 0- The external parent
com.svenruppert:dependenciesremains in use. - Logging remains based on
HasLogger. com.svenruppert:coreremains compile scope.metrics-jmxremains because JMX/console reporting is part of the metrics support.slf4j-simpleis not a main library dependency.reflections8is only a test dependency inproxybuilder-testusage.- No GitHub Actions workflow is part of the current scope.