深度剖析:Java POJO Bean 对象与 Web Form 表单的自动装配

  时下很多 Web 框架 都实现了 Form 表单域与 Java 对象属性的自动装配功能,该功能确实非常有用,试想如果没这功能则势必到处冲积着 request.getParameter() 系列方法与类型转换方法的调用。重复代码量大,容易出错,同时又不美观,影响市容。

  现在的问题是,这些框架通过什么方法实现自动装配的?如果不用这些框架我们自己如何去实现呢?尤其对于那些纯 JSP/Servlet 应用,要是拥有自动装配功能该多好啊!本座深知各位之期盼,决定把自动装配的原理和实现方法娓娓道来。

  实现原理其实比较简单,主要由以下3个步骤构成:

    1. 通过 request.getParameterMap() 获取被提交的 Form 的所有表单域的名称-值映射,其中名称和值均为字符串类型。
    2. 利用 java.beans.PropertyDescriptor 对象获取 Java Bean 的所有属性的名称及其类型。
    3. 把表单域的名称与 Bean 的属性的名称逐个进行匹配,当找到相匹配的属性时就调用 Bean 的 setter 方法把表单域的值设置给该 Bean 属性。当然,因为表单域的值总是字符串类型,很可能与 Bean 属性的类型不一致,所以在设置 Bean 属性前要进行必要的类型转换。

  上面所表述的3点原理不知大家是否完全理解,没关系,下面我们通过一个具体的表单提交的例子来看一看实际的效果,首先看看待提交的表单页面及其代码:

深度剖析:Java POJO Bean 对象与 Web Form 表单的自动装配_第1张图片

<form action="checkbean.action" method="post">
First Name: <input type="text" name="firstName" value="丑">
<br>
Last Name: <input type="text" name="lastName" value="怪兽">
<br>
Birthday: <input type="text" name="birthday" value="1978-11-03">
<br>
Gender: 男 <input type="radio"" name="gender" value="false">
 <input type="radio"" name="gender" value="true" checked="checked">
<br>
Working age: <select name="working-Age">
<option value="-1">-请选择-option>
<option value="3">三年option>
<option value="5" selected="selected">五年option>
<option value="10">十年option>
<option value="20">二十年option>
select>
<br>
Interest: 游泳 <input type="checkbox" name="its" value="1" checked="checked">
 打球 <input type="checkbox" name="its" value="2" checked="checked">
 下棋 <input type="checkbox" name="its" value="3">
 打麻将 <input type="checkbox" name="its" value="4">
 看书 <input type="checkbox" name="its" value="5" checked="checked">
<br>
<br>
<input type="submit" value="确 定">  <input type="reset" value="重 置">
form>

 

  从上图可以看出,总共有6个表单域,其名称-值分别是:{"firstName" - "丑","lastName" - "怪兽","birthday" - "1978-11-03","gender" - "女","working-Age" - "5","its" - "1,2,5"},该表单需要提交给 checkbean.action 进行处理(请注意:不要一看到 .aciton 就以为是 struts2,骑白马的不一定都是唐僧!),下面来看看 CheckBean Action 的处理代码和 Bean 的定义:

 

import java.util.HashMap;
import java.util.Map;

import vo.Persion;

import com.bruce.mvc.ActionSupport;

public class CheckBean extends ActionSupport
{
@Override
public String execute()
{
// 如果表单元素的名称和 Form Bean 属性名不一致则使用 keyMap 进行映射
// key: 表单元素名称, value: Form Bean 属性名
Map keyMap = new HashMap();
keyMap.put("working-Age", "workingAge");
keyMap.put("its", "interest");

/* 自动装配方法一 */
// 使用表单元素创建 Form Bean
// 如果表单元素的名称和 Form Bean 属性名完全一致则不需使用 keyMap 进行映射
Persion p = createFormBean(Persion.class, keyMap);

/* 自动装配方法二 */
// 先创建 Form Bean 对象, 然后再填充它的属性
Persion p2 = new Persion();
fillFormBeanProperties(p2, keyMap);

// 可以获取 Form Bean 的所有属性值
//Map result = BeanHelper.getProperties(p);

// 把 p 设置为 request 属性,并最终在结果页面展示
setRequestAttribute("persion", p);

return SUCCESS;
}
}
import java.util.Date;
import java.util.List;

