Dynamic Spring Boot Controllers Without Restart: Register Endpoints at Runtime

This article explains why static controller definitions limit flexibility, then dives into Spring MVC’s RequestMappingHandlerMapping internals, showing how its public registerMapping method can be used to add or remove controller endpoints at runtime, with two concrete implementations—bean‑method registration and full ByteBuddy‑generated controllers—plus best‑practice notes.

Senior Xiao Ying
Senior Xiao Ying
Senior Xiao Ying
Dynamic Spring Boot Controllers Without Restart: Register Endpoints at Runtime

Why Dynamically Create Controllers?

All interfaces declared with @RestController + @RequestMapping are fixed at startup and cannot be changed at runtime.

// Traditional way: interface is determined at compile time
@RestController
@RequestMapping("/api/user")
public class UserController {
    @GetMapping("/{id}")
    public User getById(@PathVariable Long id) { ... }
}

Core Principle: Deep Dive into RequestMappingHandlerMapping

HandlerMapping

In Spring MVC, HandlerMapping establishes the mapping relationship between request URLs and handling methods. DispatcherServlet iterates over all HandlerMapping instances to find a suitable handler.

RequestMappingHandlerMapping – Two Phases

Phase One: Registration at Startup (Traditional)

public class RequestMappingHandlerMapping extends AbstractHandlerMethodMapping {
    @Override
    protected void initHandlerMethods() {
        // 1. Get all bean names from the container
        String[] beanNames = obtainApplicationContext().getBeanNamesForType(Object.class);
        for (String beanName : beanNames) {
            // 2. Determine if it is a handler (has @Controller or @RequestMapping)
            if (isHandler(beanType)) {
                // 3. Detect @RequestMapping on methods
                detectHandlerMethods(beanName);
            }
        }
    }
    protected boolean isHandler(Class<?> beanType) {
        return AnnotatedElementUtils.hasAnnotation(beanType, Controller.class) ||
               AnnotatedElementUtils.hasAnnotation(beanType, RequestMapping.class);
    }
}

Phase Two: Runtime MappingRegistry

class MappingRegistry {
    // Mapping info → handler method cache
    private final Map<T, HandlerMethod> mappingLookup = new LinkedHashMap<>();
    // URL path → mapping info cache
    private final MultiValueMap<String, T> urlLookup = new LinkedMultiValueMap<>();
    public void register(T mapping, Object handler, Method method) {
        this.mappingLookup.put(mapping, new HandlerMethod(handler, method));
        this.urlLookup.add(path, mapping);
    }
}

Public Registration Method

public class RequestMappingHandlerMapping {
    // Public registration method
    public void registerMapping(RequestMappingInfo mapping, Object handler, Method method) {
        this.mappingRegistry.register(mapping, handler, method);
    }
    // Public unregistration method
    public void unregisterMapping(RequestMappingInfo mapping) {
        this.mappingRegistry.unregister(mapping);
    }
}

Solution One: Register Existing Bean Methods (Most Common)

Step 1 – Define a Handler Component

package com.example.dynamic.demo.handler;

import org.springframework.stereotype.Component;
import org.springframework.web.bind.annotation.PathVariable;
import org.springframework.web.bind.annotation.RequestParam;
import org.springframework.web.bind.annotation.ResponseBody;

import java.util.HashMap;
import java.util.Map;

/** Dynamic API handler – a regular Spring bean without @Controller */
@Component
public class DynamicApiHandler {

    @ResponseBody
    public Map<String, Object> getUserInfo(@PathVariable Long id) {
        Map<String, Object> result = new HashMap<>();
        result.put("id", id);
        result.put("name", "动态用户_" + id);
        result.put("time", System.currentTimeMillis());
        return result;
    }

    @ResponseBody
    public String saveData(@RequestParam String title, @RequestParam String content) {
        return String.format("已保存: title=%s, content=%s", title, content);
    }

    @ResponseBody
    public String healthCheck() {
        return "OK - " + System.currentTimeMillis();
    }
}

Step 2 – Dynamic Registration Configuration

package com.example.dynamic.demo.config;

import com.example.dynamic.demo.handler.DynamicApiHandler;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.context.annotation.Configuration;
import org.springframework.web.bind.annotation.RequestMethod;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import javax.annotation.PostConstruct;
import java.lang.reflect.Method;

@Configuration
public class DynamicMappingConfig {

    @Autowired
    private RequestMappingHandlerMapping requestMappingHandlerMapping;

    @Autowired
    private DynamicApiHandler dynamicApiHandler;

    @PostConstruct
    public void registerDynamicMappings() throws NoSuchMethodException {
        registerGetUserApi();
        registerPostSaveApi();
        registerHealthApi();
        System.out.println("========== 动态接口注册完成 ==========");
    }

    private void registerGetUserApi() throws NoSuchMethodException {
        RequestMappingInfo mappingInfo = RequestMappingInfo
                .paths("/api/dynamic/user/{id}")
                .methods(RequestMethod.GET)
                .consumes("application/json")
                .produces("application/json")
                .build();
        Method method = DynamicApiHandler.class.getMethod("getUserInfo", Long.class);
        requestMappingHandlerMapping.registerMapping(mappingInfo, dynamicApiHandler, method);
        System.out.println("已注册接口: GET /api/dynamic/user/{id}");
    }

