文章目录
问题起源于同事的一个提问:怎么自定义一个Controller?
乍一听这个问题似乎不太正常,我们使用@Controller或者@RestController(当然@ControllerAdvice或者@RestControllerAdvice也是一样的)不就能正常定义一个Controller吗。细问之下,果然别有用意:
如何在不使用@Controller或@RestController的情况下注册一个Controller?
这个需求似乎是希望能以动态的方式引入Controller(原话是说在类上加上个@Controller注解太丑了😂😂😂),总之,大致要满足这两个条件:
- 需要注册一个 Bean,并且是条件注册,不能在包扫描期间直接完成注册
- 这个 Bean 必须是一个 Controller,里面标注的 @RequestMapping 要能被 DispatcherServlet 感知到,作为 Controller 要有正常的功能;硬性条件是类上不加 @Controller 类似的注解(当然也包括@Component、@Service、@Repository等等,耍小聪明容易翻车哦😅),所以如果想使用 @ConditionalOnXxx 注解的各位可以换个思路了。不过如果希望类上或者类里不用任何注解,那么这边建议亲考虑回归初心自定义Servlet😅😅😅,这里就不提了
综上,目前想到的比较简单可行的方案和思考过程如下,如果只是要个方案的可以只看第一个大标题了
以下方案在 springboot v2.3.12.RELEASE 与 v2.7.16 版本验证有效,文中源码以及内容如不单独说明则以 v2.7.16 版本为例
一、解决方案
方案一、@RequestMapping + @Bean
这里的 @RequestMapping 特指 Controller 类上标注,而非具体方法上的标注
当然,@Bean 也并非特指配置类中的注册方法,也可以采用诸如 @Import 或者 BeanDefinitionRegistry 等方法,总之要保证注册成为一个 Bean
配置类示例如下(要放在扫描路径下):
@Configuration
public class WebControllerConfig {
@Bean
public CustomizedController customizedController() {
return new CustomizedController();
}
}
自定义类 CustomizedController 如下:
@RequestMapping
@ResponseBody
public class CustomizedController {
@GetMapping("customized")
public String custom() {
return "This is a method from customized controller.";
}
}
CustomizedController 类上的 @ResponseBody 注解可以根据需要写,但是类上的 @RequestMapping 是必须的(这个是代替 @Controller 注解的关键)
方案二、配置类中通过反射调用 RequestMappingHandlerMapping 的方法探测 Controller
参考 springboot手动动态注入controller和service,采用反射的方式调用 RequestMappingHandlerMapping 父类 AbstractHandlerMethodMapping 的方法 detectHandlerMethods
,这是一个 protected 类型的方法,所以需要反射调用,注册器 ReflectRequestMappingControllerRegistry
及公共接口 ControllerRegistrar
例程如下:
package gemini.utils.mvc;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.util.ClassUtils;
import org.springframework.util.ReflectionUtils;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Set;
import java.util.concurrent.locks.ReentrantLock;
public class ReflectRequestMappingControllerRegistry implements ControllerRegistry, ApplicationContextAware {
private static final Logger logger = LoggerFactory.getLogger(ReflectRequestMappingControllerRegistry.class);
private ApplicationContext ctx;
private RequestMappingHandlerMapping mapping;
private static final HashMap<Class<?>, Set<Object>> REGISTERED_HANDLERS = new HashMap<>();
private static final ReentrantLock LOCK = new ReentrantLock();
@Override
public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
try {
this.ctx = applicationContext;
this.mapping = applicationContext.getBean(RequestMappingHandlerMapping.class);
} catch (Exception e) {
this.mapping = null;
}
}
@Override
public ApplicationContext getApplicationContext() {
return ctx;
}
/**
* 注册 controller
*
* @param controller controller object, bean name or class
* @return true if register success
*/
public boolean registerController(Object controller) {
if (controller == null || this.ctx == null || this.mapping == null) {
return false;
}
LOCK.lock();
try {
controller = parseControllerInstance(controller);
if (controller == null) {
logger.warn("Controller can not be instantiated.");
return false;
}
Set<Object> handlers = REGISTERED_HANDLERS.getOrDefault(controller.getClass(), new HashSet<>());
if (handlers.contains(controller)) {
logger.warn("Controller already registered: " + controller.getClass().getName());
return false;
}
// 注册 controller method
try {
Method method = this.mapping.getClass().getSuperclass().getSuperclass()
.getDeclaredMethod("detectHandlerMethods", Object.class);
method.setAccessible(true);
method.invoke(this.mapping, controller);
return true;
} catch (Exception e) {
// failed to register controller
logger.warn("Failed to register controller", e);
return false;
} finally {
handlers.add(controller);
REGISTERED_HANDLERS.putIfAbsent(controller.getClass(), handlers);
}
} finally {
LOCK.unlock();
}
}
/**
* 注销 controller
*
* @param controller controller object, bean name or class
* @return true if success
*/
public boolean unregisterController(Object controller) {
if (controller == null || this.ctx == null || this.mapping == null) {
return false;
}
LOCK.lock();
try {
Class<?> type = parseControllerType(controller);
boolean isBean = controller instanceof String;
if (type == null) {
if (isBean) {
logger.warn("Controller bean is not exists: " + controller);
return false;
}
logger.warn("Controller type is unknown");
return false;
}
if (!isBean && !REGISTERED_HANDLERS.getOrDefault(type, new HashSet<>()).remove(controller)) {
logger.warn("Controller is not registered: " + type.getName());
return false;
}
final Class<?> targetClass = type;
List<Method> failedMethods = new ArrayList<>();
ReflectionUtils.doWithMethods(targetClass, new ReflectionUtils.MethodCallback() {
public void doWith(Method method) {
Method specificMethod = ClassUtils.getMostSpecificMethod(method, targetClass);
try {
Method createMappingMethod = RequestMappingHandlerMapping.class.
getDeclaredMethod("getMappingForMethod", Method.class, Class.class);
createMappingMethod.setAccessible(true);
RequestMappingInfo requestMappingInfo =(RequestMappingInfo)
createMappingMethod.invoke(mapping, specificMethod, targetClass);
if (requestMappingInfo != null) {
mapping.unregisterMapping(requestMappingInfo);
}
} catch (Exception e) {
// failed to unregister controller method
failedMethods.add(specificMethod);
logger.warn("Failed to unregister controller method: " + specificMethod, e);
}
}
}, ReflectionUtils.USER_DECLARED_METHODS);
if (!failedMethods.isEmpty()) {
logger.warn("Failed to unregister controller method: " + failedMethods);
}
return true;
} finally {
LOCK.unlock();
}
}
}
package gemini.utils.mvc;
import org.springframework.beans.BeansException;
import org.springframework.context.ApplicationContext;
import org.springframework.util.ReflectionUtils;
public interface ControllerRegistry {
boolean registerController(Object controller);
boolean unregisterController(Object controller);
ApplicationContext getApplicationContext();
default Object parseControllerInstance(Object controller) {
Class<?> type;
ApplicationContext ctx = getApplicationContext();
if (controller instanceof String) {
// 获取 bean
try {
if (ctx == null) {
return null;
}
type = ctx.getType((String) controller);
if (type != null) {
controller = ctx.getBean(type);
}
if (type == null || controller == null) {
return null;
}
return controller;
} catch (BeansException e) {
return null;
}
} else if (controller instanceof Class) {
// class 实例化
type = (Class<?>) controller;
try {
return ReflectionUtils.accessibleConstructor(type).newInstance();
} catch (Exception e) {
return null;
}
}
return controller;
}
default Class<?> parseControllerType(Object controller) {
ApplicationContext ctx = getApplicationContext();
if (controller instanceof String) {
// 获取 bean
try {
return ctx.getType((String) controller);
} catch (BeansException e) {
return null;
}
} else if (controller instanceof Class) {
return (Class<?>) controller;
}
return controller.getClass();
}
}
将 ControllerRegistrar
的 bean 注册到容器中(例程中使用 @Component 注册,可换成其它方式),在需要动态注册 Controller 的地方注入该注册器 bean,调用对应的 registerController
和 unregisterController
即可实现注册和注销
这个方法优势在于能够动态注册/注销,不过过程中使用了反射,可以根据场景考虑是否适用
注意:不能连续多次调用 registerController
或注册已有的 Controller
方案三、继承 WebMvcConfigurationSupport 接管 MVC 配置(方案二强化版)
package gemini.utils.mvc;
import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.context.ApplicationContext;
import org.springframework.context.ApplicationContextAware;
import org.springframework.core.MethodIntrospector;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;
import java.lang.reflect.Method;
import java.util.ArrayList;
import java.util.HashMap;
import java.util.HashSet;
import java.util.List;
import java.util.Map;
import java.util.Set;
import java.util.concurrent.atomic.AtomicBoolean;
import java.util.concurrent.locks.ReentrantLock;
public class RequestMappingControllerRegistry extends RequestMappingHandlerMapping
implements ControllerRegistry, ApplicationContextAware {
private static final Logger logger = LoggerFactory.getLogger(RequestMappingControllerRegistry.class);
private static final HashMap<Class<?>, Set<Object>> REGISTERED_HANDLERS = new HashMap<>();
private static final ReentrantLock LOCK = new ReentrantLock();
private static final AtomicBoolean INITIALIZED = new AtomicBoolean(false);
public RequestMappingControllerRegistry() {
}
public RequestMappingControllerRegistry(ApplicationContext ctx) {
setApplicationContext(ctx);
}
@Override
public void afterPropertiesSet() {
if (INITIALIZED.compareAndSet(false, true)) {
logger.info("{} init!", getClass().getName());
super.afterPropertiesSet();
}
}
/**
* 注册 controller
*
* @param controller controller object, bean name or class
* @return true if register success
*/
@Override
public boolean registerController(Object controller) {
if (controller == null || getApplicationContext() == null) {
return false;
}
LOCK.lock();
try {
controller = parseControllerInstance(controller);
if (controller == null) {
logger.warn("Controller can not be instantiated");
return false;
}
Class<?> type = controller.getClass();
Set<Object> handlers = REGISTERED_HANDLERS.getOrDefault(type, new HashSet<>());
if (handlers.contains(controller)) {
logger.warn("Controller has been registered");
return false;
}
try {
detectHandlerMethods(controller);
} catch (Exception e) {
// failed to register controller
logger.warn("Failed to register controller", e);
return false;
} finally {
handlers.add(controller);
REGISTERED_HANDLERS.putIfAbsent(type, handlers);
}
} finally {
LOCK.unlock();
}
return false;
}
/**
* 注销 controller
*
* @param controller controller object, bean name or class
* @return true if success
*/
@Override
public boolean unregisterController(Object controller) {
if (controller == null || getApplicationContext() == null) {
return false;
}
LOCK.lock();
try {
Class<?> type = parseControllerType(controller);
boolean isBean = controller instanceof String;
if (type == null) {
if (isBean) {
logger.warn("Controller bean is not exists: " + controller);
} else {
logger.warn("Controller type is unknown");
}
return false;
}
if (!isBean && !REGISTERED_HANDLERS.getOrDefault(type, new HashSet<>()).remove(controller)) {
logger.warn("Controller is not registered: " + type.getName());
return false;
}
List<Method> failedMethods = new ArrayList<>();
Map<Method, RequestMappingInfo> mappingInfoMap = MethodIntrospector.selectMethods(type,
(MethodIntrospector.MetadataLookup<RequestMappingInfo>) method -> {
try {
return getMappingForMethod(method, type);
} catch (Throwable ex) {
failedMethods.add(method);
logger.warn("Invalid mapping on handler method [" + method + "]: " + ex.getMessage());
return null;
}
});
mappingInfoMap.forEach((method, requestMappingInfo) -> {
try {
if (requestMappingInfo != null) {
unregisterMapping(requestMappingInfo);
}
} catch (Exception ex) {
failedMethods.add(method);
logger.warn("removeHandlerMethod error, method [" + method + "]: " + ex.getMessage());
}
});
if (!failedMethods.isEmpty()) {
logger.warn("Failed to unregister controller methods: " + failedMethods);
}
return true;
} finally {
LOCK.unlock();
}
}
}
然后通过继承 DelegatingWebMvcConfiguration
配置类的方式替换掉默认的 RequestMappingHandlerMapping
@Configuration
public class WebaConfig extends DelegatingWebMvcConfiguration {
private static final Logger logger = LoggerFactory.getLogger(WebAlphaConfigProxy.class);
private final RequestMappingControllerRegistry controllerRegistry = new RequestMappingControllerRegistry();
@Override
protected RequestMappingHandlerMapping createRequestMappingHandlerMapping() {
return controllerRegistry;
}
@Bean
public ControllerRegistry controllerRegistry() {
return controllerRegistry;
}
}