1. 程式人生 > >宣告式事務的應用(以ssm專案為例)

宣告式事務的應用(以ssm專案為例)

新增事務在實際專案中是必不可少的,事務是用來實現要麼全都成功,要麼全都不成功, 而主要針對的就是要不成功就全都不成功的問題.

想象你給你女朋友轉賬,這邊剛扣了錢,你女朋友賬戶還沒收到錢呢,突然出故障了,比如停電了. 那怎麼辦. 這就涉及到一個關鍵的知識點:事務. 利用的事務的回滾,把你賬戶上扣的錢再回滾到你賬戶上.

本篇採用maven構建war工程涉及主要知識點:

spring+springMVC+mybatis如何整合,  

mybatis在寫mapper.xml檔案時,如何解決引數型別只能傳一個的問題

update語句怎麼寫

如何保證正確配置了事務,事務少配一個,就會失效

宣告式事務介紹:

宣告式事務管理建立在AOP之上的。其本質是對方法前後進行攔截,然後在目標方法開始之前建立或者加入一個事務,在執行完目標方法之後根據執行情況提交或者回滾事務。宣告式事務最大的優點就是不需要通過程式設計的方式管理事務,這樣就不需要在業務邏輯程式碼中摻雜事務管理的程式碼,只需在配置檔案中做相關的事務規則宣告(或通過基於@Transactional註解的方式),便可以將事務規則應用到業務邏輯中。

        顯然宣告式事務管理要優於程式設計式事務管理,這正是spring倡導的非侵入式的開發方式。宣告式事務管理使業務程式碼不受汙染,一個普通的POJO物件,只要加上註解就可以獲得完全的事務支援。和程式設計式事務相比,宣告式事務唯一不足地方是,後者的最細粒度只能作用到方法級別,無法做到像程式設計式事務那樣可以作用到程式碼塊級別。但是即便有這樣的需求,也存在很多變通的方法,比如,可以將需要進行事務管理的程式碼塊獨立為方法等等。

         宣告式事務管理也有兩種常用的方式,一種是基於tx和aop名字空間的xml配置檔案,另一種就是基於@Transactional註解。顯然基於註解的方式更簡單易用,更清爽。實際中兩種方式常組合起來使用.本篇的案例專案就是組合使用實現事務功能.

專案結構:


資料庫部分:

資料庫中就一張表user

user表的結構


user表的資料


程式碼部分:

pojo 之 User:

package com.qx.pojo;

public class User {
	private Integer id;
	private String username;
	private Integer age;
	private String sex;
	private Double salary;
	private String department;	
	
	// getter and setter 方法
	
}

mapper之UserMapper介面:

package com.qx.mapper;

import java.util.List;

import org.apache.ibatis.annotations.Param;

import com.qx.pojo.User;

public interface UserMapper {

	public List<User> findAll() throws Exception;
	
	public void AddUser(User user) throws Exception;
	
	//更改使用者
	public void updateUserById(@Param("id") Integer id,@Param("username") String name,
			@Param("age") Integer age,@Param("sex") String sex) throws Exception;
	
}

mapper之UserMapper.xml

