Instantly Debug SpringBoot Apps in Production Without Restart

This article introduces a SpringBoot online debugging injector that lets backend developers add, adjust, and remove fine‑grained debug rules at runtime, eliminating midnight alarm calls, costly redeployments, and performance bottlenecks while keeping production services safe and uninterrupted.

Code Ape Tech Column
Code Ape Tech Column
Code Ape Tech Column
Instantly Debug SpringBoot Apps in Production Without Restart

Why Do We Need It?

As a backend developer you’ve probably experienced these frustrating moments:

Midnight alarm: "All order APIs failed!" with a NullPointerException at OrderService.createOrder:124 and no clue which parameter is null.

Local works but production intermittently errors; remote JDWP debugging is blocked by security, and adding logs requires a full release cycle.

Performance mystery: a response jumps from 100 ms to 5 s, database looks fine, yet the root cause remains hidden.

Traditional solutions (AOP logs, full‑log output, remote JDWP, temporary log releases, third‑party APM) each have fatal drawbacks such as planning overhead, high performance cost, security risk, long latency, or heavy migration effort.

What Do We Need?

Instant activation : enable debugging without restarting services.

Precise control : target specific methods to avoid unnecessary overhead.

Safety : no exposed ports, no impact on business logic.

Zero residue : rules disappear after the issue is solved.

Simple operation : web UI, no SSH required.

The goal is a SpringBoot zero‑trace debugging injector that truly solves these pain points.

System Design

Technology stack :

Bytecode manipulation: ByteBuddy (friendlier than ASM).

Class redefinition: Instrumentation.retransformClasses.

Frontend: Tailwind CSS + Alpine.js.

Core Design Principles

Minimize intrusion – no business code changes, only bytecode enhancement.

Progressive debugging levels – from exact method to global switch.

Security‑first architecture – safe parameter handling and exception isolation.

Design Decisions

Use a Java Agent to intercept class loading without touching source code.

Leverage retransformClasses for dynamic rule activation without restart.

Debugging Levels

Level 1: Precise method → debug a single method
Level 2: Class level → debug all methods of a class
Level 3: Package level → debug all classes in a package
Level 4: Pattern match → regex batch matching
Level 5: Global switch → full‑scale debugging

Safety Considerations

// Safe conversion of parameters and return values
public static String safeToString(Object obj) {
    if (obj == null) return "null";
    try {
        String str = obj.toString();
        return str.length() > 200 ? str.substring(0, 200) + "..." : str;
    } catch (Exception e) {
        return obj.getClass().getSimpleName() + "@" + Integer.toHexString(obj.hashCode());
    }
}
// Isolate debugging exceptions from business logic
try {
    System.out.println(debugInfo);
} catch (Exception e) {
    // silently handle, log to a dedicated debug error log
}

Core Implementation Details

Java Agent Entry

@Component
public class OnlineDebugAgent {
    private static Instrumentation instrumentation;
    private static final Logger logger = LoggerFactory.getLogger(OnlineDebugAgent.class);

    public static void premain(String args, Instrumentation inst) {
        logger.info("Online Debug Agent starting...");
        instrumentation = inst;
        installAgent(inst);
    }

    private static void installAgent(Instrumentation inst) {
        new AgentBuilder.Default()
            .type(ElementMatchers.any())
            .transform((builder, typeDescription, classLoader, module, protectionDomain) -> {
                String className = typeDescription.getName();
                if (DebugConfigManager.shouldDebugClass(className)) {
                    return builder.method(ElementMatchers.any())
                                  .intercept(Advice.to(UniversalDebugAdvice.class));
                }
                return builder;
            })
            .installOn(inst);
    }
}

Universal Debug Advice

