1

Disclaimer: Yes, I know that @Transactional should only be put on Service methods, ideally.

I am supporting an old and poorly-designed application where all the business logic is cluttered in Controller methods, and those methods aren't transactional, so errors don't trigger rollback and the data is corrupted.

Our team needs a quick fix to enable error rollback for this legacy application, so it was decided to put @Transactional on Controller methods (rather than refactor the application which would be a major effort).

However, when I tried the following on a Controller method, it didn't roll back my exception:

@Controller
public class ReviewDetailsController extends BaseController{

@RequestMapping(value={"/testException"}, method = RequestMethod.POST)
@Transactional(readOnly = false, rollbackFor = Exception.class)
public void testException(@RequestParam(required = true) String planId) throws Exception {  

    // Simulate some simple DB change: we'll change a Date to a dummy value, 03:33:33
    Plans plan = planService.findById(new Integer(planId));
    PlanWorkflow pw = processService.getLatestWorkflow(plan);
    SimpleDateFormat sdf = new SimpleDateFormat("MM/dd/yyyy HH:mm:ss");
    Date d = sdf.parse("12/06/2019 03:33:33");
    pw.setCreatedDate(d);

    // Save it
    processService.savePlanWorkflow(pw);

    // Exception test
    throw new Exception("EXCEPTION TEST");          

}

On the other hand, when put the exact same logic inside a sample Service class method, it did roll back successfully.

Controller now calls Service method, the Service method has @Transactional

@RequestMapping(value={"/testException"}, method = RequestMethod.POST)
@Transactional(readOnly = false, rollbackFor = Exception.class)
public void testException(@RequestParam(required = true) String planId) throws Exception { 

    // Call a Service method with the exact same logic from the Controller
    processService.testException(planId);   
}

Service

@Component
public class ProcessServiceImpl implements ProcessService {

    @Override
    @Transactional(readOnly = false, rollbackFor = Exception.class)
    public void testException(@RequestParam(required = true) String planId) throws Exception {  

        // Simulate some simple DB change: we'll change a Date to a dummy value, 03:33:33
        Plans plan = planService.findById(new Integer(planId));
        PlanWorkflow pw = processService.getLatestWorkflow(plan);
        SimpleDateFormat sdf = new SimpleDateFormat("MM/dd/yyyy HH:mm:ss");
        Date d = sdf.parse("12/06/2019 03:33:33");
        pw.setCreatedDate(d);

        // Save it
        processService.savePlanWorkflow(pw);

        // Exception test
        throw new Exception("EXCEPTION TEST");          

    }

In theory, everyone says I should be able to attach @Transactional to Controller methods (even though it's not recommended -- but I still should be able to do it). So why don't the Controller methods pick it up or honor it?

applicationContext:

<context:component-scan base-package="mypackage.myapp">
</context:component-scan>

<jpa:repositories base-package="mypackage.myapp.dao"/>

<tx:annotation-driven transaction-manager="transactionManager"/>

<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
    <property name="entityManagerFactory" ref="entityManagerFactory"/>
</bean>

<bean id="entityManagerFactory" class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
    <property name="persistenceXmlLocation" value="/META-INF/persistence.xml"/>
</bean>

<cache:annotation-driven/> 

<bean id="cacheManager" class="org.springframework.cache.ehcache.EhCacheCacheManager"
      p:cache-manager-ref="ehcache"/>

<bean id="ehcache" class="org.springframework.cache.ehcache.EhCacheManagerFactoryBean"
      p:config-location="/WEB-INF/ehcache.xml"/>


Things I've tried:
  1. Make the Controller implement an Interface, to enable "JDK Proxy". Now the Controller implements ControllerInterface where the method signatures have been extracted (Source -> Refactor -> Extract Interface in Eclipse).
  2. Add <aop:aspectj-autoproxy proxy-target-class="true" /> to enable "CGLIB Proxy".

Related threads where I got these ideas from: 1, 2.

But none of this helped. Data still isn't getting rolled back from the Controller.

Similar thread: 1. No solution offered.

gene b.
  • 10,512
  • 21
  • 115
  • 227

1 Answers1

0

The only solution we found to this is to implement a new series of @Component classes (which we call "Delegates", but that's our term) which "mirror" the Controllers and contain the business logic that gets called from the Controller. So e.g.

ReviewController Each Method -> calls ReviewDelegate Each Method, and that's where the business logic resides for each method.

All these "Delegate" classes will observe Transactions (with rollback) because they're @Component, which is impossible directly with a Controller.

gene b.
  • 10,512
  • 21
  • 115
  • 227