SpringBoot 参数无法自动注入Map、Collection、数组问题

本文探讨了SpringBoot在1.5.17版本中遇到的Map、Collection、数组参数无法自动注入的问题,分析了原因并指出在SpringBoot 2.3.1中此问题得到解决。问题关键在于类型匹配和Qualifier注解的顺序,以及Spring的实现逻辑差异。解决方案是使用@Qualifier注解指定bean名称,尤其在泛型类型不明确时。

SpringBoot 参数无法自动注入Map、Collection、数组问题

 

问题描述:

    @Bean
    @ConditionalOnMissingBean(CpfDataSourceLookup.class)
    public CpfDataSourceLookup cpfDataSourceLookup(@Qualifier("backupDataSources") Map<String,Datasource> backupDataSources){
        return new CpfDataSourceLookup(backupDataSources);
    }
<util:map id="backupDataSources">
	</util:map>

这里我希望匹配一个Map类型的参数,并且我指定了Qualifier的名称,所以按道理来说是应该要自动注入的,但实际发现在注入时报找不到该类型。

springboot版本是1.5.17,同时我有个springboot2.3.1的工程,跑了一下发现是可以的。

这时候就来看下spring代码实现,代码位于DefaultListableBeanFactory.findAutowireCandidates方法

        String[] candidateNames = BeanFactoryUtils.beanNamesForTypeIncludingAncestors(
				this, requiredType, true, descriptor.isEager());//取出容器中所需类型所有bean名称(这里是Map类型所有bean)
		Map<String, Object> result = new LinkedHashMap<String, Object>(candidateNames.length);
		for (Class<?> autowiringType : this.resolvableDependencies.keySet()) {
			if (autowiringType.isAssignableFrom(requiredType)) {
				Object autowiringValue = this.resolvableDependencies.get(autowiringType);
				autowiringValue = AutowireUtils.resolveAutowiringValue(autowiringValue, requiredType);
				if (requiredType.isInstance(autowiringValue)) {
					result.put(ObjectUtils.identityToString(autowiringValue), autowiringValue);
					break;
				}
			}
		}
        //逐一匹配容器中每个Map类型的Bean是否匹配所需类型
		for (String candidate : candidateNames) {
			if (!isSelfReference(beanName, candidate) && isAutowireCandidate(candidate, descriptor)) {
				addCandidateEntry(result, candidate, descriptor, requiredType);
			}
		}
        //还没匹配上并且不为数组、Collection、Map类型时进入
		if (result.isEmpty() && !indicatesMultipleBeans(requiredType)) {
			// Consider fallback matches if the first pass failed to find anything...
			DependencyDescriptor fallbackDescriptor = descriptor.forFallbackMatch();
			for (String candidate : candidateNames) {
				if (!isSelfReference(beanName, candidate) && isAutowireCandidate(candidate, fallbackDescriptor)) {
					addCandidateEntry(result, candidate, descriptor, requiredType);
				}
			}
            //没匹配上检查是否是自身bean引用
			if (result.isEmpty()) {
				// Consider self references as a final pass...
				// but in the case of a dependency collection, not the very same bean itself.
				for (String candidate : candidateNames) {
					if (isSelfReference(beanName, candidate) &&
							(!(descriptor instanceof MultiElementDescriptor) || !beanName.equals(candidate)) &&
							isAutowireCandidate(candidate, fallbackDescriptor)) {
						addCandidateEntry(result, candidate, descriptor, requiredType);
					}
				}
			}
		}
进入到第一次匹配的isAutowireCandidate方法中,为省篇幅直接进入最终判断方法中

最终会执行到
if (checkGenerics) {
    ResolvableType[] ourGenerics = this.getGenerics();//取出所需注入的Map范形[String,Datasource]
    ResolvableType[] typeGenerics = other.as(ourResolved).getGenerics();//取出容器中Map的范形[?,?]
    //比较元素数量是否一致
    if (ourGenerics.length != typeGenerics.length) {
        return false;
    }
    if (matchedBefore == null) {
        matchedBefore = new IdentityHashMap(1);
    }

    ((Map)matchedBefore).put(this.type, other.type);
    //逐一比较参数类型,注入的Map中的每个范形类是否为容器中对应位置范形类的父类(String和Datasource是否都为?,?的父类)
    for(int i = 0; i < ourGenerics.length; ++i) {
        if (!ourGenerics[i].isAssignableFrom(typeGenerics[i], (Map)matchedBefore)) {
            return false;
        }
    }
}

