Spring Boot 开发中有七件事,你必须知道

开发 前端
Controller控制器仅负责处理HTTP请求和响应。业务逻辑应放置在其他层(如Service层)。将业务逻辑与请求和响应处理混合在一起对编写单元测试非常不利。如果将业务逻辑移动到服务层,那么单元测试可以更加针对服务层进行。

环境:SpringBoot3.2.5

1. 简介

这篇文章将逐一探讨在SpringBoot开发中容易被忽视的7个关键细节,从而避免开发过程中的陷阱。

无论是你是初学者还是有经验的开发者,关注这些小细节往往能够预防许多常见问题,同时提高开发效率,减少开发过程中的重复工作,甚至可能提升所开发产品的质量。

2. 核心关键点

2.1 字段避免@Autowired注入

@Autowired可以将依赖注入到组件中,但过度使用它可能会导致紧密的耦合和测试困难。使用构造器注入或@Resource等方法可以使依赖关系更加清晰。

推荐做法:

优先使用构造器注入,因为它可以清晰地定义组件的依赖,并且在单元测试中更容易进行模拟(mock)。

如果你当前使用了Lombok,你可以利用@RequiredArgsConstructor注解来自动生成构造器。

private final UserRepository userRepository ;
public UserService(UserRepository userRepository) {
  this.userRepository = userRepository ;
}

我们是禁止使用Lombok的。

最后,我还是推荐使用构造函数注入,避免字段上使用@Autowired / @Resource注解,并且Spring官方推荐的也是构造函数注入。

2.2 避免在控制器中编写业务逻辑

严格来说,Controller控制器仅负责处理HTTP请求和响应。业务逻辑应放置在其他层(如Service层)。将业务逻辑与请求和响应处理混合在一起对编写单元测试非常不利。如果将业务逻辑移动到服务层,那么单元测试可以更加针对服务层进行。

推荐做法:

将业务逻辑移动到服务层(Service),并让控制器仅处理请求并调用服务方法。进行这种分离后,不仅单元测试更加方便,代码也更容易重用。

@RestController
@RequestMapping("/products")
public class ProductController {


  private final ProductService productService ;
  public ProductController(ProductService productService) {
    this.productService = productService ;
  }


  @GetMapping("/{id}")
  public ResponseEntity<Product> getProduct(@PathVariable Long id) {
    // 调用Service进行业务逻辑的处理
    Product product = productService.getProductById(id) ;
    return ResponseEntity.ok(product) ;
  }
}

这也是我们日常开发中最基本的要求了。

2.3 使用@ConfigurationProperties替代@Value

使用@Value注解来获取配置虽然简单,但缺乏结构性。此外,过度使用会导致@Value注解散布在整个项目中,这不利于代码的维护和重用。使用@ConfigurationProperties可以避免这些问题,使配置更清晰、更易于管理。

推荐做法:

创建一个专用的配置类,并使用@ConfigurationProperties注解来绑定相关的配置项,这增强了代码的可读性。当在多个地方使用相同的配置类时,它有助于避免重复配置属性,从而提高了代码的可重用性。这种方法还使配置更具结构性,便于维护和理解。

例如,在应用配置的情况下,当处理大量属性或复杂配置结构时,@ConfigurationProperties所提供的便利性和长期影响远远超过了创建一个新类所需的工作量。

@ConfigurationProperties(prefix = "pack.app")
public class AppConfig {
  private String title ;
  private String version ;
  private Integer uid ;
  // getters and setters
}

2.4 避免构造函数过于复杂

构造器应尽可能保持简单。做过多的初始化工作会使构造器变得复杂且难以理解。此外,如果构造器中做了太多工作,未来的需求变更很可能需要频繁修改,从而增加了代码维护的难度。它还显著影响性能,因为在对象创建期间执行了复杂操作。

推荐做法:

主要使用构造器进行依赖注入,并将初始化工作移动到用@PostConstruct注解的方法中或在服务方法内执行。如果必须在构造器中执行大量操作,考虑实现延迟加载或将其转换为工厂模式。

public class CommonComponent {
  private final CommonService commonService ;


  public CommonComponent(CommonService commonService) {
    this.commonService = commonService ;
  }


