18

I've been using Spring MVC for three months now. I was considering a good way to dynamically add RequestMapping. This comes from the necessity to put controller parts in a library and then add them dinamically. Anyway, the only way I can think of is to declare a controller like this:

@Controller
@RequestMapping("/mypage")
public class MyController {

@RequestMapping(method = RequestMethod.GET)
    public ModelAndView mainHandler(HttpServletRequest req) {
        return handleTheRest(req);
    }

}

Which is no good because basically I'm not using Spring. Then I cannot use form binding, annotations etc.. I'd like to add requestMappings dynamically to methods of classes that could be annotated like usual MVC controllers, with autobinding, so that I could avoid processing HttpServletRequest manually.

Any ideas? }

gotch4
  • 13,093
  • 29
  • 107
  • 170

7 Answers7

34

Spring MVC performs URL mappings using implementations of the HandlerMapping interface. The ones usually used out of the box are the default implementations, namely SimpleUrlHandlerMapping, BeanNameUrlHandlerMapping and DefaultAnnotationHandlerMapping.

If you want to implement your own mapping mechanism, this is fairly easy to do - just implement that interface (or, perhaps more likely, extend AbstractUrlHandlerMapping), declare the class as a bean in your context, and it will be consulted by DispatcherServlet when a request needs to be mapped.

Note that you can have as many HandlerMapping implementations as you like in the one context. They will be consulted in turn until one of them has a match.

skaffman
  • 398,947
  • 96
  • 818
  • 769
  • Thanks Skaff, you always give great tips. Anyway, how do I manipulate the context by code, I mean, is there a way to dynamically add an HandlerMapping bean or any other bean? – gotch4 Apr 23 '11 at 09:01
  • @gotch4: You don't need to dynamically add a `HandlerMapping`. You configure *one* custom `HandlerMapping`, and then dynamically add mappings to it. Since you're writing the `HandlerMapping` yourself, how that works is up to you. – skaffman Apr 23 '11 at 09:12
11

I spent a long time trying to get this to work, but finally managed to find a solution that returns a ResponseEntity instead of the older ModelAndView. This solution also has the added benefit of avoiding any explicit interaction with Application Context.

Endpoint Service

@Service
public class EndpointService {

  @Autowired
  private QueryController queryController;

  @Autowired
  private RequestMappingHandlerMapping requestMappingHandlerMapping;

  public void addMapping(String urlPath) throws NoSuchMethodException {

    RequestMappingInfo requestMappingInfo = RequestMappingInfo
            .paths(urlPath)
            .methods(RequestMethod.GET)
            .produces(MediaType.APPLICATION_JSON_VALUE)
            .build();

    requestMappingHandlerMapping.
            registerMapping(requestMappingInfo, queryController,
                    QueryController.class.getDeclaredMethod("handleRequests")
            );
  }

}

Controller to handle newly mapped requests

@Controller
public class QueryController {

  public ResponseEntity<String> handleRequests() throws Exception {

    //Do clever stuff here

    return new ResponseEntity<>(HttpStatus.OK);
  }

}
kaybee99
  • 4,566
  • 2
  • 32
  • 42
  • could you please add when and where does `addMapping` is invoked? – Tiago Lopo Feb 04 '18 at 10:42
  • It can be invoked from wherever you like. In my case, my application posted to a separate endpoint that parsed the new request, and eventually called `addMapping` – kaybee99 Feb 05 '18 at 09:38
8

Following construct configures and implements handler methods in a single class.

It is a combination of dynamic and static mapping - all the MVC annotations can be used like @RequestParam, @PathVariable, @RequestBody, etc.

BTW: @RestController annotation creates bean out of the class and adds @ResponseBody to every handler method so that it does not have to be done manually.

@RestController
public class MyController {

    @Inject
    private RequestMappingHandlerMapping handlerMapping;

    /***
     * Register controller methods to various URLs.
     */
    @PostConstruct
    public void init() throws NoSuchMethodException {

        /**
         * When "GET /simpleHandler" is called, invoke, parametrizedHandler(String,
         * HttpServletRequest) method.
         */
        handlerMapping.registerMapping(
                RequestMappingInfo.paths("/simpleHandler").methods(RequestMethod.GET)
                .produces(MediaType.APPLICATION_JSON_VALUE).build(),
                this,
                // Method to be executed when above conditions apply, i.e.: when HTTP
                // method and URL are called)
                MyController.class.getDeclaredMethod("simpleHandler"));

        /**
         * When "GET /x/y/z/parametrizedHandler" is called invoke
         * parametrizedHandler(String, HttpServletRequest) method.
         */
        handlerMapping.registerMapping(
                RequestMappingInfo.paths("/x/y/z/parametrizedHandler").methods(RequestMethod.GET)
                .produces(MediaType.APPLICATION_JSON_VALUE).build(),
                this,
                // Method to be executed when above conditions apply, i.e.: when HTTP
                // method and URL are called)
                MyController.class.getDeclaredMethod("parametrizedHandler", String.class, HttpServletRequest.class));
    }