从这里可以看出,因为在xml中声明的Map没有范形,而引用参数上声明了范形,而显然这代表不能进行向下转型

public boolean isAutowireCandidate(BeanDefinitionHolder bdHolder, DependencyDescriptor descriptor) {
		boolean match = super.isAutowireCandidate(bdHolder, descriptor);
		if (match && descriptor != null) {
			match = checkQualifiers(bdHolder, descriptor.getAnnotations());
			if (match) {
				MethodParameter methodParam = descriptor.getMethodParameter();
				if (methodParam != null) {
					Method method = methodParam.getMethod();
					if (method == null || void.class == method.getReturnType()) {
						match = checkQualifiers(bdHolder, methodParam.getMethodAnnotations());
					}
				}
			}
		}
		return match;
	}

在匹配类型结束后,只有当类型匹配完成后,在匹配Qualifier注解,也就是说类型要是匹配不上,qualifier不生效

到这实际上问题已经可以解决:即注入参数Map加上@Qualifier注解但不要加范形,注意范型不明确时最好要加Qualifier匹配名称,因为在不匹配Qualifier时会列举出容器中所有Map,然后依次按照:使用@Primary指定@Priority优先级最高参数名称匹配三种方式确定最终注入对象。

到这springboot2和1的实现逻辑是一样的,也就是都不匹配,而为什么2最终能匹配上?

继续看下面的,发现有个差异

springboot1版本

.......
//还没匹配上并且不为数组、Collection、Map类型时进入
if (result.isEmpty() && !indicatesMultipleBeans(requiredType)) {
	// Consider fallback matches if the first pass failed to find anything...
	DependencyDescriptor fallbackDescriptor = descriptor.forFallbackMatch();
	for (String candidate : candidateNames) {
		if (!isSelfReference(beanName, candidate) && isAutowireCandidate(candidatefallbackDescriptor)) {
			addCandidateEntry(result, candidate, descriptor, requiredType);
		}
	}
          //没匹配上检查是否是自身bean引用
	if (result.isEmpty()) {
		// Consider self references as a final pass...
		// but in the case of a dependency collection, not the very same bean itself.
		for (String candidate : candidateNames) {
			if (isSelfReference(beanName, candidate) &&
					(!(descriptor instanceof MultiElementDescriptor) |!beanName.equals(candidate)) &&
					isAutowireCandidate(candidate, fallbackDescriptor)) {
				addCandidateEntry(result, candidate, descriptor, requiredType);
			}
		}
	}
}

springboot2