    private void registerPostSaveApi() throws NoSuchMethodException {
        RequestMappingInfo mappingInfo = RequestMappingInfo
                .paths("/api/dynamic/save")
                .methods(RequestMethod.POST)
                .build();
        Method method = DynamicApiHandler.class.getMethod("saveData", String.class, String.class);
        requestMappingHandlerMapping.registerMapping(mappingInfo, dynamicApiHandler, method);
        System.out.println("已注册接口: POST /api/dynamic/save");
    }

    private void registerHealthApi() throws NoSuchMethodException {
        RequestMappingInfo mappingInfo = RequestMappingInfo
                .paths("/api/dynamic/health")
                .methods(RequestMethod.GET)
                .build();
        Method method = DynamicApiHandler.class.getMethod("healthCheck");
        requestMappingHandlerMapping.registerMapping(mappingInfo, dynamicApiHandler, method);
        System.out.println("已注册接口: GET /api/dynamic/health");
    }
}

RequestMappingInfo – Rich Conditions

RequestMappingInfo info = RequestMappingInfo
        .paths("/api/test")
        .methods(RequestMethod.POST)
        .params("version=1.0")
        .headers("X-Token")
        .consumes("application/json")
        .produces("application/xml")
        .customCondition(new MyCondition())
        .build();

Solution Two: Fully Dynamic Controller Generation (Advanced)

4.1 Generate Classes with ByteBuddy

package com.example.dynamic.demo.generator;

import net.bytebuddy.ByteBuddy;
import net.bytebuddy.description.annotation.AnnotationDescription;
import net.bytebuddy.dynamic.DynamicType;
import net.bytebuddy.implementation.MethodDelegation;
import net.bytebuddy.implementation.bind.annotation.RuntimeType;
import net.bytebuddy.implementation.bind.annotation.This;
import org.springframework.web.bind.annotation.*;

import java.lang.reflect.Modifier;

/** Dynamic Controller generator – uses ByteBuddy to create a new Controller class at runtime */
public class DynamicControllerGenerator {

    /**
     * Dynamically generate a Controller class
     * @param className the name of the class to generate
     * @param urlMapping the endpoint path
     * @return the generated class's Class object
     */
    public static Class<?> generateController(String className, String urlMapping) {
        AnnotationDescription restControllerAnno = AnnotationDescription.Builder
                .ofType(RestController.class)
                .build();
        AnnotationDescription requestMappingAnno = AnnotationDescription.Builder
                .ofType(RequestMapping.class)
                .define("value", urlMapping)
                .build();
        try (DynamicType.Unloaded<?> dynamicType = new ByteBuddy()
                .subclass(Object.class)
                .name("com.example.dynamic.generated." + className)
                .annotateType(restControllerAnno)
                .annotateType(requestMappingAnno)
                .defineMethod("hello", String.class, Modifier.PUBLIC)
                .annotateMethod(AnnotationDescription.Builder
                        .ofType(GetMapping.class)
                        .define("value", "/hello")
                        .build())
                .intercept(MethodDelegation.to(new HelloInterceptor()))
                .make()) {
            return dynamicType.load(DynamicControllerGenerator.class.getClassLoader())
                    .getLoaded();
        } catch (Exception e) {
            throw new RuntimeException("生成 Controller 失败", e);
        }
    }

    /** Method interceptor – handles the actual business logic */
    public static class HelloInterceptor {
        @RuntimeType
        public String intercept(@This Object target) {
            return "Hello from dynamically generated controller!";
        }
    }
}

4.2 Register the Dynamically Generated Class into Spring

package com.example.dynamic.demo.service;

import org.springframework.beans.factory.support.BeanDefinitionBuilder;
import org.springframework.beans.factory.support.DefaultListableBeanFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.stereotype.Service;

@Service
public class DynamicBeanRegisterService implements ApplicationContextAware {

    private DefaultListableBeanFactory beanFactory;

    @Override
    public void setApplicationContext(ApplicationContext applicationContext) {
        this.beanFactory = (DefaultListableBeanFactory) applicationContext.getAutowireCapableBeanFactory();
    }

    /** Dynamically register a Bean into the Spring container */
    public void registerController(String beanName, Class<?> controllerClass) {
        BeanDefinitionBuilder builder = BeanDefinitionBuilder.genericBeanDefinition(controllerClass);
        beanFactory.registerBeanDefinition(beanName, builder.getBeanDefinition());
        System.out.println("已注册 Bean: " + beanName);
    }
}

Best Practices and Caveats

Thread‑Safety

The RequestMappingHandlerMapping.registerMapping method is thread‑safe because it internally uses a ConcurrentHashMap. It can be safely called in a multi‑threaded environment.

Parameter Type Matching

The Method object supplied during registration must have parameter types that exactly match the types that will be supplied at runtime; otherwise an error is thrown.

Differences Across Spring Versions

Spring Boot 2.x: fully supports the approaches described above.

Spring Boot 1.x: method names may differ slightly, but the underlying principle remains the same.

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.

backendJavaSpring BootByteBuddyDynamic ControllerRequestMappingHandlerMapping
Senior Xiao Ying
Written by

Senior Xiao Ying

Dedicated to sharing Java backend technical experience and original tutorials, offering career transition advice and resume editing. Recognized as a rising star in CSDN's Java backend community and ranked Top 3 in the 2022 New Star Program for Java backend.

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.