了解 Cloud Manager 在基于来自 AEM 工程的最佳实践的代码质量测试过程中执行的自定义代码质量规则的详细信息。
此处提供的代码示例仅用于说明目的。请参阅 SonarQube 的概念文档,了解其概念和质量规则。
由于是 Adobe 专有信息,因此无法下载完整的 SonarQube 规则。可使用此链接下载规则的完整列表。有关规则的描述和示例,请继续阅读本文档。
以下部分详细介绍了 Cloud Manager 执行的 SonarQube 规则。
Thread.stop()
和 Thread.interrupt()
方法可能会产生难以重现的问题,并且有时会产生安全漏洞。 应严格监控和验证其使用情况。 总的来说,传递信息是实现类似目标的一种更安全的方式。
public class DontDoThis implements Runnable {
private Thread thread;
public void start() {
thread = new Thread(this);
thread.start();
}
public void stop() {
thread.stop(); // UNSAFE!
}
public void run() {
while (true) {
somethingWhichTakesAWhileToDo();
}
}
}
public class DoThis implements Runnable {
private Thread thread;
private boolean keepGoing = true;
public void start() {
thread = new Thread(this);
thread.start();
}
public void stop() {
keepGoing = false;
}
public void run() {
while (this.keepGoing) {
somethingWhichTakesAWhileToDo();
}
}
}
使用来自外部源的格式字符串(例如,请求参数或用户生成的内容)可能会使应用程序遭受拒绝服务攻击。在某些情况下,虽然格式字符串可能会受到外部控制,但仅允许来自受信任源。
protected void doPost(SlingHttpServletRequest request, SlingHttpServletResponse response) {
String messageFormat = request.getParameter("messageFormat");
request.getResource().getValueMap().put("some property", String.format(messageFormat, "some text"));
response.sendStatus(HttpServletResponse.SC_OK);
}
从 AEM 应用程序内部执行 HTTP 请求时,请务必确保配置适当的超时以避免不必要的线程消耗。 不幸的是,Java™ 的默认 HTTP 客户端 (java.net.HttpUrlConnection
) 和常用的 Apache HTTP 组件客户端的默认行为都是永不超时,因此必须明确设置超时。作为最佳实践,这些超时不应超过 60 秒。
@Reference
private HttpClientBuilderFactory httpClientBuilderFactory;
public void dontDoThis() {
HttpClientBuilder builder = httpClientBuilderFactory.newBuilder();
HttpClient httpClient = builder.build();
// do something with the client
}
public void dontDoThisEither() {
URL url = new URL("http://www.google.com");
URLConnection urlConnection = url.openConnection();
BufferedReader in = new BufferedReader(new InputStreamReader(
urlConnection.getInputStream()));
String inputLine;
while ((inputLine = in.readLine()) != null) {
logger.info(inputLine);
}
in.close();
}
@Reference
private HttpClientBuilderFactory httpClientBuilderFactory;
public void doThis() {
HttpClientBuilder builder = httpClientBuilderFactory.newBuilder();
RequestConfig requestConfig = RequestConfig.custom()
.setConnectTimeout(5000)
.setSocketTimeout(5000)
.build();
builder.setDefaultRequestConfig(requestConfig);
HttpClient httpClient = builder.build();
// do something with the client
}
public void orDoThis() {
URL url = new URL("http://www.google.com");
URLConnection urlConnection = url.openConnection();
urlConnection.setConnectTimeout(5000);
urlConnection.setReadTimeout(5000);
BufferedReader in = new BufferedReader(new InputStreamReader(
urlConnection.getInputStream()));
String inputLine;
while ((inputLine = in.readLine()) != null) {
logger.info(inputLine);
}
in.close();
}
从 ResourceResolverFactory
获取的 ResourceResolver
对象占用系统资源。 虽然可以采取一些措施以在不再使用 ResourceResolver
时回收这些资源,但通过调用 close()
方法显式关闭任何打开的 ResourceResolver
对象会更有效。
一个常见的误解是,使用现有 JCR 会话创建的 ResourceResolver
对象不应明确关闭,否则会关闭底层 JCR 会话。情况并非如此。 无论 ResourceResolver
的打开方式如何,只要不再使用它,就应将它关闭。 由于 ResourceResolver
实施 Closeable
接口,也可以使用 try-with-resources
语法,而不是显式调用 close()
。
public void dontDoThis(Session session) throws Exception {
ResourceResolver resolver = factory.getResourceResolver(Collections.singletonMap("user.jcr.session", (Object)session));
// do some stuff with the resolver
}
public void doThis(Session session) throws Exception {
ResourceResolver resolver = null;
try {
resolver = factory.getResourceResolver(Collections.singletonMap("user.jcr.session", (Object)session));
// do something with the resolver
} finally {
if (resolver != null) {
resolver.close();
}
}
}
public void orDoThis(Session session) throws Exception {
try (ResourceResolver resolver = factory.getResourceResolver(Collections.singletonMap("user.jcr.session", (Object) session))){
// do something with the resolver
}
}
如 Sling 文档中所述,建议不要通过路径绑定 servlet。 路径绑定的 servlet 不能使用标准 JCR 访问控制,因此,需要额外的安全严密性。 建议在存储库中创建节点并按资源类型注册 servlet,而不是使用路径绑定的 servlet。
@Component(property = {
"sling.servlet.paths=/apps/myco/endpoint"
})
public class DontDoThis extends SlingAllMethodsServlet {
// implementation
}
通常,一个异常应只记录一次。 将一个异常记录多次可能会导致混淆,因为不清楚该异常发生了几次。 导致出现此情况的最常见模式是记录并引发捕获的异常。
public void dontDoThis() throws Exception {
try {
someOperation();
} catch (Exception e) {
logger.error("something went wrong", e);
throw e;
}
}
public void doThis() {
try {
someOperation();
} catch (Exception e) {
logger.error("something went wrong", e);
}
}
public void orDoThis() throws MyCustomException {
try {
someOperation();
} catch (Exception e) {
throw new MyCustomException(e);
}
}
另一个要避免的常见模式是记录一条消息,然后立即引发异常。 这通常表明异常消息最终会在日志文件中重复。
public void dontDoThis() throws Exception {
logger.error("something went wrong");
throw new RuntimeException("something went wrong");
}
public void doThis() throws Exception {
throw new RuntimeException("something went wrong");
}
通常,应使用 INFO 日志级别来划分重要操作,默认情况下,AEM 配置为在 INFO 级别或更高级别进行记录。 GET 和 HEAD 方法只能为只读操作,因此,不会构成重要操作。 在 INFO 级别进行记录来响应 GET 或 HEAD 请求可能会产生大量日志噪音,导致更难以识别日志文件中的有用信息。 处理 GET 或 HEAD 请求时,应在 WARN 或 ERROR 级别进行记录(如果出现问题),或在 DEBUG 或 TRACE 级别进行记录(如果更深入的故障排除信息会很有用)。
这不适用于每个请求的 access.log-type 记录。
public void doGet() throws Exception {
logger.info("handling a request from the user");
}
public void doGet() throws Exception {
logger.debug("handling a request from the user.");
}
作为最佳实践,日志消息应提供有关应用程序中发生异常的位置的上下文信息。 虽然也可以使用堆栈跟踪来确定上下文,但通常日志消息将更易于阅读和理解。 因此,在记录异常时,将异常消息用作日志消息是一种不好的做法。 异常消息将包含所发生的问题,而日志消息应让日志读者知道,当发生异常时,应用程序正在做什么。 仍会记录异常消息。 通过指定您自己的消息,可使日志更易于理解。
public void dontDoThis() {
try {
someMethodThrowingAnException();
} catch (Exception e) {
logger.error(e.getMessage(), e);
}
}
public void doThis() {
try {
someMethodThrowingAnException();
} catch (Exception e) {
logger.error("Unable to do something", e);
}
}
顾名思义,Java™ 异常应始终在异常情况下使用。 因此,当捕获到异常时,请务必确保在适当的级别记录日志消息:WARN 或 ERROR。这将确保这些消息正确显示在日志中。
public void dontDoThis() {
try {
someMethodThrowingAnException();
} catch (Exception e) {
logger.debug(e.getMessage(), e);
}
}
public void doThis() {
try {
someMethodThrowingAnException();
} catch (Exception e) {
logger.error("Unable to do something", e);
}
}
上下文对于理解日志消息是至关重要的。使用 Exception.printStackTrace()
会导致仅将堆栈跟踪输出到标准错误流,从而丢失所有上下文。此外,在像 AEM 这样的多线程应用程序中,如果使用此方法并行打印多个异常,则其堆栈跟踪可能会发生重叠,从而导致产生严重混淆。应仅通过记录框架来记录异常。
public void dontDoThis() {
try {
someMethodThrowingAnException();
} catch (Exception e) {
e.printStackTrace();
}
}
public void doThis() {
try {
someMethodThrowingAnException();
} catch (Exception e) {
logger.error("Unable to do something", e);
}
}
AEM 中的记录应始终通过记录框架 SLF4J 完成。直接输出到标准输出或标准错误流将丢失记录框架提供的结构和上下文信息,并且有时可能会导致出现性能问题。
public void dontDoThis() {
try {
someMethodThrowingAnException();
} catch (Exception e) {
System.err.println("Unable to do something");
}
}
public void doThis() {
try {
someMethodThrowingAnException();
} catch (Exception e) {
logger.error("Unable to do something", e);
}
}
通常,以 /libs
和 /apps
开头的路径不应进行硬编码,因为它们引用的路径最常存储为 Sling 搜索路径(默认情况下,设置为 /libs,/apps
)的相对路径。使用绝对路径可能会引入小缺陷,这些缺陷仅在项目生命周期的后期出现。
public boolean dontDoThis(Resource resource) {
return resource.isResourceType("/libs/foundation/components/text");
}
public void doThis(Resource resource) {
return resource.isResourceType("foundation/components/text");
}
Sling 调度程序不得用于需要保证执行的任务。 Sling 计划作业可保证执行,并且更适合集群和非集群环境。
请参阅 Apache Sling 事件和作业处理文档,详细了解如何在集群环境中处理 Sling 作业。
AEM API 接口正在不断修订,以识别建议不要使用的 API,从而将其弃用。
通常情况下,使用标准 Java™ @Deprecated 注释来弃用这些 API,如 squid:CallToDeprecatedMethod
所标识。
但在某些情况下,API 会在 AEM 上下文中被弃用,但在其他上下文中可能不会被弃用。此规则标识第二个类。
以下部分详细介绍了 Cloud Manager 执行的 OakPAL 检查。
OakPAL 是一个框架,它使用独立的 Oak 存储库来验证内容包。 它由一个 AEM 合作伙伴开发,并获得了 2019 年的 AEM Rockstar 北美区大奖。
AEM API 包含 Java™ 接口和类,这些接口和类仅用于由自定义代码使用,但不能实现。例如,接口 com.day.cq.wcm.api.Page
仅由 AEM 实现。
当将新方法添加到这些接口时,这些附加方法不会影响使用这些接口的现有代码,因此,向这些接口添加新方法被认为是向后兼容的。 但是,如果自定义代 码实现了 其中一个接口,则该自定义代码会给客户带来向后兼容性风险。
仅由 AEM 实现的接口和类通过 org.osgi.annotation.versioning.ProviderType
进行注释,有时通过类似的旧批注 aQute.bnd.annotation.ProviderType
进行注释。此规则标识实现此类接口或自定义代码扩展类的情况。
import com.day.cq.wcm.api.Page;
public class DontDoThis implements Page {
// implementation here
}
客户应将 AEM 内容存储库中的 /libs
内容树视为只读,这是一个长期存在的最佳实践。修改 /libs
下的节点和属性会给主要和次要更新带来重大风险。 对 /libs
的修改只能由 Adobe 通过正式渠道进行。
复杂项目中出现的一个常见问题是,将同一个 OSGi 组件配置多次。 这会导致无法明确哪种配置将是可操作的。此规则是“运行模式感知型”的,因为它只会识别在同一运行模式或运行模式组合中多次配置同一组件的问题。
+ apps
+ projectA
+ config
+ com.day.cq.commons.impl.ExternalizerImpl
+ projectB
+ config
+ com.day.cq.commons.impl.ExternalizerImpl
+ apps
+ shared-config
+ config
+ com.day.cq.commons.impl.ExternalizerImpl
出于安全原因,包含 /config/
和 /install/
的路径只能由 AEM 中的管理用户读取,并且只能用于 OSGi 配置和 OSGi 捆绑包。 如果将其他类型的内容放在包含这些区段的路径下,则会导致应用程序行为在管理用户和非管理用户之间无意中发生变化。
一个常见问题是,在组件对话框中或在为内联编辑指定富文本编辑器配置时使用名为 config
的节点。 要解决此问题,应将违规节点重命名为合规的名称。对于富文本编辑器配置,使用 cq:inplaceEditing
节点上的 configPath
属性指定新位置。
+ cq:editConfig [cq:EditConfig]
+ cq:inplaceEditing [cq:InplaceEditConfig]
+ config [nt:unstructured]
+ rtePlugins [nt:unstructured]
+ cq:editConfig [cq:EditConfig]
+ cq:inplaceEditing [cq:InplaceEditConfig]
./configPath = inplaceEditingConfig (String)
+ inplaceEditingConfig [nt:unstructured]
+ rtePlugins [nt:unstructured]
与“包不应包含重复的 OSGi 配置”规则类似,这是复杂项目中的一个常见问题,其中同一节点路径被多个单独的内容包写入。虽然可使用内容包依赖项来确保获得一致的结果,但最好是完全避免重叠。
OSGi 配置 com.day.cq.wcm.core.impl.AuthoringUIModeServiceImpl
定义 AEM 中的默认创作模式。 由于经典 UI 自 AEM 6.4 之后已被弃用,因此,在将默认创作模式配置为经典 UI 时,现在将引发问题。
具有经典 UI 对话框的 AEM 组件应始终具有对应的 Touch UI 对话框,以提供最佳创作体验并与不支持经典 UI 的 Cloud Service 部署模型兼容。此规则验证以下场景:
dialog
子节点)的组件必须具有相应的 Touch UI 对话框(即 cq:dialog
子节点)。design_dialog
节点)的组件必须具有相应的 Touch UI 设计对话框(即 cq:design_dialog
子节点)。“AEM 现代化工具”文档提供了有关如何将组件从经典 UI 转换为 Touch UI 的详细信息以及使用的工具。有关更多详细信息,请参阅“AEM 现代化工具”文档。
要与 Cloud Service 部署模型兼容,各个内容包必须包含存储库的不可变区域(即 /apps
和 /libs
)的内容或可变区域的内容(即未在 /apps
或 /libs
中的内容),但不能同时包含这两个区域的内容。 例如,同时包含 /apps/myco/components/text and /etc/clientlibs/myco
的包不与 Cloud Service 兼容,并且将导致报告问题。
有关更多详细信息,请参阅“AEM 项目结构”文档。
客户包不应在 /libs 下创建或修改节点规则始终适用。
Cloud Service 部署中不支持反向复制,如发行说明:移除复制代理中所述。
使用反向复制的客户应联系 Adobe 以获取替代解决方案。
AEM 客户端库可能包含静态资源,如图像和字体。如“使用客户端库”文档中所述,在使用代理的客户端库时,这些静态资源必须包含在名为 resources
的子文件夹中,才能在发布实例上有效引用这些资源。
+ apps
+ projectA
+ clientlib
- allowProxy=true
+ images
+ myimage.jpg
+ apps
+ projectA
+ clientlib
- allowProxy=true
+ resources
+ myimage.jpg
迁移到 Asset 微服务以便在 AEM Cloud Service 上进行资产处理后,在 AEM 的内部部署和 AMS 版本中使用的多个工作流已变得不受支持或不必要。
AEM Assets as a Cloud Service GitHub 存储库中的迁移工具可在迁移到 AEM as a Cloud Service 的过程中用于更新工作流模型。
虽然静态模板的使用历来在 AEM 项目中很普遍,但强烈建议使用可编辑的模板,因为它们提供了最大的灵活性,并支持静态模板中不存在的附加功能。要获取更多信息,请参阅“页面模板 - 可编辑”文档。
可以使用 AEM 现代化工具在很大程度上实现从静态模板到可编辑模板的迁移的自动化。
旧的基础组件(即 /libs/foundation
下的组件)已在多个 AEM 版本中被弃用以便支持核心组件。建议不要使用旧的基础组件作为自定义组件的基础(无论是通过叠加还是继承),并且应将这些基础组件转换为对应的核心组件。
可以通过 AEM 现代化工具促进此转换。
AEM Cloud Service 对运行模式名称实施严格的命名策略,并对这些运行模式进行严格的排序。可以在“部署到 AEM as a Cloud Service”文档中找到受支持的运行模式列表,任何偏离该列表的情况都将被视为存在问题。
AEM Cloud Service 要求自定义搜索索引定义(即 oak:QueryIndexDefinition
类型的节点)是 /oak:index
的直接子节点。必须移动其他位置的索引才能与 AEM Cloud Service 兼容。有关搜索索引的更多信息,请参阅“内容搜索和索引编制”文档。
AEM Cloud Service 要求自定义搜索索引定义(即 oak:QueryIndexDefinition
类型的节点)必须将 compatVersion
属性设置为 2
。AEM Cloud Service 不支持任何其他值。有关搜索索引的更多信息,请参阅“内容搜索和索引编制”文档。
当自定义搜索索引定义节点具有无序子节点时,可能会出现难以解决的问题。为了避免出现此情况,建议 oak:QueryIndexDefinition
节点的所有后代节点的类型为 nt:unstructured
。
正确定义的自定义搜索索引定义节点必须包含一个名为 indexRules
的子节点,而该子节点必须具有至少一个子节点。有关更多信息,请参阅 Oak 文档。
AEM Cloud Service 要求自定义搜索索引定义(即 oak:QueryIndexDefinition
类型的节点)必须按照内容搜索和索引编制中所述的特定模式进行命名。
AEM Cloud Service 要求自定义搜索索引定义(即 oak:QueryIndexDefinition
类型的节点)必须将 type
属性的值设置为 lucene
。在迁移到 AEM Cloud Service 之前,必须更新使用旧索引类型的索引编制。有关更多信息,请参阅“内容搜索和索引编制”文档。
AEM Cloud Service 禁止自定义搜索索引定义(即 oak:QueryIndexDefinition
类型的节点)包含名为 seed
的属性。在迁移到 AEM Cloud Service 之前,必须更新使用此属性的索引编制。有关更多信息,请参阅“内容搜索和索引编制”文档。
AEM Cloud Service 禁止自定义搜索索引定义(即 oak:QueryIndexDefinition
类型的节点)包含名为 reindex
的属性。在迁移到 AEM Cloud Service 之前,必须更新使用此属性的索引编制。有关更多信息,请参阅“内容搜索和索引编制”文档。
以下部分列出了 Cloud Manager 执行的 Dispatcher 优化工具 (DOT) 检查。访问每个检查的链接以查看其 GitHub 定义和详细信息。