作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.
托尼Kukurin's profile image

托尼Kukurin

Toni喜欢构建软件解决方案,并运用他的工程技能来解决有趣的现实问题.

专业知识

以前在

谷歌
分享

春天可以说是最流行的Java框架之一,也是一个难以驯服的庞然大物. 虽然它的基本概念相当容易掌握, 成为一名优秀的春天开发人员需要一些时间和努力.

在本文中,我们将介绍春天中的一些常见错误, 专门面向web应用程序和春天 Boot. As 春天 Boot’s website states, 春天 Boot takes an opinionated view 关于如何构建生产就绪的应用程序, 因此,本文将尝试模仿这种观点,并概述一些技巧,这些技巧将很好地融入到标准的春天 Boot web应用程序开发中.

如果您不是很熟悉春天 Boot,但仍然想尝试一下上面提到的一些东西, 我创建了 本文附带的GitHub存储库. 如果你在阅读过程中感到迷茫, 我建议克隆存储库并在本地机器上试用代码.

常见错误1:层次过低

我们在这个常见的错误上很合得来,因为not invented 在这里综合症在软件开发世界中很常见. 症状包括经常重写常用的代码片段,许多开发人员似乎都有这种症状.

虽然了解特定库的内部结构及其实现在很大程度上是好的和必要的(并且可能是一个很好的学习过程), 作为软件工程师,不断地处理相同的底层实现细节对您的开发是有害的. 像春天这样的抽象和框架存在是有原因的, 这是为了将您从重复的手工工作中分离出来,并允许您专注于更高层次的细节—您的领域对象和业务逻辑.

所以,下次你面对一个特殊的问题时,拥抱抽象吧, do a quick search first 和 determine whether a library solving 那 problem is already integrated 成 春天; nowadays, 您可能会找到一个合适的现有解决方案. 作为一个有用库的示例,我将使用 Project Lombok 本文其余部分的示例注释. Lombok被用作样板代码生成器,希望懒惰的开发人员熟悉这个库不会有问题. 举个例子, check out what a “st和ard Java bean” looks like with Lombok:

@ getter
@ setter
@NoArgsConstructor
Bean实现Serializable {
    int firstBeanProperty;
    String secondBeanProperty;
}

正如你想象的那样,上面的代码编译成:

Bean实现Serializable {
    private int firstBeanProperty;
    私有字符串secondBeanProperty;

    getFirstBeanProperty() {
        返回这.firstBeanProperty;
    }

    getSecondBeanProperty() {
        返回这.secondBeanProperty;
    }

    setFirstBeanProperty(int firstBeanProperty) {
        这.firstBeanProperty = firstBeanProperty;
    }

    setSecondBeanProperty(String secondBeanProperty) {
        这.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 名字;

    public TopTalentEntity(字符串名称){
        这.Name = Name;
    }

}

假设存在一个端点,它需要访问 TopTalentEntity data. Tempting as it may be to return TopTalentEntity 实例时,更灵活的解决方案是创建一个新类来表示 TopTalentEntity data on the API 端点:

@AllArgsConstructor
@NoArgsConstructor
@ getter
public class TopTalentData {
    private String 名字;
}

这种方式, 对数据库后端进行更改将不需要在服务层中进行任何额外更改. 考虑将' password '字段添加到的情况下会发生什么 TopTalentEntity 用于在数据库中存储用户的密码散列—无需连接器,例如 TopTalentData, 忘记更改服务前端会意外地暴露一些非常不受欢迎的秘密信息!

常见错误#3:缺乏关注点分离

As your 应用程序 grows, 代码组织越来越成为一件越来越重要的事情. 具有讽刺意味的是, 大多数好的软件工程原则在规模上开始崩溃——特别是在没有对应用程序架构设计给予太多考虑的情况下. 开发人员最容易犯的错误之一就是混合代码关注点, 和 it’s extremely easy to do!

What usually breaks separation of concerns 只是将新功能“倾倒”到现有类中. 这是, 当然, 一个很好的短期解决方案(对于初学者来说), 它需要更少的输入),但它不可避免地成为一个问题, be it during testing, 维护, or somew在这里 in between. 考虑下面的控制器,它返回 TopTalentData from its repository:

