声明式事务的应用(以ssm项目为例)

时间:2022-02-23 08:10:35

添加事务在实际项目中是必不可少的,事务是用来实现要么全都成功,要么全都不成功, 而主要针对的就是要不成功就全都不成功的问题.

想象你给你女朋友转账,这边刚扣了钱,你女朋友账户还没收到钱呢,突然出故障了,比如停电了. 那怎么办. 这就涉及到一个关键的知识点:事务. 利用的事务的回滚,把你账户上扣的钱再回滚到你账户上.


本篇采用maven构建war工程涉及主要知识点:

spring+springMVC+mybatis如何整合,  

mybatis在写mapper.xml文件时,如何解决参数类型只能传一个的问题

update语句怎么写

如何保证正确配置了事务,事务少配一个,就会失效



声明式事务介绍:

声明式事务管理建立在AOP之上的。其本质是对方法前后进行拦截,然后在目标方法开始之前创建或者加入一个事务,在执行完目标方法之后根据执行情况提交或者回滚事务。声明式事务最大的优点就是不需要通过编程的方式管理事务,这样就不需要在业务逻辑代码中掺杂事务管理的代码,只需在配置文件中做相关的事务规则声明(或通过基于@Transactional注解的方式),便可以将事务规则应用到业务逻辑中。

        显然声明式事务管理要优于编程式事务管理,这正是spring倡导的非侵入式的开发方式。声明式事务管理使业务代码不受污染,一个普通的POJO对象,只要加上注解就可以获得完全的事务支持。和编程式事务相比,声明式事务唯一不足地方是,后者的最细粒度只能作用到方法级别,无法做到像编程式事务那样可以作用到代码块级别。但是即便有这样的需求,也存在很多变通的方法,比如,可以将需要进行事务管理的代码块独立为方法等等。

         声明式事务管理也有两种常用的方式,一种是基于tx和aop名字空间的xml配置文件,另一种就是基于@Transactional注解。显然基于注解的方式更简单易用,更清爽。实际中两种方式常组合起来使用.本篇的案例项目就是组合使用实现事务功能.


项目结构:

声明式事务的应用(以ssm项目为例)


数据库部分:

数据库中就一张表user

user表的结构

声明式事务的应用(以ssm项目为例)

user表的数据

声明式事务的应用(以ssm项目为例)


代码部分:

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:

声明式事务的应用(以ssm项目为例)

需要特别注意的是这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 请求结果为

声明式事务的应用(以ssm项目为例)

点击新增可添加一个用户(代码中定义死了,添加的是郑爽),点击更改可更改一个用户(更改后的用户叫逗逼姚,只更改username,age,sex三个字段)

为了测试事务回滚,特意加了//int a=6/0; 这样的代码来人为制造异常,此时事务是否添加上了(看是否回滚).可根据需要自行注释或解除注释.



归纳点:

要想事务配置成功:

1, 要在applicationContext-tran.xml配置文件中 添加事务的注解驱动,必须要有.

这几项缺一不可.(提醒: 声明式事务是基于AOP的)

声明式事务的应用(以ssm项目为例)

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文件中配置过了),就像增加标记不靠谱一样

声明式事务的应用(以ssm项目为例)


这个增加标记长什么样呢?在上面这个配置文件中加一行代码就会出现了

声明式事务的应用(以ssm项目为例)

实际上<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删除末尾数据后,再插入新数据id不连续解决方案


MySQL的user表中本来15条数据,我把后5条给删除了,再插入新用户后id会从16开始计数, 导致重新插入值,字段id取值不连续.

ALTER TABLE USER AUTO_INCREMENT=10;  (此处10改为自己的断点即可)