Spring Boot Hot Patch Loader: Quick Fix for Critical Production Bugs

This article explains how to build a Spring Boot hot‑patch loader that uses Java agents and dynamic class loading to apply runtime patches—such as Spring Bean, plain class, or static method replacements—allowing urgent bug fixes in production within minutes without restarting the service.

Java Companion
Java Companion
Java Companion
Spring Boot Hot Patch Loader: Quick Fix for Critical Production Bugs

Background

When a production service throws a critical exception (for example, a NullPointerException in a key API) the traditional release pipeline (code change → test → package → deploy) can take 1–2 hours, causing user loss. A hot‑patch approach can resolve the issue in a few minutes without stopping the application.

Design Principles

Dynamic class loading : Leverage Java's ClassLoader to load patch classes at runtime.

Multi‑level replacement : Support replacement of Spring beans, ordinary Java classes, static methods and (future) instance methods.

Bytecode enhancement : Use a Java Agent with the Instrumentation API to redefine classes on the fly.

Version management : Each patch carries a version number and can be rolled back.

Security control : Only files in a configured directory are accepted, and optional signature verification can be enabled.

Core Implementation

Project Structure

springboot-hot-patch/
├── src/main/java/com/example/hotpatch/
│   ├── agent/                # Java Agent
│   │   └── HotPatchAgent.java
│   ├── annotation/            # @HotPatch and PatchType
│   │   ├── HotPatch.java
│   │   └── PatchType.java
│   ├── config/                # Configuration properties
│   │   ├── HotPatchConfig.java
│   │   └── HotPatchProperties.java
│   ├── controller/            # REST API
│   │   ├── HotPatchController.java
│   │   └── TestController.java
│   ├── core/                  # Loader core
│   │   └── HotPatchLoader.java
│   ├── example/               # Sample classes to be patched
│   │   ├── UserService.java
│   │   ├── StringUtils.java
│   │   └── MathHelper.java
│   ├── instrumentation/       # Instrumentation holder
│   │   └── InstrumentationHolder.java
│   ├── model/                 # Data models
│   │   ├── PatchInfo.java
│   │   └── PatchResult.java
│   └── patches/               # Patch implementations
│       ├── UserServicePatch.java
│       ├── StringUtilsPatch.java
│       └── MathHelperDividePatch.java
├── src/main/resources/
│   └── application.properties
├── patches/                  # Directory where compiled patch JARs are placed
└── pom.xml

Maven Configuration (pom.xml)

<?xml version="1.0" encoding="UTF-8"?>
<project xmlns="http://maven.apache.org/POM/4.0.0"
         xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
         xsi:schemaLocation="http://maven.apache.org/POM/4.0.0 http://maven.apache.org/xsd/maven-4.0.0.xsd">
    <modelVersion>4.0.0</modelVersion>
    <groupId>com.example</groupId>
    <artifactId>springboot-hot-patch</artifactId>
    <version>1.0.0</version>
    <packaging>jar</packaging>
    <name>Spring Boot Hot Patch Loader</name>
    <description>A Spring Boot 3 based hot patch loader for runtime class replacement</description>
    <properties>
        <maven.compiler.source>17</maven.compiler.source>
        <maven.compiler.target>17</maven.compiler.target>
        <spring-boot.version>3.2.0</spring-boot.version>
        <asm.version>9.5</asm.version>
    </properties>
    <dependencyManagement>
        <dependencies>
            <dependency>
                <groupId>org.springframework.boot</groupId>
                <artifactId>spring-boot-dependencies</artifactId>
                <version>${spring-boot.version}</version>
                <type>pom</type>
                <scope>import</scope>
            </dependency>
        </dependencies>
    </dependencyManagement>
    <dependencies>
        <dependency>
            <groupId>org.springframework.boot</groupId>
            <artifactId>spring-boot-starter-web</artifactId>
        </dependency>
        <dependency>
            <groupId>org.ow2.asm</groupId>
            <artifactId>asm</artifactId>
            <version>${asm.version}</version>
        </dependency>
        <dependency>
            <groupId>org.projectlombok</groupId>
            <artifactId>lombok</artifactId>
            <optional>true</optional>
        </dependency>
    </dependencies>
</project>

Patch Annotation and Enum

/**
 * Patch type enum
 */