@RestController
公共类TopTalentController {

    TopTalentRepository;

    @RequestMapping("/ toptal得到")
    public List getTopTalent() {
        return topTalentRepository.findAll ()
                .流()
                .map(这::entityToData)
                .collect(Collectors.toList ());
    }

    private TopTalentData entityToData(TopTalentEntity TopTalentEntity) {
        返回新的TopTalentData(topTalentEntity.getName ());
    }

}

起初, it might not seem t在这里’s anything particularly wrong with 这 piece of code; it provides a list of TopTalentData which is being retrieved from TopTalentEntity 实例. 然而,仔细看看,我们可以看到实际上有一些事情 TopTalentController is performing 在这里; 名字ly, 它将请求映射到特定端点, retrieving data from a repository, 并转换实体接收到的 TopTalentRepository 成 a different format. 一个“更干净”的解决方案是将这些关注点分离到它们自己的类中. 它可能看起来像这样:

@RestController
@RequestMapping("/toptal")
@AllArgsConstructor
公共类TopTalentController {

    TopTalentService;

    @RequestMapping("/get")
    public List getTopTalent() {
        return topTalentService.getTopTalent();
    }
}

@AllArgsConstructor
@ service
public class TopTalentService {

    TopTalentRepository;
    TopTalentEntityConverter;

    public List getTopTalent() {
        return topTalentRepository.findAll ()
                .流()
                .地图(topTalentEntityConverter:: toResponse)
                .collect(Collectors.toList ());
    }
}

@ component
公共类TopTalentEntityConverter {
    public TopTalentData toResponse(TopTalentEntity) {
        返回新的TopTalentData(topTalentEntity.getName ());
    }
}

这种层次结构的另一个优点是,它允许我们通过检查类名来确定功能驻留的位置. 此外, 在测试期间,如果需要,我们可以很容易地用模拟实现替换任何类.

常见错误#4:不一致和糟糕的错误处理

一致性的主题并不一定是春天(或Java)所独有的, for 那 matter), 但是在开发春天项目时仍然是需要考虑的一个重要方面. 虽然编码风格可以引起争论(通常是团队或整个公司内部达成一致的问题), 有一个共同的标准是一个伟大的生产力援助. 这是 especially true with multi-person teams; consistency allows h和-off to occur without many resources being spent on h和-holding or providing lengthy explanations regarding the responsibilities of different classes

考虑一个带有各种配置文件、服务和控制器的春天项目. Being semantically consistent in naming them creates an easily searchable structure w在这里 any new developer can manage his way around the code; appending Config suffixes to your configuration classes, 服务后缀到你的服务,控制器后缀到你的控制器, 例如.

与一致性的主题密切相关, 服务器端的错误处理值得特别强调. 如果您必须处理来自编写糟糕的API的异常响应, 您可能知道为什么—正确解析异常可能是一件痛苦的事情, 更痛苦的是,首先要确定这些异常发生的原因.

As an API developer, 理想情况下,您希望覆盖所有面向用户的端点,并将它们转换为通用的错误格式. 这通常意味着有一个通用的错误代码和描述,而不是逃避解决方案(a)返回“500内部服务器错误”消息, 或者b)只是将堆栈跟踪转储给用户(实际上应该不惜一切代价避免,因为它除了难以处理客户端之外,还暴露了您的内部)。.

一个常见的错误响应格式的例子可能是:

@ value
public class ErrorResponse {

    private Integer errorCode;
    private String errorMessage;

}

