Spring WebMVC List容器元素的Data Bind

最近学习SpringMVC,做了一个小Demo, 发现一个问题:无法绑定command对象List Field中的元素的属性。

 

Command Object 类似如下:

public class CommandObject {

	private List<ListElement> mylist = new ArrayList<ListElement>();
	
	// getter and setter
}

这里list属性必须先行实例化,否则会错误。这个与Hibernate要求PO的一对多Set必须实例化一样。 

 

页面大致如下:

 

<input type="text" name="command.xxxlist[0].name" value="" />
<input type="text" name="command.xxxlist[1].name" value="" />
<input type="text" name="command.xxxlist[2].name" value="" />

 

运行后会报错, 由于绑定的时候list为empty, xxxlist.get(index)自然会数组越界,name要set都不知set到哪里去了。Spring也不会自行实例化,这让我感到相当恼火。

 

研读Source Code,寻求解决方法,不禁释然:当前版本要兼容1.4,Spring要如何知道实例化哪个Class,在没有使用泛型的情况下。

 

追踪代码,核心在org.springframework.beans.BeanWrapperImpl.java中。

 

private Object getPropertyValue(PropertyTokenHolder tokens) throws BeansException {
		String propertyName = tokens.canonicalName;
		String actualName = tokens.actualName;
		PropertyDescriptor pd = getCachedIntrospectionResults().getPropertyDescriptor(actualName);
		if (pd == null || pd.getReadMethod() == null) {
			throw new NotReadablePropertyException(getRootClass(), this.nestedPath + propertyName);
		}
		Method readMethod = pd.getReadMethod();
		try {
			if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {
				readMethod.setAccessible(true);
			}
			Object value = readMethod.invoke(this.object, (Object[]) null);
			if (tokens.keys != null) {
				// apply indexes and map keys
				for (int i = 0; i < tokens.keys.length; i++) {
					String key = tokens.keys[i];
					if (value == null) {
						throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + propertyName,
								"Cannot access indexed value of property referenced in indexed " +
								"property path '" + propertyName + "': returned null");
					}
					else if (value.getClass().isArray()) {
						value = Array.get(value, Integer.parseInt(key));
					}
					else if (value instanceof List) {
						List list = (List) value;
						value = list.get(Integer.parseInt(key));
					}
					else if (value instanceof Set) {
						// Apply index to Iterator in case of a Set.
						Set set = (Set) value;
						int index = Integer.parseInt(key);
						if (index < 0 || index >= set.size()) {
							throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName,
									"Cannot get element with index " + index + " from Set of size " +
									set.size() + ", accessed using property path '" + propertyName + "'");
						}
						Iterator it = set.iterator();
						for (int j = 0; it.hasNext(); j++) {
							Object elem = it.next();
							if (j == index) {
								value = elem;
								break;
							}
						}
					}
					else if (value instanceof Map) {
						Map map = (Map) value;
						Class mapKeyType = null;
						if (JdkVersion.isAtLeastJava15()) {
							mapKeyType = GenericCollectionTypeResolver.getMapKeyReturnType(pd.getReadMethod(), i + 1);
						}
						// IMPORTANT: Do not pass full property name in here - property editors
						// must not kick in for map keys but rather only for map values.
						Object convertedMapKey = this.typeConverterDelegate.convertIfNecessary(key, mapKeyType);
						// Pass full property name and old value in here, since we want full
						// conversion ability for map values.
						value = map.get(convertedMapKey);
					}
					else {
						throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName,
								"Property referenced in indexed property path '" + propertyName +
								"' is neither an array nor a List nor a Set nor a Map; returned value was [" + value + "]");
					}
				}
			}
			return value;
		}
		catch (InvocationTargetException ex) {
			throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName,
					"Getter for property '" + actualName + "' threw exception", ex);
		}
		catch (IllegalAccessException ex) {
			throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName,
					"Illegal attempt to get property '" + actualName + "' threw exception", ex);
		}
		catch (IndexOutOfBoundsException ex) {
			throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName,
					"Index of out of bounds in property path '" + propertyName + "'", ex);
		}
		catch (NumberFormatException ex) {
			throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName,
					"Invalid index in property path '" + propertyName + "'", ex);
		}
	}

 于是决定改Source了,具体如下。顺便还发现Map也有同样问题,便一道改了。

 

