Reputation: 45
My web service writes to two MySQL tables (one after another; foreign key dependent). I have made my service method [upload(..)] to throw a forced exception just to check the rollback functionality. Even if the exception is thrown, record is getting saved in the file_store table (first table). Please help me in figuring out whats wrong. Also, let me know in case any context configuration is incorrect. Thanks.
File: root-context.xml
<?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:jpa="http://www.springframework.org/schema/data/jpa"
xmlns:tx="http://www.springframework.org/schema/tx"
xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa-1.3.xsd
http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.1.xsd">
<!-- Root Context: defines shared resources visible to all other web components -->
<!-- Database -->
<bean id="dataSource" class="org.springframework.jdbc.datasource.DriverManagerDataSource">
<property name="driverClassName" value="com.mysql.jdbc.Driver"/>
<property name="url" value="jdbc:mysql://localhost:3306/app?autoReconnect=true"/>
<property name="username" value="${datasource.username}"/>
<property name="password" value="${datasource.password}"/>
</bean>
<!-- JPA Vendor Adapter -->
<bean id="jpaVendorAdapter"
class="org.springframework.orm.jpa.vendor.HibernateJpaVendorAdapter">
<property name="showSql" value="true" />
<property name="generateDdl" value="true" />
<property name="databasePlatform" value="org.hibernate.dialect.MySQLDialect"></property>
</bean>
<!-- Entity Manager Factory -->
<bean id="entityManagerFactory"
class="org.springframework.orm.jpa.LocalContainerEntityManagerFactoryBean">
<property name="dataSource" ref="dataSource" />
<property name="jpaVendorAdapter" ref="jpaVendorAdapter" />
<property name="packagesToScan" value="com.app.test.persistence" />
</bean>
<!-- Transaction Manager -->
<bean id="transactionManager" class="org.springframework.orm.jpa.JpaTransactionManager">
<property name="entityManagerFactory" ref="entityManagerFactory" />
</bean>
<!-- Detect @Transactional -->
<tx:annotation-driven transaction-manager="transactionManager" proxy-target-class="true" />
<!-- JPA Repositories -->
<jpa:repositories base-package="com.app.test.repository"/>
<!-- JASYPT Configuration -->
<bean id="configurationEncryptor" class="org.jasypt.encryption.pbe.StandardPBEStringEncryptor">
<property name="algorithm">
<value>PBEWithMD5AndDES</value>
</property>
<property name="password">
<value>com.app.test</value>
</property>
</bean>
<bean id="propertyConfigurer" class="org.jasypt.spring.properties.EncryptablePropertyPlaceholderConfigurer">
<constructor-arg ref="configurationEncryptor" />
<property name="locations">
<list>
<value>classpath:runtime.properties</value>
</list>
</property>
</bean>
<!-- Properties Util -->
<bean id="propertiesUtil" class="com.app.test.util.PropertiesUtil">
<property name="location" value="classpath:app.properties"></property>
</bean>
</beans>
File: servlet-context.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans:beans xmlns="http://www.springframework.org/schema/mvc"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xmlns:beans="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:jpa="http://www.springframework.org/schema/data/jpa"
xsi:schemaLocation="http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc.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/data/jpa http://www.springframework.org/schema/data/jpa/spring-jpa.xsd">
<!-- DispatcherServlet Context: defines this servlet's request-processing infrastructure -->
<!-- Detect @Controller -->
<annotation-driven />
<!-- Handles HTTP GET requests for /resources/** by efficiently serving up static resources in the ${webappRoot}/resources directory -->
<resources mapping="/**" location="/WEB-INF/" />
<!-- Resolves views selected for rendering by @Controllers to .jsp resources in the /WEB-INF/views directory -->
<beans:bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
<beans:property name="prefix" value="/WEB-INF/views/" />
<beans:property name="suffix" value=".jsp" />
</beans:bean>
<context:component-scan base-package="com.app.test" />
<beans:bean id="jsonMapper" class="com.fasterxml.jackson.databind.ObjectMapper"></beans:bean>
</beans:beans>
File: web.xml
<?xml version="1.0" encoding="UTF-8"?>
<web-app version="2.5" xmlns="http://java.sun.com/xml/ns/javaee"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd">
<!-- The definition of the Root Spring Container shared by all Servlets and Filters -->
<context-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/root-context.xml</param-value>
</context-param>
<!-- Creates the Spring Container shared by all Servlets and Filters -->
<listener>
<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
</listener>
<!-- Processes application requests -->
<servlet>
<servlet-name>appServlet</servlet-name>
<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
<init-param>
<param-name>contextConfigLocation</param-name>
<param-value>/WEB-INF/spring/appServlet/servlet-context.xml</param-value>
</init-param>
<load-on-startup>1</load-on-startup>
</servlet>
<servlet-mapping>
<servlet-name>appServlet</servlet-name>
<url-pattern>/</url-pattern>
</servlet-mapping>
</web-app>
File: File.Java (Entity-1)
@Entity
@Table(name="files")
@NamedQuery(name="File.findAll", query="SELECT f FROM File f")
public class File implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@GeneratedValue(strategy=GenerationType.AUTO)
private int id;
@Lob
private byte[] content;
//bi-directional many-to-one association to FileStore
@ManyToOne
@JoinColumn(name="file_store_key")
private FileStore fileStore;
public File() {
}
public int getId() {
return this.id;
}
public void setId(int id) {
this.id = id;
}
public byte[] getContent() {
return this.content;
}
public void setContent(byte[] content) {
this.content = content;
}
public FileStore getFileStore() {
return this.fileStore;
}
public void setFileStore(FileStore fileStore) {
this.fileStore = fileStore;
}
}
File: FileStore.Java (Entity-2)
@Entity
@Table(name="file_store")
@NamedQuery(name="FileStore.findAll", query="SELECT f FROM FileStore f")
public class FileStore implements Serializable {
private static final long serialVersionUID = 1L;
@Id
@Column(name="unique_key")
private String uniqueKey;
private String checksum;
@Column(name="uploaded_from")
private String uploadedFrom;
@Temporal(TemporalType.TIMESTAMP)
@Column(name="uploaded_on")
private Date uploadedOn;
//bi-directional many-to-one association to File
@OneToMany(mappedBy="fileStore")
private List<File> files;
public FileStore() {
}
public String getUniqueKey() {
return this.uniqueKey;
}
public void setUniqueKey(String uniqueKey) {
this.uniqueKey = uniqueKey;
}
public String getChecksum() {
return this.checksum;
}
public void setChecksum(String checksum) {
this.checksum = checksum;
}
public String getUploadedFrom() {
return this.uploadedFrom;
}
public void setUploadedFrom(String uploadedFrom) {
this.uploadedFrom = uploadedFrom;
}
public Date getUploadedOn() {
return this.uploadedOn;
}
public void setUploadedOn(Date uploadedOn) {
this.uploadedOn = uploadedOn;
}
public List<File> getFiles() {
if (null == this.files)
this.files = new ArrayList<File>();
return this.files;
}
public void setFiles(List<File> files) {
this.files = files;
}
public File addFile(File file) {
getFiles().add(file);
file.setFileStore(this);
return file;
}
public File removeFile(File file) {
getFiles().remove(file);
file.setFileStore(null);
return file;
}
}
File: FileStoreDAOImpl.java
package com.app.test.dao.impl;
@Repository
public class FileStoreDAOImpl implements FileStoreDAO {
private static final Logger LOGGER = LoggerFactory.getLogger(FileStoreDAOImpl.class);
@Autowired
private FileStoreRepo fileStoreRepo;
@Override
public FileStore saveFileStore(FileStore fStore) throws Exception {
LOGGER.info("Inside saveFileStore");
try {
fileStoreRepo.saveAndFlush(fStore);
return fStore;
} catch (Exception e) {
LOGGER.error("Exception occurred while saving file store: " + e.getMessage());
throw e;
}
}
}
File: FileStoreServiceImpl.java
package com.app.test.service.impl;
@Service
@Transactional
public class FileStoreServiceImpl implements FileStoreService {
@Autowired
private FileStoreDAO fileStoreDAO;
@Autowired
private FileDAO fileDAO;
private static final Integer ID_LENGTH = 16;
@Override
@Transactional(rollbackFor={Exception.class, RuntimeException.class})
public String upload(UploadRequest jRequest, String uploadedFrom) throws Exception {
//String fileKey = null;
try {
//get the file content from packet
String fileContent = jRequest.getContent();
//compute checksum
String checksum = DigestUtils.sha1Hex(fileContent);
//check if similar file exists
if (!fileStoreDAO.checksumExists(checksum)) {
//create file store object
FileStore fStore = new FileStore();
fStore.setUniqueKey(RandomUtil.getRandomKey(checksum, ID_LENGTH));
fStore.setChecksum(checksum);
fStore.setUploadedOn(new Date());
fStore.setUploadedFrom(uploadedFrom);
fileStoreDAO.saveFileStore(fStore);
//create file object
File file = new File();
file.setFileStore(fStore);
file.setContent(fileContent.getBytes());
throw new Exception("Forced exception"); //Expecting Spring JPA to rollback the transaction; but not happenning :(
/*fileDAO.saveFile(file);
} else {
throw new Exception("Similar file already exists.");
}
} catch (Exception e) {
throw e;
}
//return fileKey;
}
}
File: FileController.java
package com.app.test.controller;
@Controller
public class FileStoreController {
@Autowired
FileStoreService fileStoreService;
@Autowired
ObjectMapper jsonMapper;
@RequestMapping(value = "/", method = RequestMethod.GET)
public ModelAndView processIndex(HttpServletRequest request, HttpServletResponse response) {
return new ModelAndView("index", null);
}
private String retrieveData(HttpServletRequest request) {
StringBuffer jBuffer = new StringBuffer();
String line = null;
try {
BufferedReader reader = request.getReader();
while ((line = reader.readLine()) != null)
jBuffer.append(line);
} catch (Exception e) {
return null;
}
return jBuffer.toString();
}
@RequestMapping(value = "/upload", method = RequestMethod.POST)
@ResponseBody
public String processUpload(HttpServletRequest request, HttpServletResponse response) {
UploadResponse jResponse = new UploadResponse();
try {
//retrieve request packet
String pData = retrieveData(request);
String fromAddress = request.getRemoteAddr();
if (null != pData && !("".equals(pData))) {
UploadRequest jRequest = jsonMapper.readValue(pData, UploadRequest.class);
String fileKey = fileStoreService.upload(jRequest, fromAddress);
UploadSuccess success = new UploadSuccess();
success.setFileId(fileKey);
success.setMessage("File uploaded successfully");
jResponse.setSuccess(success);
} else {
Error error = new Error();
error.setMessage("Packet is empty.");
jResponse.setError(error);
}
} catch (Exception e) {
Error error = new Error();
error.setMessage(e.getMessage());
jResponse.setError(error);
}
try {
return jsonMapper.writeValueAsString(jResponse);
} catch (Exception ex) {
return "Fatal exception occurred while processing upload request.";
}
}
}
Upvotes: 1
Views: 1806
Reputation: 4158
you have to scan your services on the root context file which is your applicationContext but not within the servlet Context, see this to learn more about the differences
Scan the services on the root-context
:
<context:component-scan
base-package="com.app.test.service.impl"/>
Configure the servlet-context
so you scan just Controllers in order to avoid duplicated classes :
<context:component-scan
base-package="com.app.test.controller"/>
by default spring-transactionnal will rollback for RuntimeException
, there is no need to add them on the rollback class list :
@Transactional(rollbackFor=..)
EDIT Spring's transaction management rolls-back transactions only for unchecked exceptions (RuntimeException)
throw new RuntimeException("forced exception");
Upvotes: 2