Filters
Filters are the key to implementing cross-cutting concerns in Sproogy. They provide a powerful way to intercept requests and responses before they reach controllers.
What Are Filters?
Filters implement the Chain of Responsibility pattern. Each filter can:
- Pre-process requests (before the controller)
- Post-process responses (after the controller)
- Short-circuit the chain (block requests)
- Modify request/response data
Think of filters as middleware in Express.js or interceptors in Angular.
Filter Interface
public interface Filter {
void doFilter(Request request, Response response, FilterChain chain) throws Exception;
}
Parameters:
request: Incoming request (read/write)response: Outgoing response (write only before chain, read/write after chain)chain: Callchain.doFilter()to continue to next filter
Filter Execution Order
Filters execute in a stack-based order:
Execution:
- Filter 1 pre-processing
- Filter 2 pre-processing
- Filter 3 pre-processing
- Controller executes
- Filter 3 post-processing
- Filter 2 post-processing
- Filter 1 post-processing
Creating a Filter
Step 1: Implement the Interface
@Component
public class LoggingFilter implements Filter {
@Override
public void doFilter(Request request, Response response, FilterChain chain) throws Exception {
// PRE-PROCESSING
long startTime = System.currentTimeMillis();
System.out.println("[REQUEST] " + request.getEndpoint());
// CONTINUE CHAIN
chain.doFilter(request, response);
// POST-PROCESSING
long duration = System.currentTimeMillis() - startTime;
System.out.println("[RESPONSE] " + response.getStatus() + " (" + duration + "ms)");
}
}
Step 2: Register as Component
Use @Component to let Sproogy discover and register the filter automatically.
Common Filter Patterns
1. Authentication Filter
Check credentials before allowing access:
@Component
public class AuthenticationFilter implements Filter {
@Autowired
private TokenService tokenService;
@Override
public void doFilter(Request request, Response response, FilterChain chain) throws Exception {
String authHeader = request.getHeaders().get("Authorization");
if (authHeader == null || !authHeader.startsWith("Bearer ")) {
response.setStatus(401);
response.setData("Missing or invalid Authorization header");
return; // ❌ Don't call chain.doFilter() = request blocked
}
String token = authHeader.substring(7); // Remove "Bearer "
if (!tokenService.validateToken(token)) {
response.setStatus(401);
response.setData("Invalid token");
return; // ❌ Blocked
}
// Extract user info and add to request headers (for controllers to use)
String userId = tokenService.getUserId(token);
request.getHeaders().put("X-User-Id", userId);
// ✅ Token valid, continue
chain.doFilter(request, response);
}
}
Usage in controller:
@AppMapping("/profile")
public ResponseEntity<Profile> getProfile(Request request) {
String userId = request.getHeaders().get("X-User-Id"); // Injected by filter
return ResponseEntity.ok(profileService.getProfile(userId));
}
2. Rate Limiting Filter
Prevent abuse with rate limiting:
@Component
public class RateLimitFilter implements Filter {
private final Map<String, RateLimiter> rateLimiters = new ConcurrentHashMap<>();
@Override
public void doFilter(Request request, Response response, FilterChain chain) throws Exception {
String clientId = extractClientId(request);
RateLimiter limiter = rateLimiters.computeIfAbsent(clientId, k ->
RateLimiter.create(10.0) // 10 requests per second
);
if (!limiter.tryAcquire()) {
response.setStatus(429); // Too Many Requests
response.setData("Rate limit exceeded. Try again later.");
response.getHeaders().put("Retry-After", "1");
return; // Blocked
}
chain.doFilter(request, response);
}
private String extractClientId(Request request) {
String auth = request.getHeaders().get("Authorization");
return auth != null ? auth : "anonymous";
}
}
3. Encryption Filter
Decrypt requests, encrypt responses:
@Component
public class EncryptionFilter implements Filter {
@Autowired
private EncryptionService encryptionService;
@Override
public void doFilter(Request request, Response response, FilterChain chain) throws Exception {
// PRE: Decrypt request data if encrypted
String encryptedFlag = request.getHeaders().get("X-Encrypted");
if ("true".equals(encryptedFlag)) {
Object decryptedData = encryptionService.decrypt(request.getData());
request.setData(decryptedData);
}
// Continue chain
chain.doFilter(request, response);
// POST: Encrypt response if client expects it
if ("true".equals(encryptedFlag)) {
Object encryptedData = encryptionService.encrypt(response.getData());
response.setData(encryptedData);
response.getHeaders().put("X-Encrypted", "true");
}
}
}
Best Practices
✅ Keep filters focused: One concern per filter
✅ Always call chain.doFilter(): Unless you intentionally block the request
✅ Handle exceptions: Prevent one filter from breaking the entire chain
✅ Use meaningful status codes: 400, 401, 403, 429, 500
✅ Log important events: Auth failures, rate limit violations
Next Steps
👉 Routing