Spring @Transaction chamada de método pelo método dentro da mesma classe, não funciona?

109

Eu sou novo na Spring Transaction. Algo que achei muito estranho, provavelmente entendi direito.

Eu queria ter um método transacional em torno do nível de método e tenho um método chamador na mesma classe e parece que não gosta disso, tem que ser chamado de uma classe separada. Não entendo como isso é possível.

Se alguém tiver uma ideia de como resolver esse problema, eu ficaria muito grato. Eu gostaria de usar a mesma classe para chamar o método transacional anotado.

Aqui está o código:

public class UserService {

    @Transactional
    public boolean addUser(String userName, String password) {
        try {
            // call DAO layer and adds to database.
        } catch (Throwable e) {
            TransactionAspectSupport.currentTransactionStatus()
                    .setRollbackOnly();

        }
    }

    public boolean addUsers(List<User> users) {
        for (User user : users) {
            addUser(user.getUserName, user.getPassword);
        }
    } 
}
Mike
fonte
Dê uma olhada na TransactionTemplateabordagem: stackoverflow.com/a/52989925/355438
Lu55
Sobre por que a auto-invocação não funciona, consulte 8.6 Mecanismos de proxy .
Jason Law

Respostas:

99

É uma limitação do Spring AOP (objetos dinâmicos e cglib ).

Se você configurar o Spring para usar AspectJ para lidar com as transações, seu código funcionará.

A alternativa simples e provavelmente a melhor é refatorar seu código. Por exemplo, uma classe que lida com usuários e outra que processa cada usuário. Então, o tratamento padrão da transação com Spring AOP funcionará.


Dicas de configuração para lidar com transações com AspectJ

Para permitir que o Spring use AspectJ para transações, você deve definir o modo para AspectJ:

<tx:annotation-driven mode="aspectj"/>

Se você estiver usando o Spring com uma versão anterior à 3.0, você também deve adicionar isso à configuração do Spring:

<bean class="org.springframework.transaction.aspectj
        .AnnotationTransactionAspect" factory-method="aspectOf">
    <property name="transactionManager" ref="transactionManager" />
</bean>
Espen
fonte
Obrigado pela informação. Refatorei o código por enquanto, mas você poderia me enviar um exemplo usando AspectJ ou fornecer alguns links úteis. Desde já, obrigado. Mike.
Mike
Adicionada configuração AspectJ específica da transação em minha resposta. Espero que ajude.
Espen de
10
Isso é bom! Btw: Seria bom se você pudesse marcar minha pergunta como a melhor resposta para me dar alguns pontos. (marca de seleção verde)
Espen
2
Configuração de inicialização do Spring: @EnableTransactionManagement (mode = AdviceMode.ASPECTJ)
VinyJones
64

O problema aqui é que os proxies AOP do Spring não estendem, mas envolvem sua instância de serviço para interceptar chamadas. Isso tem o efeito de que qualquer chamada para "this" de dentro de sua instância de serviço é chamada diretamente nessa instância e não pode ser interceptada pelo proxy de agrupamento (o proxy nem mesmo está ciente de tal chamada). Uma solução já foi mencionada. Outro ótimo seria simplesmente fazer com que o Spring injetasse uma instância do serviço no próprio serviço e chamasse seu método na instância injetada, que será o proxy que manipulará suas transações. Mas esteja ciente de que isso também pode ter efeitos colaterais negativos, se seu bean de serviço não for um singleton:

<bean id="userService" class="your.package.UserService">
  <property name="self" ref="userService" />
    ...
</bean>

public class UserService {
    private UserService self;

    public void setSelf(UserService self) {
        this.self = self;
    }

    @Transactional
    public boolean addUser(String userName, String password) {
        try {
        // call DAO layer and adds to database.
        } catch (Throwable e) {
            TransactionAspectSupport.currentTransactionStatus()
                .setRollbackOnly();

        }
    }

    public boolean addUsers(List<User> users) {
        for (User user : users) {
            self.addUser(user.getUserName, user.getPassword);
        }
    } 
}
Kai
fonte
3
Se você escolher seguir este caminho (se este é um bom design ou não, é outra questão) e não usar injeção de construtor, certifique-se de ver esta pergunta
Jeshurun
E se UserServicetiver escopo singleton? E se for o mesmo objeto?
Yan Khonski
26

Com o Spring 4 é possível autowired

@Service
@Transactional
public class UserServiceImpl implements UserService{
    @Autowired
    private  UserRepository repository;

    @Autowired
    private UserService userService;

    @Override
    public void update(int id){
       repository.findOne(id).setName("ddd");
    }

