Backend Development 9 min read

Create Custom Spring MVC Core Components: HandlerMapping, Adapter, and Endpoint

This tutorial walks through building custom Spring MVC core components—including a custom @PackEndpoint annotation, HandlerMapping, HandlerAdapter, and parameter resolver—complete with code examples and a test controller to demonstrate the full request handling flow.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Create Custom Spring MVC Core Components: HandlerMapping, Adapter, and Endpoint

1. Introduction

Environment: Spring 5.3.23

Defining a simple interface in Spring is straightforward, as shown below:

<code>@RestController
@RequestMapping("/demos")
public class DemoController {
  @GetMapping("/index")
  public Object index() {
    return "index" ;
  }
}</code>

The @RestController and @RequestMapping annotations create a basic endpoint.

Under the hood, Spring Web relies on several core components: HandlerMapping , HandlerAdapter , and ViewResolver .

HandlerMapping – Finds the appropriate handler for the current request URI, returning a HandlerExecutionChain that wraps a HandlerMethod .

HandlerAdapter – Determines the adapter that can invoke the identified HandlerMethod .

ViewResolver – Renders the response when a ModelAndView object is returned.

After understanding these components, you can customize them to handle requests in your own way.

2. Practical Example

2.1 Custom Endpoint Annotation

Define a custom @PackEndpoint annotation, similar to @Controller :

<code>@Target(ElementType.TYPE)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PackEndpoint {}
</code>

2.2 Custom Parameter Annotation

Create a parameter annotation analogous to @RequestParam :

<code>@Target(ElementType.PARAMETER)
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface PackParam {}
</code>

2.3 Custom HandlerMapping

Implement Spring MVC's HandlerMapping so that DispatcherServlet can recognize it. The mapping matches request URIs to handler methods.

<code>public class PackHandlerMapping implements HandlerMapping, InitializingBean, ApplicationContextAware {
  private ApplicationContext context;
  private Map<String, PackMethodHandler> mapping = new HashMap<>();

  @Override
  public HandlerExecutionChain getHandler(HttpServletRequest request) throws Exception {
    String requestPath = request.getRequestURI();
    Optional<PackMethodHandler> opt = mapping.entrySet().stream()
        .filter(entry -> entry.getKey().equals(requestPath))
        .map(Map.Entry::getValue)
        .findFirst();
    return opt.map(HandlerExecutionChain::new).orElse(null);
  }

  // During bean initialization, find all beans annotated with @PackEndpoint
  @Override
  public void afterPropertiesSet() throws Exception {
    String[] beanNames = context.getBeanNamesForType(Object.class);
    for (String beanName : beanNames) {
      Object bean = context.getBean(beanName);
      Class<?> clazz = bean.getClass();
      if (clazz.getAnnotation(PackEndpoint.class) != null) {
        RequestMapping clazzMapping = clazz.getAnnotation(RequestMapping.class);
        String rootPath = clazzMapping.value()[0];
        ReflectionUtils.doWithMethods(clazz, method -> {
          RequestMapping nestMapping = AnnotatedElementUtils.findMergedAnnotation(method, RequestMapping.class);
          if (nestMapping != null) {
            String nestPath = nestMapping.value()[0];
            String path = rootPath + nestPath;
            PackMethodHandler handler = new PackMethodHandler(method, bean);
            mapping.put(path, handler);
          }
        });
      }
    }
  }

  @Override
  public void setApplicationContext(ApplicationContext applicationContext) throws BeansException {
    this.context = applicationContext;
  }

  public static class PackMethodHandler {
    private Method method;
    private Object instance;
    private PackMethodParameter[] parameters;

    public PackMethodHandler(Method method, Object instance) {
      this.method = method;
      this.instance = instance;
      Parameter[] params = method.getParameters();
      this.parameters = new PackMethodParameter[params.length];
      for (int i = 0; i < params.length; i++) {
        this.parameters[i] = new PackMethodParameter(i, method, params[i].getType());
      }
    }

    public Method getMethod() { return method; }
    public Object getInstance() { return instance; }
    public PackMethodParameter[] getParameter() { return parameters; }
  }
}
</code>

2.4 Custom Parameter Resolver

The resolver extracts method arguments from the request based on the custom @PackParam annotation.

<code>public interface PackHandlerMethodArgumentResolver {
  boolean supportsParameter(PackMethodParameter methodParameter);
  Object resolveArgument(PackMethodParameter methodParameter, HttpServletRequest request);
}

public class PackParamHandlerMethodArgumentResolver implements PackHandlerMethodArgumentResolver {
  @Override
  public boolean supportsParameter(PackMethodParameter methodParameter) {
    return methodParameter.hasParameterAnnotation(PackParam.class);
  }

  @Override
  public Object resolveArgument(PackMethodParameter methodParameter, HttpServletRequest request) {
    String name = methodParameter.getParameterName();
    String[] values = request.getParameterValues(name);
    if (values == null) return null;
    return values.length == 1 ? values[0] : values;
  }
}
</code>

2.5 Custom HandlerAdapter

Implement Spring MVC's HandlerAdapter to invoke the target method and write the response.

<code>public class PackHandlerAdapter implements HandlerAdapter {
  @Resource
  private ConversionService conversionService;

  private PackParamHandlerMethodArgumentResolver argumentResolver = new PackParamHandlerMethodArgumentResolver();

  @Override
  public boolean supports(Object handler) {
    return handler instanceof PackMethodHandler;
  }

  @Override
  public ModelAndView handle(HttpServletRequest request, HttpServletResponse response, Object handler) throws Exception {
    PackMethodHandler methodHandler = (PackMethodHandler) handler;
    PackMethodParameter[] parameters = methodHandler.getParameter();
    Object[] args = new Object[parameters.length];
    for (int i = 0; i < args.length; i++) {
      if (argumentResolver.supportsParameter(parameters[i])) {
        args[i] = argumentResolver.resolveArgument(parameters[i], request);
        args[i] = conversionService.convert(args[i], parameters[i].getType());
      }
    }
    Object result = methodHandler.getMethod().invoke(methodHandler.getInstance(), args);
    response.setHeader("Content-Type", "text/plain;charset=utf8");
    PrintWriter out = response.getWriter();
    out.write((String) result);
    out.flush();
    out.close();
    return null;
  }

  @Override
  public long getLastModified(HttpServletRequest request, Object handler) {
    return -1;
  }
}
</code>

2.6 Test Controller

<code>@PackEndpoint
@RequestMapping("/users")
public class UserController {
  @GetMapping("/index")
  public Object index(@PackParam Long id, @PackParam String name) {
    return "id = " + id + ", name = " + name;
  }
}
</code>

Following these steps completes a fully custom implementation of Spring MVC's core components, revealing the underlying request handling mechanism.

Hope this article helps you.

JavaBackend DevelopmentSpring MVCHandlerAdapterCustom HandlerMapping
Spring Full-Stack Practical Cases
Written by

Spring Full-Stack Practical Cases

Full-stack Java development with Vue 2/3 front-end suite; hands-on examples and source code analysis for Spring, Spring Boot 2/3, and Spring Cloud.

0 followers
Reader feedback

How this landed with the community

login Sign in to like

Rate this article

Was this worth your time?

Sign in to rate
Discussion

0 Comments

Thoughtful readers leave field notes, pushback, and hard-won operational detail here.