问题
In my Spring MVC application, I have a method in a controller that needs to save a bunch of objects (built from an uploaded file) to a database. Let us leave aside for the moment the whole question about whether transactions should be done in the controller or service layer -- the point is that it should technically be feasible to do it in the controller, but I am finding problems. If you look at the code below, what I am expecting is that if any of the three calls to saveContact fails with an Exception (any exception, since I put rollbackFor = Exception.class ), then all three should be rolled back. Still, what I see is that if for example the third one fails, the data from the first two is still present in the database. The exception thrown is a PersistenceException, so I believe this should trigger the rollback, but it doesn't (it bubbles up to the client's browser, which is what I expected since I'm not catching it).
Here's my controller code:
package ch.oligofunds.oligoworld.web;
/*imports here*/
/**
* Handles requests for the application file upload requests
*/
@Controller("ExcelUploaderImpl")
@Transactional
public class ExcelUploaderImpl implements ExcelUploader {
@Autowired
PersoninfoDAO personinfoDAO;
/**
* Upload files using Spring Controller
* @throws SecurityException
* @throws NoSuchMethodException
* @throws DataAccessException
*/
@Override
@RequestMapping(value = "/importFundNAV", method = RequestMethod.POST)
public @ResponseBody String handleFileUpload(HttpServletRequest request, @RequestParam CommonsMultipartFile[] fileUpload) throws DataAccessException, NoSuchMethodException, SecurityException {
/*here save the uploaded file and initialize the serverFile variable*/
try {
success = readExcelfile(serverFile);
} catch (IOException e) {
logger.error("Failed to read the excel file", e);
result += "Failed to read the excel file\n" + e.getStackTrace() + "\n";
} finally {
serverFile.delete();
}
if (success) {
result += "You successfully imported file " + aFile.getOriginalFilename() + "\n";
} else {
result += "Failed to import file " + aFile.getOriginalFilename() + "\n";
}
}
return result;
}
@Override
public boolean readExcelfile(File xlfile) throws IOException, DataAccessException, NoSuchMethodException, SecurityException {
FileInputStream fis = new FileInputStream(xlfile); // Finds the workbook
// instance for XLSX
// file
XSSFWorkbook myWorkBook = new XSSFWorkbook(fis); // Return first sheet
// from the XLSX
// workbook
boolean success;
success = readFundDefinition(myWorkBook);
myWorkBook.close();
return success;
}
@Override
@Transactional(rollbackFor = Exception.class)
public boolean readFundDefinition(XSSFWorkbook myWorkBook) throws DataAccessException, NoSuchMethodException, SecurityException {
/*here do stuff to extract data from the excel to initialize the administrator, custodian, invContact and success variables*/
saveContact(administrator);
saveContact(custodian);
saveContact(invContact);
/*If any of the three invocations to saveContact fails, I want all three inserts to rollback*/
return success;
}
@Override
public void saveContact(Personinfo personinfo) throws DataAccessException, NoSuchMethodException, SecurityException {
/*a bunch of stuff before this line*/
personinfo.copy(personinfoDAO.store(personinfo)); // <--- this is where the transaction could fail
/*a bunch of stuff after this line*/
}
}
Here's the interface for it:
@Controller
public interface ExcelUploader {
public @ResponseBody String handleFileUpload(HttpServletRequest request, @RequestParam CommonsMultipartFile[] fileUpload) throws DataAccessException, NoSuchMethodException, SecurityException;
public boolean readExcelfile(File xlfile) throws IOException, DataAccessException, NoSuchMethodException, SecurityException;
public boolean readFundDefinition(XSSFWorkbook myWorkBook) throws DataAccessException, NoSuchMethodException, SecurityException;
public void saveContact(Personinfo personinfo, Personaddress personAddress, Personemail personEmail, Personphone personPhone) throws DataAccessException, NoSuchMethodException, SecurityException;
public void readNAV(XSSFWorkbook myWorkBook);
public boolean isEmpty(Object object, Method... methods);
}
My web-context.xml contains:
<mvc:annotation-driven/>
<mvc:default-servlet-handler/>
<tx:annotation-driven transaction-manager="transactionManager" proxy-target-class="true"/>
<context:component-scan base-package="ch.oligofunds.oligoworld.web" scoped-proxy="interfaces" />
And my dao-context.xml contains:
<context:component-scan base-package="ch.oligofunds.oligoworld.dao" scoped-proxy="interfaces" />
<context:component-scan base-package="ch.oligofunds.oligoworld.security" scoped-proxy="interfaces" />
<tx:annotation-driven transaction-manager="transactionManager" proxy-target-class="true"/>
<context:property-placeholder location="classpath:CopyofoligoWorld-dao.properties" />
<!-- Using Atomikos Transaction Manager -->
<bean id="atomikosTransactionManager" class="com.atomikos.icatch.jta.UserTransactionManager" init-method="init"
destroy-method="close">
<property name="forceShutdown" value="true" />
<property name="startupTransactionService" value="true" />
<property name="transactionTimeout" value="60" />
</bean>
<bean id="atomikosUserTransaction" class="com.atomikos.icatch.jta.UserTransactionImp" />
<!-- Configure the Spring framework to use JTA transactions from Atomikos -->
<bean id="transactionManager" class="org.springframework.transaction.jta.JtaTransactionManager">
<property name="transactionManager" ref="atomikosTransactionManager" />
<property name="userTransaction" ref="atomikosUserTransaction" />
<property name="transactionSynchronizationName" value="SYNCHRONIZATION_ON_ACTUAL_TRANSACTION" />
</bean>
<bean name="mysqlDS,springSecurityDataSource" class="org.apache.commons.dbcp.BasicDataSource" destroy-method="close" >
<property name="driverClassName" value="${mysql.connection.driver_class}" />
<property name="username" value="${mysql.connection.username}" />
<property name="password" value="${mysql.connection.password}" />
<property name="url" value="${mysql.connection.url}" />
<property name="maxIdle" value="${mysql.minPoolSize}" />
<property name="maxActive" value="${mysql.maxPoolSize}" />
</bean>
Here's the exception that I thought would trigger the rollback:
org.springframework.web.util.NestedServletException: Request processing failed; nested exception is javax.persistence.PersistenceException: org.hibernate.exception.ConstraintViolationException: Column 'name' cannot be null
It's not clear to me whether the @Transactional annotation is being picked up at all - from my understanding it should, since I'm scanning the ch.oligofunds.oligoworld.web package and the controller interface is annotated with @Controller. But my understanding may be wrong. :)
Any hints? Thanks
回答1:
@Transactional
on that method doesn't have any added value as it is an internal method call (and your class is already transactional). Spring uses proxies and only calls into the object pass thorough the proxy.
Also your code is flawed you shouldn't catch and swallow exceptions as that interferes with the tx support (it relies on transactions to determine to rollback or not, currently there is never an exception hence always tries to commit).
Finally you are using MySQL make sure that you are using table types that actually supports transactions (MyISAM tables don't have tx support).
I would however strongly suggest to move the transactional part (or the business logic which you are now doing in your controller) to a service. The controller (or web layer in general) should only be a thin layer converting the incoming request into something useable for the service layer and the result into something useable for the web to display.
回答2:
With proxy-target-class="true" you're telling spring to use cglib to handle the proxying but you've specified scoped-proxy="interfaces".
See https://stackoverflow.com/a/15568457/117839
来源:https://stackoverflow.com/questions/31475085/transactional-on-controller-method-not-working