I just tried to develop a plugin system for my spring boot web application. The application is deployed on a tomcat server using the root context path. The plugin system allows me to load specially prepared jar files at runtime. The system should also be able to undeploy plugins at runtime. Those jars are contained inside a plugin folder in the current working dir. I wanted every plugin to have it's own spring context to operate with. Dependency injection is working as expected but spring does not discover my @RequestMapping annotation for the plugin context. So my question is: How can i make spring discover those @RequestMapping annotations for my plugins (at runtime)?
I am using the latest spring boot version and the following application.yml:
# Server
server:
error:
whitelabel:
enabled: true
session:
persistent: true
tomcat:
uri-encoding: UTF-8
# Spring
spring:
application:
name: Plugins
mvc:
favicon:
enabled: false
favicon:
enabled: false
thymeleaf:
encoding: UTF-8
# Logging
logging:
file: application.log
level.: error
AnnotationConfigApplicationContext context = new AnnotationConfigApplicationContext();
URLClassLoader urlClassLoader = URLClassLoader.newInstance(new URL[] { plugin.getPluginURL() }, getClass().getClassLoader()); // plugin.getPluginURL will refer to a jar file with the plugin code (see below).
context.setClassLoader(urlClassLoader);
context.setParent(applicationContext); // applicationContext is the the context of the original spring application. It was autowired.
ClassPathBeanDefinitionScanner scanner = new ClassPathBeanDefinitionScanner(context, true);
scanner.scan("my.plugin.package");
context.refresh();
@Controller
public class PluginTestController {
@PostConstruct
private void postContruct() {
System.out.println("Controller ready.");
}
@RequestMapping(value = "/test", method = RequestMethod.GET)
public ResponseEntity<String> doGet() {
return new ResponseEntity<>("Hello!", HttpStatus.OK);
}
}
@RequestMapping
// context is the Plugins context, that i just created earlier.
for (Map.Entry < String, Object > bean: context.getBeansWithAnnotation(Controller.class).entrySet()) {
Object obj = bean.getValue();
// From http://stackoverflow.com/questions/27929965/find-method-level-custom-annotation-in-a-spring-context
// As you are using AOP check for AOP proxying. If you are proxying with Spring CGLIB (not via Spring AOP)
// Use org.springframework.cglib.proxy.Proxy#isProxyClass to detect proxy If you are proxying using JDK
// Proxy use java.lang.reflect.Proxy#isProxyClass
Class < ? > objClz = obj.getClass();
if (org.springframework.aop.support.AopUtils.isAopProxy(obj)) {
objClz = org.springframework.aop.support.AopUtils.getTargetClass(obj);
}
for (Method m: objClz.getDeclaredMethods()) {
if (m.isAnnotationPresent(RequestMapping.class)) {
RequestMapping requestMapping = AnnotatedElementUtils.findMergedAnnotation(m, RequestMapping.class);
// @formatter:off
RequestMappingInfo requestMappingInfo = RequestMappingInfo
.paths(requestMapping.path())
.methods(requestMapping.method())
.params(requestMapping.params())
.headers(requestMapping.headers())
.consumes(requestMapping.consumes())
.produces(requestMapping.produces())
.mappingName(requestMapping.name())
.customCondition(null)
.build();
// @formatter:on
// This will register the actual mapping, so that the Controller can handle the Request
requestMappingHandlerMapping.registerMapping(requestMappingInfo, obj, m);
}
}
}
The SpringMvc has it's own cache of @RequestMapping
, the detail code is RequestMappingHandlerMapping
. As you can see, the init method is shown, Maybe you can call init method after load new plugin.
protected void initHandlerMethods() {
if (logger.isDebugEnabled()) {
logger.debug("Looking for request mappings in application context: " + getApplicationContext());
}
String[] beanNames = (this.detectHandlerMethodsInAncestorContexts ?
BeanFactoryUtils.beanNamesForTypeIncludingAncestors(getApplicationContext(), Object.class) :
getApplicationContext().getBeanNamesForType(Object.class));
for (String beanName : beanNames) {
if (isHandler(getApplicationContext().getType(beanName))){
detectHandlerMethods(beanName);
}
}
handlerMethodsInitialized(getHandlerMethods());
}
this is spring3's RequestMappingHandlerMapping
code, maybe there is some changes in spring4's impl.