AEM Annotations: The Complete Reference (OSGi, Sling Models, Servlets)

12 min read

Every annotation an AEM developer uses — OSGi Declarative Services (@Component, @Reference, @Activate), Metatype config (@ObjectClassDefinition, @AttributeDefinition), Sling Models injectors (@ValueMapValue, @ChildResource, @Self, @Via), servlet annotations, and Jackson exporters — with examples and a cheat-sheet table.

AEMAnnotationsOSGiSling ModelsJavaReference
AEM Annotations: The Complete Reference (OSGi, Sling Models, Servlets)

Modern AEM development is almost entirely annotation-driven. You rarely write boilerplate wiring code by hand; instead you declare what you want with annotations, and the runtime — OSGi for services, Sling Models for content adaptation, the Sling engine for servlets — does the wiring for you. That's powerful, but it also means a working AEM developer needs to keep a few dozen annotations straight, and it's genuinely easy to forget which package an annotation comes from or which of two similar ones to reach for.

This article is that reference. It's organized by the three families you'll use every day — OSGi Declarative Services, Sling Models, and Sling Servlets — and for each annotation it explains not just the syntax but why and when you use it, with a runnable example. A consolidated cheat-sheet table sits at the end so you can look anything up at a glance.

It pairs naturally with the Component Development guide, which shows these annotations working together, and the AEM Developer Cheat Sheet for the surrounding architecture.

OSGi Declarative Services (DS R6/R7)

Package: org.osgi.service.component.annotations

OSGi is the module system your Java runs inside, and Declarative Services (DS) is how you register components and connect them together. These annotations are the foundation of every service, servlet, and scheduled job you'll write.

One thing to settle up front: DS is the modern, correct standard. You may still encounter the old Felix SCR annotations (@Service, @Property, and a different @Reference from org.apache.felix.scr.annotations) in legacy code, but those are deprecated — new code should always use the OSGi DS annotations below.

@Component

@Component is the starting point: it tells OSGi that a class is a managed component, and optionally that it provides a service other code can consume.

@Component(
    service = MyService.class,     // the interface(s) it provides
    immediate = true,              // activate without waiting for a consumer
    property = {                   // service properties
        "process.label=My Process"
    })
public class MyServiceImpl implements MyService { }

The service attribute publishes the class under an interface so others can inject it. immediate = true starts the component as soon as its dependencies are satisfied, rather than lazily on first use — important for things like listeners that need to be running immediately. The property array attaches metadata that other parts of OSGi (event handlers, schedulers, servlet registration) read.

@Activate, @Deactivate, @Modified

These three mark lifecycle callbacks, so your component can react to being started, reconfigured, or stopped.

@Activate
@Modified
protected void activate(Config config) { this.endpoint = config.endpoint(); }

@Deactivate
protected void deactivate() { /* cleanup */ }

@Activate runs when the component starts and is where you read configuration and set up state. @Modified runs when the configuration changes — annotating the same method with both means a config change is applied in place, without OSGi tearing the component down and recreating it. @Deactivate runs on shutdown and is where you release resources.

@Reference

@Reference injects another OSGi service into your component. It also lets you express how that dependency behaves — whether it's required, and whether it can come and go at runtime.

@Reference
private QueryBuilder queryBuilder;

// Optional + dynamic (allows the service to come and go)
@Reference(cardinality = ReferenceCardinality.OPTIONAL,
           policy = ReferencePolicy.DYNAMIC)
private volatile SomeService optionalService;

// Multiple
@Reference
private List<ContentHandler> handlers;

A plain @Reference is a mandatory, static dependency — your component won't start until it's available. Setting cardinality to OPTIONAL and policy to DYNAMIC makes the dependency optional and allows it to appear or disappear while your component keeps running (note the volatile field). And injecting a List collects every service registered under that interface — the basis of a plugin pattern.

@Designate

@Designate is the link between a component and its configuration definition. Without it, the configuration class in the next section never binds to the component.

@Component(service = MyService.class)
@Designate(ocd = MyServiceImpl.Config.class)
public class MyServiceImpl implements MyService { }

The ocd ("object class definition") attribute points at the @ObjectClassDefinition-annotated interface that describes your configurable properties.

OSGi Metatype (configuration)

Package: org.osgi.service.metatype.annotations

These annotations describe a component's configurable properties — the ones that appear, editable, in the OSGi Configuration Manager console and can be supplied as configuration in your project's ui.config module.

@ObjectClassDefinition & @AttributeDefinition

You define configuration as an annotated @interface. Each method becomes one editable property, and @AttributeDefinition gives it a label, a type (inferred from the return type), and a default.

@ObjectClassDefinition(name = "My Service Configuration",
                       description = "Settings for My Service")
public @interface Config {

    @AttributeDefinition(name = "API endpoint", description = "Base URL")
    String endpoint() default "https://api.example.com";

    @AttributeDefinition(name = "Timeout (ms)")
    int timeout() default 5000;

