skip to content

How does methodOn() work internally, what are its limitations, and how does that affect testing and design?

level: principalimportance: nice to knowfreq 22%

answer

  1. DummyInvocationUtils → recording CGLIB proxy
  2. records Method + args (LastInvocationAware)
  3. no real logic runs; return value is a dummy
  4. Kotlin final default → need all-open/kotlin-spring
  5. test with MockMvc; assert path suffix not full URL

basics

~20 s

methodOn returns a CGLIB/proxy of the controller (via DummyInvocationUtils) that records the last method call and its arguments without running real logic. linkTo replays that recording to build the URL. It can't proxy final controllers/methods and needs a request context.

solid answer

~50 s

methodOn(Controller.class, ...) creates a recording proxy using DummyInvocationUtils.methodOn — typically a CGLIB subclass — whose only job is to capture the invoked method and its arguments as a LastInvocation. When you 'call' controller.getItem(id) on it, no controller code runs; linkTo(...) then reads the recorded method plus its @RequestMapping metadata to build the path, filling @PathVariable/@RequestParam from the captured args. Limitations follow from proxying: the controller class and target method must be proxyable (not final; Kotlin classes/methods are final by default, so you need all-open or an interface); constructor side effects are irrelevant because the proxy bypasses them; and the returned 'value' is a dummy you must not use. It still requires a request-bound thread. For tests, prefer MockMvc/integration tests that assert on the href path, since base URI depends on the request; unit-testing a controller that calls linkTo without a request context throws.

code

java · 18 lines
java
// Test: bind a mock request so linkTo has a base URI, then assert the path.
import org.springframework.mock.web.MockHttpServletRequest;
import org.springframework.web.context.request.RequestContextHolder;
import org.springframework.web.context.request.ServletRequestAttributes;
import static org.springframework.hateoas.server.mvc.WebMvcLinkBuilder.*;
import static org.assertj.core.api.Assertions.assertThat;

@Test
void selfLinkResolves() {
    RequestContextHolder.setRequestAttributes(
        new ServletRequestAttributes(new MockHttpServletRequest()));

    var link = linkTo(methodOn(ItemController.class).getItem(42L)).withSelfRel();

    assertThat(link.getRel().value()).isEqualTo("self");
    assertThat(link.getHref()).endsWith("/items/42"); // assert suffix, not full host
    RequestContextHolder.resetRequestAttributes();
}

go deeper

for a junior

Just know methodOn is a proxy that records the call; you don't need the internals.

for a middle

Explain that no real logic runs and the return value is a placeholder.

for a senior

Describe CGLIB proxying, the Kotlin final-default pitfall, and MockMvc-based testing.

for a principal

Treat controller signatures as a link contract; centralize assembly in assemblers; reason about proxy cost, context propagation, and test determinism.

**The mechanism.** `WebMvcLinkBuilder.methodOn(Class, args...)` delegates to Spring HATEOAS's `DummyInvocationUtils.methodOn(...)`. It builds a **proxy** of the controller type (a CGLIB subclass for classes, or a JDK dynamic proxy for interfaces) whose method bodies do nothing except **record** the invocation: which `Method` was called and the actual argument array, stored as a `LastInvocation`/`MethodInvocation`. The proxy implements a marker (`LastInvocationAware`) so `linkTo(...)` can pull the recorded invocation back out. `linkTo` then reads the mapping annotations on that `Method` and the controller class, substitutes template variables from the recorded args, resolves the base URI from the current request, and yields a `WebMvcLinkBuilder`. **Why a proxy at all?** It gives you **refactor-safe, type-checked** link building: `methodOn(ItemController.class).getItem(id)` won't compile if the method signature changes, unlike a hand-written `"/items/" + id`. The trade-off is the machinery below. **Limitations and gotchas (the principal-level substance):** - **Proxyability.** CGLIB cannot subclass `final` classes or override `final`/`private`/`static` methods. In **Kotlin** classes and members are `final` by default, so `methodOn` on a Kotlin controller needs the `kotlin-spring`/`all-open` plugin (or an interface) — a very common surprise. - **The return value is a dummy.** `methodOn(...).getItem(id)` returns a placeholder; never inspect or dereference it — only `linkTo` should consume the recorded call. - **No real logic runs.** Validation, security checks, and side effects in the method are skipped; only the signature and args matter. Overloaded methods resolve by the arguments you pass. - **Request-context dependence.** Like all `linkTo`, it reads `RequestContextHolder`; off-request threads (schedulers, async, some tests) fail. - **Cost.** Proxy creation per call is cheap but not free; on ultra-hot paths some teams cache the path template or use `linkTo(Controller.class).slash(...)` to avoid the proxy. Rarely a real bottleneck. - **WebFlux.** Use `WebFluxLinkBuilder` (reactive) instead; it returns `Mono<Link>`/uses the exchange for context rather than thread-local request attributes. **Testing implications.** - Prefer **MockMvc** or full integration tests: they establish a request context and let you assert on the JSON `_links.*.href`. Assert on the **path suffix**, not the full absolute URL, because host/scheme come from the (mock) request and can vary. - Pure unit tests that call a controller method invoking `linkTo` will throw unless you bind a request — set up `RequestContextHolder` with a `MockHttpServletRequest` (via `RequestContextHolder.setRequestAttributes(new ServletRequestAttributes(new MockHttpServletRequest()))`) or restructure so link assembly is isolated. **Design guidance.** Keep link assembly in the web layer, expose stable controller method signatures (they are effectively part of your link contract), ensure controllers are open for proxying, and centralize proxy-based assembly in a `RepresentationModelAssembler` so the mechanics and testing live in one place.

  • Why can methodOn fail specifically on a Kotlin controller?
    CGLIB must subclass the controller and override the method, but Kotlin classes and methods are final by default. The kotlin-spring/all-open plugin (or coding against an interface) makes them open so the proxy can be created.
  • In a MockMvc test, why assert on the href suffix rather than the full URL?
    The base URI (scheme/host/port) is derived from the mock request, so it can differ across setups. The path is what your mapping controls, so asserting endsWith("/items/42") is stable and intent-revealing.

saying these in an interview costs you the question

  • Using the value returned by methodOn(...).method() as if it were a real result.
  • Believing methodOn runs controller logic (validation/security) — it doesn't.
  • Ignoring that Kotlin's final-by-default breaks CGLIB proxying of controllers.
  • Unit-testing linkTo with no request context and expecting it to work.

context