使用Spring平台时最常见的10个错误。 第一部分

大家好 今天,我们分享了文章的第一部分,该文章的翻译是专门为“ Spring Framework开发人员 ”课程的学生准备的。 让我们开始吧!





Spring可能是最受欢迎的Java开发平台之一。 这是一个功能强大但很难学习的工具。 它的基本概念相当容易理解和学习,但是要成为一名经验丰富的Spring开发人员需要花费时间和精力。

在本文中,我们将介绍在Spring中工作时犯的一些最常见的错误,尤其是与Web应用程序的开发和Spring Boot平台的使用有关的错误。 如Spring Boot网站所述 ,Spring Boot采用标准化的方法来创建现成的应用程序,本文将沿用这种方法。 它将提供许多建议,这些建议可以有效地用于基于Spring Boot的标准Web应用程序的开发中。

如果您对Spring Boot平台不是很熟悉,但想尝试本文中的示例,那么我创建了一个GitHub存储库,其中包含本文的其他材料 。 如果您在阅读本文时有些困惑,我建议您创建此存储库的副本并在计算机上尝试代码。

常见错误#1:编程太底层


我们很容易屈服于这个普遍存在的错误,因为在编程环境中, “拒绝别人的发展综合征”是非常典型的。 其症状之一是经常重写一些常用的代码,这是许多程序员都看到的一种症状。
研究特定库的内部结构和实现功能通常是一个有用,必要和有趣的过程,但是如果您在执行功能的低级实现时不断编写相同类型的代码,那么对于软件开发人员来说可能对您有害。 这就是为什么存在和使用诸如Spring之类的抽象和平台的原因-它们使您免于重复的手工工作,并让您可以在更高层次上专注于主题领域的对象和程序代码逻辑。

因此,不应避免抽象。 下次遇到需要解决特定问题的需求时,请先进行快速搜索,并找出解决问题的库是否已内置在Spring中-您可能会找到合适的交钥匙解决方案。 这样的一个有用的库就是Project Lombok ,我将在本文的示例中使用这些注释。 Lombok被用作模板代码生成器,因此生活在我们每个人中的懒惰开发人员将非常高兴结识该库。 例如,请参阅标准JavaBean组件在Lombok中的外观:

@Getter @Setter @NoArgsConstructor public class Bean implements Serializable { int firstBeanProperty; String secondBeanProperty; } 

您可能已经理解,以上代码将转换为以下形式:

 public class Bean implements Serializable { private int firstBeanProperty; private String secondBeanProperty; public int getFirstBeanProperty() { return this.firstBeanProperty; } public String getSecondBeanProperty() { return this.secondBeanProperty; } public void setFirstBeanProperty(int firstBeanProperty) { this.firstBeanProperty = firstBeanProperty; } public void setSecondBeanProperty(String secondBeanProperty) { this.secondBeanProperty = secondBeanProperty; } public Bean() { } } 

请注意,如果要在集成开发环境中使用Lombok,则很可能必须安装适当的插件。 可以在此处找到用于IntelliJ IDEA的此插件的版本。

常见错误2。内部结构“泄漏”


允许访问内部结构从来都不是一个好主意,因为它会削弱服务模型的灵活性,并因此导致不良的编程风格的形成。 内部结构的“泄漏”表现在以下事实:从某些API端点可以访问数据库结构。 例如,假设下面的“良好的旧Java对象”(POJO)代表数据库中的一个表:

 @Entity @NoArgsConstructor @Getter public class TopTalentEntity { @Id @GeneratedValue private Integer id; @Column private String name; public TopTalentEntity(String name) { this.name = name; } } 

假设有一个端点需要访问TopTalentEntity对象的数据。 返回TopTalentEntity实例似乎是一个诱人的主意,但更灵活的解决方案是创建一个新类来表示API端点的TopTalentEntity数据:

 @AllArgsConstructor @NoArgsConstructor @Getter public class TopTalentData { private String name; } 

因此,对数据库的内部组件进行更改将不需要在服务级别进行其他更改。 让我们看看如果将密码字段添加到TopTalentEntity类以将用户密码哈希存储在数据库中会发生什么:如果没有诸如TopTalentData连接器,并且开发人员忘记更改服务的接口部分,这会导致非常不希望的秘密信息泄露!

常见错误编号3。合并一些功能,这些功能最好在代码中进行分配


随着应用代码的增长来组织它变得越来越重要。 奇怪的是,当达到一定程度的开发规模时,尤其是在尚未深思熟虑的应用程序体系结构的情况下,大多数有效编程的原理都将停止工作。 最常见的错误之一是代码中更合理地单独实现的功能组合。

违反职责分离原则的原因通常是在现有类中添加新功能。 也许这是一个很好的瞬时解决方案(特别是,它需要编写少量的代码),但是在将来,它将不可避免地成为一个问题,包括在测试和维护代码以及它们之间的阶段。 考虑以下从存储库返回TopTalentData控制器:

 @RestController public class TopTalentController { private final TopTalentRepository topTalentRepository; @RequestMapping("/toptal/get") public List<TopTalentData> getTopTalent() { return topTalentRepository.findAll() .stream() .map(this::entityToData) .collect(Collectors.toList()); } private TopTalentData entityToData(TopTalentEntity topTalentEntity) { return new TopTalentData(topTalentEntity.getName()); } } 