public class UniversalDebugAdvice {
    @Advice.OnMethodEnter
    public static long onEnter(@Advice.Origin Method method, @Advice.AllArguments Object[] args) {
        String fullMethodName = method.getDeclaringClass().getName() + "." + method.getName();
        if (DebugConfigManager.shouldDebug(fullMethodName)) {
            long start = System.currentTimeMillis();
            String argsStr = Arrays.stream(args)
                .map(UniversalDebugAdvice::safeToString)
                .collect(Collectors.joining(", "));
            System.out.println(String.format("[DEBUG-INJECT] %s - start | args: [%s]", fullMethodName, argsStr));
            return start;
        }
        return 0;
    }

    @Advice.OnMethodExit(onThrowable = Throwable.class)
    public static void onExit(@Advice.Origin Method method,
                              @Advice.Return Object returnValue,
                              @Advice.Thrown Throwable throwable,
                              @Advice.Enter long startTime) {
        if (startTime > 0) {
            String fullMethodName = method.getDeclaringClass().getName() + "." + method.getName();
            long duration = System.currentTimeMillis() - startTime;
            if (throwable != null) {
                System.out.println(String.format("[DEBUG-INJECT] %s - exception | %dms | %s", fullMethodName, duration, throwable.getMessage()));
            } else {
                System.out.println(String.format("[DEBUG-INJECT] %s - finished | %dms | return: %s", fullMethodName, duration, safeToString(returnValue)));
            }
        }
    }

    public static String safeToString(Object obj) { /* same as above */ }
}

Configuration Manager

public class DebugConfigManager {
    private static final Set<String> exactMethods = new CopyOnWriteArraySet<>();
    private static final Set<Pattern> patternMethods = new CopyOnWriteArraySet<>();
    private static final Set<String> debugClasses = new CopyOnWriteArraySet<>();
    private static final Set<String> debugPackages = new CopyOnWriteArraySet<>();
    private static volatile boolean globalDebugEnabled = false;

    public static boolean shouldDebug(String fullMethodName) {
        if (globalDebugEnabled) return true;
        if (exactMethods.contains(fullMethodName)) return true;
        int lastDot = fullMethodName.lastIndexOf('.');
        if (lastDot > 0) {
            String className = fullMethodName.substring(0, lastDot);
            if (debugClasses.contains(className)) return true;
            for (String pkg : debugPackages) {
                if (className.startsWith(pkg)) return true;
            }
        }
        for (Pattern p : patternMethods) {
            if (p.matcher(fullMethodName).matches()) return true;
        }
        return false;
    }
    // other rule management methods omitted for brevity
}

REST API Controller

@RestController
@RequestMapping("/api/debug")
@CrossOrigin(origins = "*")
public class OnlineDebugController {
    @Autowired
    private OnlineDebugService onlineDebugService;

    @PostMapping("/method")
    public ResponseEntity<Map<String, Object>> addMethodDebug(@RequestBody DebugRuleRequest request) {
        try {
            DebugConfigManager.addMethodDebug(request.getTarget());
            String className = extractClassName(request.getTarget());
            if (className != null) {
                DynamicRetransformManager.retransformClass(className);
            }
            return ResponseEntity.ok(Map.of("success", true, "message", "Method debug rule added: " + request.getTarget()));
        } catch (Exception e) {
            return ResponseEntity.ok(Map.of("success", false, "message", e.getMessage()));
        }
    }
    // delete, status, etc. omitted for brevity
}

Dynamic Retransform Manager

public class DynamicRetransformManager {
    private static final Logger logger = LoggerFactory.getLogger(DynamicRetransformManager.class);

    public static void retransformClass(String className) {
        try {
            Instrumentation instrumentation = OnlineDebugAgent.getInstrumentation();
            if (instrumentation == null) throw new RuntimeException("Instrumentation not available");
            Class<?> target = Class.forName(className);
            if (instrumentation.isModifiableClass(target)) {
                instrumentation.retransformClasses(target);
                logger.info("Successfully retransformed class: {}", className);
            }
        } catch (Exception e) {
            logger.error("Failed to retransform class: {}", className, e);
            throw new RuntimeException("Retransform failed: " + e.getMessage());
        }
    }
}

Frontend Console

Key UI built with Tailwind CSS and Alpine.js. The page lets users add method rules, view the rule list, and delete rules via REST calls.