public class Persion
{
private String firstName;
private String lastName;
private Date birthday;
private boolean gender;
private int workingAge;
private int[] interest;
private List photos;

// getter 和 setter 方法
// (略)。。。。。。
}


   从 CheckBean 的代码可以看出,它是通过 createFormBean() 或 fillFormBeanProperties() 方法来自动装配 Persion 的,它们之间的区别是:前者会直接创建新的 Persion 对象,而后者填充原有 Persion 对象的属性。请注意,如果表单域的名称与 Persion 对应的属性名不一致则用 keyMap 进行映射,如表单域 "working-Age" 是对应 Persion 的 workingAge 属性的,但它们的名称不一致,所以要进行映射。另外,Persion 有一个 photos 属性,而我们的表单域却没有,自动装配时会忽略该属性。最后看一下输出的结果页面及其代码:

深度剖析:Java POJO Bean 对象与 Web Form 表单的自动装配_第2张图片

  <body>
<br>
<div align="right">
<a href="index.action"><u:msg key="jsp-set_locale.back-to-index"/>a>
div>
<br><br><br><br>
<div align="center">
<table border="1">
<caption>Persion Attributscaption>

<tr><td>Nametd><td><c:out value="${persion.firstName} ${persion.lastName}"/> td>tr>
<tr><td>Brithdaytd><td><c:out value="${persion.birthday}"/> td>tr>
<tr><td>Gendertd><td><c:out value="${persion.gender}"/> td>tr>
<tr><td>Working Agetd><td><c:out value="${persion.workingAge}"/> td>tr>
<tr><td>Interesttd><td><c:forEach var="its" items="${persion.interest}">
<c:out value="${its}" />  
c:forEach> td>tr>
<tr><td>Photostd><td><c:forEach var="p" items="${persion.photos}">
<c:out value="${p}" /><br>
c:forEach> td>tr>
table>
div>
body>

  

  通过上面的例子可以看到,通过自动装配 Bean,我们获得了非常大的便利。现在我们就从createFormBean() 和 fillFormBeanProperties() 开始,逐步揭开自动装配的神秘面纱,先看看下面两个类及其方法的定义:

 

package com.bruce.mvc;

import com.bruce.util.http.HttpHelper;

/** {@link Action} 对象公共基类 */
public class ActionSupport implements Action
{
private ServletContext servletContext;
private HttpServletRequest request;
private HttpServletResponse response;
private HttpSession session;

/** 默认 {@link Action} 入口方法(返回 {@link Action#SUCCESS}) */
public String execute()
{
return SUCCESS;
}

/** 使用表单元素创建 Form Bean (表单元素的名称和 Form Bean 属性名完全一致) */
public final T createFormBean(Class clazz)
{
return HttpHelper.createFormBean(request, clazz);
}

/** 使用表单元素创建 Form Bean (用 keyMap 映射与表单元素名称不对应的 Form Bean 属性) */
public final T createFormBean(Class clazz, Map keyMap)
{
return HttpHelper.createFormBean(request, clazz, keyMap);
}

/** 使用表单元素填充 Form Bean (表单元素的名称和 Form Bean 属性名完全一致) */
public final void fillFormBeanProperties(T bean)
{
HttpHelper.fillFormBeanProperties(request, bean);
}

/** 使用表单元素填充 Form Bean (用 keyMap 映射与表单元素名称不对应的 Form Bean 属性) */
public final void fillFormBeanProperties(T bean, Map keyMap)
{
HttpHelper.fillFormBeanProperties(request, bean, keyMap);
}

// 其它方法
// (略)。。。
}
package com.bruce.util.http;

import com.bruce.util.BeanHelper;

