【Springboot】依赖注入方式
【Springboot】依赖注入方式
- 【一】可选方式
- 【1】构造器注入
- 【2】字段注入
- 【3】Setter注入
- 【二】区别
- 【1】构造器注入
- 【2】字段注入
- 【3】Setter注入
- 【三】选型场景建议
- 【1】优先构造器注入(推荐🌟)
- 【2】可选字段注入
- 【四】⚠️ 关键注意事项
- 【1】循环依赖陷阱
- 【2】final字段要求
- 【3】Lombok优化写法
- 【4】总结
- 【五】特殊案例
- 【1】构造器注解
- (1)📋 Lombok构造器注解对比
- (2)🎯 选择指南
- (3)⚠️ 注意事项
- (4)🎖️ 最佳实践
- 【2】xxljob任务中使用@PostConstruct注入线程池
- 【3】使用案例
- (1)@AllArgsConstructor注解构造器
- (2)构造器方法
- (3)@Autowired注解注入
【一】可选方式
在Spring Boot项目中,依赖注入(DI)主要有三种方式:构造器注入、Setter注入和字段注入(通过@Autowired注解直接注入字段)
【1】构造器注入
通过类的构造器来注入依赖,通常在构造器上使用@Autowired注解(Spring 4.3以后,如果类只有一个构造器,可以省略@Autowired)。
@Service
public class MyService {private final RedisTemplate<String, String> redisTemplate;private final RedissonClient redissonClient;public MyService(RedisTemplate<String, String> redisTemplate, RedissonClient redissonClient) {this.redisTemplate = redisTemplate;this.redissonClient = redissonClient;}
}
【2】字段注入
直接在字段上使用@Autowired注解。
@Service
public class MyService {@Autowiredprivate RedisTemplate<String, String> redisTemplate;@Autowiredprivate RedissonClient redissonClient;
}
【3】Setter注入
通过在Setter方法上使用@Autowired注解。
@Service
public class MyService {private RedisTemplate<String, String> redisTemplate;private RedissonClient redissonClient;@Autowiredpublic void setRedisTemplate(RedisTemplate<String, String> redisTemplate) {this.redisTemplate = redisTemplate;}@Autowiredpublic void setRedissonClient(RedissonClient redissonClient) {this.redissonClient = redissonClient;}
}
【二】区别
【1】构造器注入
(1)不可变性:依赖字段可以被声明为final,这意味着一旦对象被创建,这些依赖就不会被改变。这有助于保证依赖的不可变性和线程安全。
(2)强制依赖:构造器注入要求依赖在对象创建时就必须提供,确保对象被完整地初始化。这样,在使用对象时,所有依赖都已经被设置,避免了空指针异常。
(3)循环依赖:如果使用构造器注入,Spring在启动时就能发现循环依赖问题(会抛出BeanCurrentlyInCreationException),而不是等到运行时。
(4)测试友好:在单元测试中,你可以直接通过构造器传入依赖的模拟对象(Mock对象),而不需要Spring容器。
【2】字段注入
(1)简洁:代码简洁,不需要写构造器或Setter方法。
(2)灵活性:但是,字段注入使得依赖可以被随意更改(除非使用反射,但通常不推荐),因此不够安全。
(3)不可变:字段不能声明为final(因为注入是容器在对象创建后通过反射设置的),所以不能保证不可变性。
(4)隐藏依赖:一个类可能有很多依赖,但它们都隐藏在类中,不容易从外部发现。而构造器可以明确地展示一个类需要哪些依赖。
【3】Setter注入
(1)可选依赖:适合那些不是必须的依赖,或者可能需要更换的依赖。Setter注入允许在对象创建后重新注入依赖。
(2)灵活性:可以在对象创建后改变依赖,但通常我们不希望这样,因为这可能导致状态不一致。
(3)不可变性:同样,不能将字段设置为final。
【三】选型场景建议
【1】优先构造器注入(推荐🌟)
(1)适用场景:
强制依赖(如RedisTemplate、线程池等基础设施)
需不可变状态的组件(如Service层、Util工具类)
高安全要求场景(避免NPE)
需要脱离容器进行单元测试的类
(2)示例代码:
@Service
public class OrderService {private final RedisTemplate<String, Object> redisTemplate;private final RedissonClient redissonClient;// 构造器注入(Spring 4.3+可省略@Autowired)public OrderService(RedisTemplate<String, Object> redisTemplate, RedissonClient redissonClient) {this.redisTemplate = redisTemplate;this.redissonClient = redissonClient;}
}
【2】可选字段注入
(1)适用场景:
原型(Prototype)作用域的Bean
可选依赖(如@Autowired(required=false))
遗留代码维护(渐进式改造)
临时调试场景
(2)示例代码:
@Component
public class PaymentProcessor {@Autowired // 字段注入private RedisTemplate<String, Double> redisTemplate;
}
【四】⚠️ 关键注意事项
【1】循环依赖陷阱
构造器注入:强制在启动时暴露循环依赖问题(如A→B→A),推荐提前解耦。
字段注入:可能通过三级缓存延迟解决,但会掩盖设计问题。
【2】final字段要求
构造器注入允许依赖字段定义为final(线程安全+不变性),而字段注入不可用final。
【3】Lombok优化写法
结合@RequiredArgsConstructor简化构造器注入:
@Service
@RequiredArgsConstructor // 为final字段自动生成构造器
public class InventoryService {private final RedissonClient redissonClient;private final ExecutorService bulkTaskExecutor;
}
【4】总结
【五】特殊案例
【1】构造器注解
Lombok提供了几个用于生成构造器的注解,它们在不同场景下使用,主要区别在于生成的构造器的访问权限和包含的字段。以下是常见的几个构造器注解及其区别:
(1)📋 Lombok构造器注解对比
(1)@NoArgsConstructor:生成一个无参构造器。
参数:access(访问权限,如AccessLevel.PUBLIC、AccessLevel.PRIVATE等)、force(如果设置为true,则所有final字段都会被初始化为0/false/null,常用于JPA和Hibernate)等。
(2)@RequiredArgsConstructor:生成一个包含所有必需参数的构造器。必需的参数是指所有final字段和带有@NonNull注解且未初始化的字段。
参数:access(访问权限)、staticName(如果设置,则生成一个静态工厂方法,而不是公共构造器)等。
(3)@AllArgsConstructor:生成一个包含所有非静态字段的构造器(每个字段都是一个参数)。
参数:access(访问权限)、staticName(静态工厂方法)等。
(4)@Builder:使用建造者模式生成一个构造器,实际上会生成一个建造者类和一个私有构造器(通过建造者类来构造对象)。
(5)@Data:这是一个组合注解,包含@Getter、@Setter、@ToString、@EqualsAndHashCode和@RequiredArgsConstructor。
(2)🎯 选择指南
(1)如果你需要一个无参构造器(比如某些框架需要),使用@NoArgsConstructor。
(2)如果你只需要一个包含final字段和@NonNull字段的构造器,使用@RequiredArgsConstructor。这在依赖注入时特别有用,尤其是通过构造器注入时。
(3)如果你需要包含所有字段的构造器,使用@AllArgsConstructor。
(4)如果你想要更灵活的对象构建方式(特别是很多可选参数时),使用@Builder。
注意:如果类中有final字段,且没有使用@NoArgsConstructor并且设置了force=true,那么必须确保这些字段在无参构造器中有默认值(通过force=true可以强制初始化为默认值,但通常不推荐,除非必要如JPA)。
(3)⚠️ 注意事项
(1)字段顺序问题:@AllArgsConstructor的参数顺序与字段声明顺序一致,可能带来隐晦问题
(2)与JPA的配合:JPA实体需要无参构造器,可使用@NoArgsConstructor(access = AccessLevel.PROTECTED)
(3)final字段处理:@NoArgsConstructor需要与force = true配合才能处理final字段
(4)继承问题:父类的构造器不会被Lombok注解影响,需要单独处理
(5)与MapStruct配合:构造器注解可以帮助MapStruct更好地生成映射代码
(4)🎖️ 最佳实践
(1)Spring项目优先使用@RequiredArgsConstructor进行依赖注入
(2)实体类使用@NoArgsConstructor和@AllArgsConstructor满足JPA和业务需求
(3)复杂对象创建使用@Builder提高代码可读性
(4)考虑使用staticName参数创建更有意义的静态工厂方法
【2】xxljob任务中使用@PostConstruct注入线程池
在Spring Boot项目中整合XXL-Job,并在调度任务中使用@PostConstruct注解完成线程池的注入,需要注意以下几点:
(1)XXL-Job的执行器(JobHandler)是由XXL-Job框架通过反射创建的,因此Spring的依赖注入无法直接通过构造函数或字段注入生效。
(2)我们可以通过将JobHandler交由Spring管理,并在JobHandler中使用@PostConstruct注解来初始化线程池或其他依赖。
实现步骤:
(1)在项目中配置XXL-Job执行器。
(2)创建一个JobHandler,并使用@Component注解将其交由Spring管理。
(3)在JobHandler中,使用@PostConstruct注解的方法来初始化线程池。
注意:由于XXL-Job框架会自己实例化JobHandler,所以我们需要在XXL-Job的配置中指定使用Spring容器中的JobHandler Bean,而不是让XXL-Job自己new一个实例。
但是,XXL-Job的默认行为是自行实例化,因此我们需要通过某种方式将Spring容器中的Bean注入到XXL-Job的调度中心。这通常可以通过在XXL-Job的配置中设置JobHandler的Bean名称,并在Spring容器中注册该Bean。
@Component
public class SampleXxlJob {// 线程池实例private ExecutorService asyncTaskExecutor;// 使用@PostConstruct确保Spring容器初始化完成后创建线程池@PostConstructpublic void init() {// 创建自定义线程池asyncTaskExecutor = new ThreadPoolExecutor(5, // 核心线程数10, // 最大线程数60L, // 空闲线程存活时间TimeUnit.SECONDS,new LinkedBlockingQueue<>(100), // 任务队列new ThreadFactoryBuilder().setNameFormat("async-task-%d").build(), // 线程命名new ThreadPoolExecutor.CallerRunsPolicy() // 拒绝策略);log.info("线程池初始化完成,核心线程数:{},最大线程数:{}", 5, 10);}@PreDestroypublic void destroy() {if (asyncTaskExecutor != null) {asyncTaskExecutor.shutdown();log.info("线程池已关闭");}}/*** 示例任务 - 使用线程池处理异步任务*/@XxlJob("demoJobHandler")public void demoJobHandler() throws Exception {XxlJobHelper.log("XXL-JOB开始执行,准备提交任务到线程池");// 创建任务列表List<Callable<String>> tasks = new ArrayList<>();for (int i = 0; i < 20; i++) {final int taskId = i;tasks.add(() -> {try {// 模拟业务处理Thread.sleep(1000);XxlJobHelper.log("任务{}执行完成,线程:{}", taskId, Thread.currentThread().getName());return "Task-" + taskId + "-Success";} catch (InterruptedException e) {Thread.currentThread().interrupt();return "Task-" + taskId + "-Failed";}});}// 提交所有任务到线程池List<Future<String>> futures = asyncTaskExecutor.invokeAll(tasks);// 等待所有任务完成for (Future<String> future : futures) {try {String result = future.get(2, TimeUnit.SECONDS);XxlJobHelper.log("任务结果: {}", result);} catch (TimeoutException e) {XxlJobHelper.log("任务超时");} catch (Exception e) {XxlJobHelper.log("任务执行异常: {}", e.getMessage());}}XxlJobHelper.log("XXL-JOB执行完成,共处理{}个任务", tasks.size());}/*** 另一个示例任务 - 分片广播任务*/@XxlJob("shardingJobHandler")public void shardingJobHandler() throws Exception {// 分片参数int shardIndex = XxlJobHelper.getShardIndex();int shardTotal = XxlJobHelper.getShardTotal();XxlJobHelper.log("分片参数:当前分片序号 = {}, 总分片数 = {}", shardIndex, shardTotal);// 模拟分片数据处理List<String> dataList = fetchDataByShard(shardIndex, shardTotal);// 使用线程池并行处理分片数据List<CompletableFuture<Void>> futures = dataList.stream().map(data -> CompletableFuture.runAsync(() -> {processData(data);}, asyncTaskExecutor)).collect(Collectors.toList());// 等待所有任务完成CompletableFuture.allOf(futures.toArray(new CompletableFuture[0])).join();XxlJobHelper.log("分片任务完成,处理{}条数据", dataList.size());}private List<String> fetchDataByShard(int shardIndex, int shardTotal) {// 模拟根据分片参数获取数据List<String> allData = IntStream.range(0, 100).mapToObj(i -> "Data-" + i).collect(Collectors.toList());// 分片处理return IntStream.range(0, allData.size()).filter(i -> i % shardTotal == shardIndex).mapToObj(allData::get).collect(Collectors.toList());}private void processData(String data) {try {// 模拟数据处理Thread.sleep(500);XxlJobHelper.log("处理数据: {},线程: {}", data, Thread.currentThread().getName());} catch (InterruptedException e) {Thread.currentThread().interrupt();}}
}
【3】使用案例
(1)@AllArgsConstructor注解构造器
package com.allen.study.application.api;import com.allen.study.application.elasticSearch.ElasticsearchTemplateUtil;
import com.allen.study.application.elasticSearch.es_entity.ProductES;
import com.allen.study.common.base.ApiResponse;
import com.allen.study.common.utils.CamundaUtils;
import com.allen.study.common.utils.ThreadPoolConfig;
import com.allen.study.common.utils.datax.DataXUtils;
import com.allen.study.common.utils.kafka.KafkaUtils;
import com.allen.study.common.utils.redis.*;
import com.allen.study.common.utils.uuid.IdUtils;
import com.allen.study.domain.repository.IEmployeeInfoRepo;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.AllArgsConstructor;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import java.util.List;/*** 用户信息表测试对象注入接口** @author AllenSun* @since 2025-02-26 23:58*/
@RequestMapping(value = "/4.2.0/testdi")
@RestController
@AllArgsConstructor
@Tag(name = "用户信息表测试对象注入接口", description = "用户信息表测试对象注入接口")
@Slf4j
public class EmployeeInfoTestDIApi {// 布隆过滤器private final BloomFilterUtil bloomFilterUtil;private final RedisTemplate redisTemplate;private final RedisLock redisLock;private final RedisUtils redisUtils;private final RedisUtils2 redisUtils2;private final RedissonConfig redissonConfig;private final ElasticsearchTemplateUtil elasticsearchTemplateUtil;private final ElasticsearchRestTemplate elasticsearchRestTemplate;private final DataXUtils dataXUtils;private final KafkaUtils kafkaUtils;private final ThreadPoolConfig threadPoolConfig;private final IEmployeeInfoRepo employeeInfoRepo;@GetMapping("")public ApiResponse<String> batchInitEmps() {boolean mightContain = bloomFilterUtil.mightContain("1111111");List<String> queryAllIdList = employeeInfoRepo.queryAllIdList();Object redisVal01 = redisTemplate.opsForValue().get("111");Object redisVal02 = redisUtils.get("111");Object redisVal03 = redisUtils2.get("111");RLock rLock01 = redisLock.getRLock("lockKey01");RLock rLock02 = redissonConfig.redissonClient().getLock("lockKey02");long documentCount = elasticsearchTemplateUtil.getDocumentCount(ProductES.class);int hashCode = elasticsearchRestTemplate.hashCode();int i = dataXUtils.hashCode();int i1 = kafkaUtils.hashCode();boolean processDefinitionExists = CamundaUtils.isProcessDefinitionExists("111");String uuid = IdUtils.randomUUID();int poolSize = threadPoolConfig.customThreadPool().getPoolSize();return ApiResponse.ok();}}
(2)构造器方法
package com.allen.study.application.api;import com.allen.study.application.elasticSearch.ElasticsearchTemplateUtil;
import com.allen.study.application.elasticSearch.es_entity.ProductES;
import com.allen.study.common.base.ApiResponse;
import com.allen.study.common.utils.CamundaUtils;
import com.allen.study.common.utils.ThreadPoolConfig;
import com.allen.study.common.utils.datax.DataXUtils;
import com.allen.study.common.utils.kafka.KafkaUtils;
import com.allen.study.common.utils.redis.*;
import com.allen.study.common.utils.uuid.IdUtils;
import com.allen.study.domain.repository.IEmployeeInfoRepo;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import java.util.List;/*** 用户信息表测试对象注入接口** @author AllenSun* @since 2025-02-26 23:58*/
@RequestMapping(value = "/4.2.0/testdi")
@RestController
// @AllArgsConstructor
@Tag(name = "用户信息表测试对象注入接口", description = "用户信息表测试对象注入接口")
@Slf4j
public class EmployeeInfoTestDIApi {private final BloomFilterUtil bloomFilterUtil;private final RedisTemplate redisTemplate;private final RedisLock redisLock;private final RedisUtils redisUtils;private final RedisUtils2 redisUtils2;private final RedissonConfig redissonConfig;private final ElasticsearchTemplateUtil elasticsearchTemplateUtil;private final ElasticsearchRestTemplate elasticsearchRestTemplate;private final DataXUtils dataXUtils;private final KafkaUtils kafkaUtils;private final ThreadPoolConfig threadPoolConfig;private final IEmployeeInfoRepo employeeInfoRepo;public EmployeeInfoTestDIApi(BloomFilterUtil bloomFilterUtil, RedisTemplate redisTemplate, RedisLock redisLock, RedisUtils redisUtils, RedisUtils2 redisUtils2, RedissonConfig redissonConfig, ElasticsearchTemplateUtil elasticsearchTemplateUtil, ElasticsearchRestTemplate elasticsearchRestTemplate, DataXUtils dataXUtils, KafkaUtils kafkaUtils, ThreadPoolConfig threadPoolConfig, IEmployeeInfoRepo employeeInfoRepo) {this.bloomFilterUtil = bloomFilterUtil;this.redisTemplate = redisTemplate;this.redisLock = redisLock;this.redisUtils = redisUtils;this.redisUtils2 = redisUtils2;this.redissonConfig = redissonConfig;this.elasticsearchTemplateUtil = elasticsearchTemplateUtil;this.elasticsearchRestTemplate = elasticsearchRestTemplate;this.dataXUtils = dataXUtils;this.kafkaUtils = kafkaUtils;this.threadPoolConfig = threadPoolConfig;this.employeeInfoRepo = employeeInfoRepo;}@GetMapping("")public ApiResponse<String> batchInitEmps() {boolean mightContain = bloomFilterUtil.mightContain("1111111");List<String> queryAllIdList = employeeInfoRepo.queryAllIdList();Object redisVal01 = redisTemplate.opsForValue().get("111");Object redisVal02 = redisUtils.get("111");Object redisVal03 = redisUtils2.get("111");RLock rLock01 = redisLock.getRLock("lockKey01");RLock rLock02 = redissonConfig.redissonClient().getLock("lockKey02");long documentCount = elasticsearchTemplateUtil.getDocumentCount(ProductES.class);int hashCode = elasticsearchRestTemplate.hashCode();int i = dataXUtils.hashCode();int i1 = kafkaUtils.hashCode();boolean processDefinitionExists = CamundaUtils.isProcessDefinitionExists("111");String uuid = IdUtils.randomUUID();int poolSize = threadPoolConfig.customThreadPool().getPoolSize();return ApiResponse.ok();}}
(3)@Autowired注解注入
package com.allen.study.application.api;import com.allen.study.application.elasticSearch.ElasticsearchTemplateUtil;
import com.allen.study.application.elasticSearch.es_entity.ProductES;
import com.allen.study.common.base.ApiResponse;
import com.allen.study.common.utils.CamundaUtils;
import com.allen.study.common.utils.ThreadPoolConfig;
import com.allen.study.common.utils.datax.DataXUtils;
import com.allen.study.common.utils.kafka.KafkaUtils;
import com.allen.study.common.utils.redis.*;
import com.allen.study.common.utils.uuid.IdUtils;
import com.allen.study.domain.repository.IEmployeeInfoRepo;
import io.swagger.v3.oas.annotations.tags.Tag;
import lombok.extern.slf4j.Slf4j;
import org.redisson.api.RLock;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.data.elasticsearch.core.ElasticsearchRestTemplate;
import org.springframework.data.redis.core.RedisTemplate;
import org.springframework.web.bind.annotation.GetMapping;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.bind.annotation.RestController;import java.util.List;/*** 用户信息表测试对象注入接口** @author AllenSun* @since 2025-02-26 23:58*/
@RequestMapping(value = "/4.2.0/testdi")
@RestController
// @AllArgsConstructor
@Tag(name = "用户信息表测试对象注入接口", description = "用户信息表测试对象注入接口")
@Slf4j
public class EmployeeInfoTestDIApi {@Autowiredprivate BloomFilterUtil bloomFilterUtil;@Autowiredprivate RedisTemplate redisTemplate;@Autowiredprivate RedisLock redisLock;@Autowiredprivate RedisUtils redisUtils;@Autowiredprivate RedisUtils2 redisUtils2;@Autowiredprivate RedissonConfig redissonConfig;@Autowiredprivate ElasticsearchTemplateUtil elasticsearchTemplateUtil;@Autowiredprivate ElasticsearchRestTemplate elasticsearchRestTemplate;@Autowiredprivate DataXUtils dataXUtils;@Autowiredprivate KafkaUtils kafkaUtils;@Autowiredprivate ThreadPoolConfig threadPoolConfig;@Autowiredprivate IEmployeeInfoRepo employeeInfoRepo;@GetMapping("")public ApiResponse<String> batchInitEmps() {boolean mightContain = bloomFilterUtil.mightContain("1111111");List<String> queryAllIdList = employeeInfoRepo.queryAllIdList();Object redisVal01 = redisTemplate.opsForValue().get("111");Object redisVal02 = redisUtils.get("111");Object redisVal03 = redisUtils2.get("111");RLock rLock01 = redisLock.getRLock("lockKey01");RLock rLock02 = redissonConfig.redissonClient().getLock("lockKey02");long documentCount = elasticsearchTemplateUtil.getDocumentCount(ProductES.class);int hashCode = elasticsearchRestTemplate.hashCode();int i = dataXUtils.hashCode();int i1 = kafkaUtils.hashCode();boolean processDefinitionExists = CamundaUtils.isProcessDefinitionExists("111");String uuid = IdUtils.randomUUID();int poolSize = threadPoolConfig.customThreadPool().getPoolSize();return ApiResponse.ok();}
}