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.
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!!!
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.
How this landed with the community
Was this worth your time?
0 Comments
Thoughtful readers leave field notes, pushback, and hard-won operational detail here.