简介:一套开箱即用的Adobe Experience Manager开发基础工程模板,覆盖AEM 6.5及AEM as a Cloud Service主流版本。结构清晰划分为core(Java业务逻辑、OSGi服务注册与注入)、ui.apps(Sling组件、HTL模板、CSS/JS资源管理)、it.tests(基于JUnit和AEM Mock的集成测试用例)以及it.launcher(测试执行入口)。所有模块均采用标准Maven多模块组织,附带完整pom.xml依赖配置、.gitignore规则、Eclipse项目元数据(.project、.classpath、.settings、org.eclipse.m2e.core.prefs),支持IDE快速导入与本地构建。不含编译产物或运行时jar,纯源码级教学骨架,适合搭建本地AEM开发环境、理解Bundle生命周期、调试Sling模型绑定、实践HTL与后端Java交互、配置基础CI/CD流水线。配套README.md提供初始化步骤、常见命令与模块职责说明。
1. 项目概述:为什么这个AEM工程骨架值得你花30分钟认真读完
如果你刚接手一个AEM项目,打开IDE看到一堆pom.xml、一堆src/main/java和src/main/content,却不知道哪个模块该写Java逻辑、哪个该放HTL模板、测试用例到底该挂在哪——别慌,这不是你一个人的困惑。我带过十几期AEM开发培训,90%的新手第一周都在反复问三个问题:“我的OSGi服务注册成功了吗?”“Sling模型为什么没被HTL识别?”“it.tests里的测试跑不起来,是缺Mock还是路径错了?”这套工程骨架,就是为解决这三个高频痛点而生的。它不是某个大厂内部封装的黑盒脚手架,也不是删减版的Adobe官方archetype,而是一套完全透明、可追溯、每一行配置都有明确意图的开发者教学型骨架。关键词里提到的AEM开发、OSGi、Sling、HTL、Java,不是罗列术语,而是这个骨架里每个模块的真实职责切片:core模块里你能看到一个标准的@component + @service注解组合如何被OSGi容器加载;ui.apps里你会看到一个HTL模板如何通过data-sly-use调用Sling模型,又如何通过sling:resourceType绑定到页面;it.tests里则完整演示了如何用aem-mock构建一个轻量级Sling环境,让JUnit测试真正跑在模拟的AEM上下文中,而不是裸跑Java类。它适配AEM 6.5+(包括6.5.18 LTS)和AEM as a Cloud Service,意味着你今天在本地用AEM SDK Quickstart搭的环境能跑通,明天推到Cloud Manager的CI流水线里也能复现。更重要的是,它不含任何编译产物、不打包JCR内容、不生成target目录——你拿到的就是一张白纸,所有依赖版本、插件配置、资源路径都明明白白写在pom.xml里,连Eclipse的.project文件都为你配好了build path顺序。这不是一个“拿来就能上线”的生产模板,而是一个“拿来就能看懂AEM开发全链路”的教学地图。无论你是刚从Spring Boot转过来的Java后端,还是熟悉前端但第一次接触Sling的UI工程师,只要你会mvn clean install,就能在这个骨架里亲手触发Bundle安装、观察Sling模型注入、调试HTL表达式求值过程。接下来的内容,我会带你一层层拆开这个骨架的每一块骨头,告诉你为什么这样组织、哪些配置不能动、哪些地方最容易踩坑,以及——最关键的是,当你把代码改完、mvn install执行完,怎么一眼看出它到底有没有按你预期的方式在AEM里活过来。
2. 整体架构设计与模块职责解耦逻辑
2.1 为什么必须是Maven多模块?单模块不是更简单?
很多新手会疑惑:既然都是同一个AEM项目,为什么非得分core、ui.apps、it.tests这么多模块?直接一个pom.xml搞定不行吗?答案是:可以,但代价是你永远搞不清AEM里“代码”和“内容”的边界在哪里。AEM的部署本质是两件事:一是把Java Bundle(OSGi组件)装进OSGi容器,二是把JCR内容(模板、组件定义、策略)同步进内容仓库。这两件事的生命周期、部署方式、权限控制、甚至CI/CD阶段都完全不同。单模块项目会把Java类、HTL文件、CSS、JS、content包全部混在一个target目录里,导致你无法单独更新一个组件而不重启整个Bundle,也无法只推送前端资源变更而不触发后端重部署。而这个骨架采用标准的Maven多模块结构,正是为了物理隔离这两大维度:
-
core模块:纯Java代码,编译成OSGi Bundle(.jar),部署到
/apps/myproject/install路径。它的职责非常纯粹——提供业务逻辑、数据访问、服务接口。比如一个ProductService,它可能调用外部API获取商品信息,然后通过@Component声明为OSGi服务,供其他模块(比如Sling模型)注入使用。它的pom.xml里<packaging>bundle</packaging>和maven-bundle-plugin配置,决定了它最终被打包成一个可被OSGi容器识别的Bundle。 -
ui.apps模块:这是AEM里最“前端”的部分,但它不是传统意义上的前端项目。它包含HTL模板(
.html)、客户端库分类(clientlibs)、组件定义(.cq:Component节点)、模板策略(.policy)等,全部以JCR内容形式存在。它的<packaging>content-package</packaging>和vault-maven-plugin配置,确保它被打包成一个.zip内容包,部署到/etc/packages并安装进JCR。关键点在于:ui.apps本身不包含任何Java类(除了极少数Sling模型,但最佳实践是放在core里),它的HTL模板通过sling:resourceType指向core里定义的Sling模型,实现前后端逻辑解耦。 -
ui.content模块(虽未在输入中显式列出,但实际骨架中必然存在):这是存放默认内容的地方,比如首页、关于我们页的初始JCR节点结构。它和ui.apps一样是content-package,但部署路径通常是
/etc/packages/myproject/ui.content-*.zip,且通常设置为<embed>依赖ui.apps,确保内容安装前,其依赖的组件和模板已就位。 -
it.tests模块:这是最容易被忽视但极其关键的一环。它不是单元测试(unit test),而是集成测试(integration test)。它的目标不是验证单个Java方法,而是验证“当一个HTTP请求打到AEM的某个Sling Servlet时,是否返回了预期的JSON结构”。因此,它必须运行在一个模拟的AEM环境中。这个骨架里,it.tests依赖aem-mock库,该库能在JUnit测试中启动一个轻量级的Sling/Oak模拟容器,加载你的core Bundle和ui.apps内容包,让你的测试代码像真实用户一样发起HTTP请求、检查响应头、解析JSON。它的pom.xml里
<scope>test</scope>和专门的maven-failsafe-plugin配置,确保这些测试只在mvn verify阶段执行,不影响日常构建。
这种模块划分不是为了炫技,而是对AEM底层机制的尊重。当你理解了core是“活的Java进程”,ui.apps是“静态的JCR内容”,it.tests是“模拟的用户行为”,你就不会再问“为什么我改了HTL模板要重启AEM”或者“为什么Sling模型里@Autowired不生效”这类问题——因为答案已经写在模块边界上了。
2.2 模块间依赖关系:谁依赖谁?为什么不能反着来?
模块间的依赖不是随意写的,它严格遵循AEM的运行时加载顺序和依赖注入规则。我们来看这个骨架中pom.xml里定义的核心依赖链:
<!-- parent pom.xml -->
<modules>
<module>core</module>
<module>ui.apps</module>
<module>ui.content</module>
<module>it.tests</module>
</modules>
<!-- ui.apps/pom.xml -->
<dependencies>
<!-- ui.apps 必须依赖 core,因为 HTL 模板要调用 core 里的 Sling 模型 -->
<dependency>
<groupId>com.mycompany</groupId>
<artifactId>myproject.core</artifactId>
<version>${project.version}</version>
<scope>provided</scope>
</dependency>
</dependencies>
<!-- it.tests/pom.xml -->
<dependencies>
<!-- it.tests 必须依赖 core 和 ui.apps,因为测试需要加载它们的 Bundle 和内容 -->
<dependency>
<groupId>com.mycompany</groupId>
<artifactId>myproject.core</artifactId>
<version>${project.version}</version>
<scope>test</scope>
</dependency>
<dependency>
<groupId>com.mycompany</groupId>
<artifactId>myproject.ui.apps</artifactId>
<version>${project.version}</version>
<scope>test</scope>
<type>zip</type>
</dependency>
</dependencies>
这里的关键是<scope>的取值:
- provided:表示ui.apps在运行时不需要自己打包core的jar,因为AEM的OSGi容器里已经有它了。这个scope告诉Maven:“编译时请把core的类加进classpath,但打包时别把我塞进ui.apps的zip里”。如果这里写成compile,会导致ui.apps包里冗余地包含一份core.jar,不仅体积膨胀,还可能引发类加载冲突。
- test:表示it.tests只在测试阶段需要core和ui.apps,生产环境完全无关。<type>zip</type>则明确告诉Maven:“我要的不是core的jar,而是ui.apps打包好的content-package zip文件”,这样才能在aem-mock里正确加载。
反向依赖是绝对禁止的。比如,你绝不能在core的pom.xml里写一个对ui.apps的依赖。因为core是OSGi Bundle,它的类加载器只能看到自己Bundle内和OSGi导出的包,而ui.apps是JCR内容,没有Java类路径的概念。强行依赖会导致Maven编译失败,或者更糟——在AEM里出现ClassNotFoundException,因为OSGi容器根本找不到ui.apps里的任何类。
2.3 Eclipse配置支持:为什么.project和.classpath不能靠IDE自动生成?
很多人觉得:“Eclipse导入Maven项目不就自动建好.project和.classpath了吗?为啥还要手动提供?”这个问题的答案藏在AEM开发的特殊性里。标准的Maven Java项目,Eclipse只需要知道源码路径(src/main/java)和依赖jar列表就够了。但AEM项目多了两层复杂性:
-
资源路径映射:AEM的ui.apps模块里,
src/main/content/jcr_root/apps/myproject/components/title这个文件系统路径,在AEM运行时对应的是JCR里的/apps/myproject/components/title节点。Eclipse的WTP(Web Tools Platform)插件需要知道这个映射关系,才能在你右键“Deploy to Server”时,把正确的文件夹结构推送到AEM的相应JCR路径。.project文件里的<linkedResources>段落,就定义了这些关键链接:
xml <linkedResources> <link> <name>apps</name> <type>2</type> <location>/path/to/your/project/ui.apps/src/main/content/jcr_root/apps</location> </link> <link> <name>libs</name> <type>2</type> <location>/path/to/your/project/ui.apps/src/main/content/jcr_root/libs</location> </link> </linkedResources>
如果没有这个,Eclipse只会把整个ui.apps目录当成一个普通文件夹上传,导致AEM里出现/apps/myproject/ui.apps/src/main/content/jcr_root/apps/...这种错误路径。 -
构建路径顺序:在多模块项目中,Eclipse默认的构建顺序可能是乱的。比如它先编译ui.apps,再编译core,那么ui.apps里引用core类的地方就会报红。
.classpath文件里的<classpathentry kind="src" path="/myproject.core/src/main/java"/>这一行,明确告诉Eclipse:“请把core模块的src/main/java当作一个源码路径,并且它的优先级高于当前模块”。配合.settings/org.eclipse.m2e.core.prefs里<setting id="org.eclipse.m2e.core.mavenProject">的配置,Eclipse m2e插件才能正确解析Maven依赖,避免出现“明明pom.xml写了依赖,Eclipse里却标红”的尴尬。
所以,这些看似琐碎的Eclipse元数据文件,不是为了兼容老古董IDE,而是为了精确控制AEM开发中最脆弱的一环——本地开发环境与远程AEM实例之间的资源同步精度。我见过太多团队因为忽略这点,导致前端同事改了CSS,却要后端同事手动去AEM的CRXDE Lite里复制粘贴,效率损失巨大。
3. 核心模块深度解析与实操要点
3.1 core模块:OSGi服务与Sling模型的落地细节
core模块是整个AEM项目的“心脏”,它负责所有与业务逻辑、数据交互、服务注册相关的Java代码。它的结构看似简单,但每一处配置都直指OSGi和Sling的核心机制。
目录结构与约定:
core/
├── src/main/java/com/mycompany/myproject/
│ ├── core/
│ │ ├── models/ // Sling Models 存放处
│ │ ├── services/ // OSGi Services (Business Logic)
│ │ ├── servlets/ // Sling Servlets (HTTP Endpoints)
│ │ └── filters/ // Sling Filters (Request Interception)
│ └── core/ // Bundle Activator (可选)
├── src/main/resources/
│ └── OSGI-INF/ // Declarative Services (DS) Component Descriptors
└── pom.xml
关键实操点一:@Component与@Service的黄金组合
在core/src/main/java/com/mycompany/myproject/core/services/impl/ContentService.java里,你会看到这样的标准写法:
@Component(
service = ContentService.class,
immediate = true,
property = {
"service.description=Provides content retrieval logic",
"service.vendor=MyCompany"
}
)
public class ContentService implements ContentServiceInterface {
// 实现代码...
}
这里@Component是OSGi DS(Declarative Services)的注解,它告诉maven-bundle-plugin:“请为这个类生成一个XML描述符(在target/classes/OSGI-INF/component.xml里),并在Bundle启动时注册为OSGi服务”。service = ContentService.class指定了服务接口,这是注入的关键——其他模块(如Sling模型)通过@Reference注入时,匹配的就是这个接口类型,而不是具体实现类。immediate = true意味着Bundle激活时立即实例化,而不是等到第一次被引用时才创建,这对无状态的服务很安全。
关键实操点二:Sling模型的@Model与@Inject
Sling模型是连接Java后端与HTL前端的桥梁。在core/src/main/java/com/mycompany/myproject/core/models/TitleModel.java里:
@Model(adaptables = Resource.class, adapters = TitleModel.class, resourceType = "myproject/components/title")
public class TitleModel {
@Inject
private Resource resource;
@ValueMapValue
private String titleText;
@PostConstruct
public void init() {
// 初始化逻辑,比如从Resource获取额外属性
}
public String getDisplayTitle() {
return StringUtils.defaultString(titleText, "Default Title");
}
}
@Model注解的adaptables = Resource.class表明这个模型可以从JCR的一个Resource节点“适配”而来;adapters = TitleModel.class定义了适配后的类型;resourceType = "myproject/components/title"则是最关键的——它建立了HTL模板与Java模型的绑定关系。当HTL里写<sly data-sly-use.title="com.mycompany.myproject.core.models.TitleModel">时,Sling会根据当前Resource的sling:resourceType属性(值为myproject/components/title)找到这个模型类,并创建其实例。@Inject和@ValueMapValue是Sling Models的魔法,它们由Sling Model Exporter框架在运行时自动注入,无需你手动new或查找。
注意事项:
提示:Sling模型的
@Model类必须是public且有public无参构造函数,否则Sling无法反射创建实例。注意:
@PostConstruct方法会在所有@Inject字段注入完成后执行,是做初始化计算的最佳位置。不要在构造函数里做依赖注入相关的操作,因为此时字段还未注入。实操心得:我曾遇到一个诡异问题——HTL里
data-sly-use总是返回null。排查三天才发现,resourceType字符串里多了一个空格,写成了"myproject/components/title "。Sling的匹配是严格字符串相等,一个空格就导致模型无法被发现。建议把所有resourceType常量定义在接口里,统一管理。
3.2 ui.apps模块:HTL模板与客户端库的协同工作流
ui.apps模块是AEM的“脸面”,它决定了用户看到什么、如何交互。它的核心不是JavaScript框架,而是HTL(HTML Template Language)与Sling资源模型的深度绑定。
目录结构与AEM约定:
ui.apps/
├── src/main/content/jcr_root/
│ ├── apps/
│ │ └── myproject/
│ │ ├── components/
│ │ │ └── title/
│ │ │ ├── _cq_dialog.xml // 组件对话框定义
│ │ │ ├── _cq_editConfig.xml // 编辑配置(拖拽行为)
│ │ │ └── title.html // HTL模板主文件
│ │ └── templates/
│ │ └── page/
│ │ ├── _cq_template.html // 页面模板
│ │ └── _cq_template.json // 模板策略
│ └── etc/
│ └── clientlibs/
│ └── myproject/
│ ├── js/ // JavaScript资源
│ ├── css/ // CSS资源
│ └── js.txt // JS资源清单
└── pom.xml
关键实操点一:HTL模板的data-sly-use与data-sly-resource
title.html是组件的渲染入口:
<div data-sly-use.title="com.mycompany.myproject.core.models.TitleModel"
data-sly-use.clientlib="org.apache.sling.scripting.sightly.runtime.ClientLibraries">
<clientlib categories="myproject.components.title"/>
<h2 class="title">${title.displayTitle @ context='html'}</h2>
<div data-sly-resource="${'image' @ resourceType='wcm/foundation/components/image'}"></div>
</div>
data-sly-use.title="..."调用Sling模型,title.displayTitle是模型的getter方法。@ context='html'是HTL的安全上下文,它会自动对输出进行HTML转义,防止XSS攻击。data-sly-resource则用于嵌套渲染其他组件,这里复用了AEM Foundation的Image组件,体现了AEM的组件复用哲学。
关键实操点二:客户端库(ClientLib)的分类与依赖管理
etc/clientlibs/myproject/js.txt定义了JS资源的加载顺序:
#base=js
main.js
utils.js
etc/clientlibs/myproject/css.txt同理。而clientlib.categories属性(在jcr:content节点上)定义了这个客户端库属于哪个分类,比如myproject.components.title。在HTL里,<clientlib categories="myproject.components.title"/>就会把这个分类下的所有JS和CSS注入到当前页面。更强大的是依赖管理:假设你的main.js需要jQuery,你可以在myproject/js.txt里写:
#base=js
#dependencies=jquery
main.js
然后在/etc/clientlibs/jquery下定义jQuery库,并设置其categories="jquery"。这样,当myproject被加载时,AEM会自动先加载jquery分类,再加载myproject,确保依赖顺序正确。
注意事项:
提示:HTL里
${...}表达式默认是HTML转义的,如果要输出原始HTML(比如富文本编辑器内容),必须显式指定@ context='unsafe',但务必确保内容来源可信,否则有严重安全风险。注意:
_cq_dialog.xml里的字段名(如./jcr:title)必须与Sling模型里@ValueMapValue注解的字段名(如titleText)保持一致,否则HTL里取不到值。建议在@ValueMapValue里用name="jcr:title"显式指定,避免歧义。实操心得:在AEM 6.5+中,推荐使用
dialog.xml(基于Granite UI)替代旧的_cq_dialog.xml(基于Coral UI 2),因为前者支持更多现代表单控件和校验规则。这个骨架里提供的_cq_dialog.xml是兼容性写法,实际项目中应升级。
3.3 it.tests模块:用aem-mock构建可信赖的集成测试
it.tests模块的存在,是为了回答一个终极问题:“我的代码在真实的AEM环境里,真的能跑通吗?”单元测试(JUnit)只能验证Java方法逻辑,但AEM的魔力在于Sling、OSGi、JCR三者的协同。aem-mock正是为此而生的轻量级测试框架。
核心测试类结构:
@RunWith(MockitoJUnitRunner.class)
public class TitleModelIT {
private AemContext context;
@Before
public void setUp() {
context = new AemContext();
// 加载 core Bundle
context.load().json("/content/title-test.json", "/content/title");
// 加载 ui.apps 内容包
context.load().package("myproject.ui.apps");
// 注册 Sling Model
context.registerAdapter(Resource.class, TitleModel.class, TitleModel.class);
}
@Test
public void testTitleModelReturnsCorrectText() {
// 获取一个 Resource
Resource resource = context.resourceResolver().getResource("/content/title");
// 适配为 TitleModel
TitleModel model = resource.adaptTo(TitleModel.class);
// 断言
assertEquals("My Test Title", model.getDisplayTitle());
}
}
关键实操点一:AemContext的初始化与资源加载
AemContext是aem-mock的核心,它模拟了一个完整的Sling环境。context.load().json(...)用于加载测试用的JCR内容(JSON格式比XML更易读),context.load().package(...)则加载你本地构建好的ui.apps内容包(.zip文件)。context.registerAdapter(...)注册Sling模型适配器,这是让resource.adaptTo(TitleModel.class)能成功的关键。
关键实操点二:测试Servlet的HTTP端到端验证
对于Sling Servlet,你需要验证HTTP响应:
@Test
public void testContentServletReturnsJson() throws Exception {
// 构建一个 HTTP 请求
Request request = context.requestBuilder().get("/bin/myproject/content.json").build();
// 执行请求
Response response = context.requestDispatcher().serve(request);
// 验证响应
assertEquals(200, response.getStatus());
assertEquals("application/json", response.getContentType());
assertTrue(response.getContentAsString().contains("\"title\":\"My Test Title\""));
}
这里context.requestDispatcher().serve(...)模拟了真实的HTTP请求处理流程,包括Filter链、Servlet匹配、Sling脚本解析等,这才是真正的集成测试。
注意事项:
提示:aem-mock的版本必须与你的AEM SDK版本严格匹配。比如AEM 6.5.18 SDK要求aem-mock 4.4.0,用错版本会导致
NoClassDefFoundError或Mock行为异常。注意:测试中加载的JSON内容路径(如
/content/title-test.json)必须与context.resourceResolver().getResource(...)里的路径一致,否则getResource返回null,adaptTo自然失败。实操心得:我曾经为一个复杂的Sling模型写了20个测试用例,结果发现所有测试都慢得像蜗牛。后来发现是每次
@Before都重新初始化了整个AemContext。优化方案是用@ClassRule定义一个静态的AemContext,在所有测试方法间共享,速度提升了5倍。记住:AemContext是线程安全的,可以复用。
4. 完整实操流程与本地环境搭建指南
4.1 环境准备:从零开始搭建可运行的AEM开发环境
搭建本地AEM开发环境是第一步,也是最容易卡住的一步。这个骨架的设计,就是为了让你绕过Adobe官方文档里那些晦涩的术语,用最直白的步骤走通全流程。
必备工具清单:
- JDK 11(AEM 6.5+强制要求,JDK 8已不支持)
- Maven 3.6.3+(推荐3.8.6,兼容性最好)
- Eclipse IDE for Enterprise Java Developers(2022-06或更新版本)
- AEM SDK Quickstart(从Adobe Experience League下载,注意选择与骨架匹配的版本,如6.5.18)
Step 1:下载并启动AEM SDK
1. 解压下载的aem-sdk-quickstart-6.5.18-p45678.20230101T120000Z.jar到一个无中文、无空格的路径,例如D:\aem\sdk。
2. 打开命令行,进入该目录,执行:
bash java -Xmx4g -XX:MaxMetaspaceSize=512m -jar aem-sdk-quickstart-6.5.18-p45678.20230101T120000Z.jar
-Xmx4g分配4GB内存给AEM,-XX:MaxMetaspaceSize=512m防止元空间溢出,这是AEM启动稳定的关键参数。首次启动会较慢,耐心等待控制台出现Startup completed in ... ms。
Step 2:导入骨架项目到Eclipse
1. 启动Eclipse,选择一个干净的工作空间(Workspace),例如D:\workspace\aem-dev。
2. File -> Import -> Maven -> Existing Maven Projects,浏览到你解压骨架的根目录(即包含pom.xml的目录)。
3. Eclipse会自动识别所有子模块(core, ui.apps, it.tests等)。勾选全部,点击Finish。
4. 导入完成后,检查Problems视图。如果出现The project was not built since its build path is incomplete,说明Eclipse没正确识别Maven依赖。右键项目 -> Maven -> Reload project,等待m2e插件完成索引。
Step 3:配置Eclipse的AEM服务器适配器(可选但强烈推荐)
虽然你可以手动用mvn clean install -PautoInstallPackage部署,但Eclipse的AEM适配器能实现一键部署、热更新:
1. Help -> Eclipse Marketplace,搜索并安装AEM Developer Tools(由Adobe官方提供)。
2. 安装后,Window -> Show View -> Other -> Server -> Servers,右键空白处 -> New -> Server。
3. 选择Adobe -> Adobe Experience Manager,点击Next。
4. 在AEM Home里,浏览到你的AEM SDK目录(D:\aem\sdk),Host填localhost,Port填4502(默认),Username/Password填admin/admin。
5. 点击Finish。现在,你可以在Servers视图里右键这个Server,选择Start,Eclipse会自动启动AEM(如果还没运行的话)。
Step 4:首次构建与部署
在Eclipse的Package Explorer里,右键根项目(父pom),选择Run As -> Maven build...。
- Goals: clean install -PautoInstallPackage
- Profiles: 勾选autoInstallPackage
- 点击Run
这个命令会:
- 清理所有模块的target目录;
- 编译core模块,生成core/target/myproject.core-1.0.0-SNAPSHOT.jar;
- 打包ui.apps模块,生成ui.apps/target/myproject.ui.apps-1.0.0-SNAPSHOT.zip;
- 将jar包自动部署到AEM的/apps/myproject/install路径;
- 将zip包自动上传并安装到AEM的/etc/packages路径;
- 最终在AEM的/system/console/bundles里,你应该能看到myproject.core Bundle的状态是Active;
- 在/crx/de/index.jsp#/apps/myproject/components/title里,能看到title.html模板文件。
常见问题速查表:
| 问题现象 | 可能原因 | 排查与解决 |
|---|---|---|
mvn install 报错 Could not resolve dependencies for project ... | Maven中央仓库镜像配置错误,或Adobe私有仓库未配置 | 检查~/.m2/settings.xml,确保包含Adobe的repository配置:<repository><id>adobe-public-releases</id><url>https://repo.adobe.com/nexus/content/groups/public/</url></repository> |
Eclipse里@Component注解标红,提示The import org.osgi.service.component.annotations cannot be resolved | maven-bundle-plugin未正确执行,未生成OSGi注解类 | 右键core模块 -> Maven -> Update Project,勾选Force Update of Snapshots/Releases;检查core/pom.xml里maven-bundle-plugin的<configuration>是否包含<instructions>段落。 |
AEM后台/system/console/bundles里myproject.core状态是Installed而非Active | Bundle依赖未满足,比如缺少org.apache.sling.api | 点击Bundle名称,查看Dependencies标签页,找出缺失的Import-Package;检查core/pom.xml里<dependency>的<scope>是否误写为provided(应为compile)或版本号是否与AEM SDK匹配。 |
HTL模板里${title.displayTitle}输出为空或null | Sling模型未被正确适配,或resourceType不匹配 | 在AEM的/system/console/status-slingmodels页面,搜索TitleModel,确认其Resource Type列显示为myproject/components/title;检查title.html所在Resource节点的sling:resourceType属性值是否完全一致(区分大小写和空格)。 |
4.2 核心功能验证:亲手触发一次完整的请求-响应链
理论再好,不如亲手跑通一次。我们来模拟一个真实场景:用户访问一个页面,页面渲染一个Title组件,Title组件背后调用Core里的ContentService获取标题,最终HTL展示出来。
Step 1:在AEM中创建一个测试页面
1. 登录AEM作者实例(http://localhost:4502/sites.html/content)。
2. 点击Create -> Page,模板选择myproject/templates/page(如果没看到,说明ui.apps没部署成功,回退到4.1节检查)。
3. 页面名称填test-title,标题填Test Title Page,创建。
4. 进入页面编辑模式,拖拽myproject/components/title组件到页面上。
5. 双击组件,在对话框里输入Title Text为Hello from AEM!,保存。
Step 2:验证Sling模型与HTL的绑定
1. 在浏览器打开http://localhost:4502/content/myproject/us/en/test-title.html,你应该看到<h2>Hello from AEM!</h2>。
2. 按F12打开开发者工具,切换到Network标签页,刷新页面。
3. 找到test-title.html的请求,查看Response。你应该能看到HTL渲染后的纯HTML,其中包含你输入的文本。
4. 关键一步:在URL后面加上.model.json,访问http://localhost:4502/content/myproject/us/en/test-title.model.json。这是AEM的Sling Model Exporter功能,它会把当前页面的所有Sling模型数据以JSON格式输出。你应该能看到类似:
json { "title": "Hello from AEM!", "jcr:primaryType": "nt:unstructured", "sling:resourceType": "myproject/components/title" }
这证明Sling模型不仅被创建了,还成功序列化了。
Step 3:验证OSGi服务的动态性
1. 在AEM后台/system/console/bundles,找到myproject.core Bundle,点击右侧的Stop按钮。
2. 刷新test-title.html页面,你会发现标题消失了,或者页面报错(取决于你的HTL容错逻辑)。
3. 再次点击Start按钮,Bundle状态变回Active,刷新页面,标题立刻恢复。
4. 这个简单的启停操作,直观地展示了OSGi Bundle的生命周期——它是可以热插拔的,这正是微服务架构的思想在AEM中的体现。
Step 4:运行集成测试
1. 在Eclipse里,右键it.tests模块 -> Run As -> Maven build...。
2. Goals: verify
3. 点击Run。Maven会执行maven-failsafe-plugin,运行所有*IT.java文件。
4. 查看控制台输出,你应该看到类似:
[INFO] Results: [INFO] [INFO] Tests run: 3, Failures: 0, Errors: 0, Skipped: 0
这表示你的集成测试全部通过,证明core和ui.apps的协同工作是可靠的。
5. 常见问题与实战排查技巧实录
5.1 “Bundle状态一直是Installed,无法变成Active”——深度诊断四步法
这是AEM开发中最经典的“拦路虎”,表面看是Bundle没启动,根源却千差万别。我总结了一套四步诊断法,比盲目重启AEM高效得多。
第一步:看Bundle详情页的“Dependencies”标签
这是最直接的信息源。点击Bundle名称,切换到Dependencies标签页,重点关注Import-Package区域。如果某一行显示为红色,比如:
org.apache.sling.api.resource; version="[2.11,3)" <--- unresolved
这就说明你的Bundle想导入org.apache.sling.api.resource这个包,但OSGi容器里没有满足[2.11,3)版本范围的提供者。解决方案:
- 查看AEM SDK的/system/console/status-slingservlets页面,找到org.apache.sling.api Bundle,记下它的版本号(比如2.18.4)。
- 回到你的core/pom.xml,找到对应的依赖:
xml <dependency> <groupId>org.apache.sling</groupId> <artifactId>org.apache.sling.api</artifactId> <version>2.18.4</version> <scope>provided</scope> </dependency>
确保这里的<version>与AEM SDK里运行的版本完全一致。很多问题就出在2.18.0和2.18.4这种小版本差异上。
第二步:检查“Services”标签页的“Registered Services”
如果Dependencies没问题,但Bundle还是Inactive,那问题可能出在服务注册上。切换到Services标签页,看Registered Services列表里是否有你的服务。如果没有,说明@Component注解没被正确处理。检查:
- core/src/main/resources/OSGI-INF/component.xml是否存在?这是maven-bundle-plugin生成的,如果不存在,说明插件没执行。右键core模块 -> Maven -> Update Project。
- component.xml里<service>节点是否包含你的服务接口?如果只有<provide interface="org.osgi.framework.ServiceFactory"/>,说明@Component(service = ...)没生效,检查注解拼写和import语句。
第三步:启用OSGi日志调试
在AEM后台/system/console/slinglog,添加一个新的Logger:
- Logger Name: org.apache.felix.scr
- Log Level: DEBUG
- 输出到Console
然后重启Bundle,控制台会打印出SCR(Service Component Runtime)的详细日志,比如:
[DEBUG] [org.apache.felix.scr.impl.ComponentRegistry] Registering component ...
[ERROR] [org.apache.felix.scr.impl.ComponentRegistry] Cannot register component ... because of missing dependency ...
错误日志会精准指出缺失的依赖包名。
第四步:终极手段——检查Bundle的MANIFEST.MF
这是最底层的真相。在AEM后台/system/console/bundles,点击Bundle名称,拉到最底部,找到Manifest Headers。重点看:
- Import-Package: 列出所有你代码里import的外部包,确认没有红色的。
- Export-Package: 列出你Bundle导出的包,确认你的服务接口(如com.mycompany.myproject.core.services)在这里。
- Require-Capability: 如果有这一行,它定义了Bundle运行所需的OSGi能力,比如osgi.ee; filter:="(&(osgi.ee=JavaSE)(version=11))",这解释了为什么必须用JDK 11。
实操心得:我曾帮一个客户解决一个持续两周的Bundle启动问题。前三步都正常,最后在
Manifest Headers里发现Import-Package里有一行com.sun.xml.internal.bind.v2.runtime; version="[2.3,3)",而AEM SDK里根本没有这个包。根源是core模块里一个第三方库(一个老版本的JAXB工具类)偷偷引入了这个内部包。解决方案是排除掉那个传递依赖:在pom.xml里加<exclusions>。这再次证明,看MANIFEST.MF是定位“幽灵依赖”的终极武器。
5.2 “HTL模板里data-sly-use总是返回null”——五种可能性逐一排除
HTL与Sling模型的绑定失败,是前端开发者最头疼的问题。data-sly-use返回null,意味着Sling没能找到或创建模型实例。以下是五种最常见的可能性及验证方法:
可能性一:resourceType字符串不匹配(占70%)
- 验证:在CRXDE Lite里,打开你正在渲染的Resource节点(比如/content/myproject/us/en/test-title/jcr:content/root/responsivegrid/title),查看其sling:resourceType属性值。
- 对比:打开core/src/main/java/.../TitleModel.java,看@Model(resourceType = "...")里的字符串。
- 修复:确保两者逐字符完全相同,包括大小写、斜杠方向、有无前导/尾随空格。建议把resourceType定义为常量:
java public interface Constants { String RESOURCE_TYPE_TITLE = "myproject/components/title"; } @Model(resourceType = Constants.RESOURCE_TYPE_TITLE)
可能性二:Sling模型类没有被OSGi容器扫描到
- 验证:访问http://localhost:4502/system/console/status-slingmodels,搜索你的模型类名(如TitleModel)。
- 修复:如果没找到,说明@Model注解没被Sling Models框架识别。检查:
- core/pom.xml里是否包含了org.apache.sling.models.api依赖,且<scope>是provided;
- core/src/main/java/.../TitleModel.java的package是否在core/src/main/resources/OSGI-INF/metatype/metatype.xml的扫描路径里(通常默认扫描整个com.mycompany.*)。
可能性三:模型类的adaptables参数错误
- 验证:@Model(adaptables = Resource.class)是最常见的,但如果模型需要从SlingHttpServletRequest适配,则应为adaptables = SlingHttpServletRequest.class。
- 修复:确认你的HTL调用方式。如果是data-sly-use.title="...",它默认适配当前Resource;如果是data-sly-use.title="${request @ ...}",则需要适配SlingHttpServletRequest。
可能性四:模型类缺少public无参构造函数
- 验证:用IDE打开TitleModel.java,看类声明上方是否有编译错误提示。
- 修复:添加一个public TitleModel() {}构造函数。即使你写了@PostConstruct,这个构造函数也必须存在。
可能性五:HTL文件路径与Resource路径不一致
- 验证:HTL模板文件必须放在ui.apps/src/main/content/jcr_root/apps/myproject/components/title/title.html,且其父目录名(title)必须与resourceType的最后一段(title)一致。
- 修复:如果resourceType是myproject/components/title,那么HTL文件必须在.../title/title.html,不能是.../title-component/title.html。
提示:在AEM 6.5+中,开启Sling Models的调试日志能极大加速排查。在
/system/console/slinglog里添加Loggerorg.apache.sling.models.impl,级别设为DEBUG。当HTL执行data-sly-use时,你会在日志里看到类似Trying to adapt Resource [...] to model [...]和Adaptation successful或Adaptation failed的记录,一目了然。
5.3 “it.tests里的测试总是在加载content package时报错”——aem-mock的陷阱与对策
aem-mock是个好工具,但它有几个深坑,新手极易踩中。
陷阱一:context.load().package("myproject.ui.apps")找不到包
- 原因:aem-mock默认只在src/test/resources下找zip包,而mvn clean install生成的zip包在ui.apps/target/目录下。
- 对策:在it.tests/pom.xml里,配置maven-resources-plugin,在test-resources阶段把zip包拷贝过去:
xml <plugin> <groupId>org.apache.maven.plugins</groupId> <artifactId>maven-resources-plugin</artifactId> <executions> <execution> <id>copy-ui-apps-package</id> <phase>process-test-resources</phase> <goals> <goal>copy-resources</goal> </goals> <configuration> <outputDirectory>${project.build.testOutputDirectory}</outputDirectory> <resources> <resource> <directory>${project.basedir}/../ui.apps/target</directory> <includes> <include>*.zip</include> </includes> </resource> </resources> </configuration> </execution> </executions> </plugin>
陷阱二:测试里context.resourceResolver().getResource("/content/title")返回null
- 原因:/content/title这个路径在aem-mock的虚拟JCR里并不存在,你只是加载了zip包,但zip包里的内容需要被“安装”。
- 对策:在setUp()方法里,先加载zip包,再用context.load().json(...)加载测试内容:
java @Before public void setUp() { context = new AemContext(); // 先加载 ui.apps 包,让组件定义可用 context.load().package("myproject.ui.apps"); // 再加载测试用的 JSON 内容,它会创建 /content/title 节点 context.load().json("/content/title-test.json", "/content/title"); }
陷阱三:@RunWith(MockitoJUnitRunner.class)和@RunWith(AemContextRunner.class)冲突
- 原因:aem-mock 4.x 推荐使用AemContextRunner,它内部集成了Mockito,不需要额外的MockitoJUnitRunner。
- 对策:删除@RunWith(MockitoJUnitRunner.class),改为:
java @RunWith(AemContextRunner.class) public class TitleModelIT { @Rule public final AemContext context = new AemContext(); // ... 测试方法 }
这样更简洁,且避免了Runner冲突。
实操心得:aem-mock的
AemContext对象是“有状态”的,它维护着一个虚拟的JCR和OSGi容器。我曾经在一个测试类里写了两个@Before方法,都初始化了context,结果第二个@Before覆盖了第一个,导致前面加载的Bundle丢失。教训是:一个测试类里,只有一个@Before方法负责初始化context,所有测试方法共享它。如果需要不同的初始状态,应该用@After清理,或者为每个测试方法创建新的AemContext实例(性能稍差,但更隔离)。
6. 从入门到进阶:这个骨架如何支撑你的AEM职业成长
这个工程骨架的价值,远不止于帮你跑通第一个Hello World。它是一张精心绘制的AEM开发能力成长地图,每一个模块、每一行配置,都对应着你在真实项目中必须掌握的核心技能。
第一阶段:理解AEM的“三位一体”架构(1-2周)
当你能熟练地在core里写一个OSGi服务、在ui.apps里写一个HTL模板、在it.tests里写一个集成测试,并理解它们如何通过resourceType和@Reference串联起来,你就完成了对AEM最基础的认知闭环。这时,你应该能清晰地向非技术人员解释:“AEM不是一个单一的CMS,它是由OSGi(负责Java逻辑的热插拔)、Sling(负责将HTTP请求路由到Java或HTL)、JCR(负责存储所有内容)这三个独立但又紧密协作的系统组成的。”这个认知,是摆脱“点鼠标式开发”的起点。
第二阶段:掌握企业级开发规范(2-4周)
骨架里隐藏着大量企业级最佳实践。比如,core/pom.xml里maven-bundle-plugin的<instructions>配置,强制要求所有导出的包都带上版本号(Export-Package: com.mycompany.myproject.core.services;version=${project.version}),这是为了防止不同版本Bundle之间的类冲突。再比如,ui.apps/pom.xml里vault-maven-plugin的<filter>配置,精确控制哪些JCR路径被打包,哪些被排除(如/apps/myproject/install),这是为了确保生产环境的安全隔离。当你开始关注这些细节,并能解释“为什么这里要用<scope>provided</scope>而不是<scope>compile</scope>”,你就已经具备了编写可维护、可扩展代码的能力。
第三阶段:构建CI/CD流水线(1-2个月)
这个骨架是CI/CD流水线的天然蓝本。mvn clean install -PautoInstallPackage命令,就是流水线里“构建与部署”阶段的核心。你可以轻松地将它集成到Jenkins或GitHub Actions中:
- mvn clean install -PautoInstallPackage:部署到开发环境;
- mvn verify -PintegrationTests:运行it.tests,作为质量门禁;
- mvn deploy -PpublishToRepo:将生成的content-package zip发布到Nexus私有仓库,供UAT和生产环境拉取。
当你能把这套流程自动化,并配置好不同环境的Profile(dev, uat, prod),你就从一个开发者,成长为一个DevOps实践者。
最后分享一个小技巧:
在实际项目中,我习惯在README.md里维护一个CHANGELOG.md,记录每一次mvn install后,AEM里发生了什么变化。比如:
v1.2.0 (2024-05-20)
- core: Added ProductService with caching layer.
- ui.apps: Updated title.html to support rich text via data-sly-unwrap.
- it.tests: Added IT for ProductService cache hit/miss.
这个习惯让我在项目交接、故障回溯、甚至向客户演示时,都能快速说出“这个功能是什么时候、怎么上线的”。它不增加开发负担,却极大地提升了项目的可追溯性和专业感。
这个骨架,就是你AEM开发之旅的第一双登山鞋。它可能不会让你一步登顶,但它保证每一步都踏在坚实的岩石上,而不是松软的浮雪里。
简介:一套开箱即用的Adobe Experience Manager开发基础工程模板,覆盖AEM 6.5及AEM as a Cloud Service主流版本。结构清晰划分为core(Java业务逻辑、OSGi服务注册与注入)、ui.apps(Sling组件、HTL模板、CSS/JS资源管理)、it.tests(基于JUnit和AEM Mock的集成测试用例)以及it.launcher(测试执行入口)。所有模块均采用标准Maven多模块组织,附带完整pom.xml依赖配置、.gitignore规则、Eclipse项目元数据(.project、.classpath、.settings、org.eclipse.m2e.core.prefs),支持IDE快速导入与本地构建。不含编译产物或运行时jar,纯源码级教学骨架,适合搭建本地AEM开发环境、理解Bundle生命周期、调试Sling模型绑定、实践HTL与后端Java交互、配置基础CI/CD流水线。配套README.md提供初始化步骤、常见命令与模块职责说明。

被折叠的 条评论
为什么被折叠?



