I have my transactional public method which tries to delete many stuff from database.
Also there is database table, containing information about errors during this deletion process.
However, I need it to work in a way that when there is exception thrown during the deletion process, all deletions will be rolled back and a commit new entry to database with the exception stack trace.
My code looks something like this:
class Clazz{
#Transactional
public void classMethod(){
try {
deleteStuff();
} catch (Exception e) {
logRepository.save(new ErrorLog(e.getMessage()));
}
}
}
As far as I'm concerned, this code would not work, because the exception is caught in the try-catch block, so the log will be saved to database, but the deletions would not be rolled back.
I've read about TransactionSynchronizationAdapter#afterCompletion method, but there is no way to access the stack trace of the exception thrown from that method.
So my question is, is there any way to catch the exception, save stack trace to database and rollback all the other commits?
Thanks in advance.
Yes, there is.
You can write your own Interceptor.
For that you will want to have your own Annotation:
#InterceptorBinding
#Inherited
#Target({ TYPE, METHOD })
#Retention(RUNTIME)
#Documented
public #interface LogExceptions {
}
Then you have to annotate your Method:
class Clazz{
#Transactional(value = TxType.REQUIRES_NEW)
#LogExceptions
public void classMethod(){
deleteStuff();
}
}
And write a Interceptor:
#Interceptor
#LogExceptions
#Priority(Interceptor.Priority.PLATFORM_BEFORE + 100) //intercept before the TransactionalInterceptor at 200
public class LogExceptionsInterceptor {
public LogExceptionsInterceptor() {}
#Inject
LogBean logBean; //some bean to persist the exception
#AroundInvoke
public Object aroundInvoke(InvocationContext ic) throws Exception {
Object result = null;
LogEntry logEntry = new LogEntry(); //a entity for the stacktrace
try {
result = ic.proceed();
}catch (Exception e) {
StringWriter s = new StringWriter();
e.printStackTrace(new PrintWriter(s));
logEntry.setStacktrace(s.toString());
logBean.persist(logEntry);
}
return result;
}
}
Finally you have to activate the Interceptor in your beans.xml:
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://xmlns.jcp.org/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://xmlns.jcp.org/xml/ns/javaee http://xmlns.jcp.org/xml/ns/javaee/beans_2_0.xsd"
bean-discovery-mode="all" version="2.0">
<interceptors>
<class>your.package.LogExceptionsInterceptor</class>
</interceptors>
</beans>
Here's a link to a good tutorial:
https://rieckpil.de/howto-intercept-method-calls-using-cdi-interceptors/
Related
I am working on a spring-batch, where after reader and processor, writer is responsible to populate data to DB. Writer is calling Service which internally calls DAO layer. In method insertToDB() if some exception occurs the transaction is not being rolled back. PSB my code.
public class MyWriter{
#Autowired
private MyService myService;
#Override
public void write(List<? extends MyBO> list) {
try{
for(MyBO bo: list){
myService.insert(bo);
}
}
catch(Exception e){
log.error("Cant write to DB")
}
}
public class MyService{
#Autowired
private TableOneDAO tableOneDao;
#Autowired
private TableTwoDAO tableTwoDAO;
#Autowired
private TableThreeDAO tableThreeDAO;
public void insert(MyBO bo){
try{
// do other stuff of processing bo and create entity
MyEntityTableOne myentity1 = getEntityT1(bo);
MyEntityTableTwo myentity2 = getEntityT2(bo);
MyEntityTableThree myentity3 = getEntityT3(bo);
insertToDB(myEntity1,myEntity2,myEntity3);
}
catch(Exception e){
log.error("Error occured.");
throw new MyException("Error Blah blah occured");
}
}
#Transactional(value = "txn1")
public void insertToDB(MyEntityTableOne entity1, MyEntityTableTwo entity2, MyEntityTableThree entity3) {
try{
tableOneDao.insert(entity1);
tableTwoDAO.insert(entity2);
tableThreeDAO.insert(entity3);
}
catch(Exception e){
log.error("Error occured during insert to DB");
throw new MyException("Error Blah blah occured during DB insert");
}
}
The code goes to the catch block, but doesn't rollback records. If some error occurs during insert of Table2 then entry for Table1 is not rolled-back. And if occurs during table3 insertion then table1 and table2 records are not rolled-back.
If I move the #Transactional annotation to insert() method it works fine. What is root cause of this issue. What I have to do if I want to have transaction only on insertToDB() method.
I am trying to make it simple: To support #Transactional spring wraps the implementing class into a so called proxy and surrounds the method call / class with the transactional logic.
Now you are calling the #Transactional annotated method within the same class. Therefore the proxy is not invoked and the transactional does not work. When moving the annotation to your insert method you are invoking the method from outside of the class which means you invoke the method against the proxy.
Thats a limitation of Spring AOP (?) I think.
You can do something like following to achieve what you want:
public class MyService{
#Ressource
private MyService self;
...
self.insertToDB(myEntity1,myEntity2,myEntity3)
Your item writer will be already called in a transaction driven by Spring Batch and that you can configure at the step level by providing the transaction manager and transaction attributes. So there is no need to use #Transactional in the downstream service used by the writer.
You need to remove that annotation from MyService and it should work as expected.
#Service
public class TransactionClass{
#AutoWire
TransactionClass tranClass;
#Autowire
TransactionRepository transRepo;
public void methodA(Data data){
try{
methodB(data)
}catch(Exception e){
//some logic
}
}
public void methodB(Data data){
//some logic
tranClass.methodC(data)
}
#Transactional
public void methodC(Data data){
//some logic
transRepo.save(data);
throw new RuntimeException();
}
}
The problem is that the methodC() isn't getting rolled back even though an unchecked exception is thrown.
To check how transactions work using logs, just add this to the application.yaml
logging.level.org.springframework.transaction.interceptor: TRACE
logging.level.org.springframework.orm.jpa.JpaTransactionManager: DEBUG
logging.level.org.hibernate.SQL: DEBUG
spring.jpa.properties.hibernate.use_sql_comments: true
Transactional Method not rolling back as it had multi DB connection. So #Transactional will roll back for the only primary configuration and we can have only one primary configuration as well in an application. The solution to this is to use a chained transaction.
Refer to this link for furthermore on the chained transaction:
https://blog.usejournal.com/springboot-jpa-rollback-transaction-with-multi-databases-53e6f2f143d6
I am implementing an JEE7 web application. During my work i have found a problem with handling my custom exceptions.
I edited my account's property to have a non-unique login field. Then i invoked the AccountEditBean#editAccount() to run the editing process. When the process comes to AccountFacade#edit() i can see (in debug) that PersistenceException is caught and my custom NonUniqueException is thrown. The problem is, the exception is not propagated out of the facade class and it is not handled in AccountEditBean. Instead of that TransactionalException occurs right after throw:
WARNING: EJB5184:A system exception occurred during an invocation on
EJB ADMEndpoint, method: public void
pl.rozart.greatidea.adm.endpoints.ADMEndpoint.editAccount(pl.rozart.greatidea.entities.Account)
throws pl.rozart.greatidea.exceptions.BaseApplicationException
WARNING: javax.transaction.TransactionalException: Managed bean with
Transactional annotation and TxType of REQUIRES_NEW encountered
exception during commit javax.transaction.RollbackException:
Transaction marked for rollback.
Additional information:
NonUniqueException extends BaseApplicationException , which is marked as #ApplicationException(rollback=true).
Here's the code for the edit process:
AccountEditBean:
#Named(value = "accountEditBean")
#ViewScoped
public class AccountEditBean extends UtilityBean implements Serializable {
#Inject
ADMEndpointLocal admEndpoint;
private Account account;
public void editAccount() {
try {
admEndpoint.editAccount(this.account);
Messages.addInfo(ACCOUNT_DETAILS_FORM, KEY_CHANGES_SUCCESS);
} catch (NonUniqueException e) {
Messages.addError(ACCOUNT_DETAILS_FORM, e.getMessage());
} catch (BaseApplicationException e) {
Messages.addFatal(ACCOUNT_DETAILS_FORM, e.getMessage());
}
}
}
ADMEndpoint:
#Stateful
#Transactional(Transactional.TxType.REQUIRES_NEW)
#TransactionTracker
public class ADMEndpoint extends LoggingStateBean implements ADMEndpointLocal, SessionSynchronization {
#EJB(name = "ADMAccountFacade")
private AccountFacadeLocal accountFacade;
private Account account;
#Override
public void editAccount(Account account) throws BaseApplicationException {
this.account.setLogin(account.getLogin());
this.account.setEmail(account.getEmail());
accountFacade.edit(this.account);
}
}
ADMAccountFacade:
#Stateless(name = "ADMAccountFacade")
#Transactional(Transactional.TxType.MANDATORY)
#TransactionTracker
public class AccountFacade extends AbstractFacade<Account> implements AccountFacadeLocal {
#PersistenceContext(unitName = "myPU")
private EntityManager em;
#Override
public void edit(Account account) throws BaseApplicationException {
try {
em.merge(account);
em.flush();
} catch (PersistenceException e){
if(e.getMessage().contains(Account.CONSTRAINT_ACCOUNT_LOGIN_UNIQUE)){
throw new NonUniqueException(NonUniqueException.MSG_NON_UNIQUE_ACCOUNT_LOGIN, e);
}else{
throw new BaseDatabaseException(BaseDatabaseException.MSG_GENERAL_DATABASE_ERROR, e);
}
}
}
}
Do you know what could be the cause of the problem? It occurs in every of my facades, with all the custom exceptions.
I think you should change #Transactional to #TransactionAttribute because EJBs annotated with that. #Transactional is put on managedbean in java 7 not in EJBs...
I copied my comment here because i do not have enough points to squander :)
You are throwing an exception from a method whose invocation will be intercepted at runtime and additional logic wrapped around it:
transaction management;
exception handling.
Your exception cannot transparently jump over that logic, and the specification (probably) says a TransactionalException will be thrown, wrapping your original exception (again, probably---I am not that intimate with the details).
I am using Servlet->EJB->JPA on Glassfish 4. Application deploys successfully.
When I run the servlet, it updates the entity with id=1 in db, but doesn't do anything for the one with id=2. No exception is thrown.
#WebServlet("/AnimalServlet")
public class AnimalServlet extends HttpServlet {
#EJB AnimalDAOLocal lOBAnimalDAO;
protected void doGet(....) {
Animal lOBAnimal = lOBAnimalDAO.getAnimal(1); // gets OK
lOBAnimal.setName("Animal1"); // sets OK
lOBAnimalDAO.mergeAnimal(lOBAnimal); // updates in DB OK
lOBAnimal = lOBAnimalDAO.getAnimal(2); // gets OK
lOBAnimal.setName("Animal2"); // sets OK
lOBAnimalDAO.mergeAnimal(lOBAnimal); // doesn't update in DB.
}
And Session Bean Methods are :
#Stateless(mappedName = "AnimalDAOMapped")
public class AnimalDAO implements AnimalDAOLocal {
#PersistenceContext EntityManager em;
public Animal getAnimal(int id) {
return em.find(Animal.class, id);
}
public void mergeAnimal(Animal pOBAnimal) {
em.merge(pOBAnimal);
}
}
Persistence Unit Settings :
<persistence-unit name="JPATest" transaction-type="JTA">
<jta-data-source>jdbc/animaltest</jta-data-source>
<class>net.test.model.Animal</class>
</persistence-unit>
Use flush(); method, throw PersistenceException.Servlet catch that exception.
public void mergeAnimal(Animal pOBAnimal) throws PersistenceException {
try {
em.merge(pOBAnimal);
em.flush();
} catch (PersistenceException pe) {
throw e;
}
}
I solved the problem with the help of Chris' suggestion to turn on EclipseLink Logging ( Which I didn't know before ) .
The problem happened because of wrong #JoinColumn definition in the entity. And the first entity ( db=1 ) was committing by chance because of the existance of dummy data for this id which doesn't exist for other ids.
Is it possible to catch an exception in a CMT(Container Managed Transaction) stateless bean?
The code below wont catch any exeption when I tried it. If I use BMT(Bean Managed Transaction), I can catch the exception. But I want to remain with CMT.
#Path("books")
public class BookResource
{
#EJB
private BooksFacade book_facade;
private Books local_book;
#POST
#Consumes({"application/xml", "application/json"})
public Response create(Books entity)
{
try
{
book_facade.create(entity);
} catch (RuntimeException ex)
{
System.out.println("Caught database exception");
}
return Response.status(Response.Status.CREATED).build();
}
public class TXCatcher
{
//#Resource
//UserTransaction tx;
private final static Logger LOG = Logger.getLogger(TXCatcher.class.getName());
#AroundInvoke
public Object beginAndCommit(InvocationContext ic) throws Exception
{
//ic.proceed();
System.out.println("Invoking method: " + ic.getMethod());
try
{
//tx.begin();
Object retVal = ic.proceed();
//tx.commit();
return retVal;
}catch (RollbackException e)
{
LOG.log(Level.SEVERE, "-----------------Caught roolback(in interceptor): {0}", e.getCause());
System.out.println("Invoking method: " + ic.getMethod());
throw new CustomEx("Database error");
}catch (RuntimeException e)
{
LOG.log(Level.SEVERE, "-----------------Caught runtime (in interceptor): {0}", e.getCause());
System.out.println("Invoking method: " + ic.getMethod());
//tx.rollback();
throw new CustomEx("Database error",e.getCause());
//throw new CustomEx("Database error");
}
//return ic.proceed();
}
}
It depends what kind of problem you're trying to catch. You could try an explicit EntiyManager.flush, but depending on your data source isolation level, some errors cannot be caught until transaction commit, and there is no mechanism for catching transaction commit errors for a CMT. If that's the case, your only option is to use BMT (even though you said you don't want to). The only suggestion that might make that more palatable would be to write an EJB interceptor that behaves similarly to CMT (that is, inject UserTransaction into the interceptor, and begin/commit/rollback in the #AroundInvoke).
By placing the following above my function in my BooksFacade class create function, the CMT created a 2nd transaction within the first transaction. When the exception was thrown from the 2nd transaction, my BookResource class create method could catch it. No need for BMT.
#Overide
#TransactionAttribute(TransactionAttributeType.REQUIRES_NEW)
public void create(Books entity)
{
super.create(entity);
}
I noted that the annotation only works when placed on the individual methods, by placing it on the class itself wont make a difference.