首页 诗词 字典 板报 句子 名言 友答 励志 学校 网站地图
当前位置: 首页 > 教程频道 > 软件管理 > 软件架构设计 >

利用步骤拦截器优化Ibatis更新策略 —— 基于POJO、CGLIB、SPRING AOP

2012-11-01 
利用方法拦截器优化Ibatis更新策略—— 基于POJO、CGLIB、SPRING AOP如何让CRUD来得更优雅些?前几天整理代码时

利用方法拦截器优化Ibatis更新策略 —— 基于POJO、CGLIB、SPRING AOP
如何让CRUD来得更优雅些?前几天整理代码时,发现一位已离职的同事写的一段代码很有意思,学习研究之后整理出一片文档,供大家参考。
最后,祝开卷有益。


我们程序员在使用Ibatis开发过程中,往往会遇到多种不同条件下更新记录的情况,考虑以下场景,CMSTask是CMS系统中一个用于表示一次页面分发操作的POJO,它包含以下字段:
CmsTask task = TaskDAO.queryCmsTaskByPageId(pageId);task.setGmtPulish(currentTime + interval);TaskDAO.updateTask(task)
而你的sqlmap-mapping-CmsTask.xml很可能是这样:

<update id = "UPDATE_CMS_TASK_BY_ID" parameterMap=“CmsTask”>        <![CDATA[        UPDATE cms_task SET gmt_modified = #gmtModified#, PAGE_ID = #pageId#, STATUS = #status#, GMT_PUBLISH = #gmtPublish# WHERE  ID =  #id#]]></update>

即使你只是想更新数据库的gmtPublish字段,Ibatis还是很勤劳地将所有字段都“刷”了一遍
撇开需要先查询一次数据库的操作不说,明明只是更新一个字段却要重置其它所有无关的字段,单这一点让人感觉不是很优雅。一种解决方法是利用Ibatis自身的动态语句,如下
CmsTask task = new CmsTask();task.setGmtPulish(currentTime + interval);TaskDAO.updateTask(task)

<update id = "UPDATE_CMS_TASK_BY_ID" parameterMap=“CmsTask”>        <![CDATA[        UPDATE cms_task SET         ]]>        <dynamic prepend = "">                <isNotNull property = "pageId" prepend = ",">PAGE_ID = #pageId#</isNotNull>                <isNotNull property = "status" prepend = ",">STATUS = #status#</isNotNull>                <isNotNull property = "gmtPublish" prepend = ",">GMT_PUBLISH = #gmtPublish#</isNotNull>                <isNotNull property = "gmtModified" prepend = ",">GMT_MODIFIED = #gmtModified#</isNotNull>        </dynamic>        <![CDATA[        WHERE         ID =  #id#        ]]></update>

上述方法只会更新CmsTask中非空的属性;像上面那种情况,它实际上是执行了
UPDATE cms_task SET GMT_PUBLISH = #gmtPublish# where ID = #id#

这看起来可以满足我们的要求,唔,遗憾的是,领域专家告诉我们,99%情况下的任务都是在激活状态下的,因此CmsTask中的status缺省状态下是“Enable”。这样的情况下, 由于new出来的CmsTask对象中的status始终非空,使用上面的sqlmapping,“<isNotNull property = "status" prepend = ",">STATUS = #status#</isNotNull>”始终成立,这意味着每次都会去重写status,不是吗?
好像事情还不够糟糕似的,UC上居然写明了gmtPublish可以设置为空,即使是激活状态下也会分发,表示该页面已被废弃(不要和我争辩为什么不将gmtPublish设为过去的时间比如1949.10.01,也许是为了防止服务器时间被恶意篡改导致过期页面上线 )。你或许认为可以用<isNull property = " gmtPublish "> GMT_PUBLISH = null</isNull>来克服这个问题,但是Ibatis如何知道gmtPublish是初始化后为null的,还是task.setGmtPublish(null)的结果呢?如果是前者的情况,不是又陷入了重复刷新的泥潭了吗?
换一种思路,使用一个HashMap保存需要更新的数据如何?比如:
m.put(“id”,  id);m.put(“gmtPublish”,  gmtPublish);TaskDAO.updateTask(m);

注意修改TaskDAO.updateTask的参数为Map型,并将sqlmap中的
<update id = "UPDATE_CMS_TASK_BY_ID" parameterMap=“CmsTask”>

修改为:
<update id = "UPDATE_CMS_TASK_BY_ID" parameterClass=“java.util.Map”>

这样做虽然不会有重复更新的问题,但你必须自己维护传入的参数,如果你不小心把gmtPublish写成了gmtPublic,编译器不会提示任何错误,接着你要花大把的时间去大海(Log的海洋)捞针(空指针)
如果你是一个完美主义者 + 懒人,既看不顺眼一坨一坨的重复代码,心里又老是存着想要优化的情结,还不愿意自己维护hashMap,那么你可以试试接下来这种方法。
首先,声明一个抽象类AbstractBaseDO:
public abstract class AbstractBaseDO{    protected AbstractBaseDO(){}Map settedMap;    public Map getSettedMap(){        return settedMap;    }        public boolean setterInited(){        return settedMap != null;    }}

它里面包含一个用于存放POJO类中需要更新参数的HashMap。使CmsTask继承该类:
public class CmsTask extends AbstractBaseDO

接下来,我们需要一个工厂类,所有具备“自动化封装更新参数”功能的类,都是通过它生产出来的:
public class DOSetterFactory {private static DOChangeInterceptor interceptor = new DOChangeInterceptor();private DOSetterFactory(){}public static<T extends AbstractBaseDO> T newDOSetter(Class<T> modelClass){Enhancer enhancer = new Enhancer();       enhancer.setSuperclass(modelClass);       enhancer.setCallback(interceptor);       return (T)enhancer.create();}}

代码中用到了CGLIB的动态字节码增强,enhancer.setSuperclass(modelClass) 表示经过增强后返回的是继承自modelClass的子类,而enhancer.setCallback(interceptor) 则为该类在任何方法被调用之前添加了拦截器,拦截后的具体操作则都写在DOChangeInterceptor这个回调函数中:
public class DOChangeInterceptor implements MethodInterceptor {private static final String SET = "set";public Object intercept(Object obj, Method method, Object[] args, MethodProxy proxy) throws Throwable{String name = method.getName();//当set方法被调用时,将被设置的属性和需要更新的值自动存储在HashMap中if(obj instanceof AbstractBaseDO && name.startsWith(SET)){AbstractBaseDO model = (AbstractBaseDO)obj;if(model.getSettedMap() == null){model.settedMap = (new HashMap());}//将setGmtPublic转化为gmtPublicString filedNameTMP = method.getName().substring(3);char[] charlist = filedNameTMP.toCharArray();            charlist[0] = Character.toLowerCase(charlist[0]);            String filedName = String.valueOf(charlist);                        if ((args != null) && (args.length == 1)) {            model.getSettedMap().put(filedName,args[0]);            }}return proxy.invokeSuper(obj, args);}}

这个拦截器只会去处理POJO的set方法,而不会影响去其它方法。Ibatis提供的<isPropertyAvailable>标签可以用来检查传入的HashMap中,特定属性(key)是否存在:
<update id = "UPDATE_CMS_TASK_BY_ID">        <![CDATA[        UPDATE cms_task SET         ]]>        <dynamic prepend = "">            <isPropertyAvailable property = "pageId" prepend = ",">                <isNotNull property = "pageId">PAGE_ID = #pageId#</isNotNull>                <isNull property = "pageId">PAGE_ID = null</isNull>            </isPropertyAvailable>            <isPropertyAvailable property = "status" prepend = ",">                <isNotNull property = "status">STATUS = #status#</isNotNull>                <isNull property = "status">STATUS = null</isNull>            </isPropertyAvailable>            <isPropertyAvailable property = "gmtPublish" prepend = ",">                <isNotNull property = "gmtPublish">GMT_PUBLISH = #gmtPublish#</isNotNull>                <isNull property = "gmtPublish">GMT_PUBLISH = null</isNull>            </isPropertyAvailable><isPropertyAvailable property = "gmtModified" prepend = ",">                <isNotNull property = "gmtPublish">GMT_MODIFIED = #gmtModified#</isNotNull>                <isNull property = "gmtPublish">GMT_MODIFID= null</isNull>            </isPropertyAvailable>        </dynamic>        <![CDATA[        WHERE         ID =  #id#        ]]>    </update>

你可以通过
CmsTask task = DOSetterFactory.newDOSetter(CmsTask.class);

来获取一个为更新数据量身打造的全新的CmsTask类(严格上说是它的子类,但擎天柱和天火合体后不还是很叫擎天柱吗),当然,在查询、插入、删除操作时,你还是可以通过
CmsTask task = new CmsTask();

来获得传统意义上的CmsTask实例。
CGLIB的代理活动对用户是透明的,然而,它还有一些缺点需要考虑:
1、final方法不可以被增强,因为它们不能被覆盖;如果是final类,则根本不能被继承。
2、需要在你的类路径里有CGLIB 2的库
也可以考虑使用Spring的AOP实现同样的功能。这里以注解形式的AOP为例,首先声明一个名为AnyJoinPointAnnotation的注解:
@Retention (RetentionPolicy.RUNTIME)@Target({ElementType.METHOD, ElementType.TYPE})public @interface AnyJoinPointAnnotation {}

接下来声明一个名为IbatisPartialUpdateAspect 的Aspect,定义了相关的pointcut、advice等
@Aspectpublic class IbatisPartialUpdateAspect {    @Pointcut("@within(AnyJoinPointAnnotation)")    public void pointcutName(){};    @Before("pointcutName()")    public Object performanceUpdate(ProceedingJoinPoint joinpoint) throws Throwable    {        System.out.println("method called");        Object obj = joinpoint.getTarget();        if(obj instanceof AbstractBaseDO &&  joinpoint.getSignature().getName().startsWith("set"))        {            System.out.println("set called");            AbstractBaseDO model = ((AbstractBaseDO)obj);            if(model.getSettedMap() == null){                model.settedMap = (new HashMap());            }      ((AbstractBaseDO)obj).getSettedMap().put(joinpoint.getSignature().getName().substring(3).toLowerCase(), joinpoint.getArgs()[0]);        }        return joinpoint.proceed();    }}

其中,@Pointcut("@within(AnyJoinPointAnnotation)") 的声明了,它会为所有包含了@AnyJoinPointAnnotation注解的类增加新的行为;不过,对于没有实现接口的类来说,其内部还是使用了CGLib, 只有对实现了接口的类才是使用动态代理的。

热点排行