Spring @Transaction llamada al método por el método dentro de la misma clase, ¿no funciona?

109

Soy nuevo en Spring Transaction. Algo que encontré realmente extraño, probablemente lo entendí bien.

Quería tener un nivel de método transaccional y tengo un método de llamada dentro de la misma clase y parece que no le gusta eso, tiene que ser llamado desde la clase separada. No entiendo cómo es eso posible.

Si alguien tiene una idea de cómo resolver este problema, se lo agradecería enormemente. Me gustaría usar la misma clase para llamar al método transaccional anotado.

Aquí está el 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);
        }
    } 
}
Miguel
fuente
Eche un vistazo al TransactionTemplateenfoque: stackoverflow.com/a/52989925/355438
Lu55
Acerca de por qué la autoinvocación no funciona, consulte 8.6 Mecanismos de proxy .
Jason Law

Respuestas:

99

Es una limitación de Spring AOP (objetos dinámicos y cglib ).

Si configura Spring para usar AspectJ para manejar las transacciones, su código funcionará.

La alternativa más simple y probablemente la mejor es refactorizar su código. Por ejemplo, una clase que maneja usuarios y otra que procesa a cada usuario. Entonces funcionará el manejo de transacciones predeterminado con Spring AOP.


Consejos de configuración para manejar transacciones con AspectJ

Para permitir que Spring use AspectJ para transacciones, debe establecer el modo en AspectJ:

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

Si está utilizando Spring con una versión anterior a la 3.0, también debe agregar esto a su configuración de Spring:

<bean class="org.springframework.transaction.aspectj
        .AnnotationTransactionAspect" factory-method="aspectOf">
    <property name="transactionManager" ref="transactionManager" />
</bean>
Espen
fuente
Gracias por la información. Refactoricé el código por ahora, pero ¿podría enviarme un ejemplo usando AspectJ o proporcionarme algunos enlaces útiles? Gracias por adelantado. Miguel.
Mike
Se agregó la configuración de AspectJ específica de la transacción en mi respuesta. Espero que ayude.
Espen
10
¡Eso es bueno! Por cierto: sería bueno si pudiera marcar mi pregunta como la mejor respuesta para darme algunos puntos. (marca de verificación verde)
Espen
2
Configuración de arranque de primavera: @EnableTransactionManagement (mode = AdviceMode.ASPECTJ)
VinyJones
64

El problema aquí es que los proxies AOP de Spring no extienden, sino que envuelven su instancia de servicio para interceptar llamadas. Esto tiene el efecto de que cualquier llamada a "this" desde dentro de su instancia de servicio se invoca directamente en esa instancia y no puede ser interceptada por el proxy de envoltura (el proxy ni siquiera es consciente de dicha llamada). Ya se menciona una solución. Otro ingenioso sería simplemente hacer que Spring inyecte una instancia del servicio en el servicio mismo y llamar a su método en la instancia inyectada, que será el proxy que maneja sus transacciones. Pero tenga en cuenta que esto también puede tener efectos secundarios negativos, si su bean de servicio no es un 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
fuente
3
Si elige seguir esta ruta (si este es un buen diseño o no es otro asunto) y no usa la inyección del constructor, asegúrese de ver también esta pregunta
Jeshurun
¿Y si UserServicetiene alcance singleton? ¿Y si es el mismo objeto?
Yan Khonski
26

Con Spring 4 es posible conectarse automáticamente

@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
fuente
2
LA MEJOR RESPUESTA !! Thx
mjassani
2
Corrígeme si me equivoco, pero ese patrón es muy propenso a errores, aunque funciona. Es más como una muestra de las capacidades de Spring, ¿verdad? Alguien que no esté familiarizado con el comportamiento de "this bean call" podría eliminar accidentalmente el bean autoamplificado (después de todo, los métodos están disponibles a través de "this"), lo que podría causar problemas que son difíciles de detectar a primera vista. Incluso podría llegar al entorno de producción antes de que se encuentre).
pidabrow
2
@pidabrow tienes razón, es un patrón anti enorme y no es obvio en primer lugar. Entonces, si puedes, debes evitarlo. Si tiene que usar un método de la misma clase, intente usar bibliotecas AOP más potentes como AspectJ
Almas Abdrazak
21

A partir de Java 8 hay otra posibilidad, que prefiero por las razones que se indican a continuación:

@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();
    }
}

Este enfoque tiene las siguientes ventajas:

1) Puede aplicarse a métodos privados . Por lo tanto, no tiene que romper la encapsulación haciendo público un método solo para satisfacer las limitaciones de Spring.

2) Se puede llamar al mismo método dentro de una propagación de transacción diferente y es el llamador quien debe elegir el adecuado. Compare estas 2 líneas:

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

3) Es explícito, por lo tanto más legible.

Bunarro
fuente
¡Esto es genial! Evita todas las trampas que Spring introduce con su anotación de lo contrario. ¡Quiéralo!
Frank Hopkins
Si me extiendo TransactionHandlercomo una subclase, y la subclase llama a estos dos métodos en la TransactionHandlersuperclase, ¿podré seguir obteniendo los beneficios @Transactionalprevistos?
tom_mai78101
6

Esta es mi solución para la autoinvocación :

public class SBMWSBL {
    private SBMWSBL self;

    @Autowired
    private ApplicationContext applicationContext;

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

    // ...
}
Hlex
fuente
0

Puede conectar automáticamente BeanFactory dentro de la misma clase y hacer una

getBean(YourClazz.class)

Proxificará automáticamente su clase y tendrá en cuenta su @Transactional u otra anotación aop.

LionH
fuente
2
Se considera una mala práctica. Incluso inyectar el frijol de forma recursiva en sí mismo es mejor. Usar getBean (clazz) es un acoplamiento estrecho y una fuerte dependencia de las clases de Spring ApplicationContext dentro de su código. Además, es posible que la obtención de frijoles por clase no funcione en caso de que la primavera envuelva el frijol (la clase puede cambiarse).
Vadim Kirilchuk
0

El problema está relacionado con la forma en que el resorte carga las clases y los proxies. No funcionará, hasta que escriba su método / transacción interno en otra clase o vaya a otra clase y luego vuelva a su clase y luego escriba el método de transcación anidado interno.

En resumen, los proxies de primavera no permiten los escenarios a los que se enfrenta. tienes que escribir el segundo método de transacción en otra clase

Ujjwal Choudhari
fuente
0

Esto es lo que hago para proyectos pequeños con un uso marginal de llamadas a métodos dentro de la misma clase. Se recomienda encarecidamente la documentación en código, ya que puede parecer extraño para los colegas. Pero funciona con singleton , es fácil de probar, simple, rápido de lograr y me ahorra la instrumentación AspectJ completa. Sin embargo, para un uso más intenso, recomendaría la solución AspectJ como se describe en la respuesta de 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
fuente