Backend Development 10 min read

How to Fully Customize Spring MVC Core Components for Flexible Web Apps

This guide walks through creating custom Spring MVC core components—including DispatcherServlet, HandlerMapping, HandlerAdapter, and ViewResolver—using annotations and Java code to gain full control over request handling, parameter resolution, and response rendering in backend development.

Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
Spring Full-Stack Practical Cases
How to Fully Customize Spring MVC Core Components for Flexible Web Apps

1. Introduction

Spring framework is one of the most popular Java development frameworks, and Spring MVC is a key module for building high‑performance, scalable web applications. This article explains how to customize Spring MVC core components such as DispatcherServlet, HandlerMapping, HandlerAdapter, and ViewResolver to increase flexibility and extensibility.

Defining a request interface is straightforward with annotations:

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

Spring Web performs many behind‑the‑scenes tasks. Core components include HandlerMapping, HandlerAdapter, and ViewResolver.

HandlerMapping Finds the appropriate Handler for a request URI, e.g., a HandlerExecutionChain wrapping a HandlerMethod.

HandlerAdapter Invokes the identified HandlerMethod using a suitable adapter.

ViewResolver Renders a ModelAndView via the appropriate ViewResolver.

Understanding these components enables custom implementations.

2. Custom Endpoint

Define custom annotations to mark controller classes and request parameters.

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

3. Endpoint Parameter Wrapper

The following class stores method‑parameter information annotated with @PackParam .

<code>public class PackMethodParameter {
  private ParameterNameDiscoverer parameterNameDiscoverer = new LocalVariableTableParameterNameDiscoverer();
  private String name;
  private Executable executable;
  private int parameterIndex;
  private Class<?> type;

  public PackMethodParameter(String name, int parameterIndex, Executable executable) {
    this.name = name;
    this.parameterIndex = parameterIndex;
    this.executable = executable;
  }

  public PackMethodParameter(int parameterIndex, Executable executable, Class<?> type) {
    this.parameterIndex = parameterIndex;
    this.executable = executable;
    this.type = type;
  }

  public boolean hasParameterAnnotation(Class<? extends Annotation> clazz) {
    Method method = (Method) this.executable;
    Parameter[] parameters = method.getParameters();
    return parameters[this.parameterIndex].isAnnotationPresent(clazz);
  }

  public String getParameterName() {
    String[] parameterNames = parameterNameDiscoverer.getParameterNames((Method) this.executable);
    return parameterNames[this.parameterIndex];
  }
}
</code>

4. Custom HandlerMapping

Implementation of Spring MVC's HandlerMapping so that DispatcherServlet can recognize it.

<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);
    if (opt.isPresent()) {
      return new HandlerExecutionChain(opt.get());
    }
    return null;
  }

  // Bean initialization: find beans annotated with @PackEndpoint
  @Override
  public void afterPropertiesSet() throws Exception {
    String[] beanNames = context.getBeanNamesForType(Object.class);
    for (String beanName : beanNames) {
      Object bean = this.context.getBean(beanName);
      Class<?> clazz = bean.getClass();
      if (clazz.getAnnotation(PackEndpoint.class) != null) {
        RequestMapping clazzMapping = clazz.getAnnotation(RequestMapping.class);
        String rootPath = clazzMapping.value()[0];
        if (clazzMapping != null) {
          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 Method getMethod() { return method; }
    public void setMethod(Method method) { this.method = method; }
    public Object getInstance() { return instance; }
    public void setInstance(Object instance) { this.instance = instance; }
    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 PackMethodParameter[] getParameter() { return this.parameters; }
  }
}
</code>

5. Custom Argument Resolver

Resolves method parameters annotated with @PackParam from the HTTP request.

<code>public interface PackHandlerMethodArgumentResolver {
  boolean supportsParameter(PackMethodParameter methodParameter);
  Object resolveArgument(PackMethodParameter methodParameter, HttpServletRequest request);
}
</code>
<code>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();
    Object arg = null;
    String[] parameterValues = request.getParameterValues(name);
    if (parameterValues != null) {
      arg = parameterValues.length == 1 ? parameterValues[0] : parameterValues;
    }
    return arg;
  }
}
</code>

6. Custom HandlerAdapter

Implementation of Spring MVC's HandlerAdapter so that DispatcherServlet can invoke the custom handler.

<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 (this.argumentResolver.supportsParameter(parameters[i])) {
        args[i] = this.argumentResolver.resolveArgument(parameters[i], request);
        args[i] = this.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>

With these steps the full customization of Spring MVC core components is completed.

7. Test Endpoint

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

Done!!!

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.