Java API for RESTful Web Services
—
The JAX-RS extension APIs provide customization points for message body processing, parameter conversion, exception handling, and context resolution. These provider interfaces enable deep integration with JAX-RS runtime for serialization, custom parameter types, error handling, and dependency injection.
import javax.ws.rs.ext.Provider;
import javax.ws.rs.ext.MessageBodyReader;
import javax.ws.rs.ext.MessageBodyWriter;
import javax.ws.rs.ext.ReaderInterceptor;
import javax.ws.rs.ext.ReaderInterceptorContext;
import javax.ws.rs.ext.WriterInterceptor;
import javax.ws.rs.ext.WriterInterceptorContext;
import javax.ws.rs.ext.InterceptorContext;
import javax.ws.rs.ext.ExceptionMapper;
import javax.ws.rs.ext.ParamConverter;
import javax.ws.rs.ext.ParamConverterProvider;
import javax.ws.rs.ext.ContextResolver;
import javax.ws.rs.ext.Providers;
import javax.ws.rs.ext.RuntimeDelegate;
import javax.ws.rs.NameBinding;
import javax.ws.rs.Priorities;
import javax.annotation.Priority;
import javax.ws.rs.core.MediaType;
import javax.ws.rs.core.MultivaluedMap;
import javax.ws.rs.WebApplicationException;
import java.io.IOException;
import java.io.InputStream;
import java.io.OutputStream;
import java.lang.annotation.Annotation;
import java.lang.reflect.Type;Marks an implementation of an extension interface for automatic discovery.
@Target({ElementType.TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Provider {
}Provider Registration Example:
// Automatic registration via @Provider
@Provider
public class CustomJsonProvider implements MessageBodyWriter<Object>, MessageBodyReader<Object> {
// Implementation...
}
// Programmatic registration
public class MyApplication extends Application {
@Override
public Set<Class<?>> getClasses() {
return Set.of(
UserResource.class,
CustomJsonProvider.class,
CustomExceptionMapper.class
);
}
}
// Client-side registration
Client client = ClientBuilder.newClient()
.register(CustomJsonProvider.class)
.register(LoggingFilter.class);Meta-annotation for creating name binding annotations that selectively bind filters and interceptors to specific resource methods or classes.
@Target({ElementType.ANNOTATION_TYPE})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface NameBinding {
}Name Binding Usage Example:
// Create a custom name binding annotation
@NameBinding
@Target({ElementType.TYPE, ElementType.METHOD})
@Retention(RetentionPolicy.RUNTIME)
public @interface Authenticated {
}
// Use name binding on resource methods
@Path("/users")
public class UserResource {
@GET
@Path("/public")
public Response getPublicInfo() {
// No authentication required
return Response.ok("Public information").build();
}
@GET
@Path("/private")
@Authenticated // This method requires authentication
public Response getPrivateInfo() {
return Response.ok("Private information").build();
}
}
// Create a filter bound to the name binding
@Provider
@Authenticated // Only applies to methods/classes annotated with @Authenticated
@Priority(Priorities.AUTHENTICATION)
public class AuthenticationFilter implements ContainerRequestFilter {
@Override
public void filter(ContainerRequestContext requestContext) throws IOException {
String authHeader = requestContext.getHeaderString("Authorization");
if (authHeader == null || !isValidToken(authHeader)) {
requestContext.abortWith(
Response.status(Response.Status.UNAUTHORIZED).build()
);
}
}
private boolean isValidToken(String token) {
// Token validation logic
return token.startsWith("Bearer ") && token.length() > 7;
}
}Control the execution order of filters and interceptors.
@Target({ElementType.TYPE, ElementType.PARAMETER})
@Retention(RetentionPolicy.RUNTIME)
@Documented
public @interface Priority {
int value();
}
public final class Priorities {
public static final int AUTHENTICATION = 1000;
public static final int AUTHORIZATION = 2000;
public static final int HEADER_DECORATOR = 3000;
public static final int ENTITY_CODER = 4000;
public static final int USER = 5000;
}Converts input streams to Java objects.
@Provider
public interface MessageBodyReader<T> {
boolean isReadable(Class<?> type, Type genericType,
Annotation[] annotations, MediaType mediaType);
T readFrom(Class<T> type, Type genericType, Annotation[] annotations,
MediaType mediaType, MultivaluedMap<String, String> httpHeaders,
InputStream entityStream) throws IOException, WebApplicationException;
}Converts Java objects to output streams.
@Provider
public interface MessageBodyWriter<T> {
boolean isWriteable(Class<?> type, Type genericType,
Annotation[] annotations, MediaType mediaType);
long getSize(T t, Class<?> type, Type genericType, Annotation[] annotations,
MediaType mediaType);
void writeTo(T t, Class<?> type, Type genericType, Annotation[] annotations,
MediaType mediaType, MultivaluedMap<String, Object> httpHeaders,
OutputStream entityStream) throws IOException, WebApplicationException;
}Message Body Provider Examples:
// Custom JSON provider using Jackson
@Provider
@Consumes(MediaType.APPLICATION_JSON)
@Produces(MediaType.APPLICATION_JSON)
public class JacksonJsonProvider implements MessageBodyReader<Object>, MessageBodyWriter<Object> {
private final ObjectMapper objectMapper = new ObjectMapper();
@Override
public boolean isReadable(Class<?> type, Type genericType,
Annotation[] annotations, MediaType mediaType) {
return mediaType.isCompatible(MediaType.APPLICATION_JSON_TYPE);
}
@Override
public Object readFrom(Class<Object> type, Type genericType, Annotation[] annotations,
MediaType mediaType, MultivaluedMap<String, String> httpHeaders,
InputStream entityStream) throws IOException {
try {
if (genericType != null) {
JavaType javaType = objectMapper.getTypeFactory().constructType(genericType);
return objectMapper.readValue(entityStream, javaType);
} else {
return objectMapper.readValue(entityStream, type);
}
} catch (JsonProcessingException e) {
throw new WebApplicationException("Invalid JSON", Response.Status.BAD_REQUEST);
}
}
@Override
public boolean isWriteable(Class<?> type, Type genericType,
Annotation[] annotations, MediaType mediaType) {
return mediaType.isCompatible(MediaType.APPLICATION_JSON_TYPE);
}
@Override
public long getSize(Object t, Class<?> type, Type genericType,
Annotation[] annotations, MediaType mediaType) {
return -1; // Unknown size
}
@Override
public void writeTo(Object t, Class<?> type, Type genericType, Annotation[] annotations,
MediaType mediaType, MultivaluedMap<String, Object> httpHeaders,
OutputStream entityStream) throws IOException {
objectMapper.writeValue(entityStream, t);
}
}
// CSV provider for specific types
@Provider
@Consumes("text/csv")
@Produces("text/csv")
public class CsvProvider implements MessageBodyReader<List<User>>, MessageBodyWriter<List<User>> {
@Override
public boolean isReadable(Class<?> type, Type genericType,
Annotation[] annotations, MediaType mediaType) {
return List.class.isAssignableFrom(type) &&
genericType instanceof ParameterizedType &&
((ParameterizedType) genericType).getActualTypeArguments()[0] == User.class;
}
@Override
public List<User> readFrom(Class<List<User>> type, Type genericType,
Annotation[] annotations, MediaType mediaType,
MultivaluedMap<String, String> httpHeaders,
InputStream entityStream) throws IOException {
List<User> users = new ArrayList<>();
try (BufferedReader reader = new BufferedReader(new InputStreamReader(entityStream))) {
String line;
boolean firstLine = true;
while ((line = reader.readLine()) != null) {
if (firstLine) {
firstLine = false; // Skip header
continue;
}
String[] parts = line.split(",");
users.add(new User(parts[0], parts[1]));
}
}
return users;
}
@Override
public boolean isWriteable(Class<?> type, Type genericType,
Annotation[] annotations, MediaType mediaType) {
return isReadable(type, genericType, annotations, mediaType);
}
@Override
public long getSize(List<User> users, Class<?> type, Type genericType,
Annotation[] annotations, MediaType mediaType) {
return -1;
}
@Override
public void writeTo(List<User> users, Class<?> type, Type genericType,
Annotation[] annotations, MediaType mediaType,
MultivaluedMap<String, Object> httpHeaders,
OutputStream entityStream) throws IOException {
try (PrintWriter writer = new PrintWriter(new OutputStreamWriter(entityStream))) {
writer.println("name,email"); // Header
for (User user : users) {
writer.printf("%s,%s%n", user.getName(), user.getEmail());
}
}
}
}
// Binary data provider
@Provider
@Consumes(MediaType.APPLICATION_OCTET_STREAM)
@Produces(MediaType.APPLICATION_OCTET_STREAM)
public class BinaryDataProvider implements MessageBodyReader<byte[]>, MessageBodyWriter<byte[]> {
@Override
public boolean isReadable(Class<?> type, Type genericType,
Annotation[] annotations, MediaType mediaType) {
return byte[].class == type;
}
@Override
public byte[] readFrom(Class<byte[]> type, Type genericType, Annotation[] annotations,
MediaType mediaType, MultivaluedMap<String, String> httpHeaders,
InputStream entityStream) throws IOException {
return entityStream.readAllBytes();
}
@Override
public boolean isWriteable(Class<?> type, Type genericType,
Annotation[] annotations, MediaType mediaType) {
return byte[].class == type;
}
@Override
public long getSize(byte[] data, Class<?> type, Type genericType,
Annotation[] annotations, MediaType mediaType) {
return data.length;
}
@Override
public void writeTo(byte[] data, Class<?> type, Type genericType, Annotation[] annotations,
MediaType mediaType, MultivaluedMap<String, Object> httpHeaders,
OutputStream entityStream) throws IOException {
entityStream.write(data);
}
}Base context for interceptor operations.
public interface InterceptorContext {
Object getProperty(String name);
Collection<String> getPropertyNames();
void setProperty(String name, Object object);
void removeProperty(String name);
Annotation[] getAnnotations();
void setAnnotations(Annotation[] annotations);
Class<?> getType();
void setType(Class<?> type);
Type getGenericType();
void setGenericType(Type genericType);
MediaType getMediaType();
void setMediaType(MediaType mediaType);
}@Provider
public interface ReaderInterceptor {
Object aroundReadFrom(ReaderInterceptorContext context) throws IOException, WebApplicationException;
}
public interface ReaderInterceptorContext extends InterceptorContext {
Object proceed() throws IOException, WebApplicationException;
InputStream getInputStream();
void setInputStream(InputStream is);
MultivaluedMap<String, String> getHeaders();
}
@Provider
public interface WriterInterceptor {
void aroundWriteTo(WriterInterceptorContext context) throws IOException, WebApplicationException;
}
public interface WriterInterceptorContext extends InterceptorContext {
void proceed() throws IOException, WebApplicationException;
Object getEntity();
void setEntity(Object entity);
OutputStream getOutputStream();
void setOutputStream(OutputStream os);
MultivaluedMap<String, Object> getHeaders();
}Interceptor Examples:
// Compression reader interceptor
@Provider
@Priority(Priorities.ENTITY_CODER)
public class GZipReaderInterceptor implements ReaderInterceptor {
@Override
public Object aroundReadFrom(ReaderInterceptorContext context)
throws IOException, WebApplicationException {
MultivaluedMap<String, String> headers = context.getHeaders();
String contentEncoding = headers.getFirst("Content-Encoding");
if ("gzip".equals(contentEncoding)) {
InputStream originalStream = context.getInputStream();
context.setInputStream(new GZIPInputStream(originalStream));
}
return context.proceed();
}
}
// Compression writer interceptor
@Provider
@Priority(Priorities.ENTITY_CODER)
public class GZipWriterInterceptor implements WriterInterceptor {
@Override
public void aroundWriteTo(WriterInterceptorContext context)
throws IOException, WebApplicationException {
MultivaluedMap<String, Object> headers = context.getHeaders();
String acceptEncoding = (String) headers.getFirst("Accept-Encoding");
if (acceptEncoding != null && acceptEncoding.contains("gzip")) {
headers.putSingle("Content-Encoding", "gzip");
OutputStream originalStream = context.getOutputStream();
GZIPOutputStream gzipStream = new GZIPOutputStream(originalStream);
context.setOutputStream(gzipStream);
try {
context.proceed();
} finally {
gzipStream.finish();
}
} else {
context.proceed();
}
}
}
// Logging interceptor
@Provider
public class LoggingInterceptor implements ReaderInterceptor, WriterInterceptor {
private static final Logger logger = LoggerFactory.getLogger(LoggingInterceptor.class);
@Override
public Object aroundReadFrom(ReaderInterceptorContext context)
throws IOException, WebApplicationException {
logger.info("Reading entity of type: {} with media type: {}",
context.getType().getSimpleName(), context.getMediaType());
long startTime = System.currentTimeMillis();
try {
return context.proceed();
} finally {
long duration = System.currentTimeMillis() - startTime;
logger.info("Entity reading completed in {}ms", duration);
}
}
@Override
public void aroundWriteTo(WriterInterceptorContext context)
throws IOException, WebApplicationException {
logger.info("Writing entity of type: {} with media type: {}",
context.getType().getSimpleName(), context.getMediaType());
long startTime = System.currentTimeMillis();
try {
context.proceed();
} finally {
long duration = System.currentTimeMillis() - startTime;
logger.info("Entity writing completed in {}ms", duration);
}
}
}Converts string parameters to Java types.
public interface ParamConverter<T> {
T fromString(String value);
String toString(T value);
}Provider for creating ParamConverter instances.
@Provider
public interface ParamConverterProvider {
<T> ParamConverter<T> getConverter(Class<T> rawType, Type genericType,
Annotation[] annotations);
}Parameter Converter Examples:
// Date parameter converter
public class DateParamConverter implements ParamConverter<Date> {
private final DateTimeFormatter formatter = DateTimeFormatter.ISO_LOCAL_DATE_TIME;
@Override
public Date fromString(String value) {
if (value == null || value.trim().isEmpty()) {
return null;
}
try {
LocalDateTime localDateTime = LocalDateTime.parse(value, formatter);
return Date.from(localDateTime.atZone(ZoneOffset.UTC).toInstant());
} catch (DateTimeParseException e) {
throw new IllegalArgumentException("Invalid date format: " + value, e);
}
}
@Override
public String toString(Date value) {
if (value == null) {
return null;
}
LocalDateTime localDateTime = value.toInstant()
.atZone(ZoneOffset.UTC)
.toLocalDateTime();
return formatter.format(localDateTime);
}
}
// UUID parameter converter
public class UUIDParamConverter implements ParamConverter<UUID> {
@Override
public UUID fromString(String value) {
if (value == null || value.trim().isEmpty()) {
return null;
}
try {
return UUID.fromString(value);
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException("Invalid UUID format: " + value, e);
}
}
@Override
public String toString(UUID value) {
return value != null ? value.toString() : null;
}
}
// Parameter converter provider
@Provider
public class CustomParamConverterProvider implements ParamConverterProvider {
@Override
@SuppressWarnings("unchecked")
public <T> ParamConverter<T> getConverter(Class<T> rawType, Type genericType,
Annotation[] annotations) {
if (rawType == Date.class) {
return (ParamConverter<T>) new DateParamConverter();
}
if (rawType == UUID.class) {
return (ParamConverter<T>) new UUIDParamConverter();
}
// Custom enum converter
if (rawType.isEnum()) {
return (ParamConverter<T>) new EnumParamConverter<>((Class<Enum>) rawType);
}
return null; // No converter available
}
}
// Generic enum converter
public class EnumParamConverter<T extends Enum<T>> implements ParamConverter<T> {
private final Class<T> enumClass;
public EnumParamConverter(Class<T> enumClass) {
this.enumClass = enumClass;
}
@Override
public T fromString(String value) {
if (value == null || value.trim().isEmpty()) {
return null;
}
try {
return Enum.valueOf(enumClass, value.toUpperCase());
} catch (IllegalArgumentException e) {
throw new IllegalArgumentException(
"Invalid " + enumClass.getSimpleName() + " value: " + value, e);
}
}
@Override
public String toString(T value) {
return value != null ? value.name() : null;
}
}
// Usage in resource
@Path("/examples")
public class ParamConverterExamples {
@GET
@Path("/events")
public List<Event> getEvents(@QueryParam("start") Date startDate,
@QueryParam("end") Date endDate,
@QueryParam("status") EventStatus status,
@QueryParam("id") UUID eventId) {
return eventService.findEvents(startDate, endDate, status, eventId);
}
}Maps Java exceptions to HTTP responses.
@Provider
public interface ExceptionMapper<E extends Throwable> {
Response toResponse(E exception);
}Exception Mapper Examples:
// Validation exception mapper
@Provider
public class ValidationExceptionMapper implements ExceptionMapper<ValidationException> {
@Override
public Response toResponse(ValidationException exception) {
ErrorResponse error = new ErrorResponse(
"VALIDATION_ERROR",
"Request validation failed",
exception.getErrors()
);
return Response.status(Response.Status.BAD_REQUEST)
.entity(error)
.type(MediaType.APPLICATION_JSON)
.build();
}
}
// Generic throwable mapper
@Provider
public class GeneralExceptionMapper implements ExceptionMapper<Throwable> {
private static final Logger logger = LoggerFactory.getLogger(GeneralExceptionMapper.class);
@Override
public Response toResponse(Throwable exception) {
// Log the exception
logger.error("Unhandled exception", exception);
// Don't expose internal details in production
ErrorResponse error = new ErrorResponse(
"INTERNAL_ERROR",
"An internal server error occurred"
);
return Response.status(Response.Status.INTERNAL_SERVER_ERROR)
.entity(error)
.type(MediaType.APPLICATION_JSON)
.build();
}
}
// Business exception mapper
@Provider
public class BusinessExceptionMapper implements ExceptionMapper<BusinessException> {
@Override
public Response toResponse(BusinessException exception) {
Response.Status status = mapToHttpStatus(exception.getErrorCode());
ErrorResponse error = new ErrorResponse(
exception.getErrorCode(),
exception.getMessage(),
exception.getDetails()
);
return Response.status(status)
.entity(error)
.type(MediaType.APPLICATION_JSON)
.build();
}
private Response.Status mapToHttpStatus(String errorCode) {
switch (errorCode) {
case "NOT_FOUND":
return Response.Status.NOT_FOUND;
case "UNAUTHORIZED":
return Response.Status.UNAUTHORIZED;
case "FORBIDDEN":
return Response.Status.FORBIDDEN;
case "CONFLICT":
return Response.Status.CONFLICT;
default:
return Response.Status.BAD_REQUEST;
}
}
}
// Security exception mapper
@Provider
public class SecurityExceptionMapper implements ExceptionMapper<SecurityException> {
@Override
public Response toResponse(SecurityException exception) {
if (exception instanceof AuthenticationException) {
return Response.status(Response.Status.UNAUTHORIZED)
.header("WWW-Authenticate", "Bearer")
.entity("Authentication required")
.build();
}
if (exception instanceof AuthorizationException) {
return Response.status(Response.Status.FORBIDDEN)
.entity("Insufficient privileges")
.build();
}
return Response.status(Response.Status.FORBIDDEN)
.entity("Access denied")
.build();
}
}
// Error response model
public class ErrorResponse {
private String code;
private String message;
private List<String> details;
private long timestamp;
public ErrorResponse(String code, String message) {
this(code, message, null);
}
public ErrorResponse(String code, String message, List<String> details) {
this.code = code;
this.message = message;
this.details = details;
this.timestamp = System.currentTimeMillis();
}
// Getters and setters...
}Provides context information to resource classes and providers.
@Provider
public interface ContextResolver<T> {
T getContext(Class<?> type);
}Context Resolver Examples:
// ObjectMapper context resolver for Jackson
@Provider
@Produces(MediaType.APPLICATION_JSON)
public class ObjectMapperContextResolver implements ContextResolver<ObjectMapper> {
private final ObjectMapper objectMapper;
public ObjectMapperContextResolver() {
this.objectMapper = new ObjectMapper();
// Configure mapper
objectMapper.configure(DeserializationFeature.FAIL_ON_UNKNOWN_PROPERTIES, false);
objectMapper.configure(SerializationFeature.WRITE_DATES_AS_TIMESTAMPS, false);
objectMapper.setDateFormat(new SimpleDateFormat("yyyy-MM-dd'T'HH:mm:ss.SSSX"));
// Register modules
objectMapper.registerModule(new JavaTimeModule());
}
@Override
public ObjectMapper getContext(Class<?> type) {
return objectMapper;
}
}
// Database context resolver
@Provider
public class DatabaseContextResolver implements ContextResolver<EntityManager> {
@PersistenceContext
private EntityManager entityManager;
@Override
public EntityManager getContext(Class<?> type) {
return entityManager;
}
}
// Configuration context resolver
@Provider
public class ConfigurationContextResolver implements ContextResolver<AppConfig> {
private final AppConfig appConfig;
public ConfigurationContextResolver() {
this.appConfig = loadConfiguration();
}
@Override
public AppConfig getContext(Class<?> type) {
return appConfig;
}
private AppConfig loadConfiguration() {
// Load configuration from properties, environment, etc.
return new AppConfig();
}
}
// Usage in resource or provider
@Path("/configured")
public class ConfiguredResource {
@Context
private Providers providers;
@GET
public Response getConfiguredResponse() {
// Get context resolver
ContextResolver<AppConfig> resolver =
providers.getContextResolver(AppConfig.class, MediaType.APPLICATION_JSON_TYPE);
if (resolver != null) {
AppConfig config = resolver.getContext(AppConfig.class);
return Response.ok(config.getSomeValue()).build();
}
return Response.status(Response.Status.SERVICE_UNAVAILABLE).build();
}
}Injectable interface for accessing provider instances.
public interface Providers {
<T> MessageBodyReader<T> getMessageBodyReader(Class<T> type, Type genericType,
Annotation[] annotations,
MediaType mediaType);
<T> MessageBodyWriter<T> getMessageBodyWriter(Class<T> type, Type genericType,
Annotation[] annotations,
MediaType mediaType);
<T extends Throwable> ExceptionMapper<T> getExceptionMapper(Class<T> type);
<T> ContextResolver<T> getContextResolver(Class<T> contextType,
MediaType mediaType);
}Central class for JAX-RS runtime integration.
public abstract class RuntimeDelegate {
public static RuntimeDelegate getInstance();
public static void setInstance(RuntimeDelegate rd);
public abstract UriBuilder createUriBuilder();
public abstract Response.ResponseBuilder createResponseBuilder();
public abstract Variant.VariantListBuilder createVariantListBuilder();
public abstract <T> T createEndpoint(Application application, Class<T> endpointType)
throws IllegalArgumentException, UnsupportedOperationException;
public abstract <T> HeaderDelegate<T> createHeaderDelegate(Class<T> type);
public static abstract class HeaderDelegate<T> {
public abstract T fromString(String value);
public abstract String toString(T value);
}
}Provider Usage Examples:
@Path("/provider-usage")
public class ProviderUsageExample {
@Context
private Providers providers;
@POST
@Path("/transform")
public Response transformData(@Context HttpHeaders headers,
InputStream inputStream) throws IOException {
MediaType inputType = headers.getMediaType();
// Get appropriate reader
MessageBodyReader<Object> reader = providers.getMessageBodyReader(
Object.class, Object.class, new Annotation[0], inputType);
if (reader == null) {
return Response.status(Response.Status.UNSUPPORTED_MEDIA_TYPE).build();
}
// Read input
Object data = reader.readFrom(Object.class, Object.class, new Annotation[0],
inputType, headers.getRequestHeaders(), inputStream);
// Transform data
Object transformed = transformationService.transform(data);
// Get appropriate writer for JSON output
MessageBodyWriter<Object> writer = providers.getMessageBodyWriter(
Object.class, Object.class, new Annotation[0], MediaType.APPLICATION_JSON_TYPE);
if (writer == null) {
return Response.status(Response.Status.INTERNAL_SERVER_ERROR).build();
}
return Response.ok(transformed, MediaType.APPLICATION_JSON).build();
}
}Install with Tessl CLI
npx tessl i tessl/maven-javax-ws-rs--javax-ws-rs-api