跳至主要内容

Spring 拦截 DAO

缘由

由于要对数据库的部分数据做缓存,并且没有现成的缓存与数据库的一致性的保障机制,所以只能在DAO对数据库操作的同时更新缓存。
为了避免对原有代码的侵入,决定采用spring的aop拦截DAO对数据库的操作然后更新缓存。

分析

尝试

一开始,我实现了如下的代码,对删除进行拦截:

@AfterReturning(
        pointcut = "execution(public * dao.AccountDao+.deleteX(..))",
        returning = "affectedRowCount"
)
public void delete(JoinPoint joinPoint, int affectedRowCount) {
    if (affectedRowCount == 1) {
        xCache.delete((Integer) joinPoint.getArgs()[0]);
    }
}

但是测试发现,拦截并没有成功。

深入

经检查,spring的配置没有问题,aop的语法也是正确的。那为什么拦截不到呢?
于是怀疑跟AccountDao这个interface有关:

public interface AccountDao extends GenericDao {

    @DAOAction(action = DAOActionType.UPDATE)
    public int delete(@DAOParam("accountId") int accountId);

    //...
}

仔细查看源码,发现这个interface其实并没有实现类!也就是说,这个interface或者他内部的方法是动态生成的。
查看这个bean的定义如下:

<bean id="accountDao" parent="parentDao">
    <property name="proxyInterfaces"
              value="dao.AccountDao" />
    <property name="target">
        <bean parent="daoRealizeTarget">
            <constructor-arg value="Account" />
        </bean>
    </property>
</bean>

<bean id="parentDao" class="org.springframework.aop.framework.ProxyFactoryBean" abstract="true">
    <property name="interceptorNames">
        <list>
            <value>daoRealizerAdvisor</value>
        </list>
    </property>
</bean>

<!-- The introduce interceptor for all Dao  -->
<bean id="daoRealizer" class="dao.DAORealizer" />

<!-- The advisor for inject interceptor -->
<bean id="daoRealizerAdvisor" class="org.springframework.aop.aspectj.AspectJExpressionPointcutAdvisor">
    <property name="advice" ref="daoRealizer" />
    <property name="expression" value="execution(* dao..*.*(..)) and !execution(* xx.dao..*.*(..))" />
</bean>

定义比较复杂,我们一步步来看:

首先,他是有一个父类dao的基础上创建的
<bean id="accountDao" parent="parentDao">

而这个父类是由spring的ProxyFactoryBean创建的,需要注意的是:像其它的FactoryBean实现一样,ProxyFactoryBean引入了一个间接层。如果你定义一个名为parentDao的ProxyFactoryBean, 引用parentDao的对象看到的将不是ProxyFactoryBean实例本身,而是一个ProxyFactoryBean实现里getObject() 方法所创建的对象。 这个方法将创建一个AOP代理,它包装了一个目标对象。

<bean id="parentDao" class="org.springframework.aop.framework.ProxyFactoryBean" abstract="true">

也就是说,accountDao其实是AOP代理。那代理做了什么呢?

他给Dao添加了名为daoRealizerAdvisor的拦截器,拦截所有对Dao的调用。而advice便是DAORealizer这个类。

<bean id="daoRealizer" class="x.dao.DAORealizer" />

<!-- The advisor for inject interceptor -->
<bean id="daoAutoRealizerAdvisor" class="org.springframework.aop.aspectj.AspectJExpressionPointcutAdvisor">
    <property name="advice" ref="daoAutoRealizer" />
    <property name="expression" value="execution(* com.dianping..dao..*.*(..)) and !execution(* com.dianping.avatar..dao..*.*(..))" />
</bean>

--------------

public class DAORealizer implements IntroductionInterceptor {

   @Override
   public Object invoke(final MethodInvocation methodInvocation) throws Throwable {

      // some pre-condition check

      DAOMethod daoMethod = DAOUtils
      .createDAOMethod(methodInvocation.getMethod(), methodInvocation.getArguments());

      final GenericDao genericDao = (GenericDao) methodInvocation.getThis();

      if (daoMethod.actionType == DAOActionType.LOAD) {
         return genericDao.executeLoad(daoMethod);
      }

      if (daoMethod.actionType == DAOActionType.QUERY) {
         return genericDao.executeQuery(daoMethod);
      }

      // ... other different type

      return methodInvocation.proceed();
   }
}

