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.
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.
Signed-in readers can open the original source through BestHub's protected redirect.
This article has been distilled and summarized from source material, then republished for learning and reference. If you believe it infringes your rights, please contactand we will review it promptly.
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.