<!DOCTYPE html>
<html lang="en">
<head>
  <meta charset="UTF-8">
  <title>SpringBoot Online Debug Tool</title>
  <script src="https://cdn.tailwindcss.com"></script>
  <script src="https://unpkg.com/[email protected]/dist/cdn.min.js" defer></script>
</head>
<body class="bg-gray-100" x-data="debugApp()" x-init="init()">
  <!-- Tab navigation and rule list omitted for brevity -->
</body>
</html>
function debugApp() {
  return {
    activeTab: 'method',
    debugRules: [],
    methodInput: '',
    async init() { await this.refreshStatus(); await this.refreshRules(); },
    async addMethodDebug() { /* POST /api/debug/method */ },
    async removeRule(rule) { /* DELETE /api/debug/method */ }
  }
}

Project Configuration

Maven dependencies include Spring Boot Web, ByteBuddy, and ByteBuddy‑Agent. The JAR manifest declares Premain-Class and enables class retransformation.

<dependencies>
  <dependency>
    <groupId>org.springframework.boot</groupId>
    <artifactId>spring-boot-starter-web</artifactId>
    <version>3.2.0</version>
  </dependency>
  <dependency>
    <groupId>net.bytebuddy</groupId>
    <artifactId>byte-buddy</artifactId>
    <version>1.14.9</version>
  </dependency>
  <dependency>
    <groupId>net.bytebuddy</groupId>
    <artifactId>byte-buddy-agent</artifactId>
    <version>1.14.9</version>
  </dependency>
</dependencies>

<build>
  <plugins>
    <plugin>
      <groupId>org.apache.maven.plugins</groupId>
      <artifactId>maven-jar-plugin</artifactId>
      <configuration>
        <archive>
          <manifestEntries>
            <Premain-Class>com.example.onlinedebug.agent.OnlineDebugAgent</Premain-Class>
            <Agent-Class>com.example.onlinedebug.agent.OnlineDebugAgent</Agent-Class>
            <Can-Retransform-Classes>true</Can-Retransform-Classes>
            <Can-Redefine-Classes>true</Can-Redefine-Classes>
          </manifestEntries>
        </archive>
      </configuration>
    </plugin>
  </plugins>
</build>

Run the application with the agent:

java -javaagent:target/springboot-online-debug-1.0-SNAPSHOT.jar \
     -jar target/springboot-online-debug-1.0-SNAPSHOT.jar

Demo Usage

Add a debug rule via REST:

curl -X POST http://localhost:8080/api/debug/method \
     -H "Content-Type: application/json" \
     -d '{"target": "com.example.onlinedebug.demo.DemoService.getUserById"}'

Real‑time log output:

[DEBUG-INJECT] com.example.onlinedebug.demo.DemoService.getUserById() called with args: Long@123
[DEBUG-INJECT] com.example.onlinedebug.demo.DemoService.getUserById() completed in 57ms returning: User@User{id=123, name='User123', email='[email protected]'}

Production Considerations

Zero downtime – dynamic injection does not pause business services.

Precise control – method‑level matching avoids unnecessary overhead.

Immediate removal – rules can be cleared as soon as the issue is solved.

Safety – debugging logic is isolated so exceptions never affect the original flow.

Performance impact – some overhead exists while debugging is active.

Access control – production environments must enforce authentication for the debug API.

Rule management – avoid excessive rules that could degrade matching efficiency.

Conclusion

The SpringBoot zero‑trace debugging injector transforms the traditional "guess‑modify‑release‑verify" cycle into an "inject‑observe‑locate" workflow, offering faster issue resolution, lower risk, and a user‑friendly web interface for on‑the‑fly troubleshooting.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

InstrumentationspringbootProduction DebuggingByteBuddyJava AgentOnline Debugging
Code Ape Tech Column
Written by

Code Ape Tech Column

Former Ant Group P8 engineer, pure technologist, sharing full‑stack Java, job interview and career advice through a column. Site: java-family.cn

0 followers
Reader feedback

How this landed with the community

Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.