Skip to content

Commit 065b534

Browse files
feat: run Java dedup as java agent
Signed-off-by: Asish Kumar <officialasishkumar@gmail.com>
1 parent c83b581 commit 065b534

6 files changed

Lines changed: 322 additions & 47 deletions

File tree

.github/workflows/java-agent.yml

Lines changed: 33 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -0,0 +1,33 @@
1+
name: Java Agent
2+
3+
on:
4+
pull_request:
5+
branches: [main]
6+
push:
7+
branches: [main]
8+
9+
jobs:
10+
verify:
11+
name: JDK ${{ matrix.java-version }}
12+
runs-on: ubuntu-latest
13+
strategy:
14+
fail-fast: false
15+
matrix:
16+
java-version: ["8", "17", "21"]
17+
18+
steps:
19+
- name: Checkout
20+
uses: actions/checkout@v4
21+
22+
- name: Set up JDK ${{ matrix.java-version }}
23+
uses: actions/setup-java@v4
24+
with:
25+
distribution: temurin
26+
java-version: ${{ matrix.java-version }}
27+
cache: maven
28+
29+
- name: Build
30+
run: mvn -B -DskipTests clean verify
31+
32+
- name: Smoke test Java agent
33+
run: ./scripts/smoke-javaagent.sh

.woodpecker/build.yml

Lines changed: 3 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -18,13 +18,16 @@ steps:
1818
image: maven:3.9-eclipse-temurin-8
1919
commands:
2020
- mvn -B -DskipTests clean verify
21+
- ./scripts/smoke-javaagent.sh
2122

2223
build-jdk-17:
2324
image: maven:3.9-eclipse-temurin-17
2425
commands:
2526
- mvn -B -DskipTests clean verify
27+
- ./scripts/smoke-javaagent.sh
2628

2729
build-jdk-21:
2830
image: maven:3.9-eclipse-temurin-21
2931
commands:
3032
- mvn -B -DskipTests clean verify
33+
- ./scripts/smoke-javaagent.sh

README.md

Lines changed: 38 additions & 47 deletions
Original file line numberDiff line numberDiff line change
@@ -23,78 +23,69 @@ Coverage is collected at per-testcase granularity, not process granularity.
2323

2424
## How to Use
2525

26-
### 1. Add the SDK
26+
### 1. Download the Keploy Java Agent
2727

28-
Add `keploy-sdk` to your application:
28+
Download the `keploy-sdk` jar and keep it outside your application dependencies. The jar is a Java agent and should be attached only when you run Keploy dynamic deduplication.
2929

3030
```xml
31-
<dependency>
32-
<groupId>io.keploy</groupId>
33-
<artifactId>keploy-sdk</artifactId>
34-
<version>2.0.0</version>
35-
</dependency>
31+
<plugin>
32+
<groupId>org.apache.maven.plugins</groupId>
33+
<artifactId>maven-dependency-plugin</artifactId>
34+
<version>3.6.1</version>
35+
<executions>
36+
<execution>
37+
<id>copy-keploy-java-agent</id>
38+
<phase>package</phase>
39+
<goals>
40+
<goal>copy</goal>
41+
</goals>
42+
<configuration>
43+
<artifactItems>
44+
<artifactItem>
45+
<groupId>io.keploy</groupId>
46+
<artifactId>keploy-sdk</artifactId>
47+
<version>2.0.1</version>
48+
<outputDirectory>${project.build.directory}</outputDirectory>
49+
<destFileName>keploy-sdk.jar</destFileName>
50+
</artifactItem>
51+
</artifactItems>
52+
</configuration>
53+
</execution>
54+
</executions>
55+
</plugin>
3656
```
3757

38-
### 2. Activate the Agent
58+
The SDK no longer has to be added to `dependencies`, and application code should not import `io.keploy.*` classes for dynamic deduplication.
3959

