Tommy Schmidt Tommy Schmidt - 25 days ago 14
Java Question

Spring does not discover @RequestMapping annotation for a new context at runtime

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


This is the code that loads the plugin:

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();


And the controller code (inside my plugin):

@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);
}
}


When i start the application and load the plugin i can see "Controller ready." in the console. However when i try to access the url (localhost:8080/test) i just get to see the 404 error page. Every url of the non plugin spring context controllers gets mapped correctly (i can access localhost:8080/index for example). I found out that it could have something to do with the RequestMappingHandlerMapping. However i dont really understand how to make use of that in order to make the annotation work again.

Thanks for reading.

Edit: I found a way to make the
@RequestMapping
annotation work for my Controller by using the following code:

// 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 following Link refers to the way spring is parsing those Annotations (initHandlerMethods). This should be used to implement the functionality: https://github.com/spring-projects/spring-framework/blob/fb7ae010c867ae48ab51f48cce97fe2c07f44115/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java

Answer

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.

Comments