24 Dec 2012

Spring AOP. Transaction manager на коленке.

Сегодня хочу рассказть вам немного об аспектах. Парадигма Аспектно-ориентированного программирования зарадилась довольно давно и теоритическую информацию можно свободно найти на просторах интернета. Java поддерживает работу с аспектами используя расширение именуемое AspectJ.
В своем примере я буду использовать Spring AOP. Этот подпроект написан полностью на Java, полностью понимает синтаксис AspectJ и использует свои собственные механизмы для weaving. Weaving - это процесс связывания аспектов с объектами приложения. Spring выполняет связывание в runtime. Если класс реализует интерфейс, то спринг обернет его в прокси средствами JDK не используя AspectJ. Но бывают случаи когда стандартных средств не достаточно и тогда в дело вступает AspectJ. Это, например, относится с созданным hibernate entity или @Configurable бинам. Интересно, что если изучить скомпилированный байт код, то можно увидеть, что некоторые типы advice реализуются с помощью шаблона proxy.
Следующий пример показывает как можно использовать аспекты для управления Hibernate сессиями и тразакциями. Естественно, он является только наглядным примером показывающем аспекты в действии и не притендует на что-то большое.

@Component
@Aspect
public class TransactionManagerWithAspects {
    public static final Logger log = LoggerFactory.getLogger(TransactionManagerWithAspects.class);

    @Autowired
    private SessionFactory sessionFactory;

    @Pointcut("execution(* app.dao.impl.*.find*(..))") 
    public void findEntry() {}

    @Pointcut("execution(* app.dao.impl.*.save*(..))") 
    public void saveEntry() {}
    
    @Pointcut("execution(* app.dao.impl.*.delete*(..)))") 
    public void deleteEntry() {}

    @Around("findEntry()")
    public Object readOnlyTx(ProceedingJoinPoint pjp) {
        Session session = null;
        Object methodResult = null;
        try {
            session = sessionFactory.getCurrentSession();
            methodResult = process(session, pjp);
        }
        catch (Throwable t) {
            log.error("Fatal error during invoke " + createMethodInfo(pjp), t);
        }
        finally {
            closeSession(session);
        }

        return methodResult;
    }

    @Around("deleteEntry() || saveEntry()")
    public Object commitTx(ProceedingJoinPoint pjp) {
        Session session = null;
        Object methodResult = null;
        try {
            session = beginTx();
            methodResult = process(session, pjp);
        }
        catch (Throwable t) {
            log.error("Fatal error during invoke " + createMethodInfo(pjp));
        }
        finally {
            finishTx(session);
        }

        return methodResult;
    }

    private Object process(Session session, ProceedingJoinPoint pjp) throws Throwable {
        BaseDAO baseDao = (BaseDAO) pjp.getTarget();
        baseDao.setSession(session);
        return pjp.proceed();
    }

    private String createMethodInfo(ProceedingJoinPoint pjp) {
        return new StringBuilder().
                append("method '").
                append(pjp.getSignature().getDeclaringTypeName()).
                append(".").
                append(pjp.getSignature().getName()).
                append("' with args '").
                append(Arrays.toString(pjp.getArgs())).
                toString();
    }

    private void finishTx(Session session) {
        try {
            if (session != null) {
                session.getTransaction().commit();
            }
        }
        catch (Exception e) {
            log.error("Error during commit session. Transaction will be rolled back.", e);
            if (session != null && session.getTransaction() != null) {
                session.getTransaction().rollback();
            }
        }
        finally {
            closeSession(session);
        }
    }

    private void closeSession(final Session session) {
        if (session != null && session.isOpen()) {
            session.close();
        }
    }

    private Session beginTx() throws HibernateException {
        Session session = sessionFactory.getCurrentSession();
        session.getTransaction().begin();
        return session;
    }
}

В классе объявлено несколько pointcuts которые замаплены на методы для чтения, сохранения и удаления сущностей. После pointcut следуют advices, декларирующие экшены, которые будут вызываться в точках pointcut. BaseDAO реализует простой CRUD для всех сущностей. Методы на чтение подразумевается, что будут работать вне транзакций.
А теперь представьте, что будет, если сюда дописать обработку эксепшенов и pointcuts замапить не по имени метода, а на аннотацию @Transactional. Ничего не напоминает? :)

Read more