40-
For Spring Boot, import the middleware in your application:
41-
42-
```java
43-
import io.keploy.servlet.KeployMiddleware;
44-
import org.springframework.context.annotation.Import;
45-
46-
@Import(KeployMiddleware.class)
47-
public class Application {
48-
}
49-
```
50-
51-
For servlet-based applications, register the filter early in `web.xml`:
52-
53-
```xml
54-
<filter>
55-
<filter-name>middleware</filter-name>
56-
<filter-class>io.keploy.servlet.KeployMiddleware</filter-class>
57-
</filter>
58-
<filter-mapping>
59-
<filter-name>middleware</filter-name>
60-
<url-pattern>/*</url-pattern>
61-
</filter-mapping>
62-
```
63-
64-
The middleware starts the Java dedup control server automatically.
65-
66-
For Jakarta Servlet stacks, non-servlet frameworks, or any application where the `javax.servlet` filter is not available, start the agent directly during application startup:
67-
68-
```java
69-
import io.keploy.dedup.KeployDedupAgent;
70-
71-
KeployDedupAgent.start();
72-
```
73-
74-
### 3. Run the App with the JaCoCo Java Agent
60+
### 2. Run the App with the Keploy and JaCoCo Java Agents
7561

7662
The dedup agent reads coverage in-process via JaCoCo's runtime API (`org.jacoco.agent.rt.RT.getAgent()`), so attaching the JaCoCo Java agent is the only runtime requirement in the common cases below:
7763

7864
- Maven/Gradle dev runs where application classes are under `target/classes` or `build/classes/java/main`
7965
- packaged `java -jar` runs where the application classes live inside the executable jar
8066

8167
```bash
82-
java -javaagent:/path/to/jacocoagent.jar -jar your-app.jar
68+
java \
69+
-javaagent:/path/to/keploy-sdk.jar \
70+
-javaagent:/path/to/jacocoagent.jar \
71+
-jar your-app.jar
8372
```
8473

8574
If the in-process API is unavailable (for example because the JaCoCo agent is loaded into an isolated classloader), the SDK transparently falls back to JaCoCo's TCP server mode. To use the fallback explicitly, start JaCoCo in `tcpserver` mode and set `KEPLOY_JACOCO_HOST` / `KEPLOY_JACOCO_PORT`:
8675

8776
```bash
88-
java -javaagent:/path/to/jacocoagent.jar=address=127.0.0.1,port=36320,output=tcpserver \
77+
java \
78+
-javaagent:/path/to/keploy-sdk.jar \
79+
-javaagent:/path/to/jacocoagent.jar=address=127.0.0.1,port=36320,output=tcpserver \
8980
-jar your-app.jar
9081
```
9182

92-
### 4. Replay with Keploy Enterprise
83+
### 3. Replay with Keploy Enterprise
9384

9485
Run replay with dynamic dedup enabled:
9586

9687
```bash
97-
keploy test -c "java -javaagent:/path/to/jacocoagent.jar -jar your-app.jar" \
88+
keploy test -c "java -javaagent:/path/to/keploy-sdk.jar -javaagent:/path/to/jacocoagent.jar -jar your-app.jar" \
9889
--dedup \
9990
--language java
10091
```

keploy-sdk/pom.xml

Lines changed: 42 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -78,6 +78,48 @@
7878