在大多数流行的api中经常会遇到类似的情况, 并且往往工作得很好,因为它可以很容易和系统地记录. 将异常转换为这种格式可以通过提供 @ExceptionH和ler 注释到方法(注释的一个例子见常见错误#6).

常见错误#5:不正确处理多线程

无论它是在桌面应用程序还是web应用程序中遇到的, 春天 or no 春天, 多线程是一个棘手的问题. 实际上,由程序并行执行引起的问题令人伤脑筋,难以捉摸,而且常常极其难以调试, due to the nature of the problem, 一旦您意识到您正在处理并行执行问题,您可能不得不完全放弃调试器并“手工”检查代码,直到找到根本错误原因. 不幸的是, a cookie-cutter solution does not exists for solving such issues; depending on your specific case, 你必须评估情况,然后从你认为最好的角度来解决问题.

当然,理想情况下,您希望完全避免多线程错误. 再一次。, 不存在一种放之四海而皆准的方法, 但这里有一些实际的考虑事项调试和防止多线程错误:

Avoid Global State

首先,永远记住“全局状态”问题. 如果您正在创建一个多线程应用程序, 当然,任何可以全局修改的东西都应该被严密监控, 如果可能的话, removed altogether. 如果有理由必须保持全局变量的可修改性,请谨慎使用 synchronization 并跟踪应用程序的性能,以确认它不会因为新引入的等待期而变得迟钝.

Avoid Mutability

这 one comes straight from functional programming 并且,适用于OOP,声明应该避免类的可变性和改变状态. 这, 简而言之, 意味着前面的setter方法和在所有的模型类上拥有私有的final字段. 它们的值只有在构造期间才会发生变化. 通过这种方式,您可以确定不会出现争用问题,并且访问对象属性将始终提供正确的值.

Log Crucial Data

评估应用程序可能在哪里造成麻烦,并预先记录所有关键数据. If an error occurs, 您将非常感谢收到了哪些请求的信息,并更好地了解应用程序错误的原因. 需要再次注意的是,日志记录引入了额外的文件I/O,因此不应滥用,因为它会严重影响应用程序的性能.

Reuse Existing Implementations

当您需要生成自己的线程时(例如.g. 用于向不同服务发出异步请求), 重用现有的安全实现,而不是创建自己的解决方案. 这在很大程度上意味着利用 ExecutorServices 以及Java 8简洁的函数式风格 CompletableFutures for thread creation. 控件进行异步请求处理 DeferredResult class.

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

让我们想象一下之前的TopTalent服务需要一个端点来添加新的Top talent. 此外, 假设, for some really valid reason, 每个新名字的长度必须是10个字符. 这样做的一种方法可能是:

@RequestMapping("/put")
public void addTopTalent(@RequestBody TopTalentData) {
    boolean 名字NonExistentOrHasInvalidLength =
            可选.ofNullable(topTalentData)
         .map(TopTalentData::getName)
   .map(名字 -> 名字.length() == 10)
   .orElse(真正的);

    if (名字NonExistentOrInvalidLength) {
        // throw some exception
    }

    topTalentService.addTopTalent(topTalentData);
}

然而,以上(除了构造不佳之外)并不是一个真正“干净”的解决方案. 我们正在检查不止一种类型的有效性(即 TopTalentData 不为空, TopTalentData.名字 不为空, TopTalentData.名字 是10个字符长),以及抛出异常,如果数据是无效的.

可以更简洁地执行 Hibernate validator 在春天. Let’s first refactor the addTopTalent method to support validation:

@RequestMapping("/put")
公共无效addTopTalent(@Valid @NotNull @RequestBody TopTalentData TopTalentData) {
    topTalentService.addTopTalent(topTalentData);
}

@ExceptionH和ler
@ResponseStatus(HttpStatus.BAD_REQUEST)
public ErrorResponse h和leInvalidTopTalentDataException(MethodArgumentNotValidException) {
    // h和le validation exception
}

另外,我们必须指出我们想要验证的属性 TopTalentData 类:

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

现在,春天将拦截请求并在调用方法之前对其进行验证——不需要使用额外的手动测试.

另一种实现相同目的的方法是创建我们自己的注释. 尽管您通常只会在需要超过时才使用自定义注释 Hibernate的内置约束集对于本例,让我们假设@Length不存在. 您可以通过创建两个额外的类来创建一个验证器来检查字符串长度, 一个用于验证,另一个用于注释属性:

@Target({ElementType.METHOD, ElementType.FIELD, ElementType.参数})
@Retention(RetentionPolicy.运行时)
@Documented
@Constraint(validatedBy = {MyAnnotationValidator.类})
public @interface MyAnnotation {

    字符串消息()默认"字符串长度不匹配预期";

    Class[] groups() default {};

    Class[] payload() default {};

    int值();

}

@ component
public class MyAnnotationValidator implements ConstraintValidator {

    private int expectedLength;

    @Override
    public void initialize(MyAnnotation) {
        这.expectedLength = myAnnotation.值();
    }

    @Override
    public boolean isValid(String s, ConstraintValidatorContext, ConstraintValidatorContext) {
        return S == null || s.length() == 这.expectedLength;
    }
}

Note 那 in these cases, 关注点分离的最佳实践要求您在属性为null时将其标记为有效(S == nullisValid method), 和 then use a @NotNull 注释(如果这是属性的附加要求):

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

常见错误#7:(仍然)使用基于xml的配置

而XML在以前的春天版本中是必需的, nowadays most of the configuration can be done exclusively via Java code / annotations; XML configurations just pose as additional 和 unnecessary boilerplate code.

本文(以及附带的GitHub存储库)使用注释来配置春天, 春天知道应该连接哪些bean,因为根包已经用一个 @春天Boot应用程序 composite annotation, like so:

@春天Boot应用程序
public class 应用程序 {
    public static void main(String[] args) {
        春天应用程序.run(应用程序.类,args);
    }
}

复合注释(您可以在 春天 documentation 只是给春天一个提示,告诉它应该扫描哪些包来检索bean. 在我们的具体情况下,这意味着下面的顶部(co).使用Kukurin)包进行布线:

  • @ component (TopTalentConverter, MyAnnotationValidator)
  • @RestController (TopTalentController)
  • @ (TopTalentRepository)
  • @ service (TopTalentService)类

If we had any additional @Configuration 带注释的类也会被检查是否有基于java的配置.

常见错误8:忘记个人资料

在服务器开发中经常遇到的一个问题是如何区分不同的配置类型, 通常是你的生产和开发配置. 而不是每次从测试切换到部署应用程序时手动替换各种配置项, 更有效的方法是使用配置文件.

考虑使用内存数据库进行本地开发的情况, 在生产中使用MySQL数据库. 这将, 在本质上, 意味着您将使用不同的URL和(希望)不同的凭据来访问这两个站点. 让我们看看如何在两个不同的配置文件中做到这一点:

应用程序.yaml文件

# set default profile to 'dev'
春天.配置文件.活动:开发

# production database details
春天.数据源.url: jdbc: mysql: / / localhost: 3306 / toptal '
春天.数据源.user名字: root
春天.数据源.密码:

应用程序-dev.yaml文件

春天.数据源.url: 'jdbc:h2:mem:'
春天.数据源.平台:h2

大概您不希望在修改代码时意外地对生产数据库执行任何操作, 因此,将默认配置文件设置为dev是有意义的. 在服务器上,您可以通过提供 -D春天.配置文件.积极=刺激 parameter to the JVM. 或者,您也可以将操作系统的环境变量设置为所需的默认配置文件.

常见错误#9:没有接受依赖注入

Properly using dependency injection 在春天 means allowing it to wire all your objects together by scanning all desired configuration classes; 这 proves to be useful for decoupling relationships 和 also makes testing a whole lot easier. 通过这样做来代替紧耦合类:

公共类TopTalentController {

    TopTalentService;

    public TopTalentController() {
        这.topTalentService = new topTalentService ();
    }
}

我们允许春天为我们做连接:

公共类TopTalentController {

    TopTalentService;

    public TopTalentController(TopTalentService) {
        这.topTalentService = topTalentService;
    }
}

Misko Hevery’s 谷歌 talk 深入解释了依赖注入的“原因”,所以让我们看看它在实践中是如何使用的. 在关注点分离部分(常见错误#3), 我们创建了一个服务和控制器类. 假设我们想在假设下测试控制器 TopTalentService behaves correctly. 我们可以通过提供一个单独的配置类来插入一个模拟对象来代替实际的服务实现:

@Configuration
公共类SampleUnitTestConfig {
    @ bean
    公共TopTalentService ()
        TopTalentService TopTalentService = 5.mock(TopTalentService.类);
        5.when(topTalentService.getTopTalent()).thenReturn (
                流.of("Mary", "Joel").map(TopTalentData::new).collect(Collectors.toList ()));
        return topTalentService;
    }
}

然后我们可以通过告诉春天使用来注入模拟对象 SampleUnitTestConfig as its configuration supplier:

@ContextConfiguration(classes = {SampleUnitTestConfig.类})

这样我们就可以使用上下文配置将定制bean注入到单元测试中.

常见错误10:缺乏测试或不适当的测试

尽管单元测试的概念已经伴随我们很长时间了, 许多开发人员似乎要么“忘记”了这一点(特别是如果事实并非如此的话) 要求),或者只是作为事后的想法添加进去. 这显然是不可取的,因为测试不仅应该验证代码的正确性, 还可以作为应用程序在不同情况下应该如何运行的文档.