乍一看,此代码片段似乎没有明显的错误。 它提供了一个TopTalentData对象的列表,该列表取自TopTalentEntity类的实例。 但是,如果您仔细看一下代码,我们会发现实际上TopTalentController在这里执行了几个操作,即,它将请求与特定端点相关联,从存储库中检索数据,并将从TopTalentRepository接收到的对象转换为TopTalentRepository一种格式。 较干净的解决方案应将这些功能分为不同的类。 例如,它可能看起来像这样:

 @RestController @RequestMapping("/toptal") @AllArgsConstructor public class TopTalentController { private final TopTalentService topTalentService; @RequestMapping("/get") public List<TopTalentData> getTopTalent() { return topTalentService.getTopTalent(); } } @AllArgsConstructor @Service public class TopTalentService { private final TopTalentRepository topTalentRepository; private final TopTalentEntityConverter topTalentEntityConverter; public List<TopTalentData> getTopTalent() { return topTalentRepository.findAll() .stream() .map(topTalentEntityConverter::toResponse) .collect(Collectors.toList()); } } @Component public class TopTalentEntityConverter { public TopTalentData toResponse(TopTalentEntity topTalentEntity) { return new TopTalentData(topTalentEntity.getName()); } } 

这种层次结构的另一个优点是,它使我们可以通过简单地查看类名来了解函数的位置。 随后,在测试期间,如果需要,我们可以轻松地用代理代码替换任何此类的代码。

常见错误编号4。代码统一且错误处理不善


代码统一性的主题不是Spring(或Java)所独有的,但是,它是在Spring中处理项目时必须考虑的重要方面。 选择特定的编程样式可能是讨论的主题(通常在开发团队中或整个公司内是一致的),但是在任何情况下,存在用于编写代码的通用标准都有助于提高工作效率。 如果有几个人在编写代码,则尤其如此。 可以在开发人员之间传递统一的代码,而无需花费很多精力来维护它,也不必花很多时间解释为什么需要这些类。

想象有一个Spring项目,其中有各种配置文件,服务和控制器。 在命名它们时遵循语义统一性,我们创建了一个结构,您可以通过该结构轻松地进行搜索,并且任何开发人员都可以在其中轻松地理解代码。 例如,您可以将Config后缀添加到配置类的名称中,将Service后缀添加到服务名称中,并将Controller后缀添加到控制器名称中。

服务器端错误处理与代码统一性密切相关,值得特别注意。 如果您曾经处理过来自写得不好的API的异常,那么您可能知道正确理解这些异常的含义有多困难,甚至更难确定它们真正发生的原因。
作为API开发人员,理想情况下,您希望涵盖用户正在使用的所有端点,并使其使用一种错误消息格式。 通常,这意味着您需要使用标准错误代码及其说明,并放弃可疑的决定,例如向用户发送“ 500 Internal Server Error”消息或堆栈跟踪的结果(顺便说一句,应尽一切可能避免使用后一种选择,因为这是在泄露内部数据,而且,这种结果很难在客户端进行处理)。
例如,在这里,错误消息的一般格式如下所示:

 @Value public class ErrorResponse { private Integer errorCode; private String errorMessage; } 

在最流行的API中通常会找到与这种格式相似的格式,并且通常可以很好地工作,因为它可以轻松而系统地记录下来。 您可以通过将@ExceptionHandler批注添加到方法中来将异常转换为这种格式(有关批注的示例,请参见“常见错误#6”部分)。

常见错误编号5。多线程工作不正确


无论在桌面应用程序还是Web应用程序,Spring项目或其他平台项目中使用多线程,实现多线程都是一项艰巨的任务。 由程序的并行执行引起的问题很难跟踪,并且通常很难用调试器进行处理。 如果您了解正在处理并行执行错误,那么很可能将不得不放弃调试器并手动检查代码,直到找到错误的根本原因为止。 不幸的是,没有解决这些问题的标准方法。 在每种情况下,都有必要评估情况并使用在给定条件下看起来最好的方法“攻击”问题。

理想情况下,当然,您希望完全避免与多线程相关的错误。 尽管这里没有通用的方法,但我仍然可以提供一些实用的建议。

避免使用全局状态


首先,请始终记住“全局状态”的问题。 如果要开发多线程应用程序,则需要仔细监视绝对所有全局可修改的变量,并在可能的情况下完全消除它们。 如果仍然有理由修改全局变量,请正确同步并监视应用程序的性能-您应确保它不会由于增加的等待时间而变慢。

避免易变的物体


这个想法直接来自函数式编程的原理,并且与OOP的原理相适应,指出应该避免可变的类和可变的状态。 简而言之,这意味着您应该避免设置方法(设置器),并在所有模型类中使用带有final修饰符的私有字段。 它们的值唯一的改变是在创建对象时。 在这种情况下,您可以确保与资源竞争没有任何问题,并且在访问对象的属性时,将始终获得正确的值。

记录重要数据


评估应用程序中可能出现问题的位置,并预先设置所有重要数据的日志记录。 如果发生错误,您将很高兴获得有关已收到哪些请求的信息,并且您将能够更好地理解应用程序故障的原因。 但是,请记住,日志记录涉及其他文件I / O,因此不应滥用,因为这会对应用程序性能产生不利影响。

使用现成的线程实现


当您需要产生线程时(例如,创建对各种服务的异步请求),请使用现成的安全线程实现,而不是创建自己的线程。 在大多数情况下,您可以使用ExecutorService抽象和Java 8出色的功能抽象CompletableFuture创建线程,而Spring平台还允许您使用DeferredResult类处理异步请求。

第一部分结束。
阅读第二部分。

Source: https://habr.com/ru/post/zh-CN464429/


All Articles