<?xml version="1.0" encoding="UTF-8"?>
<!DOCTYPE mapper PUBLIC "-//mybatis.org//DTD Mapper 3.0//EN" "http://mybatis.org/dtd/mybatis-3-mapper.dtd" >
<mapper namespace="com.qx.mapper.UserMapper">
	<select id="findAll" resultType="com.qx.pojo.User">
		select * from user
	</select>
	
	<insert id="AddUser" parameterType="com.qx.pojo.User">
 		<selectKey keyProperty="id" resultType="integer" order="AFTER">
 			SELECT LAST_INSERT_ID()
 		</selectKey>	
		insert into user(username,age,sex,salary,department)
		values (#{username},#{age},#{sex},#{salary},#{department})
	</insert>
	
	
	<!-- 我直接在mapper介面中使用了mybatis的引數註解@Param, 在這裡就不用在指明引數型別了-->
	<!-- 即使你想指明也無法直接指明,因為引數型別只接收一個引數,無法接收多個,@Param就是解決這個問題的 -->
	<select id="updateUserById">
		update user set username=#{username},age=#{age},sex=#{sex} where id=#{id}
	</select>
	
</mapper>

新增的依賴:

pom.xml:


需要特別注意的是這4個,不要導錯了.

 	<dependency>
  		<groupId>org.springframework</groupId>
  		<artifactId>spring-aspects</artifactId>
  		<version>4.3.7.RELEASE</version>
  	</dependency>
  	<dependency>
  		<groupId>javax.servlet</groupId>
  		<artifactId>jstl</artifactId>
  		<version>1.2</version>
  	</dependency>
  	<dependency>
  		<groupId>javax.servlet</groupId>
  		<artifactId>javax.servlet-api</artifactId>
  		<version>3.1.0</version>
  	</dependency>
  	<dependency>
  		<groupId>javax.servlet.jsp</groupId>
  		<artifactId>jsp-api</artifactId>
  		<version>2.2</version>
  	</dependency>



配置檔案:

db.properties

jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql:///testtransaction?characterEncoding=utf-8
jdbc.user=root
jdbc.password=root

mybatis.xml
<?xml version="1.0" encoding="UTF-8" ?>
<!DOCTYPE configuration
PUBLIC "-//mybatis.org//DTD Config 3.0//EN"
"http://mybatis.org/dtd/mybatis-3-config.dtd">
<configuration>
			<plugins>
			<!-- 
			3.4.2版本pagehelper
			<plugin interceptor="com.github.pagehelper.PageHelper">
				<property name="dialect" value="mysql"/>
			</plugin>
			 -->
			 <!--5.0版本pagehelper -->
			<plugin interceptor="com.github.pagehelper.PageInterceptor">
				<property name="helperDialect" value="mysql"/>
			</plugin>
			
	</plugins>
</configuration>

spring之applicationContext.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:context="http://www.springframework.org/schema/context"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd">

	<context:component-scan base-package="com.qx"></context:component-scan>
		<!-- 匯入配置檔案 -->
	<context:property-placeholder location="classpath:conf/db.properties"/>
	<bean id="dataSource" class="com.mchange.v2.c3p0.ComboPooledDataSource">
		<property name="driverClass" value="${jdbc.driver}"></property>
		<property name="jdbcUrl" value="${jdbc.url}"></property>
		<property name="user" value="${jdbc.user}"></property>
		<property name="password" value="${jdbc.password}"></property>
	</bean>
	
	<!-- 配置工廠 -->
	<bean id="sqlSessionFactoryBean" class="org.mybatis.spring.SqlSessionFactoryBean">
		<property name="dataSource" ref="dataSource"></property>
		<property name="configLocation" value="classpath:mybatis/mybatis.xml"></property>
	</bean>
	
	<!-- 配置掃描mapper檔案 -->
	<bean class="org.mybatis.spring.mapper.MapperScannerConfigurer">
		<property name="basePackage" value="com.qx.mapper"></property>
	</bean>
</beans>

spring之applicationContext-tran.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:aop="http://www.springframework.org/schema/aop"
	xmlns:context="http://www.springframework.org/schema/context"
	xmlns:tx="http://www.springframework.org/schema/tx"
	xsi:schemaLocation="http://www.springframework.org/schema/beans http://www.springframework.org/schema/beans/spring-beans-4.3.xsd
		http://www.springframework.org/schema/context http://www.springframework.org/schema/context/spring-context-4.3.xsd
		http://www.springframework.org/schema/aop http://www.springframework.org/schema/aop/spring-aop-4.3.xsd
		http://www.springframework.org/schema/tx http://www.springframework.org/schema/tx/spring-tx-4.3.xsd">

	<!-- 事務管理器 -->
	<bean id="transactionManager" class="org.springframework.jdbc.datasource.DataSourceTransactionManager">
		<property name="dataSource" ref="dataSource"></property>
	</bean>
	
	<tx:annotation-driven transaction-manager="transactionManager"/>
	
	<tx:advice id="txAdvice">
		<tx:attributes>
			<!-- 約定優於編碼 設定事務詳情 -->
			<tx:method name="get*" read-only="true" propagation="SUPPORTS"/>
			<tx:method name="find*" read-only="true" propagation="SUPPORTS"/>
			<tx:method name="select*" read-only="true" propagation="SUPPORTS"/>
			<tx:method name="add*"/>
			<tx:method name="save*"/>
			<tx:method name="insert*"/>
			<tx:method name="delete*"/>
			<tx:method name="update*"/>
		</tx:attributes>
	</tx:advice>
	
	<aop:config>
		<aop:advisor advice-ref="txAdvice" pointcut="execution(* com.qx.service..*.*(..))"/>
	</aop:config>
</beans>

spring之spring-MVC.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:context="http://www.springframework.org/schema/context"
	xmlns:mvc="http://www.springframework.org/schema/mvc"
	xsi:schemaLocation="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-4.3.xsd
		http://www.springframework.org/schema/mvc http://www.springframework.org/schema/mvc/spring-mvc-4.3.xsd">

	<!-- 再配一遍,只掃描controller -->
	<context:component-scan base-package="com.qx.controller"></context:component-scan>
	
	<!-- 不需要 配置id,因為 用不到 -->
	<bean class="org.springframework.web.servlet.view.InternalResourceViewResolver">
		<!-- <property name="prefix" value="/"></property> -->
		<property name="suffix" value=".jsp"></property>
	</bean>
	
	<!-- 配置resource標籤,此標籤內部的地址不會被攔截 -->
	<mvc:resources location="/css/" mapping="/css/**"></mvc:resources>
	<mvc:resources location="/js/" mapping="/js/**"></mvc:resources>
	<mvc:resources location="/images/" mapping="/images/**"></mvc:resources>

	<mvc:annotation-driven></mvc:annotation-driven>
</beans>

配置時你也許見到有人在<tx:advice>標籤內又配置了一遍transaction-manager,其實完全沒必要,配置了也不會報錯,要想事務生效的一個關鍵點是

必須要配置 事務的註解驅動 <tx:annotation-driven transaction-manager="transactionManager"/>

<tx:annotation-driven transaction-manager="transactionManager"/>

<tx:advice id="txAdvice" transaction-manager="transactionManager">
<tx:attributes>
<!-- 約定優於編碼 設定事務詳情 -->
<tx:method name="get*" read-only="true" propagation="SUPPORTS"/>
<tx:method name="find*" read-only="true" propagation="SUPPORTS"/>
<tx:method name="select*" read-only="true" propagation="SUPPORTS"/>
<tx:method name="add*"/>
<tx:method name="save*"/>
<tx:method name="insert*"/>
<tx:method name="delete*"/>
<tx:method name="update*"/>
</tx:attributes>
</tx:advice>

web.xml中的配置:

<?xml version="1.0" encoding="UTF-8"?>
<web-app xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance" xmlns="http://java.sun.com/xml/ns/javaee" xsi:schemaLocation="http://java.sun.com/xml/ns/javaee http://java.sun.com/xml/ns/javaee/web-app_2_5.xsd" version="2.5">
  <display-name>TestTranSaction</display-name>

	<!-- needed for ContextLoaderListener -->
	<context-param>
		<param-name>contextConfigLocation</param-name>
		<param-value>classpath*:spring/app*.xml</param-value>
	</context-param>

	<!-- Bootstraps the root web application context before servlet initialization -->
	<listener>
		<listener-class>org.springframework.web.context.ContextLoaderListener</listener-class>
	</listener>
	
	<!-- The front controller of this Spring Web application, responsible for handling all application requests -->
	<servlet>
		<servlet-name>springMVC</servlet-name>
		<servlet-class>org.springframework.web.servlet.DispatcherServlet</servlet-class>
		<init-param>
			<param-name>contextConfigLocation</param-name>
			<param-value>classpath:spring/spring-MVC.xml</param-value>
		</init-param>
		<load-on-startup>2</load-on-startup>
	</servlet>

	<!-- Map all requests to the DispatcherServlet for handling -->
	<servlet-mapping>
		<servlet-name>springMVC</servlet-name>
		<url-pattern>*.action</url-pattern>
	</servlet-mapping>
</web-app>


頁面部分(兩個jsp頁面):

index.jsp

<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>
</head>
<body>
	<jsp:forward page="/user/findAll.action"></jsp:forward>
</body>
</html>

userlist.jsp
<%@ page language="java" contentType="text/html; charset=UTF-8"
    pageEncoding="UTF-8"%>
<%@ taglib prefix="c" uri="http://java.sun.com/jsp/jstl/core"%>
<!DOCTYPE html PUBLIC "-//W3C//DTD HTML 4.01 Transitional//EN" "http://www.w3.org/TR/html4/loose.dtd">
<html>
<head>
<meta http-equiv="Content-Type" content="text/html; charset=UTF-8">
<title>Insert title here</title>

<style type="text/css">
	#span {
		text-align: right;
		float: right;
	}
	a {
		text-decoration: none;
	}