When testing web services, 你很少只做“纯粹的”单元测试, 因为通过HTTP进行通信通常需要调用春天的 DispatcherServlet 看看当一个实际的 HttpServletRequest is received (making it an 集成 测试,处理验证,序列化等). 请放心, 用于轻松测试REST服务的Java DSL, on top of MockMVC, 已经证明给出了一个非常优雅的解决方案. 考虑下面的依赖注入代码片段:

@RunWith(春天JUnit4ClassRunner.类)
@ContextConfiguration(classes = {
        应用程序.类,
        SampleUnitTestConfig.class
})
RestAssuredTestDemonstration {

    @ autowired
    private TopTalentController;

    @Test
    公共void shoulgetmary和joel()抛出异常{
        / /给定
        MockMvcRequestSpecification givenRestAssuredSpecification = RestAssuredMockMvc.考虑到()
                .st和aloneSetup (topTalentController);

        / /当
        mockmv响应 = givenRestAssuredSpecification.当().get("/ toptal得到");

        / /然后
        响应.然后().statusCode(200);
        响应.然后().body(名字, hasItems(Mary, Joel));
    }

}

SampleUnitTestConfig wires a mock implementation of TopTalentServiceTopTalentController 而所有其他类都使用从根植于应用程序类包的扫描包中推断出的标准配置来连接. RestAssuredMockMvc 仅用于设置轻量级环境并发送 得到 request to the / toptal得到 端点.

Becoming a 春天 Master

春天是一个功能强大的框架,很容易入门,但需要一些投入和时间才能完全掌握. 从长远来看,花时间熟悉这个框架肯定会提高你的工作效率,并最终帮助你编写更清晰的代码,成为一个更好的开发人员.

如果你在寻找更多的资源, 春天 In Action 是一本涵盖许多春天核心主题的实用书籍吗.

就这一主题咨询作者或专家.
Schedule a call
托尼Kukurin's profile image
托尼Kukurin

位于 毛孔č,克罗地亚

成员自 November 17, 2016

关于 the author

Toni喜欢构建软件解决方案,并运用他的工程技能来解决有趣的现实问题.

Toptal作者都是各自领域经过审查的专家,并撰写他们有经验的主题. 我们所有的内容都经过同行评审,并由同一领域的Toptal专家验证.

专业知识

以前在

谷歌

世界级的文章,每周发一次.

订阅意味着同意我们的 隐私政策

世界级的文章,每周发一次.

订阅意味着同意我们的 隐私政策

Toptal 开发人员

Join the Toptal® 社区.