Spring JPA 回滚不工作

Spring JPA Rollback not working

我的 Web 服务写入两个 MySQL table(一个接一个;依赖于外键)。我已经让我的服务方法 [upload(..)] 抛出一个强制异常,只是为了检查回滚功能。即使抛出异常,记录也会保存在 file_store table(第一个 table)中。请帮我弄清楚出了什么问题。另外,如果任何上下文配置不正确,请告诉我。谢谢。

文件: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>

文件: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>

文件: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.Java(实体 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;
    }

}

文件:FileStore.Java(实体 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;
    }

}

文件: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;
        }
    }
}

文件: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;
    }

}

文件: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.";
        }
    }
}

您必须在根上下文文件上扫描您的服务,该文件是您的 applicationContext 但不在 servlet 上下文中,请参阅此以了解有关 differences

的更多信息

扫描root-context上的服务:

<context:component-scan
        base-package="com.app.test.service.impl"/>

配置 servlet-context 以便您只扫描控制器以避免重复 classes :

<context:component-scan
        base-package="com.app.test.controller"/>

默认情况下 spring-transactionnal 将回滚 RuntimeException,无需将它们添加到回滚 class 列表中:

@Transactional(rollbackFor=..)

编辑 Spring 的事务管理仅针对未经检查的异常 (RuntimeException) 回滚事务

throw new RuntimeException("forced exception");