看到这里,真相大白,原来我们对于这个Dao的调用本来就已经是被spring的interceptor1给拦截了, DaoRealizer这个Advisor在我们的aop advice之前(怎么知道在我们的aop之前的?见下一段解释),而且他直接调用的便是return genericDao.executeXXX(daoMethod);,就不会调用之后的proceed方法了2.

就是如下代码片段,在此处设置断点,可以看到对某个join point的interceptors,并且interceptors是有顺序的,可以看到不同interceptor的先后关系

public class AdvisedSupport extends ProxyConfig implements Advised {

    public List<Object> getInterceptorsAndDynamicInterceptionAdvice(Method method, Class targetClass) {
        MethodCacheKey cacheKey = new MethodCacheKey(method);
        List<Object> cached = this.methodCache.get(cacheKey);
        if (cached == null) {
            cached = this.advisorChainFactory.getInterceptorsAndDynamicInterceptionAdvice(
                    this, method, targetClass);
            this.methodCache.put(cacheKey, cached);
        }
        return cached;
    }
}

解决方案

方案一:拦截真正的方法

既然他最后调用的是GenericDao的执行方法,那我们便可以拦截这个方法,完成更新

@AfterReturning(
        pointcut = "execution(public * x.dao.GenericDao.executeInsert(..))",
        returning = "id"
)
public void insertId(JoinPoint joinPoint, int id) {
    final DAOMethod daoMethod = (DAOMethod) joinPoint.getArgs()[0];
    if (daoMethod.getName().equals("addAccount")) {
        adAccountIDCache.insert(id);
    }
}

但是,显然这样对该DAO的实现有很大的依赖性,所以并不是很理想

方案二:在我们的Dao上层添加proxy

通过配置spring的bean的id,使得调用方使用我们的proxy,然后我们再调用真正的dao。
这样子,我们便可以拦截我们自己的proxy,进行更新操作。

public class AccountDaoProxy implements AccountDao {

    @Autowired
    private AccountDao realAccountDao;

    @Override
    @DAOAction(action = DAOActionType.UPDATE)
    public int delete(@DAOParam("accountId") int accountId) {
        return accountDao.delete(accountId);
    }
    //。。。
}


<bean id="accountDao" class="dao.AccountDaoProxy"/>

<bean id="realAccountDao" parent="parentDao">

@AfterReturning(
        pointcut = "execution(public * dao.AccountDaoProxy.delete(..))",
        returning = "affectedRowCount"
)
public void delete(JoinPoint joinPoint, int affectedRowCount) {}

但是运行后发现,直接这样调用的还是不行!

分析后发现,虽然我们是在AdAccountDaoProxy上添加的aop拦截,但是我们调用的方法还是AccountDao的deleteAccount方法(调用方使用的是接口!),而所有的Dao方法都被DaoRealizer拦截了,所以我们的proxy方法还是不会执行(即,在获取Advisor的时候,还是会获取原先的DaoAutoRealizer,从而调用executeUpdate然后返回。。。)

方案三:为我们的Dao添加一个interceptor

spring官网有如下介绍:

A child bean definition inherits configuration data from a parent definition. The child definition can override some values, or add others, as needed.

所以我们可以覆盖父类的interceptor,添加我们的advisor:

<property name="interceptorNames">
    <list>       
        <value>myAdvisor</value>       
        <value>daoRealizerAdvisor</value>
    </list>
</property>

 --------------

public class CacheUpdater implements IntroductionInterceptor {
    private void deleteId(int innerAccountId, int affectedRowCount) {
        if (affectedRowCount == 1) {
            accountCache.delete(innerAccountId);
        }
    }