</style>
</head>
<body>
	<div>
		<span id="span"><a href="${pageContext.request.contextPath}/user/addUser.action">新增</a></span>
	</div>
	<table border="1" width="100%">
		<caption>使用者表</caption>
		<tr>
			<td>姓名</td>
			<td>年齡</td>
			<td>性別</td>
			<td>薪資</td>
			<td>部門</td>
			<td>更改</td>
		</tr>
	<c:forEach items="${users}" var="user">
		<tr>
			<td>${user.username }</td>
			<td>${user.age }</td>
			<td>${user.sex }</td>
			<td>${user.salary }</td>
			<td>${user.department }</td>
			<td><a href="${pageContext.request.contextPath }/user/updateUser.action?id=${user.id}">更改</a></td>
		</tr>
	</c:forEach>
	</table>
</body>

</html>

業務層之UserService介面

package com.qx.service;

import java.util.List;

import com.qx.pojo.User;

public interface UserService {

	public List<User> findAll() throws Exception;
	
	public void AddUser(User user) throws Exception;
	
	public void updateUserById(Integer id,String username,int age,String sex) throws Exception;
	
}

業務層之UserServiceImpl

實現事務回滾,1.必須要加@Transactional註解   2. catch塊中必須throw個異常出來,是觸發事務生效的必要條件

