0
\$\begingroup\$

I'd like to implement a simple interceptor mechanism that, unlike Servlet Filters, can be applied to simple "doX" methods. This is what I'd like to achieve:

@WebServlet(...)
public class MyServlet extends InterceptableHttpServlet{
    @MyInterceptor(myValue="hello world")
    public void doGet(HttpServletRequest req, HttpServletResponse resp){
        [...]
    }
}

I want it to be applied to already existing Servlet classes so I can't afford to implement a Front controller and Action classes.

The syntax of the interceptor classes should be similar to the syntax of servlet filters (which acts as a chain of responsibility).

This is how I set up the Interceptor class:

public abstract class ServletInterceptor<A extends Annotation>{
    protected abstract void init (A annotation);

    public abstract void handle(HttpServletRequest req, HttpServletResponse resp, HttpServletBiConsumer next)
            throws ServletException, IOException;
}  

The HttpServletBiConsumer functional interface is the following:

@FunctionalInterface
public interface HttpServletBiConsumer {
    void handle(HttpServletRequest request, HttpServletResponse response)
            throws ServletException, IOException;
}

The chain is built in the following way:

private HttpServletBiConsumer buildChain(ServletInterceptor<?>[] interceptors, HttpServletBiConsumer target){
        HttpServletBiConsumer current = target;
        for(int i = interceptors.length-1; i >= 0; i--){
            ServletInterceptor<?> interceptor = interceptors[i];

            HttpServletBiConsumer next = current;
            current = (req, res) -> interceptor.handle(req, res, next);
        }
        return current;
    }

Now the first "real" challenge: how can I map each interceptor with an annotation (and its parameters) in the most automatic way? (Most automatic way = no manual registrations).

Since I'm using TomEE 8.0.8 (which is a CDI environment), I'm using it to look up the classpath in order to get one (and only one) implementation of the ServletInterceptor class. Here is the factory class:

@SuppressWarnings("unchecked")
public final class ServletInterceptorFactory {
    public static <T extends Annotation> ServletInterceptor<T> instantiate(T annotation){
        try {
            Class<T> annotationType = (Class<T>) annotation.annotationType();
            Class<? extends ServletInterceptor<T>> interceptorClass = resolve(annotationType);
            ServletInterceptor<T> interceptor = interceptorClass.getConstructor().newInstance();
            interceptor.init(annotation);
            return interceptor;
        } catch (ReflectiveOperationException e) {
            throw new RuntimeException(e);
        }
    }

    private static <T extends Annotation> Class<ServletInterceptor<T>> resolve(Class<T> annotationType){
        ParameterizedType interceptorType = new ParameterizedType() {
            @Override
            public Type[] getActualTypeArguments() {
                return new Class[]{annotationType};
            }

            @Override
            public Type getRawType() {
                return ServletInterceptor.class;
            }

            @Override
            public Type getOwnerType() {
                return ServletInterceptor.class.getSuperclass();
            }
        };

        Set<Bean<?>> beans = CDI.current().getBeanManager().getBeans(interceptorType, Any.Literal.INSTANCE);
        if(beans.isEmpty())
            throw new RuntimeException("No implementation for annotation " + annotationType.getName());
        else if(beans.size() > 1)
            throw new RuntimeException("There are multiple implementations for annotation" + annotationType.getName());

        return (Class<ServletInterceptor<T>>) beans.iterator().next().getBeanClass();
    }
}

Now, the second "real" challenge: how can I decorate the Servlet class? The servlets are registered as soon as the webapp is deployed (since the webapp are marked with the @WebServlet annotation), so I can't decorate them with some kind of Wrapper class. (Unless I use Servlet 3.0 programmatic registration mechanism, which precludes me from the usage of the comfy @WebServlet annotation).

So I chose to implement an abstract InterceptableServlet class, which uses the init() method to lookup the annotations on the doX methods, builds the chains accordingly and stores them in a private field. The service() method checks request.getMethod() to check the HTTP method (get, post, put, etc...), calls the appropriate chain and then continues with super.service() to do the ordinary service stuff (route to the appropriate doX method).

