10个最常见的Spring框架错误

哈Ha! 我向您介绍了Toni Kukurin撰写的文章“ Top 10最常见的Spring框架错误”

Spring可能是最受欢迎的Java框架之一,也是驯服的强大野兽。 尽管其基本概念相当容易理解,但要成为一名强大的Spring开发人员需要花费时间和精力。

在本文中,我们将介绍一些Spring中最常见的错误,尤其是有关Web应用程序和Spring Boot的错误。 如Spring Boot网站所述 ,它强加了有关如何构建工业应用程序的想法,因此在本文中,我们将尝试证明这一想法,并概述一些非常适合标准Spring Boot Web应用程序开发过程的技巧。
如果您对Spring Boot不太熟悉,但是仍然想尝试其中提到的一些事情,那么我创建了本文随附的GitHub存储库 。 如果您觉得自己迷失了本文中的任何地方,建议您将存储库克隆到本地计算机并使用代码。

常见错误1:降得太低


我们遇到这个常见错误是因为“未在这里发明”综合症在软件开发界非常普遍,其症状包括定期重写经常使用的代码片段,许多开发人员似乎为此感到痛苦。

尽管在大多数情况下理解特定库的内部及其实现是好的和必要的(并且可能是一个很好的学习过程),但是不断解决相同的低级实现细节对于作为软件工程师的开发是有害的。 有一个原因,就是存在诸如Spring之类的抽象和框架,这些抽象和框架将您与重复的手工艺品严格分开,并允许您专注于更高级别的细节-域对象和业务逻辑。

因此,请使用抽象-下次您遇到特定问题时,请先进行快速搜索并确定解决该问题的库是否已集成到Spring中。 当前,您最有可能找到现有的合适解决方案。 作为有用的库的示例,在本文其余部分的示例中,我将使用Lombok项目的注释。 Lombok用作模板代码生成器以及您内部的懒惰开发人员,希望该库的思想不会出现问题。 例如,看一下带有Lombok的“标准Java Bean”是什么样的:

@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() { } } 

但是,请注意,如果您打算在IDE中使用Lombok,则很可能必须安装插件。 IntelliJ IDEA的插件版本可以在这里找到。

常见错误2:内部内容泄漏


公开内部结构始终不是一个好主意,因为它在服务的设计中造成了灵活性,因此导致不良的编码实践。 内部内容“泄漏”的事实是,可以从某些API端点访问数据库结构。 例如,假设以下POJO(“普通旧Java对象”)表示数据库中的表:

 @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()); } } 

最初,这段代码有什么问题并不明显。 它提供了一个从TopTalentEntity实例检索的TopTalentData列表。 但是,如果仔细观察,我们会发现实际上TopTalentController在这里做了一些事情。 即:它映射对特定端点的请求,从存储库中提取数据,并将从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添加到配置类,将服务后缀添加到服务,将控制器后缀添加到控制器。

与一致性主题密切相关,服务器端错误处理值得特别注意。 如果您曾经不得不通过编写得不好的API处理异常答案,那么您可能知道为什么解析异常可能会很痛苦,而且确定这些异常最初发生的原因更加困难。

作为API开发人员,理想情况下,您希望涵盖所有用户端点并将其转换为常见的错误格式。 这通常意味着您有一个常见的错误代码和描述,而不仅仅是以下形式的借口:a)返回消息“ 500 Internal Server Error”或b)只是将堆栈跟踪重置给用户(应该不惜一切代价避免,因为它显示了您的内部信息)除了客户端处理的复杂性之外)。
常见错误响应格式的示例可能是:

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