package com.qx.service;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Service;
import org.springframework.transaction.annotation.Transactional;

import com.qx.mapper.UserMapper;
import com.qx.pojo.User;

@Service
@Transactional
public class UserServiceImpl implements UserService{
	
	@Autowired
	private UserMapper userMapper;

	@Override
	public List<User> findAll() {
		// TODO Auto-generated method stub
		List<User> users = null;
		try {
			users = userMapper.findAll();
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return users;
	}

	@Override
	public void AddUser(User user)  {
		// TODO Auto-generated method stub
		try {
			userMapper.AddUser(user);
			//int a=6/0;
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
			throw new RuntimeException("事務回滾了嗎");
		}
	}

	
	@Override
	public void updateUserById(Integer id,String username, int age, String sex) {
		// TODO Auto-generated method stub
		try {
			userMapper.updateUserById(id,username, age, sex);
			int b=10/0;
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
			throw new RuntimeException("事務回滾了嗎");
		}
	}

}


控制層:UserController

package com.qx.controller;

import java.util.List;

import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.stereotype.Controller;
import org.springframework.web.bind.annotation.RequestMapping;
import org.springframework.web.servlet.ModelAndView;

import com.qx.pojo.User;
import com.qx.service.UserService;

@Controller
@RequestMapping("/user")
public class UserController {
	
	@Autowired
	private UserService userService;

	@RequestMapping("/findAll")
	public ModelAndView findAll() throws Exception{
		ModelAndView mv=new ModelAndView();
		List<User> users = userService.findAll();
		mv.addObject("users", users);
		mv.setViewName("/userlist");
		return mv;
	}
	