    // GET /simpleHandler
    public List<String> simpleHandler() {
        return Arrays.asList("simpleHandler called");
    }

    // GET /x/y/z/parametrizedHandler
    public ResponseEntity<List<String>> parametrizedHandler(
            @RequestParam(value = "param1", required = false) String param1,
            HttpServletRequest servletRequest) {
        return ResponseEntity.ok(Arrays.asList("parametrizedHandler called", param1));
    }
}
Michal Foksa
  • 11,225
  • 9
  • 50
  • 68
7

I know this is really old but I figured I toss this in in case anyone else has the same rough experience I did trying to make this work. I ended up taking advantage of two features of Spring: the ability to dynamically register beans after the context is started and the afterPropertiesSet() method on the RequestMappingHandlerMapping object.

When RequestMappingHandlerMapping is initialized, it scans the context and creates a map of all @RequestMappings that it needs to serve (presumably for performance reasons). If you dynamically register beans annotated with @Controller, they will not be picked them up. To retrigger this scan, you just need to call afterPropertiesSet() after you've added your beans.

In my particular use case, I instantiated the new @Controller objects in a separate Spring context and needed to wire them into my WebMvc context. The particulars of how the objects don't matter for this though, all you need is an object reference:

//register all @Controller beans from separateContext into webappContext
separateContext.getBeansWithAnnotation(Controller.class)
   .forEach((k, v) -> webappContext.getBeanFactory().registerSingleton(k, v));

//find all RequestMappingHandlerMappings in webappContext and refresh them
webappContext.getBeansOfType(RequestMappingHandlerMapping.class)
   .forEach((k, v) -> v.afterPropertiesSet());

For example, you could also do this:

//class annotated with @Controller
MyController controller = new MyController

//register new controller object
webappContext.getBeanFactory().registerSingleton("myController", controller);

//find all RequestMappingHandlerMappings in webappContext and refresh them
webappContext.getBeansOfType(RequestMappingHandlerMapping.class)
   .forEach((k, v) -> v.afterPropertiesSet());
monitorjbl
  • 4,280
  • 3
  • 36
  • 45
4

Please look at my solution. It doesn't create dynamic @RequestMapping in your code, but provides a HandlerMapping and Controller that handles all request. If you run that application, you will get hello world message in json.

Application class:

@SpringBootApplication
public class Application {
  public static void main(String[] args) {
    SpringApplication.run(Application.class, args);
  }

  @Bean
  public MyCustomHandler myCustomHandler(MyCustomController myCustomController) {
    MyCustomHandler myCustomHandler = new MyCustomHandler(myCustomController);
    myCustomHandler.setOrder(Ordered.HIGHEST_PRECEDENCE);
    return myCustomHandler;
  }
}

MyCustomController

@Component
public class MyCustomController extends AbstractController {

  @Override
  protected ModelAndView handleRequestInternal(HttpServletRequest request,
      HttpServletResponse response) throws Exception {
    response.setContentType(MediaType.APPLICATION_JSON_UTF8_VALUE);
    response.getWriter().println("{\"hello\":\"world\"}");
    return null;
  }
}

MyCustomHandler

public class MyCustomHandler extends AbstractHandlerMapping {

  private MyCustomController myCustomController;

  public MyCustomHandler(MyCustomController myCustomController) {
    this.myCustomController = myCustomController;
  }

  @Override
  protected Object getHandlerInternal(HttpServletRequest request) throws Exception {
    return myCustomController;
  }
}

https://github.com/nowszy94/spring-mvc-dynamic-controller

nowszy94
  • 3,151
  • 4
  • 18
  • 33
2
@RequestMapping(value = "/bla/{myParam1}", method = RequestMethod.GET)
public String media(@PathVariable("myParam1") String myParam1, HttpServletRequest request, HttpServletResponse response) {
    return "bla/" + myParam1;
}
Laurent
  • 469
  • 3
  • 7
0

Very important for solution https://stackoverflow.com/a/39671540/8665226

When testing, I noticed that this solution makes existing routes no longer work. After a little debugging I found out that this is related to the fact that the routes (because internally a MultiValueMap is used) are added several times during a refresh. To fix this error the routes must first be removed from the RequestMappingHandlerMapping before a refresh. This can be done by using requestMappingHandlerMapping.getHandlerMethods().keySet().forEach(requestMappingHandlerMapping::unregisterMapping);

So a complete code would look like this:

applicationContext.getBeansOfType(RequestMappingHandlerMapping.class).forEach((name, requestMappingHandlerMapping) -> {
   requestMappingHandlerMapping.getHandlerMethods().keySet().forEach(requestMappingHandlerMapping::unregisterMapping);
   requestMappingHandlerMapping.afterPropertiesSet();
});

This must be executed after a new RestController has been registered as a bean.

Dominic
  • 70
  • 1
  • 7