/** HTTP 帮助类 */
public class HttpHelper
{
/** 使用表单元素创建 Form Bean (表单元素的名称和 Form Bean 属性名完全一致) */
public final static T createFormBean(HttpServletRequest request, Class clazz)
{
return createFormBean(request, clazz, null);
}

/** 使用表单元素创建 Form Bean (用 keyMap 映射与表单元素名称不对应的 Form Bean 属性) */
public final static T createFormBean(HttpServletRequest request, Class clazz, Map keyMap)
{
Map properties = getParamMap(request);
return BeanHelper.createBean(clazz, properties, keyMap);
}

/** 使用表单元素填充 Form Bean (表单元素的名称和 Form Bean 属性名完全一致) */
public final static void fillFormBeanProperties(HttpServletRequest request, T bean)
{
fillFormBeanProperties(request, bean, null);
}

/** 使用表单元素填充 Form Bean (用 keyMap 映射与表单元素名称不对应的 Form Bean 属性) */
public final static void fillFormBeanProperties(HttpServletRequest request, T bean, Map keyMap)
{
Map properties = getParamMap(request);
BeanHelper.setProperties(bean, properties, keyMap);
}

/** 获取 {@link HttpServletRequest} 的所有参数名称和值 */
public final static Map getParamMap(HttpServletRequest request)
{
return request.getParameterMap();
}

// 其它方法
// (略)。。。
}


  哈哈,大家看到了吧,我们迂回了那么久,但 ActionSupport 类和 HttpHelper 类并没有并没有做多少事情,他们只是获取请求参数 Map,并传递给 BeanHelper 类的 createBean() 和 setProperties() 方法进行装配,实际负责装配工作的是 BeanHelper 。这里解析一下为何要写得那么迂回,其实这些代码是从本座自己写的 “Portal-Basic MVC & REST 开发框架” 中摘录出来的,总之一句话:是因为框架的需要而写得那么迂回的,并非本座有意而为之。顺便做下广告:“Portal-Basic MVC & REST 开发框架是一套功能完备的 Web 服务端开发框架,内置 MVC Web 基础架构,支持可扩展的数据访问接口(已内置 Hibernate、MyBaits 和 JDBC 支持),集成拦截器、国际化、文件上传下载和缓存等基础 Web 服务,基于纯 Jsp/Servlet API 实现,非常容易学习和使用尤其适合那些希望使用纯 Jsp/Servlet API 进行开发或对 SSH 等主流框架的复杂性感到繁琐与无奈的人士使用。该框架已通过多个商业项目考验,并不是写来玩的哦。如果各位有兴趣,本座以后再找个机会开个专贴详细介绍下这个框架。

  不扯远了,回到我们的正题,我们再来看看 BeanHelper 的装配工装是如何实现的:

 