private Object getPropertyValue(PropertyTokenHolder tokens) throws BeansException {
		String propertyName = tokens.canonicalName;
		String actualName = tokens.actualName;
		PropertyDescriptor pd = getCachedIntrospectionResults().getPropertyDescriptor(actualName);
		if (pd == null || pd.getReadMethod() == null) {
			throw new NotReadablePropertyException(getRootClass(), this.nestedPath + propertyName);
		}
		Method readMethod = pd.getReadMethod();
		try {
			if (!Modifier.isPublic(readMethod.getDeclaringClass().getModifiers())) {
				readMethod.setAccessible(true);
			}
			Object value = readMethod.invoke(this.object, (Object[]) null);
			if (tokens.keys != null) {
				// apply indexes and map keys
				for (int i = 0; i < tokens.keys.length; i++) {
					String key = tokens.keys[i];
					if (value == null) {
						throw new NullValueInNestedPathException(getRootClass(), this.nestedPath + propertyName,
								"Cannot access indexed value of property referenced in indexed " +
								"property path '" + propertyName + "': returned null");
					}
					else if (value.getClass().isArray()) {
						value = Array.get(value, Integer.parseInt(key));
					}
					else if (value instanceof List) {
						List list = (List) value;
						int positionOfList = Integer.parseInt(key);
						Class listElementClass = GenericCollectionTypeResolver.getCollectionReturnType(readMethod);
						if(list.size() <= positionOfList && JdkVersion.isAtLeastJava15()
								&& listElementClass != null
								&& !(ClassUtils.isPrimitiveOrWrapper(listElementClass) 
										|| listElementClass.equals(String.class))) {
							if(positionOfList > Byte.MAX_VALUE) {
								throw new IllegalAccessException("Invalid property '" + nestedPath + propertyName + "' of ["
										+ getRootClass() + "] : "
										+ positionOfList
										+ " is too large to support (max value is " 
										+ Byte.MAX_VALUE
										+")");
							}
							for(int j = list.size(); j < positionOfList; j++) {
								Object listElement = BeanUtils.instantiateClass(listElementClass);
								list.add(listElement);
							}
							value = BeanUtils.instantiateClass(listElementClass);
							list.add(value);
							
						} else {
							value = list.get(positionOfList);
						}
					}
					else if (value instanceof Set) {
						// Apply index to Iterator in case of a Set.
						Set set = (Set) value;
						int index = Integer.parseInt(key);
						if (index < 0 || index >= set.size()) {
							throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName,
									"Cannot get element with index " + index + " from Set of size " +
									set.size() + ", accessed using property path '" + propertyName + "'");
						}
						Iterator it = set.iterator();
						for (int j = 0; it.hasNext(); j++) {
							Object elem = it.next();
							if (j == index) {
								value = elem;
								break;
							}
						}
					}
					else if (value instanceof Map) {
						Map map = (Map) value;
						Class mapKeyType = null;
						Class mapValueType = null;
						if (JdkVersion.isAtLeastJava15()) {
							mapKeyType = GenericCollectionTypeResolver.getMapKeyReturnType(readMethod, i + 1);
							mapValueType = GenericCollectionTypeResolver.getMapValueReturnType(readMethod, i + 1);
						}
						// IMPORTANT: Do not pass full property name in here - property editors
						// must not kick in for map keys but rather only for map values.
						Object convertedMapKey = this.typeConverterDelegate.convertIfNecessary(key, mapKeyType);
						// Pass full property name and old value in here, since we want full
						// conversion ability for map values.
						value = map.get(convertedMapKey);
						
						if(value == null && mapValueType != null 
								&& !(ClassUtils.isPrimitiveOrWrapper(mapValueType) 
										|| mapValueType.equals(String.class))) {
							value = BeanUtils.instantiateClass(mapValueType);
							map.put(convertedMapKey, value);
						}
					}
					else {
						throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName,
								"Property referenced in indexed property path '" + propertyName +
								"' is neither an array nor a List nor a Set nor a Map; returned value was [" + value + "]");
					}
				}
			}
			return value;
		}
		catch (InvocationTargetException ex) {
			throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName,
					"Getter for property '" + actualName + "' threw exception", ex);
		}
		catch (IllegalAccessException ex) {
			throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName,
					"Illegal attempt to get property '" + actualName + "' threw exception", ex);
		}
		catch (IndexOutOfBoundsException ex) {
			throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName,
					"Index of out of bounds in property path '" + propertyName + "'", ex);
		}
		catch (NumberFormatException ex) {
			throw new InvalidPropertyException(getRootClass(), this.nestedPath + propertyName,
					"Invalid index in property path '" + propertyName + "'", ex);
		}
	}

 虽然代码有点难看,毕竟解决了问题。最后顺便提下,Spring内部的工具类相当给力,大大减轻了了修改工作量。^_^

