One codebase, many product tiers.
Strip premium logic out of your compiled bytecode with a single annotation and a Gradle flag.
BypassLogic is a Gradle plugin that removes the body of annotated methods at build time and replaces it with a safe default return. The code isn't hidden behind a runtime if (isPremium) check. It simply doesn't exist in the artifact you ship.
When you deliver software to run on the customer's premises, you hand over your binaries. Feature gating done at runtime (license checks, feature flags, config switches) has two weaknesses:
- Reverse engineering: a decompiler shows exactly what the paid feature does.
- Accidental or deliberate unlocking: flipping one flag, patching one boolean, or editing one config value can enable features the customer didn't pay for.
BypassLogic removes the problem at the source. Annotate the premium methods, build with the flag on, and the shipped .class files contain nothing but a default return.
Source code:
@BypassLogic
public void processPaidPlanRequest(String userId) {
if (!isValidUser(userId)) {
return;
}
handlePaidPlanUserRequest();
}
private boolean isValidUser(String userId) {
return ...;
}Decompiled .class after a build with BypassLogic enabled:
public void processPaidPlanRequest(String userId) {
return;
}Methods with a return value get a sensible default (see Default return values):
@BypassLogic
public List<Report> premiumReports(String tenantId) {
// expensive proprietary logic
}becomes
public List<Report> premiumReports(String tenantId) {
return Collections.emptyList(); // empty collection
}
- The plugin hooks into every
JavaCompiletask and runs after compilation (doLast). - Each generated
.classfile is scanned cheaply (SKIP_CODE,SKIP_DEBUG,SKIP_FRAMES) for the@BypassLogicannotation. Classes without it are left untouched. - Classes that contain the annotation are rewritten with ASM. A
MethodVisitorreplaces the body of each annotated method with an instruction sequence that returns the default value for the method's return type. - Stack map frames are recomputed (
COMPUTE_FRAMES), with a customClassWriterthat resolves common super classes through the project's compile classpath, so the output passes JVM bytecode verification.
Because the transformation works on bytecode, there's no annotation processor, no source rewriting, and no change to your IDE workflow.
// build.gradle
plugins {
id 'java'
id '<plugin-id>' version '<version>' // TODO: fill in once published
}
dependencies {
compileOnly 'assaviv:bypass-logic-annotations:<version>' // TODO: fill in coordinates
}import assaviv.annotations.BypassLogic;
public class ReportService {
@BypassLogic
public List<Report> premiumReports(String tenantId) {
...
}
}bypassLogicConfig {
enabled = project.hasProperty('community')
}./gradlew build # full build, all features present
./gradlew build -Pcommunity # stripped build, annotated logic removedenabled = false (the default behavior of a normal build) leaves your bytecode completely untouched.
| Return type | Replacement |
|---|---|
void |
immediate return |
int, byte, short, char |
0 |
long, float, double |
0 |
boolean |
false |
String |
default String value |
Integer, Byte, Short, Character, Long, Float, Double, Boolean |
default boxed value |
List, Set, Map |
empty collection |
Optional |
Optional.empty() |
| arrays | empty array |
| any other object | null |
Design your callers so these defaults mean "feature not available" (empty result,
false,null), and handle them gracefully.
BypassLogic is a code-removal tool, not a complete protection scheme. Please keep these points in mind:
- It removes method bodies only. Class names, method signatures, field names, and string constants outside the removed bodies remain visible. An attacker can still see that a feature exists, just not how it works.
- Callers are not changed. Code that calls a stripped method still runs and receives the default value. Make sure it handles that safely.
- Constructors and static initializers are not supported. Annotate regular methods only.
- Reflection, serialization, and dynamic proxies that reference removed members still see the (empty) members.
- Java compilation only (for now). The plugin hooks
JavaCompiletasks. Classes produced by other compilers (e.g.compileKotlin) are not processed yet. - Treat this as one layer of a defense-in-depth strategy, alongside licensing, obfuscation, and legal agreements.
BypassLogic/
├── bypass-logic-annotations/ # The @BypassLogic annotation
├── bypass-logic-plugin/ # Gradle plugin + ASM visitors
├── bypass-logic-tests/ # Tests for the plugin
├── build.gradle
└── settings.gradle
Key classes in the plugin:
| Class | Responsibility |
|---|---|
BypassLogicPlugin |
Registers the bypassLogicConfig extension and hooks into JavaCompile. |
BypassLogicExtension |
Plugin configuration (enabled). |
AnnotationsCheckerVisitor |
Fast pre-scan that detects whether a class has @BypassLogic. |
BypassLogicClassAdapter |
ASM class visitor that wraps each method. |
BypassLogicMethodAdapter |
ASM method visitor that emits the default-return bytecode. |
SuperClassResolvingWriter |
ClassWriter that resolves super classes via the project classpath. |
types.* |
Bytecode emitters for primitives, boxed types, and collections. |
Requirements: JDK 11 or newer (the Gradle wrapper is included).
git clone https://github.com/Assaviv/BypassLogic.git
cd BypassLogic
./gradlew build- Publish to the Gradle Plugin Portal / Maven Central
- Kotlin support (
compileKotlinoutput) - Configurable default values per method (e.g.
@BypassLogic(returns = ...)) - Option to throw
UnsupportedOperationExceptioninstead of returning a default - Gradle configuration cache and incremental build support
- GitHub Actions CI across multiple JDK versions
Issues and pull requests are welcome. Please open an issue first to discuss larger changes.
Distributed under the Apache License 2.0.
