Committing where I'm at even though I've discovered that jax.rs would be a better approach
diff --git a/org.apache.sling.servlets.json/pom.xml b/org.apache.sling.servlets.json/pom.xml
index 6c42b60..7c5b214 100644
--- a/org.apache.sling.servlets.json/pom.xml
+++ b/org.apache.sling.servlets.json/pom.xml
@@ -44,6 +44,14 @@
             <plugin>
                 <groupId>biz.aQute.bnd</groupId>
                 <artifactId>bnd-maven-plugin</artifactId>
+                <configuration>
+                    <bnd>
+                        <![CDATA[
+                        -fixupmessages "Classes found in the wrong directory"; restrict:=error; is:=warning
+                        -includeresource: glob-[0-9\.]*.jar;lib:=true
+                    ]]>
+                    </bnd>
+                </configuration>
             </plugin>
             <plugin>
                 <groupId>org.jacoco</groupId>
@@ -162,6 +170,13 @@
             <groupId>org.osgi</groupId>
             <artifactId>org.osgi.annotation.versioning</artifactId>
         </dependency>
+
+        <dependency>
+            <groupId>com.hrakaroo</groupId>
+            <artifactId>glob</artifactId>
+            <version>0.9.0</version>
+        </dependency>
+
         <dependency>
             <groupId>org.junit.jupiter</groupId>
             <artifactId>junit-jupiter</artifactId>
diff --git a/org.apache.sling.servlets.json/src/main/java/org/apache/sling/servlets/json/BaseJsonServlet.java b/org.apache.sling.servlets.json/src/main/java/org/apache/sling/servlets/json/BaseJsonServlet.java
index 9932510..e63c12f 100644
--- a/org.apache.sling.servlets.json/src/main/java/org/apache/sling/servlets/json/BaseJsonServlet.java
+++ b/org.apache.sling.servlets.json/src/main/java/org/apache/sling/servlets/json/BaseJsonServlet.java
@@ -20,8 +20,6 @@
 import java.util.Optional;
 import java.util.Set;
 
-import javax.servlet.ServletException;
-import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
@@ -30,11 +28,8 @@
 import org.apache.sling.auth.core.AuthenticationSupport;
 import org.apache.sling.servlets.json.problem.Problem;
 import org.apache.sling.servlets.json.problem.ProblemBuilder;
-import org.apache.sling.servlets.json.problem.Problematic;
 import org.jetbrains.annotations.NotNull;
 import org.osgi.annotation.versioning.ConsumerType;
-import org.slf4j.Logger;
-import org.slf4j.LoggerFactory;
 
 import com.fasterxml.jackson.core.type.TypeReference;
 
@@ -50,129 +45,13 @@
  * Problem response based on the thrown exception
  */
 @ConsumerType
-public abstract class BaseJsonServlet extends HttpServlet {
+public interface BaseJsonServlet {
 
-    private static final String RESPONSE_CONTENT_TYPE = "application/json";
+    static final String RESPONSE_CONTENT_TYPE = "application/json";
 
-    private static final Set<String> SERVLET_SUPPORTED_METHODS = Set.of("GET", "HEAD", "POST", "PUT", "DELETE",
+    static final Set<String> SERVLET_SUPPORTED_METHODS = Set.of("GET", "HEAD", "POST", "PUT", "DELETE",
             "OPTIONS", "TRACE");
 
-    private static final Logger log = LoggerFactory.getLogger(BaseJsonServlet.class);
-
-    /**
-     * Called by the
-     * {@link #service(HttpServletRequest, HttpServletResponse)} method
-     * to handle an HTTP <em>GET</em> request.
-     * <p>
-     * This default implementation reports back to the client that the method is
-     * not supported.
-     * <p>
-     * Implementations of this class should overwrite this method with their
-     * implementation for the HTTP <em>PATCH</em> method support.
-     *
-     * @param request  The HTTP request
-     * @param response The HTTP response
-     * @throws ServletException Not thrown by this implementation.
-     * @throws IOException      If the error status cannot be reported back to the
-     *                          client.
-     */
-    @Override
-    protected void doGet(final HttpServletRequest req, final HttpServletResponse resp)
-            throws ServletException, IOException {
-        sendProblemResponse(resp, ProblemBuilder.get().withStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED).build());
-    }
-
-    /**
-     * Called by the
-     * {@link #service(HttpServletRequest, HttpServletResponse)} method
-     * to handle an HTTP <em>POST</em> request.
-     * <p>
-     * This default implementation reports back to the client that the method is
-     * not supported.
-     * <p>
-     * Implementations of this class should overwrite this method with their
-     * implementation for the HTTP <em>PATCH</em> method support.
-     *
-     * @param request  The HTTP request
-     * @param response The HTTP response
-     * @throws ServletException Not thrown by this implementation.
-     * @throws IOException      If the error status cannot be reported back to the
-     *                          client.
-     */
-    @Override
-    protected void doPost(final HttpServletRequest req, final HttpServletResponse resp)
-            throws ServletException, IOException {
-        sendProblemResponse(resp, ProblemBuilder.get().withStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED).build());
-    }
-
-    /**
-     * Called by the
-     * {@link #service(HttpServletRequest, HttpServletResponse)} method
-     * to handle an HTTP <em>PUT</em> request.
-     * <p>
-     * This default implementation reports back to the client that the method is
-     * not supported.
-     * <p>
-     * Implementations of this class should overwrite this method with their
-     * implementation for the HTTP <em>PATCH</em> method support.
-     *
-     * @param request  The HTTP request
-     * @param response The HTTP response
-     * @throws ServletException Not thrown by this implementation.
-     * @throws IOException      If the error status cannot be reported back to the
-     *                          client.
-     */
-    @Override
-    protected void doPut(final HttpServletRequest req, final HttpServletResponse resp)
-            throws ServletException, IOException {
-        sendProblemResponse(resp, ProblemBuilder.get().withStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED).build());
-    }
-
-    /**
-     * Called by the
-     * {@link #service(HttpServletRequest, HttpServletResponse)} method
-     * to handle an HTTP <em>DELETE</em> request.
-     * <p>
-     * This default implementation reports back to the client that the method is
-     * not supported.
-     * <p>
-     * Implementations of this class should overwrite this method with their
-     * implementation for the HTTP <em>PATCH</em> method support.
-     *
-     * @param request  The HTTP request
-     * @param response The HTTP response
-     * @throws ServletException Not thrown by this implementation.
-     * @throws IOException      If the error status cannot be reported back to the
-     *                          client.
-     */
-    @Override
-    protected void doDelete(final HttpServletRequest req, final HttpServletResponse resp)
-            throws ServletException, IOException {
-        sendProblemResponse(resp, ProblemBuilder.get().withStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED).build());
-    }
-
-    /**
-     * Called by the
-     * {@link #service(HttpServletRequest, HttpServletResponse)} method
-     * to handle an HTTP <em>PATCH</em> request.
-     * <p>
-     * This default implementation reports back to the client that the method is
-     * not supported.
-     * <p>
-     * Implementations of this class should overwrite this method with their
-     * implementation for the HTTP <em>PATCH</em> method support.
-     *
-     * @param request  The HTTP request
-     * @param response The HTTP response
-     * @throws ServletException Not thrown by this implementation.
-     * @throws IOException      If the error status cannot be reported back to the
-     *                          client.
-     */
-    protected void doPatch(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response)
-            throws ServletException, IOException {
-        handleMethodNotImplemented(request, response);
-    }
-
     /**
      * Retrieves a <code>ResourceResolver</code> that can be used to perform various
      * operations against the underlying repository.
@@ -180,56 +59,22 @@
      * @return Resolver for performing operations. Will not be null.
      * @throws LoginException unable to find resource resolver in request
      */
