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:

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

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:

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

2.2 Custom Parameter Annotation

Create a parameter annotation analogous to @RequestParam:

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

2.3 Custom HandlerMapping

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

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; }
  }
}

2.4 Custom Parameter Resolver

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

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;
  }
}

2.5 Custom HandlerAdapter

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

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;
  }
}

2.6 Test Controller

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

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.

Original Source

Signed-in readers can open the original source through BestHub's protected redirect.

Sign in to view source
Republication Notice

This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactadmin@besthub.devand we will review it promptly.

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

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.