在大多数流行的API中通常都可以找到类似的东西,并且通常可以很好地工作,因为它可以轻松而系统地记录下来。 您可以通过为方法提供@ExceptionHandler批注将异常转换为这种格式(常见错误#6中提供了批注的示例)。

常见错误5:错误的多线程


无论在桌面应用程序还是Web应用程序中找到它,在Spring中还是在Spring中,多线程都是一项艰巨的任务。 运行并行程序导致的问题难以捉摸,并且通常很难调试-实际上,由于问题的性质,一旦您了解要处理并行执行问题,就应该完全放弃调试器并开始手动检查代码,直到找到错误原因。 不幸的是,没有这样的解决方案来解决模板问题。 根据具体情况,您将不得不评估情况,然后从您认为最好的角度来解决问题。

理想情况下,当然,您希望完全避免多线程错误。 同样,这没有单一的方法,但是这里是调试和防止多线程错误的一些实际注意事项:

避免全球地位


首先,请始终记住“全局状态”问题。 如果要创建多线程应用程序,则应仔细监视绝对可以全局更改的所有内容,如果可能的话,应将其完全删除。 如果有理由使全局变量保持可变,请仔细使用同步并监视应用程序的性能,以确保它不会由于新的等待时间而变慢。

避免变异


这直接来自函数式编程,并且根据OOP的说法,应避免类的易变性和状态更改。 简而言之,前文意味着在模型的所有类别中都存在setter和私有final字段。 它们的价值仅在施工期间改变。 因此,您可以确保在资源争夺中不会有任何问题,并且访问对象的属性将始终提供正确的值。

记录关键数据


评估应用程序可能导致问题的位置,并预先记录所有重要数据。 如果发生错误,您将不胜感激,希望获得有关收到了哪些请求的信息,并且您可以更好地理解您的应用程序表现不佳的原因。 同样,应注意的是,日志记录会增加文件I / O,因此您不应滥用它,因为这会严重影响应用程序的性能。

重用现有的实现


每当您需要创建自己的线程(例如,对各种服务发出异步请求)时,请重用现有的安全实现,而不是创建自己的解决方案。 在大多数情况下,这意味着要使用Java 8简洁的函数样式中的ExecutorServicesCompletableFutures来创建线程。 Spring还允许通过DeferredResult类进行异步请求处理。

常见错误#6:不使用基于注释的验证


让我们想象一下,我们上面提到的TopTalent服务需要一个端点来添加新的Super Talents。 此外,假设出于某些充分的理由,每个新名称的长度应恰好为10个字符。 一种方法如下:

 @RequestMapping("/put") public void addTopTalent(@RequestBody TopTalentData topTalentData) { boolean nameNonExistentOrHasInvalidLength = Optional.ofNullable(topTalentData) .map(TopTalentData::getName) .map(name -> name.length() == 10) .orElse(true); if (nameNonExistentOrInvalidLength) { // throw some exception } topTalentService.addTopTalent(topTalentData); } 

但是,以上内容(除了设计欠佳之外)并不是真正的“清洁”解决方案。 我们检查一种以上类型的有效性(即,TopTalentData不为null,TopTalentData.name不为null,TopTalentData.name的长度为10个字符),并且如果数据无效,则还会引发异常。

使用Spring的Hibernate验证器可以更干净地完成此操作。 首先,我们重写addTopTalent方法以支持验证:

 @RequestMapping("/put") public void addTopTalent(@Valid @NotNull @RequestBody TopTalentData topTalentData) { topTalentService.addTopTalent(topTalentData); } @ExceptionHandler @ResponseStatus(HttpStatus.BAD_REQUEST) public ErrorResponse handleInvalidTopTalentDataException(MethodArgumentNotValidException methodArgumentNotValidException) { // handle validation exception } 

另外,我们必须在TopTalentData类中指出要检查的属性:

 public class TopTalentData { @Length(min = 10, max = 10) @NotNull private String name; } 

Spring现在将拦截该请求并在调用该方法之前对其进行验证-无需使用其他手动测试。

我们可以实现同一目标的另一种方法是创建自己的注释。 尽管通常仅在您的需求超出Hibernate内置常量集时才使用自定义批注,但在本示例中,让我们假设不存在Length批注。 您必须通过创建两个其他类来创建一个检查字符串长度的验证器,一个用于检查,另一个用于注释属性:

 @Target({ElementType.METHOD, ElementType.FIELD, ElementType.PARAMETER}) @Retention(RetentionPolicy.RUNTIME) @Documented @Constraint(validatedBy = { MyAnnotationValidator.class }) public @interface MyAnnotation { String message() default "String length does not match expected"; Class<?>[] groups() default {}; Class<? extends Payload>[] payload() default {}; int value(); } @Component public class MyAnnotationValidator implements ConstraintValidator<MyAnnotation, String> { private int expectedLength; @Override public void initialize(MyAnnotation myAnnotation) { this.expectedLength = myAnnotation.value(); } @Override public boolean isValid(String s, ConstraintValidatorContext constraintValidatorContext) { return s == null || s.length() == this.expectedLength; } } 

请注意,在这些情况下,职责分离的最佳实践要求将属性标记为有效(如果为空)(isValid方法中的s == null),然后,如果这是该属性的附加要求,则使用NotNull批注:

 public class TopTalentData { @MyAnnotation(value = 10) @NotNull private String name; } 

常见错误7:使用(仍然)XML配置


尽管XML对于Spring的早期版本是必需的,但是当前大多数配置只能由Java代码/注释来完成。 XML配置只是代表了一个额外的和不必要的样板。
本文(及其附带的GitHub存储库)使用注释来配置Spring,并且Spring知道应该连接哪个bean,因为使用@SpringBootApplication复合注释对根包进行了注释,例如:

 @SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } } 

这个复合注释(您可以在Spring文档中了解更多信息)只是给Spring一个提示,提示应该扫描哪些软件包以提取bean。 在我们的特殊情况下,这意味着将从顶级程序包(co.kukurin)开始,使用以下类连接Bean:

  • @Component (TopTalentConverter, MyAnnotationValidator) @RestController (TopTalentController) @Repository (TopTalentRepository) @Service (TopTalentService)
  • @Component (TopTalentConverter, MyAnnotationValidator) @RestController (TopTalentController) @Repository (TopTalentRepository) @Service (TopTalentService)
  • @Component (TopTalentConverter, MyAnnotationValidator) @RestController (TopTalentController) @Repository (TopTalentRepository) @Service (TopTalentService)
  • @Component (TopTalentConverter, MyAnnotationValidator) @RestController (TopTalentController) @Repository (TopTalentRepository) @Service (TopTalentService)

如果我们还有其他用@Configuration注释的类,则还将检查它们的Java配置。

常见错误8:忘记配置文件


开发服务器时经常遇到的问题是不同类型的配置(通常是工业配置和开发配置)之间的差异。 每次从测试切换到应用程序部署时,与其手动更改各种配置参数,不如更有效地使用配置文件。

考虑将内存数据库用于本地开发以及将PROM中的MySQL数据库用于这种情况。 从本质上讲,这意味着您将使用不同的URL和(希望)使用不同的凭据来访问每个URL。 让我们看看如何使用两个不同的配置文件来完成此操作:

文件APPLICATION.YAML


 # set default profile to 'dev' spring.profiles.active: dev # production database details spring.datasource.url: 'jdbc:mysql://localhost:3306/toptal' spring.datasource.username: root spring.datasource.password: 

文件APPLICATION-DEV.YAML


 spring.datasource.url: 'jdbc:h2:mem:' spring.datasource.platform: h2 

显然,您不希望在弄乱代码的同时意外地对工业数据库执行任何操作,因此在dev中设置默认配置文件是有意义的。 然后,在服务器上,可以通过为JVM指定-Dspring.profiles.active = prod参数来手动覆盖配置概要文件。 此外,您还可以将OS环境变量设置为所需的默认配置文件。

常见错误9:无法接受依赖注入


在Spring中正确使用依赖项注入意味着它允许您通过扫描所有必需的配置类将所有对象绑定在一起。 这对于解耦关系很有用,也使测试更加容易。 通过执行以下操作,而不是硬链接类:

 public class TopTalentController { private final TopTalentService topTalentService; public TopTalentController() { this.topTalentService = new TopTalentService(); } } 


我们让Spring为我们做绑定:

 public class TopTalentController { private final TopTalentService topTalentService; public TopTalentController(TopTalentService topTalentService) { this.topTalentService = topTalentService; } } 

Google Talk的Misko Hevery详细解释了依赖项注入的“原因”,因此让我们看看如何在实践中使用它。 在职责划分(常见错误3)中,我们创建了服务和控制器类。 假设我们要在TopTalentService行为正确的假设下测试控制器。 我们可以插入一个模拟对象而不是实际的服务实现,从而提供一个单独的配置类:

 @Configuration public class SampleUnitTestConfig { @Bean public TopTalentService topTalentService() { TopTalentService topTalentService = Mockito.mock(TopTalentService.class); Mockito.when(topTalentService.getTopTalent()).thenReturn( Stream.of("Mary", "Joel") .map(TopTalentData::new).collect(Collectors.toList())); return topTalentService; } } 

然后我们可以通过告诉Spring使用SampleUnitTestConfig作为配置提供程序来嵌入模拟对象:

 @ContextConfiguration(classes = { SampleUnitTestConfig.class }) 

然后,这将使我们能够使用上下文配置将定制bean嵌入到单元测试中。

常见错误10:缺少测试或测试不正确


尽管单元测试的想法已经存在了很长时间,但许多开发人员似乎“忘记”了这样做(尤其是在不必要的情况下),或者只是将其留待以后使用。 显然,这是不可取的,因为测试不仅应验证代码的正确性,而且还可以作为文档说明应用程序在不同情况下的行为。

在测试Web服务时,您很少执行“干净的”单元测试,因为通过HTTP进行交互通常需要调用DispatcherServlet Spring并查看接收到实际HttpServletRequest时发生的情况(这使其成为集成测试,并通过使用验证,序列化等)。 REST保证 -用于在MockMVC之上轻松测试REST服务的Java DSL已被证明是一个非常优雅的解决方案。 考虑以下带有依赖项注入的代码片段:

 @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = { Application.class, SampleUnitTestConfig.class }) public class RestAssuredTestDemonstration { @Autowired private TopTalentController topTalentController; @Test public void shouldGetMaryAndJoel() throws Exception { // given MockMvcRequestSpecification givenRestAssuredSpecification = RestAssuredMockMvc.given() .standaloneSetup(topTalentController); // when MockMvcResponse response = givenRestAssuredSpecification.when().get("/toptal/get"); // then response.then().statusCode(200); response.then().body("name", hasItems("Mary", "Joel")); } } 

SampleUnitTestConfig在TopTalentController中启用TopTalentService模拟实现,而所有其他类均使用标准配置进行连接,该配置通过扫描在Application类的包中具有根的数据包而获得。RestAssuredMockMvc仅用于创建轻量级环境,并将GET请求发送到/ toptal / get端点。

成为春季大师


Spring是一个功能强大的框架,很容易上手,但是要花费一定的精力和时间才能完全掌握。如果您花时间了解框架,从长远来看,它肯定会提高您的生产力,并最终帮助您编写更简洁的代码并成为一个更好的开发人员。

如果您正在寻找其他资源,那么《Spring In Action》是一本很好的练习书,涵盖了许多Spring核心主题。

标记
Java SpringFramework

留言


Timothy Schimandle
在#2,我认为在大多数情况下,最好返回域对象。您的示例自定义对象是具有我们要隐藏的字段的几个类之一。但是我使用过的绝大多数对象都没有这样的限制,而添加dto类只是不必要的代码。
总而言之,一篇好文章。辛苦了我完全同意

蒂莫西·希曼德尔(Timothy Schimandle)的观点
。似乎已经添加了不必要的额外代码层,我认为@JsonIgnore将有助于忽略字段(尽管默认存储库检测策略存在缺陷),但是总的来说,这是一篇很棒的博客文章。自豪地跌倒了...

Arokiadoss Asirvatham
杜德(Dude),另一个常见的初学者错误是:1)循环依赖,以及2)不遵守基本的Singleton Class声明原则,例如在具有singleton范围的bean中使用实例变量。

Hlodowig
关于第8条,我认为剖析轮廓的方法非常不令人满意。让我们看看:

  • 安全性:有人说:如果您的存储库是公共的,会不会有任何秘密密钥/密码?按照这种方法,很可能会这样。当然,除非您将配置文件添加到.gitignore,但这不是一个严肃的选择。
  • 复制:每次我进行不同的设置时,都需要创建一个新的属性文件,这很烦人。
  • 可移植性:我知道这只是一个JVM参数,但零胜于一。无限减少出错的可能性。

我试图找到一种在配置文件中使用环境变量的方法,而不是对这些值进行“硬编码”,但是到目前为止,我还没有成功,我认为我需要做更多的研究。

伟大的文章Tony,请继续努力!

翻译完成:tele.gg/middle_java

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


All Articles