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.
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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.
