4

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.

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);
            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();
            // This will register the actual mapping, so that the Controller can handle the Request
            requestMappingHandlerMapping.registerMapping(requestMappingInfo, obj, m);
        }
    }
}

However i am still searching for a way to make it work using the spring way: https://github.com/spring-projects/spring-framework/blob/fb7ae010c867ae48ab51f48cce97fe2c07f44115/spring-webmvc/src/main/java/org/springframework/web/servlet/handler/AbstractHandlerMethodMapping.java

Thanks for reading.

Tommy Schmidt
  • 1,224
  • 1
  • 16
  • 30
  • can you post your application.properties ? – alexbt Nov 06 '16 at 01:38
  • @Alex i just updated the post to include my application.yml – Tommy Schmidt Nov 06 '16 at 16:49
  • I just noticed your PluginTestController.doGet method is private. I can't test it at the moment, but are you sure this is supported ? Ok, looked it up, it is supported. (https://sebastian.marsching.com/blog/archives/149-Springs-RequestMapping-annotation-works-on-private-methods.html) still I would try changing the methods to public – alexbt Nov 06 '16 at 16:54
  • @Alex i just tried it and it still does not work – Tommy Schmidt Nov 06 '16 at 17:05
  • You are creating a new context (`AnnotationConfigApplicationContext`), but what you need is to add your controllers to the *existing* one. – Ruben Nov 07 '16 at 20:34
  • i forgot to mention that the plugins should also be undeployed and therefor their context (or everything they just registered) should be destroyed. if i understand you correctly, registering the controller in the existing context (of the base application) does not destroy the requestmapping when i destroy the plugins context. however i will give it a try – Tommy Schmidt Nov 07 '16 at 21:38
  • This sounds silly, but does it help if you access localhost:8080/test.html rather than just test? – Pedantic Nov 07 '16 at 21:49
  • @Pedantic that was actually something i tried out at the beginning ^^ and it did not help – Tommy Schmidt Nov 07 '16 at 21:50
  • @TommySchmidt ok :) Sometimes one forgets the seemingly obvious. – Pedantic Nov 07 '16 at 21:52
  • 2
    I think you might need to register a new instance of Spring's dispatcher servlet with the container. Speculating here, but when you initialize a spring web app, spring's dispatcher servlet and associated delegates figure out how to map requests to your beans. You probably need a new web app context with associated plumbing with the servlet container (e.g. tomcat or jetty), gonna suggest an alternative approach in a separate comment since I'm running out of sp – Taylor Nov 07 '16 at 22:28
  • Please note I have no idea if this matches your requirements, but I'd suggest an alternative architecture. I'd keep each one of these as their own application and use programmatic interaction with something like netflix's Zuul (with ribbon) to give the "appearance" of a single application. This keeps plugins from "mucking about" with your application's state, except through defined network APIs (REST, or SOAP if you're a masochist). Downside is you have to build all those APIs. – Taylor Nov 07 '16 at 22:30
  • @Ruben any hints on how to add the controller to the existing context without refreshing it (as refreshing is not supported by AnnotationConfigEmbeddedWebApplicationContext)? – Tommy Schmidt Nov 07 '16 at 22:32
  • @Taylor well we also had a similar aproach but that did not work out that well as the api was huge. i wanted to shrink it down to a minimum. what do your think about registering a controller in the base context that basically would redirect it to every registered plugin if it matches a given request path? – Tommy Schmidt Nov 07 '16 at 22:39
  • So basically i just ended up creating the context the same way that i did before. The only difference is: i am using reflection to find every `@Controller` annotated class and loop through each `@RequestMapping` annotated Method. Then i use springs RequestMappingHandlerMapping to register those methods myself. I hope that i can find some more informations about the way spring parses those annotations but for now i got a working controller for my plugin. At least for the simplest case. I will update the code when it passed all my tests. – Tommy Schmidt Nov 08 '16 at 05:44

3 Answers3

3

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.

BeeNoisy
  • 1,254
  • 1
  • 14
  • 23
  • 1
    this is actually really useful and similar to the code that i already figured out last night. As of now i am using a similar aproach based on the answer for this question: http://stackoverflow.com/questions/27929965/find-method-level-custom-annotation-in-a-spring-context. i just updated my answer to include the code that made the controller work for me. I dont think that my aproach is perfect yet, but at least it does work for now. i will take a look at the way spring4 does implement it. Thanks for pointing to the code. – Tommy Schmidt Nov 08 '16 at 22:52
  • I am very happy to help you, If you have a new research result, would you update this post? – BeeNoisy Nov 09 '16 at 01:04
0

Have you added your controller location in context:component-scan base-package="location" in spring_servlet.xml

Vignesh
  • 3
  • 3
  • i am using spring boot and rhe java annotatiom driven way to configure my controllers. so there is no spring_servlet.xml for that project – Tommy Schmidt Nov 08 '16 at 18:40
0

I think, You need to add

<context:component-scan base-package="package_name" />
<mvc:annotation-driven />
<mvc:default-servlet-handler />

in your xxx-servlet.xml file.

Brajesh
  • 1,515
  • 13
  • 18
  • i am using spring boot and the java annotatiom driven way to configure my controllers. so there is no spring_servlet.xml for that project. also that question was already answered – Tommy Schmidt Jan 23 '18 at 17:08