-    public @NotNull ResourceResolver getResourceResolver(@NotNull HttpServletRequest request) throws LoginException {
+    default @NotNull ResourceResolver getResourceResolver(@NotNull HttpServletRequest request) throws LoginException {
         return Optional.ofNullable(request.getAttribute(AuthenticationSupport.REQUEST_ATTRIBUTE_RESOLVER))
                 .map(ResourceResolver.class::cast)
                 .orElseThrow(() -> new LoginException("Could not get ResourceResolver from request"));
     }
 
     /**
-     * Tries to handle the request by calling a Java method implemented for the
-     * respective HTTP request method.
-     * <p>
-     * This implementation first calls the base class implementation and only if
-     * the base class cannot dispatch will try to dispatch the supported methods
-     * <em>PATCH</em>
-     * <p>
-     * In addition, this method catches ServletException, IOException and
-     * RuntimeExceptions thrown from the called methods and sends a JSON
-     * Problem response based on the thrown exception
+     * Read an object from the request, handing invalid or missing request bodies
+     * and returning a 400 response.
      *
-     * @param request  The HTTP request
-     * @param response The HTTP response
-     * @return <code>true</code> if the requested method
-     *         (<code>request.getMethod()</code>)
-     *         is known. Otherwise <code>false</code> is returned.
-     * @throws ServletException Forwarded from any of the dispatched methods
-     * @throws IOException      Forwarded from any of the dispatched methods
+     * @param <T>     the type of object to be read from the request
+     * @param request the request from which to read the object
+     * @param type    the class of the type to read
+     * @return the object read from the request
      */
-    @Override
-    protected void service(@NotNull HttpServletRequest request,
-            @NotNull HttpServletResponse response) throws ServletException,
-            IOException {
-        final String method = request.getMethod();
-        try {
-            // assume the method is known for now
-            if (SERVLET_SUPPORTED_METHODS.contains(method)) {
-                super.service(request, response);
-            } else if ("PATCH".equals(method)) {
-                doPatch(request, response);
-            } else {
-                handleMethodNotImplemented(request, response);
-            }
-        } catch (IOException | ServletException | RuntimeException e) {
-            if (e instanceof Problematic) {
-                sendProblemResponse(response, ((Problematic) e).getProblem());
-            } else {
-                log.error("Handing uncaught exception", e);
-                sendProblemResponse(response, ProblemBuilder.get().fromException(e).build());
-            }
-        }
-
-    }
+    <T> T readRequestBody(HttpServletRequest request, Class<T> type);
 
     /**
      * Read an object from the request, handing invalid or missing request bodies
@@ -240,18 +85,7 @@
      * @param type    the class of the type to read
      * @return the object read from the request
      */
-    protected abstract <T> T readRequestBody(HttpServletRequest request, Class<T> type);
-
-    /**
-     * Read an object from the request, handing invalid or missing request bodies
-     * and returning a 400 response.
-     *
-     * @param <T>     the type of object to be read from the request
-     * @param request the request from which to read the object
-     * @param type    the class of the type to read
-     * @return the object read from the request
-     */
-    protected abstract <T> T readRequestBody(HttpServletRequest request, TypeReference<T> type);
+    <T> T readRequestBody(HttpServletRequest request, TypeReference<T> type);
 
     /**
      * Sends a JSON response with the content type application/json and a 200 status
@@ -261,7 +95,7 @@
      * @param responseBody the object to write to the response
      * @throws IOException an exception occurs writing the object to the response
      */
-    protected void sendJsonResponse(HttpServletResponse response, Object responseBody)
+    default void sendJsonResponse(HttpServletResponse response, Object responseBody)
             throws IOException {
         sendJsonResponse(response, HttpServletResponse.SC_OK, responseBody);
     }
@@ -274,7 +108,7 @@
      * @param responseBody the object to write to the response
      * @throws IOException an exception occurs writing the object to the response
      */
-    protected void sendJsonResponse(HttpServletResponse response, int statusCode, Object responseBody)
+    default void sendJsonResponse(HttpServletResponse response, int statusCode, Object responseBody)
             throws IOException {
         sendJsonResponse(response, statusCode, RESPONSE_CONTENT_TYPE, responseBody);
     }
@@ -288,7 +122,7 @@
      * @param responseBody the object to write to the response
      * @throws IOException an exception occurs writing the object to the response
      */
-    protected abstract void sendJsonResponse(HttpServletResponse response, int statusCode, String contentType,
+    void sendJsonResponse(HttpServletResponse response, int statusCode, String contentType,
             Object responseBody) throws IOException;
 
     /**
@@ -300,7 +134,7 @@
      * @param problemBuilder the problem to write
      * @throws IOException Thrown if the problem cannot be written to the response
      */
-    protected void sendProblemResponse(HttpServletResponse response, Problem problem)
+    default void sendProblemResponse(HttpServletResponse response, Problem problem)
             throws IOException {
         sendJsonResponse(response, problem.getStatus(), ProblemBuilder.RESPONSE_CONTENT_TYPE,
                 problem);
@@ -314,7 +148,7 @@
      * @param response The HTTP response to which the error status is sent.
      * @throws IOException Thrown if the status cannot be sent to the client.
      */
-    protected void handleMethodNotImplemented(@NotNull HttpServletRequest request,
+    default void handleMethodNotImplemented(@NotNull HttpServletRequest request,
             @NotNull HttpServletResponse response) throws IOException {
         sendProblemResponse(response,
                 ProblemBuilder.get().withStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED).build());
diff --git a/org.apache.sling.servlets.json/src/main/java/org/apache/sling/servlets/json/DynamicRequestServlet.java b/org.apache.sling.servlets.json/src/main/java/org/apache/sling/servlets/json/DynamicRequestServlet.java
new file mode 100644
index 0000000..55d7f9f
--- /dev/null
+++ b/org.apache.sling.servlets.json/src/main/java/org/apache/sling/servlets/json/DynamicRequestServlet.java
@@ -0,0 +1,82 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.servlets.json;
+
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.sling.servlets.json.dynamicrequest.DynamicRequestMapper;
+import org.apache.sling.servlets.json.problem.ProblemBuilder;
+import org.apache.sling.servlets.json.problem.Problematic;
+import org.jetbrains.annotations.NotNull;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class DynamicRequestServlet extends JacksonJsonServlet {
+
+    private static final Logger log = LoggerFactory.getLogger(DynamicRequestServlet.class);
+
+    private final DynamicRequestMapper mapper;
+
+    public DynamicRequestServlet() {
+        mapper = new DynamicRequestMapper(this);
+    }
+
+    /**
+     * Tries to handle the request by calling a Java method implemented for the
+     * respective HTTP request method.
+     * <p>
+     * This implementation first attempts to resolve handling methods by
+     * identifying methods with the @RequestHandler annotation which
+     * apply to the provided request. Any such request handler
+     * <p>
+     * In addition, this method catches ServletException, IOException and
+     * RuntimeExceptions thrown from the called methods and sends a JSON
+     * Problem response based on the thrown exception
+     *
+     * @param request  The HTTP request
+     * @param response The HTTP response
+     * @return <code>true</code> if the requested method
+     *         (<code>request.getMethod()</code>)
+     *         is known. Otherwise <code>false</code> is returned.
+     * @throws ServletException Forwarded from any of the dispatched methods
+     * @throws IOException      Forwarded from any of the dispatched methods
+     */
+    @Override
+    protected void service(@NotNull HttpServletRequest request,
+            @NotNull HttpServletResponse response) throws ServletException,
+            IOException {
+        boolean serviced = false;
+        try {
+            serviced = mapper.mayService(request, response);
+            if (!serviced) {
+                super.service(request, response);
+            }
+        } catch (IOException | ServletException | RuntimeException e) {
+            if (e instanceof Problematic) {
+                sendProblemResponse(response, ((Problematic) e).getProblem());
+            } else {
+                log.error("Handing uncaught exception", e);
+                sendProblemResponse(response, ProblemBuilder.get().fromException(e).build());
+            }
+        }
+    }
+
+}
diff --git a/org.apache.sling.servlets.json/src/main/java/org/apache/sling/servlets/json/JacksonJsonServlet.java b/org.apache.sling.servlets.json/src/main/java/org/apache/sling/servlets/json/JacksonJsonServlet.java
index d040274..a220f9a 100644
--- a/org.apache.sling.servlets.json/src/main/java/org/apache/sling/servlets/json/JacksonJsonServlet.java
+++ b/org.apache.sling.servlets.json/src/main/java/org/apache/sling/servlets/json/JacksonJsonServlet.java
@@ -19,10 +19,14 @@
 import java.io.IOException;
 import java.nio.charset.StandardCharsets;
 
+import javax.servlet.ServletException;
+import javax.servlet.http.HttpServlet;
 import javax.servlet.http.HttpServletRequest;
 import javax.servlet.http.HttpServletResponse;
 
 import org.apache.sling.servlets.json.problem.ProblemBuilder;
+import org.apache.sling.servlets.json.problem.Problematic;
+import org.jetbrains.annotations.NotNull;
 import org.osgi.annotation.versioning.ConsumerType;
 import org.slf4j.Logger;
 import org.slf4j.LoggerFactory;
@@ -36,7 +40,7 @@
  * An extension of the BaseJsonServlet using Jackson for serialization.
  */
 @ConsumerType
-public abstract class JacksonJsonServlet extends BaseJsonServlet {
+public abstract class JacksonJsonServlet extends HttpServlet implements BaseJsonServlet {
 
     private static final Logger log = LoggerFactory.getLogger(JacksonJsonServlet.class);
 
@@ -45,6 +49,164 @@
     private static final ObjectReader objectReader = objectMapper.reader();
 
     /**
+     * Called by the
+     * {@link #service(HttpServletRequest, HttpServletResponse)} method
+     * to handle an HTTP <em>GET</em> request.
+     * <p>
+     * This default implementation reports back to the client that the method is
+     * not supported.
+     * <p>
+     * Implementations of this class should overwrite this method with their
+     * implementation for the HTTP <em>PATCH</em> method support.
+     *
+     * @param request  The HTTP request
+     * @param response The HTTP response
+     * @throws ServletException Not thrown by this implementation.
+     * @throws IOException      If the error status cannot be reported back to the
+     *                          client.
+     */
+    @Override
+    protected void doGet(final HttpServletRequest req, final HttpServletResponse resp)
+            throws ServletException, IOException {
+        sendProblemResponse(resp, ProblemBuilder.get().withStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED).build());
+    }
+
+    /**
+     * Called by the
+     * {@link #service(HttpServletRequest, HttpServletResponse)} method
+     * to handle an HTTP <em>POST</em> request.
+     * <p>
+     * This default implementation reports back to the client that the method is
+     * not supported.
+     * <p>
+     * Implementations of this class should overwrite this method with their
+     * implementation for the HTTP <em>PATCH</em> method support.
+     *
+     * @param request  The HTTP request
+     * @param response The HTTP response
+     * @throws ServletException Not thrown by this implementation.
+     * @throws IOException      If the error status cannot be reported back to the
+     *                          client.
+     */
+    @Override
+    protected void doPost(final HttpServletRequest req, final HttpServletResponse resp)
+            throws ServletException, IOException {
+        sendProblemResponse(resp, ProblemBuilder.get().withStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED).build());
+    }
+
+    /**
+     * Called by the
+     * {@link #service(HttpServletRequest, HttpServletResponse)} method
+     * to handle an HTTP <em>PUT</em> request.
+     * <p>
+     * This default implementation reports back to the client that the method is
+     * not supported.
+     * <p>
+     * Implementations of this class should overwrite this method with their
+     * implementation for the HTTP <em>PATCH</em> method support.
+     *
+     * @param request  The HTTP request
+     * @param response The HTTP response
+     * @throws ServletException Not thrown by this implementation.
+     * @throws IOException      If the error status cannot be reported back to the
+     *                          client.
+     */
+    @Override
+    protected void doPut(final HttpServletRequest req, final HttpServletResponse resp)
+            throws ServletException, IOException {
+        sendProblemResponse(resp, ProblemBuilder.get().withStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED).build());
+    }
+
+    /**
+     * Called by the
+     * {@link #service(HttpServletRequest, HttpServletResponse)} method
+     * to handle an HTTP <em>DELETE</em> request.
+     * <p>
+     * This default implementation reports back to the client that the method is
+     * not supported.
+     * <p>
+     * Implementations of this class should overwrite this method with their
+     * implementation for the HTTP <em>PATCH</em> method support.
+     *
+     * @param request  The HTTP request
+     * @param response The HTTP response
+     * @throws ServletException Not thrown by this implementation.
+     * @throws IOException      If the error status cannot be reported back to the
+     *                          client.
+     */
+    @Override
+    protected void doDelete(final HttpServletRequest req, final HttpServletResponse resp)
+            throws ServletException, IOException {
+        sendProblemResponse(resp, ProblemBuilder.get().withStatus(HttpServletResponse.SC_METHOD_NOT_ALLOWED).build());
+    }
+
+    /**
+     * Called by the
+     * {@link #service(HttpServletRequest, HttpServletResponse)} method
+     * to handle an HTTP <em>PATCH</em> request.
+     * <p>
+     * This default implementation reports back to the client that the method is
+     * not supported.
+     * <p>
+     * Implementations of this class should overwrite this method with their
+     * implementation for the HTTP <em>PATCH</em> method support.
+     *
+     * @param request  The HTTP request
+     * @param response The HTTP response
+     * @throws ServletException Not thrown by this implementation.
+     * @throws IOException      If the error status cannot be reported back to the
+     *                          client.
+     */
+    protected void doPatch(@NotNull HttpServletRequest request, @NotNull HttpServletResponse response)
+            throws ServletException, IOException {
+        handleMethodNotImplemented(request, response);
+    }
+
+    /**
+     * Tries to handle the request by calling a Java method implemented for the
+     * respective HTTP request method.
+     * <p>
+     * This implementation first calls the base class implementation and only if
+     * the base class cannot dispatch will try to dispatch the supported methods
+     * <em>PATCH</em>
+     * <p>
+     * In addition, this method catches ServletException, IOException and
+     * RuntimeExceptions thrown from the called methods and sends a JSON
+     * Problem response based on the thrown exception
+     *
+     * @param request  The HTTP request
+     * @param response The HTTP response
+     * @return <code>true</code> if the requested method
+     *         (<code>request.getMethod()</code>)
+     *         is known. Otherwise <code>false</code> is returned.
+     * @throws ServletException Forwarded from any of the dispatched methods
+     * @throws IOException      Forwarded from any of the dispatched methods
+     */
+    @Override
+    protected void service(@NotNull HttpServletRequest request,
+            @NotNull HttpServletResponse response) throws ServletException,
+            IOException {
+        final String method = request.getMethod();
+        try {
+            // assume the method is known for now
+            if (SERVLET_SUPPORTED_METHODS.contains(method)) {
+                super.service(request, response);
+            } else if ("PATCH".equals(method)) {
+                doPatch(request, response);
+            } else {
+                handleMethodNotImplemented(request, response);
+            }
+        } catch (IOException | ServletException | RuntimeException e) {
+            if (e instanceof Problematic) {
+                sendProblemResponse(response, ((Problematic) e).getProblem());
+            } else {
+                log.error("Handing uncaught exception", e);
+                sendProblemResponse(response, ProblemBuilder.get().fromException(e).build());
+            }
+        }
+    }
+
+    /**
      * Provides the Jackson ObjectWriter instance to use for writing objects to the
      * response.
      * <p>
@@ -53,7 +215,7 @@
      *
      * @return the ObjectWriter
      */
-    protected ObjectWriter getObjectWriter() {
+    public ObjectWriter getObjectWriter() {
         return objectWriter;
     }
 
@@ -66,7 +228,7 @@
      *
      * @return the ObjectReader
      */
-    protected ObjectReader getObjectReader() {
+    public ObjectReader getObjectReader() {
         return objectReader;
     }
 
@@ -80,7 +242,7 @@
      * @return the object read from the request
      */
     @Override
-    protected <T> T readRequestBody(HttpServletRequest request, Class<T> type) {
+    public <T> T readRequestBody(HttpServletRequest request, Class<T> type) {
         try {
             return getObjectReader().readValue(request.getReader(), type);
         } catch (IOException e) {
@@ -99,7 +261,7 @@
      * @return the object read from the request
      */
     @Override
-    protected <T> T readRequestBody(HttpServletRequest request, TypeReference<T> type) {
+    public <T> T readRequestBody(HttpServletRequest request, TypeReference<T> type) {
         try {
             return getObjectReader().forType(type).readValue(request.getReader());
         } catch (IOException e) {
@@ -118,7 +280,7 @@
      * @throws IOException an exception occurs writing the object to the response
      */
     @Override
-    protected void sendJsonResponse(HttpServletResponse response, int statusCode, String contentType,
+    public void sendJsonResponse(HttpServletResponse response, int statusCode, String contentType,
             Object responseBody) throws IOException {
         if (!response.isCommitted()) {
             response.reset();
diff --git a/org.apache.sling.servlets.json/src/main/java/org/apache/sling/servlets/json/annotations/RequestBody.java b/org.apache.sling.servlets.json/src/main/java/org/apache/sling/servlets/json/annotations/RequestBody.java
new file mode 100644
index 0000000..2f6980a
--- /dev/null
+++ b/org.apache.sling.servlets.json/src/main/java/org/apache/sling/servlets/json/annotations/RequestBody.java
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.servlets.json.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.PARAMETER)
+public @interface RequestBody {
+
+}
diff --git a/org.apache.sling.servlets.json/src/main/java/org/apache/sling/servlets/json/annotations/RequestHandler.java b/org.apache.sling.servlets.json/src/main/java/org/apache/sling/servlets/json/annotations/RequestHandler.java
new file mode 100644
index 0000000..36335b7
--- /dev/null
+++ b/org.apache.sling.servlets.json/src/main/java/org/apache/sling/servlets/json/annotations/RequestHandler.java
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.servlets.json.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.METHOD)
+public @interface RequestHandler {
+
+    String[] methods();
+
+    String path();
+
+}
diff --git a/org.apache.sling.servlets.json/src/main/java/org/apache/sling/servlets/json/annotations/RequestParameter.java b/org.apache.sling.servlets.json/src/main/java/org/apache/sling/servlets/json/annotations/RequestParameter.java
new file mode 100644
index 0000000..78eb5b9
--- /dev/null
+++ b/org.apache.sling.servlets.json/src/main/java/org/apache/sling/servlets/json/annotations/RequestParameter.java
@@ -0,0 +1,30 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.servlets.json.annotations;
+
+import java.lang.annotation.ElementType;
+import java.lang.annotation.Retention;
+import java.lang.annotation.RetentionPolicy;
+import java.lang.annotation.Target;
+
+@Retention(RetentionPolicy.RUNTIME)
+@Target(ElementType.PARAMETER)
+public @interface RequestParameter {
+
+    String name();
+
+}
diff --git a/org.apache.sling.servlets.json/src/main/java/org/apache/sling/servlets/json/dynamicrequest/DynamicRequestMapper.java b/org.apache.sling.servlets.json/src/main/java/org/apache/sling/servlets/json/dynamicrequest/DynamicRequestMapper.java
new file mode 100644
index 0000000..b5f4e3b
--- /dev/null
+++ b/org.apache.sling.servlets.json/src/main/java/org/apache/sling/servlets/json/dynamicrequest/DynamicRequestMapper.java
@@ -0,0 +1,183 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.servlets.json.dynamicrequest;
+
+import java.io.IOException;
+import java.lang.reflect.InvocationTargetException;
+import java.lang.reflect.Method;
+import java.lang.reflect.Parameter;
+import java.util.ArrayList;
+import java.util.Arrays;
+import java.util.Collection;
+import java.util.Collections;
+import java.util.HashMap;
+import java.util.HashSet;
+import java.util.List;
+import java.util.Map;
+import java.util.Optional;
+import java.util.Set;
+import java.util.stream.Collectors;
+
+import javax.servlet.ServletException;
+import javax.servlet.ServletRequest;
+import javax.servlet.ServletResponse;
+import javax.servlet.http.HttpServlet;
+import javax.servlet.http.HttpServletRequest;
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.sling.api.resource.ValueMap;
+import org.apache.sling.api.wrappers.ValueMapDecorator;
+import org.apache.sling.servlets.json.DynamicRequestServlet;
+import org.apache.sling.servlets.json.annotations.RequestBody;
+import org.apache.sling.servlets.json.annotations.RequestHandler;
+import org.apache.sling.servlets.json.annotations.RequestParameter;
+import org.slf4j.Logger;
+import org.slf4j.LoggerFactory;
+
+public class DynamicRequestMapper {
+
+    private static final Logger log = LoggerFactory.getLogger(DynamicRequestMapper.class);
+
+    private final Set<String> mappingKeys = new HashSet<>();
+    private final Map<String, List<DynamicRequestMapping>> mappings = new HashMap<>();
+    private final DynamicRequestServlet instance;
+
+    public DynamicRequestMapper(DynamicRequestServlet instance) {
+        readRequestHandlers(instance);
+        this.instance = instance;
+    }
+
+    public boolean mayService(HttpServletRequest request, HttpServletResponse response)
+            throws IOException, ServletException {
+        final String method = request.getMethod();
+        final String path = request.getRequestURI();
+        Optional<DynamicRequestMapping> mappingOp = Optional.ofNullable(mappings.get(method))
+                .orElse(Collections.emptyList()).stream().filter(drm -> drm.matches(path)).findFirst();
+        if (mappingOp.isPresent()) {
+            callMethod(request, response, mappingOp.get());
+            return true;
+        } else {
+            return false;
+        }
+    }
+
+    private void callMethod(final HttpServletRequest request, final HttpServletResponse response,
+            final DynamicRequestMapping dynamicRequestMapping) throws IOException, ServletException {
+        final Method method = dynamicRequestMapping.getMethod();
+        final List<Object> values = new ArrayList<>();
+        final ValueMap parameters = new ValueMapDecorator(request.getParameterMap());
+        for (Parameter param : method.getParameters()) {
+            if (ServletRequest.class.isAssignableFrom(param.getType())) {
+                log.trace("Adding request for param: {}", param);
+                values.add(request);
+            } else if (ServletResponse.class.isAssignableFrom(param.getType())) {
+                log.trace("Adding response for param: {}", param);
+                values.add(response);
+            } else if (param.isAnnotationPresent(RequestBody.class)) {
+                log.debug("Adding response body as {} for param: {}", param.getType(), param);
+                values.add(instance.readRequestBody(request, param.getType()));
+            } else if (param.isAnnotationPresent(RequestParameter.class)) {
+                RequestParameter rp = param.getAnnotation(RequestParameter.class);
+                log.debug("Adding response parameter {} as {} for param: {}", rp.name(), param.getType(), param);
+                values.add(parameters.get(rp.name(), param.getType()));
+            } else {
+                throw new RequestMappingException("Failed to call : " + dynamicRequestMapping
+                        + " parameter " + param.getName()
+                        + " must either be a ServletRequest, ServletResponse or be annotated with request value mappings");
+            }
+        }
+
+        try {
+            log.trace("Invoking method {} with parameters {}", method, values);
+            Object value = method.invoke(instance, values.toArray());
+            if (value != null) {
+                log.trace("Recieved response {}", value);
+                instance.sendJsonResponse(response, value);
+            }
+        } catch (IllegalAccessException | IllegalArgumentException e) {
+            throw new RequestMappingException("Unexpected exception invoking method " + method.toGenericString(), e);
+        } catch (InvocationTargetException e) {
+            Throwable cause = e.getCause();
+            if (cause instanceof RuntimeException) {
+                throw (RuntimeException) cause;
+            }
+            if (cause instanceof IOException) {
+                throw (IOException) cause;
+            }
+            if (cause instanceof ServletException) {
+                throw (ServletException) cause;
+            }
+            throw new RequestMappingException("Unexpected exception invoking method " + method.toGenericString(), e);
+        }
+    }
+
+    private void readRequestHandlers(HttpServlet instance) {
+        log.debug("Loading request handlers from: {}", instance.getClass());
+        for (Method method : instance.getClass().getDeclaredMethods()) {
+            log.trace("Evaluating method: {}", method);
+            RequestHandler handler = method.getAnnotation(RequestHandler.class);
+            if (handler != null) {
+                readRequestHandler(handler, method);
+            }
+        }
+    }
+
+    private void readRequestHandler(RequestHandler handler, Method method) {
+        log.trace("Found request handler {} in method: {}", handler, method);
+        DynamicRequestMapping mapping = new DynamicRequestMapping(handler, method);
+        validateMapping(mapping);
+        Arrays.stream(handler.methods()).forEach(m -> {
+
+            log.trace("Adding request handler {} for method: {}", mapping, m);
+            mappings.computeIfAbsent(m, k -> new ArrayList<>());
+            mappings.get(m).add(mapping);
+        });
+    }
+
+    private Collection<String> getMappingKeys(RequestHandler handler) {
+        return Arrays.stream(handler.methods()).map(m -> '[' + m + "] " + handler.path()).collect(Collectors.toList());
+    }
+
+    private void validateMapping(DynamicRequestMapping mapping) {
+        // validate that there aren't duplicate request handlers
+        log.trace("Validing that requst handler is duplicate");
+        getMappingKeys(mapping.getHandler()).forEach(kp -> {
+            if (mappingKeys.contains(kp)) {
+                throw new RequestMappingException("Validation failed for method: " + mapping.getMethod()
+                        + " a request handler is already registred for " + kp);
+            }
+            mappingKeys.add(kp);
+        });
+
+        log.trace("Validing request handler can be injecteds");
+        validateCanInject(mapping.getMethod());
+    }
+
+    private void validateCanInject(Method method) {
+        for (Parameter param : method.getParameters()) {
+            if (!ServletRequest.class.isAssignableFrom(param.getType())
+                    && !ServletResponse.class.isAssignableFrom(param.getType())
+                    && !param.isAnnotationPresent(RequestBody.class)
+                    && !param.isAnnotationPresent(RequestParameter.class)) {
+                throw new RequestMappingException("Validation failed for method: " + method
+                        + " parameter " + param.getName()
+                        + " must either be a ServletRequest, ServletResponse or be annotated with request value mappings");
+            }
+        }
+    }
+
+}
diff --git a/org.apache.sling.servlets.json/src/main/java/org/apache/sling/servlets/json/dynamicrequest/DynamicRequestMapping.java b/org.apache.sling.servlets.json/src/main/java/org/apache/sling/servlets/json/dynamicrequest/DynamicRequestMapping.java
new file mode 100644
index 0000000..7c521c8
--- /dev/null
+++ b/org.apache.sling.servlets.json/src/main/java/org/apache/sling/servlets/json/dynamicrequest/DynamicRequestMapping.java
@@ -0,0 +1,76 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.servlets.json.dynamicrequest;
+
+import java.lang.reflect.Method;
+
+import org.apache.sling.servlets.json.annotations.RequestHandler;
+
+import com.hrakaroo.glob.GlobPattern;
+import com.hrakaroo.glob.MatchingEngine;
+
+public class DynamicRequestMapping {
+
+    private final RequestHandler handler;
+    private final Method method;
+    private final MatchingEngine matcher;
+
+    /**
+     * @param handler
+     * @param method
+     */
+    public DynamicRequestMapping(RequestHandler handler, Method method) {
+        this.handler = handler;
+        this.method = method;
+        matcher = GlobPattern.compile(handler.path());
+    }
+
+    /**
+     * @return the handler
+     */
+    public RequestHandler getHandler() {
+        return handler;
+    }
+
+    /**
+     * @return the method
+     */
+    public Method getMethod() {
+        return method;
+    }
+
+    /**
+     * If true, this DynamicRequestMapping instance matches the request path
+     *
+     * @param requestPath the request path for this request
+     * @return whether or not the request path matches
+     */
+    public boolean matches(String requestPath) {
+        return matcher.matches(requestPath);
+    }
+
+    /*
+     * (non-Javadoc)
+     *
+     * @see java.lang.Object#toString()
+     */
+    @Override
+    public String toString() {
+        return "DynamicRequestMapping [handler=" + handler + ", method=" + method + "]";
+    }
+
+}
diff --git a/org.apache.sling.servlets.json/src/main/java/org/apache/sling/servlets/json/dynamicrequest/RequestMappingException.java b/org.apache.sling.servlets.json/src/main/java/org/apache/sling/servlets/json/dynamicrequest/RequestMappingException.java
new file mode 100644
index 0000000..3d9bdce
--- /dev/null
+++ b/org.apache.sling.servlets.json/src/main/java/org/apache/sling/servlets/json/dynamicrequest/RequestMappingException.java
@@ -0,0 +1,57 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.servlets.json.dynamicrequest;
+
+import org.apache.sling.api.SlingException;
+
+public class RequestMappingException extends SlingException {
+
+    /**
+     * Constructs a new RequestMappingException
+     */
+    protected RequestMappingException() {
+        super();
+    }
+
+    /**
+     * Constructs a new RequestMappingException with the given text
+     *
+     * @param text the exception text
+     */
+    protected RequestMappingException(String text) {
+        super(text);
+    }
+
+    /**
+     * Constructs a new RequestMappingException with a cause
+     *
+     * @param text  the exception text
+     * @param cause the root cause
+     */
+    public RequestMappingException(String text, Throwable cause) {
+        super(text, cause);
+    }
+
+    /**
+     * Constructs a new RequestMappingException with only a cause
+     *
+     * @param cause the root cause
+     */
+    protected RequestMappingException(Throwable cause) {
+        super(cause);
+    }
+}
diff --git a/org.apache.sling.servlets.json/src/test/java/org/apache/sling/servlets/json/DynamicRequestServletTest.java b/org.apache.sling.servlets.json/src/test/java/org/apache/sling/servlets/json/DynamicRequestServletTest.java
new file mode 100644
index 0000000..5d9e2b8
--- /dev/null
+++ b/org.apache.sling.servlets.json/src/test/java/org/apache/sling/servlets/json/DynamicRequestServletTest.java
@@ -0,0 +1,127 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.servlets.json;
+
+import static org.junit.jupiter.api.Assertions.assertEquals;
+import static org.junit.jupiter.api.Assertions.assertThrows;
+
+import java.io.IOException;
+
+import javax.servlet.ServletException;
+
+import org.apache.sling.servlets.json.dynamicrequest.DuplicateHandlersServlet;
+import org.apache.sling.servlets.json.dynamicrequest.InvalidArgumentsServlet;
+import org.apache.sling.servlets.json.dynamicrequest.RequestMappingException;
+import org.apache.sling.servlets.json.dynamicrequest.SampleDynamicRequestServlet;
+import org.apache.sling.testing.mock.sling.junit5.SlingContext;
+import org.apache.sling.testing.mock.sling.junit5.SlingContextExtension;
+import org.junit.jupiter.api.Test;
+import org.junit.jupiter.api.extension.ExtendWith;
+import org.junit.jupiter.params.ParameterizedTest;
+import org.junit.jupiter.params.provider.CsvSource;
+
+@ExtendWith(SlingContextExtension.class)
+class DynamicRequestServletTest {
+
+    private final SampleDynamicRequestServlet servlet = new SampleDynamicRequestServlet();
+
+    private final SlingContext context = new SlingContext();
+
+    @Test
+    void testSimple() throws ServletException, IOException {
+        context.request().setServletPath("/simple");
+        context.request().setMethod("GET");
+        servlet.service(context.request(), context.response());
+
+        assertEquals(200, context.response().getStatus());
+        assertEquals("application/json;charset=UTF-8", context.response().getContentType());
+        assertEquals("{\"Hello\":\"World\"}", context.response().getOutputAsString());
+    }
+
+    @ParameterizedTest
+    @CsvSource(value = {
+            "/simple/bob,200",
+            "/simple/bob/sal,200",
+            "/simple/bob.png,200",
+            "/simple2/glob,405"
+    })
+    void testGlob(String path, int status) throws ServletException, IOException {
+        context.request().setServletPath(path);
+        context.request().setMethod("GET");
+        servlet.service(context.request(), context.response());
+        assertEquals(status, context.response().getStatus());
+    }
+
+    @Test
+    void testOrdering() throws ServletException, IOException {
+        context.request().setServletPath("/simple/glob");
+        context.request().setMethod("GET");
+        servlet.service(context.request(), context.response());
+        assertEquals(200, context.response().getStatus());
+        assertEquals("{\"Hello\":\"World2\"}", context.response().getOutputAsString());
+    }
+
+    @Test
+    void testNoResponse() throws ServletException, IOException {
+        context.request().setServletPath("/no-response");
+        context.request().setMethod("POST");
+        servlet.service(context.request(), context.response());
+        assertEquals(202, context.response().getStatus());
+        assertEquals("", context.response().getOutputAsString());
+    }
+
+    @Test
+    void supportsParameters() throws ServletException, IOException {
+        context.request().setServletPath("/with-param");
+        context.request().setMethod("GET");
+        context.request().addRequestParameter("name", "Sling");
+        servlet.service(context.request(), context.response());
+        assertEquals(200, context.response().getStatus());
+        assertEquals("{\"Hello\":\"Sling\"}", context.response().getOutputAsString());
+    }
+
+    @Test
+    void supportsMissingParams() throws ServletException, IOException {
+        context.request().setServletPath("/with-param");
+        context.request().setMethod("GET");
+        servlet.service(context.request(), context.response());
+        assertEquals(200, context.response().getStatus());
+        assertEquals("{\"Hello\":null}", context.response().getOutputAsString());
+    }
+
+    @Test
+    void handlesThrownExceptions() throws ServletException, IOException {
+        context.request().setServletPath("/npe");
+        context.request().setMethod("GET");
+        servlet.service(context.request(), context.response());
+        assertEquals(500, context.response().getStatus());
+        assertEquals("application/problem+json;charset=UTF-8", context.response().getContentType());
+        assertEquals(
+                "{\"title\":\"Internal Server Error\",\"status\":500,\"detail\":\"java.lang.NullPointerException\"}",
+                context.response().getOutputAsString());
+    }
+
+    @Test
+    void wontConstructWithDuplicateHandlers() {
+        assertThrows(RequestMappingException.class, () -> new DuplicateHandlersServlet());
+    }
+
+    @Test
+    void wontConstructWithInvalidArguments() {
+        assertThrows(Exception.class, () -> new InvalidArgumentsServlet());
+    }
+}
diff --git a/org.apache.sling.servlets.json/src/test/java/org/apache/sling/servlets/json/dynamicrequest/DuplicateHandlersServlet.java b/org.apache.sling.servlets.json/src/test/java/org/apache/sling/servlets/json/dynamicrequest/DuplicateHandlersServlet.java
new file mode 100644
index 0000000..dccfbfb
--- /dev/null
+++ b/org.apache.sling.servlets.json/src/test/java/org/apache/sling/servlets/json/dynamicrequest/DuplicateHandlersServlet.java
@@ -0,0 +1,32 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.servlets.json.dynamicrequest;
+
+import org.apache.sling.servlets.json.DynamicRequestServlet;
+import org.apache.sling.servlets.json.annotations.RequestHandler;
+
+public class DuplicateHandlersServlet extends DynamicRequestServlet {
+
+    @RequestHandler(methods = { "GET" }, path = "/simple")
+    public void simpleHandler() {
+    }
+
+    @RequestHandler(methods = { "GET" }, path = "/simple")
+    public void duplicateHandler() {
+    }
+
+}
diff --git a/org.apache.sling.servlets.json/src/test/java/org/apache/sling/servlets/json/dynamicrequest/InvalidArgumentsServlet.java b/org.apache.sling.servlets.json/src/test/java/org/apache/sling/servlets/json/dynamicrequest/InvalidArgumentsServlet.java
new file mode 100644
index 0000000..058e90e
--- /dev/null
+++ b/org.apache.sling.servlets.json/src/test/java/org/apache/sling/servlets/json/dynamicrequest/InvalidArgumentsServlet.java
@@ -0,0 +1,28 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.servlets.json.dynamicrequest;
+
+import org.apache.sling.servlets.json.DynamicRequestServlet;
+import org.apache.sling.servlets.json.annotations.RequestHandler;
+
+public class InvalidArgumentsServlet extends DynamicRequestServlet {
+
+    @RequestHandler(methods = { "GET" }, path = "/simple")
+    public void duplicateHandler(String name) {
+    }
+
+}
diff --git a/org.apache.sling.servlets.json/src/test/java/org/apache/sling/servlets/json/dynamicrequest/SampleDynamicRequestServlet.java b/org.apache.sling.servlets.json/src/test/java/org/apache/sling/servlets/json/dynamicrequest/SampleDynamicRequestServlet.java
new file mode 100644
index 0000000..31d91fa
--- /dev/null
+++ b/org.apache.sling.servlets.json/src/test/java/org/apache/sling/servlets/json/dynamicrequest/SampleDynamicRequestServlet.java
@@ -0,0 +1,59 @@
+/*
+ * Licensed to the Apache Software Foundation (ASF) under one or more
+ * contributor license agreements.  See the NOTICE file distributed with
+ * this work for additional information regarding copyright ownership.
+ * The ASF licenses this file to You under the Apache License, Version 2.0
+ * (the "License"); you may not use this file except in compliance with
+ * the License.  You may obtain a copy of the License at
+ *
+ *      http://www.apache.org/licenses/LICENSE-2.0
+ *
+ * Unless required by applicable law or agreed to in writing, software
+ * distributed under the License is distributed on an "AS IS" BASIS,
+ * WITHOUT WARRANTIES OR CONDITIONS OF ANY KIND, either express or implied.
+ * See the License for the specific language governing permissions and
+ * limitations under the License.
+ */
+package org.apache.sling.servlets.json.dynamicrequest;
+
+import java.util.HashMap;
+import java.util.Map;
+
+import javax.servlet.http.HttpServletResponse;
+
+import org.apache.sling.servlets.json.DynamicRequestServlet;
+import org.apache.sling.servlets.json.annotations.RequestHandler;
+import org.apache.sling.servlets.json.annotations.RequestParameter;
+
+public class SampleDynamicRequestServlet extends DynamicRequestServlet {
+
+    @RequestHandler(methods = { "GET" }, path = "/simple")
+    public Map<String, Object> simpleHandler() {
+        return Map.of("Hello", "World");
+    }
+
+    @RequestHandler(methods = { "GET" }, path = "/simple/**")
+    public Map<String, Object> globHandler() {
+        return Map.of("Hello", "World2");
+    }
+
+    @RequestHandler(methods = { "POST" }, path = "/no-response")
+    public void noResponse(HttpServletResponse response) {
+        response.setStatus(HttpServletResponse.SC_ACCEPTED);
+    }
+
+    @RequestHandler(methods = { "GET" }, path = "/with-param")
+    public Map<String, Object> withParameter(HttpServletResponse response,
+            @RequestParameter(name = "name") String name) {
+        Map<String, Object> resp = new HashMap<>();
+        resp.put("Hello", name);
+        return resp;
+    }
+
+    @RequestHandler(methods = { "GET" }, path = "/npe")
+    public void npe(HttpServletResponse response,
+            @RequestParameter(name = "name") String name) {
+        name.length();
+    }
+
+}