if (result.isEmpty()) {
boolean multiple = indicatesMultipleBeans(requiredType);
// Consider fallback matches if the first pass failed to find anything...
DependencyDescriptor fallbackDescriptor = descriptor.forFallbackMatch();
for (String candidate : candidateNames) {
	if (!isSelfReference(beanName, candidate) && isAutowireCandidate(candidate, fallbackDescriptor) &&
			(!multiple || getAutowireCandidateResolver().hasQualifier(descriptor))) {
		addCandidateEntry(result, candidate, descriptor, requiredType);
	}
}
if (result.isEmpty() && !multiple) {
	// Consider self references as a final pass...
	// but in the case of a dependency collection, not the very same bean itself.
	for (String candidate : candidateNames) {
		if (isSelfReference(beanName, candidate) &&
				(!(descriptor instanceof MultiElementDescriptor) || !beanName.equals(candidate)) &&
				isAutowireCandidate(candidate, fallbackDescriptor)) {
			addCandidateEntry(result, candidate, descriptor, requiredType);
		}
	}
}
private boolean indicatesMultipleBeans(Class<?> type) {
		return (type.isArray() || (type.isInterface() &&
				(Collection.class.isAssignableFrom(type) || Map.class.isAssignableFrom(type))));
	}

可以看出1中类型匹配失败后,后续匹配不支持数组、集合、Map类型;2中类型匹配失败后还可以对这几种类型匹配注解Qualifier,所以在2中参数加上注解Qualifier指定注入bean的名称即可

<think> 我们根据用户需求,需要编写一个批量修改接口。用户希望接收一个包含多个对象的数组,然后进行批量修改。 在Spring Boot中,我们可以使用@RestController来创建RESTful接口,使用@PostMapping或@PutMapping来接收数据。 考虑到是批量修改,通常使用PUT请求,但也可以根据实际情况使用POST。这里我们使用PUT。 步骤: 1. 创建一个Controller类,定义一个方法处理批量修改请求。 2. 方法参数使用@RequestBody注解来接收前端传来的对象数组(或列表)。 3. 调用Service层的方法来执行批量更新操作。 在Service层,我们可以使用MyBatis的批量更新功能。这里我们使用MyBatis的注解方式,但也可以使用XML配置。不过,批量操作使用XML或Provider可能更方便。 注意:MyBatis通过@Update注解执行单条更新,但批量更新可以通过动态SQL实现,例如使用<foreach>标签,这通常需要在XML中编写。但也可以使用@Update配合@Provider注解实现动态SQL。这里我们提供两种方式: 方式一:在XML中编写批量更新的SQL语句(推荐,因为更清晰且易于处理复杂批量操作)。 方式二:使用@Update注解结合动态SQL(但MyBatis注解中不支持直接写动态SQL,因此我们可以使用@UpdateProvider)。 由于用户引用中提到了注解方式,但批量更新使用XML更常见,我们这里给出两种方式的示例。 假设我们有一个User对象,需要根据id批量更新其他字段。 首先,定义实体类User(省略getter和setter): ``` public class User { private Long id; private String username; private String password; private String email; } ``` 然后,我们创建Controller: ```java @RestController @RequestMapping("/users") public class UserController { @Autowired private UserService userService; @PutMapping("/batch") public Result batchUpdate(@RequestBody List<User> userList) { userService.batchUpdate(userList); return Result.success(); // 假设Result是一个通用的返回结果类 } } ``` 接下来,在Service层: ```java @Service public class UserService { @Autowired private UserMapper userMapper; public void batchUpdate(List<User> userList) { userMapper.batchUpdate(userList); } } ``` 在Mapper层,我们提供两种实现: 方式一:使用XML配置(推荐) 在resources/mapper/UserMapper.xml中: ```xml <mapper namespace="com.example.demo.mapper.UserMapper"> <update id="batchUpdate" parameterType="java.util.List"> <foreach collection="list" item="user" separator=";"> UPDATE user SET username = #{user.username}, password = #{user.password}, email = #{user.email} WHERE id = #{user.id} </foreach> </update> </mapper> ``` 注意:上述SQL语句在MySQL中执行需要数据库连接允许执行多条语句(在连接字符串中添加allowMultiQueries=true)。 方式二:使用注解和SQL Provider(避免XML) 首先,创建一个批量更新的Provider类: ```java public class UserSqlProvider { public String batchUpdate(Map<String, List<User>> map) { List<User> userList = map.get("list"); StringBuilder sb = new StringBuilder(); for (User user : userList) { sb.append("UPDATE user SET "); sb.append("username = '").append(user.getUsername()).append("', "); sb.append("password = '").append(user.getPassword()).append("', "); sb.append("email = '").append(user.getEmail()).append("' "); sb.append("WHERE id = ").append(user.getId()).append(";"); } return sb.toString(); } } ``` 然后在Mapper接口中: ```java @Mapper public interface UserMapper { @UpdateProvider(type = UserSqlProvider.class, method = "batchUpdate") void batchUpdate(List<User> userList); } ``` 但是,请注意,使用字符串拼接SQL容易引起SQL注入,所以不推荐直接拼接值。我们可以使用参数化方式,但@UpdateProvider中构建动态SQL并安全地使用参数比较复杂。因此,更推荐使用XML方式。 另外,也可以使用MyBatis-Plus等第三方库来简化操作。 考虑到安全性和可维护性,这里推荐使用XML方式。 另外,用户引用的内容中提到了事务,批量操作通常需要事务管理。我们可以在Service方法上添加@Transactional注解,确保整个批量更新在一个事务中。 修改Service方法: ```java @Service public class UserService { @Autowired private UserMapper userMapper; @Transactional public void batchUpdate(List<User> userList) { userMapper.batchUpdate(userList); } } ``` 这样,如果更新过程中出现错误,整个操作会回滚。 注意:批量更新操作如果数据量很大,可能会影响性能,可以考虑分批次更新(例如每次更新100条)。 总结代码结构: - Controller: 接收请求体(User对象列表),调用Service - Service: 添加事务注解,调用Mapper - Mapper: 定义批量更新方法,通过XML或Provider实现 这里我们给出XML方式的完整示例,因为更安全且常见。 注意:在application.properties中需要配置MyBatis的XML位置(如果不在默认位置),例如: ``` mybatis.mapper-locations=classpath:mapper/**/*.xml ``` 另外,确保数据库连接允许执行多条语句(MySQL): ``` spring.datasource.url=jdbc:mysql://localhost:3306/test?allowMultiQueries=true ``` 以上是完整的批量修改接口实现。 最后,我们根据要求生成相关问题。 </think> ### Spring Boot批量修改接口实现 在Spring Boot项目中,实现批量修改接口需要以下步骤: #### 1. 创建Controller接收数组 ```java @RestController @RequestMapping("/api/batch") public class BatchUpdateController { @Autowired private BatchService batchService; @PutMapping("/update") public ResponseEntity<?> batchUpdate(@RequestBody List<EntityDTO> entities) { batchService.batchUpdateEntities(entities); return ResponseEntity.ok().build(); } } ``` #### 2. 创建DTO对象 ```java public class EntityDTO { private Long id; // 标识符 private String name; private String email; // 其他需要修改的字段 // getters/setters } ``` #### 3. Service层实现 ```java @Service public class BatchService { @Autowired private EntityRepository entityRepository; @Transactional public void batchUpdateEntities(List<EntityDTO> dtos) { // 获取所有ID List<Long> ids = dtos.stream() .map(EntityDTO::getId) .collect(Collectors.toList()); // 批量查询现有实体 Map<Long, Entity> entityMap = entityRepository.findByIdIn(ids) .stream() .collect(Collectors.toMap(Entity::getId, Function.identity())); // 更新实体 dtos.forEach(dto -> { Entity entity = entityMap.get(dto.getId()); if (entity != null) { entity.setName(dto.getName()); entity.setEmail(dto.getEmail()); // 设置其他字段 } }); // 批量保存 entityRepository.saveAll(entityMap.values()); } } ``` #### 4. Repository接口 ```java @Repository public interface EntityRepository extends JpaRepository<Entity, Long> { List<Entity> findByIdIn(List<Long> ids); } ``` #### 5. 实体类 ```java @Entity public class Entity { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; private String email; // 其他字段 // getters/setters } ``` ### 关键点说明: 1. **@RequestBody注解**:接收前端JSON数组自动转换为List 2. **@Transactional**:确保批量操作的事务一致性[^3] 3. **批量查询优化**:使用`findByIdIn`一次查询所有实体 4. **Map优化**:使用ID映射提高更新效率 5. **批量保存**:`saveAll()`方法优化数据库操作 前端调用示例(JSON): ```json [ { "id": 1, "name": "更新名称1", "email": "new1@example.com" }, { "id": 2, "name": "更新名称2", "email": "new2@example.com" } ] ``` ### 性能优化建议: 1. 对于超大批量更新(>1000条),考虑分批次处理 2. 使用JdbcTemplate或MyBatis批量操作提升性能 3. 添加参数验证确保数据完整性 4. 实现乐观锁机制处理并发更新
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

当前余额3.43前往充值 >
需支付:10.00
成就一亿技术人!
领取后你会自动成为博主和红包主的粉丝 规则
hope_wisdom
发出的红包
实付
使用余额支付
点击重新获取
扫码支付
钱包余额 0

抵扣说明:

1.余额是钱包充值的虚拟货币,按照1:1的比例进行支付金额的抵扣。
2.余额无法直接购买下载,可以购买VIP、付费专栏及课程。

余额充值