在J2EE开发过程中,碰到这样的一个需求:在用户编辑某个敏感表单的时候,如果有数据项修改则需要将修改项、修改前的值、修改后的值、修改人、修改时间插入到数据变更记录表中。一般碰到这种需求会去直接对数据表各字段(或类属性)逐个判定是否相同。这种方法毫无通用性可言,需要对特定表(特定类)编写特定的判定的代码。
我们项目采用的是Spring MVC+Spring3+Hibernate3架构,实体层是基于注解方式配置的。为了判定用户提交的表单是否有数据项的修改,只需判定在执行更新操作前的实体对象和数据库中取出的实体对象(更新操作前取出)的各属性是否相同,一旦不相同则存入变更记录表中。
因此采用java反射和注解的方式实现实体对象通用属性对比。其基本思路是:在一个实体类中,类的属性总体有以下二种类型。
1、java.lang.String/java.lang.Integer/java.lang.Double/java.lang.Float/java.math.BigDecimal/java.Util.Date
2、其他自定义关联类
1中的类型通过java反射得到值,并转化为字符串类型比较,就可判定是否有过修改,所以1的类型比较比较简单。
而2的比较就相对复杂,比如要判定的是人员对象userInfo中人员组织机构org是否有过修改,其中org是一个Org类的对象。则实际上要判定的两个关联对象要显示的文本,比如“销售部”与“技术部”,因为最终存储在记录表中是文本,而不是无意义的org的主键或者org对象本身。当然,如果判定两个org均为空,或者主键相等则不需要再取出要显示的文本作判断。
至于哪些类的属性需要判断属性变更,关联类的哪个属性作为展示的文本属性都是通过自定义注解方式配置的。
以用户信息变更为例,开始上代码。
1、Hibernate3下一个实体的配置(使用注解方式)
package com.test.base.user.model;
import java.io.Serializable;
import java.util.Date;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.Transient;
import org.codehaus.jackson.annotate.JsonIgnoreProperties;
import org.codehaus.jackson.annotate.JsonProperty;
import org.hibernate.annotations.GenericGenerator;
import com.cnpc.base.dict.model.Dict;
import com.cnpc.base.org.model.Org;
import com.cnpc.framework.annotation.Header;
import com.cnpc.framework.utils.DateUtil;
@Entity
@Table(name = "USER")
@JsonIgnoreProperties(value = { "hibernateLazyInitializer", "handler",
"fieldHandler" })
public class UserInfo implements Serializable {
private static final long serialVersionUID = 2891353226199297040L;
@Id
@Column(name = "id", length = 36)
@GeneratedValue(generator = "uuid")
@GenericGenerator(name = "uuid", strategy = "org.hibernate.id.UUIDGenerator")
@JsonProperty("id")
private String id;
@Header(name="照片")
@Column(name = "imageId", length = 32)
private String imageId;
@Header(name="姓名")
@Column(name = "name", length = 100)
private String name;
@Header(name="性别")
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "dictSex")
private Dict sex;
@Header(name="所在单位")
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "OrgID")
private Org org;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "deptID")
private Org corg;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "officeId")
private Org orgOffice;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "dictJobStatus")
private Dict jobStatus;
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "dictempType")
private Dict empType;
@Column(name = "empNO", length = 100)
private String empNO;
@Column(name = "officeAdr", length = 200)
private String officeAdr;
@Column(name = "nation", length = 20)
private String nation;
/** 出生年月 **/
@Header(name="出生年月",format="yyyy-MM-dd")
@Column(name = "birth")
private Date birth;
/** 婚姻状况 **/
@Header(name="婚姻状况")
@ManyToOne(fetch = FetchType.LAZY)
@JoinColumn(name = "dictMarriage")
private Dict marriage;
/** 编号 **/
@Column(name = "workCard", length = 100)
private String workCard;
/** 毕业院校 **/
@Column(name = "graduate", length = 100)
private String graduate;
/** 专业 **/
@Column(name = "major", length = 100)
private String major;
@Transient
private String oName;
@Transient
private String coId;
@Transient
private String coName;
@Transient
private String politicalId;
@Transient
private String jobStatusId;
@Transient
private String postId;
@Transient
private String password;
public UserInfo() {
super();
}
public UserInfo(String id) {
super();
this.id = id;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getImageId() {
return imageId;
}
public void setImageId(String imageId) {
this.imageId = imageId;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public Dict getSex() {
return sex;
}
public void setSex(Dict sex) {
this.sex = sex;
}
public Org getOrg() {
return org;
}
public void setOrg(Org org) {
this.org = org;
}
public Org getCorg() {
return corg;
}
public void setCorg(Org corg) {
this.corg = corg;
}
public Org getOrgOffice() {
return orgOffice;
}
public void setOrgOffice(Org orgOffice) {
this.orgOffice = orgOffice;
}
public Dict getJobStatus() {
return jobStatus;
}
public void setJobStatus(Dict jobStatus) {
this.jobStatus = jobStatus;
}
public Dict getEmpType() {
return empType;
}
public void setEmpType(Dict empType) {
this.empType = empType;
}
public String getEmpNO() {
return empNO;
}
public void setEmpNO(String empNO) {
this.empNO = empNO;
}
//部分getter setter方法略去
}
以上Header注解为自定义注解,在后续的类属性对比中,只有Header注解的属性才会参与对比,其他注解为Hibernate的常用注解。
2、Header注解,包含两个属性一个是name代表修改项,一个是format主要用在日期格式转化为文本时用
package com.test.framework.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)//注解会在class字节码文件中存在,在运行时可以通过反射获取到
@Target({ElementType.FIELD,ElementType.METHOD})//定义注解的作用目标**作用范围字段、枚举的常量/方法
@Documented //说明该注解将被包含在javadoc中
public @interface Header {
/**
* 字段说明
*
* @return
*/
String name() default "";
/**
* 格式
*/
String format() default "";
}
3、以UserInfo为例,其关联了组织机构Org和字典Dict,则需在实体中配置@ForeignShow自定义注解,该注解主要是存储变更关联类的文本属性。以Dict为例,两个关联的Dict不相等,则将两个Dict对象的name属性存入变更表中:
package com.test.base.dict.model;
import java.io.Serializable;
import javax.persistence.Column;
import javax.persistence.Entity;
import javax.persistence.FetchType;
import javax.persistence.GeneratedValue;
import javax.persistence.Id;
import javax.persistence.JoinColumn;
import javax.persistence.ManyToOne;
import javax.persistence.Table;
import javax.persistence.Transient;
import org.codehaus.jackson.annotate.JsonIgnoreProperties;
import org.codehaus.jackson.annotate.JsonProperty;
import org.hibernate.annotations.GenericGenerator;
import com.cnpc.framework.annotation.ForeignShow;
@Entity
@Table(name = "base_dict")
@JsonIgnoreProperties(value = {"hibernateLazyInitializer","handler","fieldHandler","parentDict" })
public class Dict implements Serializable {
@Id
@Column(name = "id", length = 36)
@GeneratedValue(generator = "uuid")
@GenericGenerator(name = "uuid", strategy = "org.hibernate.id.UUIDGenerator")
@JsonProperty("id")
private String id;
/** 字典名称 */
@ForeignShow
@Column(name = "name", length = 200)
private String name;
/** 字典编码 */
@Column(name = "code", length = 20)
private String code;
/** 上级组织 */
@ManyToOne(fetch=FetchType.LAZY)
@JoinColumn(name = "parentId")
private Dict parentDict;
/** 组织机构层级编号 */
@Column(name = "levelCode", length = 36)
private String levelCode;
/** 是否可用逻辑删除 */
@Column(name = "isDel")
private String isDel;
/** 备注 */
@Column(name = "remark", length = 300)
private String remark;
/** 对应类型 */
@Column(name = "staffType")
private String staffType;
/** 对应类型 */
@Column(name = "postRatio")
private String postRatio;
@Transient
private String pId;
@Transient
private String pName;
public String getPostRatio() {
return postRatio;
}
public void setPostRatio(String postRatio) {
this.postRatio = postRatio;
}
public String getStaffType() {
return staffType;
}
public void setStaffType(String staffType) {
this.staffType = staffType;
}
public String getpId() {
return parentDict == null ? this.pId : parentDict.getId();
}
public void setpId(String pId) {
this.pId = pId;
}
public String getpName() {
return parentDict == null ? this.pName : parentDict.getName();
}
public void setpName(String pName) {
this.pName = pName;
}
public String getId() {
return id;
}
public void setId(String id) {
this.id = id;
}
public String getName() {
return name;
}
public void setName(String name) {
this.name = name;
}
public String getCode() {
return code;
}
public void setCode(String code) {
this.code = code;
}
public Dict getParentDict() {
return parentDict;
}
public void setParentDict(Dict parentDict) {
this.parentDict = parentDict;
}
public String getLevelCode() {
return levelCode;
}
public void setLevelCode(String levelCode) {
this.levelCode = levelCode;
}
public String getIsDel() {
return isDel;
}
public void setIsDel(String isDel) {
this.isDel = isDel;
}
public String getRemark() {
return remark;
}
public void setRemark(String remark) {
this.remark = remark;
}
}
而ForeignShow注解很简单,如下:
package com.test.framework.annotation;
import java.lang.annotation.Documented;
import java.lang.annotation.ElementType;
import java.lang.annotation.Retention;
import java.lang.annotation.RetentionPolicy;
import java.lang.annotation.Target;
@Retention(RetentionPolicy.RUNTIME)//注解会在class字节码文件中存在,在运行时可以通过反射获取到
@Target({ElementType.FIELD,ElementType.METHOD})//定义注解的作用目标**作用范围字段、枚举的常量/方法
@Documented //说明该注解将被包含在javadoc中
public @interface ForeignShow {
}
4、在执行更新操作前,获取数据库中对象,并将对象传入类属性比较方法(基于反射和注解,本文的重点):
/**
* 将类属性变更信息存入变更记录表中
* @author jrn1012 2014-5-28
* @param modifiedObj 修改后的值
* @param originalObj 修改前的值
* @param operUserid 操作用户id
*/
public void saveChangeRecord(Object modifiedObj,Object originalObj,String operUserid){
//获取所有属性
Field[] fds = originalObj.getClass().getDeclaredFields();
UserInfo operUser=(UserInfo)this.findById(UserInfo.class, operUserid);
Date updateTime=new Date();
for (int i = 0; i < fds.length; i++) {
try {
fds[i].setAccessible(true);
Header header=fds[i].getAnnotation(Header.class);
if(header==null)
continue;
String name=header.name(); //修改项
Object newValue=(fds[i].get(modifiedObj));//修改后值
Object oldValue=(fds[i].get(originalObj));//修改前值
String newV="";
String oldV="";
//文本类型
if(fds[i].getType()==String.class){
newV=newValue==null?"":newValue.toString();
oldV=oldValue==null?"":oldValue.toString();
}
//时间类型
else if(fds[i].getType()==Date.class){
String format=header.format()!=null?header.format():"yyyy-MM-dd";
oldV=oldValue==null?"":DateUtil.format((Date)oldValue,format);
newV=newValue==null?"":DateUtil.format((Date)newValue,format);
}
//整型
else if(fds[i].getType()==Integer.class){
oldV=oldValue==null?"":String.valueOf((Integer)oldValue);
newV=newValue==null?"":String.valueOf((Integer)newValue);
}
//双精度浮点型
else if(fds[i].getType()==Double.class){
oldV=oldValue==null?"":String.valueOf((Double)oldValue);
newV=newValue==null?"":String.valueOf((Double)newValue);
}
//浮点型
else if(fds[i].getType()==Float.class){
oldV=oldValue==null?"":String.valueOf((Float)oldValue);
newV=newValue==null?"":String.valueOf((Float)newValue);
}
//高精度BigDecimal
else if(fds[i].getType()==BigDecimal.class){
oldV=oldValue==null?"":String.valueOf((BigDecimal)oldValue);
newV=newValue==null?"":String.valueOf((BigDecimal)newValue);
}
//布尔类型
else if(fds[i].getType()==Boolean.class){
oldV=oldValue==null?"":String.valueOf((Boolean)oldValue);
newV=newValue==null?"":String.valueOf((Boolean)newValue);
}
//其他关联类类型
else{
Class c=fds[i].getType();
Field[] fs=c.getDeclaredFields();
String nId="";
String oId="";
Field showName=null;
for(int j=0;j<fs.length;j++){
Id idAno=fs[j].getAnnotation(Id.class);
if(idAno!=null){
fs[j].setAccessible(true);
oId=ObjectUtil.ObjectToString(oldValue, fs[j].getName(),null);
oId=oId==null?"":oId;
//oId=fs[j].get(oldValue)!=null?fs[j].get(oldValue).toString():"";
nId=ObjectUtil.ObjectToString(newValue, fs[j].getName(),null);
nId=nId==null?"":nId;
break;
}
}
if(oId.equals(nId)){
continue;
}
for(int j=0;j<fs.length;j++){
ForeignShow showAno=fs[j].getAnnotation(ForeignShow.class);
if(showAno!=null){
fs[j].setAccessible(true);
showName=fs[j];
break;
}
}
if (StrUtil.isNotBlank(oId)) {
//Object objo = this.findById(c, oId);
oldV = ObjectUtil.ObjectToString(oldValue, showName.getName(),null);
oldV=oldV==null?"":oldV;
}
if (StrUtil.isNotBlank(nId)) {
Object objn = this.findById(c, nId);
newV = ObjectUtil.ObjectToString(objn, showName.getName(),null);
newV=newV==null?"":newV;
}
}
if(StrUtil.isBlank(newV)&&StrUtil.isBlank(oldV))
continue;
else{
if(!newV.equals(oldV)){
ChangeRecord record=new ChangeRecord();
record.setName(name);
record.setNewValue(newV);
record.setOldValue(oldV);
record.setUpdateTime(updateTime);
record.setUser(operUser);
this.save(record);
}
}
} catch (Exception e) {
e.printStackTrace();
}
}
}
上文中还用到了工具类ObjectUtil,该类为:
package com.test.framework.utils;
import java.lang.reflect.Field;
import java.lang.reflect.Method;
import java.util.Date;
import java.util.Iterator;
import org.hibernate.criterion.DetachedCriteria;
import org.hibernate.impl.CriteriaImpl;
import org.hibernate.impl.CriteriaImpl.Subcriteria;
public class ObjectUtil {
/**
* 根据属性名获取getter setter方法名
*
* @param field 字段名
* @param prefix 前缀
* @return
*/
public static String methodName(String field, String prefix) {
if (field == null)
return null;
if (field.length() > 1 && Character.isUpperCase(field.charAt(1)))
return prefix + field;
else
return prefix + field.substring(0, 1).toUpperCase() + field.substring(1);
}
/**
* 根据属性名获取值
*
* @param obj 对象
* @param field 字段名
* @return
*/
public static Object getValueByKey(Object obj, String field) {
try {
Method method = null;
if (obj == null || StrUtil.isBlank(field))
return null;
String[] fieldArr = field.split("[.]");
for (String str : fieldArr) {
method = obj.getClass().getMethod(methodName(str, "get"));
obj = method.invoke(obj);
}
//System.out.println("value:"+value);
return obj;
} catch (Exception e) {
return null;
}
}
/**
* 将对象object特定方法的返回值(
*
* @param object 对象
* @param method 方法
* @param format 格式
* @return
*/
public static String ObjectToString(Object obj, String field, String format) throws Exception {
try {
Method method = null;
if (obj == null || StrUtil.isBlank(field))
return null;
String[] fieldArr = field.split("[.]");
for (String str : fieldArr) {
if (method != null)
obj = method.invoke(obj);
method = obj.getClass().getMethod(methodName(str, "get"));
}
// System.out.println("value:"+value);
return ObjectToString(obj, method, format);
} catch (Exception e) {
return null;
}
}
/**
* 将对象object特定方法的返回值(主要是get方法)按照format格式转化为字符串类型
*
* @param object 对象
* @param method 方法
* @param format 格式
* @return
*/
public static String ObjectToString(Object object, Method method, String format) throws Exception {
if (object == null || method == null)
return null;
// 时间类型
if (method.getReturnType().getName().equals(Date.class.getName())) {
if (StrUtil.isEmpty(format))
return DateUtil.format((Date) method.invoke(object));
else
return DateUtil.format((Date) method.invoke(object), format);
}
return method.invoke(object).toString();
}
public static DetachedCriteria getCriteriaWithAlias(DetachedCriteria criteria, String columnName) {
if (columnName.indexOf(".") == -1)
return criteria;
String[] nameArr = columnName.split("[.]");
for (int index = 0; index < nameArr.length - 1; index++) {
String str = nameArr[index];
if (index > 0 && !isExistAlias((DetachedCriteria) criteria, "" + nameArr[index - 1] + "." + str + "")) {
criteria.createAlias("" + nameArr[index - 1] + "." + str + "", "" + str + "",DetachedCriteria.LEFT_JOIN);
}
if (index == 0 && !isExistAlias((DetachedCriteria) criteria, str)) {
criteria.createAlias("" + str + "", "" + str + "",DetachedCriteria.LEFT_JOIN);
}
}
return criteria;
}
在更新操作前,调用saveChangeRecord方法,刚方法采用了直接获取属性值和调用get方法两种反射方法获取属性值。通过遍历自定义@Header注解找到要判定变更的属性,通过遍历关联类@Id注解找到关联类的主键值,主键值为均为空或相等则判定关联对象没有修改,否则通过主键获取关联对象并继续遍历自定义@ForeignShow注解,得到要存储到变更表的对象的属性值。
我的邮箱是jrn1012@petrochina.com.cn,欢迎大家交流。