环境: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文档说明:
图片
这点并非必须遵守,当你确实需要高度定制化,那么使用自定义的结果对象也当然没有问题。