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() {
}
}
但请注意,如果您打算在IDE中使用Lombok,则最有可能需要安装插件。可以在此处找到IntelliJ IDEA的插件版本。
常见错误#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
实例,但更灵活的解决方案是创建一个新类来表示TopTalentEntity
API端点上的数据:
@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
转换为不同的格式。一个"更清洁"的解决方案是将这些问题分成他们自己的类。它可能看起来像这样:
@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内部服务器错误"消息,或b)只是将堆栈跟踪转储给用户(实际上应该不惜一切代价避免)的删除解决方案因为除了难以处理客户端之外,它还暴露了你的内部因素。
常见错误响应格式的示例可能是:
@Value
public class ErrorResponse {
private Integer errorCode;
private String errorMessage;
}
类似于此的东西在大多数流行的API中经常遇到,并且由于可以容易且系统地记录,因此往往效果很好。可以通过向@ExceptionHandler
方法提供注释来完成将异常转换为此格式(注释的示例在Common Mistake#6中)。
常见错误#5:多线程处理不当
无论是在桌面应用程序还是Web应用程序中遇到它,Spring还是没有Spring,多线程都是一个难以破解的难题。并行执行程序引起的问题是令人难以理解的,并且通常非常难以调试 - 实际上,由于问题的本质,一旦你意识到你正在处理并行执行问题,你可能会去必须完全放弃调试器并"手动"检查代码,直到找到根错误原因。不幸的是,没有解决这些问题的千篇一律的解决方案; 根据您的具体情况,您将不得不评估情况,然后从您认为最好的角度来解决问题。
理想情况下,您当然希望完全避免多线程错误。同样,这样做并不存在一种通用的方法,但这里有一些调试和防止多线程错误的实际考虑因素:
避免全球状态
首先,要始终记住"全球状态"问题。如果您正在创建一个多线程应用程序,那么应该密切监视任何可全局修改的内容,如果可能的话,完全删除。如果全局变量必须保持可修改的原因,请仔细使用同步并跟踪应用程序的性能,以确认由于新引入的等待时间而导致它不会缓慢。
避免可变性
这个直接来自函数式编程,并且适用于OOP,声明应该避免类可变性和改变状态。简而言之,这意味着前面的setter方法,并在所有模型类上都有私有的final字段。他们的价值观变异的唯一时间是在施工期间。这样,您可以确定不会出现争用问题,并且访问对象属性将始终提供正确的值。
记录关键数据
评估应用程序可能导致问题的位置,并抢先记录所有关键数据。如果发生错误,您将很高兴获得有关收到哪些请求的信息,并更好地了解您的应用程序出现问题的原因。还需要注意的是,日志记录引入了额外的文件I / O,因此不应滥用,因为它会严重影响应用程序的性能。
重用现有实现
每当您需要生成自己的线程时(例如,为了向不同的服务发出异步请求),重用现有的安全实现而不是创建自己的解决方案。在大多数情况下,这将意味着利用ExecutorServices和Java 8的整洁功能样式CompletableFutures进行线程创建。Spring还允许通过DeferredResult类进行异步请求处理。
常见错误#6:未使用基于注释的验证
让我们假设我们之前的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的内置约束集时,您通常只会使用自定义注释,但对于此示例,我们假设@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(s == null
在isValid
方法内),然后使用@NotNull
注释,如果这是属性的附加要求:
public class TopTalentData {
@MyAnnotation(value = 10)
@NotNull
private String name;
}
常见错误#7 :(仍然)使用基于XML的配置
虽然XML是以前版本的Spring的必需品,但现在大多数配置都可以通过Java代码/注释完成; XML配置只是附加和不必要的样板代码。
本文(及其附带的GitHub存储库)使用注释来配置Spring和Spring知道它应该连接哪些bean,因为root包已经使用@SpringBootApplication
复合注释进行了注释,如下所示:
@SpringBootApplication
public class Application {
public static void main(String[] args) {
SpringApplication.run(Application.class, args);
}
}
复合注释(您可以在Spring文档中了解更多信息,只是简单地告诉Spring应该扫描哪些包来检索bean。在我们的具体情况中,这意味着将使用top(co.kukurin)包下的以下内容用于接线:
-
@Component
(TopTalentConverter
,MyAnnotationValidator
) -
@RestController
(TopTalentController
) -
@Repository
(TopTalentRepository
) -
@Service
(TopTalentService
)类
如果我们有任何其他@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/toptal'
spring.datasource.username: root
spring.datasource.password:
APPLICATION-DEV.YAML文件
spring.datasource.url: 'jdbc:h2:mem:'
spring.datasource.platform: h2
据推测,您不希望在修改代码时意外地对生产数据库执行任何操作,因此将默认配置文件设置为dev是有意义的。然后,在服务器上,您可以通过向-Dspring.profiles.active=prod
JVM 提供参数来手动覆盖配置概要文件。或者,您也可以将操作系统的环境变量设置为所需的默认配置文件。
常见错误#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演讲深入解释了依赖注入的"为什么",所以让我们看看它是如何在实践中使用的。在关注点分离(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:缺乏测试或不正确的测试
尽管单元测试的想法已经在我们这里使用了很长时间,但很多开发人员似乎"忘记"这样做(特别是如果不需要),或者只是将其添加到事后的想法中。这显然是不可取的,因为测试不仅应验证代码的正确性,还应作为应用程序在不同情况下的行为方式的文档。
在测试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
成TopTalentController
,而所有其它类是使用从植根于应用类的包扫描包推导出的标准配置布线。RestAssuredMockMvc
仅用于设置轻量级环境并向端点发送GET
请求/toptal/get
。
成为一名春天大师
Spring是一个功能强大的框架,很容易上手,但需要一些奉献精神和时间来实现完全掌握。花时间熟悉框架肯定会提高您的工作效率,最终帮助您编写更清晰的代码并成为更好的开发人员。
如果您正在寻找更多资源,Spring In Action是一本很好的动手书,涵盖了许多Spring核心主题。