“它可以在我的本地机器上运行!” 如今,这听起来像模因,但仍然存在“开发环境与生产环境”的问题。 作为开发人员,您应始终牢记,您的应用程序有一天将在生产环境中开始运行。 在本文中,我们将讨论一些特定于CUBA的事情,这些事情将帮助您避免在应用程序投入生产时出现问题。
编码准则
优先服务
几乎每个CUBA应用程序都实现一些业务逻辑算法。 此处的最佳实践是在CUBA Services中实现所有业务逻辑。 所有其他类:屏幕控制器,应用程序侦听器等应将业务逻辑执行委托给服务。 此方法具有以下优点:
- 一处只有一个业务逻辑实现
- 您可以从不同位置调用此业务逻辑,并将其公开为REST服务。
请记住,业务逻辑包括条件,循环等。这意味着理想情况下,服务调用应该是单行的。 例如,假设我们在屏幕控制器中具有以下代码:
Item item = itemService.findItem(itemDate);
if (item.isOld()) {itemService.doPlanA(item);
} else {itemService.doPlanB(item);
}
如果您看到这样的代码,请考虑将其从屏幕控制器移至itemService
作为单独的方法processOldItem(Date date)
因为它看起来像是应用程序业务逻辑的一部分。
由于屏幕和API可以由不同的团队开发,因此将业务逻辑放在一个地方可以帮助您避免生产中应用程序行为的不一致。
无国籍
开发Web应用程序时,请记住它将被多个用户使用。 在代码中,这意味着某些代码可以由多个线程同时执行。 几乎所有应用程序组件:服务,Bean以及事件侦听器都受多线程执行的影响。 此处的最佳做法是使组件保持无状态。 这意味着您不应引入共享的可变类成员。 使用局部变量并将特定于会话的信息保留在应用程序存储中,用户之间不共享这些信息。 例如,您可以在用户会话中保留少量可序列化的数据。
如果需要共享一些数据,请使用数据库或专用的共享内存存储(例如Redis)。
使用记录
有时生产中会出问题。 而且,当发生这种情况时,很难弄清到底是什么导致了故障,您无法调试部署到生产的应用程序。 为了简化您自己的工作,开发人员和支持团队的同伴并帮助您理解问题并能够重现此问题,请始终将日志记录添加到应用程序中。
此外,日志记录还充当被动监视角色。 应用程序重新启动,更新或重新配置后,管理员通常会查看日志以确保一切都已成功启动。
日志记录可能有助于解决可能不是在您的应用程序中发生的问题,而是在与应用程序集成的服务中发生的问题的解决方法。 例如,要弄清楚为什么付款网关拒绝某些交易,您可能需要记录所有数据,然后在与支持团队进行对话时使用它们。
CUBA使用了经过验证的slf4j库软件包作为外观和注销实现。 您只需要向类代码注入日志记录工具,就可以了。
@Inject
private Logger log;
然后只需在您的代码中调用此服务:
log.info("Transaction for the customer {} has succeeded at {}", customer, transaction.getDate());
请记住,日志消息应该有意义并且包含足够的信息以了解应用程序中发生了什么。 在系列文章“干净的代码,干净的日志”中,您可以找到更多关于Java应用程序的日志记录技巧。 另外,我们建议您参阅“ 9个记录的罪过”一文 。
另外,在CUBA中,我们有性能统计日志,因此您始终可以查看应用程序如何消耗服务器资源。 当客户支持开始收到用户对应用程序运行缓慢的投诉时,这将非常有帮助。 通过此登录,您可以更快地找到瓶颈。
处理异常
异常非常重要,因为异常会在您的应用程序出现问题时提供有价值的信息。 因此,第一条规则-永远不要忽略例外。 使用log.error()
方法,创建有意义的消息,添加上下文和堆栈跟踪。 该消息将是您用来标识发生了什么的唯一信息。
如果您有代码约定,请在其中添加错误处理规则部分。
让我们考虑一个示例–将用户的个人资料图片上传到应用程序。 此个人资料图片将保存到CUBA的文件存储和文件上传API服务中。
这是您不得处理异常的方式:
try {fileUploadingAPI.putFileIntoStorage(uploadField.getFileId(), fd); } catch (Exception e) {}
如果发生错误,则没人会知道,当用户看不到个人资料照片时,他们会感到惊讶。
这好一些,但远非理想。
try {fileUploadingAPI.putFileIntoStorage(uploadField.getFileId(), fd); } catch (FileStorageException e) {log.error (e.getMessage)}
日志中将出现错误消息,我们将仅捕获特定的异常类。 但是将没有有关上下文的信息:文件的名称是谁,谁曾尝试上传它。 而且,将没有堆栈跟踪,因此很难找到异常发生的位置。 还有一件事–用户不会收到有关该问题的通知。
这可能是一个好方法。
try {fileUploadingAPI.putFileIntoStorage(uploadField.getFileId(), fd); } catch (FileStorageException e) {throw new RuntimeException("Error saving file to FileStorage", e);}
我们知道错误,不要丢失原始异常,添加一条有意义的消息。 调用方法将收到有关异常的通知。 我们可以在消息中添加当前用户名以及可能的文件名,以添加更多上下文数据。 这是CUBA Web模块的示例。
在CUBA应用程序中,由于其分布式特性,您可能对核心模块和Web模块具有不同的异常处理规则。 文档中有一个关于异常处理的特殊部分。 实施该政策之前,请先阅读它。
特定于环境的配置
开发应用程序时,请尝试隔离应用程序代码中特定于环境的部分,然后使用功能切换和配置文件根据环境切换这些部分。
使用适当的服务实施
CUBA中的任何服务都由两部分组成:接口(服务API)及其实现。 有时,实现可能取决于部署环境。 例如,我们将使用文件存储服务。
在CUBA中,您可以使用文件存储来保存已发送到应用程序的文件,然后在服务中使用它们。 默认实现使用服务器上的本地文件系统保留文件。
但是,当您将应用程序部署到生产服务器时,此实现可能不适用于云环境或集群部署配置 。
为了启用特定于环境的服务实现,CUBA支持运行时配置文件 ,该配置文件允许您根据启动参数或环境变量来使用特定服务。
对于这种情况,如果我们决定在生产中使用文件存储的Amazon S3实现,则可以通过以下方式指定bean:
<beans profile="prod"><bean name="cuba_FileStorage" class="com.haulmont.addon.cubaaws.s3.AmazonS3FileStorage"/>
</beans>
设置该属性后,将自动启用S3实现:
spring.profiles.active=prod
因此,在开发CUBA应用程序时,请尝试识别特定于环境的服务,并为每种环境启用正确的实现。 尽量不要编写看起来像这样的代码:
If (“prod”.equals(getEnvironment())) {executeMethodA();
} else {executeMethodB();
}
尝试实现一个单独的服务myService
,该服务具有一个方法executeMethod()
和两个实现,然后使用配置文件对其进行配置。 之后,您的代码将如下所示:
myService.executeMethod();
更清洁,更简单,更易于维护。
外部化设置
如果可能,将应用程序设置提取到属性文件中。 如果参数将来可以更改(即使概率很小),请始终对其进行外部化。 避免将连接URL,主机名等作为纯字符串存储在应用程序的代码中,切勿将其复制粘贴。 在代码中更改硬编码值的成本要高得多。 邮件服务器地址,用户的照片缩略图大小,没有网络连接时的重试次数–所有这些都是您需要外部化的属性的示例。 使用[配置接口] https://doc.cuba-platform.com/manual-latest/config_interface_usage.html )并将它们注入您的类中以获取配置值。
利用运行时配置文件将特定于环境的属性保存在单独的文件中。
例如,您在应用程序中使用支付网关。 当然,您不应在开发过程中花费大量金钱来测试功能。 因此,您在本地环境中有一个网关存根,在网关端有一个用于生产前测试环境的测试API,一个为产品提供了真实的网关。 显然,这些环境的网关地址不同。
不要这样写代码:
If (“prod”.equals(getEnvironment())) {gatewayHost = “gateway.payments.com”;
} else if (“test”.equals(getEnvironment())) {gatewayHost = “testgw.payments.com”;
} else {gatewayHost = “localhost”;
}
connectToPaymentsGateway(gatewayHost);
而是定义三个属性文件: dev-app.properties
, test-app.properties
和prod-app.properties
并在其中定义database.host.name
属性的三个不同值。
之后,定义一个配置接口:
@Source(type = SourceType.DATABASE)
public interface PaymentGwConfig extends Config {@Property("payment.gateway.host.name")String getPaymentGwHost();
}
然后注入接口并在您的代码中使用它:
@Inject
PaymentGwConfig gwConfig;//service codeconnectToPaymentsGateway(gwConfig.getPaymentGwHost());
该代码更简单,并且不依赖于环境,所有设置都在属性文件中,如果更改了某些内容,则不应在代码中搜索它们。
添加网络超时处理
始终认为通过网络进行的服务调用不可靠。 当前用于Web服务调用的大多数库都基于同步阻塞通信模型。 这意味着,如果您从主执行线程调用Web服务,则应用程序将暂停直到收到响应。
即使您在单独的线程中执行Web服务调用,该线程也有可能由于网络超时而永远无法恢复执行。
超时有两种类型:
- 连接超时
- 读取超时
在应用程序中,这些超时类型应分开处理。 让我们使用与上一章相同的示例-付款网关。 对于这种情况,读取超时可能明显长于连接超时。 银行交易可以处理很长的时间,数十秒,最多几分钟。 但是连接应该很快,因此,值得将连接超时设置为例如10秒。
超时值是要移至属性文件的良好候选者。 并始终为通过网络交互的所有服务设置它们。 以下是服务bean定义的示例:
<bean id="paymentGwConfig" class="com.global.api.serviceConfigs.GatewayConfig"><property name="connectionTimeout" value="${xxx.connectionTimeoutMillis}"/><property name="readTimeout" value="${xxx.readTimeoutMillis}"/>
</bean>
在您的代码中,您应该包括一个特殊部分来处理超时。
数据库准则
数据库是几乎所有应用程序的核心。 在生产部署和更新方面,不破坏数据库非常重要。 除此之外,开发人员工作站上的数据库工作负载显然与生产服务器不同。 这就是为什么您可能想要实施以下描述的一些做法。
生成特定于环境的脚本
在CUBA中,我们生成用于创建和更新应用程序数据库的SQL脚本。 在生产服务器上首次创建数据库之后,一旦模型更改,CUBA框架就会生成更新脚本。
关于生产中的数据库更新,有一个特殊的部分 ,请在首次生产之前阅读它。
最终建议:始终在更新之前执行数据库备份。 如果出现任何问题,这将节省大量时间和精力。
考虑多租户
如果您的项目将成为多租户应用程序 ,请在项目开始时将其考虑在内。
CUBA通过该插件支持多租户,它对应用程序的数据模型和数据库的查询逻辑进行了一些更改。 例如,一个单独的列tenantId
被添加到所有特定于Tenant的实体。 因此,所有查询都隐式修改为使用此列。 这意味着在编写本机SQL查询时应考虑此列。
请注意,由于上面提到的特定功能,向在生产环境中运行的应用程序添加多租户功能可能很棘手。 为了简化迁移,请将所有自定义查询保留在同一应用程序层中,最好在服务中或在单独的数据访问层中。
安全注意事项
对于可以被多个用户访问的应用程序,安全性起着重要的作用。 为了避免数据泄漏,未经授权的访问等,您需要认真考虑安全性。 您可以在下面找到一些原则,这些原则将帮助您改善安全性。
安全编码
安全性始于防止问题的代码。 您可以在此处找到有关Oracle提供的安全编码的很好的参考。 在下面,您可以从本指南中找到一些(也许很明显)建议。
准则3-2 / INJECT-2:避免使用动态SQL
众所周知,动态创建的SQL语句(包括不受信任的输入)会受到命令注入的影响。 在CUBA中,您可能需要执行JPQL语句,因此,也请避免使用动态JPQL。 如果需要添加参数,请使用适当的类和语句语法:
try (Transaction tx = persistence.createTransaction()) {// get EntityManager for the current transactionEntityManager em = persistence.getEntityManager();// create and execute QueryQuery query = em.createQuery("select sum(o.amount) from sample_Order o where o.customer.id = :customerId");query.setParameter("customerId", customerId);result = (BigDecimal) query.getFirstResult();// commit transactiontx.commit();}
准则5-1 / INPUT-1:验证输入
在使用之前,必须验证来自不受信任来源的输入。 精心设计的输入可能会导致问题,无论是通过方法参数还是外部流。 其中一些示例是整数值溢出和通过在文件名中包含“ ../”序列的目录遍历攻击。 在CUBA中,除了签入代码外,您还可以在GUI中使用验证器 。
以上只是安全编码原理的几个示例。 请仔细阅读该指南,它将以多种方式帮助您改进代码。
保护个人资料的安全
由于法律要求,某些个人信息应受到保护。 在欧洲,我们有GDPR ,在美国的医疗应用中,有HIPAA要求等。因此,在实施您的应用时要考虑到这一点。
CUBA允许您设置各种权限,并使用角色和访问组限制对数据的访问。 在后者中,您可以定义各种约束条件 ,以防止未经授权访问个人数据。
但是提供访问权限只是确保个人数据安全的一部分。 数据保护标准和行业特定要求中有很多要求。 在规划应用程序的体系结构和数据模型之前,请先查看这些文档。
更改或禁用默认用户和角色
使用CUBA框架创建应用程序时,系统中将创建两个用户: admin
和anonymous
。 始终在生产环境中更改其默认密码,然后用户才能使用该应用程序。 您可以手动执行此操作,也可以将SQL语句添加到30-....sql
初始化脚本中。
使用CUBA文档中的建议,这些建议将帮助您正确配置生产中的角色。
如果您具有复杂的组织结构,请考虑为每个分支机构创建本地管理员 ,而不是在组织级别上创建多个“超级管理员”用户。
将角色导出到生产
在第一次部署之前,通常需要将角色和访问组从开发(或登台)服务器复制到生产服务器。 在CUBA中,您可以使用内置的管理UI来执行此操作,而不必手动执行。
要导出角色和特权,您可以使用Administration -> Roles
屏幕。 下载文件后,您可以将其上传到应用程序的生产版本。
对于访问组,有一个类似的过程,但是您需要使用Administration -> Access Groups
屏幕。
配置应用
生产环境通常与开发环境以及应用程序配置不同。 这意味着您需要执行一些其他检查,以确保您的应用程序在生产时能够平稳运行。
配置日志
确保已针对生产环境正确配置了日志记录子系统:日志级别已设置为所需级别(通常为INFO),并且在应用程序重新启动时不会删除日志。 您可以参考文档以获取正确的日志设置和有用的记录器参考。
如果使用Docker,请使用Docker卷将日志文件存储在容器外部。
为了进行正确的日志记录分析,您可以部署特殊的工具来收集,存储和分析日志。 例如ELK stack和Graylog 。 建议将日志记录软件安装到单独的服务器上,以避免对应用程序造成性能影响。
在群集配置中运行
可以将CUBA应用程序配置为在群集配置中运行。 如果决定使用此功能,则需要注意您的应用程序体系结构,否则,您可能会从应用程序中得到意外的行为。 我们希望引起您对专门针对集群环境进行调整的最常用功能的注意:
任务调度
如果要在应用程序中执行预定任务(例如每日报告生成或每周电子邮件发送),则可以使用相应的框架内置功能ъ( https://doc.cuba-platform.com/manual-latest /scheduled_tasks.html )。 但是,请想象自己是一位获得了三封相同营销电子邮件的客户。 你快乐吗? 如果您的任务在三个群集节点上执行,则可能会发生这种情况。 为避免这种情况,最好使用CUBA任务计划程序 ,该程序使您可以创建单例任务。
分布式缓存
缓存是可以提高应用程序性能的东西。 有时开发人员尝试缓存几乎所有内容,因为现在内存非常便宜。 但是,当您的应用程序部署在多台服务器上时,缓存将在服务器之间分配,并且应该同步。 同步过程发生在相对较慢的网络连接上,这可能会增加响应时间。 这里的建议–在决定添加更多缓存之前(尤其是在集群环境中),执行负载测试并衡量性能。
结论
CUBA平台简化了开发,您可能会完成开发并开始考虑比预期更早的投入生产。 但是,无论是否使用CUBA,部署都不是一件容易的事。 而且,如果您开始考虑在开发的早期阶段就进行部署并遵循本文所述的简单规则,那么您的生产方式很可能会很顺利,所需的工作量很小,并且不会遇到严重的问题。
翻译自: https://www.javacodegeeks.com/2020/03/cuba-getting-ready-for-production.html