|
| 1 | +--- |
| 2 | +title: Spring Request Context Options |
| 3 | +lang: en |
| 4 | +layout: post |
| 5 | +audio: false |
| 6 | +translated: false |
| 7 | +generated: true |
| 8 | +image: false |
| 9 | +--- |
| 10 | + |
| 11 | +Great use-case for a tiny “request context” in Spring. You’ve got a few clean options—pick one based on your stack: |
| 12 | + |
| 13 | +### 1) Request-scoped bean (simple & safe) |
| 14 | + |
| 15 | +Store the test user id in a request-scoped bean that your `TestUserFilter` sets, and any component can inject/read. |
| 16 | + |
| 17 | +```java |
| 18 | +// holds the value for the lifetime of a single HTTP request |
| 19 | +@Component |
| 20 | +@RequestScope |
| 21 | +public class TestUserContext { |
| 22 | + private String userId; |
| 23 | + public String getUserId() { return userId; } |
| 24 | + public void setUserId(String userId) { this.userId = userId; } |
| 25 | +} |
| 26 | +``` |
| 27 | + |
| 28 | +```java |
| 29 | +@Component |
| 30 | +public class TestUserFilter extends OncePerRequestFilter { |
| 31 | + private final TestUserContext ctx; |
| 32 | + public TestUserFilter(TestUserContext ctx) { this.ctx = ctx; } |
| 33 | + |
| 34 | + @Override |
| 35 | + protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) |
| 36 | + throws ServletException, IOException { |
| 37 | + String id = req.getHeader("X-Test-User-Id"); |
| 38 | + if (id == null) id = req.getParameter("testUserId"); |
| 39 | + if (id != null && !id.isBlank()) ctx.setUserId(id); |
| 40 | + chain.doFilter(req, res); // request-scope bean is auto-cleaned after request ends |
| 41 | + } |
| 42 | +} |
| 43 | +``` |
| 44 | + |
| 45 | +Use anywhere: |
| 46 | + |
| 47 | +```java |
| 48 | +@Service |
| 49 | +public class SomeService { |
| 50 | + private final TestUserContext ctx; |
| 51 | + public SomeService(TestUserContext ctx) { this.ctx = ctx; } |
| 52 | + public void doWork() { |
| 53 | + String testUserId = ctx.getUserId(); // null if not provided |
| 54 | + } |
| 55 | +} |
| 56 | +``` |
| 57 | + |
| 58 | +**Why:** No manual cleanup; works across controllers/services/DAOs within the same request. |
| 59 | + |
| 60 | +--- |
| 61 | + |
| 62 | +### 2) Spring Security context (recommended if you already use Spring Security) |
| 63 | + |
| 64 | +Let the filter set an `Authentication` so all components can read it via `SecurityContextHolder` or `@AuthenticationPrincipal`. |
| 65 | + |
| 66 | +```java |
| 67 | +@Component |
| 68 | +public class TestUserFilter extends OncePerRequestFilter { |
| 69 | + @Override |
| 70 | + protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) |
| 71 | + throws IOException, ServletException { |
| 72 | + String id = req.getHeader("X-Test-User-Id"); |
| 73 | + if (id == null) id = req.getParameter("testUserId"); |
| 74 | + |
| 75 | + if (id != null && !id.isBlank()) { |
| 76 | + var auth = new UsernamePasswordAuthenticationToken( |
| 77 | + id, "N/A", List.of(new SimpleGrantedAuthority("ROLE_TEST"))); |
| 78 | + SecurityContextHolder.getContext().setAuthentication(auth); |
| 79 | + } |
| 80 | + try { |
| 81 | + chain.doFilter(req, res); |
| 82 | + } finally { |
| 83 | + SecurityContextHolder.clearContext(); |
| 84 | + } |
| 85 | + } |
| 86 | +} |
| 87 | +``` |
| 88 | + |
| 89 | +Use anywhere: |
| 90 | + |
| 91 | +```java |
| 92 | +String testUserId = (String) SecurityContextHolder.getContext().getAuthentication().getPrincipal(); |
| 93 | +``` |
| 94 | + |
| 95 | +or inject `Principal`/`Authentication` in controllers. |
| 96 | + |
| 97 | +**Why:** Plays nicely with method security, auditing, logs, etc. |
| 98 | + |
| 99 | +--- |
| 100 | + |
| 101 | +### 3) ThreadLocal holder (works without web/security deps; remember to clear) |
| 102 | + |
| 103 | +If you’re not using request scope or Security: |
| 104 | + |
| 105 | +```java |
| 106 | +public final class TestUserHolder { |
| 107 | + private static final ThreadLocal<String> USER_ID = new InheritableThreadLocal<>(); |
| 108 | + public static void set(String id) { USER_ID.set(id); } |
| 109 | + public static String get() { return USER_ID.get(); } |
| 110 | + public static void clear() { USER_ID.remove(); } |
| 111 | +} |
| 112 | +``` |
| 113 | + |
| 114 | +```java |
| 115 | +@Component |
| 116 | +public class TestUserFilter extends OncePerRequestFilter { |
| 117 | + @Override |
| 118 | + protected void doFilterInternal(HttpServletRequest req, HttpServletResponse res, FilterChain chain) |
| 119 | + throws IOException, ServletException { |
| 120 | + String id = req.getHeader("X-Test-User-Id"); |
| 121 | + if (id == null) id = req.getParameter("testUserId"); |
| 122 | + if (id != null && !id.isBlank()) TestUserHolder.set(id); |
| 123 | + try { |
| 124 | + chain.doFilter(req, res); |
| 125 | + } finally { |
| 126 | + TestUserHolder.clear(); // CRITICAL for pooled threads |
| 127 | + } |
| 128 | + } |
| 129 | +} |
| 130 | +``` |
| 131 | + |
| 132 | +If you run async tasks and need propagation, add a `TaskDecorator` to copy the ThreadLocal. |
| 133 | + |
| 134 | +--- |
| 135 | + |
| 136 | +### Quick tips |
| 137 | + |
| 138 | +* Put the id in a header like `X-Test-User-Id` (easy to pass from Postman/MockMvc) or a query param for local testing. |
| 139 | +* For unit tests with MockMvc: `.header("X-Test-User-Id", "u123")`. |
| 140 | +* Prefer **Option 1** if you want a simple “bag” visible to any bean in the same HTTP request. |
| 141 | +* Prefer **Option 2** if you already use Spring Security or want authorization/auditing integration. |
| 142 | +* Use **Option 3** only when you don’t want Spring scopes/security; be meticulous about clearing to avoid leaks. |
0 commit comments