For web MVC Spring app should @Transactional go on controller or service?
Asked Answered
D

3

17

For WebApplicationContext, should I put @Transactional annotations in the controller or in services? The Spring docs have me a bit confused.

Here is my web.xml:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xmlns:web="http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" id="WebApp_ID" version="2.5">
  <display-name>Alpha v0.02</display-name>
  <servlet>
    <servlet-name>spring</servlet-name>
    <servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    <load-on-startup>1</load-on-startup>
  </servlet>

  <servlet-mapping>
    <servlet-name>spring</servlet-name>
    <url-pattern>*.htm</url-pattern>
  </servlet-mapping>

  <servlet-mapping>
    <servlet-name>spring</servlet-name>
    <url-pattern>*.json</url-pattern>
  </servlet-mapping>

  <welcome-file-list>
    <welcome-file>index.jsp</welcome-file>
  </welcome-file-list>
</web-app>

Here is my application-context.xml defining a spring dispatcher servlet:

<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
        xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
        xmlns:context="http://www.springframework.org/schema/context"
        xmlns:mvc="http://www.springframework.org/schema/mvc"
        xmlns:tx="http://www.springframework.org/schema/tx"
        xsi:schemaLocation="
            http://www.springframework.org/schema/tx
            http://www.springframework.org/schema/tx/spring-tx.xsd
            http://www.springframework.org/schema/beans    
            http://www.springframework.org/schema/beans/spring-beans.xsd
            http://www.springframework.org/schema/context 
            http://www.springframework.org/schema/context/spring-context.xsd
            http://www.springframework.org/schema/mvc 
            http://www.springframework.org/schema/mvc/spring-mvc.xsd">

    <context:annotation-config />
    <mvc:annotation-driven />
    <tx:annotation-driven />

    <context:component-scan base-package="com.visitrend" />

    <bean id="viewResolver" class="org.springframework.web.servlet.view.InternalResourceViewResolver">
        <property name="prefix" value="/WEB-INF/jsp/"/>
        <property name="suffix" value=".jsp"/>
    </bean>

     <bean id="dataSource"  class="com.mchange.v2.c3p0.ComboPooledDataSource" destroy-method="close">
        <property name="driverClass" value="org.postgresql.Driver" />
        <property name="jdbcUrl" value="jdbc:postgresql://localhost:5432/postgres" />
        <property name="user" value="someuser" />
        <property name="password" value="somepasswd" />
    </bean>

    <bean id="sessionFactory" class="org.springframework.orm.hibernate4.LocalSessionFactoryBean">
        <property name="dataSource" ref="dataSource" />
        <property name="configLocation" value="classpath:test.hibernate.cfg.xml" />
    </bean>

    <bean id="transactionManager" class="org.springframework.orm.hibernate4.HibernateTransactionManager">
      <property name="dataSource" ref="dataSource" />
      <property name="sessionFactory" ref="sessionFactory" />
    </bean>    
</beans>

Here's a service interface:

public interface LayerService {
    public void createLayer(Integer layerListID, Layer layer);
}

Here's a service implementation:

@Service
public class LayerServiceImpl implements LayerService {

    @Autowired
    public LayerDAO layerDAO;

    @Transactional
    @Override
    public void createLayer(Integer layerListID, Layer layer) {
        layerDAO.createLayer(layerListID, layer);
    }
}

And here's my controller:

@Controller
public class MainController {

    @Autowired
    private LayerService layerService;

    @RequestMapping(value = "/addLayer.json", method = RequestMethod.POST)
    public @ResponseBody
    LayerListSetGroup addLayer(@RequestBody JSONLayerFactory request) {
        layerService.createLayer(request.getListId(), request.buildLayer());
        return layerService.readLayerListSetGroup(llsgID);
    }
}

The Spring documentation has me a bit confused. It seems to indicate that using a WebApplicationContext means only controllers will be investigated for @Transactional annotations and not services. Meanwhile I see tons of recommendations to make services transactional and not controllers. I'm thinking that using <context:component-scan base-package="com..." /> in our spring-servlet.xml above so that it includes the services packages means the services are part of the context, and therefore will be "investigated" for transactional annotations. Is this accurate?

Here's the Spring documentation blurb that got me confused:

@EnableTransactionManagement and only looks for @Transactional on beans in the same application context they are defined in. This means that, if you put annotation driven configuration in a WebApplicationContext for a DispatcherServlet, it only checks for @Transactional beans in your controllers, and not your services.

Further, is there any performance implications or "badness" if I define a controller method as transactional, and it calls a transactional method in a another class? My hunch is no, based on the documentation, but would love validation on that.

Dabney answered 25/3, 2013 at 16:49 Comment(0)
S
18

There is no requirement for whether the @Transactional annotation should go on a Controller or on a Service, but typically it would go on a Service that would perform the logic for one request that logically should be performed within one ACID transaction.

In a typical Spring MVC application, you would have, minimally, two contexts: the application context and the servlet context. A context is a sort of configuration. The application context holds the configuration that is relevant for your entire application, whereas the servlet context holds configuration relevant only to your servlets. As such, the servlet context is a child of the application context and can reference any entity in the application context. The reverse is not true.

