Статьи

Видимость весенних транзакций

Spring при инициализации контекста приложения создает прокси, когда сталкивается с классами, помеченными @Transactional. @Transactional может применяться на уровне класса или метода. Применение его на уровне класса означает, что все открытые методы, определенные в классе, являются транзакционными. Тип прокси, который Spring создает, например, Jdk Proxy или CGLIB proxy, зависит от класса, в котором метод помечен как транзакционный. Если класс реализует хотя бы один интерфейс, Spring создает динамический прокси Jdk. Этот прокси реализует тот же интерфейс, что и исходный класс, и перехватывает методы интерфейса с помощью логики обслуживания транзакций. Он делегирует вызов исходному объекту, составленному в нем. Предположим, что класс не реализует никакого интерфейса, Spring создает прокси CGLIB. Этот прокси расширяет исходный класс и переопределяет открытые методы. Мы рассмотрим это в ближайшее время. Предположим, у нас есть класс, определенный следующим образом:

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
public interface BookDao{
    void buyBook(String isbn) throws BookNotFoundException;
    Book findByIsbn(String isbn);
    int deductStock(Book book);
}
 
public class JdbcBookDao implements BookDao{
    void buyBook(String isbn) throws BookNotFoundException{
        Book book = findByIsbn(isbn);
        if(book == null){
        throw new BookNotFoundException();
        }
        deductStock(book);
    }
 
    @Transactional(propagation=Propagation.REQUIRES_NEW)
    Book findByIsbn(String isbn){
        Book book = getJdbcTemplate().queryForObject("SELECT * FROM BOOK WHERE ISBN=?",
        ParameterizedBeanPropertyRowMapper.newInstance(Book.class), isbn);
        return book;
    }
 
    @Transactional(propagation=Propagation.REQUIRES_NEW)
        int deductStock(Book book){
        String sql = "UPDATE BOOK_STOCK SET STOCK=STOCK-1 WHERE BOOK_ID=?";
        return getJdbcTemplate().update(sql, stockIncrement, book.getId());
    }
}

Будет ли Spring автоматически создавать транзакцию при вызове метода findByIsbn bookDao из метода main? Нет. Мы должны объявить это в конфигурации xml:

1
<tx:annotation-driven>

Так что, если он не создает транзакцию, он выдаст ошибку? Ответ снова — нет. Spring выполняет это утверждение без транзакций.

Как только мы объявили ‹tx: annotation-driven›, как и ожидалось, Spring создает динамический прокси Jdk для JdbcBookDao, поскольку класс реализует интерфейс. Теперь, скажем, мы вызываем метод buyBook в JdbcBookDao, сколько транзакций создается Spring?

  1. Два: 1 для findByIsbn и еще 1 для deductStock.
  2. Первый: оба находятByIsbn и deductStock в одной передаче.
  3. Нет транзакции на всех!

Ответ (3). Режим транзакции по умолчанию — «прокси» для транзакций. Это означает, что вызовы методов, выполняемые только через прокси, будут рассматриваться Spring для автоматического управления транзакциями. Теперь, если вы внимательно наблюдаете, метод buyBook не помечен как транзакционный. И, следовательно, при создании транзакционного прокси этот метод не перехватывается логикой управления транзакциями, поскольку он не помечен как @Transactional. Короче говоря, buyBook не переопределяется в прокси. И, следовательно, метод напрямую вызывается для исходного объекта. И поэтому два других метода также вызывают для исходного объекта. Помните, что только PROXY содержит код управления транзакциями. Таким образом, поскольку другие методы также вызываются для исходного объекта, Spring вообще не создает транзакцию. Теперь проблема будет решена, если мы отметим buyBook как @Transactional? Будет ли Spring создавать две отдельные транзакции для каждого метода findByIsbn и deductStock?

Нет. Spring создает только одну транзакцию, когда вызывается buyBook (). Это не приведет к созданию новой транзакции, так как соответствующие методы вызываются для самого исходного объекта, а не для прокси. Так как решить эту проблему?

