Logo Questions Linux Laravel Mysql Ubuntu Git Menu
 

Calling Spring controller method without going to internet

tldr: Is there a way to make an internal request (using the method's path) without going to the internet?

--

Why do I need it? I have a project which receives many events. The decision of who will handle each event is made by a Controller. So I have something similar to this:

@RestController
@RequestMapping("/events")
public class EventHandlerAPI {

    @Autowired 
    private EventAHandler eventAhandler;
    @Autowired 
    private EventBHandler eventBhandler;

    @PostMapping("/a")
    public void handleEventA(@RequestBody EventA event) {
       eventAhandler.handle(id, event);
    }

    @PostMapping("/b")
    public void handleEventB(@RequestBody EventB event) {
       eventBhandler.handle(id, event);
    }

}

We recently added support to receive events through a Queue service. It sends to us the payload and the event class. Our decision is to let both interfaces working (rest and queue). The solution to avoid code duplication was to keep the Controller choosing which handler will take care of the event. The code nowadays is similar to this:

@Configuration
public class EventHandlerQueueConsumer {

    @Autowired 
    private EventHandlerAPI eventHandlerAPI;

    private Map<Class, EventHandler> eventHandlers;

    @PostConstruct 
    public void init() { 
        /* start listen queue */ 
        declareEventHandlers();
    }

    private void declareEventHandlers() {
        eventHandlers = new HashMap<>();
        eventHandlers.put(EventAHandler.class, (EventHandler<EventAHandler>) eventHandlerAPI::handleEventA);
        eventHandlers.put(EventBHandler.class, (EventHandler<EventBHandler>) eventHandlerAPI::handleEventB);
    }

   private void onEventReceived(AbstractEvent event) {
       EventHandler eventHandler = eventHandlers.get(event.getClass());
       eventHandler.handle(event);
   }

    private interface EventHandler<T extends AbstractEvent> {
        void handle(T event);
    }

}

This code works, but it doesn't let the controller choose who will handle the event (our intention). The decision is actually being made by the map.

What I would like to do was to invoke the controller method through it's request mapping without going to the internet. Something like this:

@Configuration
public class EventHandlerQueueConsumer {

    // MADE UP CLASS TO SHOW WHAT I WANT
    @Autowired
    private ControllerInkover controllerInvoker;

    @PostConstruct 
    public void init() { /* start listen queue */ }

   private void onEventReceived(AbstractEvent event) {
       controllerInvoker.post(event.getPath(), new Object[] { event });
   }

}

This way is much cleaner and let all the decisions be made by the controller.

I've researched a lot and didn't found a way to implement it. Debugging spring, I found how he routes the request after the DispatcherServlet, but all the spring internals uses HttpServletRequest and HttpServletResponse :(

Is there a way to make an internal request (using the method's path) without going to the internet?

like image 406
Rômulo M. Farias Avatar asked Oct 16 '22 06:10

Rômulo M. Farias


1 Answers

They are classes of the same application

Then it should easy enough.

1) You can call your own API on http(s)://localhost:{port}/api/{path} using RestTemplate utility class. This is preferred way, since you'll follow standard MVC pattern. Something like:

restTemplate.exchange(uri, HttpMethod.POST, httpEntity, ResponseClass.class);

2) If you don't want to invoke network connection at all, then you can either use Spring's internal to find the mapping/method map or use some reflection to build custom map upon controller's startup. Then you can pass your event/object to the method from the map in a way shown in your mock-up class. Something like:

@RequestMapping("foo")
 public void fooMethod() {
    System.out.println("mapping = " + getMapping("fooMethod")); // you can get all methods/mapping in @PostContruct initialization phase
 }

 private String getMapping(String methodName) {
    Method methods[] = this.getClass().getMethods();
    for (int i = 0; i < methods.length; i++) {
        if (methods[i].getName() == methodName) {
            String mapping[] = methods[i].getAnnotation(RequestMapping.class).value();
            if (mapping.length > 0) {
                return mapping[mapping.length - 1];
            }
        }
    }
    return null;
}
like image 96
Mikhail Kholodkov Avatar answered Oct 21 '22 06:10

Mikhail Kholodkov