Search code examples
javaspringservletsthrift

Perform service multiplexing with Apache Thrift and TServlet


I have a system (Java with Spring Framework) that exposes 7 different Apache Thrift servlets over HTTP using the TServlet class. Currently they all need their own Servlets, ServletMappings, Processors, Handlers etc. so implementing clients have to also keep an internal list of all the various URLs for the different services.

I understand that Apache Thrift supports multiplexing when using TServer and its derivatives by using TMultiplexingProcessor, however since I am using Spring and my Servlet, Handler and Processor are all Spring Beans that get autowired into one another, I'm unsure how to proceed.

Here's an example of how one of the services gets wired up:

UserServiceHandler.java

@Component
public class UserServiceHandler implements UserService.Iface {
    @Override
    public User getUser(String userId) throws TException {
        // implementation logic goes here
    }
}

UserServiceProcessor.java

@Component
public class UserServiceProcessor extends UserService.Processor<UserServiceHandler> {

    private UserServiceHandler handler;

    @Autowired
    public UserServiceProcessor(UserServiceHandler iface) {
        super(iface);
        handler = iface;
    }

    public UserServiceHandler getHandler() {
        return handler;
    }

    public void setHandler(UserServiceHandler handler) {
        this.handler = handler;
    }

}

UserServiceServlet.java

@Component
public class UserServiceServlet extends TServlet {
    private UserServiceProcessor processor;

    @Autowired
    public UserServiceServlet(UserServiceProcessor p) {
        super(p, new TBinaryProtocol.Factory());
        processor = p;
    }
}

Servlet Registration

ServletRegistration.Dynamic userService = servletContext.addServlet("UserServiceServlet", (UserServiceServlet) ctx.getBean("userServiceServlet"));
userService.setLoadOnStartup(1);
userService.addMapping("/api/UserService/*");
// This same block repeated 7 times for each *ServiceServlet with different mappings

I would like to have all 7 service handlers map to a single URL like /api/*. Is this even possible? I suppose I would have to create a single servlet and processor, but I'm unsure what they should look like. My processors extend UserService.Processor and the like.


Solution

  • OK, figured it out. Might not be the best way, so I welcome criticism.

    Here were my rough steps:

    1. Keep the handler classes the way they were.
    2. Create a new class that extends TMultiplexedProcessor
    3. Create a new class that extends TServlet
    4. All Processors (e.g. the UserServiceProcessor have a handler property and a corresponding getter and setter

    Here is my ApiMultiplexingProcessor:

    @Component
    public class ApiMultiplexingProcessor extends TMultiplexedProcessor {
    
        UserServiceHandler userServiceHandler;
        ReportServiceHandler reportServiceHandler;
        // ... more service handlers can go here
    
        @Autowired
        public ApiMultiplexingProcessor(UserServiceProcessor userServiceProcessor, ReportServiceProcessor reportServiceProcessor) {
            this.registerProcessor("UserService", userServiceProcessor);
            this.registerProcessor("ReportService", reportServiceProcessor);
            // add more registerProcessor lines here for additional services
    
            userServiceHandler = userServiceProcessor.getHandler();
            reportServiceHandler = reportServiceProcessor.getHandler();
            // set any additional service handlers here
        }
    
        // getters and setters for the handlers
    
        public UserServiceHandler getUserServiceHandler() {
            return userServiceHandler;
        }
    
        public void setUserServiceHandler(UserServiceHandler userServiceHandler) {
            this.userServiceHandler = userServiceHandler;
        }
    
        public ReportServiceHandler getReportServiceHandler() {
            return reportServiceHandler;
        }
    
        public void setReportServiceHandler(ReportServiceHandler reportServiceHandler) {
            this.reportServiceHandler = reportServiceHandler;
        }
    }
    

    So to explain the above a bit, if you add any additional services, you need to add the *ServiceHandler classes as fields on this class, and create the getters and setters etc.

    So now that we have that, we can create a new single servlet that will be added to the servlet context.

    Here is my ApiServlet:

    @Component
    public class ApiServlet extends TServlet {
        private ApiMultiplexingProcessor processor;
    
        @Autowired
        public ApiServlet(ApiMultiplexingProcessor p) {
            super(p, new TBinaryProtocol.Factory());
            processor = p;
        }
    }
    

    And then you just add this servlet to the servlet context (from a bean) as before:

    ServletRegistration.Dynamic api = servletContext.addServlet("ApiServlet", (ApiServlet) ctx.getBean("apiServlet"));
    api.setLoadOnStartup(1);
    api.addMapping("/api/*");
    // yay now we have a single URL and a single servlet
    

    This all could be helpful to someone else in my situation, so enjoy!

    P.S. make sure when adapting your clients you use the TMultiplexedProtocol so that you can pass the service name through when talking to the server e.g.

    TTransport transport = new THttpClient(new Uri("https://myapp.com/api/"));
    TProtocol protocol = new TBinaryProtocol(transport);
    TMultiplexedProtocol mp = new TMultiplexedProtocol(protocol, "UserService");
    UserService.Client userServiceClient = new UserService.Client(mp);