How to override @RequestMapping in another controller?

I need to extend an existing controller and add some functionality to it. But as a project requirement I can't touch in the original controller, the problem is that this controller have an @RequestMapping annotation on it. So my question is how can I make requests to /someUrl go to my new controller instead of the old one.

here is a example just to clarify what I'm talking about:

Original controller:

@Controller
public class HelloWorldController {

    @RequestMapping("/helloWorld")
    public String helloWorld(Model model) {
        model.addAttribute("message", "Hello World!");
        return "helloWorld";
    }
}

new Controller:

@Controller
public class MyHelloWorldController {

    @RequestMapping("/helloWorld")
    public String helloWorld(Model model) {
        model.addAttribute("message", "Hello World from my new controller");
        // a lot of new logic
        return "helloWorld";
    }
}

how can I override the original mapping without editing HelloWorldController?

Upvotes: 31

Views: 44096

Answers (4)

Ahmed Sayed
Ahmed Sayed

Reputation: 1564

You can dynamically (on application startup) deregister the existing handler methods from the RequestMappingHandlerMapping, and register your (new) handler method instead. This could be done as follows:

class ApplicationConfig {

    @Bean
    NewController newController() {
        return new NewController();
    }

    @Autowired
    public void registerOverriddenControllerEndpoint(final RequestMappingHandlerMapping handlerMapping,
            final NewController controller) throws NoSuchMethodException {

        final RequestMappingInfo mapping = RequestMappingInfo.paths("path/to/be/overridden")
           .methods(RequestMethod.GET) // or any other request method
           .build();

        handlerMapping.unregisterMapping(mapping);
        Class[] argTypes = new Class[]{/* The parameter types needed for the 'methodThatHandlesTheEndpoint' method */};
        handlerMapping.registerMapping(mapping, controller, NewController.class.getMethod("methodThatHandlesTheEndpoint", argTypes));
    }
}

This means, that I have now two methods with the same mapping:

class ExistingController {
   // This will be now ignored
   @GetMapping("path/to/be/overridden")
   public ResponseEntity<Void> methodThatHandlesTheEndpoint() {
   }
}

and

class NewController {
   // This will be now the main handler
   @GetMapping("path/to/be/overridden")
   public ResponseEntity<Void> methodThatHandlesTheEndpoint() {
   }
}

Upvotes: 0

Bruce Grobler
Bruce Grobler

Reputation: 41

Here is another workaround, that may or may not be dangerous.

Create the below class "MyRequestMappingHandler", then wire it up in your MvcConfig

@Bean
public RequestMappingHandlerMapping requestMappingHandlerMapping() {

    return new MyRequestMappingHandler();
}

RequestMappingHandlerMapping: * THIS IS NOT PRODUCTION CODE - UP TO YOU *

import org.slf4j.Logger;
import org.slf4j.LoggerFactory;
import org.springframework.aop.support.AopUtils;
import org.springframework.context.annotation.Primary;
import org.springframework.stereotype.Controller;
import org.springframework.web.servlet.mvc.method.RequestMappingInfo;
import org.springframework.web.servlet.mvc.method.annotation.RequestMappingHandlerMapping;

import java.lang.reflect.Method;
import java.util.List;
import java.util.Map;
import java.util.stream.Collectors;