In your quote,

@EnableTransactionManagement and only looks for @Transactional on beans in the same application context they are defined in. This means that, if you put annotation driven configuration in a WebApplicationContext for a DispatcherServlet, it only checks for @Transactional beans in your controllers, and not your services.

@EnableTransactionManagement looks for @Transactional in beans in packages declared in the @ComponentScan annotation but only in the context (@Configuration) they are defined in. So If you have a WebApplicationContext for your DispatcherServlet (this is a servlet context), then @EnableTransactionManagement will look for @Transactional in classes you told it to component scan in that context (@Configuration class).

@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages = "my.servlet.package")
public class ServletContextConfiguration {
    // this will only find @Transactional annotations on classes in my.servlet.package package
}

Since your @Service classes are part of the Application context, if you want to make those transactional, then you need to annotate your @Configuration class for the Application Context with @EnableTransactionManagement.

@Configuration
@EnableTransactionManagement
@ComponentScan(basePackages = "my.package.services")
public class ApplicationContextConfiguration {
    // now this will scan your my.package.services package for @Transactional
}

Use your Application Context configuration with a ContextLoaderListener and your Servlet Context configuration when instantiating your DispatcherServlet. (See the javadoc for a full java based config, instead of xml, if you aren't doing it already.)

Addendum: @EnableTransactionManagement has the same behavior as <tx:annotation-driven /> in a java configuration. Check here for using ContextLoaderListener with XML.

Sperry answered 25/3, 2013 at 20:41 Comment(2)
Thanks! That really drives it home for me.Dabney
You're welcome. I wish this was clearer to me as well when I started Spring MVC.Sperry
V
5

The Service is the best place for putting transactional demarcations. The service should hold the detail-level use case behavior for a user interaction, meaning stuff that would logically go together in a transaction. Also that way a separation is maintained between web application glue code and business logic.

There are a lot of CRUD applications that don't have any significant business logic, for them having a service layer that just passes stuff through between the controllers and data access objects is not useful. In those cases you could get away with putting the transaction annotation on the data access objects.

Putting the transactional annotation on the controller can cause problems, see [the Spring MVC documentation][1], 17.3.2:

A common pitfall when working with annotated controller classes happens when applying functionality that requires creating a proxy for the controller object (e.g. @Transactional methods). Usually you will introduce an interface for the controller in order to use JDK dynamic proxies. To make this work you must move the @RequestMapping annotations, as well as any other type and method-level annotations (e.g. @ModelAttribute, @InitBinder) to the interface as well as the mapping mechanism can only "see" the interface exposed by the proxy. Alternatively, you could activate proxy-target-class="true" in the configuration for the functionality applied to the controller (in our transaction scenario in ). Doing so indicates that CGLIB-based subclass proxies should be used instead of interface-based JDK proxies. For more information on various proxying mechanisms see Section 9.6, “Proxying mechanisms”.

The transaction propagation behaviors that you set up on the attributes decide what happens when a transactional method calls another transactional method. You can configure it so that the method called uses the same transaction, or so that it always uses a new transaction.

By having multiple calls to your service in the example code you're defeating the transactional purpose of the service. The different calls to your service will execute in different transactions if you put the transactional annotations on the service.

Vassallo answered 25/3, 2013 at 19:42 Comment(6)
Do you know why the Spring documentation was referring to only controller annotations being read? Is this considering no component scan? Here's a link to the most recent docs where I found this: static.springsource.org/spring/docs/3.2.x/…Dabney
@user2208384: It's just warning you that the scanning will only find things within the same application context. so if you want to annotate services, put that configuration in the application context that manages the services.Vassallo
How do you put the services in the same WebApplicationContext? I think the component scan does that if you include your services packages, but want to make sure. In other words, do my example files not already do that?Dabney
@User2208384: i haven't tried it out, but looking at it i think that your example file should work ok.Vassallo
If LayerServiceImpl is in package com.visitrend, its annotated methods will be performed within a transaction.Sperry
+1 for the reference to Spring docs and proxy issues. Here's a current link.Planter
E
0

Sometimes it is very convenient to have @Transactional controller methods, especially when performing trivial operations using Hibernate. To enable this using XML configuration, add this to your dispatch-servlet.xml:

<beans ...
 xmlns:tx="http://www.springframework.org/schema/tx"
 xsi:schemaLocation="...
 http://www.springframework.org/schema/tx
 http://www.springframework.org/schema/tx/spring-tx-3.2.xsd">    
  <tx:annotation-driven transaction-manager="transactionManager"
   proxy-target-class="true" />
  ..
</beans>

The purpose of proxy-target-class is to use the CGLIB proxies which are required for AOP on controllers to work. If you don't add this, you'll get an error when starting up. Also, if you have any final methods in your controllers, note that they cannot be proxied (in particular, made transactional), and you will also get a warning from CGLIB for each of these methods, when starting up.

Evenfall answered 26/4, 2015 at 6:22 Comment(0)

© 2022 - 2024 — McMap. All rights reserved.