Is it possible to dynamically set RequestMappings in Spring MVC?
Asked Answered
S

8

19

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? }

Sting answered 22/4, 2011 at 18:9 Comment(0)
O
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.

Oospore answered 22/4, 2011 at 18:11 Comment(2)
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?Sting
@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.Oospore
K
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);
  }

}
Klenk answered 26/1, 2018 at 17:45 Comment(2)
could you please add when and where does addMapping is invoked?Moen
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 addMappingKlenk
D
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));
    }
}
Dodona answered 7/3, 2019 at 13:1 Comment(0)
D
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());
Duggan answered 24/9, 2016 at 1:9 Comment(0)
S
5

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

Subchloride answered 7/12, 2016 at 16:29 Comment(0)
F
2
@RequestMapping(value = "/bla/{myParam1}", method = RequestMethod.GET)
public String media(@PathVariable("myParam1") String myParam1, HttpServletRequest request, HttpServletResponse response) {
    return "bla/" + myParam1;
}
Fenny answered 23/7, 2020 at 12:25 Comment(0)
S
0

Very important for solution https://mcmap.net/q/628253/-is-it-possible-to-dynamically-set-requestmappings-in-spring-mvc

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.

Shakedown answered 4/7, 2022 at 8:0 Comment(0)
J
0

I ended up going in a similar manner to what is described here but I will try to show an example.

I should mention, anytime I called afterPropertiesSet, before I wrote all this code, it would duplicate registered endpoints and cause exceptions to be thrown. It looks like @Dominic had the same issue and simply unregisters the existing before calling afterPropertiesSet

I am working in a large project with over 1000 endpoints mapped with spring annotations. The requirements for me were to have Spring handle my dynamic endpoints exactly the same as a fully annotated and registered by Spring RestController.

I wanted to see if it was possible to populate a custom class implementing ConfigurationProperties with application.yml or application.properties (or another propertySource) supplied data and to then use that mapping to define my endpoints at runtime.

It took some time but this "does" work. I cannot say it is the prettiest code I have written, also, we did not end up using it in the end.

ExampleService.java is simply an @Service style bean with the business logic to retrieve data from somewhere, like a database or rabbitmq or a file or some other sort of storage means. In my example below it simply has a String variable named "name" to explain how the @ConfigurationProperties work. I did not have to use the Service class, it could just be a regular POJO that gets mapped and used to generate the service and controller beans.

Define the custom ConfigurationProperties:

@ConfigurationProperties(prefix = ExampleConfigurationProperties.PREFIX)
@ConstructorBinding
public class ExampleConfigurationProperties {
    /**
     * Properties prefix.
     */
    public static final String PREFIX = "custom.endpoints";

    private final Set<ExampleService> exampleService;

    public ExampleConfigurationProperties(@Nullable final Set<ExampleService> services) {
        this.exampleService = services;
    }

    public Set<ExampleService> getExampleServices() {
        return exampleService;
    }
}

In your application.yml or properties(properties is similar but is written different), you will want something like:

custom:
  endpoints:
    services[0]:
      name: SomeName
      # also some other properties in the ExampleService to map
    services[1]:
      name: SomeOtherName
      # also some other properties in the ExampleService to map

Configuration class, The important part in the constructor is the

@Qualifier

, in this application there were 4 beans of type RequestMappingHandlerMapping defined and I needed to specify the one I wanted to wire my endpoints into. I ended up wiring up a List and finding them in debug mode to identify the one I wanted.

@Configuration(proxyBeanMethods = false)
@EnableConfigurationProperties(ExampleConfigurationProperties.class)
public class ExampleSpringConfig implements ApplicationContextAware {
    private final ExampleConfigurationProperties properties;
    public ExampleSpringConfig (
        final ExampleConfigurationProperties properties,
        @Qualifier("requestMappingHandlerMapping") RequestMappingHandlerMapping handlerMapping) {
        this.properties = Objects.requireNonNull(properties);
    }
    