public class MyRequestMappingHandler extends RequestMappingHandlerMapping {

@Override
protected RequestMappingInfo getMappingForMethod(Method method, Class<?> handlerType) {

        RequestMappingInfo mappingForMethod = super.getMappingForMethod(method, handlerType);

        // Check if this class extends a super. and that super is annotated with @Controller.
        Class superClass = handlerType.getSuperclass();

        if (superClass.isAnnotationPresent(Controller.class)) {
            // We have a super class controller.

            if (handlerType.isAnnotationPresent(Primary.class)) {
            // We have a @Primary on the child.
            return mappingForMethod;
            }
        } else {
            // We do not have a super class, therefore we need to look for other implementations of this class.
            Map<String, Object> controllerBeans = getApplicationContext().getBeansWithAnnotation(Controller.class);

            List<Map.Entry<String, Object>> classesExtendingHandler = controllerBeans.entrySet().stream().filter(e ->
                AopUtils.getTargetClass(e.getValue()).getSuperclass().getName().equalsIgnoreCase(handlerType
                        .getName()) &&
                        !AopUtils.getTargetClass(e.getValue()).getName().equalsIgnoreCase(handlerType.getName()))
                .collect(Collectors.toList());


            if (classesExtendingHandler == null || classesExtendingHandler.isEmpty()) {
                // No classes extend this handler, therefore it is the only one.
                return mappingForMethod;
            } else {
                // Classes extend this handler,

                // If this handler is marked with @Primary and no others are then return info;
                List<Map.Entry<String, Object>> classesWithPrimary = classesExtendingHandler
                    .stream()
                    .filter(e -> e.getValue().getClass().isAnnotationPresent(Primary.class) &&
                            !AopUtils.getTargetClass(e.getValue().getClass()).getName().equalsIgnoreCase
                                    (handlerType.getName()))
                    .collect(Collectors.toList());
                if (classesWithPrimary == null || classesWithPrimary.isEmpty()) {
                    // No classes are marked with primary.
                    return null;
                } else {
                    // One or more classes are marked with @Primary,

                    if (classesWithPrimary.size() == 1 && AopUtils.getTargetClass(classesWithPrimary.get(0).getValue
                        ()).getClass().getName().equalsIgnoreCase(handlerType.getName())) {
                        // We have only one and it is this one, return it.
                        return mappingForMethod;
                    } else if (classesWithPrimary.size() == 1 && !AopUtils.getTargetClass(classesWithPrimary.get(0)
                        .getValue()).getClass().getName().equalsIgnoreCase(handlerType.getName())) {
                        // Nothing.
                    } else {
                        // nothing.
                    }
                }
            }
        }



        // If it does, and it is marked with @Primary, then return info.

        // else If it does not extend a super with @Controller and there are no children, then return info;

        return null;
    }
}

What this allows you to do is, extend a @Controller class, and mark it with @Primary, and override a method on that class, your new class will now be loaded up when spring starts up instead of blowing up with "multiple beans / request mappings etc"

Example of "super" Controller :

@Controller 
public class Foobar {

    @RequestMapping(method = "GET")
    private String index() {
      return "view";
    }

} 

Example of implementation :

@Primary
@Controller 
public class MyFoobar extends Foobar {

    @Override
    private String index() {
      return "myView";
    }

}

Upvotes: 4

devops
devops

Reputation: 9197

Url mapping as annotation can not be overridden. You will get an error if two or more Controllers are configured with the same request url and request method.

What you can do is to extend the request mapping:

@Controller
public class MyHelloWorldController {

    @RequestMapping("/helloWorld", params = { "type=42" })
    public String helloWorld(Model model) {
        model.addAttribute("message", "Hello World from my new controller");
        return "helloWorld";
    }

}

Example: Now if you call yourhost/helloWorld?type=42 MyHelloWorldController will response the request


By the way. Controller should not be a dynamic content provider. You need a @Service instance. So you can implement Controller once and use multiple Service implementation. This is the main idea of Spring MVC and DI

@Controller
public class HelloWorldController {

    @Autowired
    private MessageService _messageService; // -> new MessageServiceImpl1() or new MessageServiceImpl2() ...

    @RequestMapping("/helloWorld")
    public String helloWorld(Model model) {
        model.addAttribute("message", messageService.getMessage());
        return "helloWorld";
    }

}

Upvotes: 17

Jordi Castilla
Jordi Castilla

Reputation: 27003

Each mapping must be unique.. There is no way to overrule an existing @RequestMapping.



BUT You can always do some workarounds:

Use a param in the request like this will create a new @RequestMapping that will differ from the existing one.

@RequestMapping("/helloWorld/{someDataId}", method = RequestMethod.GET)
public String helloWorld(@PathVariable("someDataId") final long id, Model model) {
 /* your code here */ 
}

Or creating another @Controller extending the existing one:

public class YourController extends BaseController {

    @Override
    @RequestMapping("/helloWorld")
    public void renderDashboard(Model model){
        // Call to default functionallity (if you want...)
        super.renderDashboard(patientId, map);
    }
}   

Upvotes: 3

Related Questions