写SpringBoot时值得注意的8件小事

业余草

共 7769字,需浏览 16分钟

 ·

2024-04-10 17:03

来源: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}.propertiesapplication-{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<ErrorResponsehandleResourceNotFoundException(ResourceNotFoundException ex
{
        ErrorResponse errorResponse = new ErrorResponse(HttpStatus.NOT_FOUND.value(), ex.getMessage());
        return new ResponseEntity<>(errorResponse, HttpStatus.NOT_FOUND);
    }

    @ExceptionHandler(Exception.class)
    public ResponseEntity<ErrorResponsehandleGenericException(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件,在开发过程中只有不断抠细节,才能逐步提升能力,不积跬步无以至千里,不积小流难以成江海。

前路漫漫,愿你在这段奋斗的旅程中取得不断的成长和成功。

浏览 18
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报
评论
图片
表情
推荐
点赞
评论
收藏
分享

手机扫一扫分享

分享
举报