使用Spring Boot、Spring MVC、JPA或Spring Data JPA开发小型学生选课Web应用系统,可按以下方法进行: ### 技术选型 - **后端语言**:选用Java,其强大的类型系统、跨平台能力、丰富生态系统和成熟并发处理机制,能应对选课等高并发场景,构建高可靠性系统[^1]。 - **核心框架**:采用Spring Boot,它通过自动配置、起步依赖和内嵌Web服务器(如Tomcat),简化基于Spring应用的初始搭建和部署过程,让开发者专注于选课业务逻辑开发[^1]。 - **数据持久化**: - **数据库**:选择MySQL,它作为流行的开源关系型数据库,提供ACID事务支持、数据完整性约束和良好的并发性能,可保证选课数据最终一致性,同时要注重合理的表结构设计和索引优化[^1]。 - **持久层框架**:可使用Spring Data JPA,它基于JPA标准,能减少数据访问层的代码量,简化数据库操作;若涉及较多状态更新和复杂查询,MyBatis也是不错的选择,其在编写高性能、复杂SQL(如判断时间冲突、查询选课名单)方面更具灵活性[^1]。 - **前端技术**:可以选用JSP (JavaServer Pages),它是经典的服务器端模板技术,用于快速构建动态Web页面,能与Spring MVC控制器无缝集成,便于在页面中嵌套Java代码和JSTL标签来渲染数据;也可考虑更现代的前端框架如Vue.js或React.js,以提供更好的用户交互体验[^1]。 - **开发环境**:可使用Eclipse,它是功能强大的开源IDE,对Java Web开发提供良好支持[^1]。 ### 系统搭建 - **创建Spring Boot项目**:可使用Spring Initializr(https://start.spring.io/ )快速生成项目骨架,选择所需的依赖,如Spring Web、Spring Data JPA、MySQL Driver等。 - **配置数据库连接**:在`application.properties`或`application.yml`文件中配置MySQL数据库的连接信息,示例如下: ```properties spring.datasource.url=jdbc:mysql://localhost:3306/student_course_selection spring.datasource.username=root spring.datasource.password=your_password spring.datasource.driver-class-name=com.mysql.cj.jdbc.Driver spring.jpa.hibernate.ddl-auto=update spring.jpa.show-sql=true ``` - **定义实体类**:使用JPA注解定义与数据库表对应的实体类,例如学生类、课程类和选课记录类。 ```java import javax.persistence.Entity; import javax.persistence.GeneratedValue; import javax.persistence.GenerationType; import javax.persistence.Id; @Entity public class Student { @Id @GeneratedValue(strategy = GenerationType.IDENTITY) private Long id; private String name; // 其他属性、构造方法、getter和setter方法 } ``` - **创建数据访问层(Repository)**:使用Spring Data JPA的Repository接口来定义数据访问方法,Spring Data JPA会自动实现这些接口。 ```java import org.springframework.data.jpa.repository.JpaRepository; public interface StudentRepository extends JpaRepository<Student, Long> { // 可自定义查询方法 } ``` - **创建服务层(Service)**:实现业务逻辑,调用Repository层的方法进行数据操作。 ```java import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Service; import java.util.List; @Service public class StudentService { @Autowired private StudentRepository studentRepository; public List<Student> getAllStudents() { return studentRepository.findAll(); } } ``` - **创建控制器层(Controller)**:使用Spring MVC的注解创建RESTful接口,处理HTTP请求并返回响应。 ```java import org.springframework.beans.factory.annotation.Autowired; import org.springframework.web.bind.annotation.GetMapping; import org.springframework.web.bind.annotation.RestController; import java.util.List; @RestController public class StudentController { @Autowired private StudentService studentService; @GetMapping("/students") public List<Student> getAllStudents() { return studentService.getAllStudents(); } } ``` ### 前端开发 - **页面设计**:根据需求设计学生选课系统的前端页面,如学生登录页、课程列表页、选课结果页等。 - **与后端交互**:使用AJAX或其他方式与后端的RESTful接口进行数据交互,实现页面的动态更新。 ### 测试与部署 - **单元测试**:使用JUnit和Mockito等工具对各个模块进行单元测试,确保代码的正确性。 - **集成测试**:对整个系统进行集成测试,验证各个模块之间的协作是否正常。 - **部署**:将应用打包成可执行的JAR文件,部署到服务器上运行。
评论
添加红包

请填写红包祝福语或标题

红包个数最小为10个

红包金额最低5元

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

抵扣说明:

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

余额充值