    @Override
    public void save(Users user) {
        repository.save(user);
        userService.update(1);
    }
}
Almas Abdrazak
fonte
2
MELHOR RESPOSTA !! Thx
mjassani
2
Corrija-me se eu estiver errado, mas esse padrão é realmente sujeito a erros, embora funcione. É mais como uma demonstração dos recursos do Spring, certo? Alguém não familiarizado com o comportamento de "esta chamada de bean" pode remover acidentalmente o bean autowired (os métodos estão disponíveis por meio de "this", afinal), o que pode causar problemas difíceis de detectar à primeira vista. Ele poderia até chegar ao ambiente de produção antes de ser encontrado).
pidabrow
2
@pidabrow você está certo, é um anti-padrão enorme e não é óbvio em primeiro lugar. Portanto, se você puder, deve evitá-lo. Se você tiver que usar o método da mesma classe, tente usar bibliotecas AOP mais poderosas, como AspectJ
Almas Abdrazak
21

A partir do Java 8, há outra possibilidade, que prefiro pelos motivos abaixo:

@Service
public class UserService {

    @Autowired
    private TransactionHandler transactionHandler;

    public boolean addUsers(List<User> users) {
        for (User user : users) {
            transactionHandler.runInTransaction(() -> addUser(user.getUsername, user.getPassword));
        }
    }

    private boolean addUser(String username, String password) {
        // TODO
    }
}

@Service
public class TransactionHandler {

    @Transactional(propagation = Propagation.REQUIRED)
    public <T> T runInTransaction(Supplier<T> supplier) {
        return supplier.get();
    }

    @Transactional(propagation = Propagation.REQUIRES_NEW)
    public <T> T runInNewTransaction(Supplier<T> supplier) {
        return supplier.get();
    }
}

Essa abordagem tem as seguintes vantagens:

1) Pode ser aplicado a métodos privados . Portanto, você não precisa interromper o encapsulamento tornando um método público apenas para satisfazer as limitações do Spring.

2) O mesmo método pode ser chamado em diferentes propagações de transações e cabe ao chamador escolher o adequado. Compare estas 2 linhas:

transactionHandler.runInTransaction(() -> userService.addUser(user.getUserName, user.getPassword));
transactionHandler.runInNewTransaction(() -> userService.addUser(user.getUserName, user.getPassword));

3) É explícito, portanto mais legível.

Bunarro
fonte
Isso é ótimo! Ele evita todas as armadilhas que o Spring apresenta com suas anotações. Adoro!
Frank Hopkins
Se eu estender TransactionHandlercomo uma subclasse, e a subclasse chamar esses dois métodos na TransactionHandlersuperclasse, ainda poderei obter os benefícios @Transactionalpretendidos?
tom_mai78101
6

Esta é minha solução para auto-invocação :

public class SBMWSBL {
    private SBMWSBL self;

    @Autowired
    private ApplicationContext applicationContext;

    @PostConstruct
    public void postContruct(){
        self = applicationContext.getBean(SBMWSBL.class);
    }

    // ...
}
Hlex
fonte
0

Você pode autowired BeanFactory dentro da mesma classe e fazer um

getBean(YourClazz.class)

Ele automaticamente fará o proxy de sua classe e levará em consideração sua anotação @Transactional ou outra anotação aop.

LionH
fonte
2
É considerada uma má prática. Até mesmo injetar o bean recursivamente nele mesmo é melhor. Usar getBean (clazz) é um acoplamento forte e forte dependência das classes Spring ApplicationContext dentro do seu código. Também obter o feijão por classe pode não funcionar no caso de embrulhar o feijão na primavera (a classe pode ser alterada).
Vadim Kirilchuk
0

O problema está relacionado a como classes de carregamento de mola e proxies. Não funcionará, até que você escreva seu método / transação interno em outra classe ou vá para outra classe e, em seguida, volte para sua classe e, em seguida, escreva o método de transcação aninhado interno.

Para resumir, os proxies spring não permitem os cenários que você está enfrentando. você tem que escrever o segundo método de transação em outra classe

Ujjwal Choudhari
fonte
0

Aqui está o que eu faço para pequenos projetos com uso apenas marginal de chamadas de método dentro da mesma classe. A documentação no código é altamente recomendada, pois pode parecer estranha para os colegas. Mas funciona com singletons , é fácil de testar, simples, rápido de conseguir e me poupa da instrumentação AspectJ completa. No entanto, para um uso mais intenso, aconselho a solução AspectJ conforme descrito na resposta do Espens.

@Service
@Scope(proxyMode = ScopedProxyMode.TARGET_CLASS)
class PersonDao {

    private final PersonDao _personDao;

    @Autowired
    public PersonDao(PersonDao personDao) {
        _personDao = personDao;
    }

    @Transactional
    public void addUser(String username, String password) {
        // call database layer
    }

    public void addUsers(List<User> users) {
        for (User user : users) {
            _personDao.addUser(user.getUserName, user.getPassword);
        }
    }
}
Mario Eis
fonte