1. 转账案例
1.1 前置准备
1.1.1 准备数据库环境
CREATE DATABASE `spring_db`;
USE `spring_db`;
CREATE TABLE `account`
(
`id` int(11) NOT NULL AUTO_INCREMENT,
`name` varchar(32) DEFAULT NULL,
`money` double DEFAULT NULL,
PRIMARY KEY (`id`)
);
insert into `account`(`id`, `name`, `money`)
values (1, 'tom', 1000),
(2, 'jerry', 1000);
1.1.2 Maven配置
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.15</version>
</dependency>
<!--此处需要注意的是,spring5 及以上版本要求 junit 的版本必须是 4.12 及以上-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.3.15</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
</dependency>
<!-- 注解依赖 -->
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
</dependency>
<!-- 数据库连接相关 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.9</version>
</dependency>
<dependency>
<groupId>commons-dbutils</groupId>
<artifactId>commons-dbutils</artifactId>
<version>1.6</version>
</dependency>
</dependencies>
1.1.4 数据库连接配置
jdbc.driver=com.mysql.jdbc.Driver
jdbc.url=jdbc:mysql://localhost:3306/spring_db?useSSL=false
jdbc.username=root
jdbc.password=123456
1.1.5 创建实体类
public class Account {
private Integer id;
private String name;
private Double money;
}
1.1.6 AccountDao接口
-
AccountDao
package cn.knightzz.dao; public interface AccountDao { /** * 转出操作 * @param outUser * @param money */ public void out(String outUser, Double money); /** * 转入操作 * @param inUser * @param money */ public void in(String inUser, Double money); }
-
AccountDaoImpl
package cn.knightzz.dao.impl; import cn.knightzz.dao.AccountDao; import org.apache.commons.dbutils.QueryRunner; import org.springframework.stereotype.Repository; import javax.annotation.Resource; import java.sql.SQLException; @Repository("accountDao") public class AccountDaoImpl implements AccountDao { @Resource private QueryRunner queryRunner; /** * 转出操作 * * @param outUser * @param money */ @Override public void out(String outUser, Double money) { String sql = "update account set money = ? where name = ?"; try { queryRunner.update(sql, money, outUser); } catch (SQLException e) { e.printStackTrace(); } } /** * 转入操作 * * @param inUser * @param money */ @Override public void in(String inUser, Double money) { String sql = "update account set money = ? where name = ?"; try { queryRunner.update(sql, money, inUser); } catch (SQLException e) { e.printStackTrace(); } } }
1.1.7 AccountService接口
-
AccountService
package cn.knightzz.service; */ public interface AccountService { public void transfer(String outUser, String inUser, Double money); }
-
AccountServiceImpl
package cn.knightzz.service.impl; import cn.knightzz.dao.AccountDao; import cn.knightzz.entity.Account; import cn.knightzz.service.AccountService; import javax.annotation.Resource; public class AccountServiceImpl implements AccountService { @Resource private AccountDao accountDao; @Override public void transfer(String outUser, String inUser, Double money) { accountDao.out(outUser, money); accountDao.in(inUser, money); } }
1.1.8 Spring相关配置
-
SpringConfig
package cn.knightzz.config; import org.apache.commons.dbutils.QueryRunner; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.ComponentScan; import org.springframework.context.annotation.Configuration; import org.springframework.context.annotation.Import; import javax.sql.DataSource; @Configuration @ComponentScan("cn.knightzz") @Import(DataSourceConfig.class) public class SpringConfig { @Bean("queryRunner") public QueryRunner getQueryRunner(@Autowired DataSource dataSource){ return new QueryRunner(dataSource); } }
-
DataSourceConfig
package cn.knightzz.config; import com.alibaba.druid.pool.DruidDataSource; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.PropertySource; import javax.sql.DataSource; /** * @author 王天赐 * @title: DataSourceConfig * @projectName mybatis-apply-06 * @description: * @website http://knightzz.cn/ * @github https://github.com/knightzz1998 * @date 2022/1/25 13:03 */ @PropertySource("classpath:jdbc.properties") public class DataSourceConfig { @Value("${jdbc.driver}") private String driver; @Value("${jdbc.url}") private String url; @Value("${jdbc.username}") private String username; @Value("${jdbc.password}") private String password; @Bean("dataSource") public DataSource getDataSource(){ DruidDataSource dataSource = new DruidDataSource(); dataSource.setUsername(username); dataSource.setPassword(password); dataSource.setDriverClassName(driver); dataSource.setUrl(url); return dataSource; } }
1.1.9 测试代码
package cn.knightzz;
import cn.knightzz.config.SpringConfig;
import cn.knightzz.service.AccountService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import javax.annotation.Resource;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {SpringConfig.class})
public class App {
@Resource
AccountService accountService;
@Test
public void testTransfer(){
accountService.transfer("tom", "jerry", 100d);
}
}
1.1.10 问题分析
- 上述的代码的事务是在Dao层实现的, 转入和转出的事务都是独立的, 这样有很大的问题,比如 转出操作正常, 转入操作不正常的情况下, 就会出现转出账户的钱减少了, 但是转入账户的钱没有增加
- 所以我们需要将事务放到Service层实现
1.2 传统事务编写
1.2.1 线程绑定工具类编写
-
从数据库连接池中获取连接并绑定线程, 这样可以保证转入转出使用的同一个连接对象
-
ConnectionUtils
package cn.knightzz.utils; import org.springframework.context.annotation.ComponentScan; import org.springframework.stereotype.Component; import javax.annotation.Resource; import javax.sql.DataSource; import java.sql.Connection; import java.sql.SQLException; @Component public class ConnectionUtils { /** * 待绑定的线程 */ private ThreadLocal<Connection> threadLocal = new ThreadLocal<>(); /** * 数据库连接源 */ @Resource DataSource dataSource; public Connection getThreadConnection(){ // 1. 先从ThreadLocal获取连接, 如果获取不到再从数据库连接池中获取 Connection connection = threadLocal.get(); // 2. 从连接池中获取数据 if (connection == null) { try { connection = dataSource.getConnection(); threadLocal.set(connection); } catch (SQLException e) { e.printStackTrace(); } } return connection; } /** * 解除绑定 */ public void removeThreadConnection(){ threadLocal.remove(); } }
1.2.2 事务管理器编写
-
事务管理器编写主要步骤
- 开始事务 : 关闭自动提交事务
- 提交事务
- 回滚事务
- 解除绑定
- 改回自动提交事务
- 将数据库连接归还到连接池
- 解除线程绑定
-
事务管理器代码
package cn.knightzz.utils; import org.springframework.stereotype.Component; import javax.annotation.Resource; import java.sql.SQLException; 5 @Component public class TransactionManager { @Resource private ConnectionUtils connectionUtils; /** * 开启事务 */ public void beginTransaction(){ // 关闭自动提交事务 try { connectionUtils.getThreadConnection().setAutoCommit(false); } catch (SQLException e) { e.printStackTrace(); } } /** * 提交事务 */ public void commit(){ try { connectionUtils.getThreadConnection().commit(); } catch (SQLException e) { e.printStackTrace(); } } /** * 回滚事务 */ public void rollBack(){ try { connectionUtils.getThreadConnection().rollback(); } catch (SQLException e) { e.printStackTrace(); } } /** * 释放资源 */ public void release(){ try { // 改回自动提交事务 connectionUtils.getThreadConnection().setAutoCommit(true); // 将数据库连接归还到连接池 connectionUtils.getThreadConnection().close(); // 解除线程绑定 connectionUtils.removeThreadConnection(); } catch (SQLException e) { e.printStackTrace(); } } }
1.2.3 修改Dao层和Service层
-
修改Service层代码 : 手动控制事务的开启提交和回滚以及资源的释放
package cn.knightzz.service.impl; import cn.knightzz.dao.AccountDao; import cn.knightzz.entity.Account; import cn.knightzz.service.AccountService; import cn.knightzz.utils.TransactionManager; import org.springframework.stereotype.Service; import javax.annotation.Resource; @Service public class AccountServiceImpl implements AccountService { @Resource private AccountDao accountDao; @Resource private TransactionManager transactionManager; @Override public void transfer(String outUser, String inUser, Double money) { try { // 开启事务 transactionManager.beginTransaction(); accountDao.out(outUser, money); // 模拟异常情况 int i = 1 / 0 ; accountDao.in(inUser, money); // 提交事务 transactionManager.commit(); } catch (Exception e) { e.printStackTrace(); // 回滚 transactionManager.rollBack(); } finally { // 释放资源 transactionManager.release(); } } }
-
修改Dao层代码 : 手动传入数据库连接
package cn.knightzz.dao.impl; import cn.knightzz.dao.AccountDao; import cn.knightzz.utils.ConnectionUtils; import org.apache.commons.dbutils.QueryRunner; import org.springframework.stereotype.Repository; import javax.annotation.Resource; import java.sql.SQLException; @Repository("accountDao") public class AccountDaoImpl implements AccountDao { @Resource private QueryRunner queryRunner; @Resource private ConnectionUtils connectionUtils; /** * 转出操作 * * @param outUser * @param money */ @Override public void out(String outUser, Double money) { String sql = "update account set money = money - ? where name = ?"; try { // 手动传入数据库连接 queryRunner.update(connectionUtils.getThreadConnection() , sql, money, outUser); } catch (SQLException e) { e.printStackTrace(); } } /** * 转入操作 * * @param inUser * @param money */ @Override public void in(String inUser, Double money) { String sql = "update account set money = money + ? where name = ?"; try { queryRunner.update(connectionUtils.getThreadConnection(),sql, money, inUser); } catch (SQLException e) { e.printStackTrace(); } } }
1.2.4 问题分析
- 上面代码,通过对业务层改造,已经可以实现事务控制了,但是由于我们添加了事务控制,也产生了
一个新的问题:Service方法变得臃肿了,里面充斥着很多重复代码。并且业务层方法和事务控制方法耦
合了,违背了面向对象的开发思想 - 如果需要事务控制的方法很多, 我们需要写很多重复的代码
1.3 Proxy优化转账案例
-
我们可以将业务代码和事务代码进行拆分,通过动态代理的方式,对业务方法进行事务的增强。这样
就不会对业务层产生影响,解决了耦合性的问题啦!
1.3.1 常用动态代理技术
- JDK 代理 : 基于接口的动态代理技术·:利用拦截器(必须实现invocationHandler)加上反射机制生成
一个代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理,从而实现方法增强 - CGLIB代理:基于父类的动态代理技术:动态生成一个要代理的子类,子类重写要代理的类的所有不是
final的方法。在子类中采用方法拦截技术拦截所有的父类方法的调用,顺势织入横切逻辑,对方法进行
增强
1.3.2 JDK动态代理方式
-
JDK工厂类
-
InvocationTargetException
异常由Method.invoke(obj, args…)方法抛出。当被调用的方法的内部抛出了异常而没有被捕获时,将由此异常接收, 这个异常不要处理,package cn.knightzz.proxy; import cn.knightzz.service.AccountService; import cn.knightzz.utils.TransactionManager; import org.springframework.beans.factory.annotation.Autowired; import org.springframework.stereotype.Component; import javax.annotation.Resource; import java.lang.reflect.InvocationHandler; import java.lang.reflect.InvocationTargetException; import java.lang.reflect.Method; import java.lang.reflect.Proxy; /** * @author 王天赐 * @title: JdkProxyFactory * @projectName mybatis-apply-06 * @description: * @website http://knightzz.cn/ * @github https://github.com/knightzz1998 * @date 2022/1/25 20:13 */ @Component public class JdkProxyFactory { @Resource AccountService accountService; @Resource TransactionManager transactionManager; /** * ClassLoader : 类加载器, 借助被代理的对象获取 ClassLoader * Interfaces : 被代理类的 * InvocationHandler : 代理对象 * @exception : InvocationTargetException异常由Method.invoke(obj, args...)方法抛出。当被调用的方法的内部抛出了异常而没有被捕获时,将由此异常接收!!! * @return */ public AccountService createAccountServiceJdkProxy(){ AccountService accountServiceProxy = null; // 使用JDK的代理类创建实例 // 基于接口的动态代理技术·:利用拦截器(必须实现invocationHandler)加上反射机制生成 // 一个代理接口的匿名类,在调用具体方法前调用InvokeHandler来处理,从而实现方法增强 accountServiceProxy = (AccountService) Proxy.newProxyInstance(accountService.getClass().getClassLoader(), accountService.getClass().getInterfaces(), new InvocationHandler() { // 1. proxy 当前代理对象引用 // 2. method 被调用的目标方法的引用 @Override public Object invoke(Object proxy, Method method, Object[] args) throws Throwable { Object result = null; try { // 开启事务 transactionManager.beginTransaction(); result = method.invoke(accountService, args); // 提交事务 transactionManager.commit(); } catch (Exception e) { e.printStackTrace(); // 回滚事务 transactionManager.rollBack(); } finally { // 释放资源 transactionManager.release(); } return result; } }); return accountServiceProxy; } }
-
修改AccountService
-
不要使用 try catch 处理异常, 如果异常在 Service被处理, 在 JDK代理处就无法被捕获,
package cn.knightzz.service.impl; import cn.knightzz.dao.AccountDao; import cn.knightzz.entity.Account; import cn.knightzz.service.AccountService; import cn.knightzz.utils.TransactionManager; import org.springframework.stereotype.Service; import javax.annotation.Resource; @Service public class AccountServiceImpl implements AccountService { @Resource private AccountDao accountDao; @Resource private TransactionManager transactionManager; @Override public void transfer(String outUser, String inUser, Double money) { // 开启事务 accountDao.out(outUser, money); // 模拟异常情况 int i = 1 / 0; accountDao.in(inUser, money); } }
-
测试代码
package cn.knightzz; import cn.knightzz.config.SpringConfig; import cn.knightzz.proxy.JdkProxyFactory; import cn.knightzz.service.AccountService; import org.junit.Test; import org.junit.runner.RunWith; import org.springframework.test.context.ContextConfiguration; import org.springframework.test.context.junit4.SpringJUnit4ClassRunner; import javax.annotation.Resource; /** * @author 王天赐 * @title: App * @projectName mybatis-apply-06 * @description: * @website http://knightzz.cn/ * @github https://github.com/knightzz1998 * @date 2022/1/25 13:11 */ @RunWith(SpringJUnit4ClassRunner.class) @ContextConfiguration(classes = {SpringConfig.class}) public class App { @Resource JdkProxyFactory jdkProxyFactory; @Resource AccountService accountService; @Test public void testProxyTransfer(){ // 获取代理对象 AccountService accountServiceJdkProxy = jdkProxyFactory.createAccountServiceJdkProxy(); // 代理对象调用任何接口的方法, 都会直接调用 InvocationHandler 的 invoke 方法 accountServiceJdkProxy.transfer("tom", "jerry", 10d); } @Test public void testTransfer(){ accountService.transfer("tom", "jerry", 10d); } }
1.3.3 CGLIB动态代理方式
- Enhancer.create(参数一 , 参数二) :
- 参数一:目标对象的字节码对象
- 参数二:动作类,实现增强功能
package cn.knightzz.proxy;
import cn.knightzz.service.AccountService;
import cn.knightzz.utils.TransactionManager;
import org.springframework.cglib.proxy.Enhancer;
import org.springframework.cglib.proxy.MethodInterceptor;
import org.springframework.cglib.proxy.MethodProxy;
import org.springframework.stereotype.Component;
import javax.annotation.Resource;
import java.lang.reflect.InvocationTargetException;
import java.lang.reflect.Method;
/**
* @author 王天赐
* @title: CglibProxyFactory
* @projectName mybatis-apply-06
* @description:
* @website http://knightzz.cn/
* @github https://github.com/knightzz1998
* @date 2022/1/25 22:05
*/
@Component
public class CglibProxyFactory {
@Resource
AccountService accountService;
@Resource
TransactionManager transactionManager;
public AccountService createAccountServiceJdkProxy(){
AccountService accountServiceProxy = null;
accountServiceProxy = (AccountService) Enhancer.create(accountService.getClass(), new MethodInterceptor() {
@Override
public Object intercept(Object o, Method method, Object[] objects, MethodProxy methodProxy) throws Throwable {
Object result = null;
try {
// 1. 开启事务
transactionManager.beginTransaction();
// 2. 处理业务
result = method.invoke(accountService, objects);
// 3. 提交事务
transactionManager.commit();
} catch (Exception e) {
e.printStackTrace();
transactionManager.rollBack();
} finally {
transactionManager.release();
}
return result;
}
});
return accountServiceProxy;
}
}
2. 初识AOP
2.1 AOP概述
2.1.1 AOP基本概念
- AOP 为 Aspect Oriented Programming 的缩写,意思为面向切面编程
- AOP 是 OOP(面向对象编程) 的延续,是软件开发中的一个热点,也是Spring框架中的一个重要内
容,利用AOP可以对业务逻辑的各个部分进行隔离,从而使得业务逻辑各部分之间的耦合度降低,提高
程序的可重用性,同时提高了开发的效率。
2.1.2 AOP优点
- 在程序运行期间,在不修改源码的情况下对方法进行功能增强
- 逻辑清晰,开发核心业务的时候,不必关注增强业务的代码
- 减少重复代码,提高开发效率,便于后期维护
2.1.3 AOP底层实现
- 实际上,AOP 的底层是通过 Spring 提供的的动态代理技术实现的。在运行期间,Spring通过动态代
理技术动态的生成代理对象,代理对象方法执行时进行增强功能的介入,在去调用目标对象的方法,从
而完成功能的增强。
2.1.4 AOP相关概念
-
Spring 的 AOP 实现底层就是对上面的动态代理的代码进行了封装,封装后我们只需要对需要关注的
部分进行代码编写,并通过配置的方式完成指定目标的方法增强。
-
Target
(目标对象):代理的目标对象 -
Proxy
(代理):一个类被 AOP 织入增强后,就产生一个结果代理类 -
Joinpoint
(连接点):所谓连接点是指那些可以被拦截到的点。在spring中,这些点指的是方法,因为 spring只支持方法类型的连接点 -
Pointcut
(切入点):所谓切入点是指我们要对哪些 Joinpoint 进行拦截的定义 -
Advice
(通知/ 增强):所谓通知是指拦截到 Joinpoint 之后所要做的事情就是通知 分类:前置通知、后置通知、异常通知、最终通知、环绕通知 -
Aspect
(切面):是切入点和通知(引介)的结合 -
Weaving
(织入):是指把增强应用到目标对象来创建新的代理对象的过程。spring采用动态代理织 入,而AspectJ采用编译期织入和类装载期织入
2.1.4 AOP开发注意事项
-
开发阶段
- 编写核心业务代码(目标类的目标方法) 切入点
- 把公用代码抽取出来,制作成通知(增强功能方法) 通知
- 在配置文件中,声明切入点与通知间的关系,即切面
-
运行阶段
-
Spring 框架监控切入点方法的执行。一旦监控到切入点方法被运行,使用代理机制,动态创建目标对
象的代理对象,根据通知类别,在代理对象的对应位置,将通知对应的功能织入,完成完整的代码逻辑
运行
-
-
底层代理实现
- 在 Spring 中,框架会根据目标类是否实现了接口来决定采用哪种动态代理的方式
- 当 bean 实现接口时,会用JDK代理模式
- 当 bean 没有实现接口,用cglib实现(可以强制使用cglib(在spring配置中加入
<aop:aspectj-autoproxy proxyt-target-class="true"/>
)
2.2 基于XML的AOP
2.2.1 Maven 配置
<dependencies>
<!--导入spring的context坐标,context依赖aop-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.15</version>
</dependency>
<!-- aspectj的织入(切点表达式需要用到该jar包) -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.7</version>
</dependency>
<!--spring整合junit-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.3.15</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
</dependency>
</dependencies>
2.2.2 Service接口
-
AccountService
package cn.knightzz.service; public interface AccountService { /** * 转账 */ void transfer(); }
-
AccountServiceImpl
package cn.knightzz.service.impl; import cn.knightzz.service.AccountService; public class AccountServiceImpl implements AccountService { @Override public void transfer() { System.out.println("开启转账业务 ... "); } }
2.2.3 创建通知类
package cn.knightzz.advice;
public class MyAdvice {
public void before(){
System.out.println("开始前置通知 ... ");
}
}
2.2.4 Spring配置
- 需要添加 aop 的命名空间
xmlns:aop="http://www.springframework.org/schema/aop
<?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"
xsi:schemaLocation="http://www.springframework.org/schema/beans
http://www.springframework.org/schema/beans/spring-beans.xsd
http://www.springframework.org/schema/aop
http://www.springframework.org/schema/aop/spring-aop.xsd"
xmlns:p="http://www.springframework.org/schema/p">
<!-- 配置 Service 接口和前置通知类-->
<bean id="accountService" class="cn.knightzz.service.impl.AccountServiceImpl"></bean>
<bean id="myAdvice" class="cn.knightzz.advice.MyAdvice"></bean>
<aop:config>
<aop:aspect ref="myAdvice">
<aop:before method="before"
pointcut="execution(public void cn.knightzz.service.impl.AccountServiceImpl.transfer())" />
</aop:aspect>
</aop:config>
</beans>
2.2.5 测试代码
package cn.knightzz.test;
import cn.knightzz.service.AccountService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration("classpath:spring-context.xml")
public class AccountServiceAspectTest {
@Autowired
AccountService accountService;
@Test
public void beforeAspectTest(){
accountService.transfer();
}
}
2.3 AOP切入点配置
2.3.1 切点表达式
-
表达式语法 :
execution([修饰符] 返回值类型 包名.类名.方法名(参数))
-
访问修饰符可以省略
-
返回值类型、包名、类名、方法名可以使用星号 * 代替,代表任意
-
包名与类名之间一个点
.
代表当前包下的类,两个点..
表示当前包及其子包下的类 -
参数列表可以使用两个点
..
表示任意个数,任意类型的参数列表 -
例如 :
execution(public void com.lagou.service.impl.AccountServiceImpl.transfer()) execution(void com.lagou.service.impl.AccountServiceImpl.*(..)) execution(* com.lagou.service.impl.*.*(..)) execution(* com.lagou.service..*.*(..))
2.3.2 切点表达式抽取
-
当多个增强的切点表达式相同时,可以将切点表达式进行抽取,在增强中使用 pointcut-ref 属性代替
pointcut 属性来引用抽取后的切点表达式<aop:config> <!--抽取的切点表达式--> <aop:pointcut id="myPointcut" expression="execution(* com.lagou.service..*.* (..))"></aop:pointcut> <aop:aspect ref="myAdvice"> <aop:before method="before" pointcut-ref="myPointcut"></aop:before> </aop:aspect> </aop:config>
2.3.3 通知类型
- 通知的配置语法 :
<aop:通知类型 method=“通知类中方法名” pointcut=“切点表达式"></aop:通知类型>
![image-20220126151956154](https://i-blog.csdnimg.cn/blog_migrate/05d4637cbaff614e170a26917dcaa640.png)
2.4 基于注解的AOP
2.4.1 步骤分析
-
创建java项目,导入AOP相关坐标
-
创建目标接口和目标实现类(定义切入点)
-
创建通知类(定义通知)
-
将目标类和通知类对象创建权交给spring
-
在通知类中使用注解配置织入关系,升级为切面类
-
在配置文件中开启组件扫描和 AOP 的自动代理
-
编写测试代码
2.4.2 Maven配置
<dependencies>
<!--导入spring的context坐标,context依赖aop-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.15</version>
</dependency>
<!-- aspectj的织入 -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.7</version>
</dependency>
<!--spring整合junit-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.3.15</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
</dependency>
</dependencies>
2.4.3 Spring配置
@EnableAspectJAutoProxy
开启AOP代理
package cn.knightzz.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
/**
* @author 王天赐
* @title: SpringConfig
* @projectName mybatis-apply-06
* @description:
* @website http://knightzz.cn/
* @github https://github.com/knightzz1998
* @date 2022/1/26 15:37
*/
@Configuration
@ComponentScan("cn.knightzz")
@EnableAspectJAutoProxy
public class SpringConfig {
}
2.4.4 Service接口
-
AccountService
package cn.knightzz.service; public interface AccountService { /** * 转账 */ void transfer(); }
-
AccountServiceImpl
package cn.knightzz.service.impl; import cn.knightzz.service.AccountService; import org.springframework.stereotype.Service; @Service public class AccountServiceImpl implements AccountService { @Override public void transfer() { System.out.println("开启转账业务 ... "); } }
2.4.5 创建通知类
- @Aspect 标识切面通知类
package cn.knightzz.advice;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class MyAdvice {
@Before("execution(public void cn.knightzz.service.impl.AccountServiceImpl.transfer())")
public void before(){
System.out.println("开始前置通知 ... ");
}
}
2.4.6 测试代码
package cn.knightzz.test;
import cn.knightzz.config.SpringConfig;
import cn.knightzz.service.AccountService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.beans.factory.annotation.Autowired;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {SpringConfig.class})
public class AccountServiceAspectTest {
@Autowired
AccountService accountService;
@Test
public void beforeAspectTest(){
accountService.transfer();
}
}
2.5 AOP注解配置
2.5.1 切点表达式抽取
package cn.knightzz.advice;
import org.aspectj.lang.annotation.Aspect;
import org.aspectj.lang.annotation.Before;
import org.aspectj.lang.annotation.Pointcut;
import org.springframework.stereotype.Component;
@Component
@Aspect
public class MyAdvice {
@Pointcut("execution(public void cn.knightzz.service.impl.AccountServiceImpl.transfer())")
public void myServicePoint(){}
/**
* 访问修饰符可以省略
* 返回值类型、包名、类名、方法名可以使用星号 * 代替,代表任意
* 两个点 .. 表示当前包及其子包下的类
* 参数列表可以使用两个点 .. 表示任意个数,任意类型的参数列表
*
* 第一个 * 表示 任意类型
* 第二个 * 表示 knightzz 包以及其子包下的所有的类
* 第三个 * 表示 任意方法
*/
@Pointcut("execution(* cn.knightzz..*.*(..))")
public void myPoint(){}
/**
* 切入点抽取
*/
@Before("MyAdvice.myServicePoint()")
public void before(){
System.out.println("开始前置通知 ... ");
}
}
2.5.2 通知类型
![image-20220126191333694](https://i-blog.csdnimg.cn/blog_migrate/5c2c2f0c86f8b273dd4c68c2378bdee4.png)
-
当前四个通知组合在一起时,执行顺序如下:
@Before -> @After -> @AfterReturning(如果有异常:@AfterThrowing) -
环绕通知 使用
ProceedingJoinPoint pjp
对象调用原方法执行业务逻辑, 类似于JDK代理的invoke@Around("execution(* cn.knightzz.service..*.*(..))") public Object around(ProceedingJoinPoint pjp){ // 业务逻辑 pjp.proceed(); }
2.5.3 开启注解配置
- @EnableAspectJAutoProxy 等价于
<aop:aspectj-autoproxy></aop:aspectj-autoproxy>
package cn.knightzz.config;
import org.springframework.context.annotation.ComponentScan;
import org.springframework.context.annotation.Configuration;
import org.springframework.context.annotation.EnableAspectJAutoProxy;
@Configuration
@ComponentScan("cn.knightzz")
@EnableAspectJAutoProxy
public class SpringConfig {
}
2.5.4 总结
- 使用@Aspect注解,标注切面类
- 使用@Before等注解,标注通知方法
- 使用@Pointcut注解,抽取切点表达式
- 配置aop自动代理
<aop:aspectj-autoproxy/>
或@EnableAspectJAutoProxy
2.6 AOP优化转账案例
2.6.1 Maven 配置
<dependencies>
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-context</artifactId>
<version>5.3.15</version>
</dependency>
<!-- aspectj的织入 -->
<dependency>
<groupId>org.aspectj</groupId>
<artifactId>aspectjweaver</artifactId>
<version>1.9.7</version>
</dependency>
<!--此处需要注意的是,spring5 及以上版本要求 junit 的版本必须是 4.12 及以上-->
<dependency>
<groupId>org.springframework</groupId>
<artifactId>spring-test</artifactId>
<version>5.3.15</version>
</dependency>
<dependency>
<groupId>junit</groupId>
<artifactId>junit</artifactId>
<version>4.13.2</version>
</dependency>
<!-- 注解依赖 -->
<dependency>
<groupId>javax.annotation</groupId>
<artifactId>javax.annotation-api</artifactId>
<version>1.3.2</version>
</dependency>
<!-- 数据库连接相关 -->
<dependency>
<groupId>mysql</groupId>
<artifactId>mysql-connector-java</artifactId>
<version>5.1.47</version>
</dependency>
<dependency>
<groupId>com.alibaba</groupId>
<artifactId>druid</artifactId>
<version>1.1.9</version>
</dependency>
<dependency>
<groupId>commons-dbutils</groupId>
<artifactId>commons-dbutils</artifactId>
<version>1.6</version>
</dependency>
</dependencies>
2.6.2 数据库连接配置
-
jdbc.properties
jdbc.driver=com.mysql.jdbc.Driver jdbc.url=jdbc:mysql://localhost:3306/spring_db?useSSL=false jdbc.username=root jdbc.password=123456
-
DataSourceConfig
package cn.knightzz.config; import com.alibaba.druid.pool.DruidDataSource; import org.springframework.beans.factory.annotation.Value; import org.springframework.context.annotation.Bean; import org.springframework.context.annotation.PropertySource; import javax.sql.DataSource; @PropertySource("classpath:jdbc.properties") public class DataSourceConfig { @Value("${jdbc.driver}") private String driver; @Value("${jdbc.url}") private String url; @Value("${jdbc.username}") private String username; @Value("${jdbc.password}") private String password; @Bean("dataSource") public DataSource getDataSource(){ DruidDataSource dataSource = new DruidDataSource(); dataSource.setUsername(username); dataSource.setPassword(password); dataSource.setDriverClassName(driver); dataSource.setUrl(url); return dataSource; } }
2.6.3 Dao层接口
-
AccountDao
package cn.knightzz.dao; public interface AccountDao { /** * 转出操作 * @param outUser * @param money */ public void out(String outUser, Double money); /** * 转入操作 * @param inUser * @param money */ public void in(String inUser, Double money); }
-
AccountDaoImpl
package cn.knightzz.dao.impl; import cn.knightzz.dao.AccountDao; import cn.knightzz.utils.ConnectionUtils; import org.apache.commons.dbutils.QueryRunner; import org.springframework.stereotype.Repository; import javax.annotation.Resource; import java.sql.SQLException; @Repository("accountDao") public class AccountDaoImpl implements AccountDao { @Resource private QueryRunner queryRunner; @Resource private ConnectionUtils connectionUtils; /** * 转出操作 * * @param outUser * @param money */ @Override public void out(String outUser, Double money) { String sql = "update account set money = money - ? where name = ?"; try { // 手动传入数据库连接 queryRunner.update(connectionUtils.getThreadConnection() , sql, money, outUser); } catch (SQLException e) { e.printStackTrace(); } } /** * 转入操作 * * @param inUser * @param money */ @Override public void in(String inUser, Double money) { String sql = "update account set money = money + ? where name = ?"; try { queryRunner.update(connectionUtils.getThreadConnection(),sql, money, inUser); } catch (SQLException e) { e.printStackTrace(); } } }
2.6.4 Service层接口
-
AccountService
package cn.knightzz.service; public interface AccountService { /** * 转账操作 * @param outUser * @param inUser * @param money */ public void transfer(String outUser, String inUser, Double money); }
-
AccountServiceImpl
package cn.knightzz.service.impl; import cn.knightzz.dao.AccountDao; import cn.knightzz.service.AccountService; import cn.knightzz.utils.TransactionManager; import org.springframework.stereotype.Service; import javax.annotation.Resource; @Service public class AccountServiceImpl implements AccountService { @Resource private AccountDao accountDao; @Resource private TransactionManager transactionManager; @Override public void transfer(String outUser, String inUser, Double money) { // 开启事务 accountDao.out(outUser, money); // 模拟异常情况 int i = 1 / 0; accountDao.in(inUser, money); } }
2.6.5 事务管理工具
-
ConnectionUtils
package cn.knightzz.utils; import org.springframework.stereotype.Component; import javax.annotation.Resource; import javax.sql.DataSource; import java.sql.Connection; import java.sql.SQLException; @Component public class ConnectionUtils { /** * 待绑定的线程 */ private ThreadLocal<Connection> threadLocal = new ThreadLocal<>(); /** * 数据库连接源 */ @Resource DataSource dataSource; public Connection getThreadConnection(){ // 1. 先从ThreadLocal获取连接, 如果获取不到再从数据库连接池中获取 Connection connection = threadLocal.get(); // 2. 从连接池中获取数据 if (connection == null) { try { connection = dataSource.getConnection(); threadLocal.set(connection); } catch (SQLException e) { e.printStackTrace(); } } return connection; } /** * 解除绑定 */ public void removeThreadConnection(){ threadLocal.remove(); } }
-
TransactionManager
package cn.knightzz.utils; import org.aspectj.lang.ProceedingJoinPoint; import org.aspectj.lang.annotation.Around; import org.aspectj.lang.annotation.Aspect; import org.springframework.stereotype.Component; import javax.annotation.Resource; import java.sql.SQLException; @Component @Aspect public class TransactionManager { @Resource private ConnectionUtils connectionUtils; @Around("execution(* cn.knightzz.service..*.*(..))") public Object around(ProceedingJoinPoint pjp){ Object result = null; try { // 开启事务 connectionUtils.getThreadConnection().setAutoCommit(false); // 业务逻辑 pjp.proceed(); // 提交事务 connectionUtils.getThreadConnection().commit(); } catch (Throwable e) { e.printStackTrace(); try { // 回滚事务 connectionUtils.getThreadConnection().rollback(); } catch (SQLException ex) { ex.printStackTrace(); } }finally { try { // 设置自动提交 connectionUtils.getThreadConnection().setAutoCommit(true); // 将数据库连接释放到连接池 connectionUtils.getThreadConnection().close(); // 解除线程绑定 connectionUtils.removeThreadConnection(); } catch (SQLException e) { e.printStackTrace(); } } return result; } }
2.6.6 测试代码
package cn.knightzz;
import cn.knightzz.config.SpringConfig;
import cn.knightzz.service.AccountService;
import org.junit.Test;
import org.junit.runner.RunWith;
import org.springframework.test.context.ContextConfiguration;
import org.springframework.test.context.junit4.SpringJUnit4ClassRunner;
import javax.annotation.Resource;
@RunWith(SpringJUnit4ClassRunner.class)
@ContextConfiguration(classes = {SpringConfig.class})
public class App {
@Resource
AccountService accountService;
@Test
public void testTransfer(){
accountService.transfer("tom", "jerry", 10d);
}
}