成为 Spring 大师

  Spring 能够说是最流行的 Java 框架之一,也是一只须要驯服的强大野兽。虽然它的基本概念至关容易掌握,但成为一名强大的 Spring 开发者仍须要不少时间和努力。
  
  在本文中,咱们将介绍 Spring 中一些常见的错误,特别是面向 Web 应用程序和 Spring Boot。正如 Spring Boot 官网 所说,Spring Boot 对应该如何构建 Production-Ready 的应用保持着 至关执拗的观点,本文将尝试模仿这种观点,并提供一些技巧的概述,这些技巧将很好地融入标准 Spring Boot 的 web 应用程序开发中去。
  
  若是你对 Spring Boot 还不是很熟悉,但仍想尝试下接下来提到的一些内容,我也为本文建立了一个 GitHub 仓库。若是你在阅读过程当中感到困惑,我建议把代码 clone 下来,并在本地电脑上使用这些代码。
  
  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 实例可能很诱人,但更灵活的解决方案是建立一个新的类来表示 API 端点上的 TopTalentEntity 数据。
  
  @AllArgsConstructor
  
  @NoArgsConstructor
  
  @Getter
  
  public class TopTalentData {
  
  private String name;
  
  }
  
  这样,对数据库后端进行更改将不须要在服务层进行任何额外的更改。考虑下,在 TopTalentEntity 中添加一个 “password” 字段来存储数据库中用户密码的 Hash 值 —— 若是没有 TopTalentData 之类的链接器,忘记更改服务前端,将会意外地暴露一些没必要要的秘密信息。
  
  3. 常见错误三:缺少关注点分离
  
  随着程序规模的增加,逐渐地,代码组织成为一个愈来愈重要的问题。讽刺的是,大多数好的软件工程原则开始在规模上崩溃 —— 特别是在没有太多考虑程序体系结构设计的状况下。开发人员最常犯的一个错误就是混淆代码关注点,这很容易作到!
  
  一般,打破 关注点分离 的是将新功能简单地 “倒” 在现有类中。固然,这是一个很好的短时间解决方案(对于初学者来讲,它须要更少的输入),但它也不可避免地会在未来成为一个问题,不管是在测试期间、维护期间仍是介于二者之间。考虑下下面的控制器,它将从数据库返回 TopTalentData。
  
  @RestController
  
  public class TopTalentController {
  
  private final TopTalentRepository topTalentRepository;
  
  @RequestMapping("/toptal/get"www.hengxyul.com)
  
  public List<TopTalentData> getTopTalent() {
  
  return topTalentRepository.findAll()
  
  .stream()
  
  .map(this::entityToData)
  
  .collect(Collectors.toList());
  
  }
  
  private TopTalentData entityToData(TopTalentEntity topTalentEntity) {
  
  return new TopTalentData(topTalentEntity.getName(www.hengxyul.com));
  
  }
  
  }
  
  起初,这段代码彷佛没什么特别的问题;它提供了一个从 TopTalentEntity 实例检索出来的 TopTalentData 的 List。然而,仔细观察下,咱们能够看到 TopTalentController 实际上在此作了些事情;也就是说,它将请求映射到特定端点,从数据库检索数据,并将从 TopTalentRepository 接收的实体转换为另外一种格式。一个“更干净” 的解决方案是将这些关注点分离到他们本身的类中。看起来多是这个样子的:
  
  @RestController
  
  @RequestMapping("/toptal")
  
  @AllArgsConstructor
  
  public class TopTalentController {
  
  private final TopTalentService topTalentService;
  
  @RequestMapping(www.ztylegw.cn"/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(www.yisheng3yuL.com)
  
  .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 后缀添加到配置类,服务层以 Service 结尾,以及控制器用 Controller 结尾。
  
  与一致性主题密切相关,服务器端的错误处理值得特别强调。若是你曾经不得不处理编写不好的 API 的异常响应,那你可能知道缘由 —— 正确解析异常会是一件痛苦的事情,而肯定这些异常最初发生的缘由则更为痛苦。
  
  做为一名 API 开发者,理想状况下你但愿覆盖全部面向用户的端点,并将他们转换为常见的错误格式。这一般意味着有一个通用的错误代码和描述,而不是逃避解决问题:a) 返回一个 “500 Internal Server Error”信息。b) 直接返回异常的堆栈信息给用户。(实际上,这些都应该不惜一切代价地去避免,由于除了客户端难以处理之外,它还暴露了你的内部信息)。
  
  例如,常见错误响应格式可能长这样:
  
  @Value
  
  public class ErrorResponse {
  
  private Integer errorCode;
  
  private String errorMessage;
  
  }
  
  与此相似的事情在大多数流行的 API 中也常常遇到,因为能够容易且系统地记录,效果每每很不错。将异常转换为这种格式能够经过向方法提供 @ExceptionHandler 注解来完成(注解案例可见于第六章)。
  
  5. 常见错误五:多线程处理不当
  
  无论是桌面应用仍是 Web 应用,不管是 Spring 仍是 No Spring,多线程都是很难破解的。由并行执行程序所引发的问题是使人不寒而栗且难以捉摸的,并且经常难以调试 —— 实际上,因为问题的本质,一旦你意识到你正在处理一个并行执行问题,你可能就不得不彻底放弃调试器了,并 “手动” 检查代码,直到找到根本上的错误缘由。不幸的是,这类问题并无千篇一概的解决方案;根据具体场景来评估状况,而后从你认为最好的角度来解决问题。
  
  固然,理想状况下,你也但愿彻底避免多线程错误。一样,不存在那种一刀切的方法,但这有一些调试和防止多线程错误的实际考虑因素:
  
  5.1. 避免全局状态
  
  首先,牢记 “全局状态” 问题。若是你正建立一个多线程应用,那么应该密切关注任何可能全局修改的内容,若是可能的话,将他们所有删掉。若是某个全局变量有必须保持可修改的缘由,请仔细使用 synchronization,并对程序性能进行跟踪,以肯定没有由于新引入的等待时间而致使系统性能下降。
  
  5.2. 避免可变性
  
  这点直接来自于 函数式编程,而且适用于 OOP,声明应该避免类和状态的改变。简而言之,这意味着放弃 setter 方法,并在全部模型类上拥有私有的 final 字段。它们的值惟一发生变化的时间是在构造期间。这样,你能够肯定不会出现争用问题,且访问对象属性将始终提供正确的值。
  
  5.3. 记录关键数据
  
  评估你的程序可能会在何处发生异常,并预先记录全部关键数据。若是发生错误,你将很高兴能够获得信息说明收到了哪些请求,并可更好地了解你的应用程序为何会出现错误。须要再次注意的是,日志记录引入了额外的文件 I/O,可能会严重影响应用的性能,所以请不要滥用日志。
  
  5.4. 复用现存实现
  
  每当你须要建立本身的线程时(例如:向不一样的服务发出异步请求),复用现有的安全实现来代替建立本身的解决方案。这在很大程度上意味着要使用 ExecutorServices 和 Java 8 简洁的函数式 CompletableFutures 来建立线程。Spring 还容许经过 DeferredResult 类来进行异步请求处理。
  
  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 validator,数据校验能够更干净地进行。让咱们首先重构 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 时,将其标记为有效(isValid 方法中的 s == null),若是这是属性的附加要求,则使用 @NotNull 注解。
  
  public class TopTalentData {
  
  @MyAnnotation(value = 10)
  
  @NotNull
  
  private String name;
  
  }
  
  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 提示应该扫描哪些包来检索 Bean。在咱们的案例中,这意味着这个顶级包 (co.kukurin)将用于检索:
  
  @Component (TopTalentConverter, MyAnnotationValidator)
  
  @RestController (TopTalentController)
  
  @Repository (TopTalentRepository)
  
  @Service (TopTalentService) 类
  
  若是咱们有任何额外的 @Configuration 注解类,它们也会检查基于 Java 的配置。
  
  8. 常见错误八:忽略 profile
  
  在服务端开发中,常常遇到的一个问题是区分不一样的配置类型,一般是生产配置和开发配置。在每次从测试切换到部署应用程序时,不要手动替换各类配置项,更有效的方法是使用 profile。
  
  考虑这么一种状况:你正在使用内存数据库进行本地开发,而在生产环境中使用 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 是颇有意义的。而后,在服务器上,你能够经过提供 -Dspring.profiles.active=prod 参数给 JVM 来手动覆盖配置文件。另外,还可将操做系统的环境变量设置为所需的默认 profile。
  
  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 talk 深刻解释了依赖注入的 “为何”,因此,让咱们看看它在实践中是如何使用的。在关注点分离(常见错误 #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,一个用于简化测试REST服务的 Java DSL,在 MockMVC 之上,已经被证实提供了一个很是优雅的解决方案。考虑如下带有依赖项注入的代码片断:
  
  @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 只是用来设置一个轻量级环境,并向 /toptal/get 端点发送一个 GET 请求。
  
  11. 成为 Spring 大师
  
  Spring 是一个功能强大的框架,很容易上手,但须要一些投入和时间才能够彻底掌握。长远来看,花时间熟悉框架确定会提升你的生产力,并最终助你写出更干净的代码,成为更好的开发人员。
  
  想寻找更多资源,Spring In Action 是一本涵盖了不少 Spring 核心主题的优秀实战书籍。前端

相关文章
相关标签/搜索