Skip to main content

🎢 MVC Controller Mapping

Overview​

This document provides a technical analysis of Sprout Framework's MVC controller mapping implementation, examining the internal architecture, algorithms, and design patterns used to route HTTP requests to controller methods.

Core Architecture​

Component Interaction Flow​

HTTP Request → HandlerMapping → RequestMappingRegistry → PathPattern Matching
↓
Controller Discovery ← HandlerMethodScanner ← BeanFactory
↓
Handler Invocation ← HandlerMethodInvoker ← Argument Resolution

Key Components Analysis​

1. PathPattern: Advanced Pattern Matching Engine​

Implementation Details:

The PathPattern class implements a sophisticated URL pattern matching system using Java's regex engine with custom parsing logic:

public class PathPattern implements Comparable<PathPattern> {
private final String originalPattern;
private final Pattern regex; // Compiled regex for matching
private final List<String> varNames; // Variable names in order
private final List<Integer> varGroups; // Corresponding regex groups
private final int staticLen; // Length of static content
private final int singleStarCount; // Count of * wildcards
private final int doubleStarCount; // Count of ** wildcards
}

Pattern Compilation Algorithm:

The constructor implements a state machine that processes different token types:

  1. Variable Tokens ({name} or {name:regex}):

    • Uses VAR_TOKEN pattern: \\{([^/:}]+)(?::([^}]+))?}
    • Extracts variable name and optional custom regex
    • Defaults to [^/]+ for unconstrained variables
  2. Wildcard Processing:

    • * → ([^/]+) (single path segment)
    • ** → (.+?) (multiple segments, non-greedy)
    • ? → [^/] (single character)
  3. Static Content: Escaped using Pattern.quote()

Specificity Algorithm:

The compareTo method implements a multi-criteria sorting algorithm:

public int compareTo(PathPattern other) {
// Priority order (ascending = more specific):
// 1. Fewer ** wildcards
// 2. Fewer * wildcards
// 3. Fewer path variables
// 4. Longer static content
// 5. Lexicographic pattern string
}

This ensures the most specific patterns are matched first, preventing broad patterns from shadowing specific ones.

2. HandlerMethodScanner: Reflection-Based Discovery​

Scanning Strategy:

The scanner uses a multi-phase approach:

  1. Bean Enumeration: Iterates through all beans in the container
  2. Controller Detection: Checks for @Controller annotation
  3. Method Introspection: Examines all public methods for mapping annotations
  4. Annotation Processing: Handles meta-annotations and inheritance

Annotation Resolution Algorithm:

public RequestMappingInfoExtractor findRequestMappingInfoExtractor(Method method) {
for (Annotation ann : method.getDeclaredAnnotations()) {
// Check direct @RequestMapping or meta-annotated mappings
RequestMapping rm = ann instanceof RequestMapping
? (RequestMapping) ann
: ann.annotationType().getAnnotation(RequestMapping.class);

if (rm == null) continue;

// Extract paths with fallback hierarchy: value() → path() → fallback
String[] paths = extractPaths(ann, rm);
HttpMethod[] methods = rm.method();

return new RequestMappingInfoExtractor(path, methods);
}
return null;
}

Path Combination Logic:

The combinePaths method handles edge cases in URL construction:

public String combinePaths(String basePath, String methodPath) {
// Handles cases like:
// ("", "/users") → "/users"
// ("/api", "users") → "/api/users"
// ("/api/", "/users") → "/api/users"
// ("/api", "/") → "/api"
}

3. RequestMappingRegistry: Concurrent Mapping Storage​

Data Structure Design:

private final Map<PathPattern, Map<HttpMethod, RequestMappingInfo>> mappings 
= new ConcurrentHashMap<>();

This nested map structure provides:

  • Thread Safety: ConcurrentHashMap for concurrent access
  • Efficient Lookup: O(1) access by HTTP method after pattern matching
  • Memory Efficiency: EnumMap for HTTP methods reduces memory overhead

Handler Resolution Algorithm:

The getHandlerMethod implementation uses a three-phase approach:

  1. Pattern Matching Phase:

    for (PathPattern registeredPattern : mappings.keySet()) {
    if (registeredPattern.matches(path)) {
    // Collect matching handlers
    }
    }
  2. Sorting Phase:

    matchingHandlers.sort(Comparator.comparing(RequestMappingInfo::pattern));
  3. Selection Phase: Returns the first (most specific) match

Performance Characteristics:

  • Time Complexity: O(n * m) where n = registered patterns, m = average regex complexity
  • Space Complexity: O(p * h) where p = patterns, h = HTTP methods per pattern
  • Optimization: Early termination when no matches found