/** Java Bean 帮助类,执行 Java Bean 属性的 get / set 相关操作 */
public class BeanHelper
{
/** 创建指定类型的 Java Bean,并设置相关属性
*
*
@param clazz : Bean 类型
*
@param properties : 属性名 / 值映射

* 其中名称为 {
@link String} 类型,与属性名称可能一直也可能不一致

* 属性值可能为以下 3 中类型:

*     1) 属性的实际类型:直接对属性赋值

*     2) {
@link String} 类型:先执行自动类型转换再对属赋值

*     3) {
@link String}[] 类型:先执行自动类型转换再对属赋值

*
@param keyMap : properties.key / Bean 属性名映射,当 properties 的 key 与属性名不对应时,
* 用 keyMap 把它们关联起来
*
@return 生成的 Bean实例
*/
public static final B createBean(Class clazz, Map properties, Map keyMap)
{
B bean = null;

try
{
// 创建 Bean 实例
bean = clazz.newInstance();
// 设置 Bean 属性
setProperties(bean, properties, keyMap);
}
catch(Exception e)
{
throw new RuntimeException(e);
}

return bean;
}

public static final B createBean(Class clazz, Map properties)
{
return createBean(clazz, properties, null);
}

/** 设置 Java Bean 的属性
*
*
@param bean : Bean 实例
*
@param properties : 属性名 / 值映射

* 其中名称为 {
@link String} 类型,与属性名称可能一直也可能不一致

* 属性值可能为以下 3 中类型:

*     1) 属性的实际类型:直接对属性赋值

*     2) {
@link String} 类型:先执行自动类型转换再对属赋值

*     3) {
@link String}[] 类型:先执行自动类型转换再对属赋值

*
@param keyMap : properties.key / Bean 属性名映射,当 properties 的 key 与属性名不对应时,
* 用 keyMap 把它们关联起来
*/
public static final void setProperties(Object bean, Map properties, Map keyMap)
{
// 获取所有 Bean 属性
Map pps = getPropDescMap(bean.getClass());
Set> set = properties.entrySet();

// 根据属性名称设置 Bean 的每个属性值
for(Map.Entry o : set)
{
String name = null; // 属性名称
String key = o.getKey();

if(keyMap != null)
name = keyMap.get(key);
if(name == null)
name = key;

T value = o.getValue();
PropertyDescriptor pd = pps.get(name); // 名称对应的 PropertyDescriptor

if(pd != null && value != null)
// 设置指定属性值
setProperty(bean, pd, value);
}
}

public static final void setProperties(Object bean, Map properties)
{
setProperties(bean, properties, null);
}

// 设置指定属性值
private static final boolean setProperty(Object bean, PropertyDescriptor pd, T value)
{
// 获取属性的 setter 方法
Method method = pd.getWriteMethod();
// 只处理 public 的实例 setter 方法
if(method != null && isPublicInstanceMethod(method))
{
method.setAccessible(true);

Class clazz = pd.getPropertyType();
// 设置具体属性值
setProperty(bean, value, method, clazz);

return true;
}

return false;
}

// 设置具体属性值
private static void setProperty(Object bean, T value, Method method, Class clazz)
{
Object param = null;
Class valueType = value.getClass();
Class valueComType = valueType.getComponentType();
Class clazzComType = clazz.getComponentType();

// 检查是否需要作类型转换
if(
!clazz.isAssignableFrom(valueType) &&
(
(valueType.equals(String.class)) ||
(valueType.isArray() && valueComType.equals(String.class))
) &&
(
(GeneralHelper.isSimpleType(clazz)) ||
(clazz.isArray() && GeneralHelper.isSimpleType(clazzComType))
)
)
// 转换为目标类型的属性值
param = parseParameter(clazz, value);
else
param = value;

// 调研 setter 方法设置属性值
invokeMethod(bean, method, param);
}

// 执行类型转换 (不解释了,看官们自己参详吧 ^_^)
private static final Object parseParameter(Class clazz, T obj)
{
Object param = null;
Class valueType = obj.getClass();

if(clazz.isArray())
{
String[] value = null;

if(valueType.isArray())
value = (String[])obj;
else
{
String str = (String)obj;
StringTokenizer st = new StringTokenizer(str, " ,;\t\n\r\f");
value = new String[st.countTokens()];

for(int i = 0; st.hasMoreTokens(); i++)
value[i] = st.nextToken();
}

int length = value.length;
Class type = clazz.getComponentType();
param = Array.newInstance(type, length);

for(int i = 0; i < length; i++)
{
String v = value[i];
Object p = GeneralHelper.str2Object(type, v);
Array.set(param, i, p);
}
}
else
{
String value = null;

if(valueType.isArray())
{
String[] array = (String[])obj;
if(array.length > 0)
value = array[0];
}
else
value = (String)obj;

param = GeneralHelper.str2Object(clazz, value);
}

return param;
}

// 其他方法
// (略)。。。
}
public class GeneralHelper
{
/** 简单数据类型集合 */
public static final Set> SMIPLE_CLASS_SET = new HashSet>(18);

static
{
SMIPLE_CLASS_SET.add(int.class);
SMIPLE_CLASS_SET.add(long.class);
SMIPLE_CLASS_SET.add(float.class);
SMIPLE_CLASS_SET.add(double.class);
SMIPLE_CLASS_SET.add(byte.class);
SMIPLE_CLASS_SET.add(char.class);
SMIPLE_CLASS_SET.add(short.class);
SMIPLE_CLASS_SET.add(boolean.class);
SMIPLE_CLASS_SET.add(Integer.class);
SMIPLE_CLASS_SET.add(Long.class);
SMIPLE_CLASS_SET.add(Float.class);
SMIPLE_CLASS_SET.add(Double.class);
SMIPLE_CLASS_SET.add(Byte.class);
SMIPLE_CLASS_SET.add(Character.class);
SMIPLE_CLASS_SET.add(Short.class);
SMIPLE_CLASS_SET.add(Boolean.class);
SMIPLE_CLASS_SET.add(String.class);
SMIPLE_CLASS_SET.add(Date.class);
}

/** 检查 clazz 是否为简单数据类型 */
public final static boolean isSimpleType(Class clazz)
{
return SMIPLE_CLASS_SET.contains(clazz);
}

/** String -> Any,如果 handler 为 null 则把字符串转换为 8 种基础数据类型、及其包装类、 {@link Date} 或 {@link String},
* 如果 handler 不为 null 则由 handler 执行转换
*
*
@param type : 目标类型的 {@link Class} 对象
*
@param v : 要转换的字符串
*
@param handler : 类型转换处理器
*
@return : 转换结果,如果转换不成功返回 null
*
@throws : 如果目标类型不支持抛出 {@link IllegalArgumentException}
*
*/
@SuppressWarnings("unchecked")
public static final T str2Object(Class type, String v, TypeHandler handler)
{
Object param = null;

if(handler != null)
return handler.handle(v);

if(type == String.class)
param = safeTrimString(v);
else if(type == int.class)
param = str2Int_0(v);
else if(type == long.class)
param = str2Long_0(v);
else if(type == byte.class)
param = str2Byte_0(v);
else if(type == char.class)
param = str2Char_0(v);
else if(type == float.class)
param = str2Float_0(v);
else if(type == double.class)
param = str2Double_0(v);
else if(type == short.class)
param = str2Short_0(v);
else if(type == boolean.class)
param = str2Boolean_False(v);
else if(type == Integer.class)
param = str2Int(v);
else if(type == Long.class)
param = str2Long(v);
else if(type == Byte.class)
param = str2Byte(v);
else if(type == Character.class)
param = str2Char(v);
else if(type == Float.class)
param = str2Float(v);
else if(type == Double.class)
param = str2Double(v);
else if(type == Short.class)
param = str2Short(v);
else if(type == Boolean.class)
param = str2Boolean(v);
else if(Date.class.isAssignableFrom(type))
param = str2Date(v);
else
throw new IllegalArgumentException(String.format("object type '%s' not valid", type));

return (T)param;
}

public static final T str2Object(Class type, String v)
{
return str2Object(type, v, null);
}

// 其他方法
// (略)。。。
}

 

  从上面的代码可以看出,BeanHelper 支持8种简单数据类型及其包装类、String 和 Date 类型以及它们的数组类型的自动装配,最后强调一下:BeanHelper 和 GeneralHelper 其实是两个用途非常广泛的类,其作用不单是为了协助 Form 表单域自动装配 Bean 。下面列出一些使用例子,帮助大家进一步了解 BeanHelper 的使用方法:

 