public abstract class InterceptableServlet extends HttpServlet {
    private static final Map<String, String> methods =
            Map.of(
                    "GET", "doGet",
                    "POST", "doPost",
                    "PUT", "doPut",
                    "TRACE", "doTrace",
                    "OPTIONS", "doOptions",
                    "DELETE", "doDelete",
                    "HEAD", "doHead"
            );

    private final Map<String, HttpServletBiConsumer> chains = new HashMap<>();

    @Override
    public final void init(ServletConfig config) throws ServletException {
        for(Map.Entry<String,String> entry :  methods.entrySet()) {
            Method method = null;
            try {
                method = this.getClass().getMethod(entry.getValue(), HttpServletRequest.class, HttpServletResponse.class);
                chains.put(entry.getKey(), getInterceptors(method));
            } catch (NoSuchMethodException e) {
                chains.put(entry.getKey(), this::superService);
            }
        }
    }

    private HttpServletBiConsumer getInterceptors(Method method){
        ServletInterceptor[] servletInterceptors =
                Arrays.stream(method.getAnnotations())
                        .map(ServletInterceptorFactory::instantiate)
                        .toArray(ServletInterceptor[]::new);
        return buildChain(servletInterceptors, this::superService);
    }

    private HttpServletBiConsumer buildChain(ServletInterceptor<?>[] interceptors, HttpServletBiConsumer target){
        HttpServletBiConsumer current = target;
        for(int i = interceptors.length-1; i >= 0; i--){
            ServletInterceptor<?> interceptor = interceptors[i];

            HttpServletBiConsumer next = current;
            current = (req, res) -> interceptor.handle(req, res, next);
        }
        return current;
    }

    //there are two service methods and java 8 lambda mechanism complains about that, so here's the solution.
    private final void superService(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        super.service(req, resp);
    }

    @Override
    protected final void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        chains.get(req.getMethod()).handle(req,resp);
    }

    @Override
    public final void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
        service((HttpServletRequest) req, (HttpServletResponse) res);
    }

    @Override
    public final void init() throws ServletException {
        init(this.getServletConfig());
    }

}

So now I can create an Interceptor-aware servlet in this way:

