十大最常见的Spring框架错误

本文概述

  • 常见错误1:级别过低
  • 常见错误2:内部” 泄漏”
  • 常见错误3:缺乏关注点分离
  • 常见错误4:不一致和差错处理
  • 常见错误5:不正确地处理多线程
  • 常见错误6:不采用基于注释的验证
  • 常见错误7 :(仍然)使用基于XML的配置
  • 常见错误#8:忘记个人资料
  • 常见错误9:无法接受依赖注入
  • 常见错误10:缺乏测试或测试不当
  • 成为Spring大师
可以说, Spring是最受欢迎的Java框架之一, 也是驯服的强大野兽。虽然其基本概念相当容易掌握, 但要成为一名强大的Spring开发人员, 需要一些时间和精力。
在本文中, 我们将介绍一些在Spring中更常见的错误, 特别是针对Web应用程序和Spring Boot的错误。如Spring Boot网站所言, Spring Boot对应如何构建生产就绪的应用程序持保留意见, 因此本文将尝试模仿该视图, 并概述一些技巧, 这些技巧将很好地融入标准Spring Boot Web应用程序开发中。
如果你对Spring Boot不太熟悉, 但是仍然想尝试一些提到的内容, 那么我将在本文附带的地方创建一个GitHub存储库。如果你在本文中的任何时候感到迷惑, 建议你克隆存储库并在本地计算机上使用代码。
常见错误1:级别过低我们之所以会遇到这个常见错误, 是因为” 未在这里发明” 综合症在软件开发界非常普遍。包括定期重写一些常用代码的症状和许多开发人员的症状似乎都在受此困扰。
虽然了解特定库的内部及其实现在很大程度上是有益且必要的(并且可能也是一个很好的学习过程), 但对于作为软件工程师的开发人员而言, 不断解决相同的低级别实现是有害的细节。存在诸如Spring之类的抽象和框架是有原因的, 这恰恰是将你与重复的手工工作区分开来, 并允许你专注于更高级别的详细信息—域对象和业务逻辑。
因此, 请接受抽象-下次遇到特定问题时, 请先进行快速搜索, 然后确定解决该问题的库是否已集成到Spring中;如今, 你很可能会找到合适的现有解决方案。作为有用的库的示例, 在本文的其余部分, 我将在示例中使用Project 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() { } }

但是请注意, 如果你打算将Lombok与IDE一起使用, 则很可能必须安装插件。你可以在此处找到IntelliJ IDEA的插件版本。
常见错误2:内部” 泄漏” 公开内部结构绝不是一个好主意, 因为它会在服务设计中造成僵化, 并因此导致不良的编码习惯。通过使数据库结构可从某些API端点访问来体现” 内部” 泄漏。举例来说, 假设以下POJO(“ 普通的Java旧对象” )表示数据库中的一个表:
@Entity @NoArgsConstructor @Getter public class srcminientEntity {@Id @GeneratedValue private Integer id; @Column private String name; public srcminientEntity(String name) { this.name = name; }}

假设存在一个需要访问srcminientEntity数据的端点。可能会尝试返回srcminientEntity实例, 因此更灵活的解决方案是创建一个新类来表示API端点上的srcminientEntity数据:
@AllArgsConstructor @NoArgsConstructor @Getter public class srcminientData { private String name; }

这样, 对数据库后端进行更改将不需要在服务层中进行任何其他更改。考虑在将” 密码” 字段添加到srcminientEntity以便在数据库中存储用户的密码哈希的情况下会发生什么—如果没有诸如srcminientData之类的连接器, 忘记更改服务前端会意外地暴露一些非常不受欢迎的秘密信息!
常见错误3:缺乏关注点分离随着应用程序的增长, 代码组织越来越开始变得越来越重要。具有讽刺意味的是, 大多数良好的软件工程原理开始大规模分解—特别是在对应用程序体系结构设计没有过多考虑的情况下。然后, 开发人员往往会屈服于的最常见错误之一就是混合代码问题, 而且这非常容易做到!
通常打破关注点分离的只是将新功能” 倾销” 到现有类中。当然, 这是一个很好的短期解决方案(对于初学者来说, 它需要更少的输入), 但是不可避免地会成为进一步的问题, 无论是在测试, 维护还是在两者之间。考虑以下控制器, 该控制器从其存储库返回srcminientData:
@RestController public class srcminientController {private final srcminientRepository srcminientRepository; @RequestMapping("/srcmini/get") public List< srcminientData> getsrcminient() { return srcminientRepository.findAll() .stream() .map(this::entityToData) .collect(Collectors.toList()); }private srcminientData entityToData(srcminientEntity srcminientEntity) { return new srcminientData(srcminientEntity.getName()); }}

