`
grunt1223
  • 浏览: 422920 次
  • 性别: Icon_minigender_1
  • 来自: 杭州
社区版块
存档分类
最新评论

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

阅读更多
如何让CRUD来得更优雅些?前几天整理代码时,发现一位已离职的同事写的一段代码很有意思,学习研究之后整理出一片文档,供大家参考。
最后,祝开卷有益。


我们程序员在使用Ibatis开发过程中,往往会遇到多种不同条件下更新记录的情况,考虑以下场景,CMSTask是CMS系统中一个用于表示一次页面分发操作的POJO,它包含以下字段:
id记录的ID
pageId待分发的页面ID
status页面的状态,例如激活、禁用等等
gmtCreate创建时间
gmtModified最后修改时间
gmtPublish下一次的发布时间

现假设业务逻辑层(BO类)需要进行以下更新操作
  • DisableTaskStatus(Long pageId):管理员禁用该记录的分发任务,需要同时更新数据库中的status、gmtModified字段
  • updateTaskPublishTime(Long pageId,String interval):每次分发完后,脚本自动指定该页面的下次分发时间,其中interval为预设的两次分发的间隔时间,需要更新数据库中的gmtPublish字段
  • EnableTaskStatus(Long pageId,String interval):管理员重新启用该记录的分发任务,与禁用不同,启用后还需要指定下次分发的时间,需要同时更新gmtModified、status、gmtPublish字段

上述三种不同的更新要求是否意味着我们必须在TaskDAO中写三个update方法(updateTaskStatusAndGmtModified()、updateTaskGmtPublish()、updateTaskStatusAndGmtModifeidAndGmtPublish()呢?随着需求的不断增加,以后可能还要面临更严重的维护困难。当然,你也可以只使用一个“统一”的更新方法 —— updateTask(CmsTask task),它接受一个CmsTask的POJO作为参数,你的updateTaskPublishTime(Long pageId,String interval)的具体实现可能是下面的样子:
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转化为gmtPublic
			String 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等
@Aspect
public 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, 只有对实现了接口的类才是使用动态代理的。

5
2
分享到:
评论

相关推荐

    Spring-Struts-IBatis-AOP-Transaction

    它继承了 Struts 1 的优点,并引入了许多改进,如拦截器机制,使开发者可以更加灵活地处理请求和响应。Struts 2 与 Spring 的整合提供了更强大的功能,例如利用 Spring 的 DI 来管理 Struts 2 的动作类,以及事务...

    spring ibatis整合所需jar包

    在Java Web开发中,Spring和iBatis是两个非常重要的框架...这种整合方式允许开发者充分利用Spring的高级特性,如AOP事务管理,同时享受iBatis带来的灵活SQL操作。在实际项目中,这可以大大提高开发效率,降低维护成本。

    ssi——struts2+spring+ibatis(登入+增删改查)

    Struts2、Spring和iBatis是Java Web开发中常用的三大框架,它们组合起来被称为SSI,常用于构建高效、灵活的企业级应用。本项目通过这三个框架实现了一个基础的登录及增删改查功能,非常适合初学者进行学习和实践。 ...

    Spring与iBATIS的集成

    Spring与iBATIS的集成 iBATIS似乎已远离众说纷纭的OR框架之列,通常人们对非常流行的Hibernate情有独钟。但正如Spring A Developer's Notebook作者Bruce Tate 和Justin Gehtland所说的那样,与其他的OR框架相比...

    Mybatis拦截器记录数据更新历史记录到MongoDB

    在“Mybatis拦截器记录数据更新历史记录到MongoDB”这个项目中,我们需要创建一个自定义的拦截器类,该类需要实现`org.apache.ibatis.plugin.Interceptor`接口并覆写`intercept`方法。在这个方法里,我们可以捕获到...

    Spring高版本对ibatis的支持

    最近想在最新的Spring5.0中集成ibatis(不是mybatis),发现已经不在支持SqlmapClientTemplate和SqlmapClientFactoryBean,于是搞了这个工具jar来进行支持如下配置 &lt;bean id="sqlMapClient" class="org.spring...

    struts2 spring ibatis整合以及拦截器日志记录

    同时,Spring还提供了AOP(面向切面编程)功能,拦截器在某种程度上就是AOP的一种实现。 iBatis作为一个轻量级的ORM框架,它简化了SQL操作,将数据库查询与业务逻辑分离。iBatis允许开发者直接编写SQL语句,提供了...

    Spring+Struts2+Ibatis

    Struts2是Struts1的升级版,它基于拦截器模型,提供了更灵活的架构。Struts2负责接收用户请求,调用业务逻辑,并返回相应的视图。它通过Action类处理请求,配置Action映射决定如何路由请求。同时,Struts2支持多种...

    Struts2+Spring+Hibernate和Struts2+Spring+Ibatis

    Struts2+Spring+Hibernate和Struts2+Spring+Ibatis是两种常见的Java Web应用程序集成框架,它们分别基于ORM框架Hibernate和轻量级数据访问框架Ibatis。这两种框架结合Spring,旨在提供一个强大的、可扩展的、易于...

    ibatis 完美例子 一对多 批处理 事务 和 spring struts2集成

    Struts2的拦截器可以与Spring的AOP配合,实现如权限控制等功能。在配置文件中,需要配置Spring的ApplicationContextListener监听器,以便在Web应用启动时加载Spring容器,同时还需要配置Struts2的Spring插件,使得...

    webwork+spring+ibatis很适合初学者的实例

    WebWork、Spring 和 iBATIS 是三个非常重要的Java Web开发框架,它们的组合为初学者提供了丰富的学习资源。WebWork 是一个MVC(Model-View-Controller)框架,Spring 是一个全面的后端开发框架,而 iBATIS 是一个...

    基于struts+spring+ibatis的J2EE开发

    基于Struts+Spring+Ibatis的J2EE开发模式就是为了应对这些挑战而诞生的一种解决方案。这个模式结合了Struts的MVC框架、Spring的依赖注入和事务管理以及Ibatis的数据持久层,旨在降低开发复杂性,提高代码的可读性...

    spring+struts2+ibatis整合的jar包

    在Java Web开发中,Spring、Struts2和iBatis是三个非常重要的框架,它们各自在不同的层面上提供了强大的功能。Spring是一个全面的后端应用框架,提供了依赖注入(DI)、面向切面编程(AOP)、事务管理等功能;Struts...

    xfire ibatis spring web service

    这篇博客可能讨论的是如何将这三个技术——XFire、iBatis和Spring——集成在一起,构建一个Web服务应用程序。在这样的集成中,Spring可以管理整个应用的生命周期,包括XFire的Web服务和iBatis的数据访问层。通过...

    ibatis spring struts 整合案例

    开发者可以利用Spring的AOP功能来管理事务,确保在多条数据库操作之间保持一致性。Struts负责处理HTTP请求和响应,调度业务逻辑,而Ibatis则作为数据访问层,执行数据库查询和更新。 例如,在实际应用中,你可以...

    struts2+spring3+ibatis项目整合案例

    Struts2、Spring3和iBATIS是Java Web开发中常用的三大框架,它们各自负责不同的职责,协同工作可以构建出高效、松耦合的Web应用。在这个“struts2+spring3+ibatis项目整合案例”中,我们将深入探讨这三个框架如何...

    login——struts + spring + ibatis 框架(别下)

    **Spring** 是一个全面的后端开发框架,它不仅提供了依赖注入(DI)和面向切面编程(AOP)等功能,还包含了一个强大的事务管理器、数据访问抽象层,以及对其他框架如Struts、Hibernate的集成。在本项目中,Spring...

    ibatis与spring整合

    为了更好地利用ibatis的功能,并充分利用Spring框架所提供的AOP、DI等功能,将ibatis与Spring进行整合是非常必要的。这种整合可以提高代码的可维护性、可测试性和可扩展性。 1. **配置SqlMapConfig.xml**: ```xml...

    ibatis 与spring3整合

    "Ibatis 与 Spring3 整合"这一主题,涉及到的是两个知名Java框架——Ibatis(一个轻量级的持久层框架)和Spring(一个全面的企业级应用框架)的协同工作。下面我们将深入探讨这一整合过程中的关键知识点。 Ibatis ...

    ibatis与spring的整合

    在整合iBATIS和Spring的过程中,主要目标是利用Spring的IOC(Inversion of Control)容器来管理和协调数据访问层(DAO)以及事务处理,同时利用iBATIS作为SQL映射框架,提供灵活的数据库操作。以下将详细阐述整合的...

Global site tag (gtag.js) - Google Analytics