Announcement Announcement Module
Collapse
No announcement yet.
How let @Transactional annotation works with Spring Security? Page Title Module
Move Remove Collapse
X
Conversation Detail Module
Collapse
  • Filter
  • Time
  • Show
Clear All
new posts

  • How let @Transactional annotation works with Spring Security?

    Hi everyone.

    I'm fazed by a transaction problem recently. the story becomes I involved Spring security 3.0 into my webapp project.

    At very beginning, I loaded all *-context.xml files by "DispatchServlet", the web.xml code was:
    Code:
    ...
    <!-- Servlet Dispatcher -->
    <servlet>
    	<servlet-name>Servlet Dispatcher</servlet-name>
    	<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    	<init-param>
    		<param-name>contextConfigLocation</param-name>
    		<param-value>
    			classpath:applicationContext.xml
    			classpath:applicationContext-database.xml;
    			classpath:applicationContext-servlet.xml;
    		</param-value>
    	</init-param>
    	<load-on-startup>1</load-on-startup>
    </servlet>
    
    <servlet-mapping>
    	<servlet-name>Servlet Dispatcher</servlet-name>
    	<url-pattern>/</url-pattern>
    </servlet-mapping>
    ...
    At beginning, Every thing was fine. But because spring security filter will be loaded before DispatcherServlet, so my customized authentication provider bean, which defined in applicationContext.xml, can't be found(ClassNotFoundException was thrown), I have to add the ContextLoaderListener in to load necessary beans before security filter works, and because some servlets and my authentication provider bean depends on dataSource and other beans, finally, I have to put all context files to ContextLoaderListener section, now my web.xml file looks like:
    Code:
    ...
    <listener>
    	<listener-class>org.springframework.web.context.request.RequestContextListener</listener-class>
    </listener>
    
    <listener>
    	<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
    </listener>
    <context-param>
    	<param-name>contextConfigLocation</param-name>
    	<param-value>
    		classpath:applicationContext.xml;
    		classpath:applicationContext-database.xml;
    		classpath:applicationContext-servlet.xml;
    	</param-value>
    </context-param>
    
    <!-- Servlet Dispatcher -->
    <servlet>
    	<servlet-name>Servlet Dispatcher</servlet-name>
    	<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
    	</servlet>
    	<servlet-mapping>
    	<servlet-name>Servlet Dispatcher</servlet-name>
    	<url-pattern>/</url-pattern>
    </servlet-mapping>
    ...
    And then, I found @Transactional annotation, which works before, doesn't work anymore.

    There I declared annotation drivers asserts in applicationContext.xml like the following:
    Code:
    ...
    <bean class="org.springframework.web.servlet.mvc.annotation.DefaultAnnotationHandlerMapping" />
    <bean class="org.springframework.web.servlet.mvc.annotation.AnnotationMethodHandlerAdapter" />
    
    <mvc:annotation-driven/>
    <context:component-scan base-package="com.wang.rms"/>
    <tx:annotation-driven transaction-manager="transactionManager"/>
    ...
    And applicationContext-database.xml:
    Code:
    ...
    <context:property-placeholder location="classpath:database.properties"/>
    <bean id="dataSource"		class="org.springframework.jdbc.datasource.DriverManagerDataSource">
    	<property name="driverClassName" 
                value="${database.connection.driver_class}"/>
    	<property name="url" value="${database.connection.url}"/>
    	<property name="username" value="${database.connection.username}"/>
    	<property name="password" value="${database.connection.password}"/>
    </bean>
    
    <bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
    	<property name="dataSource">
    		<ref bean="dataSource"/>
    	</property>
    </bean>
    
    <bean id="sqlSessionFactory" class="org.springframework.orm.ibatis3.SqlSessionFactoryBean">
    	<property name="configLocation" value="classpath:com/wang/rms/dao/ibatis/SqlMapConfig.xml" />
    	<property name="dataSource" ref="dataSource" />
    </bean>
    
    <bean id="credentialsDao"
    	class="org.springframework.orm.ibatis3.MapperFactoryBean">
    	<property name="sqlSessionFactory" ref="sqlSessionFactory" />
    	<property name="mapperInterface" value="com.wang.rms.dao.ibatis.CredentialDao" />
    </bean>
    
    <bean id="branchDao"
    	class="org.springframework.orm.ibatis3.MapperFactoryBean">
    	<property name="sqlSessionFactory" ref="sqlSessionFactory" />
    	<property name="mapperInterface" value="com.wang.rms.dao.ibatis.BranchDao" />
    </bean>
    ...
    There is one of my servlet controller:
    Code:
    package com.wang.rms.web.controllers;
    
    import java.io.IOException;
    import java.util.List;
    
    import javax.servlet.ServletException;
    import javax.validation.Valid;
    
    import org.springframework.beans.factory.annotation.Autowired;
    import org.springframework.security.access.prepost.PreAuthorize;
    import org.springframework.stereotype.Controller;
    import org.springframework.test.context.transaction.TransactionConfiguration;
    import org.springframework.transaction.annotation.Propagation;
    import org.springframework.transaction.annotation.Transactional;
    import org.springframework.ui.Model;
    import org.springframework.ui.ModelMap;
    import org.springframework.validation.BindingResult;
    import org.springframework.web.bind.WebDataBinder;
    import org.springframework.web.bind.annotation.InitBinder;
    import org.springframework.web.bind.annotation.ModelAttribute;
    import org.springframework.web.bind.annotation.PathVariable;
    import org.springframework.web.bind.annotation.RequestMapping;
    import org.springframework.web.bind.annotation.RequestMethod;
    
    import com.wang.rms.dao.ibatis.BanchDao;
    import com.wang.rms.dao.vo.Banch;
    import com.wang.rms.web.forms.validators.BranchFormValidator;
    import com.wang.rms.web.forms.vo.BranchForm;
    
    @Controller
    @PreAuthorize("hasRole('admin')")
    @RequestMapping(value="/admin/branch")
    @TransactionConfiguration(transactionManager="transactionManager", defaultRollback=false)
    @Transactional
    public class BranchController
    {
    	@Autowired
    	BranchDao branchDao;
    	@Autowired
    	private BranchFormValidator validator;
    	
    	@InitBinder
    	protected void initBinder(WebDataBinder binder) {
    		binder.setValidator(this.validator);
    	}
    
    	@RequestMapping(value={"/create"}, method = RequestMethod.GET)
    	protected String showBranchCreationForm(
    			ModelMap model)
    	{
    		BranchForm branchForm = new BranchForm();
    		model.put("branchForm", branchForm);
    		return "/admin/branch/create";
    	}
    
    	@RequestMapping(value={"/create"}, method = RequestMethod.POST)
    	@Transactional(propagation=Propagation.REQUIRED, 
    			readOnly=false,
    			rollbackFor=Exception.class)
    	protected String createBranch(
    			@Valid @ModelAttribute("branchForm") final BranchForm branchForm,
    			BindingResult result,
    			Model model) throws ServletException, IOException
    	{
    		if(result.hasErrors())
    		{
    			model.addAttribute("branchForm", branchForm);
    			return "/admin/branch/create";
    		}
    
    		banchDao.createBanch(branchForm);
    
    		model.addAttribute("newBranch", branchForm);
    		return "redirect:/admin/branch/" + branchForm.getId();
    	}
    }
    Dao bean's code is:
    Code:
    package com.wang.rms.dao.ibatis;
    
    import java.util.List;
    
    import com.wang.rms.dao.vo.Branch;
    
    public interface BranchDao 
    {
    	public void createBranch(Brancho);
    }
    I'm using ibatis3 as backend database framework.

    I also tried put tx:advice into applicationContext-database.xml
    Code:
    <tx:advice id="txAdvice" transaction-manager="transactionManager">
    	<tx:attributes>
    		<tx:method name="create*" propagation="REQUIRED"/>
    		<tx:method name="delete*" propagation="REQUIRED"/>
    		<tx:method name="update*" propagation="REQUIRED"/>
    		<tx:method name="*" read-only="true" />
    	</tx:attributes>
    </tx:advice>
    <aop:config>
    	<aop:pointcut id="ServiceOperation" expression="execution(* com.wang.rms.dao.ibatis.BranchDao.*(..))" />
    	<aop:advisor advice-ref="txAdvice" pointcut-ref="ServiceOperation" />
    </aop:config>
    but it still doesn't work.

    And, more strange thing is @Transactional annotation works with my testCase.
    package com.wang.rms.dao;

    import static org.junit.Assert.*;

    import java.util.List;

    import org.apache.log4j.LogManager;
    import org.apache.log4j.Logger;

    import org.junit.Test;
    import org.junit.runner.RunWith;
    import org.springframework.beans.factory.annotation.Autow ired;
    import org.springframework.test.annotation.Rollback;
    import org.springframework.test.context.ContextConfigurat ion;
    import org.springframework.test.context.junit4.SpringJUni t4ClassRunner;
    import org.springframework.test.context.transaction.Trans actionConfiguration;
    import org.springframework.transaction.annotation.Transac tional;

    import com.wang.rms.dao.ibatis.BranchDao;
    import com.wang.rms.dao.vo.Branch;

    @RunWith(SpringJUnit4ClassRunner.class)
    @ContextConfiguration(locations={"classpath:applic ationContext-dbunit-tests.xml","classpath:applicationContext-database.xml"})
    @TransactionConfiguration(transactionManager="tran sactionManager", defaultRollback=false)
    @Transactional
    public class BranchTests
    {
    protected final Logger logger = LogManager.getLogger(getClass());
    @Autowired
    private BranchDao branchDao;

    @Test
    @Rollback(true)
    public void testCreateBranch()
    {
    Branch newBranch = new Branch();
    newBranch.setName("New Branch");

    branchDao.createBranch(newBranch);

    org.junit.Assert.assertTrue(newBranch.getId()>0);
    logger.debug(newBranch);
    }
    }
    I think the differens between testcase and controller environment are:
    1. There's not security component works in test case but controller has.
    2. controllers are initiated by ContextLoaderListener in Tomcat, and test case is not.

    Anyway, I still have not idea about why my controller doesn't woks with @Transactional annotation, is there anybody can give me some advice?

  • #2
    Scan only for controllers in the configuration loaded by your servlet.

    Also you shouldn't load the web related stuff in your applicationContext.xml but in the dispatcherservlet specific configuration.

    Comment


    • #3
      Hi Marten, thanks for your answer, but where the component scaner should be put in to let @Transactional works? I find someone asked the same question here but no answer still:
      https://jira.springframework.org/browse/SPR-5082

      Comment


      • #4
        The question has been answered numerous times. You should have a component-scan that scan for @Service/@Repository in your root applicationcontext and one that scans for @Controller in your dispatcherservlet configuration.

        Use the forum search, as stated it has been answered numerous times before (I answered it a couple of times that is for sure!).

        Comment


        • #5
          It is not because component-scan exists or not, actually I always put it in root applicationContext.xml (see above code), it just doesn't work.

          But eventually I resolved it, the way is separate transactional code to a standalone bean and annotate by @Transactional, then load it in by contextloader, that's it! it seems like controller interceptor always skips transactional annotation but others care, I don't know why, but it works for me.

          Comment


          • #6
            Sigh... It matters all.... It also matters what I told you.... If you do a component-scan in both you end up with 2 instances of a bean one proxied/transactional in your root context and one in yourservlet which isn't. Hence the extra configuration...

            Comment


            • #7
              I only have one component scan in root, which loads transactional bean in, servlet does not have any transational things now, as they are moved out, it just call the trasactional bean, where is the extra configuration?

              Comment


              • #8
                In general you also have (@)Controllers, apparently you don't at least not detected by component scan. People tend to do a full component-scan in the root and a full component-scan in the servlet, which leads to double instances. As people tend to have a tx:annotation-driven in the root, those are proxied and those in the servlet config aren't. For wiring spring first looks in the own container (servlet) and then the parent, due to the double instances you get a unproxied instance. In most cases that is the issue at hand.

                At further looking at your code there are somethings you shouldn't do, mixing proxy strategies for 1 goal. You have aop:config/tx:advice for your transactions and you have a tx:annotation-driven which is strange.

                Next to that putting @Transactional on your weblayer isn't really a good place to put @Transactional, it should be on a service layer. Which is what you have done here, you created a service layer.

                Next you don't tell tx:annotation-driven to use classproxies your controllers don't implement interfaces so you should use classproxies. Which you should also tell to the security annotations.

                @TransactionConfiguration is an annotation for testing purpose (check the package) and shoudn't be used for this. Next to that due to the testing nature it is useless here.

                Comment


                • #9
                  Thank you Marten!!!

                  I know this thread is quite old but I had to ay THANK YOU Marten! You fixed the problem that I was struggling with for days.

                  Comment

                  Working...
                  X