Before we start — the one question that decides this topic Your syllabus point 15 says servlets are registered two ways and asks what the difference is. Almost every candidate answers that with something like:
"By path you give it a URL, by resource type you tie it to a component."
That is true, and it scores about half a mark. It describes the syntax but misses the two things that actually matter:
Path-bound servlets bypass content permissions. There is no resource, so there is nothing for Sling to check ACLs against. You have to do authorisation yourself, and most people don't. Path-bound servlets have to be whitelisted in the dispatcher. Resource-type servlets sit on real content paths, which are already allowed. Those two are why Adobe recommends resource-type registration and treats path binding as the exception. If you lead with security and dispatcher behaviour rather than syntax, you have given a clearly better answer than the room.
There is also a strong continuity thread here. File 02's flagship component story — the tabbed listing with Load More — hinged on registering a servlet by resource type with a selector and suffix so the responses stayed dispatcher-cacheable. That decision lives in this file. If you tell that story, expect to be asked to justify it here.
- Introduction 1.1 What is a servlet, and why do you need one in AEM? A servlet is a Java class that handles an HTTP request and writes a response. That is standard Java EE, not AEM-specific.
But here is the question worth asking first, because interviewers do: AEM already renders pages through HTL and Sling Models. So when do you actually need a servlet?
You don't need one for rendering a page. That is what components and HTL are for.
You need one when the browser needs data rather than a page. Three real cases from our project:
Case one — the browser asks for more data after the page has loaded. The product listing's Load More button. The page is already rendered; JavaScript needs the next batch of cards. That is not a page render, so HTL is the wrong tool.
Case two — a form submits somewhere that isn't a repository node. A "request a quote" form that has to reach an external CRM. The Sling Post Servlet writes to the repository, which is not what we want here.
Case three — an alternative representation of existing content. The same product page as JSON, for the mobile app or a partner integration.
The interview answer:
"In AEM most rendering goes through HTL and Sling Models, so I write a servlet when the browser needs data rather than a page. Typically that's three cases: an AJAX call after page load, like a Load More or a filter; a form posting somewhere that isn't a repository node; or exposing existing content in another format, usually JSON for a front-end or a partner system.
If I find myself writing a servlet to render HTML for a page, that's usually a sign I should have used a component instead."
1.2 A servlet is just an OSGi component This is the bridge from file 06, and it explains everything about how servlets are registered.
A Sling servlet is an OSGi component registered as a service under the javax.servlet.Servlet interface.
@Component(service = Servlet.class) // ← this is the whole mechanism public class ProductSpecsServlet extends SlingSafeMethodsServlet { }Copy Copy That is it. Registering under Servlet.class puts it on the noticeboard from file 06. Sling's servlet resolver watches that noticeboard and reads each servlet's service properties to decide which requests it should handle.
Two consequences worth stating in an interview, because they tie the files together:
One — @Reference works in a servlet. File 05 explained why it does not work in a Sling Model: Declarative Services only manages @Component classes. A servlet is one, so DS manages it and can inject into it.
Two — a servlet can be unsatisfied. All of file 06's component lifecycle applies. If a servlet has a mandatory @Reference to a service that isn't registered, the servlet component never activates — and you get no error, just a URL that returns 404 forever. That is a genuinely common and confusing production problem.
1.3 A real project example to adapt "We have around eight servlets. Most are registered by resource type — the Load More endpoint for our product listing, a specifications endpoint the product page calls, and a couple of JSON endpoints for the mobile app. Two are path-bound: a form handler that posts to an external CRM, and a health check endpoint. For those two we do our own authorisation, because path-bound servlets bypass content ACLs, and both paths are explicitly whitelisted in the dispatcher configuration."
That covers the split, the security consequence, and the dispatcher — three follow-ups pre-empted.
- Core Concepts 2.1 The two base classes (syllabus point 16) Your syllabus asks when you extend each. Let's start here because it shapes the code you write.
There are two abstract classes, and one extends the other:
javax.servlet.Servlet (interface) ↑ GenericServlet ↑ SlingSafeMethodsServlet ← GET, HEAD, OPTIONS, TRACE ↑ SlingAllMethodsServlet ← adds POST, PUT, DELETECopy Copy A small precision point, since your syllabus says "extend/implement": you extend these — they are abstract classes, not interfaces. You would implement javax.servlet.Servlet, but in practice nobody does that directly. Saying "extend" correctly is a small but real signal.
SlingSafeMethodsServlet handles only the HTTP methods that are safe — meaning they read but do not change anything:
Method What it does GET Retrieve something HEAD Like GET but headers only OPTIONS What methods does this support? TRACE Diagnostic echo You override doGet, and occasionally doHead.
SlingAllMethodsServlet extends the safe one and adds the unsafe methods — the ones that change state:
Method What it does POST Create, or submit PUT Replace DELETE Remove You override doPost, doPut, doDelete, plus anything from the safe set.
Now the question your syllabus is actually asking: when do you use each?
The rule is one sentence:
If the servlet only reads, extend SlingSafeMethodsServlet. If it changes anything, extend SlingAllMethodsServlet.
And the follow-up that separates a good answer: why not just always use SlingAllMethodsServlet?
Three reasons:
One — least privilege. Extending SlingAllMethodsServlet means your servlet advertises that it handles POST, PUT and DELETE. If you only implemented doGet, you have declared an interface you did not build. The inherited methods return 405 Method Not Allowed, so nothing breaks — but you have widened the surface for no reason.
Two — intent. A reader seeing SlingSafeMethodsServlet knows immediately this endpoint is read-only. That is free documentation.
Three — code review. Extending SlingAllMethodsServlet for a read-only servlet is a standard code review comment, and on a security-reviewed project it will be raised.
The interview answer:
"SlingSafeMethodsServlet handles the safe HTTP methods — GET, HEAD, OPTIONS and TRACE — meaning the ones that read without changing state. SlingAllMethodsServlet extends it and adds POST, PUT and DELETE.
So the rule is: read-only servlet extends the safe one, anything that writes extends the all-methods one.
I wouldn't just always use SlingAllMethodsServlet, even though it would work, for three reasons. It advertises methods I haven't implemented — they'd return 405, so nothing breaks, but it's a wider surface than needed. It loses the intent, because SlingSafeMethodsServlet tells the next developer at a glance that this endpoint is read-only. And it's a routine code review finding on any security-conscious project.
Small precision: these are abstract classes, so you extend them. You'd only implement javax.servlet.Servlet directly, which nobody does in practice."
2.2 Registering by path (syllabus point 15) @Component(service = Servlet.class) @SlingServletPaths("/bin/energy/quote-request") public class QuoteRequestServlet extends SlingAllMethodsServlet {
}Copy Copy What this means: the servlet answers at that exact URL. https://www.example.com/bin/energy/quote-request.
The URL is arbitrary. It does not correspond to anything in the repository. There is no node at /bin/energy/quote-request.
And that last sentence is the source of everything important about path binding, which we come to in 2.4.
The older property-based form, which you will see in existing codebases:
@Component( service = Servlet.class, property = { "sling.servlet.paths=/bin/energy/quote-request", "sling.servlet.methods=POST" })Copy Copy Same thing, less readable. The annotation is current.
A constraint worth knowing: recent AEM and Sling versions restrict which path prefixes a path-bound servlet may use, and there is an allowlist configuration in the Sling Servlet Resolver. Conventionally these live under /bin/. Registering a servlet at an arbitrary top-level path is either discouraged or blocked depending on your version, so /bin/
2.3 Registering by resource type (syllabus point 15) @Component(service = Servlet.class) @SlingServletResourceTypes( resourceTypes = "energy/components/categorylisting", selectors = "cards", extensions = "html", methods = HttpConstants.METHOD_GET ) public class CategoryCardsServlet extends SlingSafeMethodsServlet { }Copy Copy What this means: this servlet handles requests for any content node whose sling:resourceType is energy/components/categorylisting, when the request has the selector cards, the extension html, and the method GET.
So it answers at URLs like:
/content/energy/global/en/products.cards.html/2 └─── a real content path ───┘ └sel┘└ext┘└suffix┘Copy Copy Notice this is exactly the Sling resolution model from file 01 — URL → Resource → Resource Type → Script. A resource-type servlet slots into that chain in place of an HTL script. It is not a separate mechanism; it is the same one.
The four attributes:
Attribute Purpose Required? resourceTypes Which content this serves Yes selectors Narrow to a selector, e.g. cards No, but almost always used extensions html, json, csv Recommended methods GET, POST, … Recommended Why you almost always want a selector. Without one, your servlet takes over every GET for that resource type — including the page render itself. With selectors = "cards", the normal page still renders through HTL and only .cards. requests reach your servlet.
The older property form:
@Component( service = Servlet.class, property = { "sling.servlet.resourceTypes=energy/components/categorylisting", "sling.servlet.selectors=cards", "sling.servlet.extensions=html", "sling.servlet.methods=GET" })Copy Copy 2.4 The real difference — and why it matters (syllabus point 15) This is the heart of the file. Four differences, and the first two are the ones that count.
Difference 1 — permissions This is the most important one, and most candidates miss it.
With a resource-type servlet, the URL is a real content path. So Sling does what it always does: it resolves the path to a resource using the current user's permissions. If an anonymous visitor cannot read that content, the resource does not resolve and they get a 404 before your code runs at all.
You get content permissions for free.
With a path-bound servlet, there is no resource. /bin/energy/quote-request is not a node. So there is nothing for Sling to check permissions against, and your servlet runs for anyone who knows the URL.
You must do authorisation yourself, in code. And if you forget — which is common — you have an unauthenticated endpoint on your public site.
"The difference people usually miss is permissions. A resource-type servlet sits on a real content path, so Sling resolves that resource using the current user's session and content ACLs apply automatically — if the user can't read the content, they never reach my code. A path-bound servlet has no resource behind it, so there's nothing to check against and it runs for anyone who knows the URL. That means every path-bound servlet has to do its own authorisation, and forgetting is how you end up with an unauthenticated endpoint on a public site."
Difference 2 — the dispatcher From file 01: the dispatcher denies everything by default and whitelists what the site needs.
A resource-type servlet lives under /content, which is already allowed. Nothing extra to do.
A path-bound servlet lives under /bin, which is not allowed by default. It must be explicitly whitelisted:
/0200 { /type "allow" /path "/bin/energy/*" /method "POST" }Copy Copy And this produces a very common bug: the servlet works perfectly on author and locally, and returns 404 on the published site — because the dispatcher rejected the request before it ever reached AEM. It is the same shape of failure as allowProxy in file 04: environment-specific, and the code is fine.
Difference 3 — caching A resource-type servlet with a selector and extension produces a URL that is part of the path, so the dispatcher can cache it as a file.
/content/energy/global/en/products.cards.html/2 ← cacheable /bin/energy/cards?page=2 ← not cacheableCopy Copy This is exactly the decision in file 02's Load More story. Same data, but one form caches and the other hits publish on every click.
Difference 4 — flexibility Path binding gives you any URL you like, with no content required. Resource-type binding requires real content to exist at that path.
That is the genuine advantage of path binding, and it is why it still exists.
The comparison table By Path By Resource Type Annotation @SlingServletPaths @SlingServletResourceTypes URL Arbitrary, e.g. /bin/energy/x A real content path with a selector Backed by a real resource No Yes Content ACLs apply No — do it yourself Yes, automatically Dispatcher Must be whitelisted Already allowed Cacheable Rarely Yes, with selector + extension Needs content to exist No Yes Adobe's recommendation Use sparingly Preferred Good for Global utilities, external form handlers, health checks Almost everything else When to actually use path binding Be specific here, because "never" is a weak answer:
Use a path-bound servlet when there is genuinely no content context. Three legitimate cases:
A form handler posting to an external system, where no repository node is involved. A health check or diagnostic endpoint. A webhook receiver where an external system must call a fixed, stable URL. Everything else should be resource-type bound.
The complete interview answer:
"Two mechanisms. @SlingServletPaths binds to a fixed URL like /bin/energy/export. @SlingServletResourceTypes binds to a resource type plus selector, extension and method, so it answers on real content paths.
The syntax difference is the obvious part. The two that actually matter are security and the dispatcher.
Security: a resource-type servlet resolves a real resource, so content ACLs are applied automatically — an anonymous user who can't read that content gets a 404 before my code runs. A path-bound servlet has no resource, so nothing is checked and it runs for anyone with the URL. Authorisation becomes my job.
Dispatcher: /content paths are already allowed, but /bin has to be explicitly whitelisted. That's why a path-bound servlet frequently works on author and 404s on the live site.
There's also caching. A resource-type servlet with a selector produces a path-based URL the dispatcher can cache as a file. A path-bound one usually takes query parameters, which aren't cacheable — that difference was significant on a listing component we built with a Load More.
So I default to resource type. I use path binding only when there's genuinely no content context — a form handler posting to an external CRM, a health check, or a webhook that needs a fixed URL. And when I do, I write explicit authorisation and add the dispatcher rule."
2.5 The methods — GET, POST, PUT, DELETE (syllabus point 15) Your syllabus says to memorise examples of each. Note the signature: SlingHttpServletRequest, not the plain HttpServletRequest. The Sling versions give you getResource(), getResourceResolver() and getRequestPathInfo().
@Override protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response) throws ServletException, IOException { }
@Override protected void doPost(SlingHttpServletRequest request, SlingHttpServletResponse response) throws ServletException, IOException { }
@Override protected void doPut(SlingHttpServletRequest request, SlingHttpServletResponse response) throws ServletException, IOException { }
@Override protected void doDelete(SlingHttpServletRequest request, SlingHttpServletResponse response) throws ServletException, IOException { }Copy Copy Four things they all share:
protected, not public. Return void — you write to the response, you do not return a value. Throw ServletException and IOException. Take the Sling request and response types. What each is for, semantically — worth being able to say, because it shows you understand HTTP rather than just AEM:
Method Meaning Safe? Idempotent? GET Read something Yes Yes POST Create, or submit No No PUT Replace at a known location No Yes DELETE Remove No Yes "Idempotent" means calling it twice has the same effect as calling it once. PUT and DELETE are — deleting the same thing twice leaves it deleted. POST is not — posting a form twice creates two records. That is why browsers warn you before re-submitting a form, and it is a good detail to drop in.
Full working examples of all four are in section 8.
2.6 Every servlet and service annotation (syllabus point 14) Your syllabus asks for all the annotations for services and servlets together. Services were file 06; here is the combined map.
Servlet registration (org.apache.sling.servlets.annotations):
Annotation Purpose @SlingServletResourceTypes Register by resource type — preferred @SlingServletPaths Register by fixed path @SlingServletName Give it a readable name in the consoles @SlingServletFilter Register a request filter Always paired with:
@Component(service = Servlet.class)Copy Copy @SlingServletResourceTypes attributes:
@SlingServletResourceTypes( resourceTypes = "energy/components/categorylisting", // required resourceSuperType = "...", // optional selectors = { "cards", "list" }, // optional, use one extensions = { "html", "json" }, methods = { HttpConstants.METHOD_GET } )Copy Copy @SlingServletPaths:
@SlingServletPaths({ "/bin/energy/quote", "/bin/energy/quote/v2" })Copy Copy Note it takes no methods attribute — you control that by which doXxx methods you override, and by your base class.
The old property names, so you can read legacy code:
sling.servlet.resourceTypes sling.servlet.resourceSuperType sling.servlet.selectors sling.servlet.extensions sling.servlet.methods sling.servlet.paths sling.servlet.prefix ← search-path priority: "/apps" beats "/libs"Copy Copy Service annotations from file 06, which servlets also use:
Annotation Purpose in a servlet @Component(service = Servlet.class) Registers it @Reference Inject a service — works because a servlet is an OSGi component @Activate / @Deactivate Lifecycle, if the servlet holds resources @ObjectClassDefinition / @Designate Configure the servlet per run mode @ServiceDescription A human-readable description in the console The one-line summary for point 14:
"For servlets it's @Component(service = Servlet.class) plus either @SlingServletResourceTypes or @SlingServletPaths, and @SlingServletFilter for filters. Because a servlet is an OSGi component, everything from the service side applies too — @Reference for injection, @Activate and @Deactivate for lifecycle, and @ObjectClassDefinition with @Designate if it needs configuration. The older style used sling.servlet.* properties on @Component directly, which still works but is much less readable."
2.7 The default servlets — the ones you didn't write Two servlets ship with Sling and handle requests you never wrote code for. Both come up in interviews, and both have security implications.
DefaultGetServlet handles a GET when nothing else matches. It renders a node as .json, .html, .xml or .res.
This is what serves .infinity.json — and it is why /content.infinity.json can dump your repository if the dispatcher does not block it. That connects straight back to file 01.
It is configurable — you can disable the JSON renderer, and cap how many nodes .infinity.json will return.
SlingPostServlet handles a POST to a repository path, creating and updating nodes from form fields.
This is what saves your component dialogs — the mechanism from file 02. Nobody writes a servlet for a dialog because this one already exists.
Its special parameters are worth knowing:
Parameter Effect :operation delete, move, copy, import @TypeHint Force a property type, e.g. Boolean, Long, Date @Delete Remove a property @ValueFrom Take the value from a differently-named field :redirect Where to go after the POST :status browser for a redirect, or return a plain status Why this matters for security: the Sling Post Servlet means any authenticated user who can write to a path can create content there by POSTing to it. On publish that is why POST is normally blocked at the dispatcher for content paths.
- Internal Working 3.1 How Sling picks a servlet This extends the script resolution from file 01, and it explains why path-bound servlets behave differently.
graph TD
A["Request arrives"] --> B{"Does a PATH-BOUND
servlet match the URL?"}
B -->|Yes| C["Use it.
No resource resolved.
No ACL check."]
B -->|No| D["Resolve the URL to a RESOURCE
— user permissions apply here"]
D --> E{"Resource exists
and is readable?"}
E -->|No| F["404"]
E -->|Yes| G["Read sling:resourceType"]
G --> H["Look for a servlet or script matching
resourceType + selectors + extension + method
— /apps first, then /libs"]
H --> I{"Found?"}
I -->|Yes| J["Use it"]
I -->|No| K["Follow sling:resourceSuperType"]
K --> L{"Found?"}
L -->|No| M["Default servlets
DefaultGetServlet / SlingPostServlet"]
⛶ Fullscreen
The single most important thing in that diagram is the top branch.
Path-bound servlets are checked first, in a separate registry, and they skip the resource resolution step entirely. That is not a quirk — it is the definition of path binding. And since resource resolution is where permissions are checked, skipping it is exactly why ACLs do not apply.
Being able to explain the security consequence as a consequence of the resolution order, rather than as an isolated fact, is what makes the answer sound like understanding.
3.2 Servlet versus script — the same slot A useful reframing that impresses interviewers.
A resource-type servlet and an HTL script compete for the same position in resolution. Both answer "what renders this resource type with these selectors?"
resourceType = energy/components/categorylisting, selector = cards, extension = html
Candidates, in order: /apps/energy/components/categorylisting/cards.html ← an HTL script a registered servlet with matching properties ← your servletCopy Copy So you could implement Load More as either an HTL script or a servlet. The reason to choose a servlet is that the logic is substantial and belongs in testable Java rather than in a template.
"A resource-type servlet occupies the same slot in Sling's resolution as an HTL script — they're alternatives for the same job. I use a servlet when there's real logic that belongs in testable Java, and a script when it's genuinely presentational. Knowing they're interchangeable is useful, because it means a servlet with a selector doesn't need any special URL — it just uses the normal content URL with a selector on it."
3.3 What a servlet has access to Because it takes SlingHttpServletRequest, a servlet gets the whole Sling context:
Resource resource = request.getResource(); // the resolved resource ResourceResolver resolver = request.getResourceResolver(); // the user's session RequestPathInfo info = request.getRequestPathInfo(); // selectors, suffix, extension String param = request.getParameter("q"); // query/form parameterCopy Copy getResourceResolver() is important and easy to get wrong. It returns a resolver bound to the current user — anonymous on publish. So repository reads through it automatically respect that user's permissions.
That is usually what you want. If you need to read something the visitor cannot see, you need a service user — the pattern from file 01, with try-with-resources. But do that deliberately, because you are stepping outside the permission model on purpose.
And crucially: never close the request's resolver. You did not open it; Sling did, and Sling will close it. Closing it breaks the rest of the request.
That is a genuinely good detail — it inverts the usual "always close your resolver" rule, and knowing when the rule applies shows real understanding.
4 sections hidden in Learn mode 4. Important Interview Questions · 5. Cross Questions — how this topic gets drilled · 6. Best Interview Answers · 7. Real Project Examples Show 8. Coding Examples 8.1 Resource-type servlet with all four methods The reference example. Note the base class choice and the input handling.
package com.energy.core.servlets;
import com.energy.core.services.ProductDataService; import com.fasterxml.jackson.databind.ObjectMapper; import org.apache.sling.api.SlingHttpServletRequest; import org.apache.sling.api.SlingHttpServletResponse; import org.apache.sling.api.resource.Resource; import org.apache.sling.api.servlets.HttpConstants; import org.apache.sling.api.servlets.SlingAllMethodsServlet; import org.apache.sling.servlets.annotations.SlingServletResourceTypes; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Reference; import org.slf4j.Logger; import org.slf4j.LoggerFactory;
import javax.servlet.Servlet; import java.io.IOException;
/**
-
Demonstrates all four methods on ONE servlet for reference.
-
In production these would normally be separate servlets with
-
separate selectors -- this is deliberately a teaching example. */ @Component(service = Servlet.class) @SlingServletResourceTypes( resourceTypes = "energy/components/productdetail", selectors = "specs", extensions = "json", methods = { HttpConstants.METHOD_GET, HttpConstants.METHOD_POST, HttpConstants.METHOD_PUT, HttpConstants.METHOD_DELETE } ) public class ProductSpecsServlet extends SlingAllMethodsServlet { // SlingAllMethodsServlet because this one WRITES. // A read-only servlet would extend SlingSafeMethodsServlet.
private static final long serialVersionUID = 1L; private static final Logger LOG = LoggerFactory.getLogger(ProductSpecsServlet.class); private static final ObjectMapper MAPPER = new ObjectMapper();
/**
- @Reference works because a servlet IS an OSGi component.
- MANDATORY by default -- if this service isn't registered,
- this servlet never activates and the URL 404s with no error. */ @Reference private ProductDataService productDataService;
// ---------------- GET: read ----------------
@Override protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response) throws IOException {
}
// ---------------- POST: create / submit ----------------
@Override protected void doPost(SlingHttpServletRequest request, SlingHttpServletResponse response) throws IOException {
}
// ---------------- PUT: replace ----------------
@Override protected void doPut(SlingHttpServletRequest request, SlingHttpServletResponse response) throws IOException {
}
// ---------------- DELETE: remove ----------------
@Override protected void doDelete(SlingHttpServletRequest request, SlingHttpServletResponse response) throws IOException {
}
// ---------------- helpers ----------------
/**
- Reads the suffix. NEVER trust a value that arrived in a URL --
- validate its shape before using it. */ private String readProductId(SlingHttpServletRequest request) { String suffix = request.getRequestPathInfo().getSuffix(); if (suffix == null) { return null; // getSuffix() is null when absent } String id = suffix.replaceFirst("^/", "").trim(); // Whitelist the shape rather than blacklisting bad characters return id.matches("[A-Za-z0-9_-]{1,64}") ? id : null; }
private void writeJson(SlingHttpServletResponse response, Object payload) throws IOException { // Set BOTH -- container defaults vary and a wrong charset // mangles non-ASCII characters. response.setContentType("application/json"); response.setCharacterEncoding("UTF-8"); // Serialise with a real library. String concatenation breaks // the moment a value contains a quote or apostrophe. MAPPER.writeValue(response.getWriter(), payload); }
private void sendError(SlingHttpServletResponse response, int status, String message) throws IOException { response.setStatus(status); writeJson(response, java.util.Collections.singletonMap("error", message)); } }Copy Copy The eight decisions to be able to defend:
SlingAllMethodsServlet because it writes. Read-only would be the safe class.
@Reference works because a servlet is an OSGi component — and the comment names the failure mode.
The suffix is read via getRequestPathInfo(), never string-sliced.
Input is whitelisted by shape, not blacklisted — a URL value can be anything.
Errors log the detail and return something generic. A stack trace to the client is an information leak.
Content type and encoding are both set explicitly.
JSON is serialised by Jackson, not built with string concatenation.
Correct status codes — 201 for created, 204 for no content, 400 for bad input, 404, 500.
8.2 The cacheable Load More servlet — file 02's story in code package com.energy.core.servlets;
import org.apache.sling.api.SlingHttpServletRequest; import org.apache.sling.api.SlingHttpServletResponse; import org.apache.sling.api.request.RequestDispatcherOptions; import org.apache.sling.api.servlets.HttpConstants; import org.apache.sling.api.servlets.SlingSafeMethodsServlet; import org.apache.sling.servlets.annotations.SlingServletResourceTypes; import org.osgi.service.component.annotations.Component;
import javax.servlet.Servlet; import java.io.IOException;
/**
-
Serves additional card batches for the product listing.
-
WHY RESOURCE TYPE + SELECTOR + SUFFIX, not a path + query parameter:
-
The dispatcher builds its cache filename from the URL PATH.
-
A selector and a suffix are part of the path, so
-
-
is cached as a file, exactly like a page.
-
A query parameter is not part of the path, so
-
-
goes through to publish on EVERY Load More click.
-
Same data. One caches, the other doesn't.
-
It also means content ACLs apply automatically, because Sling
-
resolves a real resource before this code runs. */ @Component(service = Servlet.class) @SlingServletResourceTypes( resourceTypes = "energy/components/categorylisting", selectors = "cards", extensions = "html", methods = HttpConstants.METHOD_GET ) public class CategoryCardsServlet extends SlingSafeMethodsServlet { // SAFE methods class: this endpoint only reads.
private static final long serialVersionUID = 1L; private static final int MAX_BATCH = 20;
@Override protected void doGet(SlingHttpServletRequest request, SlingHttpServletResponse response) throws IOException {
}
/**
- NEVER trust a suffix value. Without clamping, someone requesting
- batch 99999 makes the server build an enormous list. */ private int parseBatch(String suffix) { if (suffix == null) { return 1; } try { int value = Integer.parseInt(suffix.replace("/", "").trim()); return Math.max(1, Math.min(value, MAX_BATCH)); } catch (NumberFormatException e) { return 1; } } }Copy Copy 8.3 A path-bound servlet — done correctly When path binding is genuinely justified, this is what it has to include.
package com.energy.core.servlets;
import com.energy.core.services.CrmService; import org.apache.sling.api.SlingHttpServletRequest; import org.apache.sling.api.SlingHttpServletResponse; import org.apache.sling.api.resource.ResourceResolver; import org.apache.sling.api.servlets.SlingAllMethodsServlet; import org.apache.sling.servlets.annotations.SlingServletPaths; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.annotations.Reference; import org.slf4j.Logger; import org.slf4j.LoggerFactory;
import javax.servlet.Servlet; import java.io.IOException;
/**
- Handles "request a quote" submissions, which go to an external CRM
- rather than to a repository node.
- PATH-BOUND IS JUSTIFIED HERE because there is genuinely no content
- context -- nothing is created in the repository, so there is no
- resource for a resource-type registration to attach to.
- TWO THINGS THIS COSTS US, both handled explicitly below:
-
- NO CONTENT ACLs. Sling resolves no resource, so no permission
-
-
- DISPATCHER WHITELIST. /bin is denied by default, so this needs
-
-
*/ @Component(service = Servlet.class) @SlingServletPaths("/bin/energy/quote-request") public class QuoteRequestServlet extends SlingAllMethodsServlet {
}Copy Copy And the dispatcher rule it requires:
/bin is denied by default. Without this, the servlet works on
author and returns 404 on the published site.
/0200 { /type "allow" /path "/bin/energy/quote-request" /method "POST" }Copy Copy Note the rule is as narrow as possible — one exact path, one method. Allowing /bin/* for all methods would expose every path-bound servlet in the system, including ones you didn't write.
8.4 A Sling filter package com.energy.core.filters;
import org.apache.sling.api.SlingHttpServletRequest; import org.apache.sling.api.SlingHttpServletResponse; import org.apache.sling.engine.EngineConstants; import org.apache.sling.servlets.annotations.SlingServletFilter; import org.apache.sling.servlets.annotations.SlingServletFilterScope; import org.osgi.service.component.annotations.Component; import org.osgi.service.component.propertytypes.ServiceRanking;
import javax.servlet.*; import java.io.IOException;
/**
-
Adds security headers to every request.
-
REQUEST scope = runs ONCE per request.
-
COMPONENT scope would run for every component include, which for
-
response headers would be pointless work repeated dozens of times. */ @Component(service = Filter.class) @SlingServletFilter(scope = SlingServletFilterScope.REQUEST) @ServiceRanking(-700) // ordering relative to other filters public class SecurityHeadersFilter implements Filter {
@Override public void doFilter(ServletRequest request, ServletResponse response, FilterChain chain) throws IOException, ServletException {
}
@Override public void init(FilterConfig config) { } @Override public void destroy() { } }Copy Copy 8.5 Unit testing a servlet package com.energy.core.servlets;
import com.energy.core.services.ProductDataService; import io.wcm.testing.mock.aem.junit5.AemContext; import io.wcm.testing.mock.aem.junit5.AemContextExtension; import org.junit.jupiter.api.BeforeEach; import org.junit.jupiter.api.Test; import org.junit.jupiter.api.extension.ExtendWith; import org.mockito.Mockito;
import static org.junit.jupiter.api.Assertions.*;
@ExtendWith(AemContextExtension.class) class ProductSpecsServletTest {
}Copy Copy Note what is being tested: the failure paths. Missing input, malformed input, the content type. Testing the happy path proves the least — it is the bad input and the error handling that break in production, and for a public endpoint the malformed-input test is the one a security reviewer will look for.
- Common Mistakes The mistake What actually happens The fix Path-bound servlet with no authorisation Unauthenticated endpoint — anyone with the URL gets data Register by resource type, or check permissions in code Path-bound servlet not whitelisted in the dispatcher Works on author, 404 on publish Add a narrow allow rule for that exact path and method /bin/* allowed broadly in the dispatcher Exposes every path-bound servlet, including ones you didn't write One rule per path, per method Resource-type servlet with no selector Takes over the page render for that resource type Always use a selector Query parameters instead of a selector or suffix Response isn't dispatcher-cacheable Selector + suffix Not clamping a value from the URL Someone requests page 99999 and the server builds a huge list Parse, validate the shape, clamp SlingAllMethodsServlet for a read-only servlet Advertises methods you didn't implement; a review finding SlingSafeMethodsServlet Closing request.getResourceResolver() Breaks the rest of the request — you didn't open it Only close resolvers you opened yourself Leaking a service-user resolver you opened Session leak, eventually memory exhaustion try-with-resources Returning a stack trace to the client Leaks internal paths, class names, versions Log the detail, return something generic Building JSON with string concatenation Breaks on the first quote or apostrophe in the data Jackson or another serialiser Not setting content type and encoding Wrong rendering, mangled non-ASCII characters Set both explicitly Always returning 200 Clients can't distinguish success from failure 201, 204, 400, 403, 404, 500 as appropriate Parsing the URL with substring Breaks as soon as a selector or suffix appears getRequestPathInfo() Heavy logic inside a filter Runs on every single request, so the cost multiplies Move it to a servlet or service Forgetting the servlet is an OSGi component A missing @Reference gives a silent 404 Check /system/console/components
- Best Practices On registration. Default to resource type. Always include a selector so you don't shadow the page render. Use path binding only when there is genuinely no content context, and when you do, add the authorisation check and the dispatcher rule in the same change.
On base classes. SlingSafeMethodsServlet unless it writes. That is free documentation and a smaller surface.
On input. Everything from a URL or a form is untrusted. Validate by whitelisting the expected shape rather than blacklisting bad characters, and clamp anything numeric.
On output. Set content type and encoding explicitly. Serialise with a library. Use meaningful status codes. Never return a stack trace.
On resolvers. Never close the request's resolver. Always close one you opened yourself, with try-with-resources. Use a service user only when you genuinely need to read past the visitor's permissions, and check that the output doesn't leak what you just read.
On structure. Keep servlets thin — parse the request, call a service, write the response. Business logic belongs in an OSGi service where it is reusable and testable.
On caching. Decide cacheability before you write the endpoint. Selector and suffix if it should cache; accept that query parameters won't.
On filters. Only for genuinely cross-cutting concerns, REQUEST scope unless you specifically need per-include behaviour, and keep them light.
- Debugging Tips The single most useful console for this topic is /system/console/servletresolver. You give it a URL and it tells you exactly which servlet or script will handle it — and which candidates it considered and rejected. That converts "why isn't my servlet called" from guesswork into one reading.
The four-step checklist, in order:
/system/console/servletresolver — what actually resolves for that URL? /system/console/components — is the servlet component Active or Unsatisfied? A missing @Reference gives a silent 404. Registration properties — do resource type, selector, extension and method all match? Dispatcher — if path-bound and only failing on publish, check dispatcher.log to see whether the request reached AEM at all. Reading the status code is a diagnostic in itself:
Status What it tells you 404 No servlet matched — or the component is unsatisfied, or the dispatcher blocked it 405 A servlet was found, but that HTTP method isn't implemented — wrong base class, or methods doesn't include it 403 Permission denied — the resource ACL, or your own check 500 Your code threw — check error.log 200 with an empty body The servlet ran but wrote nothing — usually an early return without a status 404 versus 405 is the most useful distinction here. 405 means the resolution worked and only the method is wrong, which eliminates three of the four checks above immediately.
Console Answers /system/console/servletresolver Which servlet handles this URL /system/console/components Is the servlet component Active or Unsatisfied /system/console/services Is it registered under javax.servlet.Servlet /system/console/slinglog A DEBUG logger for just your servlet package dispatcher.log Did the request even reach AEM Browser network tab Status code, content type, response body 12. Performance Optimization Make it cacheable, and this dominates everything else. A resource-type servlet with a selector and suffix produces a path-based URL that the dispatcher caches as a file. The same endpoint with query parameters hits publish on every single request. On the Load More story that was the difference between a cached fragment and a page-tree traversal per click.
Keep the servlet thin. Parse, delegate to a service, write. Logic in a service can be cached at the service layer and reused by models and other servlets.
Never make a blocking external call without a timeout. Same rule as file 06 — a hung upstream consumes request threads until the instance stops responding.
Clamp everything. Limits, page sizes, batch numbers. An unbounded value from a URL is how someone makes your server do arbitrary amounts of work.
Return only what's needed. A JSON endpoint returning the full node tree when the client uses four fields wastes bandwidth and serialisation time.
Set cache headers deliberately, so the CDN can cache too, not just the dispatcher.
Keep filters minimal. A filter runs on every request, so a few milliseconds there is a few milliseconds on every page in the site.
2 sections hidden in Learn mode 13. Real Production Scenarios · 14. Follow-up Questions Show 15. Comparison Tables Path vs Resource Type — syllabus point 15
@SlingServletPaths @SlingServletResourceTypes URL Fixed, e.g. /bin/energy/x Content path + selector Real resource behind it No Yes Content ACLs Not applied Applied automatically Authorisation Your responsibility Free Dispatcher Must be whitelisted Already allowed Cacheable Rarely Yes, with selector/suffix Needs content to exist No Yes Recommendation Exception Default SlingSafeMethodsServlet vs SlingAllMethodsServlet — syllabus point 16
Safe All Methods GET, HEAD, OPTIONS, TRACE + POST, PUT, DELETE Changes state No Yes Override doGet, doHead + doPost, doPut, doDelete Relationship — Extends the safe one Use for Read-only endpoints Anything that writes HTTP methods
Method Purpose Safe Idempotent Typical success status GET Read Yes Yes 200 POST Create / submit No No 201 or 202 PUT Replace No Yes 200 or 204 DELETE Remove No Yes 204 Servlet vs Filter vs Sling Model
Sling Model Servlet Filter Runs when A page renders A URL is requested directly Every request Purpose Supply data to HTL Handle a data request Cross-cutting concerns Injection @OSGiService @Reference @Reference Use for Component logic AJAX, form handlers, JSON Headers, logging, redirects Selector vs Suffix vs Query parameter (from file 01, applied here)
Selector Suffix Query param Example .cards. .html/2 ?page=2 Part of the path Yes Yes No Dispatcher-cacheable Yes Yes No Read with getSelectors() getSuffix() getParameter() Default servlets
DefaultGetServlet SlingPostServlet Handles GET with no match POST to a repository path Produces .json, .html, .xml Creates/updates nodes Known for .infinity.json Saving dialogs Security concern Can dump content Anyone with write access can create nodes
6 sections hidden in Learn mode 16. Memory Tricks · 17. Revision Notes · 18. Cheat Sheet · 19. Frequently Forgotten Things · 20. Final Interview Summary · 21. Mock Interview Show Next topic 08-HTL-Sightly — your syllabus points 17 and 18: how you call a Sling Model from HTL, every data-sly block element and what it's for, expression options and the display contexts, and why HTL deliberately can't do what JSP could.