View Code
  1 package test;
2
3 import java.beans.IntrospectionException;
4 import java.util.Date;
5 import java.util.HashMap;
6 import java.util.Map;
7 import java.util.StringTokenizer;
8
9 import com.bruce.util.BeanHelper;
10 import com.bruce.util.GeneralHelper;
11
12 @SuppressWarnings("unused")
13 public class TestBeanHelper extends Object
14 {
15 public static void main(String[] args) throws Exception
16 {
17 test();
18 testStr2Object();
19 test_setProperty();
20 test_setProperties_1();
21 test_setProperties_2();
22 }
23
24 private static void test()
25 {
26 //System.out.println(GeneralHelper.str2Date(" 1978-11-03 "));
27 //System.out.println(GeneralHelper.str2Date(" 1978-11-03 "));
28 //GeneralHelper.str2Byte(null);
29 //GeneralHelper.str2Char_0(null);
30 //GeneralHelper.str2Boolean(null);
31 //GeneralHelper.str2Boolean_False(null);
32 }
33
34 private static void testStr2Object() throws IntrospectionException
35 {
36 int i = GeneralHelper.str2Object(int.class, "123");
37 Date dt = GeneralHelper.str2Object(Date.class, "1978-11-03");
38 String[] arr = GeneralHelper.str2Object(String[].class, "12, 34, 56, 78", new GeneralHelper.TypeHandler()
39 {
40 @Override
41 public String[] handle(String v)
42 {
43 StringTokenizer st = new StringTokenizer(v, " ,;\t\r\n\f");
44 String[] result = new String[st.countTokens()];
45
46 for(int i = 0; st.hasMoreTokens(); i++)
47 result[i] = st.nextToken();
48
49 return result;
50 }
51 });
52
53 // !! error !!
54 // String[] arr2 = GeneralHelper.str2Object(String[].class, "12, 34, 56, 78");
55 }
56
57 private static void test_setProperty()
58 {
59 C c = new C();
60 BeanHelper.setProperty(c, "dt", "2010-10-10");
61 BeanHelper.setProperty(c, "i", 456);
62 BeanHelper.setProperty(c, "l", "999");
63 int i = BeanHelper.getProperty(c, "i");
64 double l = BeanHelper.getProperty(c, "l");
65 boolean b = BeanHelper.getProperty(c, "b");
66 Date dt = BeanHelper.getProperty(c, "dt");
67 System.out.println(c);
68 }
69
70 private static void test_setProperties_1() throws Exception
71 {
72 Map objs = new HashMap();
73 objs.put("si", new String[] {"888"});
74 objs.put("fi", new String[] {"999"});
75 objs.put("b", new String[] {"true"});
76 objs.put("i", new String[] {"1"});
77 objs.put("l", new String[] {"2.3"});
78 objs.put("dt", new String[] {"2011-09-17"});
79 objs.put("__str", new String[] {"我是怪兽"});
80 objs.put("__ia", new String[] {"12", "34", "56"});
81 objs.put("__sa", new String[] {"ab", "cd", "ef"});
82
83 Map keyMap = new HashMap();
84 keyMap.put("__str", "str");
85 keyMap.put("__ia", "ia");
86 keyMap.put("__sa", "sa");
87
88 C c = BeanHelper.createBean(C.class, objs, keyMap);
89 Map result = BeanHelper.getProperties(c);
90 System.out.println(result);
91 }
92
93 private static void test_setProperties_2() throws Exception
94 {
95 java.sql.Date dt = new java.sql.Date(new java.util.Date().getTime());
96
97 Map objs = new HashMap();
98 objs.put("si", 888);
99 objs.put("fi", 999);
100 objs.put("b", "True");
101 objs.put("i", 123);
102 objs.put("l", "2.3");
103 //objs.put("dt", new String[] {"2011-09-17"});
104 objs.put("dt", dt);
105 objs.put("str", "我是怪兽");
106 objs.put("ia", new int[] {12, 34, 56});
107 objs.put("sa", "ab, cd, ef");
108
109 C c = new C();
110 BeanHelper.setProperties(c, objs);
111 Map result = BeanHelper.getProperties(c);
112 System.out.println(result);
113 }
114
115
116 }
View Code
 1 package test;