	@RequestMapping("/addUser")
	public String addUser(){
		User user=new User();
		user.setUsername("鄭爽");
		user.setAge(17);
		user.setSex("女");
		user.setSalary(9856.00);
		user.setDepartment("銷售");
		try {
			userService.AddUser(user);
			
			
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return "redirect:/user/findAll.action";
		
	}
	
	@RequestMapping("/updateUser")
	public String updateUser(Integer id){
		
		try {
			userService.updateUserById(id, "逗逼姚", 22, "男");
		} catch (Exception e) {
			// TODO Auto-generated catch block
			e.printStackTrace();
		}
		return "redirect:/user/findAll.action";
	}
	
}

向tomcat內新增專案,啟動tomcat,地址類輸入localhostL8080/TestTranSaction 請求結果為


點選新增可新增一個使用者(程式碼中定義死了,新增的是鄭爽),點選更改可更改一個使用者(更改後的使用者叫逗逼姚,只更改username,age,sex三個欄位)

為了測試事務回滾,特意加了//int a=6/0; 這樣的程式碼來人為製造異常,此時事務是否新增上了(看是否回滾).可根據需要自行註釋或解除註釋.

歸納點:

要想事務配置成功:

1, 要在applicationContext-tran.xml配置檔案中 新增事務的註解驅動,必須要有.

這幾項缺一不可.(提醒: 宣告式事務是基於AOP的)


2. 在業務層業務實現類必須要加@Transaction註解

3. 在業務層業務實現類catch塊中必須要throw一個異常,以便觸發回滾(這是觸發回滾的一個條件)

4. spring-MVC.xml中要掃具體包只掃到Controller, 因為spring-MVC.xml是輸入dispatcherServlet的,而applicationContext.xml,applicationContext-tran.xml是輸入contextLoaderListener的, 因此spring-MVC.xml的載入要晚於它倆,會造成本已增加過的方法被覆蓋

以上四點缺一不可.

常見的誤解:

顯示有增加標記的就一定配置事務成功了.沒顯示增加比較的說明事務沒配置成功.

這個思維完全是荒謬的.

比如我的spring,springdao(她倆合在一個配置檔案applicationContext.xml裡面了)和事務tran是分開配置的.(其實spring,springdao也能拆開配置).,事務的配置檔案中就沒顯示增強標記,它深圳還報了一個警告,下圖中黃色區域 說Multiple annotations found at this line:- Referenced bean 'dataSource' not   其實這個警告說dataSource未找到,完全不靠譜(我其實在springContext.xml檔案中配置過了),就像增加標記不靠譜一樣


這個增加標記長什麼樣呢?在上面這個配置檔案中加一行程式碼就會出現了


實際上<context:component-scan base-package="xxxx"/>並不是隨便就能寫的,  只有滿足了前面提到的4點,宣告式事務才能生效

擴充套件知識:

spring事務回滾規則

     指示spring事務管理器回滾一個事務的推薦方法是在當前事務的上下文內丟擲異常。spring事務管理器會捕捉任何未處理的異常,然後依據規則決定是否回滾丟擲異常的事務。

        預設配置下,spring只有在丟擲的異常為執行時unchecked異常時才回滾該事務,也就是丟擲的異常為RuntimeException的子類(Errors也會導致事務回滾),而丟擲checked異常則不會導致事務回滾。可以明確的配置在丟擲那些異常時回滾事務,包括checked異常。也可以明確定義那些異常丟擲時不回滾事務。還可以程式設計性的通過setRollbackOnly()方法來指示一個事務必須回滾,在呼叫完setRollbackOnly()後你所能執行的唯一操作就是回滾。


事務只讀屬性

只讀事務用於客戶程式碼只讀但不修改資料的情形,只讀事務用於特定情景下的優化,比如使用Hibernate的時候。 預設為讀寫事務。

        “只讀事務”並不是一個強制選項,它只是一個“暗示”,提示資料庫驅動程式和資料庫系統,這個事務並不包含更改資料的操作,那麼JDBC驅動程式和資料庫就有可能根據這種情況對該事務進行一些特定的優化,比方說不安排相應的資料庫鎖,以減輕事務對資料庫的壓力,畢竟事務也是要消耗資料庫的資源的。 

但是你非要在“只讀事務”裡面修改資料,也並非不可以,只不過對於資料一致性的保護不像“讀寫事務”那樣保險而已。 

因此,“只讀事務”僅僅是一個性能優化的推薦配置而已,並非強制你要這樣做不可


mybatis在寫mapper.xml檔案時,如何解決引數型別只能傳一個的問題?update語句怎麼寫?

這兩個問題,見mapper介面檔案和mapper.xml檔案中的寫法或說明.

還是寫在這吧:

	//更改使用者
	public void updateUserById(@Param("id") Integer id,@Param("username") String name,
			@Param("age") Integer age,@Param("sex") String sex) throws Exception;

	<!-- 我直接在mapper介面中使用了mybatis的引數註解@Param, 在這裡就不用在指明引數型別了-->
	<!-- 即使你想指明也無法直接指明,因為引數型別只接收一個引數,無法接收多個,@Param就是解決這個問題的 -->
	<select id="updateUserById">
		update user set username=#{username},age=#{age},sex=#{sex} where id=#{id}
	</select>



MySQL的user表中本來15條資料,我把後5條給刪除了,再插入新使用者後id會從16開始計數, 導致重新插入值,欄位id取值不連續.

ALTER TABLE USER AUTO_INCREMENT=10;  (此處10改為自己的斷點即可)