How do you scope a @ControllerAdvice to only some controllers using basePackages, assignableTypes, and annotations?
answer
- default = global
- basePackages/value + basePackageClasses = by package
- assignableTypes = by base class/interface
- annotations = by marker annotation
- selectors OR-combine; empty = all
basics
~10 sBy default a @ControllerAdvice applies to every controller. You can narrow it with its attributes: basePackages/basePackageClasses (controllers in those packages), assignableTypes (controllers of those specific types/interfaces), and annotations (controllers marked with a given annotation).
solid answer
~40 s@ControllerAdvice is global by default — its @ExceptionHandler/@InitBinder/@ModelAttribute methods apply to all controllers. Its attributes let you restrict the target set: basePackages (alias value) and basePackageClasses limit it to controllers in those packages/subpackages; assignableTypes limits it to controllers that are instances of the listed classes/interfaces; and annotations limits it to controllers annotated with the given annotation (e.g. a custom @PublicApi or @RestController). These are OR-combined selectors. This is how you run different error contracts side by side — say one @RestControllerAdvice for your public API package and another for admin controllers — without them interfering. A common pattern is defining marker annotations (@ApiV1, @InternalApi) and scoping advice by them. If you leave the attributes empty, the advice stays global, which is the usual choice for a single unified error handler.
code
java · 14 lines// Only controllers in com.acme.api.public (and sub-packages)
@RestControllerAdvice(basePackages = "com.acme.api.public")
public class PublicApiExceptionHandler { /* RFC 7807 bodies */ }
// Only controllers implementing AdminApi, regardless of package
@RestControllerAdvice(assignableTypes = AdminApi.class)
public class AdminExceptionHandler { /* legacy error envelope */ }
// Only controllers tagged with a custom marker annotation
@Target(ElementType.TYPE) @Retention(RetentionPolicy.RUNTIME)
public @interface PublicApi {}
@RestControllerAdvice(annotations = PublicApi.class)
public class TaggedApiExceptionHandler { /* ... */ }go deeper
Know advice is global by default and can be narrowed by package/type/annotation.
Correctly map each attribute (basePackages/assignableTypes/annotations) to what it selects.
Design multiple coexisting advices (public vs admin) and resolve conflicts via scoping/order.
Use scoping to enforce per-module/per-API-version error contracts in a modular codebase.
## Default: global An `@ControllerAdvice` with no attributes applies to **every** `@Controller`/`@RestController` in the application context. Most apps want exactly one such global advice. Scoping matters when you need **different behavior for different groups** of controllers. ## The selector attributes `@ControllerAdvice` (and `@RestControllerAdvice`, which shares them) exposes: - **`value` / `basePackages`** (`value` is an alias for `basePackages`): a list of package names. The advice applies only to controllers whose class lives in one of those packages or a **sub-package**. Example: `@ControllerAdvice("com.acme.api.public")`. - **`basePackageClasses`**: same idea but **type-safe** — you pass marker classes and their packages are used, so a rename/refactor doesn't break a string. Example: `@ControllerAdvice(basePackageClasses = PublicApiMarker.class)`. - **`assignableTypes`**: the advice applies only to controllers that are **assignable to** (i.e. instances/implementations of) the listed classes or interfaces. Great when your controllers share a base class or interface: `@ControllerAdvice(assignableTypes = { OrderController.class, PaymentController.class })` or an interface like `SecuredApi.class`. - **`annotations`**: the advice applies only to controllers **annotated with** one of the given annotations — including your own meta/marker annotations. Example: `@ControllerAdvice(annotations = RestController.class)` targets only `@RestController`s; `@ControllerAdvice(annotations = PublicApi.class)` targets controllers you tagged `@PublicApi`. ## Combination semantics When you specify multiple selector kinds, a controller is in scope if it matches **any** of them (they broaden the set, OR-style). Within a kind, listing several values also OR-combines. Leaving all empty = global. ## Why scope at all - **Multiple error contracts**: a public API advice returning RFC 7807 ProblemDetail vs an internal/admin advice returning a legacy envelope. - **Versioned APIs**: `@ApiV1`-tagged controllers get one advice, `@ApiV2` another. - **Isolation in modular apps** (e.g. Spring Modulith): keep a module's error translation from leaking onto another module's controllers. ## Gotchas - Two **unscoped** advices that both handle the same exception create ambiguity; scoping (or `@Order`) resolves who wins. Ordering still applies among multiple in-scope advices. - `basePackages` matches **sub-packages** too — easy to over-capture. - `annotations = RestController.class` catches every `@RestController`; if you also meant to exclude some, prefer a dedicated marker annotation. - Scoping is by **controller** type/location, not by exception type — you still choose exception coverage via the `@ExceptionHandler` signatures. - An advice bean must still be component-scanned; scoping only filters which controllers it targets, not whether the bean exists. ## When to use Default to a single global advice. Reach for `assignableTypes`/`annotations`/`basePackages` only when you genuinely need divergent error handling for distinct controller groups — otherwise scoping adds complexity for no benefit.
- Why prefer basePackageClasses over basePackages(String)?basePackageClasses is refactor-safe and type-checked: you point at a real class (often a package-marker), so renaming the package updates automatically, whereas a String package name silently breaks.
- If two in-scope advices both handle IllegalArgumentException, how is the winner chosen?By ordering — implement Ordered or use @Order on the advice beans; the lowest order value (highest precedence) wins. Scoping them to disjoint controller sets avoids the conflict entirely.
saying these in an interview costs you the question
- Believing @ControllerAdvice applies only to controllers in the same package by default
- Thinking assignableTypes filters by exception type rather than controller type
- Assuming basePackages does not include sub-packages
- Expecting multiple selector attributes to AND together