    @AttributeDefinition(name = "Enabled paths",
                         cardinality = 50)        // array → multi-value
    String[] paths() default {"/content"};

    @AttributeDefinition(name = "Mode",
        options = {
            @Option(label = "Fast", value = "fast"),
            @Option(label = "Safe", value = "safe")
        })
    String mode() default "safe";
}

A few patterns are worth noting: an array return type with a cardinality produces a multi-value field, and an options list turns a field into a dropdown of fixed choices. You read these values back in your @Activate(Config config) method.

Gotcha: OSGi property names can't contain dots, but configuration keys often need them. The convention is that a double underscore in a method name maps to a dot in the property name — so my__property() becomes the configuration key my.property.

Sling Models

Package: org.apache.sling.models.annotations (plus the injectorspecific subpackage)

Sling Models are how you turn content into typed Java objects. A model adapts a Resource or a SlingHttpServletRequest into a bean whose fields are populated automatically from JCR properties, child nodes, services, and request data — so your templates and servlets work with clean getters instead of raw repository APIs.

@Model

@Model declares a class as a Sling Model and, crucially, says what it can be adapted from.

@Model(
    adaptables = {Resource.class, SlingHttpServletRequest.class},
    adapters = {Teaser.class, ComponentExporter.class},  // optional
    resourceType = "mysite/components/teaser",            // for exporters
    defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
public class TeaserModel implements Teaser { }

adaptables lists the source objects; adapters (optional) registers the model under one or more interfaces, which is how exporters and the SPA framework find it; and resourceType ties the model to a component so exporters resolve automatically.

Always set defaultInjectionStrategy = OPTIONAL. The default is REQUIRED, which throws an exception if any field can't be injected — and in practice optional dialog fields are empty all the time. OPTIONAL leaves them null instead, which is what you want.

Injector-specific annotations (preferred)

The single most useful habit with Sling Models is to use injector-specific annotations rather than the generic @Inject. Each one names exactly where a value comes from, which makes the code self-documenting and avoids the ambiguity of letting Sling guess the source.

AnnotationInjects from
@ValueMapValueA property on the resource's ValueMap
@ChildResourceA child resource (single or List)
@RequestAttributeA request attribute
@OSGiServiceAn OSGi service
@ScriptVariableAn HTL/JSP binding (currentPage, pageManager, resource…)
@SelfThe adaptable itself (the request/resource)
@SlingObjectSling objects (ResourceResolver, SlingHttpServletRequest, Resource…)
@ResourcePathA resource at a configured/looked-up path

In practice they read very clearly — the annotation tells you the origin of every field:

@ValueMapValue(name = "jcr:title")   // map to a differently-named property
private String title;

@ValueMapValue
@Default(values = "Untitled")
private String heading;

@ChildResource
private List<Resource> items;

@ScriptVariable
private Page currentPage;

@SlingObject
private ResourceResolver resourceResolver;

@OSGiService
private QueryBuilder queryBuilder;

@Self
private SlingHttpServletRequest request;

Generic @Inject + modifiers

The older, generic injection style still works, and you'll see it in existing codebases. It relies on Sling choosing an injector by field name and type, and uses a set of modifier annotations to refine behavior. Prefer the injector-specific annotations above for new code, but it's worth recognizing these:

AnnotationPurpose
@InjectGeneric injection (Sling picks the injector by name/type)
@OptionalField is optional (with REQUIRED default strategy)
@DefaultFallback value(s) when nothing is injected
@NamedInject by a specific name
@ViaAdapt via an intermediate (e.g. delegate to super-type model)
@SourceForce a specific injector
@FilterLDAP filter for @OSGiService selection

@Via — delegating to a Core Component

@Via deserves a closer look because it powers one of the most common patterns in real projects: extending a Core Component's model and overriding just one piece of its behavior. You inject the original model and delegate to it, customizing only what you need.

@Self
@Via(type = ResourceSuperType.class)
private Teaser delegate;     // the Core Component's Teaser model

public String getTitle() {
    return StringUtils.upperCase(delegate.getTitle());
}

Here @Via(type = ResourceSuperType.class) tells Sling to adapt using the component's sling:resourceSuperType — i.e. the Core Component — so delegate is Adobe's fully-featured Teaser model, and you simply wrap the one method you care about.

@PostConstruct

Package: javax.annotation (this import trips people up — it is not a Sling annotation).

A method annotated with @PostConstruct runs once, after all injection has completed. It's the correct place for any derived or computed values, because by then every injected field is guaranteed to be populated.

@PostConstruct
protected void init() {
    this.formatted = title != null ? title.trim() : "";
}

Exporters: @Exporter / @Exporters

When a component needs to be consumed as JSON — by the SPA Editor or a headless front end — you don't write a serializer. You add @Exporter, and Sling serializes the model with Jackson.

@Model(adaptables = SlingHttpServletRequest.class,
       adapters = {Teaser.class, ComponentExporter.class},
       resourceType = "mysite/components/teaser",
       defaultInjectionStrategy = DefaultInjectionStrategy.OPTIONAL)
@Exporter(name = "jackson", extension = "json")
public class TeaserModel implements Teaser, ComponentExporter { }

You then shape the JSON output with standard Jackson annotations from com.fasterxml.jackson.annotation:

@JsonProperty("titleText")     // rename
private String title;

@JsonIgnore                    // exclude
private String internalNote;

@JsonInclude(JsonInclude.Include.NON_NULL)  // on the class/field
public class TeaserModel { }

@JsonProperty renames a field in the output, @JsonIgnore hides it entirely, and @JsonInclude can suppress nulls so the JSON stays lean.

Sling Servlet annotations

Package: org.apache.sling.servlets.annotations

These let you register servlets declaratively, which is far clearer than the old approach of stuffing registration details into @Component properties.

@SlingServletResourceTypes (preferred)

The recommended way to register a servlet is to bind it to a resource type. Doing so means the servlet inherits AEM's resource-level access control automatically.

@Component(service = Servlet.class)
@SlingServletResourceTypes(
    resourceTypes = "mysite/components/teaser",
    selectors = "data",
    extensions = "json",
    methods = HttpConstants.METHOD_GET)
public class TeaserServlet extends SlingSafeMethodsServlet { }

This servlet responds to a GET for the teaser resource with the data selector and json extension — for example /content/.../teaser.data.json.

@SlingServletPaths

You can also bind a servlet to a fixed path, but do so sparingly: a path-bound servlet sidesteps resource-level access control and must be explicitly allowed through the Dispatcher.

@Component(service = Servlet.class)
@SlingServletPaths("/bin/mysite/export")
public class ExportServlet extends SlingAllMethodsServlet { }

@SlingServletFilter

To intercept requests before they reach a servlet — for logging, headers, or rewriting — register a filter with a scope and a URL pattern.

@Component(service = Filter.class)
@SlingServletFilter(
    scope = {SlingServletFilterScope.REQUEST},
    pattern = "/content/mysite/.*",
    methods = {"GET"})
public class MyFilter implements Filter { }

Quick reference / cheat sheet

AnnotationPackage (short)Purpose
@Componentosgi.service.componentDeclare OSGi component/service
@Activate / @Deactivate / @Modifiedosgi.service.componentLifecycle callbacks
@Referenceosgi.service.componentInject another service
@Designateosgi.service.componentBind config OCD
@ObjectClassDefinitionosgi.service.metatypeConfig definition
@AttributeDefinitionosgi.service.metatypeConfig property
@Modelsling.models.annotationsDeclare Sling Model
@ValueMapValue…injectorspecificJCR property
@ChildResource…injectorspecificChild node(s)
@ScriptVariable…injectorspecificHTL global binding
@Self…injectorspecificThe adaptable
@SlingObject…injectorspecificSling runtime objects
@OSGiService…injectorspecificInject service into a model
@Viasling.models.annotationsDelegate / intermediate adapt
@Defaultsling.models.annotationsFallback value
@PostConstructjavax.annotationPost-injection logic
@Exportersling.models.annotationsJSON serialization
@JsonProperty / @JsonIgnorejackson.annotationShape exporter output
@SlingServletResourceTypessling.servlets.annotationsResource-bound servlet
@SlingServletPathssling.servlets.annotationsPath-bound servlet
@SlingServletFiltersling.servlets.annotationsRequest filter

Best practices & gotchas

  • Use OSGi DS annotations, never the deprecated Felix SCR ones.
  • ✅ Prefer injector-specific annotations (@ValueMapValue, @ChildResource) over the generic @Inject.
  • ✅ Set defaultInjectionStrategy = OPTIONAL on every model.
  • ✅ Annotate one method with both @Activate and @Modified so configuration changes apply without a restart.
  • ✅ Adapt from SlingHttpServletRequest (not Resource) when you need request-scoped data or exporters.
  • ❌ Don't mix Felix SCR and OSGi DS annotations in the same class.
  • ❌ Don't forget @Designate(ocd = ...) — without it your @ObjectClassDefinition is never bound.
  • ❌ Don't import @PostConstruct from the wrong place — it lives in javax.annotation, not Sling.

Wrapping up

Annotations are the wiring diagram of AEM. Once you can place any annotation into one of the three families — OSGi DS for components, services, and configuration; Sling Models for adapting content into Java; and Sling Servlets for HTTP endpoints — the platform stops feeling like magic and starts feeling declarative and predictable. You describe intent; the runtime connects the dots.

To see all of these working together in a real component, read the Component Development guide, and for the templating layer continue to the HTL cheat sheet. To skip the boilerplate, generate fully annotated models with my AEM Component Generator.

Share this article

Subscribe to the Newsletter

Get the latest articles, tutorials, and tech insights delivered straight to your inbox. No spam, unsubscribe anytime.

Back to Blog