目录
1.模拟转账案例
使用DbUtils、Junit整合Spring进行模拟。
pom.xml
<dependencies>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.12</version>
</dependency>
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>commons-dbutils</groupId>
<artifactId>commons-dbutils</artifactId>
<version>1.6</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.9</version>
</dependency>
<dependency>
<groupId>org.projectlombok</groupId>
<artifactId>lombok</artifactId>
<version>1.16.20</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.2.8.RELEASE</version>
</dependency>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.2.8.RELEASE</version>
</dependency>
</dependencies>
applicationContext.xml
<?xml version="1.0" encoding="UTF-8"?>
<beans xmlns="http://www.springframework.org/schema/beans"
xmlns:context="http://www.springframework.org/schema/context"
xmlns:xsi="http://www.w3.org/2001/XMLSchema-instance"
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.xsd">
<context:component-scan base-package="com.study"/>
<context:property-placeholder location="classpath:jdbc.properties"/>
<bean id="dataSource" class="com.alibaba.druid.pool.DruidDataSource">
<property name="driverClassName" value="${jdbc.driver}"/>
<property name="url" value="${jdbc.url}"/>
<property name="username" value="${jdbc.username}"/>
<property name="password" value="${jdbc.password}"/>
</bean>
<bean id="queryRunner" class="org.apache.commons.dbutils.QueryRunner">
<constructor-arg name="ds" ref="dataSource"/>
</bean>
</beans>
测试类
@RunWith(SpringRunner.class)
@ContextConfiguration(locations = "classpath:applicationContext.xml")
public class TestDemo {
@Autowired
AccountService accountService;
@Test
public void transfer(){
accountService.transfer("tom","jerry",100D);
}
}
service接口
public interface AccountService {
void transfer(String inName,String outName,Double money);
}
service实现类
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao;
@Override
public void transfer(String inName, String outName, Double money) {
accountDao.outMoney(outName,money);
int i= 1/0;//模拟异常
accountDao.inMoney(inName,money);
}
}
dao接口
public interface AccountDao {
void inMoney(String name,Double money);
void outMoney(String name,Double money);
}
dao实现类
@Repository
public class AccountDaoImpl implements AccountDao {
@Autowired
private QueryRunner queryRunner;
@Override
public void inMoney(String name, Double money) {
try {
String sql = "update account set money = money - ? where name = ?";
queryRunner.update(sql,money,name);
} catch (SQLException e) {
e.printStackTrace();
}
}
@Override
public void outMoney(String name, Double money) {
try {
String sql = "update account set money = money + ? where name = ?";
queryRunner.update(sql,money,name);
} catch (SQLException e) {
e.printStackTrace();
}
}
}
上述service实现类当中,模拟了在转账过程当中出现了不可抗力因素的异常,导致数据库表中没有达到预期效果,转出用户少了钱但是转入用户没有增加,正常是不允许这种情况出现的。
2.事务控制
MySQL中事务的提交方式默认是自动提交,并且一个数据库连接一旦提交,就不可以回滚。上述案例中,service中执行了outMoney后就直接将事务进行提交,如果想要控制这种情况不再发生,那么我们需要把转入和转出操作作为一个最小的逻辑单元,也就是使用同一个事务去进行控制。使用同一个事务进行控制就需要两次操作使用同一个连接。如下代码:
service实现类
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao;
@Autowired
private DataSource dataSource;//在配置文件中,已经将数据源加载进IOC容器当中,在此直接注入即可
@Override
public void transfer(String inName, String outName, Double money) {
Connection connection = null;
try {
//获取连接
connection = dataSource.getConnection();
//设置为手动提交
connection.setAutoCommit(false);
accountDao.outMoney(connection,outName,money);
int i= 1/0;//模拟异常
accountDao.inMoney(connection,inName,money);
//提交事务
connection.commit();
} catch (Exception e) {
try {
connection.rollback();
} catch (SQLException e1) {
e1.printStackTrace();
}
e.printStackTrace();
} finally {
try {
//将事务还原为自动提交
connection.setAutoCommit(true);
//将连接归还给连接池
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
dao接口
public interface AccountDao {
void inMoney(String name,Double money);
void outMoney(String name,Double money);
void inMoney(Connection connection,String name, Double money);
void outMoney(Connection connection,String name,Double money);
}
dao实现类
@Repository
public class AccountDaoImpl implements AccountDao {
@Autowired
private QueryRunner queryRunner;
@Override
public void inMoney(String name, Double money) {
try {
String sql = "update account set money = money - ? where name = ?";
queryRunner.update(sql,money,name);
} catch (SQLException e) {
e.printStackTrace();
}
}
@Override
public void outMoney(String name, Double money) {
try {
String sql = "update account set money = money + ? where name = ?";
queryRunner.update(sql,money,name);
} catch (SQLException e) {
e.printStackTrace();
}
}
@Override
public void inMoney(Connection connection,String name, Double money) {
try {
String sql = "update account set money = money - ? where name = ?";
queryRunner.update(connection,sql,money,name);
} catch (SQLException e) {
e.printStackTrace();
}
}
@Override
public void outMoney(Connection connection,String name, Double money) {
try {
String sql = "update account set money = money + ? where name = ?";
queryRunner.update(connection,sql,money,name);
} catch (SQLException e) {
e.printStackTrace();
}
}
}
其余类未修改
以上方式解决了转账案例的数据不一致问题,控制住了事务。
3.ThreadLocal的说明
在2中,service调用dao时每次都需要传入一个connection连接,耦合性比较高。如果想要降低代码的耦合性,我们可以将同一个连接放入到一个容器当中,service中控制事务使用这个连接,dao中操作数据库也使用这个连接。这样就降低了代码的耦合性。如下图:
在操作共享变量时,会出现线程安全问题,Java类库提供了ThreadLocal类来解决这个问题。
防止正在使用的变量被篡改。
在一个线程中,保证每个步骤使用的是同一个变量。
ThreadLocal的key就是当前线程,value是个泛型,定义时传入的是什么就是什么。
4.ThreadLocal的使用
工具类
@Component
public class ConnectionUtil {
private static final ThreadLocal<Connection> TL = new ThreadLocal<>();
@Autowired
private DataSource ds;
/**
* 既可以向ThreadLocal中添加连接,也可以从ThreadLocal中取出连接。
* 先取出,如果ThreadLocal中没有连接,则从数据源中获取连接放入ThreadLocal
*/
public Connection getConnFromThreadLocal(){
Connection connection = null;
try {
connection = TL.get();
if (connection == null){
connection = ds.getConnection();
TL.set(connection);
}
connection.setAutoCommit(false);
} catch (Exception e) {
e.printStackTrace();
}
return connection;
}
/**
* 将与此线程绑定的连接从ThreadLocal中剔除.
* 若一直不剔除会出现内存溢出的问题.
*/
public void removeFromThreadLocal(){
TL.remove();
}
}
service实现类
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao;
@Autowired
private DataSource dataSource;//在配置文件中,已经将数据源加载进IOC容器当中,在此直接注入即可
@Autowired
private ConnectionUtil connectionUtil;
@Override
public void transfer(String inName, String outName, Double money) {
Connection connection = null;
try {
/*//获取连接
connection = dataSource.getConnection();
//设置为手动提交
connection.setAutoCommit(false);*/
//从ThreadLocal中获取连接
connection= connectionUtil.getConnFromThreadLocal();
accountDao.outMoney(outName,money);
int i= 1/0;//模拟异常
accountDao.inMoney(inName,money);
//提交事务
connection.commit();
} catch (Exception e) {
try {
connection.rollback();
} catch (SQLException e1) {
e1.printStackTrace();
}
e.printStackTrace();
} finally {
try {
//将线程绑定的连接从ThreadLocal中移除
connectionUtil.removeFromThreadLocal();
//将事务还原为自动提交
connection.setAutoCommit(true);
//将连接归还给连接池
connection.close();
} catch (SQLException e) {
e.printStackTrace();
}
}
}
}
dao实现类
@Repository
public class AccountDaoImpl implements AccountDao {
@Autowired
private QueryRunner queryRunner;
@Autowired
private ConnectionUtil connectionUtil;
@Override
public void inMoney(String name, Double money) {
try {
String sql = "update account set money = money - ? where name = ?";
queryRunner.update(connectionUtil.getConnFromThreadLocal(),sql,money,name);
} catch (SQLException e) {
e.printStackTrace();
}
}
@Override
public void outMoney(String name, Double money) {
try {
String sql = "update account set money = money + ? where name = ?";
queryRunner.update(connectionUtil.getConnFromThreadLocal(),sql,money,name);
} catch (SQLException e) {
e.printStackTrace();
}
}
}
5.自定义事务管理器
在4中,我们使用ThreadLocal解决了事务的问题,但是不难看出service实现类中的代码过于冗余。所以我们抽出一个工具类用于解决业务逻辑以外的操作。
自定义事务管理器
@Component
public class MyTrunsactionManager {
@Autowired
private ConnectionUtil connectionUtil;
//开启事务
public void begin(){
connectionUtil.getConnFromThreadLocal();
}
//提交事务
public void commit(){
try {
connectionUtil.getConnFromThreadLocal().commit();
} catch (Exception e) {
e.printStackTrace();
}
}
//回滚事务
public void rollback(){
try {
connectionUtil.getConnFromThreadLocal().rollback();
} catch (Exception e) {
e.printStackTrace();
}
}
//释放资源
public void release(){
try {
connectionUtil.getConnFromThreadLocal().setAutoCommit(true);
connectionUtil.removeFromThreadLocal();
connectionUtil.getConnFromThreadLocal().close();
} catch (Exception e) {
e.printStackTrace();
}
}
}
service实现类
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao;
@Autowired
private MyTrunsactionManager trunsactionManager;
@Override
public void transfer(String inName, String outName, Double money) {
Connection connection = null;
try {
//开启事务
trunsactionManager.begin();
accountDao.outMoney(outName,money);
int i= 1/0;//模拟异常
accountDao.inMoney(inName,money);
//提交事务
trunsactionManager.commit();
} catch (Exception e) {
trunsactionManager.rollback();
e.printStackTrace();
} finally {
trunsactionManager.release();
}
}
}
其余代码不变
6.JDK动态代理方式简化代码
在service中只应该关心业务逻辑,其他操作比如开启事务、提交事务、回滚事务、释放资源本不应该是service层处理的。所以使用动态代理的方式对其增强。
JDK动态代理有个前提,就是目标对象需要有实现接口。
定义一个JDK动态代理的工厂
@Component
public class JDKProxyFactory {
@Autowired
private MyTrunsactionManager trunsactionManager;
public Object createProxyObject(Object target){
return Proxy.newProxyInstance(JDKProxyFactory.class.getClassLoader(),
target.getClass().getInterfaces(),
new InvocationHandler() {
@Override
public Object invoke(Object proxy, Method method, Object[] args) throws Throwable {
Object object = null;
trunsactionManager.begin();
try {
object = method.invoke(target,args);
trunsactionManager.commit();
} catch (Exception e) {
trunsactionManager.rollback();
e.printStackTrace();
} finally {
trunsactionManager.release();
}
return object;
}
});
}
}
service实现类
@Service
public class AccountServiceImpl implements AccountService {
@Autowired
private AccountDao accountDao;
@Autowired
private MyTrunsactionManager trunsactionManager;
@Override
public void transfer(String inName, String outName, Double money) {
accountDao.outMoney(outName,money);
int i= 1/0;//模拟异常
accountDao.inMoney(inName,money);
}
}
测试类
@RunWith(SpringRunner.class)
@ContextConfiguration(locations = "classpath:applicationContext.xml")
public class TestDemo {
@Autowired
private AccountService accountService;
@Autowired
private JDKProxyFactory jdkProxyFactory;
@Test
public void transfer(){
AccountService accountServiceProxy = (AccountService) jdkProxyFactory.createProxyObject(accountService);
accountServiceProxy.transfer("tom","jerry",100D);
}
}
其余代码不变
7.CGLIB动态代理方式简化代码
CGLIB动态代理比JDK动态代理要强大,因为JDK动态代理中目标对象必须要实现接口,而CGLIB动态代理对目标对象没有这种要求。但CGLIB动态代理的效率要低于JDK动态代理。
定义一个CGLIB动态代理的工厂
@Component
public class CGLIBProxyFactory {
@Autowired
private MyTrunsactionManager trunsactionManager;
public Object createProxyObject(Object target){
return Enhancer.create(target.getClass(), new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
Object invoke = null;
try {
trunsactionManager.begin();
invoke = method.invoke(target, objects);
trunsactionManager.commit();
} catch (Exception e) {
trunsactionManager.rollback();
e.printStackTrace();
} finally {
trunsactionManager.rollback();
}
return invoke;
}
});
}
}
测试类
@RunWith(SpringRunner.class)
@ContextConfiguration(locations = "classpath:applicationContext.xml")
public class TestDemo {
@Autowired
private AccountService accountService;
@Autowired
private JDKProxyFactory jdkProxyFactory;
@Autowired
private CGLIBProxyFactory cglibProxyFactory;
@Test
public void transfer(){
// AccountService accountServiceProxy = (AccountService) jdkProxyFactory.createProxyObject(accountService);
AccountService accountServiceProxy = (AccountService) cglibProxyFactory.createProxyObject(accountService);
accountServiceProxy.transfer("tom","jerry",100D);
}
}