  @PostConstruct
  public void init() {
    // TODO
  }
}

构造器只做基本的注入操作。其它初始化的工作通过@PostConstruct注解的方法来处理。

2.5 定义不同的环境配置文件

为不同环境(开发、测试、生产)使用不同的配置有助于隔离环境差异。

推荐做法:

使用application-{profile}.properties或application-{profile}.yml来为每个环境定义配置。

激活不同的配置文件:

spring:
  profiles:
    active:
    - dev

2.6 使用异常替代返回值

首先,先来看段代码:

@Service
public class ProductService {


  private final ProductRepository productRepository ;
  public ProductService(ProductRepository productRepository) {
    this.productRepository = productRepository ;
  }


  public R<Product, String> queryById(Long id) {
    Optional<Product> opt = productRepository.findById(id) ;
    if (opt.isPresent()) {
      return R.success(opt.get()) ;
    } else {
      return R.error("商品不存在id: " + id) ;
    }
  }
}

上面代码直接使用R作为方法的返回值显得不够优雅。

如果将返回R.error的部分替换为抛出new XXException异常,这不仅能提高代码的可读性,还能让服务返回业务结果,而不是与控制器结果纠缠在一起。

优化代码:

@Service
public class ProductService {


  private final ProductRepository productRepository ;
  public ProductService(ProductRepository productRepository) {
    this.productRepository = productRepository ;
  }


  public Product queryById(Long id) {
    return productRepository.findById(id)
      .orElseThrow(() -> new ProductNotFoundException("商品不存在id: " + id));
  }
}

Service层应该只返回业务结果,而不应涉及控制器的结果。此外,抛出的异常可以让维护人员立即理解,并指出问题所在。

最后,我们利用@RestControllerAdvice注解来进行全局异常处理,以便及时捕获业务逻辑中抛出的异常,避免500错误,如下示例:

@RestControllerAdvice
public class GlobalExceptionHandler {


  @ExceptionHandler(ProductNotFoundException.class)
  public ResponseEntity<ErrorResponse> handleProductNotFoundException(ProductNotFoundException ex) {
    ErrorResponse errorResponse = new ErrorResponse(HttpStatus.NOT_FOUND.value(), ex.getMessage()) ;
    return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND) ;
  }
  // 其它异常处理句柄
}

有人可能会说这种抛异常处理方式是反模式设计,你觉得呢?

2.7 优先考虑ResponseEntity作为响应

很多人会自定义对象作为Controller接口返回的统一对象,但SpringBoot本身提供了一个专门的响应实体,即ResponseEntity。

ResponseEntity提供了更大的灵活性,允许控制响应的各个方面,包括HTTP状态码、响应头、响应体等。这使得程序能够更精确地构建响应结果,根据业务需求返回不同的HTTP状态码。

此外,ResponseEntity还支持泛型,允许返回不同类型的响应体,满足各种业务场景下的响应需求。

下面是ResponseEntity API文档说明:

图片图片

这点并非必须遵守,当你确实需要高度定制化,那么使用自定义的结果对象也当然没有问题。

责任编辑:武晓燕 来源: Spring全家桶实战案例源码
相关推荐

2016-12-01 14:54:57

2016-11-21 11:50:37

2012-02-08 09:44:05

ChromeAndroid

2010-07-28 14:21:43

Flex

2015-01-20 11:24:52

Win 10

2015-05-29 09:45:42

Google IOA

2017-04-26 16:24:49

路由器5GHz频段

2017-07-04 08:59:35

2020-02-28 14:05:00

Linuxshell命令

2017-12-07 15:47:25

2020-07-09 07:34:40

开发Web工具

2017-12-07 15:28:36

2012-09-29 09:22:24

.NETGC内存分配

2012-09-29 10:29:56

.Net内存分配继承

2018-08-01 22:14:23

Kubernetes容器云迁移

2015-04-09 09:53:08

CA TechnoloDevOps

2010-04-12 14:58:56

Meego开发

2011-11-30 09:09:13

王涛Windows Pho移动开发

2015-06-29 09:40:10

Rails新特性
点赞
收藏

51CTO技术栈公众号