可以说是最流行的 Java 框架之一,Spring 是一头难以驯服的野兽。虽然它的基本概念相当容易掌握,但要成为一个强大的 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() { } }复制代码
但是,请注意,如果您计划在 IDE 中使用 Lombok,您很可能需要安装插件,可以在此处找到该插件的 Intellij IDEA 版本。
2. 常见错误2:内部结构“泄漏”
暴露你的内部结构从来都不是一个好主意,因为它会在服务设计中造成不灵活,从而促进糟糕的编码实践。“泄漏”的内部机制表现为使数据库结构可以从某些 API 端点访问。例如,以下 POJO(“Plain Old Java Object”)类表示数据库中的一个表:
@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. 常见错误 #3:缺乏关注点分离
随着程序规模的扩大,代码组织逐渐成为一个越来越重要的问题。具有讽刺意味的是,大多数优秀的软件工程原则开始大规模崩溃——尤其是在没有太多考虑程序架构设计的情况下。开发人员最常犯的错误之一是混淆代码问题,而且很容易做到!
通常,打破关注点分离的只是简单地将新功能“转储”到现有类中。当然,这是一个很好的短期解决方案(对于初学者来说,它需要更少的打字),但它也不可避免地在未来成为一个问题,无论是在测试、维护期间,还是介于两者之间的某个地方。考虑以下控制器,它将从数据库中返回 TopTalentData。
@RestController public class TopTalentController { private final TopTalentRepository topTalentRepository; @RequestMapping("/toptal/get") public List 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 getTopTalent() { return topTalentService.getTopTalent(); } } @AllArgsConstructor @Service public class TopTalentService { private final TopTalentRepository topTalentRepository; private final TopTalentEntityConverter topTalentEntityConverter; public List 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. 常见错误#4:缺乏异常处理或不当处理
一致性主题并不是 Spring(或 Java)独有的,但它仍然是处理 Spring 项目时需要考虑的一个重要方面。虽然编码风格可能会引起争议(通常在团队或整个公司内达成一致),但拥有一个共同的标准最终会极大地提高生产力。对于多人团队尤其如此;一致性允许沟通发生,而无需花费大量资源进行交接或为不同类别的职责提供冗长的解释。
考虑一个包含各种配置文件、服务和控制器的 Spring 项目。保持命名中的语义一致性会创建一个易于搜索的结构,任何新开发人员都可以在其中以自己的方式管理代码;比如给一个配置类加一个Config后缀,一个以Service结尾的服务层,一个以Controller结尾的controller。
与一致性主题密切相关的服务器端错误处理值得特别强调。如果您曾经不得不处理来自编写不佳的 API 的异常响应没有网络spring配置文件报错,您可能知道原因——正确解析异常可能会很痛苦,而首先确定这些异常发生的原因则更加痛苦。
作为 API 开发人员,理想情况下,您希望涵盖所有面向用户的端点并将它们转换为常见的错误格式。这通常意味着有一个通用的错误代码和描述,而不是回避问题:a) 返回“500 Internal Server Error”消息。b) 将异常的堆栈信息直接返回给用户。(实际上,应该不惜一切代价避免这些,因为除了让客户难以处理之外,它还会暴露您的内部信息)。
例如,常见的错误响应格式可能如下所示:
@Value public class ErrorResponse { private Integer errorCode; private String errorMessage; }复制代码
像这样的事情在大多数流行的 API 中经常遇到,并且通常工作得很好,因为它们可以轻松且系统地记录下来。可以通过为方法提供@ExceptionHandler 注释来将异常转换为这种格式(注释示例可以在第 6 章中找到)。
5. 常见错误5:不正确的多线程
无论是桌面应用还是Web应用,无论是Spring还是No Spring,多线程都很难破解。由程序并行执行引起的问题令人毛骨悚然且难以捉摸,而且通常难以调试——事实上,由于问题的性质,一旦你意识到你正在处理并行执行问题,你可能不得不完全放弃调试器,并“手动”检查代码,直到找到错误的根本原因。不幸的是,对于这类问题没有万能的解决方案。逐案评估情况,并从您认为最好的方法解决问题。
当然,理想情况下,您还希望完全避免多线程错误。同样,这种一刀切的方法并不存在,但这对于调试和防止多线程错误有一些实际的考虑:
5.1. 避免全局状态
首先,牢记“全球状态”问题。如果你正在构建一个多线程应用程序,你应该留意任何可能被全局修改的东西,如果可能的话,将它们全部删除。如果出于某种原因必须保持可修改全局变量,请谨慎使用同步,并跟踪程序性能,以确保系统性能不会因新引入的延迟而降低。
5.2. 避免可变性
这直接来自函数式编程,并适用于 OOP,声明应避免类和状态更改。简而言之,这意味着放弃 setter 方法并在所有模型类上拥有私有 final 字段。它们的值唯一变化是在施工期间。这样,您可以确保不会出现争用问题,并且访问对象属性将始终提供正确的值。
5.3. 记录关键数据
评估您的程序可能出现异常的位置并预先记录所有关键数据。如果发生错误,您将很高兴获得有关收到哪些请求的信息,并更好地了解您的应用程序失败的原因。再次注意,日志记录引入了额外的文件 I/O,会严重影响应用程序的性能,所以不要滥用日志记录。
5.4. 重用现有实现
每当您需要创建自己的线程(例如:向不同的服务发出异步请求)时,请重用现有的安全实现,而不是创建自己的解决方案。这在很大程度上意味着使用 ExecutorServices 和 Java 8 的简洁功能 CompletableFutures 来创建线程。Spring 还允许通过 DeferredResult 类进行异步请求处理。
6. 常见错误 #6:不使用基于注释的验证
假设我们之前的 TopTalent 服务需要一个端点来添加新的 TopTalent。另外,假设由于某种原因,每个新名词都需要 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 不能为空,TopTalentData.name 不能为空,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 的内置约束集时才使用自定义注解,但在此示例中没有网络spring配置文件报错,我们假设 @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[] payload() default {}; int value(); } @Component public class MyAnnotationValidator implements ConstraintValidator { 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 TopTalentData { @MyAnnotation(value = 10) @NotNull private String name; }复制代码
7. 常见错误 #7:(仍然)使用基于 XML 的配置
虽然以前的 Spring 版本需要 XML,但如今大多数配置都可以通过 Java 代码或注解来完成;XML 配置只是额外的不必要的样板代码。
本文(及其随附的 GitHub 存储库)使用注解来配置 Spring,Spring 知道要连接哪些 bean,因为要扫描的顶级包目录在 @SpringBootApplication 复合注解中声明,如下所示:
@SpringBootApplication public class Application { public static void main(String[] args) { SpringApplication.run(Application.class, args); } }复制代码
复合注释(在 Spring 文档中有更多内容)只是向 Spring 提示 Spring 应该扫描哪些包以检索 bean。在我们的例子中,这意味着这个顶级包(co.kukurin)将用于检索:
如果我们有任何额外的 @Configuration 注释类,它们也会检查基于 Java 的配置。
8. 常见错误 8:忽略配置文件
在服务器端开发中,一个常见的问题是区分不同的配置类型,通常是生产配置和开发配置。与每次从测试切换到部署应用程序时手动替换各种配置项相比,使用配置文件更有效。
考虑一种情况,您使用内存数据库进行本地开发,使用 MySQL 数据库进行生产。从本质上讲,这意味着您需要使用不同的 URL 和(希望)不同的凭据来访问两者。让我们看看如何使用这两个不同的配置文件来做到这一点:
8.1. 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:复制代码
8.2. APPLICATION-DEV.YAML 文件
spring.datasource.url: 'jdbc:h2:mem:' spring.datasource.platform: h2复制代码
假设您不想在修改代码时不小心对生产数据库执行任何操作,那么将默认配置文件设置为 dev 是有意义的。然后,在服务器上,您可以通过向 JVM 提供 -Dspring.profiles.active=prod 参数来手动覆盖配置文件。或者,可以将操作系统的环境变量设置为所需的默认配置文件。
9. 常见错误 #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; } }复制代码
Misko Hevery 的 Google 演讲深入解释了依赖注入的“原因”,让我们看看它是如何在实践中使用的。在 Separation of Concerns (Common Mistakes #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. 常见错误 #10:缺乏测试或测试不当
尽管单元测试的概念已经存在了很长时间,但许多开发人员似乎要么“忘记”去做它(特别是如果它不是“必需的”),要么只是事后才添加它。这显然是不可取的,因为测试不仅应该验证代码的正确性,还应该作为程序在不同场景下应该如何表现的文档。
在测试 Web 服务时,很少只进行“纯”单元测试,因为通过 HTTP 进行通信通常需要调用 Spring 的 DispatcherServlet 并查看接收到实际 HttpServletRequest 时会发生什么(使其成为处理验证、序列化、等等。)。REST Assured 是一种在 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 类将 TopTalentService 的 mock 实现连接到 TopTalentController,而其他所有类都是通过扫描应用程序类所在包的子包目录推断出的标准配置。RestAssuredMockMvc 仅用于设置轻量级环境并向 /toptal/get 端点发送 GET 请求。
11.成为Spring Master
Spring 是一个功能强大的框架,易于上手,但需要一些投资和时间才能完全掌握。花时间熟悉框架肯定会从长远来看提高你的生产力,并最终帮助你编写更简洁的代码并成为更好的开发人员。
寻找更多资源,Spring In Action 是一本优秀的实践书籍,涵盖了 Spring 的许多核心主题。
常见问题FAQ
- 免费下载或者VIP会员专享资源能否直接商用?
- 本站所有资源版权均属于原作者所有,这里所提供资源均只能用于参考学习用,请勿直接商用。若由于商用引起版权纠纷,一切责任均由使用者承担。更多说明请参考 VIP介绍。
- 提示下载完但解压或打开不了?
- 找不到素材资源介绍文章里的示例图片?
- 欧资源网