写SpringBoot时值得注意的8件小事
来源:juejin.cn/post/7266612337192599571
推荐:https ://t.zsxq.com/16PJX2MKf
前言
这篇文章将逐一探讨在SpringBoot开发中容易被忽视的十个关键小事,从而在开发过程中不再走入陷阱。
无论是新手还是有经验的开发者,通过注意这些小事,往往能够避免不少常见问题,同时在开发过程中提高效率,减少重复劳动,兴许还能提高研发产品的质量。
耐心看完,你一定有所收获。
避免滥用@Autowired
为什么
@Autowired
可以将依赖注入到组件中,但滥用它可能导致代码的紧耦合和难以测试。使用构造函数注入或@Resource
等方式可以使依赖关系更明确。
其实使用IDEA可以很直观的注意到这个问题,毕竟它已经给出了警告和提示,任何有代码强迫症的选手都应该干掉这条可恶的波浪线!
建议:
优先使用构造函数注入,它明确了组件的依赖关系,而且在单元测试中更易于模拟。
如果用了Lombok,那可以使用@RequiredArgsConstructor
注解来自动生成构造函数。
避免在Controller中写业务逻辑
为什么
严格来说,控制器只负责处理HTTP请求和响应,业务逻辑应该放在其他层(如Service层)中进行。
如果业务和请求响应混在一起,非常不利于单元测试的编写。如果把业务放到Service里,那单元测试可以针对性地去测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);
}
}
使用@ConfigurationProperties
多使用@ConfigurationProperties而非@Value。
为什么:
使用@Value
注解获取配置虽然简单,但是缺乏结构,而且用的一旦多了,会导致项目里到处都是@Value
,不利于代码维护和重用。
使用@ConfigurationProperties
可以避免上述问题,使配置更清晰且易于管理。
建议:
创建一个专门的配置类,使用@ConfigurationProperties
注解来绑定相关配置项,提高代码的可读性。
在多个地方使用同一个配置类时,可以避免重复配置属性,提高代码的重用性。
同时使配置更加结构化,便于维护和理解。
比如下面这个App的配置,当需要处理大量属性或复杂配置结构时,它提供的便利性和长远影响远大于建个类所花费的时间。
@ConfigurationProperties(prefix = "app")
public class AppConfig {
private String name;
private int timeout;
// getter 和 setter 方法
}
避免在构造函数中做过多工作:
为什么
构造函数应该尽量保持简洁,做过多的初始化工作可能导致构造函数变得复杂难以理解。
并且一旦在构造函数中做了太多工作,将来的需求变更必然会导致其被频繁修改,反而增加了代码维护的难度。
同时也相当影响性能,毕竟要在对象创建时执行复杂操作。
建议
构造函数主要用于依赖注入,将初始化工作移到@PostConstruct
注解的方法中或者在服务方法中进行。
如果确实需要在构造函数中执行大量操作,那可以考虑延迟加载,或者改造成工厂模式。
延迟加载的示例如下,这种做法也许并不常见,但不失为一种方式:
public class LazyInitializationExample {
private final ExpensiveResource expensiveResource;
public LazyInitializationExample() {
// 不在构造函数中初始化
this.expensiveResource = null;
}
public ExpensiveResource getExpensiveResource() {
if (expensiveResource == null) {
// 在需要时才进行初始化
this.expensiveResource = new ExpensiveResource();
}
return expensiveResource;
}
}
使用Profiles配置不同环境
为什么:
在不同的环境(开发、测试、生产)中使用不同的配置有助于隔离环境差异。
建议:
使用application-{profile}.properties
或application-{profile}.yml
来定义各个环境的配置。
使用Lombok
使用Lombok可以减少样板代码,使代码更加简洁,提高可读性。
而且Lombok提供了许多高级的功能,比如@Builder
注解用于构建复杂的对象、@AllArgsConstructor
注解生成全参构造函数等。
把时间都利用在业务上,减少重复代码,早点摸鱼下班岂不是更爽。
当然这点的争议比较大,一个原因是因为Lombok会污染整个项目,一旦用了这个工具,那么团队中的所有人都不可避免的要去安装Lombok插件。另一个原因是过度使用反而会降低代码可读性,在部分场景下也不够灵活。
总是得有个取舍。
抛出异常而非返回Result
先看这段代码:
@Service
public class ProductService {
@Autowired
private ProductRepository productRepository;
public Result<Product, String> getProductById(Long id) {
Optional<Product> optionalProduct = productRepository.findById(id);
if (optionalProduct.isPresent()) {
return Result.ok(optionalProduct.get());
} else {
return Result.error("Product not found with id: " + id);
}
}
}
是不是闻到一股不好的味道,为了在service中能够判断查询结果,直接把Result作为方法返回值,太不优雅。
如果把 return Result.error
这段代码换成 throw new XXException
,一个是可读性变强,另一个是可以让service返回业务的结果,而且控制器的结果。
再看优化后的代码:
@Service
public class ProductService {
@Autowired
private ProductRepository productRepository;
public Product getProductById(Long id) {
return productRepository.findById(id)
.orElseThrow(() -> new NotFoundException("Product not found with id: " + id));
}
}
在其位,谋其政。
service就只返回业务结果,不应该与控制器结果有牵扯。
同时抛出的异常,能让维护人员一眼就能看出端倪,这里不能为空。
当然实际开发时的业务代码不会这样简短,这里为了偷懒直接用lambda语法做精简。
值得注意的是,务必使用@ControllerAdvice
注解,利用SpringBoot的全局异常处理机制及时捕获业务中抛出的异常,避免500错误,这里提供简单的示例:
@ControllerAdvice
public class GlobalExceptionHandler {
@ExceptionHandler(ResourceNotFoundException.class)
public ResponseEntity<ErrorResponse> handleResourceNotFoundException(ResourceNotFoundException ex) {
ErrorResponse errorResponse = new ErrorResponse(HttpStatus.NOT_FOUND.value(), ex.getMessage());
return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND);
}
@ExceptionHandler(Exception.class)
public ResponseEntity<ErrorResponse> handleGenericException(Exception ex) {
ErrorResponse errorResponse = new ErrorResponse(HttpStatus.INTERNAL_SERVER_ERROR.value(), "An internal error occurred.");
return new ResponseEntity<>(errorResponse, HttpStatus.INTERNAL_SERVER_ERROR);
}
}
使用ResponseEntity
使用自带的响应结果类ResponseEntity。
为什么
很多人会封装一个Result类作为Controller的实体,其实SpringBoot本身就有一个专用的响应实体叫做ResponseEntity
。
ResponseEntity
提供了更大的灵活性,能够控制响应的各个方面,包括HTTP状态码、响应头、响应体等,使得程序能够更精确地构建响应结果,根据业务需求返回不同的HTTP状态码等。
而且ResponseEntity
也支持范型,能够返回不同类型的响应体,使得你能够处理不同业务场景的响应需求。
建议
ResponseEntity
值得一试,毕竟SpringBoot源码里也说了:
当然如果系统具有高度定制化的结构化响应结果,创建自己的Result类也无可厚非。
结尾
其实值得注意的事远不止这8件,在开发过程中只有不断抠细节,才能逐步提升能力,不积跬步无以至千里,不积小流难以成江海。
前路漫漫,愿你在这段奋斗的旅程中取得不断的成长和成功。