QuarkFlix Guard Duty: A Hands-On CDI Interceptor Tutorial with Quarkus
Learn how to build modular, secure, and testable Java applications with CDI interceptors in Quarkus — all while helping run the world’s most secure (and imaginary) streaming platform.
Welcome to QuarkFlix, the streaming platform where every movie request is secured by a battalion of CDI interceptors. Whether it's logging suspicious binge patterns, blocking underage viewers, or giving VIPs early access to that hot new sci-fi reboot, you’re the engineer behind the guards.
In this hands-on tutorial, you'll build four powerful interceptors using Quarkus and CDI (Contexts and Dependency Injection). Each interceptor is presented as a "mission" that reflects a real-world concern in modern service architectures.
What You’ll Learn
How CDI interceptors work in Quarkus.
Creating custom interceptor bindings.
Using
@AroundInvoke
,InvocationContext
, and@Priority
.Writing unit tests with
@QuarkusTest
and config profiles.Applying interceptors to solve real-world problems.
What are CDI Interceptors?
In a nutshell, CDI (Contexts and Dependency Injection) interceptors allow you to add cross-cutting logic (like logging, security checks, or transaction management) to your business methods in a clean and reusable way. They work by "intercepting" method calls, allowing you to execute code before, after, or around the actual method execution. This is a core concept of Aspect-Oriented Programming (AOP).
Phase 0: Spinning Up QuarkFlix
Let’s bootstrap a fresh Quarkus project:
mvn io.quarkus.platform:quarkus-maven-plugin:create \
-DprojectGroupId=com.quarkflix \
-DprojectArtifactId=quarkflix-guards \
-DclassName="com.quarkflix.resource.StreamingResource" \
-Dpath="/stream" \
-Dextensions="resteasy-jackson"
cd quarkflix-guards
You'll be using:
Quarkus - CDI for Interceptor Magic
REST Jackson – For HTTP endpoints.
JUnit5 + REST Assured – For testing.
And if you can not wait and want to see the code in action, make sure to check out the quarkus-quarkflix project in my Github repository.
Mission 1: “The Auditron”. The Activity Logger
CDI interceptors allow you to modularize cross-cutting concerns like logging, metrics, and security. The @AuditLog
interceptor will capture method calls for user actions like streaming or accessing account details.
You’ll create:
A custom annotation
@AuditLog
.An interceptor using
@AroundInvoke
.A Quarkus service to be audited.
REST endpoints.
Unit tests verifying the audit logs.
This interceptor teaches you the basics: how to bind, intercept, and inspect method metadata.
Define the @AuditLog
Binding Annotation
Create src/main/java/com/quarkflix/annotation/AuditLog.java
:
package com.quarkflix.annotation;
import jakarta.enterprise.util.Nonbinding;
import jakarta.interceptor.InterceptorBinding;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Inherited
@InterceptorBinding
@Retention(RetentionPolicy.RUNTIME)
@Target({ElementType.TYPE, ElementType.METHOD})
public @interface AuditLog {
@Nonbinding // This allows us to provide a default value or use it without specifying
String value() default "Default Audit"; // Optional: description of the audited action
}
Implement the AuditLogInterceptor
Create src/main/java/com/quarkflix/interceptor/AuditLogInterceptor.java
:
package com.quarkflix.interceptor;
import com.quarkflix.annotation.AuditLog;
import jakarta.annotation.Priority;
import jakarta.interceptor.AroundInvoke;
import jakarta.interceptor.Interceptor;
import jakarta.interceptor.InvocationContext;
import org.jboss.logging.Logger; // Using JBoss Logging, Quarkus default
import java.util.Arrays;
@AuditLog
@Interceptor
@Priority(Interceptor.Priority.APPLICATION + 200) // Define a priority
public class AuditLogInterceptor {
private static final Logger LOG = Logger.getLogger(AuditLogInterceptor.class);
// For testing purposes, we'll store the last log message
// In a real app, you'd use a robust logging framework or send to a log aggregator
public static String lastLoggedMessage_test_only = null;
@AroundInvoke
Object logInvocation(InvocationContext context) throws Exception {
String methodName = context.getMethod().getName();
String className = context.getTarget().getClass().getSimpleName();
String params = Arrays.toString(context.getParameters());
// Get the optional value from the annotation if needed
AuditLog auditLogAnnotation = context.getMethod().getAnnotation(AuditLog.class);
if (auditLogAnnotation == null) { // Could be on class level
auditLogAnnotation = context.getTarget().getClass().getAnnotation(AuditLog.class);
}
String auditDescription = auditLogAnnotation != null ? auditLogAnnotation.value() : "N/A";
String logMessage = String.format("AUDIT [%s]: Method '%s.%s' called with params %s.",
auditDescription, className, methodName, params);
LOG.info(logMessage);
lastLoggedMessage_test_only = logMessage; // Store for testing
Object ret = context.proceed(); // Execute the original method
LOG.info(String.format("AUDIT [%s]: Method '%s.%s' completed.", auditDescription, className, methodName));
return ret;
}
}
Note: We added lastLoggedMessage_test_only
to make testing simpler by checking this static variable. In a real app, you'd assert against a log capture mechanism or mock a logging service.
Define a User Model and Streaming Service
Create src/main/java/com/quarkflix/model/User.java
:
package com.quarkflix.model;
public class User {
private String username;
private int age;
private boolean vip;
public User(String username, int age, boolean vip) {
this.username = username;
this.age = age;
this.vip = vip;
}
public String getUsername() { return username; }
public int getAge() { return age; }
public boolean isVip() { return vip; }
@Override
public String toString() {
return "User{username='" + username + "'}";
}
}
Create src/main/java/com/quarkflix/service/StreamingService.java
:
package com.quarkflix.service;
import com.quarkflix.annotation.AuditLog;
import com.quarkflix.model.User;
import jakarta.enterprise.context.ApplicationScoped;
import org.jboss.logging.Logger;
@ApplicationScoped
public class StreamingService {
private static final Logger LOG = Logger.getLogger(StreamingService.class);
@AuditLog("User Playback") // Applying the interceptor
public String playMovie(User user, String movieId) {
String message = String.format("User %s is now playing movie %s.", user.getUsername(), movieId);
LOG.info("Core logic: " + message); // Simulate business logic
return message;
}
@AuditLog("Account Access")
public String getAccountDetails(User user) {
String message = String.format("Fetching account details for %s.", user.getUsername());
LOG.info("Core logic: " + message); // Simulate business logic
return message + " Balance: $10.00";
}
// A method without the interceptor for comparison
public String browseCatalog(User user) {
String message = String.format("User %s is Browse the catalog.", user.getUsername());
LOG.info("Core logic: " + message);
return message;
}
}
Create a REST Resource (for manual testing via HTTP)
Modify src/main/java/com/quarkflix/resource/StreamingResource.java
:
package com.quarkflix.resource;
import com.quarkflix.model.User;
import com.quarkflix.service.StreamingService;
import jakarta.inject.Inject;
import jakarta.ws.rs.GET;
import jakarta.ws.rs.Path;
import jakarta.ws.rs.Produces;
import jakarta.ws.rs.QueryParam;
import jakarta.ws.rs.core.MediaType;
@Path("/stream")
public class StreamingResource {
@Inject
StreamingService streamingService;
@GET
@Path("/play")
@Produces(MediaType.TEXT_PLAIN)
public String play(@QueryParam("user") String userName, @QueryParam("movie") String movieId) {
User user = new User(userName != null ? userName : "Guest", 25, false);
return streamingService.playMovie(user, movieId != null ? movieId : "default_movie_id");
}
@GET
@Path("/account")
@Produces(MediaType.TEXT_PLAIN)
public String account(@QueryParam("user") String userName) {
User user = new User(userName != null ? userName : "Guest", 25, false);
return streamingService.getAccountDetails(user);
}
@GET
@Path("/browse")
@Produces(MediaType.TEXT_PLAIN)
public String browse(@QueryParam("user") String userName) {
User user = new User(userName != null ? userName : "Guest", 25, false);
return streamingService.browseCatalog(user);
}
}
Testing Your @AuditLog
Guard
Create src/test/java/com/quarkflix/interceptor/AuditLogInterceptorTest.java
:
package com.quarkflix.interceptor;
import com.quarkflix.model.User;
import com.quarkflix.service.StreamingService;
import io.quarkus.test.junit.QuarkusTest;
import jakarta.inject.Inject;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
import org.jboss.logging.Logger;
@QuarkusTest
public class AuditLogInterceptorTest {
private static final Logger LOG = Logger.getLogger(AuditLogInterceptorTest.class);
@Inject
StreamingService streamingService;
@BeforeEach
void setUp() {
// Reset the test-specific static variable before each test
AuditLogInterceptor.lastLoggedMessage_test_only = null;
}
@Test
void testPlayMovieIsAudited() {
User user = new User("testUser", 30, false);
String movieId = "movie123";
String result = streamingService.playMovie(user, movieId);
assertNotNull(result, "Service method should return a result.");
assertEquals(String.format("User %s is now playing movie %s.", user.getUsername(), movieId), result);
assertNotNull(AuditLogInterceptor.lastLoggedMessage_test_only, "Audit log message should have been captured.");
assertTrue(AuditLogInterceptor.lastLoggedMessage_test_only.contains("AUDIT"));
assertTrue(AuditLogInterceptor.lastLoggedMessage_test_only.contains("params [" + user.toString() + ", " + movieId + "]"), "Log message parameters are incorrect.");
}
@Test
void testGetAccountDetailsIsAudited() {
User user = new User("accountUser", 40, true);
streamingService.getAccountDetails(user);
assertNotNull(AuditLogInterceptor.lastLoggedMessage_test_only);
assertTrue(AuditLogInterceptor.lastLoggedMessage_test_only.contains("Account Access"));
}
@Test
void testBrowseCatalogIsNotAudited() {
User user = new User("browseUser", 20, false);
streamingService.browseCatalog(user); // This method is not annotated with @AuditLog
assertNull(AuditLogInterceptor.lastLoggedMessage_test_only, "Browse catalog should not trigger audit log interceptor.");
}
}
Running and Observing
Run Quarkus in dev mode:
./mvnw quarkus:dev
Access the endpoints:
http://localhost:8080/stream/play?user=Alice&movie=Matrix
http://localhost:8080/stream/account?user=Bob
http://localhost:8080/stream/browse?user=Charlie
Observe the console logs. You should see messages from
AuditLogInterceptor
forplayMovie
andgetAccountDetails
, but not forbrowseCatalog
. The logs will also show the "Core logic" messages.Run the tests:
./mvnw test
or run them from your IDE. They should pass!
Mission 2: “The Age Verifier”. Implement Content Restrictions
Want to ensure nobody under 18 is watching gory thrillers at 2am? This interceptor introduces parameter inspection and business rule enforcement.
New annotation:
@RequiresAgeVerification(minimumAge)
Logic to inspect method parameters and throw custom exceptions
Execution ordering via
@Priority
You'll learn how to extract arguments from InvocationContext
, and how interceptor order determines control flow.
Define Custom Exception
Create src/main/java/com/quarkflix/exception/ContentRestrictionException.java
:
package com.quarkflix.exception;
public class ContentRestrictionException extends RuntimeException {
public ContentRestrictionException(String message) {
super(message);
}
}
Define the @RequiresAgeVerification
Binding Annotation
Create src/main/java/com/quarkflix/annotation/RequiresAgeVerification.java
:
package com.quarkflix.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import jakarta.enterprise.util.Nonbinding;
import jakarta.interceptor.InterceptorBinding;
@Inherited
@InterceptorBinding
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
public @interface RequiresAgeVerification {
@Nonbinding // This allows us to provide a default value or use it without specifying
public int minimumAge(); // Member to specify the required age
}
Implement the AgeVerificationInterceptor
Create src/main/java/com/quarkflix/interceptor/AgeVerificationInterceptor.java
:
package com.quarkflix.interceptor;
import org.jboss.logging.Logger;
import com.quarkflix.annotation.RequiresAgeVerification;
import com.quarkflix.exception.ContentRestrictionException;
import com.quarkflix.model.User;
import jakarta.annotation.Priority;
import jakarta.interceptor.AroundInvoke;
import jakarta.interceptor.Interceptor;
import jakarta.interceptor.InvocationContext;
@RequiresAgeVerification(minimumAge = 0) // Default minimumAge for the binding, actual value comes from annotation instance
@Interceptor
@Priority(Interceptor.Priority.APPLICATION + 100) // Higher priority (lower number) means it runs before AuditLog
public class AgeVerificationInterceptor {
private static final Logger LOG = Logger.getLogger(AgeVerificationInterceptor.class);
@AroundInvoke
Object verifyAge(InvocationContext context) throws Exception {
RequiresAgeVerification ageVerificationAnnotation = context.getMethod().getAnnotation(RequiresAgeVerification.class);
int requiredAge = ageVerificationAnnotation.minimumAge();
User user = null;
// Attempt to find a User object in the method parameters
for (Object param : context.getParameters()) {
if (param instanceof User) {
user = (User) param;
break;
}
}
if (user == null) {
LOG.warnf("Method %s.%s is annotated with @RequiresAgeVerification but no User parameter was found. Skipping check.",
context.getTarget().getClass().getSimpleName(), context.getMethod().getName());
return context.proceed();
}
LOG.infof("AGE_VERIFY: User %s (age %d) attempting to access content requiring age %d.",
user.getUsername(), user.getAge(), requiredAge);
if (user.getAge() < requiredAge) {
String message = String.format("User %s (age %d) does not meet minimum age requirement of %d for %s.",
user.getUsername(), user.getAge(), requiredAge, context.getMethod().getName());
LOG.warn(message);
throw new ContentRestrictionException(message);
}
LOG.infof("AGE_VERIFY: User %s meets age requirement for %s.", user.getUsername(), context.getMethod().getName());
return context.proceed();
}
}
Note: The priority is set lower (e.g. +100
) than AuditLogInterceptor
(+200
) so age verification happens before detailed auditing of the method execution itself if both are applied.
Update StreamingService
Add a new method to src/main/java/com/quarkflix/service/StreamingService.java
:
// ... (inside StreamingService class)
@RequiresAgeVerification(minimumAge = 18) // Must be 18 or older
@AuditLog("Mature Content Playback") // Also audit this
public String streamMatureContent(User user, String movieId) {
String message = String.format("User %s is streaming mature content: %s.", user.getUsername(), movieId);
LOG.info("Core logic: " + message);
return message;
}
Update StreamingResource
Add a new endpoint to src/main/java/com/quarkflix/resource/StreamingResource.java
:
// ... (inside StreamingResource class)
@GET
@Path("/maturePlay")
@Produces(MediaType.TEXT_PLAIN)
public String maturePlay(@QueryParam("user") String userName, @QueryParam("age") int age, @QueryParam("movie") String movieId) {
User user = new User(userName != null ? userName : "Guest", age, false);
try {
return streamingService.streamMatureContent(user, movieId != null ? movieId : "mature_movie_id");
} catch (ContentRestrictionException e) {
// In a real JAX-RS app, you'd map this to an appropriate HTTP status (e.g., 403 Forbidden)
// using an ExceptionMapper.
return "ACCESS DENIED: " + e.getMessage();
}
}
Testing Your @RequiresAgeVerification
Guard
Create src/test/java/com/quarkflix/interceptor/AgeVerificationInterceptorTest.java
:
package com.quarkflix.interceptor;
import com.quarkflix.exception.ContentRestrictionException;
import com.quarkflix.model.User;
import com.quarkflix.service.StreamingService;
import io.quarkus.test.junit.QuarkusTest;
import jakarta.inject.Inject;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import static org.junit.jupiter.api.Assertions.*;
@QuarkusTest
public class AgeVerificationInterceptorTest {
@Inject
StreamingService streamingService;
@BeforeEach
void setUp() {
AuditLogInterceptor.lastLoggedMessage_test_only = null; // Reset audit log for consistent testing
}
@Test
void testMatureContentAccessGrantedForAdult() {
User adultUser = new User("AdultAdam", 25, false);
String movieId = "movie_for_adults";
String result = null;
try {
result = streamingService.streamMatureContent(adultUser, movieId);
} catch (ContentRestrictionException e) {
fail("Adult user should not be restricted: " + e.getMessage());
}
assertNotNull(result);
assertEquals(String.format("User %s is streaming mature content: %s.", adultUser.getUsername(), movieId),
result);
assertNotNull(AuditLogInterceptor.lastLoggedMessage_test_only,
"Audit log should be present for successful access.");
assertTrue(AuditLogInterceptor.lastLoggedMessage_test_only
.contains("Mature Content Playback"));
}
@Test
void testMatureContentAccessDeniedForMinor() {
User minorUser = new User("MinorMary", 15, false);
String movieId = "movie_for_adults";
ContentRestrictionException thrown = assertThrows(ContentRestrictionException.class, () -> {
streamingService.streamMatureContent(minorUser, movieId);
});
assertTrue(thrown.getMessage().contains("does not meet minimum age requirement of 18"));
assertNull(AuditLogInterceptor.lastLoggedMessage_test_only,
"AuditLogInterceptor should not have logged method entry if AgeVerificationInterceptor blocked execution.");
}
}
Running and Observing
Restart Quarkus if it was running:
./mvnw quarkus:dev
Access the endpoints:
http://localhost:8080/stream/maturePlay?user=MinorTom&age=16&movie=Hostel
(Should see "ACCESS DENIED")http://localhost:8080/stream/maturePlay?user=AdultAnna&age=22&movie=Hostel
(Should play successfully and be audited)
Observe logs for both scenarios.
Run the tests:
./mvnw test
.
Mission 3: “The Binge Monitor”. The Performance Watchdog
Some operations need a performance spotlight. You’ll build:
@TrackPerformance
– measures and logs execution timeA sleep-simulated method to track
Unit tests asserting millisecond bounds
This interceptor wraps methods in a stopwatch and gives you an early taste of building metrics tools.
Define the @TrackPerformance
Binding Annotation
Create src/main/java/com/quarkflix/annotation/TrackPerformance.java
:
package com.quarkflix.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import jakarta.interceptor.InterceptorBinding;
@Inherited
@InterceptorBinding
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
public @interface TrackPerformance {
}
Implement the PerformanceTrackingInterceptor
Create src/main/java/com/quarkflix/interceptor/PerformanceTrackingInterceptor.java
:
package com.quarkflix.interceptor;
import org.jboss.logging.Logger;
import com.quarkflix.annotation.TrackPerformance;
import jakarta.annotation.Priority;
import jakarta.interceptor.AroundInvoke;
import jakarta.interceptor.Interceptor;
import jakarta.interceptor.InvocationContext;
@TrackPerformance
@Interceptor
@Priority(Interceptor.Priority.APPLICATION + 150) // Between AgeVerifier (100) and AuditLog (200)
public class PerformanceTrackingInterceptor {
private static final Logger LOG = Logger.getLogger(PerformanceTrackingInterceptor.class);
public static long lastExecutionTime_test_only = -1;
@AroundInvoke
Object track(InvocationContext context) throws Exception {
long startTime = System.nanoTime();
try {
return context.proceed();
} finally {
long endTime = System.nanoTime();
long durationNanos = endTime - startTime;
long durationMillis = durationNanos / 1_000_000;
lastExecutionTime_test_only = durationMillis; // For testing
LOG.infof("PERF_TRACK: Method %s.%s executed in %d ms (%d ns).",
context.getTarget().getClass().getSimpleName(),
context.getMethod().getName(),
durationMillis,
durationNanos);
}
}
}
Update StreamingService
Add a new method to src/main/java/com/quarkflix/service/StreamingService.java
:
@TrackPerformance // Apply performance tracking
@AuditLog("Continue Watching List") // Also audit this action
public String getContinueWatchingList(User user) {
LOG.infof("Core logic: Fetching continue watching list for %s...", user.getUsername());
try {
// Simulate some work
Thread.sleep(50 + (long) (Math.random() * 100)); // Simulate 50-150ms delay
} catch (InterruptedException e) {
Thread.currentThread().interrupt();
}
return String.format("Continue Watching List for %s: [MovieA, MovieB, MovieC]", user.getUsername());
}
Order of Execution (based on priority):
AgeVerificationInterceptor
(if applied, Priority 100 - most "outer")PerformanceTrackingInterceptor
(Priority 150)AuditLogInterceptor
(Priority 200 - most "inner" before method)StreamingService.getContinueWatchingList()
method execution.
The logs should reflect this. For getContinueWatchingList
, only Performance and Audit will apply. Performance (150) will wrap Audit (200).
Update StreamingResource
Add a new endpoint to src/main/java/com/quarkflix/resource/StreamingResource.java
:
@GET
@Path("/continueWatching")
@Produces(MediaType.TEXT_PLAIN)
public String continueWatching(@QueryParam("user") String userName) {
User user = new User(userName != null ? userName : "Guest", 30, false);
return streamingService.getContinueWatchingList(user);
}
Testing Your @TrackPerformance
Guard
Create src/test/java/com/quarkflix/interceptor/PerformanceTrackingInterceptorTest.java
:
package com.quarkflix.interceptor;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import com.quarkflix.model.User;
import com.quarkflix.service.StreamingService;
import io.quarkus.test.junit.QuarkusTest;
import jakarta.inject.Inject;
@QuarkusTest
public class PerformanceTrackingInterceptorTest {
@Inject
StreamingService streamingService;
@BeforeEach
void setUp() {
AuditLogInterceptor.lastLoggedMessage_test_only = null;
PerformanceTrackingInterceptor.lastExecutionTime_test_only = -1;
}
@Test
void testContinueWatchingListPerformanceIsTrackedAndAudited() {
User user = new User("perfUser", 30, false);
String result = streamingService.getContinueWatchingList(user);
assertNotNull(result);
assertTrue(result.startsWith("Continue Watching List for perfUser"));
// Check Performance Tracking
assertTrue(PerformanceTrackingInterceptor.lastExecutionTime_test_only >= 50,
"Execution time should be recorded and be at least 50ms.");
assertTrue(PerformanceTrackingInterceptor.lastExecutionTime_test_only < 200,
"Execution time should be reasonable (sanity check)."); // Adjusted if Thread.sleep is longer
// Check Audit Log (it should still run)
// With Performance (150) and Audit (200), Performance is outer.
// AuditLogInterceptor should still be invoked.
assertNotNull(AuditLogInterceptor.lastLoggedMessage_test_only, "Audit log should not be null.");
assertTrue(AuditLogInterceptor.lastLoggedMessage_test_only
.contains("AUDIT"));
// To confirm order, you would need to capture multiple log lines and check
// their sequence.
// For this test, we primarily confirm both interceptors ran and did their job.
}
}
Running and Observing
Restart Quarkus.
Access
http://localhost:8080/stream/continueWatching?user=Binger
.Observe the logs. You should see messages from
PerformanceTrackingInterceptor
wrapping the execution, and messages fromAuditLogInterceptor
. The performance log should show a duration.Run the tests:
./mvnw test
.
Mission 4: “The Premiere Shield” — Feature Gating with VIP Access
Your final mission shows how to:
Inject configuration into interceptors
Enforce feature flags at runtime
Using @VipAccessRequired
, we’ll block regular users from accessing early-release movies — unless the config flag is off.
Add Configuration Property
In src/main/resources/application.properties
:
quarkflix.features.vip-premiere.enabled=true
Define the @VipAccessRequired
Binding Annotation
Create src/main/java/com/quarkflix/annotation/VipAccessRequired.java
:
package com.quarkflix.annotation;
import java.lang.annotation.ElementType;
import java.lang.annotation.Inherited;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
import jakarta.interceptor.InterceptorBinding;
@Inherited
@InterceptorBinding
@Retention(RetentionPolicy.RUNTIME)
@Target({ ElementType.TYPE, ElementType.METHOD })
public @interface VipAccessRequired {
}
Define FeatureAccessException
Create src/main/java/com/quarkflix/exception/FeatureAccessException.java
:
package com.quarkflix.exception;
public class FeatureAccessException extends RuntimeException {
public FeatureAccessException(String message) {
super(message);
}
}
Implement the VipAccessInterceptor
Create src/main/java/com/quarkflix/interceptor/VipAccessInterceptor.java
:
package com.quarkflix.interceptor;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import com.quarkflix.exception.FeatureAccessException;
import com.quarkflix.model.User;
import com.quarkflix.service.StreamingService;
import io.quarkus.test.junit.QuarkusTest;
import jakarta.inject.Inject;
@QuarkusTest
public class VipAccessInterceptorTest {
@Inject
StreamingService streamingService;
@BeforeEach
void setup() {
AuditLogInterceptor.lastLoggedMessage_test_only = null;
PerformanceTrackingInterceptor.lastExecutionTime_test_only = -1;
}
@Test
void testVipUserAccessesPremiere() {
User vipUser = new User("VipVictor", 25, true);
String result = streamingService.playPremiereMovie(vipUser, "ExclusiveFilm");
assertNotNull(result);
assertTrue(result.contains("VIP User VipVictor is playing premiere movie"));
assertNotNull(AuditLogInterceptor.lastLoggedMessage_test_only, "Audit log should exist for VIP user.");
assertTrue(PerformanceTrackingInterceptor.lastExecutionTime_test_only >= 0,
"Performance should be tracked for VIP user.");
}
@Test
void testNonVipUserBlockedFromPremiere() {
User nonVipUser = new User("RegularRita", 25, false);
FeatureAccessException thrown = assertThrows(FeatureAccessException.class, () -> {
streamingService.playPremiereMovie(nonVipUser, "ExclusiveFilm");
});
assertTrue(thrown.getMessage().contains("is not a VIP"));
assertNull(AuditLogInterceptor.lastLoggedMessage_test_only,
"AuditLogInterceptor should not log if blocked by VIP check.");
}
@Test
void testVipUserButUnderAgeBlockedFromPremiere() {
User youngVipUser = new User("YoungVipYara", 12, true); // VIP but underage (premiere requires 13)
assertThrows(com.quarkflix.exception.ContentRestrictionException.class, () -> { // Expecting age restriction
streamingService.playPremiereMovie(youngVipUser, "ExclusiveFilmPG13");
});
// VipAccessInterceptor (priority 50) passes.
// AgeVerificationInterceptor (priority 100) should block.
// Performance (150) and Audit (200) should not be fully executed for the method
// itself.
assertNull(AuditLogInterceptor.lastLoggedMessage_test_only,
"AuditLogInterceptor should not log method success if blocked by age check.");
}
}
Update StreamingService
Add a new method to src/main/java/com/quarkflix/service/StreamingService.java
@VipAccessRequired // VipAccessRequired annotation to check VIP status
@RequiresAgeVerification(minimumAge = 13) // Premieres might also have age ratings
@TrackPerformance
@AuditLog("VIP Premiere Playback")
public String playPremiereMovie(User user, String movieId) {
String message = String.format("VIP User %s is playing premiere movie: %s (Age %d).",
user.getUsername(), movieId, user.getAge());
LOG.info("Core logic: " + message);
return message;
}
Order of Execution:
VipAccessInterceptor
(+50)AgeVerificationInterceptor
(+100)PerformanceTrackingInterceptor
(+150)AuditLogInterceptor
(+200)Method
playPremiereMovie
Update StreamingResource
Add a new endpoint to src/main/java/com/quarkflix/resource/StreamingResource.java
:
@GET
@Path("/premiere")
@Produces(MediaType.TEXT_PLAIN)
public String premiere(@QueryParam("user") String userName,
@QueryParam("age") int age,
@QueryParam("isVip") boolean isVip,
@QueryParam("movie") String movieId) {
User user = new User(userName != null ? userName : "Guest", age, isVip);
try {
return streamingService.playPremiereMovie(user, movieId != null ? movieId : "premiere_movie_XYZ");
} catch (FeatureAccessException | ContentRestrictionException e) {
return "ACCESS DENIED: " + e.getMessage();
}
}
Testing Your @VipAccessRequired
Guard
Create src/test/java/com/quarkflix/interceptor/VipAccessInterceptorTest.java
:
package com.quarkflix.interceptor;
import static org.junit.jupiter.api.Assertions.assertNotNull;
import static org.junit.jupiter.api.Assertions.assertNull;
import static org.junit.jupiter.api.Assertions.assertThrows;
import static org.junit.jupiter.api.Assertions.assertTrue;
import org.junit.jupiter.api.BeforeEach;
import org.junit.jupiter.api.Test;
import com.quarkflix.exception.FeatureAccessException;
import com.quarkflix.model.User;
import com.quarkflix.service.StreamingService;
import io.quarkus.test.junit.QuarkusTest;
import jakarta.inject.Inject;
@QuarkusTest
public class VipAccessInterceptorTest {
@Inject
StreamingService streamingService;
@BeforeEach
void setup() {
AuditLogInterceptor.lastLoggedMessage_test_only = null;
PerformanceTrackingInterceptor.lastExecutionTime_test_only = -1;
}
@Test
void testVipUserAccessesPremiere() {
User vipUser = new User("VipVictor", 25, true);
String result = streamingService.playPremiereMovie(vipUser, "ExclusiveFilm");
assertNotNull(result);
assertTrue(result.contains("VIP User VipVictor is playing premiere movie"));
assertNotNull(AuditLogInterceptor.lastLoggedMessage_test_only, "Audit log should exist for VIP user.");
assertTrue(PerformanceTrackingInterceptor.lastExecutionTime_test_only >= 0,
"Performance should be tracked for VIP user.");
}
@Test
void testNonVipUserBlockedFromPremiere() {
User nonVipUser = new User("RegularRita", 25, false);
FeatureAccessException thrown = assertThrows(FeatureAccessException.class, () -> {
streamingService.playPremiereMovie(nonVipUser, "ExclusiveFilm");
});
assertTrue(thrown.getMessage().contains("is not a VIP"));
assertNull(AuditLogInterceptor.lastLoggedMessage_test_only,
"AuditLogInterceptor should not log if blocked by VIP check.");
}
@Test
void testVipUserButUnderAgeBlockedFromPremiere() {
User youngVipUser = new User("YoungVipYara", 12, true); // VIP but underage (premiere requires 13)
assertThrows(com.quarkflix.exception.ContentRestrictionException.class, () -> { // Expecting age restriction
streamingService.playPremiereMovie(youngVipUser, "ExclusiveFilmPG13");
});
// VipAccessInterceptor (priority 50) passes.
// AgeVerificationInterceptor (priority 100) should block.
// Performance (150) and Audit (200) should not be fully executed for the method
// itself.
assertNull(AuditLogInterceptor.lastLoggedMessage_test_only,
"AuditLogInterceptor should not log method success if blocked by age check.");
}
}
Running and Observing
Restart Quarkus.
Test various scenarios:
VIP user:
http://localhost:8080/stream/premiere?user=Victor&age=25&isVip=true&movie=NewHit
(Access)Non-VIP user:
http://localhost:8080/stream/premiere?user=Rita&age=25&isVip=false&movie=NewHit
(Denied)VIP user, underage:
http://localhost:8080/stream/premiere?user=Yara&age=10&isVip=true&movie=NewHitPG13
(Denied by age)
Modify
application.properties
toquarkflix.features.vip-premiere.enabled=false
, restart Quarkus (or rely on live reload for config if supported for this type of injection).Test Non-VIP user again:
http://localhost:8080/stream/premiere?user=Rita&age=25&isVip=false&movie=NewHit
(Should now get access, assuming age is fine).
Run the tests:
./mvnw test
. Or use the Dev UI to see them turn green:
Mission Debrief: What You’ve Learned
Congratulations, Developer! You've successfully implemented a suite of powerful CDI Interceptor "Guards" for QuarkFlix:
@AuditLog
: For comprehensive activity logging.@RequiresAgeVerification
: For enforcing content ratings.@TrackPerformance
: For monitoring method execution times.@VipAccessRequired
: For conditional feature access based on user status and configuration.
You've seen how interceptors, along with @Priority
, help create clean, modular, and testable code by separating cross-cutting concerns from your core business logic in a Quarkus application.
Where to Go Next
Add
@PostConstruct
and@PreDestroy
interceptors.Combine bindings into CDI stereotypes.
Hook up OpenTelemetry for distributed tracing.
Build security interceptors using JWT or OAuth scopes.
Interceptors are everywhere. From Quarkus security to fault tolerance. Mastering them gives you a deep superpower for building clean, enterprise-ready Java apps.
CDI interceptors in Quarkus help you separate concerns like security, logging, and validation without cluttering business logic. They are powerful, testable, and surprisingly fun when you treat your services like a streaming platform on the edge of chaos.
Now go forth and protect QuarkFlix! Your code (and your viewers) depend on it.