public enum PatchType {
    SPRING_BEAN,
    JAVA_CLASS,
    STATIC_METHOD,
    INSTANCE_METHOD
}

/**
 * Enhanced hot‑patch annotation
 */
@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface HotPatch {
    PatchType type() default PatchType.SPRING_BEAN;
    String originalBean() default "";               // for SPRING_BEAN
    String originalClass() default "";               // for JAVA_CLASS / STATIC_METHOD
    String methodName() default "";                 // for method patches
    String methodSignature() default "";            // overload discrimination
    String version() default "1.0";
    String description() default "";
    boolean securityCheck() default true;
}

Java Agent Support

/**
 * Instrumentation holder – obtains the JVM Instrumentation instance
 */
public class InstrumentationHolder {
    private static volatile Instrumentation instrumentation;
    public static void setInstrumentation(Instrumentation inst) { instrumentation = inst; }
    public static Instrumentation getInstrumentation() { return instrumentation; }
    public static boolean isAvailable() { return instrumentation != null; }
}

/**
 * Java Agent entry class
 */
public class HotPatchAgent {
    public static void premain(String agentArgs, Instrumentation inst) {
        System.out.println("HotPatch Agent started");
        InstrumentationHolder.setInstrumentation(inst);
    }
    public static void agentmain(String agentArgs, Instrumentation inst) {
        System.out.println("HotPatch Agent dynamically attached");
        InstrumentationHolder.setInstrumentation(inst);
    }
}

Core Loader

@Component
@Slf4j
public class HotPatchLoader {
    private final ConfigurableApplicationContext applicationContext;
    private final HotPatchProperties properties;
    private final Map<String, PatchInfo> loadedPatches = new ConcurrentHashMap<>();
    private final Instrumentation instrumentation;

    public HotPatchLoader(ConfigurableApplicationContext ctx, HotPatchProperties props) {
        this.applicationContext = ctx;
        this.properties = props;
        this.instrumentation = InstrumentationHolder.getInstrumentation();
    }

    public PatchResult loadPatch(String patchName, String version) {
        if (!properties.isEnabled()) {
            return PatchResult.failed("Hot‑patch feature is disabled");
        }
        try {
            // 1. Validate patch file
            File patchFile = validatePatchFile(patchName, version);
            // 2. Create dedicated class loader
            URLClassLoader patchClassLoader = createPatchClassLoader(patchFile);
            // 3. Load the patch class
            Class<?> patchClass = loadPatchClass(patchClassLoader, patchName);
            HotPatch patchAnnotation = patchClass.getAnnotation(HotPatch.class);
            if (patchAnnotation == null) {
                return PatchResult.failed("Patch class missing @HotPatch annotation");
            }
            // 4. Choose replacement strategy based on patch type
            PatchType patchType = patchAnnotation.type();
            switch (patchType) {
                case SPRING_BEAN:
                    replaceSpringBean(patchClass, patchAnnotation);
                    break;
                case JAVA_CLASS:
                    replaceJavaClass(patchClass, patchAnnotation);
                    break;
                case STATIC_METHOD:
                    replaceStaticMethod(patchClass, patchAnnotation);
                    break;
                case INSTANCE_METHOD:
                    return PatchResult.failed("Instance method replacement not implemented yet");
                default:
                    return PatchResult.failed("Unsupported patch type: " + patchType);
            }
            // 5. Record patch info
            PatchInfo info = new PatchInfo(patchName, version, patchClass, patchType, System.currentTimeMillis());
            loadedPatches.put(patchName, info);
            log.info("Hot patch {}:{} ({}) loaded successfully", patchName, version, patchType);
            return PatchResult.success("Patch loaded successfully");
        } catch (Exception e) {
            log.error("Hot patch loading failed: {}", e.getMessage(), e);
            return PatchResult.failed("Patch loading failed: " + e.getMessage());
        }
    }

    // Helper methods (validatePatchFile, createPatchClassLoader, loadPatchClass,
    // replaceSpringBean, replaceJavaClass, replaceStaticMethod) are omitted for brevity.
}

REST API Controller

@RestController
@RequestMapping("/api/hotpatch")
@Slf4j
public class HotPatchController {
    private final HotPatchLoader patchLoader;
    public HotPatchController(HotPatchLoader loader) { this.patchLoader = loader; }

