π MVC Argument Resolution
Overviewβ
This document provides a technical analysis of Sprout Framework's argument resolution system, which handles the automatic binding of HTTP request data to controller method parameters. The system transforms raw HTTP data (path variables, headers, query parameters, request body) into strongly-typed method arguments through a flexible, extensible resolver chain.
Architecture Overviewβ
Argument Resolution Flowβ
HTTP Request Data β CompositeArgumentResolver β Specific ArgumentResolvers
β
Controller Method Parameters β TypeConverter β Resolved Arguments
Component Interactionβ
βββββββββββββββββββββββ ββββββββββββββββββββββββ βββββββββββββββββββ
β HTTP Request ββββββ CompositeArgument ββββββ ArgumentResolverβ
β - Path Variables β β Resolver β β Implementations β
β - Headers β β (Coordinator) β β β
β - Query Params β ββββββββββββββββββββββββ βββββββββββββββββββ
β - Body β β β
βββββββββββββββββββββββ ββββββββββββββββββββββββ βββββββββββββββββββ
β Type Converter ββββββ Resolved Values β
β (Type Safety) β β β
ββββββββββββββββββββββββ βββββββββββββββββββ
Core Components Analysisβ
1. CompositeArgumentResolver: The Resolution Orchestratorβ
Delegation Strategy
The CompositeArgumentResolver
implements the Composite pattern to coordinate multiple specialized resolvers.
public Object[] resolveArguments(Method method, HttpRequest<?> request,
Map<String, String> pathVariables) throws Exception {
Parameter[] params = method.getParameters();
Object[] args = new Object[params.length];
for (int i = 0; i < params.length; i++) {
Parameter p = params[i];
// Find first supporting resolver
ArgumentResolver resolver = delegates.stream()
.filter(ar -> ar.supports(p))
.findFirst()
.orElseThrow(() -> new IllegalStateException(
"No ArgumentResolver for parameter " + p));
args[i] = resolver.resolve(p, request, pathVariables);
}
return args;
}
Design Pattern Analysis
- Chain of Responsibility: Each resolver checks if it can handle a parameter
- Strategy Pattern: Different resolution strategies for different parameter types
- Template Method: Common resolution framework with specialized implementations
Performance Characteristics
- Time Complexity: O(n * m) where n = parameters, m = average resolvers checked
- Early Termination: Stops at first matching resolver
- Caching Opportunity: Could cache resolver mappings for repeated calls
Error Handling Strategy
.orElseThrow(() -> new IllegalStateException("No ArgumentResolver for parameter " + p));
- Fail-fast approach: Unknown parameter types cause immediate failure
- Clear error messages for debugging
- No partial resolution attempts
2. ArgumentResolver Interface: The Resolver Contractβ
Contract Definition
public interface ArgumentResolver {
boolean supports(Parameter parameter);
Object resolve(Parameter parameter, HttpRequest<?> request,
Map<String, String> pathVariables) throws Exception;
}
Two-Phase Resolution Protocol
- Support Check: Determines if resolver can handle the parameter
- Resolution: Performs actual value extraction and conversion
Interface Design Benefits
- Extensibility: Easy to add new resolver types
- Testability: Each resolver can be unit tested independently
- Separation of Concerns: Clear responsibility boundaries
3. TypeConverter: Centralized Type Conversionβ
Conversion Algorithm
public static Object convert(String value, Class<?> targetType) {
if (value == null) {
if (targetType.isPrimitive()) {
throw new IllegalArgumentException(
"Null value cannot be assigned to primitive type: " + targetType.getName());
}
return null;
}
if (targetType.equals(String.class)) {
return value;
} else if (targetType.equals(Long.class) || targetType.equals(long.class)) {
return Long.parseLong(value);
} else if (targetType.equals(Integer.class) || targetType.equals(int.class)) {
return Integer.parseInt(value);
} else if (targetType.equals(Boolean.class) || targetType.equals(boolean.class)) {
return Boolean.parseBoolean(value);
}
throw new IllegalArgumentException(
"Cannot convert String value [" + value + "] to target class [" + targetType.getName() + "]");
}
Type Safety Features
- Primitive Null Check: Prevents null assignment to primitive types
- Wrapper Type Handling: Supports both primitive and wrapper types
- Explicit Type Mapping: Clear conversion rules for supported types
- Fail-Safe: Throws exception for unsupported conversions
Supported Conversions
Source | Target Types | Conversion Method |
---|---|---|
String | String | Identity |
String | Long/long | Long.parseLong() |
String | Integer/int | Integer.parseInt() |
String | Boolean/boolean | Boolean.parseBoolean() |
Resolver Implementation Analysisβ
1. PathVariableArgumentResolverβ
Support Detection
public boolean supports(Parameter parameter) {
return parameter.isAnnotationPresent(PathVariable.class);
}
Resolution Logic
public Object resolve(Parameter parameter, HttpRequest<?> request,
Map<String, String> pathVariables) throws Exception {
PathVariable pathVariableAnnotation = parameter.getAnnotation(PathVariable.class);
String variableName = pathVariableAnnotation.value();
if (variableName.isEmpty()) {
variableName = parameter.getName(); // Convention over configuration
}
String value = pathVariables.get(variableName);
if (value == null) {
throw new IllegalArgumentException(
"Path variable '" + variableName + "' not found in path.");
}
return TypeConverter.convert(value, parameter.getType());
}
Key Features
- Convention over Configuration: Uses parameter name if annotation value is empty
- Strict Validation: Throws exception if path variable not found
- Type Conversion: Delegates to centralized converter
2. RequestParamArgumentResolverβ
Enhanced Support Logic
public Object resolve(Parameter parameter, HttpRequest<?> request,
Map<String, String> pathVariables) throws Exception {
RequestParam requestParam = parameter.getAnnotation(RequestParam.class);
String paramName = requestParam.value().isEmpty() ?
parameter.getName() : requestParam.value();
String paramValue = request.getQueryParams().get(paramName);
if (paramValue == null) {
if (requestParam.required()) {
throw new IllegalArgumentException(
"Required request parameter '" + paramName + "' not found in request.");
}
}
return TypeConverter.convert(paramValue, parameter.getType());
}
Advanced Features
- Optional Parameters: Supports required/optional distinction
- Null Handling: Graceful handling of missing optional parameters
- Validation Logic: Enforces required parameter constraints
3. HeaderArgumentResolverβ
Dual-Mode Resolution
The system provides two header resolvers.
Individual Header Resolution
// HeaderArgumentResolver - handles specific headers
public boolean supports(Parameter parameter) {
return parameter.isAnnotationPresent(Header.class) &&
!parameter.getAnnotation(Header.class).value().isEmpty();
}
All Headers Resolution
// AllHeaderArgumentResolver - handles header map injection
public boolean supports(Parameter parameter) {
return parameter.isAnnotationPresent(Header.class) &&
parameter.getAnnotation(Header.class).value().isEmpty();
}
Type-Based Dispatch
if (parameter.getType().equals(Map.class)) {
if (headerName.isBlank()) {
return request.getHeaders(); // Return all headers
} else {
throw new IllegalArgumentException(
"Cannot bind specific header '" + headerName +
"' to a Map parameter. Use Map<String, String> without @Header for all headers.");
}
}
4. RequestBodyArgumentResolverβ
JSON Deserialization Integration
@Component
public class RequestBodyArgumentResolver implements ArgumentResolver {
private final ObjectMapper objectMapper = new ObjectMapper();
public Object resolve(Parameter parameter, HttpRequest<?> request,
Map<String, String> pathVariables) throws Exception {
String rawBody = (String) request.getBody();
if (rawBody == null || rawBody.isBlank()) {
return null;
}
try {
return objectMapper.readValue(rawBody.trim(), parameter.getType());
} catch (Exception e) {
throw new BadRequestException(
"Failed to parse request body as JSON or convert to '" +
parameter.getType().getName() + "'. Check JSON format and target type. Cause: " +
e.getMessage(), ResponseCode.BAD_REQUEST, e);
}
}
}
Advanced Features
- Jackson Integration: Uses ObjectMapper for JSON deserialization
- Generic Type Support: Can deserialize to any class
- Error Context: Provides detailed error information
- HTTP Status Mapping: Maps parsing errors to appropriate HTTP responses
Type System Integrationβ
Java Reflection Integrationβ
Parameter Introspection
Parameter[] params = method.getParameters();
// Each Parameter provides:
// - parameter.getType() - Class<?> for type checking
// - parameter.getName() - String for convention over configuration
// - parameter.getAnnotations() - Annotation[] for metadata
// - parameter.isAnnotationPresent(Class) - boolean for quick checks
Type Erasure Handling
The system currently handles basic types but has limitations with generic types.
// Works
public void method(@RequestBody User user)
// Limited - loses generic type information
public void method(@RequestBody List<User> users)
Potential Enhancement
// Could use ParameterizedType for generic support
if (parameter.getParameterizedType() instanceof ParameterizedType) {
ParameterizedType pType = (ParameterizedType) parameter.getParameterizedType();
Type[] actualTypeArguments = pType.getActualTypeArguments();
// Handle List<User>, Map<String, Object>, etc.
}
Performance Analysisβ
Resolution Complexityβ
Per-Request Resolution
- Time: O(p * r) where p = parameters, r = average resolvers to check
- Space: O(p) for argument array allocation
- Optimization: Could implement resolver caching
Type Conversion Overhead
- Primitive conversions: O(1)
- String operations: O(1)
- JSON deserialization: O(json_size)
Memory Usage Patternsβ
Resolver Chain
private final List<ArgumentResolver> delegates;
- Static resolver list shared across all requests
- No per-request resolver allocation
Argument Array
Object[] args = new Object[params.length];
- Temporary array per method invocation
- Size determined by method signature
Optimization Opportunitiesβ
Resolver Mapping Cache
// Potential enhancement
private final Map<Parameter, ArgumentResolver> resolverCache = new ConcurrentHashMap<>();
public Object[] resolveArguments(...) {
// Cache resolver mappings per parameter
ArgumentResolver resolver = resolverCache.computeIfAbsent(p,
param -> delegates.stream().filter(ar -> ar.supports(param)).findFirst().orElse(null));
}
Error Handling Strategyβ
Exception Hierarchyβ
Resolution Failures
- IllegalStateException: No resolver found for parameter
- IllegalArgumentException: Parameter validation failures
- BadRequestException: Client data format errors
Error Context Preservation
throw new BadRequestException(
"Failed to parse request body as JSON or convert to '" +
parameter.getType().getName() + "'. Check JSON format and target type. Cause: " +
e.getMessage(), ResponseCode.BAD_REQUEST, e);
Recovery Strategies
- Fail-Fast: Stop resolution on first error
- Error Propagation: Preserve original exception causes
- HTTP Mapping: Map internal errors to appropriate HTTP status codes
Extensibility Analysisβ
Adding New Resolversβ
Implementation Requirements
- Implement
ArgumentResolver
interface - Add
@Component
annotation for auto-registration - Define clear support criteria
- Handle type conversion appropriately
Example Custom Resolver
@Component
public class SessionArgumentResolver implements ArgumentResolver {
@Override
public boolean supports(Parameter parameter) {
return parameter.isAnnotationPresent(SessionAttribute.class);
}
@Override
public Object resolve(Parameter parameter, HttpRequest<?> request,
Map<String, String> pathVariables) throws Exception {
// Custom session handling logic
SessionAttribute annotation = parameter.getAnnotation(SessionAttribute.class);
return sessionManager.getAttribute(annotation.value());
}
}
Type Converter Extensionβ
Current Limitations
- Fixed set of supported types
- No custom converter registration
- No complex object conversion
Potential Enhancement
public interface TypeConverter {
boolean supports(Class<?> sourceType, Class<?> targetType);
Object convert(Object source, Class<?> targetType);
}
// Registry of converters
private final List<TypeConverter> converters;
Integration with IoC Containerβ
Automatic Resolver Discoveryβ
Component Scanning
@Component
public class PathVariableArgumentResolver implements ArgumentResolver
Dependency Injection
public CompositeArgumentResolver(List<ArgumentResolver> delegates) {
this.delegates = delegates;
}
IoC Benefits
- Automatic resolver registration
- Ordered resolver injection via
@Order
- Easy testing with mock resolvers
Resolver Orderingβ
Current Behavior
- Order determined by IoC container bean creation order
- No explicit priority handling
- Could benefit from
@Order
annotation support
Potential Enhancement
@Component
@Order(100)
public class PathVariableArgumentResolver implements ArgumentResolver
Security Considerationsβ
Input Validationβ
Current State
- Basic type validation through conversion
- No input sanitization
- No size limits on input data
Security Gaps
- JSON Bomb Protection: No limits on JSON parsing depth/size
- Path Variable Validation: No regex validation on path variables
- Header Injection: No validation of header content
Improvement Considerations
// Size limits
public Object resolve(Parameter parameter, HttpRequest<?> request, ...) {
String rawBody = (String) request.getBody();
if (rawBody != null && rawBody.length() > MAX_BODY_SIZE) {
throw new PayloadTooLargeException();
}
// Configure ObjectMapper with security settings
objectMapper.configure(JsonParser.Feature.STRICT_DUPLICATE_DETECTION, true);
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, true);
}
Type Safety Enforcementβ
Primitive Type Protection
if (targetType.isPrimitive() && value == null) {
throw new IllegalArgumentException("Null value cannot be assigned to primitive type");
}
Class Loading Security
- Uses reflection but doesn't allow arbitrary class instantiation
- Type conversion limited to known safe types
- Jackson deserialization could be configured with security settings
Comparison with Spring MVCβ
Similaritiesβ
- Annotation-driven parameter binding
- Extensible resolver chain architecture
- Type conversion system
- Support for common HTTP parameter types
Differencesβ
Simplified Type System
- Spring: Complex ConversionService with extensive type support
- Sprout: Simple TypeConverter with basic types
Resolver Discovery
- Spring: Complex HandlerMethodArgumentResolverComposite with ordering
- Sprout: Simple List-based iteration
Error Handling
- Spring: Sophisticated MethodArgumentResolutionException hierarchy
- Sprout: Basic exception types with HTTP status mapping
Performance
- Spring: Optimized with caching and pre-computed resolver mappings
- Sprout: Linear search through resolver list (optimization opportunity)
Sprout's argument resolution demonstrates a well-designed, extensible architecture that successfully abstracts HTTP request data binding. The system leverages proven design patterns (Composite, Strategy, Chain of Responsibility) while maintaining simplicity suitable for educational purposes.
Strengths
- Clear separation of concerns
- Extensible resolver architecture
- Type-safe parameter binding
- Good error handling with HTTP status mapping
Areas for Enhancement
- Performance optimization through resolver caching
- Enhanced type conversion system
- Security hardening for input validation
- Support for generic types and complex objects
Additional issue creation and contributions to these enhancement areas would contribute to the continued development of the Sprout framework.