如何在不使用@Controller或@RestController的情况下注册一个Controller

问题起源于同事的一个提问:怎么自定义一个Controller?

乍一听这个问题似乎不太正常,我们使用@Controller或者@RestController(当然@ControllerAdvice或者@RestControllerAdvice也是一样的)不就能正常定义一个Controller吗。细问之下,果然别有用意:

如何在不使用@Controller或@RestController的情况下注册一个Controller?

这个需求似乎是希望能以动态的方式引入Controller(原话是说在类上加上个@Controller注解太丑了😂😂😂),总之,大致要满足这两个条件:

  1. 需要注册一个 Bean,并且是条件注册,不能在包扫描期间直接完成注册
  2. 这个 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,调用对应的 registerControllerunregisterController 即可实现注册和注销

这个方法优势在于能够动态注册/注销,不过过程中使用了反射,可以根据场景考虑是否适用

注意:不能连续多次调用 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;
    }

}
  • 22
    点赞
  • 18
    收藏
    觉得还不错? 一键收藏
  • 0
    评论
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值