最初, 这段代码似乎没有什么特别错误;它提供了从srcminientEntity实例检索的srcminientData列表。但是, 仔细观察一下, 我们可以看到srcminientController实际上在执行一些操作。也就是说, 它将请求映射到特定的端点, 从存储库中检索数据, 并将从srcminientRepository接收到的实体转换为其他格式。 “ 更清洁” 的解决方案是将这些问题分为自己的类。它可能看起来像这样:
@RestController @RequestMapping("/srcmini") @AllArgsConstructor public class srcminientController {private final srcminientService srcminientService; @RequestMapping("/get") public List< srcminientData> getsrcminient() { return srcminientService.getsrcminient(); } }@AllArgsConstructor @Service public class srcminientService {private final srcminientRepository srcminientRepository; private final srcminientEntityConverter srcminientEntityConverter; public List< srcminientData> getsrcminient() { return srcminientRepository.findAll() .stream() .map(srcminientEntityConverter::toResponse) .collect(Collectors.toList()); } }@Component public class srcminientEntityConverter { public srcminientData toResponse(srcminientEntity srcminientEntity) { return new srcminientData(srcminientEntity.getName()); } }

此层次结构的另一个优点是, 它使我们可以仅通过检查类名来确定功能所在的位置。此外, 在测试期间, 如果需要, 我们可以轻松地用模拟实现替换任何类。
常见错误4:不一致和差错处理一致性主题并不一定是Spring(或Java)所独有的, 但在处理Spring项目时仍然是要考虑的重要方面。虽然编码风格可能会引起争论(通常是团队内部或整个公司内部的协议问题), 但拥有通用标准却可以极大地提高生产力。多人团队尤其如此。一致性允许进行交接, 而无需花费大量资源进行交接或提供有关不同类职责的冗长解释
考虑一个带有各种配置文件, 服务和控制器的Spring项目。在命名它们时在语义上保持一致, 从而创建了一个易于搜索的结构, 任何新开发人员都可以使用该结构来管理代码。例如, 将Config后缀附加到你的配置类中, 将Service后缀附加到你的服务中, 并将Controller后缀附加到你的控制器中。
与一致性主题密切相关, 服务器端的错误处理应特别强调。如果你不得不处理来自写得不好的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, 因此不应滥用, 因为它会严重影响应用程序的性能。
重用现有的实现
每当你需要产生自己的线程时(例如, 向不同的服务发出异步请求), 请重用现有的安全实现而不是创建自己的解决方案。在大多数情况下, 这意味着利用ExecutorServices和Java 8的简洁功能样式CompletableFutures进行线程创建。 Spring还允许通过DeferredResult类进行异步请求处理。
常见错误6:不采用基于注释的验证假设我们以前的srcminient服务需要一个端点来添加新的Top Talents。此外, 可以说, 出于某些确实合理的原因, 每个新名称都必须恰好包含10个字符。执行此操作的一种方法可能是以下方法:
@RequestMapping("/put") public void addsrcminient(@RequestBody srcminientData srcminientData) { boolean nameNonExistentOrHasInvalidLength = Optional.ofNullable(srcminientData) .map(srcminientData::getName) .map(name -> name.length() == 10) .orElse(true); if (nameNonExistentOrInvalidLength) { // throw some exception }srcminientService.addsrcminient(srcminientData); }

但是, 以上内容(除了结构较差之外)并不是真正的” 干净” 解决方案。我们正在检查一种以上的有效性(即srcminientData不为null, srcminientData.name不为null, srcminientData.name的长度为10个字符), 并检查数据是否无效。 。
通过在Spring中使用Hibernate验证器, 可以更清晰地执行此操作。首先, 我们重构addsrcminient方法以支持验证:
@RequestMapping("/put") public void addsrcminient(@Valid @NotNull @RequestBody srcminientData srcminientData) { srcminientService.addsrcminient(srcminientData); }@ExceptionHandler @ResponseStatus(HttpStatus.BAD_REQUEST) public ErrorResponse handleInvalidsrcminientDataException(MethodArgumentNotValidException methodArgumentNotValidException) { // handle validation exception }

此外, 我们将必须在srcminientData类中指出要验证的属性:
public class srcminientData { @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; } }

请注意, 在这些情况下, 关注点分离的最佳实践要求你在属性为null时将其标记为有效(在isValid方法中为s == null), 然后使用@NotNull批注(如果这是该属性的附加要求)属性:
public class srcminientData { @MyAnnotation(value = http://www.srcmini.com/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)软件包下的以下内容)用于接线:
  • @Component(srcminientConverter, MyAnnotationValidator)
  • @RestController(srcminientController)
  • @Repository(srcminientRepository)
  • @Service(srcminientService)类
如果我们还有其他@Configuration注释的类, 那么还将检查它们是否基于Java。
常见错误#8:忘记个人资料服务器开发中经常遇到的一个问题是区分不同的配置类型, 通常是生产和开发配置。每次从测试切换到部署应用程序时, 与其手动替换各种配置条目, 不如更有效地使用配置文件。
考虑将内存数据库用于本地开发且将MySQL数据库投入生产的情况。从本质上讲, 这意味着你将使用不同的URL和(希望)使用不同的凭据来访问两者。让我们看看如何通过两个不同的配置文件来完成此操作:
application.yaml文件
# set default profile to 'dev' spring.profiles.active: dev# production database details spring.datasource.url: 'jdbc:mysql://localhost:3306/srcmini' 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参数来手动覆盖配置概要文件。另外, 你也可以将操作系统的环境变量设置为所需的默认配置文件。
常见错误9:无法接受依赖注入在Spring中正确使用依赖注入意味着允许它通过扫描所有所需的配置类将所有对象连接在一起。事实证明, 这对于解耦关系很有用, 也使测试变得更加容易。而不是通过执行类似以下操作来紧密耦合类:
public class srcminientController {private final srcminientService srcminientService; public srcminientController() { this.srcminientService = new srcminientService(); } }

我们允许Spring为我们做接线:
public class srcminientController {private final srcminientService srcminientService; public srcminientController(srcminientService srcminientService) { this.srcminientService = srcminientService; } }

Misko Hevery在Google上的演讲深入解释了依赖项注入的” 原因” , 因此让我们看看它在实践中的用法。在关注点分离部分(常见错误3)中, 我们创建了服务和控制器类。假设我们要在srcminientService行为正确的假设下测试控制器。我们可以通过提供一个单独的配置类来插入一个模拟对象来代替实际的服务实现:
@Configuration public class SampleUnitTestConfig { @Bean public srcminientService srcminientService() { srcminientService srcminientService = Mockito.mock(srcminientService.class); Mockito.when(srcminientService.getsrcminient()).thenReturn( Stream.of("Mary", "Joel").map(srcminientData::new).collect(Collectors.toList())); return srcminientService; } }

然后, 我们可以通过告诉Spring使用SampleUnitTestConfig作为其配置提供者来注入模拟对象:
@ContextConfiguration(classes = { SampleUnitTestConfig.class })

然后, 这允许我们使用上下文配置将定制bean注入到单元测试中。
常见错误10:缺乏测试或测试不当尽管单元测试的想法已经存在了很长时间, 但许多开发人员似乎要么” 忘记” 了这样做(特别是如果不需要的话), 或者只是事后补充。这显然是不希望的, 因为测试不仅应验证代码的正确性, 而且还可以作为文档说明应用程序在不同情况下的行为。
【十大最常见的Spring框架错误】在测试Web服务时, 你很少只进行” 纯” 单元测试, 因为通过HTTP进行的通信通常要求你调用Spring的DispatcherServlet并查看接收到实际的HttpServletRequest时会发生什么(使其成为集成测试, 处理验证, 序列化)等)。 REST Assured是一种在MockMVC之上的Java DSL, 用于轻松测试REST服务, 已被证明是一种非常优雅的解决方案。考虑以下具有依赖项注入的代码段:
@RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = { Application.class, SampleUnitTestConfig.class }) public class RestAssuredTestDemonstration {@Autowired private srcminientController srcminientController; @Test public void shouldGetMaryAndJoel() throws Exception { // given MockMvcRequestSpecification givenRestAssuredSpecification = RestAssuredMockMvc.given() .standaloneSetup(srcminientController); // when MockMvcResponse response = givenRestAssuredSpecification.when().get("/srcmini/get"); // then response.then().statusCode(200); response.then().body("name", hasItems("Mary", "Joel")); }}

SampleUnitTestConfig将srcminientService的模拟实现连接到srcminientController中, 而所有其他类都使用从根植于Application类的包中的扫描包中推断出的标准配置进行连接。 RestAssuredMockMvc仅用于设置轻量级环境, 并将GET请求发送到/ srcmini / get端点。
成为Spring大师Spring是一个功能强大的框架, 易于入门, 但需要一定的奉献精神和时间才能完全掌握。从长远来看, 花点时间熟悉框架肯定会提高你的生产率, 并最终帮助你编写更简洁的代码并成为更好的开发人员。
如果你正在寻找更多资源, 《 Spring In Action》是一本很好的动手手册, 涵盖许多Spring核心主题。

    推荐阅读