    @Override
    public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
        //there is probably a better way of getting this beanFactory but I have no reason to further research this as we did not end up going this route.
        ConfigurableApplicationContext context = (ConfigurableApplicationContext) applicationContext;
        DefaultListableBeanFactory beanFactory = (DefaultListableBeanFactory) context.getBeanFactory();
        for (ExampleService svc : properties.getExampleService) {
            // good spot to "wire" in any required dependencies/beans for this "Properties" mapped list of objects
            // svc.setRandomInternalProperty(<someValue>);
            
            // give your new "Properties" initiated object a unique name and make it a bean in the current context
            beanFactory.registerSingleton(svc.getClass().getSimpleName() + "_" + svc.getName(), svc);
            
            // this will map the controllers methods to the "Properties" mapped name paramenter appended to "Endpoint"
            ExampleControllerRest controller =
                ExampleControllerRest.createController(handlerMapping, "/" + svc.getName() + "Endpoint", svc);

            // once the controller has all of its methods I want exposed mapped, register the controller as a Spring Bean for use elsewhere
            beanFactory.registerSingleton(controller.getClass().getSimpleName() + "_" + svc.getName(), controller);
        }
    }
}

Then the Controller class:

public class ExampleController {
    private final ExampleService exampleService;

    public ExampleController(final ExampleService exampleService) {
        this.exampleService = exampleService;
    }

    public static ExampleController createController(RequestMappingHandlerMapping handlerMapping, String path, ExampleService exampleService) {
        try (DynamicType.Unloaded<ExampleController> temp = new ByteBuddy()
                .subclass(ExampleController.class)
                .annotateType(AnnotationDescription.Builder
                        .ofType(RequestMapping.class)
                        .defineArray("value", new String[]{path})
                        .build())
                .annotateType(AnnotationDescription.Builder
                        .ofType(RestController.class)
                        .build())
                .make();
        ) {
            ExampleController controller = temp.load(ExampleController.class.getClassLoader())
                    .getLoaded()
                    .getConstructor(ExampleService.class)
                    .newInstance(baseUnitAssignmentsDTOService);

            RequestMappingInfo.BuilderConfiguration builderConfiguration = new RequestMappingInfo.BuilderConfiguration();
            // this was important to get a response back that replicated what Spring autowiring does with custom 
            // response types, otherwise an error is thrown about an empty response body
            // if you are going to use ResponseEntity for response object type wrapping, this might not be needed
            builderConfiguration.setContentNegotiationManager(handlerMapping.getContentNegotiationManager());

            // the pathMatcher was added to have these endpoints look exactly the same as the ones that Spring autowires, I am not sure if this is needed
            AntPathMatcher pathMatcher = (AntPathMatcher) handlerMapping.getPathMatcher();
            pathMatcher.match(path, path);
            builderConfiguration.setPathMatcher(pathMatcher);

            // handler endpoint mappings, these are all of the endpoints I want defined for each element in the custom "Properties" set of services
            handlerMapping.registerMapping(RequestMappingInfo
                    .paths(path + "/getAll")
                    .methods(RequestMethod.GET)
                    .options(builderConfiguration)
                    .produces(MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE)
                    .build(), controller, controller.getClass().getMethod("getAll"));
            // I have redacted a bunch of endpoints but the essential part is to define all endpoints similar to the "getAll" above

            handlerMapping.registerMapping(RequestMappingInfo
                    .paths(path + "/insert")
                    .methods(RequestMethod.POST)
                    .options(builderConfiguration)
                    .consumes(MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE)
                    .produces(MediaType.APPLICATION_JSON_VALUE, MediaType.APPLICATION_XML_VALUE)
                    .build(), controller, controller.getClass().getMethod("insert", PickListOptionDTO.class)); 
                    // just remember to define all method body param types for reflection to find the corret method


            return controller;
        } catch (IOException | InvocationTargetException | InstantiationException | IllegalAccessException |
                 NoSuchMethodException e) {
            throw new RuntimeException(e);
        }
    }


    public List<PickListOptionDTO> getAll() {
        return getService().getAllUsers();
    }

    public PickListOptionDTO insert(@RequestBody PickListOptionDTO dto) {
        return getService().insertUnit(dto);
    }

    public ExampleService getService() {
        return exampleService;
    }
}
Jandy answered 2/12, 2023 at 14:44 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.