7979
<build>
8080
<plugins>
81+
<plugin>
82+
<groupId>org.apache.maven.plugins</groupId>
83+
<artifactId>maven-shade-plugin</artifactId>
84+
<version>3.5.3</version>
85+
<executions>
86+
<execution>
87+
<id>shade-java-agent</id>
88+
<phase>package</phase>
89+
<goals>
90+
<goal>shade</goal>
91+
</goals>
92+
<configuration>
93+
<createDependencyReducedPom>false</createDependencyReducedPom>
94+
<transformers>
95+
<transformer implementation="org.apache.maven.plugins.shade.resource.ManifestResourceTransformer">
96+
<manifestEntries>
97+
<Premain-Class>io.keploy.dedup.KeployDedupAgent</Premain-Class>
98+
<Agent-Class>io.keploy.dedup.KeployDedupAgent</Agent-Class>
99+
<Can-Redefine-Classes>false</Can-Redefine-Classes>
100+
<Can-Retransform-Classes>false</Can-Retransform-Classes>
101+
<Implementation-Title>${project.name}</Implementation-Title>
102+
<Implementation-Version>${project.version}</Implementation-Version>
103+
<Automatic-Module-Name>io.keploy.sdk</Automatic-Module-Name>
104+
</manifestEntries>
105+
</transformer>
106+
</transformers>
107+
<filters>
108+
<filter>
109+
<artifact>*:*</artifact>
110+
<excludes>
111+
<exclude>META-INF/*.SF</exclude>
112+
<exclude>META-INF/*.DSA</exclude>
113+
<exclude>META-INF/*.RSA</exclude>
114+
<exclude>module-info.class</exclude>
115+
<exclude>META-INF/versions/*/module-info.class</exclude>
116+
</excludes>
117+
</filter>
118+
</filters>
119+
</configuration>
120+
</execution>
121+
</executions>
122+
</plugin>
81123
<plugin>
82124
<groupId>org.apache.maven.plugins</groupId>
83125
<artifactId>maven-javadoc-plugin</artifactId>

keploy-sdk/src/main/java/io/keploy/dedup/KeployDedupAgent.java

Lines changed: 39 additions & 0 deletions
Original file line numberDiff line numberDiff line change
@@ -24,6 +24,7 @@
2424
import java.io.InputStream;
2525
import java.io.InputStreamReader;
2626
import java.io.OutputStream;
27+
import java.lang.instrument.Instrumentation;
2728
import java.lang.reflect.InvocationTargetException;
2829
import java.lang.reflect.Method;
2930
import java.net.InetAddress;
@@ -65,11 +66,32 @@ public final class KeployDedupAgent {
6566
private static final int SOCKET_BACKLOG = 50;
6667

6768
private static final AtomicBoolean STARTED = new AtomicBoolean(false);
69+
private static final AtomicBoolean SHUTDOWN_HOOK_REGISTERED = new AtomicBoolean(false);
6870
private static volatile CommandServer commandServer;
6971

7072
private KeployDedupAgent() {
7173
}
7274

75+
/**
76+
* JVM entrypoint used when the SDK is attached with {@code -javaagent}.
77+
*
78+
* @param agentArgs optional Java agent arguments
79+
* @param instrumentation JVM instrumentation handle
80+
*/
81+
public static void premain(String agentArgs, Instrumentation instrumentation) {
82+
start();
83+
}
84+
85+
/**
86+
* JVM entrypoint used when the SDK is attached to an already running JVM.
87+
*
88+
* @param agentArgs optional Java agent arguments
89+
* @param instrumentation JVM instrumentation handle
90+
*/
91+
public static void agentmain(String agentArgs, Instrumentation instrumentation) {
92+
start();
93+
}
94+
7395
/**
7496
* Starts the background control socket listener used by Keploy replay.
7597
*
@@ -91,6 +113,7 @@ public static boolean start() {
91113
thread.setDaemon(true);
92114
commandServer = server;
93115
thread.start();
116+
registerShutdownHook();
94117
return true;
95118
}
96119

@@ -120,6 +143,22 @@ private static boolean isDisabled() {
120143
|| isTruthy(System.getProperty("keploy.java.dedup.disabled"));
121144
}
122145

146+
private static void registerShutdownHook() {
147+
if (!SHUTDOWN_HOOK_REGISTERED.compareAndSet(false, true)) {
148+
return;
149+
}
150+
try {
151+
Runtime.getRuntime().addShutdownHook(new Thread(new Runnable() {
152+
@Override
153+
public void run() {
154+
KeployDedupAgent.stop();
155+
}
156+
}, "keploy-java-dedup-shutdown"));
157+
} catch (IllegalStateException ignored) {
158+
// The VM is already shutting down; the process exit will reclaim resources.
159+
}
160+
}
161+
123162
private static boolean diagnosticsEnabled() {
124163
return isTruthy(System.getenv("KEPLOY_JAVA_DEDUP_DIAGNOSTICS"))
125164
|| isTruthy(System.getProperty("keploy.java.dedup.diagnostics"));

0 commit comments

Comments
 (0)