How data binding works, @RequestParam, customising binding with @InitBinder and converters, binding pitfalls (including mass assignment), serving static resources and making them fast, Spring's Resource abstraction, and @PathVariable design.
Published September 25, 2026
Data binding looks like magic until it binds a field you didn't intend. That's why "challenges of data binding" is really a security question. The static-resource and URL-design questions reward practical, performance-minded answers.
Short answer: Spring's WebDataBinder copies request data (query parameters, form fields, path variables) into handler arguments and objects, converting types along the way through its ConversionService (String → int, LocalDate, enums, …). Binding and validation errors are collected in a BindingResult. JSON bodies take a different path: @RequestBody is deserialised by message converters (Jackson), not by the data binder.
Key points to cover:
BindException vs HttpMessageNotReadableException).Learn it in depth → Bean Validation
@RequestParam do?Short answer: It binds a query-string or form parameter to a method argument, with optional required, defaultValue and type conversion.
@GetMapping("/products")
Page<ProductDto> search(@RequestParam(required = false) String q,
@RequestParam(defaultValue = "0") int page,
@RequestParam(defaultValue = "20") int size,
@RequestParam(name = "tag", required = false) List<String> tags) { // ?tag=a&tag=b
…
}
Key points to cover:
MissingServletRequestParameterException). A conversion failure (?page=abc) also gives a 400.@RequestParam Map<String, String> captures all the parameters.Short answer: In increasing scope:
@InitBinder methods in a controller or @ControllerAdvice: register custom editors, set allowed or disallowed fields, trim strings.Converter/Formatter beans, registered through WebMvcConfigurer.addFormatters, or simply as beans in Spring Boot.@DateTimeFormat and @NumberFormat.@ControllerAdvice
class BindingConfig {
@InitBinder
void init(WebDataBinder binder) {
binder.registerCustomEditor(String.class, new StringTrimmerEditor(true)); // " " → null
}
}
@Component
class SkuConverter implements Converter<String, Sku> { // ?sku=AB-123 → Sku value object
public Sku convert(String source) { return Sku.parse(source); }
}
Short answer:
BindingResult, or a global handler that returns clear 400 messages.role=ADMIN or price=0, and the binder will happily set any matching property.Key points to cover:
binder.setAllowedFields(...). Never bind directly onto entities.Common trap: binding an @Entity straight from a form. It's the classic mass-assignment hole (see OWASP's Mass Assignment cheat sheet).
Short answer:
classpath:/static/, /public/, /resources/ or /META-INF/resources/ are served automatically from the root path. For example, src/main/resources/static/css/app.css is served at /css/app.css.@Configuration
class StaticResourceConfig implements WebMvcConfigurer {
@Override public void addResourceHandlers(ResourceHandlerRegistry registry) {
registry.addResourceHandler("/assets/**")
.addResourceLocations("classpath:/static/assets/")
.setCacheControl(CacheControl.maxAge(Duration.ofDays(365)).cachePublic());
}
}
Common trap: adding @EnableWebMvc in a Boot app while doing this. It turns off Boot's default static-resource handling.
Short answer: Put them in a static location (Boot), or map a URL pattern to a location with addResourceHandlers. Reference them from templates with context-aware URLs (for example, Thymeleaf's @{/css/app.css}), so they keep working behind a context path.
Short answer: Static files are a large share of page weight, so how they're served matters:
Cache-Control max-age for immutable files, plus ETag or Last-Modified for validation.VersionResourceResolver produces content-hash file names such as app-3f9a1c.css, so they can be cached "forever" but change the moment the content does.server.compression.enabled=true, or pre-compressed gzip/brotli files.registry.addResourceHandler("/assets/**")
.addResourceLocations("classpath:/static/assets/")
.setCacheControl(CacheControl.maxAge(Duration.ofDays(365)))
.resourceChain(true)
.addResolver(new VersionResourceResolver().addContentVersionStrategy("/**"));
Short answer: Spring's Resource abstraction loads files uniformly from different places:
classpath: → ClassPathResource;file: → FileSystemResource;http: → UrlResource;ServletContextResource (such as /WEB-INF/…).A WebApplicationContext resolves unprefixed paths against the servlet context rather than the classpath. It also adds the web-specific bean scopes (request, session, application).
@Value("classpath:templates/invoice.html") Resource invoiceTemplate;
Resource logo = applicationContext.getResource("/WEB-INF/images/logo.png"); // ServletContextResource
Key points to cover:
getInputStream(), not getFile().@PathVariable?Short answer: It binds a segment of the URL path (a URI template variable) to a method parameter. It identifies which resource the request is about, as in /orders/{orderId}.
@GetMapping("/customers/{customerId}/orders/{orderId}")
OrderDto get(@PathVariable long customerId, @PathVariable long orderId) { … }
@PathVariable?Short answer: Declare placeholders in the mapping, and matching parameters (by name, or @PathVariable("id")). Spring converts the text to the parameter type. You can also constrain the format with a regex: @GetMapping("/files/{name:[a-z0-9-]+}.{ext}").
Key points to cover:
@PathVariable Map<String, String> captures all the variables.-parameters. Spring Boot's parent POM enables it. Otherwise, name the variable explicitly.@PathVariable?Short answer:
/orders/42), and query parameters for filters and options (/orders?status=PAID)./customers/7/orders, not five levels deep)./orders/latest vs /orders/{id} works, because literals win, but it's fragile.@PathVariable interact with other request mappings?Short answer: Class-level and method-level patterns are combined, so variables can appear at either level. @PathVariable works alongside @RequestParam, @RequestBody and headers in the same method. When several mappings could match, Spring chooses the most specific (fewest variables and wildcards). Two equally specific mappings cause a startup error.
@RestController
@RequestMapping("/tenants/{tenantId}/invoices")
class InvoiceController {
@GetMapping("/{invoiceId}")
InvoiceDto get(@PathVariable String tenantId, @PathVariable long invoiceId,
@RequestParam(defaultValue = "false") boolean includeLines) { … }
}
Q: What's the difference between @RequestParam and @ModelAttribute?
A: @RequestParam binds a single parameter. @ModelAttribute binds many parameters onto an object's properties (and adds the object to the model). It's useful for search forms with many optional filters.
Q: How do you make a path variable optional?
A: Map two paths to the same method (@GetMapping({"/reports", "/reports/{year}"})), and declare @PathVariable(required = false) Integer year, or use Optional<Integer>.
Q: How do you trim whitespace from all incoming string parameters?
A: Register a StringTrimmerEditor in an @InitBinder method inside a @ControllerAdvice. For JSON bodies, configure a Jackson deserialiser instead.
Q: Where should static files live in a modern setup?
A: For SPAs, usually on a CDN or object storage, built by the frontend pipeline. The Spring app then serves only the API. Serving from /static is fine for server-rendered apps and admin pages.