@Target({ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface MyInterceptorAnnotation {
    String message();
}

public class MyInterceptor extends ServletInterceptor<MyInterceptorAnnotation> {
    public String message;

    @Override
    protected void init(MyInterceptorAnnotation annotation) {
        this.message = annotation.message();
    }

    @Override
    public void handle(HttpServletRequest req, HttpServletResponse resp, HttpServletBiConsumer next) throws ServletException, IOException {
        System.out.println(message);
        next.handle(req,resp);
    }
}


public class MyInterceptableServlet extends InterceptableServlet {
    @Override
    @MyInterceptorAnnotation(message = "hello world")
    protected void doGet(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        System.out.println("aloha");
    }
}

This looks quite over-engineered, doesn't it? However, the mechanism looks invisible enough when writing Servlet classes. Do you think there's some better way to implement it?

Edit: Just found out the existence of ServletContainerInitializer, which has the capability of scanning for all subclasses of a specified Class<> through the @HandlesTypes annotation. Gonna try it tomorrow.


2nd iteration

ServletContainerInitializer

@HandlesTypes(ServletInterceptor.class)
public class ServletContainerInitializer implements javax.servlet.ServletContainerInitializer {
    @Override
    public void onStartup(Set<Class<?>> set, ServletContext servletContext) {
        //noinspection unchecked
        set.stream().map(x -> (Class<? extends ServletInterceptor<?>>) x).forEach(ServletInterceptorFactory::register);
    }
}

Factory

public final class ServletInterceptorFactory {
    private ServletInterceptorFactory(){}

    private static final
    ConcurrentMap<Class<? extends Annotation>, Class<? extends ServletInterceptor<?>>> classes =
            new ConcurrentHashMap<>();

    public static <T extends Annotation> ServletInterceptor<T> instantiate(T annotation){
        Class<? extends ServletInterceptor<?>> clazz = ServletInterceptorFactory.classes.get(annotation.annotationType());
        if(clazz == null)
            return null;

        ServletInterceptor<T> interceptor;
        try {
            //noinspection unchecked
            interceptor = (ServletInterceptor<T>) clazz.getConstructor().newInstance();
        } catch (ReflectiveOperationException e) {
            throw new RuntimeException(e);
        }

        interceptor.init(annotation);
        return interceptor;
    }

    public static void register(Class<? extends ServletInterceptor<? extends Annotation>> interceptorClass){
        if(Modifier.isAbstract(interceptorClass.getModifiers())){
            throw new IllegalArgumentException("Interceptor class must be concrete");
        }
        if(interceptorClass.getTypeParameters().length > 0){
            throw new IllegalArgumentException("Interceptor class must be non-generic");
        }

        //get ServletInterceptor.class immediate subclass
        Class<?> clazz = interceptorClass;
        while(clazz.getSuperclass() != ServletInterceptor.class){
            clazz = clazz.getSuperclass();
        }
        //get ServletInterceptor.class as ParameterizedType object
        ParameterizedType genericSuperclass = (ParameterizedType) clazz.getGenericSuperclass();

        @SuppressWarnings("unchecked")
        Class<? extends Annotation> actualTypeArgument =
                (Class<? extends Annotation>) genericSuperclass.getActualTypeArguments()[0];

        Class<?> oldValue = classes.putIfAbsent(actualTypeArgument, interceptorClass);
        if(oldValue != null){
            throw new IllegalArgumentException("There already is a registered Servlet Interceptor with annotation "
                    + actualTypeArgument.getName());
        }
    }
}

InterceptableServlet

public abstract class InterceptableServlet extends HttpServlet {
    private static final Map<String, String> methods =
            Map.of(
                    "GET",     "doGet",
                    "POST",    "doPost",
                    "PUT",     "doPut",
                    "TRACE",   "doTrace",
                    "OPTIONS", "doOptions",
                    "DELETE",  "doDelete",
                    "HEAD",    "doHead"
            );

    private final Map<String, HttpServletBiConsumer> chains = new HashMap<>();
    private boolean chainInitialized = false;

    private void initChain(){
        methods.forEach((httpMethod,servletMethod)
                -> chains.put( httpMethod, buildChain(getInterceptors(servletMethod), this::superService)) );
        chainInitialized = true;
    }

    private ServletInterceptor<?>[] getInterceptors(String methodName) {
        //Not a "doX" method
        if(!methods.containsValue(methodName)){
            throw new IllegalArgumentException();
        }

        Method method;
        try {
            method = this.getClass()
                    .getDeclaredMethod(methodName, HttpServletRequest.class, HttpServletResponse.class);
        } catch (NoSuchMethodException e) {
            //method has not been overriden by this class.
            //return empty array
            return new ServletInterceptor[]{};
        }

        //get annotations and their respective interceptor
        return Arrays.stream(method.getAnnotations())
                .map(ServletInterceptorFactory::instantiate)
                .filter(Objects::nonNull)
                .toArray(ServletInterceptor[]::new);
    }

    private HttpServletBiConsumer buildChain(ServletInterceptor<?>[] interceptors, HttpServletBiConsumer target){
        interceptors = interceptors == null ? new ServletInterceptor[]{} : interceptors;

        HttpServletBiConsumer current = target;
        for(int i = interceptors.length-1; i >= 0; i--){
            ServletInterceptor<?> interceptor = interceptors[i];

            HttpServletBiConsumer next = current;
            current = (req, res) -> interceptor.handle(req, res, next);
        }
        return current;
    }

    //unambiguous super.service (there are 2 service methods! java lambdas hate that.)
    private void superService(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        super.service(req, resp);
    }

    @Override
    protected final void service(HttpServletRequest req, HttpServletResponse resp) throws ServletException, IOException {
        if(!chainInitialized)
            initChain();
        chains.get(req.getMethod()).handle(req,resp);
    }

    @Override
    public final void service(ServletRequest req, ServletResponse res) throws ServletException, IOException {
        service((HttpServletRequest) req, (HttpServletResponse) res);
    }

}
\$\endgroup\$

Your Answer

By clicking “Post Your Answer”, you agree to our terms of service, privacy policy and cookie policy

Browse other questions tagged or ask your own question.