Можем ли мы попросить Spring создать прокси-сервер CGLIB ()? Теперь, когда прокси является подклассом с переопределенными открытыми транзакционными методами, он создает новую транзакцию для каждого вызова метода? Опять НЕТ. Прокси-сервер CGLIB напрямую не вызывает метод в суперклассе. Вот примерное представление о том, как это реализовано.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
23
24
25
26
27
28
29
30
31
32
33
34
35
36
37
38
39
40
41
42
43
import net.sf.cglib.proxy.MethodInterceptor;
import net.sf.cglib.proxy.MethodProxy;
import java.lang.reflect.Method;
 
public class MyInterceptor implements MethodInterceptor {
    private Object realObject;
 
    public MyInterceptor(Object original) {
        this.realObject = original;
    }
 
    // This method will be called every time the object proxy calls any of its methods
    public Object intercept(Object o, Method method, Object[] args,
                          MethodProxy methodProxy) throws Throwable {
        /**
        * Transaction Management Code
        */
 
        // Invoke the method on the real object with the given params
        Object res = method.invoke(realObject, args);
 
        /**
        * Transaction Management Code
        */
        return res;
    }
}
 
import net.sf.cglib.proxy.Enhancer;
 
public class ProxyCreator(){
 
    public static T createProxy(T original){
        // Enhancer is CGLIB class which builds a dynamic proxy with new capabilities
        Enhancer e = new Enhancer();
        e.setSuperclass(original.getClass());
 
        // We have to declare the interceptor whose 'intercept' will be called when methods are called on proxy.
        e.setCallback(new MyInterceptor(original));
        T proxy = (T) e.create();
        return proxy;
    }
}

Итак, как вы видели здесь, прокси расширяет исходный класс и состоит из его объекта. Поэтому, когда мы вызываем buyBook, прокси-сервер создает транзакцию и делегирует вызов исходному объекту. Нет, поскольку findByIsbn и deductStock вызываются из buyBook исходного объекта, новые транзакции не создаются.

Быстрое решение проблемы для этого было бы, так как JdbcBookDao является одиночным, получить этот объект из контекста приложения. Теперь вместо вызова методов непосредственно для объекта, вызовите его, используя ссылку (чтобы убедиться, что прокси вызывается) Вот как теперь может выглядеть метод.

01
02
03
04
05
06
07
08
09
10
11
12
13
14
15
16
17
18
19
20
21
22
public class JdbcBookDao implements BookDao, ApplicationContextAware{
    private ApplicationContext context;
    private BookDao bookDao;
 
    public void setApplicationContext(ApplicationContext context){
        this.context = context;
    }
 
    public BookDao getBookDao(){
        bookDao = (BookDao)context.getBean("jdbcBookDao");
    }
 
    void buyBook(String isbn) throws BookNotFoundException{
        Book book = getBookDao().findByIsbn(isbn);
        if(book == null){
            throw new BookNotFoundException();
        }
        getBookDao().deductStock(book);
    }
 
    .....
}

Просто реализовал сырую версию, чтобы все заработало. Мы можем определенно улучшить его дизайн. Вместо непосредственного внедрения контекста приложения в DAO, может быть, у нас может быть своего рода вспомогательный класс, который делает это. Или другой альтернативой для достижения этой цели является использование программных транзакций.

И последнее замечание: Spring управляет транзакциями только тогда, когда открытые методы помечены как транзакционные. Для закрытых, защищенных и закрытых пакетов Spring не предоставляет поддержку управления транзакциями. В случае динамических прокси, поскольку они реализуют интерфейс, все транзакционные методы являются открытыми. Поэтому не нужно беспокоиться о закрытых методах. А в случае прокси CGLIB только публичные методы переопределяются при создании подкласса. Так что даже здесь непубличные методы не рассматриваются.

Позвольте мне закончить эту дискуссию вопросом. Когда я попытался проксировать целевой класс с помощью ‹tx: annotation-based proxy-target-class =” true ”/›, он действительно не работал, т.е. прокси CGLIB не создавались . Чтобы это работало, я должен сделать небольшой взлом. Как ясно сказано в документации Spring, если proxy-target-class включен на любом из: ‹tx: annotation-driven›, ‹aop: config› или ‹aop: aspectj-autoproxy›, Spring включит создание прокси CGLIB на контейнере. Поэтому я просто создал пустой ‹aop: config proxy-target-class =” true ”/›. И не волнуйтесь, все заработало! Не уверен, что это ошибка в самой Spring. Высоко ценится, если кто-то может ответить на это.