    @Override
    public Object invoke(MethodInvocation methodInvocation) throws Throwable {
        final Object proceed = methodInvocation.proceed();
        final String methodName = methodInvocation.getMethod().getName();
        if (methodName.equals("addAccount")) {
            insertId((Integer) proceed);
        } else if (methodName.equals("deleteAccount")) {
            final Integer id = (Integer) methodInvocation.getArguments()[0];
            deleteId(id, (Integer) proceed);
        }
        return proceed;
    }

    private void insertId(int innerAccountId) {
        accountCache.insert(innerAccountId);
    }

}

方案四:Order

说到底,一开始的aop不能成功执行都是因为avatar的aop在我的aop之前,并且他直接返回了,没有继续调用之后的interceptor,所以我们要在他前面添加一个interceptor。
搜索之后发现,aspectJ直接提供了注解来做这个事情,即order,方便快捷

@Aspect
@Order(1)
public class CacheUpdater {

    @Autowired
    private AccountCache accountCache;

    @Around(value = "execution(public * dao.AccountDao+.addAccount(..))")
    public Object insertId(ProceedingJoinPoint pjp) {
        Object proceed = null;
        try {
            proceed = pjp.proceed();
            accountCache.insert((Integer) proceed);
        } catch (Throwable throwable) {
            throwable.printStackTrace();
        }
        return proceed;
    }
//。。。
}

Written with StackEdit.


  1. Advice: action taken by an aspect at a particular join point. Different types of advice include “around,” “before” and “after” advice. (Advice types are discussed below.) Many AOP frameworks, including Spring, model an advice as an interceptor, maintaining a chain of interceptors around the join point
  2. interceptors的结构类似与责任链或者是servlet中的filter:可以想象一个调用链,调用proceed则进入下一个节点,在中间某一环return就直接返回了,之后的interceptor或是真正的实现,都没有机会被调用了!也就是说我们之后添加的advice是不会被调用的!!

评论

此博客中的热门博文

Spring Boot: Customize Environment

Spring Boot: Customize Environment Environment variable is a very commonly used feature in daily programming: used in init script used in startup configuration used by logging etc In Spring Boot, all environment variables are a part of properties in Spring context and managed by Environment abstraction. Because Spring Boot can handle the parse of configuration files, when we want to implement a project which uses yml file as a separate config file, we choose the Spring Boot. The following is the problems we met when we implementing the parse of yml file and it is recorded for future reader. Bind to Class Property values can be injected directly into your beans using the @Value annotation, accessed via Spring’s Environment abstraction or bound to structured objects via @ConfigurationProperties. As the document says, there exists three ways to access properties in *.properties or *.yml : @Value : access single value Environment : can access multi...

Elasticsearch: Join and SubQuery

Elasticsearch: Join and SubQuery Tony was bothered by the recent change of search engine requirement: they want the functionality of SQL-like join in Elasticsearch! “They are crazy! How can they think like that. Didn’t they understand that Elasticsearch is kind-of NoSQL 1 in which every index should be independent and self-contained? In this way, every index can work independently and scale as they like without considering other indexes, so the performance can boost. Following this design principle, Elasticsearch has little related supports.” Tony thought, after listening their requirements. Leader notice tony’s unwillingness and said, “Maybe it is hard to do, but the requirement is reasonable. We need to search person by his friends, didn’t we? What’s more, the harder to implement, the more you can learn from it, right?” Tony thought leader’s word does make sense so he set out to do the related implementations Application-Side Join “The first implementation ...

Learn Spring Expression Language

When reading the source code of some Spring based projects, we can see some code like following: @Value( "${env}" ) private int value ; and like following: @Autowired public void configure (MovieFinder movieFinder, @ Value ("#{ systemProperties[ 'user.region' ] } ") String defaultLocale) { this.movieFinder = movieFinder; this.defaultLocale = defaultLocale; } In this way, we can inject values from different sources very conveniently, and this is the features of Spring EL. What is Spring EL? How to use this handy feature to assist our developments? Today, we are going to learn some basics of Spring EL. Features The full name of Spring EL is Spring Expression Language, which exists in form of Java string and evaluated by Spring. It supports many syntax, from simple property access to complex safe navigation – method invocation when object is not null. And the following is the feature list from Spring EL document : ...