    @PostMapping("/load")
    public ResponseEntity<PatchResult> loadPatch(@RequestParam String patchName,
                                                @RequestParam String version) {
        log.info("Loading hot patch: {}:{}", patchName, version);
        PatchResult result = patchLoader.loadPatch(patchName, version);
        return ResponseEntity.ok(result);
    }

    @GetMapping("/list")
    public ResponseEntity<List<PatchInfo>> listPatches() {
        return ResponseEntity.ok(patchLoader.getLoadedPatches());
    }

    @PostMapping("/rollback")
    public ResponseEntity<PatchResult> rollbackPatch(@RequestParam String patchName) {
        log.info("Rolling back patch: {}", patchName);
        PatchResult result = patchLoader.rollbackPatch(patchName);
        return ResponseEntity.ok(result);
    }

    @GetMapping("/status")
    public ResponseEntity<String> getStatus() {
        return ResponseEntity.ok("Hot Patch Loader is running");
    }
}

Example Patch Implementations

Spring Bean replacement – fixes a NullPointerException in UserService:

@HotPatch(
    type = PatchType.SPRING_BEAN,
    originalBean = "userService",
    version = "1.0.1",
    description = "Fix NPE in getUserInfo"
)
@Service
public class UserServicePatch {
    public String getUserInfo(Long userId) {
        if (userId == null) {
            return "未知用户"; // default instead of null
        }
        if (userId == 1L) return "Alice";
        if (userId == 2L) return "Bob";
        return "未知用户";
    }
    public int getUserNameLength(Long userId) {
        String name = getUserInfo(userId);
        return name != null ? name.length() : 0;
    }
}

Plain Java class replacement – improves StringUtils.isEmpty to trim whitespace:

@HotPatch(
    type = PatchType.JAVA_CLASS,
    originalClass = "com.example.hotpatch.example.StringUtils",
    version = "1.0.2",
    description = "Consider whitespace in isEmpty"
)
public class StringUtilsPatch {
    public static boolean isEmpty(String str) {
        return str == null || str.trim().length() == 0;
    }
    public static String trim(String str) {
        return str == null ? null : str.trim();
    }
}

Build & Deploy

Compile the patch against the original project's classpath and package it as a JAR:

# Compile patch class
javac -cp "target/classes:target/lib/*" src/main/java/patches/UserServicePatch.java
# Package into JAR (including annotation metadata)
jar cf UserService-1.0.1.jar -C target/classes patches/UserServicePatch.class
# Copy the JAR to the configured patches directory
cp *.jar ./patches/

Start the Spring Boot application with the Java Agent:

java -javaagent:target/springboot-hot-patch-1.0.0-agent.jar \
    -Dhotpatch.enabled=true \
    -Dhotpatch.path=./patches \
    -jar target/springboot-hot-patch-1.0.0.jar

Runtime Usage

Load a patch via the provided REST API (curl examples):

# Load Spring Bean patch
curl -X POST "http://localhost:8080/api/hotpatch/load" -d "patchName=UserService&version=1.0.1"
# Load Java class patch
curl -X POST "http://localhost:8080/api/hotpatch/load" -d "patchName=StringUtils&version=1.0.2"

Best Practices & Cautions

Naming convention : Patch class name = OriginalClassName + Patch.

Semantic versioning : Use MAJOR.MINOR.PATCH to track changes.

Thorough testing : Every patch must pass unit and integration tests before being uploaded.

Minimal changes : Only fix the defect; do not add new features.

Access control : Restrict who can upload or trigger patches in production.

Applicable Scenarios

Urgent bug fixes in production where downtime is unacceptable.

Temporary performance optimisations while a proper release is prepared.

Feature toggles that need to be switched on/off instantly.

Parameter tuning of algorithms without redeploying the whole service.

Important Notes

Hot‑patching is an emergency tool; it does not replace a regular release workflow.

Always monitor the system after applying a patch to ensure stability.

Maintain audit logs and alerting (example shown in the source) for every patch operation.

https://github.com/yuboon/java-examples/tree/master/springboot-hot-patch
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.

Dynamic LoadingJava Agentspring-boothot-patchspring-beanruntime-replacement
Java Companion
Written by

Java Companion

A highly professional Java public account

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.