2
3 import java.util.Date;
4
5
6 class A
7 {
8 private boolean b;
9
10 public boolean isB()
11 {
12 return b;
13 }
14
15 public void setB(boolean b)
16 {
17 this.b = b;
18 }
19
20 }
21
22 public class C extends A
23 {
24 static int si;
25 final int fi = 100;
26
27 private int i;
28 private Double l;
29 private Date dt;
30 private String str;
31 private int[] ia;
32 private String[] sa;
33
34 public String[] getSa()
35 {
36 return sa;
37 }
38 public void setSa(String[] sa)
39 {
40 this.sa = sa;
41 }
42 public static int getSi()
43 {
44 return si;
45 }
46 public static void setSi(int si)
47 {
48 C.si = si;
49 }
50 public int getFi()
51 {
52 return fi;
53 }
54 public String getStr()
55 {
56 return str;
57 }
58 public void setStr(String str)
59 {
60 this.str = str;
61 }
62 public int[] getIa()
63 {
64 return ia;
65 }
66 public void setIa(int[] ia)
67 {
68 this.ia = ia;
69 }
70 public int getI()
71 {
72 return i;
73 }
74 public void setI(int i)
75 {
76 this.i = i;
77 }
78 public Double getL()
79 {
80 return l;
81 }
82 public void setL(Double l)
83 {
84 this.l = l;
85 }
86 public Date getDt()
87 {
88 return dt;
89 }
90 public void setDt(Date dt)
91 {
92 this.dt = dt;
93 }
94 }

 

  老规矩,想看完整代码者请轻轻 Touch 一下:  Source Code ^_^ 

 

CodeProject

你可能感兴趣的:(深度剖析:Java POJO Bean 对象与 Web Form 表单的自动装配)