Skip to content
Open
Show file tree
Hide file tree
Changes from 2 commits
Commits
File filter

Filter by extension

Filter by extension

Conversations
Failed to load comments.
Loading
Jump to
Jump to file
Failed to load files.
Loading
Diff view
Diff view
36 changes: 36 additions & 0 deletions buildSrc/README.md
Original file line number Diff line number Diff line change
@@ -0,0 +1,36 @@
# buildSrc

Gradle build logic for the New Relic Java Agent. Contains custom tasks, Shadow JAR transformers,
and build utilities used across the project.

## DependencyPatcher

`DependencyPatcher` is a Shadow JAR `Transformer` that post-processes shaded dependency classes
during the agent shadow JAR build. It runs on every `.class` file under
`com/newrelic/agent/deps/` — the namespace into which all agent dependencies are relocated.

Because the Shadow plugin allows only one transformer to process a given class, all patching
logic is consolidated here rather than spread across individual transformers.

### How it works

For each candidate class, `DependencyPatcher` runs two ASM passes:

1. **Verification pass** (`canTransformResource`) — each `Patcher` contributes a read-only
`ClassVisitor`. If any patcher sets `shouldTransform = true`, the class is queued for
rewriting. This avoids the cost of rewriting classes that need no changes.

2. **Rewriting pass** (`transform`) — each `Patcher` contributes a rewriting `ClassVisitor`
that is chained together. The chain feeds into a `ClassWriter`, producing the patched
bytecode which is written to a temp file and later emitted into the output JAR.

### Patcher implementations

| Class | What it does |
|--------------------------------------------------|---|
| `ModifyReferencesToLog4j2Plugins` | Rewrites string LDC constants (bytecode instructions that load a constant value — in this case a string path — onto the operand stack) that reference the original `Log4j2Plugins.dat` path to the relocated path inside the agent JAR. |
| `RedirectGetLoggerCalls` | Redirects `Logger.getLogger(String)` and `Logger.getLogger(String, String)` static calls to `Logger.getGlobal()` to prevent dependency code from creating loggers that conflict with the agent's logging setup. |
| `RemoveUnsupportedAnnotationTargets`<sup>1</sup> | Caffeine v3 introduced a dependency on jspecify, whose `NullMarked` annotation declares `@Target` values that include `ElementType.MODULE` (Java 9) and `ElementType.RECORD_COMPONENT` (Java 16). When the shaded Caffeine v3 classes are present in the agent JAR and Spring scans it, Java 8's `AnnotationParser` encounters these unknown enum constants and throws `ArrayStoreException`. This patcher removes those two values from the `@Target` annotation on the shaded `NullMarked` class at build time. |
| `UnmappedDependencyErrorGenerator` | Validates that no `com/newrelic/` class references packages outside the allowed set. Throws a build error if an unshaded dependency reference is found. This is always last in the chain and has no rewriting visitor — it exists solely to catch missing shadow relocations at build time. |

`1` - This is safe because Caffeine 3 only utilizes the jspecify annotations for static code analysis tools, not for any sort of runtime reflection.
2 changes: 2 additions & 0 deletions buildSrc/src/main/java/com/nr/builder/DependencyPatcher.java
Original file line number Diff line number Diff line change
Expand Up @@ -11,6 +11,7 @@
import com.github.jengelman.gradle.plugins.shadow.transformers.TransformerContext;
import com.nr.builder.patcher.ModifyReferencesToLog4j2Plugins;
import com.nr.builder.patcher.RedirectGetLoggerCalls;
import com.nr.builder.patcher.RemoveUnsupportedAnnotationTargets;
import com.nr.builder.patcher.UnmappedDependencyErrorGenerator;
import org.gradle.api.file.FileTreeElement;
import org.gradle.api.logging.Logging;
Expand Down Expand Up @@ -48,6 +49,7 @@ public class DependencyPatcher implements Transformer {
private static final List<Patcher> patchers = Arrays.asList(
new ModifyReferencesToLog4j2Plugins(),
new RedirectGetLoggerCalls(),
new RemoveUnsupportedAnnotationTargets(),
new UnmappedDependencyErrorGenerator()
);

Expand Down
Original file line number Diff line number Diff line change
@@ -0,0 +1,79 @@
/*
*
* * Copyright 2020 New Relic Corporation. All rights reserved.
Comment thread
sharvath-newrelic marked this conversation as resolved.
Outdated
* * SPDX-License-Identifier: Apache-2.0
*
*/

package com.nr.builder.patcher;

import com.nr.builder.Const;
import com.nr.builder.Patcher;
import org.objectweb.asm.AnnotationVisitor;
import org.objectweb.asm.ClassVisitor;

import java.util.concurrent.atomic.AtomicBoolean;

/**
* Removes ElementType.MODULE and ElementType.RECORD_COMPONENT from the @Target annotation on
* the shaded jspecify NullMarked class. Both enum values were introduced after Java 8 and cause
* ArrayStoreException in Java 8's AnnotationParser when Spring scans the agent JAR because of
* the presence of the shaded Caffeine v3 jar.
*/
public class RemoveUnsupportedAnnotationTargets implements Patcher {

private static final String NULL_MARKED_INTERNAL_NAME =
"com/newrelic/agent/deps/caffeine3/org/jspecify/annotations/NullMarked";
private static final String TARGET_DESCRIPTOR = "Ljava/lang/annotation/Target;";

/**
* This will set shouldTransform to true if this is our target NullMarked class
*/
@Override
public ClassVisitor getVerificationVisitor(ClassVisitor next, AtomicBoolean shouldTransform) {
return new ClassVisitor(Const.ASM_API, next) {
@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
if (NULL_MARKED_INTERNAL_NAME.equals(name)) {
shouldTransform.set(true);
}
super.visit(version, access, name, signature, superName, interfaces);
}
};
}

@Override
public ClassVisitor getRewritingVisitor(ClassVisitor next) {
return new ClassVisitor(Const.ASM_API, next) {
private boolean isNullMarked = false;

@Override
public void visit(int version, int access, String name, String signature, String superName, String[] interfaces) {
isNullMarked = NULL_MARKED_INTERNAL_NAME.equals(name);
super.visit(version, access, name, signature, superName, interfaces);
}

@Override
public AnnotationVisitor visitAnnotation(String descriptor, boolean visible) {
AnnotationVisitor av = super.visitAnnotation(descriptor, visible);
if (!isNullMarked || !TARGET_DESCRIPTOR.equals(descriptor)) {
return av;
}
return new AnnotationVisitor(Const.ASM_API, av) {
@Override
public AnnotationVisitor visitArray(String name) {
AnnotationVisitor arrayAv = super.visitArray(name);
return new AnnotationVisitor(Const.ASM_API, arrayAv) {
@Override
public void visitEnum(String name, String descriptor, String value) {
if (!"MODULE".equals(value) && !"RECORD_COMPONENT".equals(value)) {
super.visitEnum(name, descriptor, value);
}
}
};
}
};
}
};
}
}
Loading