4. HandlerMethodInvoker: Method Execution Engine​

Invocation Pipeline:

public Object invoke(RequestMappingInfo requestMappingInfo, HttpRequest<?> request) {
// 1. Path variable extraction
Map<String, String> pathVariables = pattern.extractPathVariables(request.getPath());

// 2. Argument resolution via composite resolver
Object[] args = resolvers.resolveArguments(handlerMethod, request, pathVariables);

// 3. Reflective method invocation
return handlerMethod.invoke(controller, args);
}

This design separates concerns and allows for extensible argument resolution strategies.

Advanced Technical Features​

1. Meta-Annotation Support​

The framework supports Spring-style meta-annotations:

@RequestMapping(method = HttpMethod.GET)
@Retention(RetentionPolicy.RUNTIME)
@Target(ElementType.METHOD)
public @interface GetMapping {
String[] value() default {};
String[] path() default {};
}

The scanner detects these through annotation introspection:

RequestMapping rm = ann.annotationType().getAnnotation(RequestMapping.class);

2. Regex Compilation Optimization​

Patterns are compiled once during registration and reused for all matching operations:

this.regex = Pattern.compile(re.toString());

This avoids the overhead of repeated pattern compilation during request processing.

3. Variable Group Mapping​

The implementation maintains a parallel mapping between variable names and regex capture groups:

private final List<String> varNames;      // ["id", "category"]
private final List<Integer> varGroups; // [1, 2]

This enables efficient variable extraction without string parsing.

Performance Analysis​

Initialization Performance​

Controller Scanning Complexity:

  • Time: O(C * M * A) where C = controllers, M = methods per controller, A = annotations per method
  • Space: O(P) where P = total registered patterns
  • Optimization: Performed once at startup, results cached

Runtime Performance​

Request Routing Complexity:

  • Best Case: O(1) with unique static patterns
  • Average Case: O(log P) with well-distributed patterns
  • Worst Case: O(P * R) where P = patterns, R = regex complexity

Memory Usage:

  • Pattern storage: ~200-500 bytes per pattern (regex + metadata)
  • Registry overhead: ~100 bytes per mapping
  • Total: Typically <1MB for applications with <1000 endpoints

Optimization Strategies​

  1. Pattern Ordering: Most specific patterns checked first
  2. Lazy Compilation: Regex compiled only once per pattern
  3. Efficient Data Structures: EnumMap for HTTP methods, ArrayList for ordered collections
  4. Short-Circuit Evaluation: Early return on first match

Design Patterns and Principles​

1. Strategy Pattern​

  • CompositeArgumentResolver delegates to specific resolvers
  • Allows extensible argument resolution without modifying core logic

2. Template Method Pattern​

  • HandlerMethodScanner defines scanning algorithm
  • Subclasses can override specific steps

3. Registry Pattern​

  • RequestMappingRegistry centralizes mapping storage
  • Provides unified interface for registration and lookup

4. Comparable/Comparator Pattern​

  • PathPattern implements natural ordering by specificity
  • Enables automatic sorting without external logic

Error Handling and Edge Cases​

1. Ambiguous Mappings​

if (cnt != 1) {
System.out.printf("[WARN] %s.%s() - skipped: ambiguous @RequestMapping",
method.getDeclaringClass().getSimpleName(), method.getName());
return null;
}

2. Invalid Pattern Syntax​

if (!varMatcher.region(i, pattern.length()).lookingAt()) {
throw new IllegalArgumentException("Invalid variable syntax at index " + i);
}

3. No Handler Found​

if (matchingHandlers.isEmpty()) {
return null;
}

The system gracefully handles these scenarios with clear error messages and fallback behavior.

Comparison with Spring MVC​

Similarities​

  • Annotation-based configuration
  • Pattern matching with variables and wildcards
  • Handler method introspection
  • Argument resolution pipeline

Differences​

  • Simplified Architecture: Fewer abstraction layers
  • Regex-based Matching: Direct regex compilation vs Spring's AntPathMatcher
  • Reduced Configurability: Focus on common use cases
  • Performance Focus: Optimized for speed over flexibility

Extensibility Points​

1. Custom Argument Resolvers​

Implement ArgumentResolver interface and register with CompositeArgumentResolver

2. Custom Pattern Matching​

Extend PathPattern to support additional syntax

3. Custom Handler Discovery​

Implement alternative HandlerMethodScanner strategies

4. Custom Mapping Storage​

Replace RequestMappingRegistry with alternative storage mechanisms