在公司开发一个后台管理系统时有这样的需求:提交一个表单时,要把表单域内容和上传的文件内容(可以是多个上传文件)一并提交到后台去,并且数据库持久化失败后数据要回滚且文件不应该上传上去,如果文件上传失败同样数据库也要回滚。
我的做法是:
1. Spring MVC的controller只是将参数包装成DTO,提交给service层一并处理文件上传和数据库保存操作。controller中的方法,如:
@RequestMapping("save.do")
public @ResponseBody String storySave(MultipartHttpServletRequest request, StoryDTO storyDTO, String[] fileName) throws Exception {
storyDTO.setFiles(request.getFileMap());
storyDTO.setFileUploadPath(webConfig.getFileUploadPath());
storyDTO.setFileName(fileName);
try {
storyService.processingStory(storyDTO);
} catch (Exception e) {
e.printStackTrace();
return "{\"status\":\"err\"}";
}
return "{\"status\":\"ok\"}";
}
在service里的方法processingStory是被Spring事务管控的,那么这个方法头部就抛出一个Exception异常,且在这个方法里的操作一定是数据持久化操作在先,而文件操作在后。
文件操作的潜在的异常也是由processingStory这个方法统一抛出的。service中的处理方法如下:
@Override
public void processingStory(StoryDTO storyDTO) throws Exception {
Map<String, MultipartFile> files = storyDTO.getFiles();
Boolean file1=null,file2=null,file3=null,file4=null;
String[] fileName = storyDTO.getFileName();
String fileUploadPath = storyDTO.getFileUploadPath();
String[] pathNames = new String[4];
InputStream[] ins = new InputStream[4];
for (Entry<String, MultipartFile> entry : files.entrySet()) {
if("toUpload0".equals(entry.getKey())) {
file1=true;
String pathName = fileUploadPath+entry.getValue().getOriginalFilename();
pathNames[0] = pathName;
ins[0] = entry.getValue().getInputStream();
storyDTO.setPosterUrl1(pathName);
}
if("toUpload1".equals(entry.getKey())) {
file2=true;
String pathName = fileUploadPath+entry.getValue().getOriginalFilename();
pathNames[1] = pathName;
ins[1] = entry.getValue().getInputStream();
storyDTO.setPosterUrl2(pathName);
}
if("toUpload2".equals(entry.getKey())) {
file3=true;
String pathName = fileUploadPath+entry.getValue().getOriginalFilename();
pathNames[2] = pathName;
ins[2] = entry.getValue().getInputStream();
storyDTO.setPosterUrl3(pathName);
}
if("toUpload3".equals(entry.getKey())) {
file4=true;
String pathName = fileUploadPath+entry.getValue().getOriginalFilename();
pathNames[3] = pathName;
ins[3] = entry.getValue().getInputStream();
storyDTO.setPosterUrl4(pathName);
}
}
if(null == storyDTO.getStoryId()) {
this.saveStory(storyDTO);
} else {
String[] deletePaths = new String[4];
StoryDTO oldStoryDTO = this.getStoryById(storyDTO.getStoryId());
if(file1==null && StringUtils.isBlank(fileName[0])){
deletePaths[0]=oldStoryDTO.getPosterUrl1();
storyDTO.setPosterEmptyUrl1("Y");
}
if(file2==null && StringUtils.isBlank(fileName[1])){
deletePaths[1]=oldStoryDTO.getPosterUrl2();
storyDTO.setPosterEmptyUrl2("Y");
}
if(file3==null && StringUtils.isBlank(fileName[2])){
deletePaths[2]=oldStoryDTO.getPosterUrl3();
storyDTO.setPosterEmptyUrl3("Y");
}
if(file4==null && StringUtils.isBlank(fileName[3])){
deletePaths[3]=oldStoryDTO.getPosterUrl4();
storyDTO.setPosterEmptyUrl4("Y");
}
StoryUpdateDTO updateDTO = new StoryUpdateDTO();
BeanUtils.copyProperties(updateDTO, storyDTO);
updateDTO.setUpdateStoryId(storyDTO.getStoryId());
this.updateStory(updateDTO);
for (String path : deletePaths) {
if(StringUtils.isNotBlank(path)) {
FileOprUtils.deleteFile(path);
}
}
}
for(int i = 0; i < pathNames.length; i++) {
FileOprUtils.copyFileToServerPath(pathNames[i], ins[i]);
}
}
最后是Spring的声明式事务配置:
<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager"> <property name="dataSource"> <ref bean="dataSource" /> </property> <property name="rollbackOnCommitFailure" value="true" /> <property name="globalRollbackOnParticipationFailure" value="true" /> </bean> <tx:advice id="txAdvice" transaction-manager="transactionManager"> <tx:attributes> <tx:method name="*" rollback-for="Exception" propagation="REQUIRED" /> </tx:attributes> </tx:advice> <aop:config> <aop:advisor advice-ref="txAdvice" pointcut="execution(* com.focoon.ds..service.*.*(..))" /> </aop:config>
注意需要使用rollback-for属性明确指出抛出哪种异常需要回滚